From 49bf36a1bb73185cda822077c0ca65888b09ff0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ng=C3=B4=20Qu=E1=BB=91c=20=C4=90=E1=BA=A1t?= Date: Mon, 6 Apr 2026 16:19:21 +0700 Subject: [PATCH] refactor: replace 6 timing hacks with structured signals - Remove redundant configureWelcomeWindow() polling (windowDidBecomeKey handles it) - Replace Phase 2 query delays with Task(priority: .background) - Replace 16ms tab-switch sleep with Task.yield() - Replace connection form window polling with NSWindow.didBecomeKeyNotification - Replace 300ms notification delay with pending URL pattern - Replace WindowOpener 5s polling with continuation-based readiness signal --- CHANGELOG.md | 1 + TablePro/AppDelegate+ConnectionHandler.swift | 16 +++++++--- TablePro/AppDelegate+FileOpen.swift | 6 ++-- TablePro/AppDelegate+WindowConfig.swift | 16 ---------- TablePro/AppDelegate.swift | 5 +-- .../Infrastructure/WindowOpener.swift | 23 ++++++++++++- TablePro/ViewModels/WelcomeViewModel.swift | 32 ++++++++++++++----- .../MainContentCoordinator+QueryHelpers.swift | 8 ++--- TablePro/Views/Main/MainContentView.swift | 3 +- 9 files changed, 68 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e9994cd94..f7e8d265e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Use sheet presentation for all file open/save panels instead of free-floating dialogs - Replace event monitor with native SwiftUI .onKeyPress() in connection switcher - Extract reusable SearchFieldView component from 4 custom search field implementations +- Replace timing hacks with structured signals (polling loops, arbitrary delays) ### Changed diff --git a/TablePro/AppDelegate+ConnectionHandler.swift b/TablePro/AppDelegate+ConnectionHandler.swift index ad0af0051..c779d898e 100644 --- a/TablePro/AppDelegate+ConnectionHandler.swift +++ b/TablePro/AppDelegate+ConnectionHandler.swift @@ -264,10 +264,18 @@ extension AppDelegate { Task { @MainActor [weak self] in defer { self?.isProcessingQueuedURLs = false } - var ready = false - for _ in 0..<25 { - if WindowOpener.shared.openWindow != nil { ready = true; break } - try? await Task.sleep(for: .milliseconds(200)) + let ready = await withTaskGroup(of: Bool.self) { group in + group.addTask { + await WindowOpener.shared.waitUntilReady() + return true + } + group.addTask { + try? await Task.sleep(for: .seconds(5)) + return false + } + let result = await group.next() ?? false + group.cancelAll() + return result } guard let self else { return } if !ready { diff --git a/TablePro/AppDelegate+FileOpen.swift b/TablePro/AppDelegate+FileOpen.swift index e56ea4765..acce0da1d 100644 --- a/TablePro/AppDelegate+FileOpen.swift +++ b/TablePro/AppDelegate+FileOpen.swift @@ -244,10 +244,8 @@ extension AppDelegate { private func handleConnectionShareFile(_ url: URL) { openWelcomeWindow() - // Delay to ensure WelcomeWindowView's .onReceive is registered after window renders - DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { - NotificationCenter.default.post(name: .connectionShareFileOpened, object: url) - } + pendingConnectionShareURL = url + NotificationCenter.default.post(name: .connectionShareFileOpened, object: url) } // MARK: - Plugin Install diff --git a/TablePro/AppDelegate+WindowConfig.swift b/TablePro/AppDelegate+WindowConfig.swift index e1da705df..9874246c2 100644 --- a/TablePro/AppDelegate+WindowConfig.swift +++ b/TablePro/AppDelegate+WindowConfig.swift @@ -151,22 +151,6 @@ extension AppDelegate { NotificationCenter.default.post(name: .openWelcomeWindow, object: nil) } - func configureWelcomeWindow() { - Task { @MainActor [weak self] in - for _ in 0 ..< 5 { - guard let self else { return } - let found = NSApp.windows.contains(where: { self.isWelcomeWindow($0) }) - if found { - for window in NSApp.windows where self.isWelcomeWindow(window) { - self.configureWelcomeWindowStyle(window) - } - return - } - try? await Task.sleep(for: .milliseconds(50)) - } - } - } - private func configureWelcomeWindowStyle(_ window: NSWindow) { window.standardWindowButton(.miniaturizeButton)?.isHidden = true window.standardWindowButton(.zoomButton)?.isHidden = true diff --git a/TablePro/AppDelegate.swift b/TablePro/AppDelegate.swift index db256904d..54906c841 100644 --- a/TablePro/AppDelegate.swift +++ b/TablePro/AppDelegate.swift @@ -59,6 +59,9 @@ class AppDelegate: NSObject, NSApplicationDelegate { /// Prevents duplicate connections when the same file is opened twice rapidly. var connectingFilePaths = Set() + /// Connection share file URL pending consumption by WelcomeViewModel.setUp() + var pendingConnectionShareURL: URL? + // MARK: - NSApplicationDelegate func application(_ application: NSApplication, open urls: [URL]) { @@ -105,8 +108,6 @@ class AppDelegate: NSObject, NSApplicationDelegate { _ = QueryHistoryStorage.shared } - configureWelcomeWindow() - let settings = AppSettingsStorage.shared.loadGeneral() if settings.startupBehavior == .reopenLast, let lastConnectionId = AppSettingsStorage.shared.loadLastConnectionId() { diff --git a/TablePro/Core/Services/Infrastructure/WindowOpener.swift b/TablePro/Core/Services/Infrastructure/WindowOpener.swift index d85bfafd4..35fc553f9 100644 --- a/TablePro/Core/Services/Infrastructure/WindowOpener.swift +++ b/TablePro/Core/Services/Infrastructure/WindowOpener.swift @@ -15,9 +15,30 @@ internal final class WindowOpener { internal static let shared = WindowOpener() + private var readyContinuation: CheckedContinuation? + /// Set on appear by ContentView, WelcomeViewModel, or ConnectionFormView. /// Safe to store — OpenWindowAction is app-scoped, not view-scoped. - internal var openWindow: OpenWindowAction? + internal var openWindow: OpenWindowAction? { + didSet { + if openWindow != nil { + readyContinuation?.resume() + readyContinuation = nil + } + } + } + + /// Suspends until openWindow is set. Returns immediately if already available. + internal func waitUntilReady() async { + if openWindow != nil { return } + await withCheckedContinuation { continuation in + if openWindow != nil { + continuation.resume() + } else { + readyContinuation = continuation + } + } + } /// Ordered queue of pending payloads — windows requested via openNativeTab /// but not yet acknowledged by MainContentView.configureWindow. diff --git a/TablePro/ViewModels/WelcomeViewModel.swift b/TablePro/ViewModels/WelcomeViewModel.swift index 198952d55..8c1a8110b 100644 --- a/TablePro/ViewModels/WelcomeViewModel.swift +++ b/TablePro/ViewModels/WelcomeViewModel.swift @@ -181,6 +181,12 @@ final class WelcomeViewModel { loadConnections() linkedConnections = LinkedFolderWatcher.shared.linkedConnections + + if let appDelegate = NSApp.delegate as? AppDelegate, + let pendingURL = appDelegate.pendingConnectionShareURL { + appDelegate.pendingConnectionShareURL = nil + activeSheet = .importFile(pendingURL) + } } deinit { @@ -504,15 +510,25 @@ final class WelcomeViewModel { } func focusConnectionFormWindow() { + if let window = NSApp.windows.first(where: { $0.identifier?.rawValue == "connection-form" }) { + window.makeKeyAndOrderFront(nil) + return + } + + var observer: NSObjectProtocol? + observer = NotificationCenter.default.addObserver( + forName: NSWindow.didBecomeKeyNotification, + object: nil, + queue: .main + ) { notification in + guard let window = notification.object as? NSWindow, + window.identifier?.rawValue == "connection-form" else { return } + if let observer { NotificationCenter.default.removeObserver(observer) } + } + Task { @MainActor in - for _ in 0..<10 { - for window in NSApp.windows where - window.identifier?.rawValue == "connection-form" { - window.makeKeyAndOrderFront(nil) - return - } - try? await Task.sleep(for: .milliseconds(20)) - } + try? await Task.sleep(for: .milliseconds(500)) + if let observer { NotificationCenter.default.removeObserver(observer) } } } diff --git a/TablePro/Views/Main/Extensions/MainContentCoordinator+QueryHelpers.swift b/TablePro/Views/Main/Extensions/MainContentCoordinator+QueryHelpers.swift index fb9f2c96a..9160e17fb 100644 --- a/TablePro/Views/Main/Extensions/MainContentCoordinator+QueryHelpers.swift +++ b/TablePro/Views/Main/Extensions/MainContentCoordinator+QueryHelpers.swift @@ -250,11 +250,10 @@ extension MainContentCoordinator { ) { let isNonSQL = PluginManager.shared.editorLanguage(for: connectionType) != .sql - // Phase 2a: Exact row count + // Phase 2a: Exact row count (background priority to let Phase 1 render first) // Redis/non-SQL drivers don't support SELECT COUNT(*); use approximate count instead. - Task { [weak self] in + Task(priority: .background) { [weak self] in guard let self else { return } - try? await Task.sleep(nanoseconds: 200_000_000) guard !self.isTearingDown else { return } guard let mainDriver = DatabaseManager.shared.driver(for: connectionId) else { return } @@ -289,9 +288,8 @@ extension MainContentCoordinator { // Phase 2b: Fetch enum/set values (not applicable for non-SQL databases) guard !isNonSQL else { return } guard let enumDriver = DatabaseManager.shared.driver(for: connectionId) else { return } - Task { [weak self] in + Task(priority: .background) { [weak self] in guard let self else { return } - try? await Task.sleep(nanoseconds: 200_000_000) guard !self.isTearingDown else { return } // Use schema if available, otherwise fetch column info for enum parsing diff --git a/TablePro/Views/Main/MainContentView.swift b/TablePro/Views/Main/MainContentView.swift index 4c9de15ce..b5c94519c 100644 --- a/TablePro/Views/Main/MainContentView.swift +++ b/TablePro/Views/Main/MainContentView.swift @@ -277,8 +277,7 @@ struct MainContentView: View { .onChange(of: tabManager.selectedTabId) { _, newTabId in pendingTabSwitch?.cancel() pendingTabSwitch = Task { @MainActor in - // Let other onChange handlers (tabs, resultColumns) settle first - try? await Task.sleep(for: .milliseconds(16)) + await Task.yield() guard !Task.isCancelled else { return } handleTabSelectionChange(from: previousSelectedTabId, to: newTabId) previousSelectedTabId = newTabId