From c91ad2aa5b5f19f2a1749b870d1e83e162b6bf0f Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Tue, 2 Jun 2026 18:05:38 +0700 Subject: [PATCH] refactor(plugins): consolidate connect-time driver readiness and tidy binary selection --- TablePro/Core/Database/DatabaseDriver.swift | 22 +---------------- .../Plugins/PluginManager+AutoUpdate.swift | 22 +++++++++++++++++ .../Plugins/Registry/RegistryModels.swift | 16 +++++++------ .../PluginManagerReconciliationTests.swift | 24 +++++++++---------- scripts/check-registry-readiness.py | 6 ++++- 5 files changed, 49 insertions(+), 41 deletions(-) diff --git a/TablePro/Core/Database/DatabaseDriver.swift b/TablePro/Core/Database/DatabaseDriver.swift index e2a051875..3d1cbcc19 100644 --- a/TablePro/Core/Database/DatabaseDriver.swift +++ b/TablePro/Core/Database/DatabaseDriver.swift @@ -400,27 +400,7 @@ enum DatabaseDriverFactory { passwordOverride: String? = nil, awaitPlugins: Bool ) async throws -> DatabaseDriver { - let pluginId = connection.type.pluginTypeId - if PluginManager.shared.driverPlugin(for: connection.type) == nil, - !PluginManager.shared.hasFinishedInitialLoad { - logger.info("Plugin '\(pluginId)' not loaded yet, waiting for background load") - await PluginManager.shared.waitForInitialLoad() - } - if PluginManager.shared.driverPlugin(for: connection.type) == nil, - PluginManager.shared.hasOutdatedRejectedPlugin(forTypeId: pluginId) { - logger.info("Plugin '\(pluginId)' is installed but outdated, updating it before connect") - await PluginManager.shared.ensurePluginReady(forTypeId: pluginId) - } - if PluginManager.shared.driverPlugin(for: connection.type) == nil, - connection.type.isDownloadablePlugin, - !PluginManager.shared.hasOutdatedRejectedPlugin(forTypeId: pluginId) { - logger.info("Plugin '\(pluginId)' not installed, installing on demand before connect") - do { - try await PluginManager.shared.installMissingPlugin(for: connection.type) { _ in } - } catch { - logger.warning("On-demand install for '\(pluginId)' did not complete: \(error.localizedDescription)") - } - } + await PluginManager.shared.prepareForConnecting(to: connection.type) return try await createDriverFromPlugin(for: connection, passwordOverride: passwordOverride) } diff --git a/TablePro/Core/Plugins/PluginManager+AutoUpdate.swift b/TablePro/Core/Plugins/PluginManager+AutoUpdate.swift index 50a9dec9b..db12235e7 100644 --- a/TablePro/Core/Plugins/PluginManager+AutoUpdate.swift +++ b/TablePro/Core/Plugins/PluginManager+AutoUpdate.swift @@ -276,6 +276,26 @@ extension PluginManager { rejectedPlugins.first { $0.isOutdated && $0.providedDatabaseTypeIds.contains(typeId) }?.reason } + func prepareForConnecting(to type: DatabaseType) async { + let typeId = type.pluginTypeId + if driverPlugin(for: type) == nil, !hasFinishedInitialLoad { + Self.logger.info("Plugin '\(typeId)' not loaded yet, waiting for background load") + await waitForInitialLoad() + } + if driverPlugin(for: type) == nil, hasOutdatedRejectedPlugin(forTypeId: typeId) { + Self.logger.info("Plugin '\(typeId)' is installed but outdated, updating it before connect") + await ensurePluginReady(forTypeId: typeId) + } + if driverPlugin(for: type) == nil, type.isDownloadablePlugin, !hasOutdatedRejectedPlugin(forTypeId: typeId) { + Self.logger.info("Plugin '\(typeId)' not installed, installing on demand before connect") + do { + try await installMissingPlugin(for: type) { _ in } + } catch { + Self.logger.warning("On-demand install for '\(typeId)' did not complete: \(error.localizedDescription)") + } + } + } + func ensurePluginReady(forTypeId typeId: String) async { if reconciliationActive, let task = reconciliationTask { await task.value @@ -287,6 +307,8 @@ extension PluginManager { private func reconcileOutdated(matchingTypeId typeId: String) async { let targets = rejectedPlugins.filter { $0.isOutdated && $0.providedDatabaseTypeIds.contains(typeId) } guard !targets.isEmpty else { return } + reconciliationActive = true + defer { reconciliationActive = false } await RegistryClient.shared.fetchManifest(forceRefresh: true) guard let manifest = RegistryClient.shared.manifest else { return } for target in targets { diff --git a/TablePro/Core/Plugins/Registry/RegistryModels.swift b/TablePro/Core/Plugins/Registry/RegistryModels.swift index 35f516106..101a2b622 100644 --- a/TablePro/Core/Plugins/Registry/RegistryModels.swift +++ b/TablePro/Core/Plugins/Registry/RegistryModels.swift @@ -108,16 +108,18 @@ extension RegistryPlugin { currentKitVersion: Int, minimumKitVersion: Int ) throws -> RegistryBinary { - let compatible = binaries + let highestInRange = binaries .filter { $0.architecture == arch } - .filter { binary in - guard let kit = binary.pluginKitVersion else { return false } - return kit >= minimumKitVersion && kit <= currentKitVersion + .compactMap { binary -> (binary: RegistryBinary, kit: Int)? in + guard let kit = binary.pluginKitVersion, kit >= minimumKitVersion, kit <= currentKitVersion else { + return nil + } + return (binary, kit) } - .max { ($0.pluginKitVersion ?? 0) < ($1.pluginKitVersion ?? 0) } + .max { $0.kit < $1.kit } - if let compatible { - return compatible + if let highestInRange { + return highestInRange.binary } throw PluginError.noCompatibleBinary diff --git a/TableProTests/Core/Plugins/PluginManagerReconciliationTests.swift b/TableProTests/Core/Plugins/PluginManagerReconciliationTests.swift index 057a2b915..467198c06 100644 --- a/TableProTests/Core/Plugins/PluginManagerReconciliationTests.swift +++ b/TableProTests/Core/Plugins/PluginManagerReconciliationTests.swift @@ -49,7 +49,7 @@ struct PluginManagerReconciliationTests { ) } - private func makeRegistryPlugin(id: String = "com.example.driver", kitVersions: [Int]) -> RegistryPlugin { + private func makeRegistryPlugin(id: String = "com.example.driver", kitVersions: [Int]) throws -> RegistryPlugin { let arch = PluginArchitecture.current.rawValue let binaries = kitVersions .map { "{\"architecture\": \"\(arch)\", \"downloadURL\": \"https://x\", \"sha256\": \"deadbeef\", \"pluginKitVersion\": \($0)}" } @@ -65,7 +65,7 @@ struct PluginManagerReconciliationTests { "binaries": [\(binaries)] } """ - return try! JSONDecoder().decode(RegistryPlugin.self, from: Data(json.utf8)) + return try JSONDecoder().decode(RegistryPlugin.self, from: Data(json.utf8)) } private func kind(_ action: RejectedPluginAction) -> String { @@ -78,8 +78,8 @@ struct PluginManagerReconciliationTests { } @Test("rejectedAction awaits while the manifest is still loading") - func rejectedActionAwaitsWithoutManifest() { - let plugin = makeRegistryPlugin(kitVersions: [18]) + func rejectedActionAwaitsWithoutManifest() throws { + let plugin = try makeRegistryPlugin(kitVersions: [18]) let action = PluginManager.rejectedAction( registryPlugin: plugin, manifestLoaded: false, currentKitVersion: 18, minimumKitVersion: 18 ) @@ -95,8 +95,8 @@ struct PluginManagerReconciliationTests { } @Test("rejectedAction offers an update when a current-kit binary exists") - func rejectedActionUpdateAvailable() { - let plugin = makeRegistryPlugin(kitVersions: [17, 18]) + func rejectedActionUpdateAvailable() throws { + let plugin = try makeRegistryPlugin(kitVersions: [17, 18]) let action = PluginManager.rejectedAction( registryPlugin: plugin, manifestLoaded: true, currentKitVersion: 18, minimumKitVersion: 18 ) @@ -104,8 +104,8 @@ struct PluginManagerReconciliationTests { } @Test("rejectedAction offers an update for a resilient older-kit binary under a newer app") - func rejectedActionUpdateAvailableForwardCompat() { - let plugin = makeRegistryPlugin(kitVersions: [18]) + func rejectedActionUpdateAvailableForwardCompat() throws { + let plugin = try makeRegistryPlugin(kitVersions: [18]) let action = PluginManager.rejectedAction( registryPlugin: plugin, manifestLoaded: true, currentKitVersion: 19, minimumKitVersion: 18 ) @@ -113,8 +113,8 @@ struct PluginManagerReconciliationTests { } @Test("rejectedAction asks for an app update when only a newer-kit binary exists") - func rejectedActionRequiresAppUpdate() { - let plugin = makeRegistryPlugin(kitVersions: [18, 19]) + func rejectedActionRequiresAppUpdate() throws { + let plugin = try makeRegistryPlugin(kitVersions: [18, 19]) let action = PluginManager.rejectedAction( registryPlugin: plugin, manifestLoaded: true, currentKitVersion: 17, minimumKitVersion: 17 ) @@ -122,8 +122,8 @@ struct PluginManagerReconciliationTests { } @Test("rejectedAction awaits when only pre-floor binaries are published") - func rejectedActionAwaitsForOlderKits() { - let plugin = makeRegistryPlugin(kitVersions: [16, 17]) + func rejectedActionAwaitsForOlderKits() throws { + let plugin = try makeRegistryPlugin(kitVersions: [16, 17]) let action = PluginManager.rejectedAction( registryPlugin: plugin, manifestLoaded: true, currentKitVersion: 18, minimumKitVersion: 18 ) diff --git a/scripts/check-registry-readiness.py b/scripts/check-registry-readiness.py index 0f19f787a..934f5dbb5 100644 --- a/scripts/check-registry-readiness.py +++ b/scripts/check-registry-readiness.py @@ -6,6 +6,10 @@ plugin binaries. With range-aware binary selection an additive PluginKit bump needs no re-publish (an older resilient binary still serves), so this only fails after a breaking bump that raised the floor and left the registry behind. + +The gate reads the raw GitHub origin, not the jsDelivr CDN that clients use, so +it sees the manifest the moment the registry push lands rather than waiting out +(or racing) the CDN edge cache. """ import argparse @@ -13,7 +17,7 @@ import sys import urllib.request -DEFAULT_MANIFEST_URL = "https://cdn.jsdelivr.net/gh/TableProApp/plugins@main/plugins.json" +DEFAULT_MANIFEST_URL = "https://raw.githubusercontent.com/TableProApp/plugins/main/plugins.json" def fetch_manifest(url, retries=4):