Skip to content

Commit 5b30cc6

Browse files
committed
refactor: split AppDelegate into focused extension files
- AppDelegate.swift: class body, stored properties, lifecycle (103 lines) - AppDelegate+FileOpen.swift: URL dispatch, deeplinks, plugins (214 lines) - AppDelegate+ConnectionHandler.swift: DB URL, SQLite, unified queue (356 lines) - AppDelegate+WindowConfig.swift: window lifecycle, dock, styling (320 lines) Unified queuedDatabaseURLs + queuedSQLiteFileURLs into single QueuedURLEntry enum with one polling loop. Extracted shared suppressWelcomeWindow() and openNewConnectionWindow() helpers. Replaced guard-return with if-block in SQL file handler.
1 parent 789b292 commit 5b30cc6

4 files changed

Lines changed: 912 additions & 1011 deletions

File tree

Lines changed: 356 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,356 @@
1+
//
2+
// AppDelegate+ConnectionHandler.swift
3+
// TablePro
4+
//
5+
// Database URL and SQLite file open handlers with cold-start queuing
6+
//
7+
8+
import AppKit
9+
import os
10+
11+
private let connectionLogger = Logger(subsystem: "com.TablePro", category: "ConnectionHandler")
12+
13+
/// Typed queue entry for URLs waiting on the SwiftUI window system.
14+
/// Replaces the separate `queuedDatabaseURLs` and `queuedSQLiteFileURLs` arrays.
15+
enum QueuedURLEntry {
16+
case databaseURL(URL)
17+
case sqliteFile(URL)
18+
}
19+
20+
extension AppDelegate {
21+
// MARK: - Database URL Handler
22+
23+
func handleDatabaseURL(_ url: URL) {
24+
guard WindowOpener.shared.openWindow != nil else {
25+
queuedURLEntries.append(.databaseURL(url))
26+
scheduleQueuedURLProcessing()
27+
return
28+
}
29+
30+
let result = ConnectionURLParser.parse(url.absoluteString)
31+
guard case .success(let parsed) = result else {
32+
connectionLogger.error("Failed to parse database URL: \(url.sanitizedForLogging, privacy: .public)")
33+
return
34+
}
35+
36+
let connections = ConnectionStorage.shared.loadConnections()
37+
let matchedConnection = connections.first { conn in
38+
conn.type == parsed.type
39+
&& conn.host == parsed.host
40+
&& (parsed.port == nil || conn.port == parsed.port)
41+
&& conn.database == parsed.database
42+
&& (parsed.username.isEmpty || conn.username == parsed.username)
43+
}
44+
45+
let connection: DatabaseConnection
46+
if let matched = matchedConnection {
47+
connection = matched
48+
} else {
49+
connection = buildTransientConnection(from: parsed)
50+
}
51+
52+
if !parsed.password.isEmpty {
53+
ConnectionStorage.shared.savePassword(parsed.password, for: connection.id)
54+
}
55+
56+
if DatabaseManager.shared.activeSessions[connection.id]?.driver != nil {
57+
handlePostConnectionActions(parsed, connectionId: connection.id)
58+
bringConnectionWindowToFront(connection.id)
59+
return
60+
}
61+
62+
if let activeId = findActiveSessionByParams(parsed) {
63+
handlePostConnectionActions(parsed, connectionId: activeId)
64+
bringConnectionWindowToFront(activeId)
65+
return
66+
}
67+
68+
openNewConnectionWindow(for: connection)
69+
70+
Task { @MainActor in
71+
do {
72+
try await DatabaseManager.shared.connectToSession(connection)
73+
for window in NSApp.windows where self.isWelcomeWindow(window) {
74+
window.close()
75+
}
76+
self.handlePostConnectionActions(parsed, connectionId: connection.id)
77+
} catch {
78+
connectionLogger.error("Database URL connect failed: \(error.localizedDescription)")
79+
await self.handleConnectionFailure(error)
80+
}
81+
}
82+
}
83+
84+
// MARK: - SQLite File Handler
85+
86+
func handleSQLiteFile(_ url: URL) {
87+
guard WindowOpener.shared.openWindow != nil else {
88+
queuedURLEntries.append(.sqliteFile(url))
89+
scheduleQueuedURLProcessing()
90+
return
91+
}
92+
93+
let filePath = url.path
94+
let connectionName = url.deletingPathExtension().lastPathComponent
95+
96+
for (sessionId, session) in DatabaseManager.shared.activeSessions {
97+
if session.connection.type == .sqlite
98+
&& session.connection.database == filePath
99+
&& session.driver != nil {
100+
bringConnectionWindowToFront(sessionId)
101+
return
102+
}
103+
}
104+
105+
let connection = DatabaseConnection(
106+
name: connectionName,
107+
host: "",
108+
port: 0,
109+
database: filePath,
110+
username: "",
111+
type: .sqlite
112+
)
113+
114+
openNewConnectionWindow(for: connection)
115+
116+
Task { @MainActor in
117+
do {
118+
try await DatabaseManager.shared.connectToSession(connection)
119+
for window in NSApp.windows where self.isWelcomeWindow(window) {
120+
window.close()
121+
}
122+
} catch {
123+
connectionLogger.error("SQLite file open failed for '\(filePath, privacy: .public)': \(error.localizedDescription)")
124+
await self.handleConnectionFailure(error)
125+
}
126+
}
127+
}
128+
129+
// MARK: - Unified Queue
130+
131+
func scheduleQueuedURLProcessing() {
132+
Task { @MainActor [weak self] in
133+
var ready = false
134+
for _ in 0..<25 {
135+
if WindowOpener.shared.openWindow != nil { ready = true; break }
136+
try? await Task.sleep(for: .milliseconds(200))
137+
}
138+
guard let self else { return }
139+
if !ready {
140+
connectionLogger.warning(
141+
"SwiftUI window system not ready after 5s, dropping \(self.queuedURLEntries.count) queued URL(s)"
142+
)
143+
self.queuedURLEntries.removeAll()
144+
return
145+
}
146+
let entries = self.queuedURLEntries
147+
self.queuedURLEntries.removeAll()
148+
for entry in entries {
149+
switch entry {
150+
case .databaseURL(let url): self.handleDatabaseURL(url)
151+
case .sqliteFile(let url): self.handleSQLiteFile(url)
152+
}
153+
}
154+
}
155+
}
156+
157+
// MARK: - SQL File Queue (drained by .databaseDidConnect)
158+
159+
@objc func handleDatabaseDidConnect() {
160+
guard !queuedFileURLs.isEmpty else { return }
161+
let urls = queuedFileURLs
162+
queuedFileURLs.removeAll()
163+
postSQLFilesWhenReady(urls: urls)
164+
}
165+
166+
private func postSQLFilesWhenReady(urls: [URL]) {
167+
Task { @MainActor [weak self] in
168+
try? await Task.sleep(for: .milliseconds(100))
169+
if !NSApp.windows.contains(where: { self?.isMainWindow($0) == true && $0.isKeyWindow }) {
170+
connectionLogger.warning("postSQLFilesWhenReady: no key main window, posting anyway")
171+
}
172+
NotificationCenter.default.post(name: .openSQLFiles, object: urls)
173+
}
174+
}
175+
176+
// MARK: - Connection Window Helper
177+
178+
private func openNewConnectionWindow(for connection: DatabaseConnection) {
179+
let hadExistingMain = NSApp.windows.contains { isMainWindow($0) && $0.isVisible }
180+
if hadExistingMain {
181+
NSWindow.allowsAutomaticWindowTabbing = false
182+
}
183+
let payload = EditorTabPayload(connectionId: connection.id)
184+
WindowOpener.shared.openNativeTab(payload)
185+
}
186+
187+
// MARK: - Post-Connect Actions
188+
189+
private func handlePostConnectionActions(_ parsed: ParsedConnectionURL, connectionId: UUID) {
190+
Task { @MainActor in
191+
await waitForConnection(timeout: .seconds(5))
192+
193+
if let schema = parsed.schema {
194+
NotificationCenter.default.post(
195+
name: .switchSchemaFromURL,
196+
object: nil,
197+
userInfo: ["connectionId": connectionId, "schema": schema]
198+
)
199+
try? await Task.sleep(for: .milliseconds(500))
200+
}
201+
202+
if let tableName = parsed.tableName {
203+
let payload = EditorTabPayload(
204+
connectionId: connectionId,
205+
tabType: .table,
206+
tableName: tableName,
207+
isView: parsed.isView
208+
)
209+
WindowOpener.shared.openNativeTab(payload)
210+
211+
if parsed.filterColumn != nil || parsed.filterCondition != nil {
212+
try? await Task.sleep(for: .milliseconds(300))
213+
NotificationCenter.default.post(
214+
name: .applyURLFilter,
215+
object: nil,
216+
userInfo: [
217+
"connectionId": connectionId,
218+
"column": parsed.filterColumn as Any,
219+
"operation": parsed.filterOperation as Any,
220+
"value": parsed.filterValue as Any,
221+
"condition": parsed.filterCondition as Any
222+
]
223+
)
224+
}
225+
}
226+
}
227+
}
228+
229+
private func waitForConnection(timeout: Duration) async {
230+
await withCheckedContinuation { (continuation: CheckedContinuation<Void, Never>) in
231+
var didResume = false
232+
var observer: NSObjectProtocol?
233+
234+
func resumeOnce() {
235+
guard !didResume else { return }
236+
didResume = true
237+
if let obs = observer {
238+
NotificationCenter.default.removeObserver(obs)
239+
}
240+
continuation.resume()
241+
}
242+
243+
let timeoutTask = Task { @MainActor in
244+
try? await Task.sleep(for: timeout)
245+
resumeOnce()
246+
}
247+
observer = NotificationCenter.default.addObserver(
248+
forName: .databaseDidConnect,
249+
object: nil,
250+
queue: .main
251+
) { _ in
252+
timeoutTask.cancel()
253+
resumeOnce()
254+
}
255+
}
256+
}
257+
258+
// MARK: - Session Lookup
259+
260+
private func findActiveSessionByParams(_ parsed: ParsedConnectionURL) -> UUID? {
261+
for (id, session) in DatabaseManager.shared.activeSessions {
262+
guard session.driver != nil else { continue }
263+
let conn = session.connection
264+
if conn.type == parsed.type
265+
&& conn.host == parsed.host
266+
&& conn.database == parsed.database
267+
&& (parsed.port == nil || conn.port == parsed.port || conn.port == parsed.type.defaultPort)
268+
&& (parsed.username.isEmpty || conn.username == parsed.username)
269+
&& (parsed.redisDatabase == nil || conn.redisDatabase == parsed.redisDatabase) {
270+
return id
271+
}
272+
}
273+
return nil
274+
}
275+
276+
func bringConnectionWindowToFront(_ connectionId: UUID) {
277+
let windows = WindowLifecycleMonitor.shared.windows(for: connectionId)
278+
if let window = windows.first {
279+
window.makeKeyAndOrderFront(nil)
280+
} else {
281+
NSApp.windows.first { isMainWindow($0) && $0.isVisible }?.makeKeyAndOrderFront(nil)
282+
}
283+
}
284+
285+
// MARK: - Connection Failure
286+
287+
func handleConnectionFailure(_ error: Error) async {
288+
for window in NSApp.windows where isMainWindow(window) {
289+
let hasActiveSession = DatabaseManager.shared.activeSessions.values.contains {
290+
window.subtitle == $0.connection.name
291+
|| window.subtitle == "\($0.connection.name) — Preview"
292+
}
293+
if !hasActiveSession {
294+
window.close()
295+
}
296+
}
297+
if !NSApp.windows.contains(where: { isMainWindow($0) && $0.isVisible }) {
298+
openWelcomeWindow()
299+
}
300+
try? await Task.sleep(for: .milliseconds(200))
301+
AlertHelper.showErrorSheet(
302+
title: String(localized: "Connection Failed"),
303+
message: error.localizedDescription,
304+
window: NSApp.keyWindow
305+
)
306+
}
307+
308+
// MARK: - Transient Connection Builder
309+
310+
private func buildTransientConnection(from parsed: ParsedConnectionURL) -> DatabaseConnection {
311+
var sshConfig = SSHConfiguration()
312+
if let sshHost = parsed.sshHost {
313+
sshConfig.enabled = true
314+
sshConfig.host = sshHost
315+
sshConfig.port = parsed.sshPort ?? 22
316+
sshConfig.username = parsed.sshUsername ?? ""
317+
if parsed.usePrivateKey == true {
318+
sshConfig.authMethod = .privateKey
319+
}
320+
if parsed.useSSHAgent == true {
321+
sshConfig.authMethod = .sshAgent
322+
sshConfig.agentSocketPath = parsed.agentSocket ?? ""
323+
}
324+
}
325+
326+
var sslConfig = SSLConfiguration()
327+
if let sslMode = parsed.sslMode {
328+
sslConfig.mode = sslMode
329+
}
330+
331+
var color: ConnectionColor = .none
332+
if let hex = parsed.statusColor {
333+
color = ConnectionURLParser.connectionColor(fromHex: hex)
334+
}
335+
336+
var tagId: UUID?
337+
if let envName = parsed.envTag {
338+
tagId = ConnectionURLParser.tagId(fromEnvName: envName)
339+
}
340+
341+
return DatabaseConnection(
342+
name: parsed.connectionName ?? parsed.suggestedName,
343+
host: parsed.host,
344+
port: parsed.port ?? parsed.type.defaultPort,
345+
database: parsed.database,
346+
username: parsed.username,
347+
type: parsed.type,
348+
sshConfig: sshConfig,
349+
sslConfig: sslConfig,
350+
color: color,
351+
tagId: tagId,
352+
redisDatabase: parsed.redisDatabase,
353+
oracleServiceName: parsed.oracleServiceName
354+
)
355+
}
356+
}

0 commit comments

Comments
 (0)