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
12 changes: 12 additions & 0 deletions apple/TablePro/Extensions/NSWindow+FirstFrame.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,18 @@ internal extension NSWindow {
}
}

/// Work that must not share a run loop turn with the frame this turn builds.
@MainActor
internal enum CommittedTurn {
/// Runs `body` after this turn's frame is committed, in a later turn. A main-queue hop alone can run
/// before the run loop waits, so the hang would still be one: both pieces between two waits.
static func after(_ body: @escaping @MainActor () -> Void) {
CATransaction.setCompletionBlock {
DispatchQueue.main.async { MainActor.assumeIsolated(body) }
}
}
}

@MainActor
private final class FrameGate {
var continuation: CheckedContinuation<Void, Never>?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ internal enum SidebarObjectListPresentation: Equatable {
/// Reaching for the state enum directly instead is what left the tree with a spinner on no gate
/// at all, flashing it on every engine that groups by schema while the two flat shapes beside
/// it held theirs back.
/// What the list stands on while a loaded catalog waits for its turn: the load still reads as in flight.
internal static func presentedState(_ state: SchemaState, showsLoaded: Bool) -> SchemaState {
if case .loaded = state, !showsLoaded { return .loading }
return state
}

internal static func resolveDeferringEmptyStates(
state: SchemaState,
hasOutlastedGrace: Bool = true
Expand Down
26 changes: 24 additions & 2 deletions apple/TablePro/Views/Sidebar/SidebarView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ struct SidebarView: View {
@StateObject private var viewModel: SidebarViewModel
@ObservedObject private var settingsManager = AppSettingsManager.shared
@State private var showsSchemaProgress = false
/// See `presentedSchemaState`.
@State private var showsLoadedCatalog = false

@ObservedObject private var schemaService = SchemaService.shared
@ObservedObject private var treeMetadata = DatabaseTreeMetadataService.shared
Expand Down Expand Up @@ -200,6 +202,15 @@ struct SidebarView: View {
}
.onAppear {
coordinator?.sidebarViewModel = viewModel
// A pane put back on screen shows the list it already had.
showsLoadedCatalog = isCatalogLoaded
}
.onChange(of: isCatalogLoaded) { isLoaded in
guard isLoaded else {
showsLoadedCatalog = false
return
}
CommittedTurn.after { showsLoadedCatalog = true }
}
.onChange(of: viewModel.showOperationDialog) { isPresented in
guard isPresented else { return }
Expand Down Expand Up @@ -305,7 +316,7 @@ struct SidebarView: View {
@ViewBuilder
private var hierarchicalContent: some View {
let presentation = SidebarObjectListPresentation.resolveDeferringEmptyStates(
state: schemaService.state(for: connectionId),
state: presentedSchemaState,
hasOutlastedGrace: showsSchemaProgress
)
Group {
Expand Down Expand Up @@ -336,9 +347,20 @@ struct SidebarView: View {
)
}

private var isCatalogLoaded: Bool {
if case .loaded = schemaService.state(for: connectionId) { return true }
return false
}

// A connection's first catalog can land in the turn that commits the restored tab's frame, and building the
// outline there made one stall of both. The outline goes up in a later turn, the loading state held till then.
private var presentedSchemaState: SchemaState {
SidebarObjectListPresentation.presentedState(schemaService.state(for: connectionId), showsLoaded: showsLoadedCatalog)
}

private var objectListPresentation: SidebarObjectListPresentation {
SidebarObjectListPresentation.resolve(
state: schemaService.state(for: connectionId),
state: presentedSchemaState,
hasActiveFilter: !viewModel.filterQuery.isEmpty,
hasAnyMatch: hasAnyMatch,
hasOutlastedGrace: showsSchemaProgress,
Expand Down
11 changes: 11 additions & 0 deletions apple/TableProTests/Extensions/NSWindowNextFrameTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import AppKit
@testable import TablePro
import TableProTestSupport
import Testing

@MainActor
Expand Down Expand Up @@ -49,4 +50,14 @@ struct NSWindowNextFrameTests {

#expect(FirstFrameObserver.liveCount == before)
}

@Test("Work after the committed turn never runs in the turn that asked for it")
func committedTurnRunsLater() async throws {
var ran = false

CommittedTurn.after { ran = true }

#expect(!ran)
try await waitUntil { ran }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -167,4 +167,12 @@ struct SidebarObjectListPresentationTests {
hasOutlastedGrace: false
) == .failed("boom"))
}
@Test("A loaded catalog waiting for its turn still reads as loading, and every other state passes through")
func heldCatalogReadsAsLoading() {
let loaded = SchemaState.loaded([table("users")])
#expect(SidebarObjectListPresentation.presentedState(loaded, showsLoaded: false) == .loading)
#expect(SidebarObjectListPresentation.presentedState(loaded, showsLoaded: true) == loaded)
#expect(SidebarObjectListPresentation.presentedState(.failed("boom"), showsLoaded: false) == .failed("boom"))
#expect(SidebarObjectListPresentation.presentedState(.idle, showsLoaded: true) == .idle)
}
}
1 change: 1 addition & 0 deletions apple/changelog.d/3849.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The sidebar no longer freezes the window for a moment when a very large table list first loads.
Loading