diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a0cf8c6a..c66a01dc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fill Column: right-click a column header and choose Fill Column to set one value across all loaded rows. The change is staged like a normal edit, so you review it and Save before it applies, and one undo reverts the whole fill. Not available on primary key columns. (#1304) - AWS IAM authentication for PostgreSQL and MySQL connections to RDS and Aurora. Pick AWS IAM in the connection's Authentication field and use an access key, a named AWS profile, or SSO. TablePro generates a fresh login token on every connect and reconnect, so you never paste an expiring token, and SSL is required automatically. (#1291) +### Fixed + +- Installing or updating a plugin right after updating TablePro now refetches the current plugin list first, so it no longer fails against a stale cached list (the error a restart used to clear). (#1380) + ## [0.44.0] - 2026-05-23 ### Added diff --git a/TablePro/Core/Plugins/PluginManager+AutoUpdate.swift b/TablePro/Core/Plugins/PluginManager+AutoUpdate.swift index d89180ef8..c87632a61 100644 --- a/TablePro/Core/Plugins/PluginManager+AutoUpdate.swift +++ b/TablePro/Core/Plugins/PluginManager+AutoUpdate.swift @@ -29,7 +29,7 @@ extension PluginManager { return } - await RegistryClient.shared.fetchManifest() + await RegistryClient.shared.fetchManifest(forceRefresh: true) refreshRegistryUpdateSet() guard let manifest = RegistryClient.shared.manifest else { reconciliationManifestAttempts += 1 @@ -92,6 +92,7 @@ extension PluginManager { let outcome = try await updateFromRegistry( registryPlugin, existingPluginLoaded: false, + refreshManifest: false, progress: { _ in } ) switch outcome { diff --git a/TablePro/Core/Plugins/PluginManager+Install.swift b/TablePro/Core/Plugins/PluginManager+Install.swift index e53172eee..ceed20ad1 100644 --- a/TablePro/Core/Plugins/PluginManager+Install.swift +++ b/TablePro/Core/Plugins/PluginManager+Install.swift @@ -11,7 +11,6 @@ extension PluginManager { _ registryPlugin: RegistryPlugin, progress: @escaping @MainActor @Sendable (Double) -> Void ) async throws -> PluginEntry { - let binary = try validateRegistryCompatibility(registryPlugin) if plugins.contains(where: { $0.id == registryPlugin.id }) { throw PluginError.pluginConflict(existingName: registryPlugin.name) } @@ -23,6 +22,9 @@ extension PluginManager { installsInFlight.insert(registryPlugin.id) defer { installsInFlight.remove(registryPlugin.id) } + let registryPlugin = await RegistryClient.shared.refreshedPlugin(matching: registryPlugin) + let binary = try validateRegistryCompatibility(registryPlugin) + let userPluginsDir = self.userPluginsDir let stateHandler: @Sendable (StagedInstallState) async -> Void = { state in if case .downloading(let fraction) = state { @@ -46,10 +48,9 @@ extension PluginManager { func updateFromRegistry( _ registryPlugin: RegistryPlugin, existingPluginLoaded: Bool = true, + refreshManifest: Bool = true, progress: @escaping @MainActor @Sendable (Double) -> Void ) async throws -> PluginUpdateOutcome { - let binary = try validateRegistryCompatibility(registryPlugin) - if let existing = plugins.first(where: { $0.id == registryPlugin.id }), existing.source == .builtIn { throw PluginError.pluginConflict(existingName: existing.name) @@ -63,6 +64,11 @@ extension PluginManager { installsInFlight.insert(registryPlugin.id) defer { installsInFlight.remove(registryPlugin.id) } + let registryPlugin = refreshManifest + ? await RegistryClient.shared.refreshedPlugin(matching: registryPlugin) + : registryPlugin + let binary = try validateRegistryCompatibility(registryPlugin) + let hasLive = pluginHasLiveConnections(registryPlugin) let userPluginsDir = self.userPluginsDir let stateHandler: @Sendable (StagedInstallState) async -> Void = { state in diff --git a/TablePro/Core/Plugins/Registry/RegistryClient.swift b/TablePro/Core/Plugins/Registry/RegistryClient.swift index e69172072..0969e49a2 100644 --- a/TablePro/Core/Plugins/Registry/RegistryClient.swift +++ b/TablePro/Core/Plugins/Registry/RegistryClient.swift @@ -114,10 +114,7 @@ final class RegistryClient { UserDefaults.standard.set(currentURL, forKey: Self.lastRegistryURLKey) } - var request = URLRequest(url: registryURL) - if !forceRefresh, let etag = cachedETag { - request.setValue(etag, forHTTPHeaderField: "If-None-Match") - } + let request = makeManifestRequest(forceRefresh: forceRefresh) do { let (data, response) = try await session.data(for: request) @@ -174,6 +171,21 @@ final class RegistryClient { } } + func makeManifestRequest(forceRefresh: Bool) -> URLRequest { + var request = URLRequest(url: registryURL) + if forceRefresh { + request.cachePolicy = .reloadIgnoringLocalCacheData + } else if let etag = cachedETag { + request.setValue(etag, forHTTPHeaderField: "If-None-Match") + } + return request + } + + func refreshedPlugin(matching plugin: RegistryPlugin) async -> RegistryPlugin { + await fetchManifest(forceRefresh: true) + return manifest?.plugins.first { $0.id == plugin.id } ?? plugin + } + private func fallbackToCacheOrFail(message: String) { if manifest != nil { fetchState = .loaded diff --git a/TableProTests/Core/Plugins/PluginManagerReconciliationTests.swift b/TableProTests/Core/Plugins/PluginManagerReconciliationTests.swift index ebd582a14..5f3c4b148 100644 --- a/TableProTests/Core/Plugins/PluginManagerReconciliationTests.swift +++ b/TableProTests/Core/Plugins/PluginManagerReconciliationTests.swift @@ -105,4 +105,11 @@ struct PluginManagerReconciliationTests { #expect(!PluginManager.reconciliationShouldRetry(sawTransientFailure: false, retryRemaining: true)) #expect(!PluginManager.reconciliationShouldRetry(sawTransientFailure: false, retryRemaining: false)) } + + @Test("forceRefresh manifest request bypasses the local cache and sends no If-None-Match") + func forceRefreshRequestBypassesCache() { + let request = RegistryClient.shared.makeManifestRequest(forceRefresh: true) + #expect(request.cachePolicy == .reloadIgnoringLocalCacheData) + #expect(request.value(forHTTPHeaderField: "If-None-Match") == nil) + } }