Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions TablePro/Core/Database/DatabaseManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ final class DatabaseManager {
/// Separate from the main driver so pings never queue behind long-running user queries.
private var pingDrivers: [UUID: DatabaseDriver] = [:]

private var metadataCreationTasks: [UUID: Task<Void, Never>] = [:]

/// Current session (computed from currentSessionId)
var currentSession: ConnectionSession? {
guard let sessionId = currentSessionId else { return nil }
Expand Down Expand Up @@ -204,8 +206,9 @@ final class DatabaseManager {
let metaConnection = effectiveConnection
let metaConnectionId = connection.id
let metaTimeout = AppSettingsManager.shared.general.queryTimeoutSeconds
Task { [weak self] in
metadataCreationTasks[metaConnectionId] = Task { [weak self] in
guard let self else { return }
defer { self.metadataCreationTasks.removeValue(forKey: metaConnectionId) }
do {
let metaDriver = try DatabaseDriverFactory.createDriver(for: metaConnection)
try await metaDriver.connect()
Expand Down Expand Up @@ -269,6 +272,10 @@ final class DatabaseManager {
try? await SSHTunnelManager.shared.closeTunnel(connectionId: session.connection.id)
}

// Cancel any in-flight metadata driver creation
metadataCreationTasks[sessionId]?.cancel()
metadataCreationTasks.removeValue(forKey: sessionId)

// Stop health monitoring
await stopHealthMonitor(for: sessionId)

Expand Down Expand Up @@ -301,6 +308,9 @@ final class DatabaseManager {
await stopHealthMonitor(for: sessionId)
}

for task in metadataCreationTasks.values { task.cancel() }
metadataCreationTasks.removeAll()

let sessionIds = Array(activeSessions.keys)
for sessionId in sessionIds {
await disconnectSession(sessionId)
Expand Down Expand Up @@ -539,7 +549,7 @@ final class DatabaseManager {
}
case .failed:
Self.logger.error(
"Health monitoring failed for session \(id) after 3 retries")
"Health monitoring failed for session \(id)")
self.updateSession(id) { session in
session.status = .error(String(localized: "Connection lost"))
session.clearCachedData()
Expand Down Expand Up @@ -674,8 +684,9 @@ final class DatabaseManager {
let metaTimeout = AppSettingsManager.shared.general.queryTimeoutSeconds
let startupCmds = session.connection.startupCommands
let connName = session.connection.name
Task { [weak self] in
metadataCreationTasks[metaConnectionId] = Task { [weak self] in
guard let self else { return }
defer { self.metadataCreationTasks.removeValue(forKey: metaConnectionId) }
do {
let metaDriver = try DatabaseDriverFactory.createDriver(for: metaConnection)
try await metaDriver.connect()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
import Foundation

extension MainContentCoordinator {
func setupURLNotificationObservers() {
func setupURLNotificationObservers() -> [NSObjectProtocol] {
let connId = connectionId
NotificationCenter.default.addObserver(
let observer1 = NotificationCenter.default.addObserver(
forName: .applyURLFilter,
object: nil,
queue: .main
Expand All @@ -17,7 +17,6 @@ extension MainContentCoordinator {
let targetId = userInfo["connectionId"] as? UUID,
targetId == connId else { return }

// Extract Sendable values before crossing isolation boundary
let condition = userInfo["condition"] as? String
let column = userInfo["column"] as? String
let operation = userInfo["operation"] as? String
Expand All @@ -30,7 +29,7 @@ extension MainContentCoordinator {
}
}

NotificationCenter.default.addObserver(
let observer2 = NotificationCenter.default.addObserver(
forName: .switchSchemaFromURL,
object: nil,
queue: .main
Expand All @@ -42,14 +41,16 @@ extension MainContentCoordinator {

Task { @MainActor [weak self] in
guard let self else { return }

if self.connection.type == .postgresql {
await self.switchSchema(to: schema)
} else {
await self.switchDatabase(to: schema)
}
}
}

return [observer1, observer2]
}

private func applyURLFilterValues(
Expand Down
7 changes: 6 additions & 1 deletion TablePro/Views/Main/MainContentCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ final class MainContentCoordinator {
@ObservationIgnored private var changeManagerUpdateTask: Task<Void, Never>?
@ObservationIgnored private var activeSortTasks: [UUID: Task<Void, Never>] = [:]
@ObservationIgnored private var terminationObserver: NSObjectProtocol?
@ObservationIgnored private var urlFilterObservers: [NSObjectProtocol] = []

/// Set during handleTabChange to suppress redundant onChange(of: resultColumns) reconfiguration
@ObservationIgnored internal var isHandlingTabSwitch = false
Expand Down Expand Up @@ -156,7 +157,7 @@ final class MainContentCoordinator {

self.schemaProvider = SchemaProviderRegistry.shared.getOrCreate(for: connection.id)
SchemaProviderRegistry.shared.retain(for: connection.id)
setupURLNotificationObservers()
urlFilterObservers = setupURLNotificationObservers()

// Synchronous save at quit time. NotificationCenter with queue: .main
// delivers the closure on the main thread, satisfying assumeIsolated's
Expand Down Expand Up @@ -195,6 +196,10 @@ final class MainContentCoordinator {
/// synchronously on MainActor so we don't depend on deinit + Task scheduling.
func teardown() {
_didTeardown.withLock { $0 = true }
for observer in urlFilterObservers {
NotificationCenter.default.removeObserver(observer)
}
urlFilterObservers.removeAll()
if let observer = terminationObserver {
NotificationCenter.default.removeObserver(observer)
terminationObserver = nil
Expand Down