From 0779c0d41272619ef42dfca2b783b93e17595f5e Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Mon, 10 Aug 2026 15:01:35 +0700 Subject: [PATCH] refactor(launch): register each window presenter independently --- .../Infrastructure/WindowOpener.swift | 90 ++++++++++++------- .../Infrastructure/WindowOpenerBridge.swift | 12 +-- .../Infrastructure/WindowOpenerTests.swift | 43 +++++++-- 3 files changed, 100 insertions(+), 45 deletions(-) diff --git a/TablePro/Core/Services/Infrastructure/WindowOpener.swift b/TablePro/Core/Services/Infrastructure/WindowOpener.swift index acf72ac30..bc387f7cd 100644 --- a/TablePro/Core/Services/Infrastructure/WindowOpener.swift +++ b/TablePro/Core/Services/Infrastructure/WindowOpener.swift @@ -20,14 +20,17 @@ internal final class WindowOpener { @ObservationIgnored private var openSettingsAction: (() -> Void)? @ObservationIgnored private var stagedDraftId: UUID? @ObservationIgnored private var pendingCalls: [() -> Void] = [] - @ObservationIgnored private var isWired = false - private init() {} + /// Not private so a test can exercise the queue on an instance with no presenters + /// registered. Production code uses `shared`. + internal init() {} internal func openWelcome() { - run { - $0.openWelcomeAction?() + perform { opener in + guard let present = opener.openWelcomeAction else { return false } + present() NSApp.activate() + return true } } @@ -35,7 +38,11 @@ internal final class WindowOpener { if let tab { UserDefaults.standard.set(tab.rawValue, forKey: PreferenceKeys.selectedSettingsPane.name) } - run { $0.openSettingsAction?() } + perform { opener in + guard let present = opener.openSettingsAction else { return false } + present() + return true + } } internal func closeWelcome() { @@ -45,7 +52,11 @@ internal final class WindowOpener { } internal func openConnectionForm(editing connectionId: UUID) { - run { $0.openConnectionFormAction?(.edit(connectionId: connectionId)) } + perform { opener in + guard let present = opener.openConnectionFormAction else { return false } + present(.edit(connectionId: connectionId)) + return true + } } internal func openConnectionForm() { @@ -64,7 +75,11 @@ internal final class WindowOpener { internal func openStagedConnectionForm() { guard let draftId = stagedDraftId else { return } stagedDraftId = nil - run { $0.openConnectionFormAction?(.create(draftId: draftId)) } + perform { opener in + guard let present = opener.openConnectionFormAction else { return false } + present(.create(draftId: draftId)) + return true + } } private func discardStagedDraft() { @@ -82,36 +97,49 @@ internal final class WindowOpener { } internal func openIntegrationsActivity() { - run { $0.openIntegrationsActivityAction?() } + perform { opener in + guard let present = opener.openIntegrationsActivityAction else { return false } + present() + return true + } } - internal func wire( - openWelcome: @escaping () -> Void, - openConnectionForm: @escaping (ConnectionFormRequest) -> Void, - openIntegrationsActivity: @escaping () -> Void, - openSettings: @escaping () -> Void - ) { - openWelcomeAction = openWelcome - openConnectionFormAction = openConnectionForm - openIntegrationsActivityAction = openIntegrationsActivity - openSettingsAction = openSettings - isWired = true + internal func setWelcomePresenter(_ present: @escaping () -> Void) { + openWelcomeAction = present + drainPendingCalls() + } + + internal func setConnectionFormPresenter(_ present: @escaping (ConnectionFormRequest) -> Void) { + openConnectionFormAction = present + drainPendingCalls() + } + + internal func setIntegrationsActivityPresenter(_ present: @escaping () -> Void) { + openIntegrationsActivityAction = present + drainPendingCalls() + } + + internal func setSettingsPresenter(_ present: @escaping () -> Void) { + openSettingsAction = present + drainPendingCalls() + } + + /// Returns false when the presenter for that window has not been registered yet, which + /// queues the call. Each window registers independently, so one that has already migrated + /// to AppKit never waits on one that has not. + private func perform(_ block: @escaping (WindowOpener) -> Bool) { + guard !block(self) else { return } + Self.logger.notice("WindowOpener call queued; presenter not registered yet") + pendingCalls.append { [weak self] in + self?.perform(block) + } + } + + private func drainPendingCalls() { let drained = pendingCalls pendingCalls.removeAll() for call in drained { call() } } - - private func run(_ block: @escaping (WindowOpener) -> Void) { - if isWired { - block(self) - return - } - Self.logger.notice("WindowOpener call queued; bridge not yet wired") - pendingCalls.append { [weak self] in - guard let self else { return } - block(self) - } - } } diff --git a/TablePro/Views/Infrastructure/WindowOpenerBridge.swift b/TablePro/Views/Infrastructure/WindowOpenerBridge.swift index e4f212fed..d549adb81 100644 --- a/TablePro/Views/Infrastructure/WindowOpenerBridge.swift +++ b/TablePro/Views/Infrastructure/WindowOpenerBridge.swift @@ -16,11 +16,11 @@ internal struct WindowOpenerBridge: View { } private func wireUp() { - WindowOpener.shared.wire( - openWelcome: { openWindow(id: SceneId.welcome) }, - openConnectionForm: { request in openWindow(id: SceneId.connectionForm, value: request) }, - openIntegrationsActivity: { openWindow(id: SceneId.integrationsActivity) }, - openSettings: { openSettings() } - ) + WindowOpener.shared.setWelcomePresenter { openWindow(id: SceneId.welcome) } + WindowOpener.shared.setConnectionFormPresenter { request in + openWindow(id: SceneId.connectionForm, value: request) + } + WindowOpener.shared.setIntegrationsActivityPresenter { openWindow(id: SceneId.integrationsActivity) } + WindowOpener.shared.setSettingsPresenter { openSettings() } } } diff --git a/TableProTests/Core/Services/Infrastructure/WindowOpenerTests.swift b/TableProTests/Core/Services/Infrastructure/WindowOpenerTests.swift index 860a08bde..94006398f 100644 --- a/TableProTests/Core/Services/Infrastructure/WindowOpenerTests.swift +++ b/TableProTests/Core/Services/Infrastructure/WindowOpenerTests.swift @@ -14,14 +14,12 @@ final class WindowOpenerTests: XCTestCase { super.setUp() _ = WelcomeRouter.shared.consumePendingRequest() openedRequests = [] - WindowOpener.shared.wire( - openWelcome: {}, - openConnectionForm: { [weak self] request in - self?.openedRequests.append(request) - }, - openIntegrationsActivity: {}, - openSettings: {} - ) + WindowOpener.shared.setWelcomePresenter {} + WindowOpener.shared.setConnectionFormPresenter { [weak self] request in + self?.openedRequests.append(request) + } + WindowOpener.shared.setIntegrationsActivityPresenter {} + WindowOpener.shared.setSettingsPresenter {} } override func tearDown() { @@ -128,6 +126,35 @@ final class WindowOpenerTests: XCTestCase { XCTAssertEqual(openedRequests, [.edit(connectionId: connectionId)]) } + func testACallMadeBeforeItsPresenterIsRegisteredRunsOnceItArrives() { + let opener = WindowOpener() + var opened: [ConnectionFormRequest] = [] + let connectionId = UUID() + + opener.openConnectionForm(editing: connectionId) + XCTAssertTrue(opened.isEmpty, "No presenter yet, so the call has to wait") + + opener.setConnectionFormPresenter { opened.append($0) } + + XCTAssertEqual(opened, [.edit(connectionId: connectionId)]) + } + + /// Registering one window's presenter must not drop another window's queued call, which is + /// what lets the windows migrate to AppKit one at a time. + func testRegisteringOneWindowDoesNotDiscardAnotherWindowsQueuedCall() { + let opener = WindowOpener() + var opened: [ConnectionFormRequest] = [] + let connectionId = UUID() + + opener.openConnectionForm(editing: connectionId) + opener.setWelcomePresenter {} + XCTAssertTrue(opened.isEmpty, "The connection form still has no presenter") + + opener.setConnectionFormPresenter { opened.append($0) } + + XCTAssertEqual(opened, [.edit(connectionId: connectionId)]) + } + func testEditingTheSameConnectionTwiceRequestsTheSameWindow() { let connectionId = UUID()