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
6 changes: 6 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ Both hosts use the same per-identity App Group directory, shared keychain identi

Python projects inbound messages into the shared store. `NodeInboundReplay` runs the common app field-processing path, including reactions, replies, telemetry, and attachments. Propagation synchronization awaits replay catch-up. The durable event inbox remains bounded and is acknowledged only after consumption. Stable send IDs and the durable outbox carry complete fields and propagated fallback across host/process restarts.

App-side GRDB pools opt into explicit suspension notifications. `DatabaseSuspensionController` bridges process-level UIKit background/foreground notifications to GRDB: without foreground execution or a live system background-task grant, writes are interrupted and new lock-taking accesses are refused. The bridge is installed at app launch, independently of SwiftUI scenes. Notifications remain inside the app process; backgrounding the UI does not suspend the Network Extension's database.

Background-task deliveries own separate database-access grants, including deliveries retained before the handler is ready. Completion releases a grant before reporting task completion; expiration revokes it before waiting for cancellation cleanup. Overlapping grants cannot suspend each other's work, and ending a grant cannot suspend a foreground app. Startup waits for permitted access immediately before opening new repository pools, because a pool cannot observe a suspension notification sent before it existed. Interrupted writes retain GRDB's error semantics; the lifecycle bridge does not silently report success or retry them while suspended.

Call-history writes have a separate bounded UIKit background task and matching database-access token, acquired synchronously before joining the FIFO write chain. This covers insertion, connection milestones, and final outcomes while background audio keeps a call alive. Completion releases database access before ending the system task; expiration revokes access immediately, and an expired queued write does not start. A denied system grant never resumes the database.

The persisted propagation configuration is restored before recovering outbox sends when iOS starts the extension without the app. Live relay selection, sync/cancellation, discovery, transfer limits, and peer identity retention cross typed commands through the same dispatcher in either mode.

Propagation uploads and downloads share the router's outbound relay link. The Python sync bridge waits for a pre-existing pending link within the sync deadline, then explicitly starts retrieval; it never treats the previous sync's terminal state as the current result. Already-closed links are discarded on retry, while a link that closes during the wait reports `link_failed`. Failed operations report zero received messages rather than a previous sync's count.
Expand Down
6 changes: 6 additions & 0 deletions Sources/ColumbaApp/App/ColumbaApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ struct ColumbaApp: App {

init() {
_ = Self.launchHostingMode
#if os(iOS)
// GRDB's opt-in flag listens to explicit suspension notifications,
// not UIKit lifecycle events. Install the process-wide bridge even
// when a background launch never creates a SwiftUI scene.
DatabaseSuspensionController.shared.observeApplicationLifecycle()
#endif
// Privacy hygiene + bounded storage: wipe diag.log (and its rotated
// sibling) before ANY launch work so logs left by pre-#186 builds,
// which leaked received-message plaintext on every inbound message,
Expand Down
18 changes: 18 additions & 0 deletions Sources/ColumbaApp/Services/AppServices.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1496,6 +1496,12 @@ public final class AppServices {
Self.migrateLXMFDatabaseToAppGroupIfNeeded(identityHashHex: newIdentity.hexHash)
let grdbPath = Self.grdbDatabaseFilePath(for: newIdentity.hexHash)
self.grdbDatabasePath = grdbPath
#if os(iOS)
// Newly opened pools cannot receive a suspension notification sent
// before they existed. Wait for foreground or an authorized BG task
// before opening them; no await may separate this gate from setup.
try await DatabaseSuspensionController.shared.waitUntilAccessAllowed()
#endif
let messageRepository = try MessageRepository(grdbPath: grdbPath)
self.messageRepository = messageRepository
// Open the identity-scoped call-history store. A repository left over
Expand All @@ -1509,6 +1515,9 @@ public final class AppServices {
self.callHistoryRepository = nil
}
do {
#if os(iOS)
try await DatabaseSuspensionController.shared.waitUntilAccessAllowed()
#endif
let callHistory = try CallHistoryRepository(grdbPath: grdbPath)
self.callHistoryRepository = callHistory
DiagLog.log("[INIT] call-history repository ready at \(grdbPath)")
Expand Down Expand Up @@ -3900,6 +3909,9 @@ public final class AppServices {
Self.migrateLXMFDatabaseToAppGroupIfNeeded(identityHashHex: identityHash)
let grdbPath = Self.grdbDatabaseFilePath(for: identityHash)
self.grdbDatabasePath = grdbPath
#if os(iOS)
try await DatabaseSuspensionController.shared.waitUntilAccessAllowed()
#endif
let messageRepository = try MessageRepository(grdbPath: grdbPath)
self.messageRepository = messageRepository
// Open the identity-scoped call-history store. A repository left over
Expand All @@ -3913,6 +3925,9 @@ public final class AppServices {
self.callHistoryRepository = nil
}
do {
#if os(iOS)
try await DatabaseSuspensionController.shared.waitUntilAccessAllowed()
#endif
let callHistory = try CallHistoryRepository(grdbPath: grdbPath)
self.callHistoryRepository = callHistory
DiagLog.log("[INIT] call-history repository ready at \(grdbPath)")
Expand Down Expand Up @@ -4758,6 +4773,9 @@ public final class AppServices {
Self.migrateLXMFDatabaseToAppGroupIfNeeded(identityHashHex: existingIdentity.hexHash)
let grdbPath = Self.grdbDatabaseFilePath(for: existingIdentity.hexHash)
self.grdbDatabasePath = grdbPath
#if os(iOS)
try await DatabaseSuspensionController.shared.waitUntilAccessAllowed()
#endif
let messageRepository = try MessageRepository(grdbPath: grdbPath)
self.messageRepository = messageRepository
let recoveredRetryCount = try await messageRepository.recoverInterruptedRetries()
Expand Down
Loading
Loading