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-12-29 11:01:23 -08:00
parent 6d9107f662
commit 20dc672dbf
59 changed files with 790 additions and 416 deletions
@@ -120,7 +120,10 @@ func find_profiles_to_fetch(profiles: Profiles, load: PubkeysToLoad, cache: Even
}
func find_profiles_to_fetch_from_keys(profiles: Profiles, pks: [Pubkey]) -> [Pubkey] {
Array(Set(pks.filter { pk in !profiles.has_fresh_profile(id: pk) }))
Array(Set(pks.filter { pk in
let has_fresh_profile = (try? profiles.has_fresh_profile(id: pk)) ?? false
return !has_fresh_profile
}))
}
func find_profiles_to_fetch_from_events(profiles: Profiles, events: [NostrEvent], cache: EventCache) -> [Pubkey] {
@@ -128,11 +131,14 @@ func find_profiles_to_fetch_from_events(profiles: Profiles, events: [NostrEvent]
for ev in events {
// lookup profiles from boosted events
if ev.known_kind == .boost, let bev = ev.get_inner_event(cache: cache), !profiles.has_fresh_profile(id: bev.pubkey) {
if ev.known_kind == .boost,
let bev = ev.get_inner_event(cache: cache),
let has_fresh_profiles = try? profiles.has_fresh_profile(id: bev.pubkey),
!has_fresh_profiles {
pubkeys.insert(bev.pubkey)
}
if !profiles.has_fresh_profile(id: ev.pubkey) {
if let has_fresh_profiles = try? profiles.has_fresh_profile(id: ev.pubkey), !has_fresh_profiles {
pubkeys.insert(ev.pubkey)
}
}
@@ -19,7 +19,7 @@ struct PullDownSearchView: View {
func do_search(query: String) {
let limit = 128
let note_keys = state.ndb.text_search(query: query, limit: limit, order: .newest_first)
let note_keys = (try? state.ndb.text_search(query: query, limit: limit, order: .newest_first)) ?? []
var res = [NostrEvent]()
// TODO: fix duplicate results from search
var keyset = Set<NoteKey>()
@@ -32,7 +32,7 @@ struct PullDownSearchView: View {
do {
for note_key in note_keys {
state.ndb.lookup_note_by_key(note_key, borrow: { maybeUnownedNote in
try? state.ndb.lookup_note_by_key(note_key, borrow: { maybeUnownedNote in
switch maybeUnownedNote {
case .none: return // Skip this
case .some(let unownedNote):
@@ -142,7 +142,7 @@ struct SearchResultsView: View {
func do_search(query: String) {
let limit = 128
var note_keys = damus_state.ndb.text_search(query: query, limit: limit, order: .newest_first)
var note_keys = (try? damus_state.ndb.text_search(query: query, limit: limit, order: .newest_first)) ?? []
var res = [NostrEvent]()
// TODO: fix duplicate results from search
var keyset = Set<NoteKey>()
@@ -155,7 +155,7 @@ struct SearchResultsView: View {
do {
for note_key in note_keys {
damus_state.ndb.lookup_note_by_key(note_key, borrow: { maybeUnownedNote in
try? damus_state.ndb.lookup_note_by_key(note_key, borrow: { maybeUnownedNote in
switch maybeUnownedNote {
case .none: return
case .some(let unownedNote):
@@ -270,7 +270,7 @@ func make_hashtagable(_ str: String) -> String {
func search_profiles(profiles: Profiles, contacts: Contacts, search: String) -> [Pubkey] {
// Search by hex pubkey.
if let pubkey = hex_decode_pubkey(search),
profiles.lookup_key_by_pubkey(pubkey) != nil
(try? profiles.lookup_key_by_pubkey(pubkey)) != nil
{
return [pubkey]
}
@@ -279,12 +279,12 @@ func search_profiles(profiles: Profiles, contacts: Contacts, search: String) ->
if search.starts(with: "npub"),
let bech32_key = decode_bech32_key(search),
case Bech32Key.pub(let pk) = bech32_key,
profiles.lookup_key_by_pubkey(pk) != nil
(try? profiles.lookup_key_by_pubkey(pk)) != nil
{
return [pk]
}
return profiles.search(search, limit: 128).sorted { a, b in
return (try? profiles.search(search, limit: 128).sorted { a, b in
let aFriendTypePriority = get_friend_type(contacts: contacts, pubkey: a)?.priority ?? 0
let bFriendTypePriority = get_friend_type(contacts: contacts, pubkey: b)?.priority ?? 0
@@ -294,5 +294,5 @@ func search_profiles(profiles: Profiles, contacts: Contacts, search: String) ->
} else {
return false
}
}
}) ?? []
}
@@ -47,7 +47,7 @@ struct SearchingEventView: View {
switch search {
case .nip05(let nip05):
if let pk = state.profiles.nip05_pubkey[nip05] {
if state.profiles.lookup_key_by_pubkey(pk) != nil {
if (try? state.profiles.lookup_key_by_pubkey(pk)) != nil {
self.search_state = .found_profile(pk)
}
} else {