Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion TablePro/Core/Plugins/PluginManager+AutoUpdate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -92,6 +92,7 @@ extension PluginManager {
let outcome = try await updateFromRegistry(
registryPlugin,
existingPluginLoaded: false,
refreshManifest: false,
progress: { _ in }
)
switch outcome {
Expand Down
12 changes: 9 additions & 3 deletions TablePro/Core/Plugins/PluginManager+Install.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -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 {
Expand All @@ -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)
Expand All @@ -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
Expand Down
20 changes: 16 additions & 4 deletions TablePro/Core/Plugins/Registry/RegistryClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Loading