Skip to content

Commit d69f475

Browse files
committed
fix: address PR review — ordered FIFO queue, cold-launch fallback, restore comments
1 parent 8c222c5 commit d69f475

File tree

4 files changed

+56
-26
lines changed

4 files changed

+56
-26
lines changed

TablePro/AppDelegate+WindowConfig.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,7 @@ extension AppDelegate {
267267
configuredWindows.insert(windowId)
268268

269269
let pendingConnectionId = MainActor.assumeIsolated {
270-
WindowOpener.shared.consumeAnyPendingConnectionId()
270+
WindowOpener.shared.consumeOldestPendingConnectionId()
271271
}
272272

273273
if pendingConnectionId == nil && !isAutoReconnecting {

TablePro/Core/Services/Infrastructure/WindowOpener.swift

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// TablePro
44
//
55
// Bridges SwiftUI's openWindow environment action to imperative code.
6-
// Stored by ContentView on appear so MainContentCommandActions can open native tabs.
6+
// Stored on appear by ContentView, WelcomeViewModel, or ConnectionFormView.
77
//
88

99
import os
@@ -15,40 +15,42 @@ internal final class WindowOpener {
1515

1616
internal static let shared = WindowOpener()
1717

18-
/// Set by ContentView when it appears. Safe to store — OpenWindowAction is app-scoped, not view-scoped.
18+
/// Set on appear by ContentView, WelcomeViewModel, or ConnectionFormView.
19+
/// Safe to store — OpenWindowAction is app-scoped, not view-scoped.
1920
internal var openWindow: OpenWindowAction?
2021

21-
/// Payloads for windows that have been requested but not yet acknowledged
22-
/// by MainContentView.configureWindow. Keyed by payload.id.
23-
/// Stores connectionId so windowDidBecomeKey can compute tabbingIdentifier
24-
/// synchronously (before SwiftUI renders) to avoid flicker.
25-
internal private(set) var pendingPayloads: [UUID: UUID] = [:] // [payloadId: connectionId]
22+
/// Ordered queue of pending payloads — windows requested via openNativeTab
23+
/// but not yet acknowledged by MainContentView.configureWindow.
24+
/// Ordered so consumeOldestPendingConnectionId returns the correct entry
25+
/// when multiple windows open in quick succession (e.g., tab restore).
26+
internal private(set) var pendingPayloads: [(id: UUID, connectionId: UUID)] = []
2627

2728
/// Whether any payloads are pending — used for orphan detection in windowDidBecomeKey.
2829
internal var hasPendingPayloads: Bool { !pendingPayloads.isEmpty }
2930

3031
/// Opens a new native window tab with the given payload.
32+
/// Falls back to .openMainWindow notification if openWindow is not yet available
33+
/// (cold launch from Dock menu before any SwiftUI view has appeared).
3134
internal func openNativeTab(_ payload: EditorTabPayload) {
32-
pendingPayloads[payload.id] = payload.connectionId
33-
guard let openWindow else {
34-
Self.logger.warning("openNativeTab called before openWindow was set — payload dropped")
35-
pendingPayloads.removeValue(forKey: payload.id)
36-
return
35+
pendingPayloads.append((id: payload.id, connectionId: payload.connectionId))
36+
if let openWindow {
37+
openWindow(id: "main", value: payload)
38+
} else {
39+
Self.logger.info("openWindow not set — falling back to .openMainWindow notification")
40+
NotificationCenter.default.post(name: .openMainWindow, object: payload)
3741
}
38-
openWindow(id: "main", value: payload)
3942
}
4043

4144
/// Called by MainContentView.configureWindow after the window is fully set up.
4245
internal func acknowledgePayload(_ id: UUID) {
43-
pendingPayloads.removeValue(forKey: id)
46+
pendingPayloads.removeAll { $0.id == id }
4447
}
4548

4649
/// Consumes and returns the connectionId for the oldest pending payload.
47-
/// Removes the entry so subsequent calls don't return stale data.
48-
internal func consumeAnyPendingConnectionId() -> UUID? {
49-
guard let first = pendingPayloads.first else { return nil }
50-
pendingPayloads.removeValue(forKey: first.key)
51-
return first.value
50+
/// Removes the entry so subsequent calls return the next payload in order.
51+
internal func consumeOldestPendingConnectionId() -> UUID? {
52+
guard !pendingPayloads.isEmpty else { return nil }
53+
return pendingPayloads.removeFirst().connectionId
5254
}
5355

5456
/// Returns the tabbingIdentifier for a connection.

TablePro/Views/Main/MainContentView.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,7 @@ struct MainContentView: View {
472472
{
473473
Task { await coordinator.switchDatabase(to: selectedTab.databaseName) }
474474
} else {
475+
// columns is [] on initial load — buildFilteredQuery uses SELECT *
475476
if !selectedTab.filterState.appliedFilters.isEmpty,
476477
let tableName = selectedTab.tableName,
477478
let tabIndex = tabManager.selectedTabIndex
@@ -491,6 +492,7 @@ struct MainContentView: View {
491492
coordinator.executeTableTabQueryDirectly()
492493
}
493494
} else {
495+
// Reactive path: fires via onChange(of: sessionVersion) when connection is ready
494496
coordinator.needsLazyLoad = true
495497
}
496498
}

TableProTests/Core/Services/WindowTabGroupingTests.swift

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,23 +18,26 @@ import Testing
1818
struct WindowTabGroupingTests {
1919
// MARK: - WindowOpener pending payload tracking
2020

21-
@Test("openNativeTab without openWindow action drops payload and removes from pending")
22-
func openNativeTabWithoutOpenWindowDropsPayload() {
21+
@Test("openNativeTab without openWindow falls back to notification and keeps pending")
22+
func openNativeTabWithoutOpenWindowFallsBack() {
2323
let connectionId = UUID()
2424
let opener = WindowOpener.shared
2525

2626
opener.openWindow = nil
2727
let payload = EditorTabPayload(connectionId: connectionId, tabType: .table, tableName: "users")
2828
opener.openNativeTab(payload)
2929

30-
#expect(opener.pendingPayloads[payload.id] == nil)
30+
// Payload stays pending (notification handler will create the window)
31+
#expect(opener.pendingPayloads.contains { $0.id == payload.id })
32+
// Clean up
33+
opener.acknowledgePayload(payload.id)
3134
}
3235

3336
@Test("pendingPayloads is empty initially")
3437
func pendingPayloadsEmptyInitially() {
3538
let opener = WindowOpener.shared
36-
for id in opener.pendingPayloads.keys {
37-
opener.acknowledgePayload(id)
39+
for entry in opener.pendingPayloads {
40+
opener.acknowledgePayload(entry.id)
3841
}
3942

4043
#expect(opener.pendingPayloads.isEmpty)
@@ -46,7 +49,30 @@ struct WindowTabGroupingTests {
4649
let payloadId = UUID()
4750

4851
opener.acknowledgePayload(payloadId)
49-
#expect(opener.pendingPayloads[payloadId] == nil)
52+
#expect(!opener.pendingPayloads.contains { $0.id == payloadId })
53+
}
54+
55+
@Test("consumeOldestPendingConnectionId returns in FIFO order")
56+
func consumeOldestReturnsFIFO() {
57+
let opener = WindowOpener.shared
58+
// Clear any stale state
59+
while opener.consumeOldestPendingConnectionId() != nil {}
60+
61+
let idA = UUID()
62+
let idB = UUID()
63+
let payloadA = EditorTabPayload(connectionId: idA, tabType: .query)
64+
let payloadB = EditorTabPayload(connectionId: idB, tabType: .query)
65+
66+
opener.openWindow = nil
67+
opener.openNativeTab(payloadA)
68+
opener.openNativeTab(payloadB)
69+
70+
let first = opener.consumeOldestPendingConnectionId()
71+
let second = opener.consumeOldestPendingConnectionId()
72+
73+
#expect(first == idA)
74+
#expect(second == idB)
75+
#expect(opener.consumeOldestPendingConnectionId() == nil)
5076
}
5177

5278
// MARK: - TabbingIdentifier resolution

0 commit comments

Comments
 (0)