Redesign Ndb.swift interface with build safety

This commit redesigns the Ndb.swift interface with a focus on build-time
safety against crashes.

It removes the external usage of NdbTxn and SafeNdbTxn, restricting it
to be used only in NostrDB internal code.

This prevents dangerous and crash prone usages throughout the app, such
as holding transactions in a variable in an async function (which can
cause thread-based reference counting to incorrectly deinit inherited
transactions in use by separate callers), as well as holding unsafe
unowned values longer than the lifetime of their corresponding
transactions.

Closes: https://github.com/damus-io/damus/issues/3364
Changelog-Fixed: Fixed several crashes throughout the app
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
This commit is contained in:
Daniel D’Aquino
2025-12-07 11:02:45 -08:00
parent b562b930cc
commit f844ed9931
60 changed files with 611 additions and 497 deletions
@@ -110,29 +110,29 @@ class SearchHomeModel: ObservableObject {
}
}
func find_profiles_to_fetch<Y>(profiles: Profiles, load: PubkeysToLoad, cache: EventCache, txn: NdbTxn<Y>) -> [Pubkey] {
func find_profiles_to_fetch(profiles: Profiles, load: PubkeysToLoad, cache: EventCache) -> [Pubkey] {
switch load {
case .from_events(let events):
return find_profiles_to_fetch_from_events(profiles: profiles, events: events, cache: cache, txn: txn)
return find_profiles_to_fetch_from_events(profiles: profiles, events: events, cache: cache)
case .from_keys(let pks):
return find_profiles_to_fetch_from_keys(profiles: profiles, pks: pks, txn: txn)
return find_profiles_to_fetch_from_keys(profiles: profiles, pks: pks)
}
}
func find_profiles_to_fetch_from_keys<Y>(profiles: Profiles, pks: [Pubkey], txn: NdbTxn<Y>) -> [Pubkey] {
Array(Set(pks.filter { pk in !profiles.has_fresh_profile(id: pk, txn: txn) }))
func find_profiles_to_fetch_from_keys(profiles: Profiles, pks: [Pubkey]) -> [Pubkey] {
Array(Set(pks.filter { pk in !profiles.has_fresh_profile(id: pk) }))
}
func find_profiles_to_fetch_from_events<Y>(profiles: Profiles, events: [NostrEvent], cache: EventCache, txn: NdbTxn<Y>) -> [Pubkey] {
func find_profiles_to_fetch_from_events(profiles: Profiles, events: [NostrEvent], cache: EventCache) -> [Pubkey] {
var pubkeys = Set<Pubkey>()
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, txn: txn) {
if ev.known_kind == .boost, let bev = ev.get_inner_event(cache: cache), !profiles.has_fresh_profile(id: bev.pubkey) {
pubkeys.insert(bev.pubkey)
}
if !profiles.has_fresh_profile(id: ev.pubkey, txn: txn) {
if !profiles.has_fresh_profile(id: ev.pubkey) {
pubkeys.insert(ev.pubkey)
}
}
@@ -31,17 +31,18 @@ struct PullDownSearchView: View {
}
do {
guard let txn = NdbTxn(ndb: state.ndb) else { return }
for note_key in note_keys {
guard let note = state.ndb.lookup_note_by_key_with_txn(note_key, txn: txn) else {
continue
}
if !keyset.contains(note_key) {
let owned_note = note.to_owned()
res.append(owned_note)
keyset.insert(note_key)
}
state.ndb.lookup_note_by_key(note_key, borrow: { maybeUnownedNote in
switch maybeUnownedNote {
case .none: return // Skip this
case .some(let unownedNote):
if !keyset.contains(note_key) {
let owned_note = unownedNote.toOwned()
res.append(owned_note)
keyset.insert(note_key)
}
}
})
}
}
@@ -154,17 +154,18 @@ struct SearchResultsView: View {
}
do {
guard let txn = NdbTxn(ndb: damus_state.ndb) else { return }
for note_key in note_keys {
guard let note = damus_state.ndb.lookup_note_by_key_with_txn(note_key, txn: txn) else {
continue
}
if !keyset.contains(note_key) {
let owned_note = note.to_owned()
res.append(owned_note)
keyset.insert(note_key)
}
damus_state.ndb.lookup_note_by_key(note_key, borrow: { maybeUnownedNote in
switch maybeUnownedNote {
case .none: return
case .some(let unownedNote):
if !keyset.contains(note_key) {
let owned_note = unownedNote.toOwned()
res.append(owned_note)
keyset.insert(note_key)
}
}
})
}
}
@@ -182,12 +183,10 @@ struct SearchResultsView: View {
}
.frame(maxHeight: .infinity)
.onAppear {
guard let txn = NdbTxn.init(ndb: damus_state.ndb) else { return }
self.result = search_for_string(profiles: damus_state.profiles, contacts: damus_state.contacts, search: search, txn: txn)
self.result = search_for_string(profiles: damus_state.profiles, contacts: damus_state.contacts, search: search)
}
.onChange(of: search) { new in
guard let txn = NdbTxn.init(ndb: damus_state.ndb) else { return }
self.result = search_for_string(profiles: damus_state.profiles, contacts: damus_state.contacts, search: search, txn: txn)
self.result = search_for_string(profiles: damus_state.profiles, contacts: damus_state.contacts, search: search)
}
.onChange(of: search) { query in
debouncer.debounce {
@@ -208,7 +207,7 @@ struct SearchResultsView_Previews: PreviewProvider {
*/
func search_for_string<Y>(profiles: Profiles, contacts: Contacts, search new: String, txn: NdbTxn<Y>) -> Search? {
func search_for_string(profiles: Profiles, contacts: Contacts, search new: String) -> Search? {
guard new.count != 0 else {
return nil
}
@@ -251,7 +250,7 @@ func search_for_string<Y>(profiles: Profiles, contacts: Contacts, search new: St
return .naddr(naddr)
}
let multisearch = MultiSearch(text: new, hashtag: make_hashtagable(searchQuery), profiles: search_profiles(profiles: profiles, contacts: contacts, search: new, txn: txn))
let multisearch = MultiSearch(text: new, hashtag: make_hashtagable(searchQuery), profiles: search_profiles(profiles: profiles, contacts: contacts, search: new))
return .multi(multisearch)
}
@@ -268,7 +267,7 @@ func make_hashtagable(_ str: String) -> String {
return String(new.filter{$0 != " "})
}
func search_profiles<Y>(profiles: Profiles, contacts: Contacts, search: String, txn: NdbTxn<Y>) -> [Pubkey] {
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
@@ -285,7 +284,7 @@ func search_profiles<Y>(profiles: Profiles, contacts: Contacts, search: String,
return [pk]
}
return profiles.search(search, limit: 128, txn: txn).sorted { a, b in
return 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