Merge pull request #3728 from danieldaquino/gh-3726

Move DB compaction to CompactionView
This commit is contained in:
Daniel D’Aquino
2026-04-23 19:53:17 -07:00
committed by GitHub
6 changed files with 244 additions and 26 deletions
+16
View File
@@ -1755,6 +1755,9 @@
D78DB8592C1CE9CA00F0AB12 /* SwipeActions in Frameworks */ = {isa = PBXBuildFile; productRef = D78DB8582C1CE9CA00F0AB12 /* SwipeActions */; }; D78DB8592C1CE9CA00F0AB12 /* SwipeActions in Frameworks */ = {isa = PBXBuildFile; productRef = D78DB8582C1CE9CA00F0AB12 /* SwipeActions */; };
D78DB85B2C20FE5000F0AB12 /* VectorMath.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78DB85A2C20FE4F00F0AB12 /* VectorMath.swift */; }; D78DB85B2C20FE5000F0AB12 /* VectorMath.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78DB85A2C20FE4F00F0AB12 /* VectorMath.swift */; };
D78DB85F2C20FED300F0AB12 /* ChatBubbleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78DB85E2C20FED300F0AB12 /* ChatBubbleView.swift */; }; D78DB85F2C20FED300F0AB12 /* ChatBubbleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78DB85E2C20FED300F0AB12 /* ChatBubbleView.swift */; };
D78E5FC52F9AF73C00E3EE91 /* CompactionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78E5FC42F9AF70200E3EE91 /* CompactionView.swift */; };
D78E5FC62F9AF7F700E3EE91 /* CompactionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78E5FC42F9AF70200E3EE91 /* CompactionView.swift */; };
D78E5FC72F9AF80100E3EE91 /* CompactionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78E5FC42F9AF70200E3EE91 /* CompactionView.swift */; };
D78F080C2D7F78EF00FC6C75 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78F080B2D7F78EB00FC6C75 /* Request.swift */; }; D78F080C2D7F78EF00FC6C75 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78F080B2D7F78EB00FC6C75 /* Request.swift */; };
D78F080D2D7F78EF00FC6C75 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78F080B2D7F78EB00FC6C75 /* Request.swift */; }; D78F080D2D7F78EF00FC6C75 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78F080B2D7F78EB00FC6C75 /* Request.swift */; };
D78F080E2D7F78EF00FC6C75 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78F080B2D7F78EB00FC6C75 /* Request.swift */; }; D78F080E2D7F78EF00FC6C75 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78F080B2D7F78EB00FC6C75 /* Request.swift */; };
@@ -2889,6 +2892,7 @@
D78CD5972B8990300014D539 /* DamusAppNotificationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusAppNotificationView.swift; sourceTree = "<group>"; }; D78CD5972B8990300014D539 /* DamusAppNotificationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusAppNotificationView.swift; sourceTree = "<group>"; };
D78DB85A2C20FE4F00F0AB12 /* VectorMath.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VectorMath.swift; sourceTree = "<group>"; }; D78DB85A2C20FE4F00F0AB12 /* VectorMath.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VectorMath.swift; sourceTree = "<group>"; };
D78DB85E2C20FED300F0AB12 /* ChatBubbleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatBubbleView.swift; sourceTree = "<group>"; }; D78DB85E2C20FED300F0AB12 /* ChatBubbleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatBubbleView.swift; sourceTree = "<group>"; };
D78E5FC42F9AF70200E3EE91 /* CompactionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompactionView.swift; sourceTree = "<group>"; };
D78F080B2D7F78EB00FC6C75 /* Request.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Request.swift; sourceTree = "<group>"; }; D78F080B2D7F78EB00FC6C75 /* Request.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Request.swift; sourceTree = "<group>"; };
D78F08102D7F78F600FC6C75 /* Response.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Response.swift; sourceTree = "<group>"; }; D78F08102D7F78F600FC6C75 /* Response.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Response.swift; sourceTree = "<group>"; };
D78F08162D7F7F6C00FC6C75 /* NIP04.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP04.swift; sourceTree = "<group>"; }; D78F08162D7F7F6C00FC6C75 /* NIP04.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP04.swift; sourceTree = "<group>"; };
@@ -4253,6 +4257,7 @@
5C78A7792E22FDFE00CF177D /* Features */ = { 5C78A7792E22FDFE00CF177D /* Features */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
D78E5FC32F9AF6EC00E3EE91 /* Compaction */,
5C8F97042EB45E39009399B1 /* Live */, 5C8F97042EB45E39009399B1 /* Live */,
D5C1AFC22E5DFF040092F72F /* ContactCard */, D5C1AFC22E5DFF040092F72F /* ContactCard */,
5C78A7BC2E304D7400CF177D /* Translations */, 5C78A7BC2E304D7400CF177D /* Translations */,
@@ -5368,6 +5373,14 @@
path = Chat; path = Chat;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
D78E5FC32F9AF6EC00E3EE91 /* Compaction */ = {
isa = PBXGroup;
children = (
D78E5FC42F9AF70200E3EE91 /* CompactionView.swift */,
);
path = Compaction;
sourceTree = "<group>";
};
D78F080A2D7F78B000FC6C75 /* WalletConnect */ = { D78F080A2D7F78B000FC6C75 /* WalletConnect */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -6311,6 +6324,7 @@
D7DB1FDE2D5A78CE00CF06DA /* NIP44.swift in Sources */, D7DB1FDE2D5A78CE00CF06DA /* NIP44.swift in Sources */,
B51C1CEB2B55A60A00E312A9 /* MuteDurationMenu.swift in Sources */, B51C1CEB2B55A60A00E312A9 /* MuteDurationMenu.swift in Sources */,
4CB88389296AF99A00DC99E7 /* EventDetailBar.swift in Sources */, 4CB88389296AF99A00DC99E7 /* EventDetailBar.swift in Sources */,
D78E5FC52F9AF73C00E3EE91 /* CompactionView.swift in Sources */,
4C32B9512A9AD44700DC3548 /* FlatbuffersErrors.swift in Sources */, 4C32B9512A9AD44700DC3548 /* FlatbuffersErrors.swift in Sources */,
5CC852A62BE00F180039FFC5 /* HighlightEventRef.swift in Sources */, 5CC852A62BE00F180039FFC5 /* HighlightEventRef.swift in Sources */,
4CE8794E2996B16A00F758CC /* RelayToggle.swift in Sources */, 4CE8794E2996B16A00F758CC /* RelayToggle.swift in Sources */,
@@ -7053,6 +7067,7 @@
D7F4F0BA2F03689300B61683 /* DeveloperSettingsView.swift in Sources */, D7F4F0BA2F03689300B61683 /* DeveloperSettingsView.swift in Sources */,
82D6FC722CD99F7900C925F4 /* EULAView.swift in Sources */, 82D6FC722CD99F7900C925F4 /* EULAView.swift in Sources */,
82D6FC732CD99F7900C925F4 /* RepostsView.swift in Sources */, 82D6FC732CD99F7900C925F4 /* RepostsView.swift in Sources */,
D78E5FC62F9AF7F700E3EE91 /* CompactionView.swift in Sources */,
82D6FC742CD99F7900C925F4 /* Launch.storyboard in Sources */, 82D6FC742CD99F7900C925F4 /* Launch.storyboard in Sources */,
82D6FC752CD99F7900C925F4 /* QRCodeView.swift in Sources */, 82D6FC752CD99F7900C925F4 /* QRCodeView.swift in Sources */,
82D6FC762CD99F7900C925F4 /* RelayFilterView.swift in Sources */, 82D6FC762CD99F7900C925F4 /* RelayFilterView.swift in Sources */,
@@ -7462,6 +7477,7 @@
D73E5F452C6A97F5007EB227 /* AddRelayView.swift in Sources */, D73E5F452C6A97F5007EB227 /* AddRelayView.swift in Sources */,
D73E5F472C6A97F5007EB227 /* BookmarksView.swift in Sources */, D73E5F472C6A97F5007EB227 /* BookmarksView.swift in Sources */,
D73E5F482C6A97F5007EB227 /* CarouselView.swift in Sources */, D73E5F482C6A97F5007EB227 /* CarouselView.swift in Sources */,
D78E5FC72F9AF80100E3EE91 /* CompactionView.swift in Sources */,
D73E5F492C6A97F5007EB227 /* ConfigView.swift in Sources */, D73E5F492C6A97F5007EB227 /* ConfigView.swift in Sources */,
D733F9E12D92C1D900317B11 /* SubscriptionManager.swift in Sources */, D733F9E12D92C1D900317B11 /* SubscriptionManager.swift in Sources */,
D73E5F4A2C6A97F5007EB227 /* CreateAccountView.swift in Sources */, D73E5F4A2C6A97F5007EB227 /* CreateAccountView.swift in Sources */,
-9
View File
@@ -142,15 +142,6 @@ struct ContentView: View {
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
init(keypair: Keypair, appDelegate: AppDelegate?) { 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.keypair = keypair
self.appDelegate = appDelegate 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: {}
)
}
+1 -1
View File
@@ -28,7 +28,7 @@ struct MainView: View {
var body: some View { var body: some View {
Group { Group {
if let kp = keypair, !needs_setup { if let kp = keypair, !needs_setup {
ContentView(keypair: kp, appDelegate: appDelegate) CompactionView(keypair: kp, appDelegate: appDelegate)
.environmentObject(orientationTracker) .environmentObject(orientationTracker)
} else { } else {
SetupView() SetupView()
+8 -8
View File
@@ -56,8 +56,8 @@ final class NdbCompactionTests: XCTestCase {
// Given: the flag is false (set in setUp) // Given: the flag is false (set in setUp)
let dbPath = testDirectory.path let dbPath = testDirectory.path
// When // When / Then
Ndb.compact_if_needed(db_path: dbPath) XCTAssertNoThrow(try Ndb.compact_if_needed(db_path: dbPath))
// Then: no temp directory was created, no files were touched // Then: no temp directory was created, no files were touched
let tempPath = "\(dbPath)/ndb_compact_temp" let tempPath = "\(dbPath)/ndb_compact_temp"
@@ -79,8 +79,8 @@ final class NdbCompactionTests: XCTestCase {
let emptyPath = testDirectory.appendingPathComponent("empty_db").path let emptyPath = testDirectory.appendingPathComponent("empty_db").path
try? FileManager.default.createDirectory(atPath: emptyPath, withIntermediateDirectories: true) try? FileManager.default.createDirectory(atPath: emptyPath, withIntermediateDirectories: true)
// When // When / Then
Ndb.compact_if_needed(db_path: emptyPath) XCTAssertNoThrow(try Ndb.compact_if_needed(db_path: emptyPath))
// Then: flag is cleared, no temp directory left over // Then: flag is cleared, no temp directory left over
XCTAssertFalse( XCTAssertFalse(
@@ -112,8 +112,8 @@ final class NdbCompactionTests: XCTestCase {
Ndb.set_compact_on_next_launch() Ndb.set_compact_on_next_launch()
// When // When / Then
Ndb.compact_if_needed(db_path: dbPath) XCTAssertNoThrow(try Ndb.compact_if_needed(db_path: dbPath))
// Then: the flag is cleared // Then: the flag is cleared
XCTAssertFalse( XCTAssertFalse(
@@ -157,7 +157,7 @@ final class NdbCompactionTests: XCTestCase {
// When: compact_if_needed runs // When: compact_if_needed runs
Ndb.set_compact_on_next_launch() Ndb.set_compact_on_next_launch()
Ndb.compact_if_needed(db_path: dbPath) XCTAssertNoThrow(try Ndb.compact_if_needed(db_path: dbPath))
// Then: lock.mdb should NOT exist (it was deleted during compaction) // Then: lock.mdb should NOT exist (it was deleted during compaction)
XCTAssertFalse( XCTAssertFalse(
@@ -279,7 +279,7 @@ final class NdbCompactionTests: XCTestCase {
Ndb.set_compact_on_next_launch() Ndb.set_compact_on_next_launch()
let beforeCompact = Date() let beforeCompact = Date()
Ndb.compact_if_needed(db_path: dbPath) XCTAssertNoThrow(try Ndb.compact_if_needed(db_path: dbPath))
let afterCompact = Date() let afterCompact = Date()
guard let lastDate = Ndb.get_last_compact_date() else { guard let lastDate = Ndb.get_last_compact_date() else {
+40 -8
View File
@@ -38,6 +38,37 @@ enum AutoCompactSchedule: String, CaseIterable, Equatable {
} }
extension Ndb { extension Ndb {
/// Errors that can occur while compacting the database.
enum CompactionError: LocalizedError {
case missingDatabasePath
case createTempDirectoryFailed(underlyingError: Error)
case openDatabaseFailed(path: String)
case snapshotFailed(underlyingError: Error)
case compactedFileMissingOrEmpty(path: String)
case replaceDatabaseFailed(underlyingError: Error)
case postReplaceSizeMismatch(expected: Int, actual: Int)
/// A user-presentable description of the compaction failure.
var errorDescription: String? {
switch self {
case .missingDatabasePath:
return "Could not determine the database path."
case .createTempDirectoryFailed(let underlyingError):
return "Failed to create the temporary compaction directory: \(underlyingError.localizedDescription)"
case .openDatabaseFailed(let path):
return "Failed to open the database for compaction at path: \(path)"
case .snapshotFailed(let underlyingError):
return "Failed to create a compacted database snapshot: \(underlyingError.localizedDescription)"
case .compactedFileMissingOrEmpty(let path):
return "The compacted database file is missing or empty at path: \(path)"
case .replaceDatabaseFailed(let underlyingError):
return "Failed to replace the original database with the compacted copy: \(underlyingError.localizedDescription)"
case .postReplaceSizeMismatch(let expected, let actual):
return "The compacted database size verification failed. Expected \(expected) bytes, got \(actual) bytes."
}
}
}
/// Makes a compacted copy of the database in a separate directory. /// Makes a compacted copy of the database in a separate directory.
/// ///
/// This uses `mdb_env_copy2` with `MDB_CP_COMPACT` (flag = 0x01), which omits free pages /// This uses `mdb_env_copy2` with `MDB_CP_COMPACT` (flag = 0x01), which omits free pages
@@ -133,12 +164,13 @@ extension Ndb {
/// ///
/// - Parameter db_path: Override the database directory path. Pass `nil` (default) to use /// - Parameter db_path: Override the database directory path. Pass `nil` (default) to use
/// `Ndb.db_path`. Mainly useful for testing. /// `Ndb.db_path`. Mainly useful for testing.
static func compact_if_needed(db_path: String? = nil) { /// - Throws: `CompactionError` when compaction was requested but could not be completed.
static func compact_if_needed(db_path: String? = nil) throws {
guard UserDefaults.standard.bool(forKey: compact_on_next_launch_key) else { return } guard UserDefaults.standard.bool(forKey: compact_on_next_launch_key) else { return }
guard let path = db_path ?? Self.db_path else { guard let path = db_path ?? Self.db_path else {
Log.error("compact_if_needed: could not determine db path", for: .storage) Log.error("compact_if_needed: could not determine db path", for: .storage)
return throw CompactionError.missingDatabasePath
} }
guard db_file_exists(path: path) else { guard db_file_exists(path: path) else {
@@ -158,14 +190,14 @@ extension Ndb {
try FileManager.default.createDirectory(atPath: tempPath, withIntermediateDirectories: true) try FileManager.default.createDirectory(atPath: tempPath, withIntermediateDirectories: true)
} catch { } catch {
Log.error("compact_if_needed: failed to create temp dir: %@", for: .storage, String(describing: error)) Log.error("compact_if_needed: failed to create temp dir: %@", for: .storage, String(describing: error))
return throw CompactionError.createTempDirectoryFailed(underlyingError: error)
} }
// Open a temporary Ndb instance just to drive the compaction. // Open a temporary Ndb instance just to drive the compaction.
guard let tempNdb = Ndb(path: path) else { guard let tempNdb = Ndb(path: path) else {
Log.error("compact_if_needed: failed to open ndb for compaction", for: .storage) Log.error("compact_if_needed: failed to open ndb for compaction", for: .storage)
try? FileManager.default.removeItem(atPath: tempPath) try? FileManager.default.removeItem(atPath: tempPath)
return throw CompactionError.openDatabaseFailed(path: path)
} }
// Ensure the temporary Ndb is closed regardless of how this function exits. // Ensure the temporary Ndb is closed regardless of how this function exits.
defer { tempNdb.close() } defer { tempNdb.close() }
@@ -175,7 +207,7 @@ extension Ndb {
} catch { } catch {
Log.error("compact_if_needed: compaction failed: %@", for: .storage, String(describing: error)) Log.error("compact_if_needed: compaction failed: %@", for: .storage, String(describing: error))
try? FileManager.default.removeItem(atPath: tempPath) try? FileManager.default.removeItem(atPath: tempPath)
return throw CompactionError.snapshotFailed(underlyingError: error)
} }
tempNdb.close() tempNdb.close()
@@ -192,7 +224,7 @@ extension Ndb {
guard compactedSize > 0 else { guard compactedSize > 0 else {
Log.error("compact_if_needed: compacted file is missing or empty — aborting", for: .storage) Log.error("compact_if_needed: compacted file is missing or empty — aborting", for: .storage)
try? FileManager.default.removeItem(atPath: tempPath) try? FileManager.default.removeItem(atPath: tempPath)
return throw CompactionError.compactedFileMissingOrEmpty(path: compactedDataMdb.path)
} }
// Delete the stale lock.mdb BEFORE replacing data.mdb. // Delete the stale lock.mdb BEFORE replacing data.mdb.
@@ -213,7 +245,7 @@ extension Ndb {
} catch { } catch {
Log.error("compact_if_needed: failed to replace db file: %@", for: .storage, String(describing: error)) Log.error("compact_if_needed: failed to replace db file: %@", for: .storage, String(describing: error))
try? FileManager.default.removeItem(atPath: tempPath) try? FileManager.default.removeItem(atPath: tempPath)
return throw CompactionError.replaceDatabaseFailed(underlyingError: error)
} }
// Post-replace sanity check: verify the destination file exists with the expected size. // Post-replace sanity check: verify the destination file exists with the expected size.
@@ -221,7 +253,7 @@ extension Ndb {
if finalSize != compactedSize { if finalSize != compactedSize {
Log.error("compact_if_needed: post-replace size mismatch — expected %d, got %d", for: .storage, compactedSize, finalSize) Log.error("compact_if_needed: post-replace size mismatch — expected %d, got %d", for: .storage, compactedSize, finalSize)
try? FileManager.default.removeItem(atPath: tempPath) try? FileManager.default.removeItem(atPath: tempPath)
return throw CompactionError.postReplaceSizeMismatch(expected: compactedSize, actual: finalSize)
} }
Log.info("NostrDB compacted successfully", for: .storage) Log.info("NostrDB compacted successfully", for: .storage)