Skip to content

Commit 115eafa

Browse files
authored
fix(plugins): refetch the registry manifest before install/update (#1380) (#1402)
* fix(plugins): refetch the registry manifest before install and update so it never resolves against a stale cached list (#1380) * refactor(plugins): refresh the manifest once per reconciliation pass and claim in-flight before the network fetch (#1380)
1 parent 7ce9287 commit 115eafa

5 files changed

Lines changed: 38 additions & 8 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212
- 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)
1313
- 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)
1414

15+
### Fixed
16+
17+
- 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)
18+
1519
## [0.44.0] - 2026-05-23
1620

1721
### Added

TablePro/Core/Plugins/PluginManager+AutoUpdate.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ extension PluginManager {
2929
return
3030
}
3131

32-
await RegistryClient.shared.fetchManifest()
32+
await RegistryClient.shared.fetchManifest(forceRefresh: true)
3333
refreshRegistryUpdateSet()
3434
guard let manifest = RegistryClient.shared.manifest else {
3535
reconciliationManifestAttempts += 1
@@ -92,6 +92,7 @@ extension PluginManager {
9292
let outcome = try await updateFromRegistry(
9393
registryPlugin,
9494
existingPluginLoaded: false,
95+
refreshManifest: false,
9596
progress: { _ in }
9697
)
9798
switch outcome {

TablePro/Core/Plugins/PluginManager+Install.swift

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ extension PluginManager {
1111
_ registryPlugin: RegistryPlugin,
1212
progress: @escaping @MainActor @Sendable (Double) -> Void
1313
) async throws -> PluginEntry {
14-
let binary = try validateRegistryCompatibility(registryPlugin)
1514
if plugins.contains(where: { $0.id == registryPlugin.id }) {
1615
throw PluginError.pluginConflict(existingName: registryPlugin.name)
1716
}
@@ -23,6 +22,9 @@ extension PluginManager {
2322
installsInFlight.insert(registryPlugin.id)
2423
defer { installsInFlight.remove(registryPlugin.id) }
2524

25+
let registryPlugin = await RegistryClient.shared.refreshedPlugin(matching: registryPlugin)
26+
let binary = try validateRegistryCompatibility(registryPlugin)
27+
2628
let userPluginsDir = self.userPluginsDir
2729
let stateHandler: @Sendable (StagedInstallState) async -> Void = { state in
2830
if case .downloading(let fraction) = state {
@@ -46,10 +48,9 @@ extension PluginManager {
4648
func updateFromRegistry(
4749
_ registryPlugin: RegistryPlugin,
4850
existingPluginLoaded: Bool = true,
51+
refreshManifest: Bool = true,
4952
progress: @escaping @MainActor @Sendable (Double) -> Void
5053
) async throws -> PluginUpdateOutcome {
51-
let binary = try validateRegistryCompatibility(registryPlugin)
52-
5354
if let existing = plugins.first(where: { $0.id == registryPlugin.id }),
5455
existing.source == .builtIn {
5556
throw PluginError.pluginConflict(existingName: existing.name)
@@ -63,6 +64,11 @@ extension PluginManager {
6364
installsInFlight.insert(registryPlugin.id)
6465
defer { installsInFlight.remove(registryPlugin.id) }
6566

67+
let registryPlugin = refreshManifest
68+
? await RegistryClient.shared.refreshedPlugin(matching: registryPlugin)
69+
: registryPlugin
70+
let binary = try validateRegistryCompatibility(registryPlugin)
71+
6672
let hasLive = pluginHasLiveConnections(registryPlugin)
6773
let userPluginsDir = self.userPluginsDir
6874
let stateHandler: @Sendable (StagedInstallState) async -> Void = { state in

TablePro/Core/Plugins/Registry/RegistryClient.swift

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -114,10 +114,7 @@ final class RegistryClient {
114114
UserDefaults.standard.set(currentURL, forKey: Self.lastRegistryURLKey)
115115
}
116116

117-
var request = URLRequest(url: registryURL)
118-
if !forceRefresh, let etag = cachedETag {
119-
request.setValue(etag, forHTTPHeaderField: "If-None-Match")
120-
}
117+
let request = makeManifestRequest(forceRefresh: forceRefresh)
121118

122119
do {
123120
let (data, response) = try await session.data(for: request)
@@ -174,6 +171,21 @@ final class RegistryClient {
174171
}
175172
}
176173

174+
func makeManifestRequest(forceRefresh: Bool) -> URLRequest {
175+
var request = URLRequest(url: registryURL)
176+
if forceRefresh {
177+
request.cachePolicy = .reloadIgnoringLocalCacheData
178+
} else if let etag = cachedETag {
179+
request.setValue(etag, forHTTPHeaderField: "If-None-Match")
180+
}
181+
return request
182+
}
183+
184+
func refreshedPlugin(matching plugin: RegistryPlugin) async -> RegistryPlugin {
185+
await fetchManifest(forceRefresh: true)
186+
return manifest?.plugins.first { $0.id == plugin.id } ?? plugin
187+
}
188+
177189
private func fallbackToCacheOrFail(message: String) {
178190
if manifest != nil {
179191
fetchState = .loaded

TableProTests/Core/Plugins/PluginManagerReconciliationTests.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,4 +105,11 @@ struct PluginManagerReconciliationTests {
105105
#expect(!PluginManager.reconciliationShouldRetry(sawTransientFailure: false, retryRemaining: true))
106106
#expect(!PluginManager.reconciliationShouldRetry(sawTransientFailure: false, retryRemaining: false))
107107
}
108+
109+
@Test("forceRefresh manifest request bypasses the local cache and sends no If-None-Match")
110+
func forceRefreshRequestBypassesCache() {
111+
let request = RegistryClient.shared.makeManifestRequest(forceRefresh: true)
112+
#expect(request.cachePolicy == .reloadIgnoringLocalCacheData)
113+
#expect(request.value(forHTTPHeaderField: "If-None-Match") == nil)
114+
}
108115
}

0 commit comments

Comments
 (0)