Add sync mechanism to prevent background crashes and fix ndb reopen order

This adds a sync mechanism in Ndb.swift to coordinate certain usage of
nostrdb.c calls and the need to close nostrdb due to app lifecycle
requirements. Furthermore, it fixes the order of operations when
re-opening NostrDB, to avoid race conditions where a query uses an older
Ndb generation.

This sync mechanism allows multiple queries to happen simultaneously
(from the Swift-side), while preventing ndb from simultaneously closing
during such usages. It also does that while keeping the Ndb interface
sync and nonisolated, which keeps the API easy to use from
Swift/SwiftUI and allows for parallel operations to occur.

If Swift Actors were to be used (e.g. creating an NdbActor), the Ndb.swift
interface would change in such a way that it would propagate the need for
several changes throughout the codebase, including loading logic in
some ViewModels. Furthermore, it would likely decrease performance by
forcing Ndb.swift operations to run sequentially when they could run in
parallel.

Changelog-Fixed: Fixed crashes that happened when the app went into background mode
Closes: https://github.com/damus-io/damus/issues/3245
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
This commit is contained in:
Daniel D’Aquino
2025-11-12 16:04:47 -08:00
parent 6d9107f662
commit 20dc672dbf
59 changed files with 790 additions and 416 deletions

View File

