git.iko.soy/parseable-macos
on master help sign in
Fix 4 bugs: poll overlap on restart, stale sort task, schema stream mismatch, stale error message
commitc21859a3ee575afa0e59231eda0f7bd484e8f389 authoriko <mail@iko.soy> dateWed 24 Jun 2026, 04:54 (3 months ago) parent533ed27
5 files changed+22−5
ParseableViewer/ViewModels/LiveTailViewModel.swift+10−2 ParseableViewer/ViewModels/QueryViewModel.swift+1−1 ParseableViewer/Views/ContentView.swift+4−1 ParseableViewer/Views/LiveTailView.swift+1 ParseableViewer/Views/QueryView.swift+6−1
ParseableViewer/ViewModels/LiveTailViewModel.swift+10−2
@@ -420,10 +420,18 @@ final class LiveTailViewModel {
420420 }
421421
422422 private func poll(client: ParseableClient, stream: String) async {
423+ let generation = pollGeneration
423424 guard !isPolling else { return }
424425 isPolling = true
425− defer { isPolling = false }
426− let generation = pollGeneration
426+ // Only release the guard if this poll is still the current generation. A
427+ // stop()/start() during the network await can supersede this poll while a
428+ // newer-generation poll is already in flight; clearing isPolling
429+ // unconditionally would let a subsequent timer tick start a second poll
430+ // that overlaps the newer one, racing writes to lastTimestamp /
431+ // consecutiveErrors. start()/clear() reset isPolling on their own
432+ // generation bump, so a superseded poll leaving it set can't wedge the
433+ // new timer.
434+ defer { if generation == pollGeneration { isPolling = false } }
427435
428436 let now = Date()
429437 // The Parseable server truncates query time-ranges to minute
ParseableViewer/ViewModels/QueryViewModel.swift+1−1
@@ -79,7 +79,7 @@ final class QueryViewModel {
7979 var schemaFields: [SchemaField] = []
8080 /// The stream of the most recent `loadSchema` call, used to drop a stale
8181 /// in-flight response when the user rapidly switches streams.
82− private var latestSchemaStream: String?
82+ private(set) var latestSchemaStream: String?
8383
8484 // Query task for cancellation
8585 private var queryTask: Task<Void, Never>?
ParseableViewer/Views/ContentView.swift+4−1
@@ -19,7 +19,10 @@ struct ContentView: View {
1919 ConnectionSheet(connection: appState.editingConnection)
2020 }
2121 .alert("Error", isPresented: $appState.showError) {
22− Button("OK") { appState.showError = false }
22+ Button("OK") {
23+ appState.showError = false
24+ appState.errorMessage = nil
25+ }
2326 } message: {
2427 Text(appState.errorMessage ?? "An unknown error occurred")
2528 }
ParseableViewer/Views/LiveTailView.swift+1
@@ -272,6 +272,7 @@ struct LiveTailView: View {
272272 }
273273 .onDisappear {
274274 viewModel.stop()
275+ liveTailSortTask?.cancel()
275276 }
276277 .onChange(of: appState.selectedStream) { _, _ in
277278 if viewModel.isRunning {
ParseableViewer/Views/QueryView.swift+6−1
@@ -303,7 +303,12 @@ struct QueryView: View {
303303 }
304304 .onAppear {
305305 if let stream = appState.selectedStream {
306− if viewModel.schemaFields.isEmpty {
306+ // Reload the schema when it's missing OR when the loaded schema
307+ // belongs to a different stream than the current one — otherwise a
308+ // viewModel that survives with another stream's schema would keep
309+ // autocomplete pointed at the wrong stream's fields until the next
310+ // stream switch.
311+ if viewModel.schemaFields.isEmpty || viewModel.latestSchemaStream != stream {
307312 Task {
308313 await viewModel.loadSchema(client: appState.client, stream: stream)
309314 }