Move DB compaction to CompactionView
Introduce CompactionView and CompactionLoadingView to schedule and run Ndb compaction on a background task before showing the main UI. Replace the direct compaction calls in ContentView and make CompactionView the app's launch entry so startup compaction presents a loading UI and avoids blocking the main thread. Changelog-Fixed: Fixed startup crash when app performs long-running compaction procedure Closes: #3726 Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
This commit is contained in:
@@ -142,15 +142,6 @@ struct ContentView: View {
|
||||
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
|
||||
|
||||
init(keypair: Keypair, appDelegate: AppDelegate?) {
|
||||
// Schedule automatic compaction based on the user's configured interval, then
|
||||
// run compaction if it was previously requested (either manually or by the scheduler).
|
||||
// Both calls run before opening the main Ndb instance so they work on an idle database.
|
||||
// This also gets run here instead of `connect` because we should anticipate this to add a few seconds of delay in worst case scenarios.
|
||||
// If we were to add this in the `connect` function, parallel functions that depend on `damus_state!` could cause crashes in the app.
|
||||
// By placing this here, we only delay the splash screen a bit
|
||||
Ndb.schedule_auto_compact_if_needed()
|
||||
Ndb.compact_if_needed()
|
||||
|
||||
self.keypair = keypair
|
||||
self.appDelegate = appDelegate
|
||||
}
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
//
|
||||
// CompactionView.swift
|
||||
// damus
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
/// A view that handles database compaction on a background thread before showing the main content.
|
||||
///
|
||||
/// This view sits between `MainView` and `ContentView` to prevent the app from being killed by
|
||||
/// the watchdog due to long-running compaction operations blocking the main thread during startup.
|
||||
///
|
||||
/// ## Implementation
|
||||
///
|
||||
/// When compaction is needed, this view:
|
||||
/// 1. Shows a loading screen with progress information
|
||||
/// 2. Performs the compaction on a background thread
|
||||
/// 3. Transitions to `ContentView` once compaction completes
|
||||
/// 4. Requires explicit user acknowledgement before continuing if compaction fails
|
||||
///
|
||||
/// The compaction process is performed before `Ndb` is opened for normal use, ensuring the
|
||||
/// database is in a consistent state before the main app logic begins.
|
||||
struct CompactionView: View {
|
||||
let keypair: Keypair
|
||||
let appDelegate: AppDelegate?
|
||||
|
||||
@State private var isCompacting: Bool = false
|
||||
@State private var compactionComplete: Bool = false
|
||||
@State private var compactionError: String? = nil
|
||||
@State private var hasStartedCompactionFlow: Bool = false
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if compactionComplete {
|
||||
ContentView(keypair: keypair, appDelegate: appDelegate)
|
||||
} else {
|
||||
CompactionLoadingView(
|
||||
isCompacting: isCompacting,
|
||||
error: compactionError,
|
||||
continueAfterError: continueAfterError
|
||||
)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
guard !hasStartedCompactionFlow else { return }
|
||||
hasStartedCompactionFlow = true
|
||||
performCompactionIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if compaction is needed and performs it on a background thread.
|
||||
///
|
||||
/// This function:
|
||||
/// - Schedules auto-compaction based on the configured interval
|
||||
/// - Checks if compaction was previously requested
|
||||
/// - If compaction is needed, performs it on a background thread with a loading UI
|
||||
/// - If no compaction is needed, immediately transitions to the main content
|
||||
private func performCompactionIfNeeded() {
|
||||
Ndb.schedule_auto_compact_if_needed()
|
||||
|
||||
let needsCompaction = UserDefaults.standard.bool(forKey: Ndb.compact_on_next_launch_key)
|
||||
guard needsCompaction else {
|
||||
compactionComplete = true
|
||||
return
|
||||
}
|
||||
|
||||
isCompacting = true
|
||||
|
||||
Task.detached(priority: .userInitiated) {
|
||||
do {
|
||||
try Ndb.compact_if_needed()
|
||||
|
||||
await MainActor.run {
|
||||
isCompacting = false
|
||||
compactionComplete = true
|
||||
}
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
isCompacting = false
|
||||
compactionError = error.localizedDescription
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Continues to the main content after the user acknowledges a compaction error.
|
||||
private func continueAfterError() {
|
||||
compactionComplete = true
|
||||
}
|
||||
}
|
||||
|
||||
/// A loading view displayed during database compaction.
|
||||
///
|
||||
/// Shows a spinner and informative text to the user while the compaction process runs.
|
||||
/// If compaction fails, it shows the error and requires explicit acknowledgement before continuing.
|
||||
struct CompactionLoadingView: View {
|
||||
let isCompacting: Bool
|
||||
let error: String?
|
||||
let continueAfterError: () -> Void
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Color(uiColor: .systemBackground)
|
||||
.ignoresSafeArea()
|
||||
|
||||
VStack(spacing: 20) {
|
||||
Image("icon")
|
||||
.resizable()
|
||||
.frame(width: 80, height: 80)
|
||||
.cornerRadius(16)
|
||||
|
||||
if isCompacting {
|
||||
ProgressView()
|
||||
.scaleEffect(1.5)
|
||||
.padding()
|
||||
|
||||
Text("Optimizing Database", comment: "Title shown during database compaction")
|
||||
.font(.headline)
|
||||
.foregroundColor(.primary)
|
||||
|
||||
Text("This may take a moment…", comment: "Subtitle shown during database compaction")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
|
||||
if let error {
|
||||
VStack(spacing: 12) {
|
||||
Image(systemName: "exclamationmark.triangle")
|
||||
.font(.system(size: 40))
|
||||
.foregroundColor(.orange)
|
||||
|
||||
Text("Compaction Warning", comment: "Title shown when database compaction encounters an error")
|
||||
.font(.headline)
|
||||
.foregroundColor(.primary)
|
||||
|
||||
Text(error)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal)
|
||||
|
||||
Text("The app can continue, but some storage may not have been freed.", comment: "Message shown when compaction fails but app can continue after acknowledgement")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal)
|
||||
|
||||
Button(action: continueAfterError) {
|
||||
Text("Continue", comment: "Button title used to continue into the app after acknowledging a compaction error")
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.padding(.top, 8)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
CompactionLoadingView(
|
||||
isCompacting: true,
|
||||
error: nil,
|
||||
continueAfterError: {}
|
||||
)
|
||||
}
|
||||
|
||||
#Preview("With Error") {
|
||||
CompactionLoadingView(
|
||||
isCompacting: false,
|
||||
error: "Failed to compact database",
|
||||
continueAfterError: {}
|
||||
)
|
||||
}
|
||||
@@ -28,7 +28,7 @@ struct MainView: View {
|
||||
var body: some View {
|
||||
Group {
|
||||
if let kp = keypair, !needs_setup {
|
||||
ContentView(keypair: kp, appDelegate: appDelegate)
|
||||
CompactionView(keypair: kp, appDelegate: appDelegate)
|
||||
.environmentObject(orientationTracker)
|
||||
} else {
|
||||
SetupView()
|
||||
|
||||
Reference in New Issue
Block a user