@@ -92,11 +92,11 @@ extension NostrNetworkManager {
private func publishProfileUpdates(metadataEvent: borrowing UnownedNdbNote) {
let now = UInt64(Date.now.timeIntervalSince1970)
ndb.write_profile_last_fetched(pubkey: metadataEvent.pubkey, fetched_at: now)
try? ndb.write_profile_last_fetched(pubkey: metadataEvent.pubkey, fetched_at: now)
if let relevantStreams = streams[metadataEvent.pubkey] {
// If we have the user metadata event in ndb, then we should have the profile record as well.
guard let profile = ndb.lookup_profile_and_copy(metadataEvent.pubkey) else { return }
guard let profile = try? ndb.lookup_profile_and_copy(metadataEvent.pubkey) else { return }
for relevantStream in relevantStreams.values {
relevantStream.continuation.yield(profile)
}
@@ -107,7 +107,7 @@ extension NostrNetworkManager {
/// This is useful for local profile changes (e.g., nip05 validation, donation percentage updates)
func notifyProfileUpdate(pubkey: Pubkey) {
if let relevantStreams = streams[pubkey] {
guard let profile = ndb.lookup_profile_and_copy(pubkey) else { return }
guard let profile = try? ndb.lookup_profile_and_copy(pubkey) else { return }
for relevantStream in relevantStreams.values {
relevantStream.continuation.yield(profile)
}
@@ -141,7 +141,7 @@ extension NostrNetworkManager {
// Yield cached profile immediately so views don't flash placeholder content.
// Callers that only need updates (not initial state) can opt out via yieldCached: false.
if yieldCached, let existingProfile = ndb.lookup_profile_and_copy(pubkey) {
if yieldCached, let existingProfile = try? ndb.lookup_profile_and_copy(pubkey) {
continuation.yield(existingProfile)
}
@@ -176,7 +176,7 @@ extension NostrNetworkManager {
// Callers that only need updates (not initial state) can opt out via yieldCached: false.
if yieldCached {
for pubkey in pubkeys {
if let existingProfile = ndb.lookup_profile_and_copy(pubkey) {
if let existingProfile = try? ndb.lookup_profile_and_copy(pubkey) {
continuation.yield(existingProfile)
}
}

View File

@@ -360,7 +360,7 @@ extension NostrNetworkManager {
let filter = NostrFilter(ids: [noteId], limit: 1)
// Since note ids point to immutable objects, we can do a simple ndb lookup first
if let noteKey = self.ndb.lookup_note_key(noteId) {
if let noteKey = try? self.ndb.lookup_note_key(noteId) {
return NdbNoteLender(ndb: self.ndb, noteKey: noteKey)
}
@@ -413,18 +413,18 @@ extension NostrNetworkManager {
switch query {
case .profile(let pubkey):
let profileNotNil = self.ndb.lookup_profile(pubkey, borrow: { pr in
let profileNotNil = try? self.ndb.lookup_profile(pubkey, borrow: { pr in
switch pr {
case .some(let pr): return pr.profile != nil
case .none: return true
}
})
if profileNotNil {
if profileNotNil ?? false {
return .profile(pubkey)
}
filter = NostrFilter(kinds: [.metadata], limit: 1, authors: [pubkey])
case .event(let evid):
if let event = self.ndb.lookup_note_and_copy(evid) {
if let event = try? self.ndb.lookup_note_and_copy(evid) {
return .event(event)
}
filter = NostrFilter(ids: [evid], limit: 1)

View File

@@ -87,7 +87,7 @@ extension NostrNetworkManager {
private func getLatestNIP65RelayListEvent() -> NdbNote? {
guard let latestRelayListEventId = delegate.latestRelayListEventIdHex else { return nil }
guard let latestRelayListEventId = NoteId(hex: latestRelayListEventId) else { return nil }
return delegate.ndb.lookup_note_and_copy(latestRelayListEventId)
return try? delegate.ndb.lookup_note_and_copy(latestRelayListEventId)
}
/// Gets the latest `kind:3` relay list from NostrDB.

View File

@@ -74,12 +74,12 @@ class Profiles {
profile_data(pubkey).zapper
}
func lookup_with_timestamp<T>(_ pubkey: Pubkey, borrow lendingFunction: (_: borrowing ProfileRecord?) throws -> T) rethrows -> T {
func lookup_with_timestamp<T>(_ pubkey: Pubkey, borrow lendingFunction: (_: borrowing ProfileRecord?) throws -> T) throws -> T {
return try ndb.lookup_profile(pubkey, borrow: lendingFunction)
}
func lookup_lnurl(_ pubkey: Pubkey) -> String? {
return lookup_with_timestamp(pubkey, borrow: { pr in
func lookup_lnurl(_ pubkey: Pubkey) throws -> String? {
return try lookup_with_timestamp(pubkey, borrow: { pr in
switch pr {
case .some(let pr): return pr.lnurl
case .none: return nil
@@ -87,16 +87,16 @@ class Profiles {
})
}
func lookup_by_key<T>(key: ProfileKey, borrow lendingFunction: (_: borrowing ProfileRecord?) throws -> T) rethrows -> T {
func lookup_by_key<T>(key: ProfileKey, borrow lendingFunction: (_: borrowing ProfileRecord?) throws -> T) throws -> T {
return try ndb.lookup_profile_by_key(key: key, borrow: lendingFunction)
}
func search(_ query: String, limit: Int) -> [Pubkey] {
ndb.search_profile(query, limit: limit)
func search(_ query: String, limit: Int) throws -> [Pubkey] {
try ndb.search_profile(query, limit: limit)
}
func lookup(id: Pubkey) -> Profile? {
return ndb.lookup_profile(id, borrow: { pr in
func lookup(id: Pubkey) throws -> Profile? {
return try ndb.lookup_profile(id, borrow: { pr in
switch pr {
case .none:
return nil
@@ -107,12 +107,12 @@ class Profiles {
})
}
func lookup_key_by_pubkey(_ pubkey: Pubkey) -> ProfileKey? {
ndb.lookup_profile_key(pubkey)
func lookup_key_by_pubkey(_ pubkey: Pubkey) throws -> ProfileKey? {
try ndb.lookup_profile_key(pubkey)
}
func has_fresh_profile(id: Pubkey) -> Bool {
guard let fetched_at = ndb.read_profile_last_fetched(pubkey: id)
func has_fresh_profile(id: Pubkey) throws -> Bool {
guard let fetched_at = try ndb.read_profile_last_fetched(pubkey: id)
else {
return false
}