From 68a1fa3fb56d67b78480af0c82f890aff6b55132 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Sun, 10 May 2026 00:46:11 +0700 Subject: [PATCH] refactor(events): scope sqlFavoritesDidUpdate by connection id --- CHANGELOG.md | 1 + TablePro/Core/Events/AppEvents.swift | 7 ++++++- TablePro/Core/Storage/SQLFavoriteManager.swift | 18 +++++++++--------- TablePro/ViewModels/ConnectionDataCache.swift | 6 +++++- TablePro/Views/Editor/SQLEditorView.swift | 5 ++++- 5 files changed, 25 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 615bc60a2..f85b08788 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Internal: thread `KeychainHelper` through `ConnectionStorage`, `SSHProfileStorage`, `AIKeyStorage`, and `LicenseStorage` via init (default `.shared`). The 24 raw `KeychainHelper.shared` reads inside those classes are gone, matching the existing injected-dependency pattern (`syncTracker`, `appSettings`). - Internal: extend `AppServices` with `queryHistoryManager`, `dateFormattingService`, `copilotService`, and `mcpServerManager`. `AppSettingsManager` now takes its eight cross-singleton dependencies (`AppSettingsStorage`, `ThemeEngine`, `SyncChangeTracker`, `AppEvents`, `DateFormattingService`, `QueryHistoryManager`, `MCPServerManager`, `CopilotService`) via init with `.shared` defaults. The 32 raw `.shared` reads inside `AppSettingsManager` are gone, including the four `Task { ... }` capture closures that previously dialed `CopilotService.shared` / `MCPServerManager.shared` mid-`didSet`. The 49 caller-side `AppSettingsManager.shared` reads are unchanged in this PR; they will be threaded as their owners adopt `services` in subsequent waves. - Internal: `AppEvents.connectionUpdated` payload changes from `Void` to `UUID?`. Single-connection senders (`ConnectionFormCoordinator`, sample-DB launcher, per-connection iCloud-sync toggle) now pass the affected id; bulk senders (sync pull, multi-import, multi-select sync toggle) pass `nil`. The current `WelcomeViewModel` subscriber refreshes on every event regardless, but per-connection subscribers added in the future can filter by id without re-shaping the bus. +- Internal: `AppEvents.sqlFavoritesDidUpdate` payload changes from `Void` to `UUID?` and the per-connection subscribers (`ConnectionDataCache`, `SQLEditorView`) now filter on it. Previously, editing a favorite for connection A in one window forced `ConnectionDataCache` for every other open connection to refetch its favorite list and `SQLEditorView` for every other window to refresh its keyword map. The senders pass `favorite.connectionId` / `folder.connectionId` for adds and updates, and `nil` for deletes (where the affected connection isn't easily recoverable post-delete). `nil` payloads still trigger a refresh on every subscriber, preserving correctness for cross-connection favorites and bulk deletes. - Internal: extend `AppServices` with `tagStorage`, `sshProfileStorage`, `licenseManager`, `conflictResolver`, and `syncMetadataStorage`. `SyncCoordinator` now takes `services: AppServices` in init (default `.live`); 34 raw `.shared` reads of those types inside `SyncCoordinator` are routed through `services.*`. - Internal: Redis sidebar key tree uses SwiftUI `OutlineGroup` instead of recursive `DisclosureGroup` + `ForEach` wrapped in `AnyView`. Expansion state is now managed natively per branch identifier; the explicit `expandedPrefixes` set is gone. - Result-grid cells render via direct `draw(_:)` on a layer-backed `NSView` instead of an `NSTableCellView` wrapping an `NSTextField` plus an `NSButton` accessory. Per cell during scroll there is no Auto Layout solving, no `NSTextField` re-layout, and no `NSButton` tracking-area work. Editing for plain-text columns now opens the overlay editor (the same surface previously used for multi-line cells) rather than an inline text field. diff --git a/TablePro/Core/Events/AppEvents.swift b/TablePro/Core/Events/AppEvents.swift index d3103798a..a6eb6797b 100644 --- a/TablePro/Core/Events/AppEvents.swift +++ b/TablePro/Core/Events/AppEvents.swift @@ -48,7 +48,12 @@ final class AppEvents { let queryHistoryDidUpdate = PassthroughSubject() - let sqlFavoritesDidUpdate = PassthroughSubject() + /// SQL favorites or favorite folders changed. + /// Payload is the affected connection's id, or `nil` for cross-connection + /// favorites (`favorite.connectionId == nil`) and bulk operations + /// (multi-favorite delete) where the sender doesn't track a single id. + /// Per-connection subscribers should refresh on `payload == nil || payload == self.connectionId`. + let sqlFavoritesDidUpdate = PassthroughSubject() let linkedFoldersDidUpdate = PassthroughSubject() diff --git a/TablePro/Core/Storage/SQLFavoriteManager.swift b/TablePro/Core/Storage/SQLFavoriteManager.swift index 06314331f..edc34e44a 100644 --- a/TablePro/Core/Storage/SQLFavoriteManager.swift +++ b/TablePro/Core/Storage/SQLFavoriteManager.swift @@ -23,7 +23,7 @@ internal final class SQLFavoriteManager: @unchecked Sendable { func addFavorite(_ favorite: SQLFavorite) async -> Bool { let result = await storage.addFavorite(favorite) if result { - postUpdateNotification() + postUpdateNotification(connectionId: favorite.connectionId) } return result } @@ -31,7 +31,7 @@ internal final class SQLFavoriteManager: @unchecked Sendable { func updateFavorite(_ favorite: SQLFavorite) async -> Bool { let result = await storage.updateFavorite(favorite) if result { - postUpdateNotification() + postUpdateNotification(connectionId: favorite.connectionId) } return result } @@ -39,7 +39,7 @@ internal final class SQLFavoriteManager: @unchecked Sendable { func deleteFavorite(id: UUID) async -> Bool { let result = await storage.deleteFavorite(id: id) if result { - postUpdateNotification() + postUpdateNotification(connectionId: nil) } return result } @@ -47,7 +47,7 @@ internal final class SQLFavoriteManager: @unchecked Sendable { func deleteFavorites(ids: [UUID]) async { let result = await storage.deleteFavorites(ids: ids) if result { - postUpdateNotification() + postUpdateNotification(connectionId: nil) } } @@ -68,7 +68,7 @@ internal final class SQLFavoriteManager: @unchecked Sendable { func addFolder(_ folder: SQLFavoriteFolder) async -> Bool { let result = await storage.addFolder(folder) if result { - postUpdateNotification() + postUpdateNotification(connectionId: folder.connectionId) } return result } @@ -76,7 +76,7 @@ internal final class SQLFavoriteManager: @unchecked Sendable { func updateFolder(_ folder: SQLFavoriteFolder) async -> Bool { let result = await storage.updateFolder(folder) if result { - postUpdateNotification() + postUpdateNotification(connectionId: folder.connectionId) } return result } @@ -84,7 +84,7 @@ internal final class SQLFavoriteManager: @unchecked Sendable { func deleteFolder(id: UUID) async -> Bool { let result = await storage.deleteFolder(id: id) if result { - postUpdateNotification() + postUpdateNotification(connectionId: nil) } return result } @@ -150,9 +150,9 @@ internal final class SQLFavoriteManager: @unchecked Sendable { // MARK: - Notifications - private func postUpdateNotification() { + private func postUpdateNotification(connectionId: UUID?) { Task { @MainActor in - AppEvents.shared.sqlFavoritesDidUpdate.send(()) + AppEvents.shared.sqlFavoritesDidUpdate.send(connectionId) } } } diff --git a/TablePro/ViewModels/ConnectionDataCache.swift b/TablePro/ViewModels/ConnectionDataCache.swift index 631da242b..e2393e9e3 100644 --- a/TablePro/ViewModels/ConnectionDataCache.swift +++ b/TablePro/ViewModels/ConnectionDataCache.swift @@ -39,7 +39,11 @@ internal final class ConnectionDataCache { AppEvents.shared.sqlFavoritesDidUpdate .receive(on: RunLoop.main) - .sink { [weak self] _ in self?.scheduleRefresh() } + .sink { [weak self] payload in + guard let self else { return } + guard payload == nil || payload == self.connectionId else { return } + self.scheduleRefresh() + } .store(in: &cancellables) AppEvents.shared.linkedSQLFoldersDidUpdate diff --git a/TablePro/Views/Editor/SQLEditorView.swift b/TablePro/Views/Editor/SQLEditorView.swift index ddb9859c0..f164cafaa 100644 --- a/TablePro/Views/Editor/SQLEditorView.swift +++ b/TablePro/Views/Editor/SQLEditorView.swift @@ -162,7 +162,10 @@ struct SQLEditorView: View { } AppEvents.shared.sqlFavoritesDidUpdate .receive(on: RunLoop.main) - .sink { _ in refresh() } + .sink { payload in + guard payload == nil || payload == connectionId else { return } + refresh() + } .store(in: &favoritesCancellables) AppEvents.shared.linkedSQLFoldersDidUpdate .receive(on: RunLoop.main)