damus

nostr ios client
git clone git://jb55.com/damus
Log | Files | Refs | README | LICENSE

commit bfad2ab42d47ea91e042ee458a716051a243ff8b
parent 227734d286aa570ba72d5e6633e4cbb03ad0bed9
Author: William Casarin <jb55@jb55.com>
Date:   Tue, 12 Dec 2023 12:45:56 -0800

ndb/txn: make transactions failable

Since there may be situations where we close and re-open the database,
we need to make sure transactions fail when the database is not open.

Make NdbTxn an init?() constructor and check for ndb.closed. If it's
closed, then fail transaction construction.

This fixes crashes during high database activity when switching from
background to foreground and vice-versa.

Fixes: da2bdad18d1d ("nostrdb: close database when backgrounded")

Diffstat:
MDamusNotificationService/NotificationFormatter.swift | 6+++---
MDamusNotificationService/NotificationService.swift | 2+-
Mdamus/Components/NIP05Badge.swift | 2+-
Mdamus/ContentView.swift | 33++++++++++++++-------------------
Mdamus/Models/EventsModel.swift | 4+++-
Mdamus/Models/FollowersModel.swift | 2+-
Mdamus/Models/HomeModel.swift | 6++++--
Mdamus/Models/NoteContent.swift | 2+-
Mdamus/Models/NotificationsManager.swift | 17++++++++++-------
Mdamus/Models/ProfileModel.swift | 2+-
Mdamus/Models/SearchHomeModel.swift | 2+-
Mdamus/Models/SearchModel.swift | 2+-
Mdamus/Models/ThreadModel.swift | 2+-
Mdamus/Models/ZapsModel.swift | 2+-
Mdamus/Nostr/Profiles.swift | 19+++++++++++--------
Mdamus/Views/ActionBar/EventActionBar.swift | 4+++-
Mdamus/Views/BannerImageView.swift | 7++++---
Mdamus/Views/Events/Components/ReplyDescription.swift | 4+++-
Mdamus/Views/FollowingView.swift | 2+-
Mdamus/Views/NoteContentView.swift | 2+-
Mdamus/Views/Notifications/NotificationsView.swift | 2+-
Mdamus/Views/Onboarding/SuggestedUsersViewModel.swift | 2+-
Mdamus/Views/PostView.swift | 2+-
Mdamus/Views/Posting/UserSearch.swift | 4++--
Mdamus/Views/Profile/EditMetadataView.swift | 3++-
Mdamus/Views/Profile/EventProfileName.swift | 6+++---
Mdamus/Views/Profile/ProfileName.swift | 2+-
Mdamus/Views/Profile/ProfileNameView.swift | 2+-
Mdamus/Views/Profile/ProfilePicView.swift | 6+++---
Mdamus/Views/Profile/ProfilePictureSelector.swift | 2+-
Mdamus/Views/Profile/ProfileView.swift | 10+++++-----
Mdamus/Views/ProfileActionSheetView.swift | 2+-
Mdamus/Views/QRCodeView.swift | 7++++---
Mdamus/Views/ReplyView.swift | 3+--
Mdamus/Views/Search/PullDownSearch.swift | 2+-
Mdamus/Views/SearchResultsView.swift | 4++--
Mdamus/Views/SideMenuView.swift | 2+-
Mdamus/Views/Wallet/WalletView.swift | 4++--
Mdamus/Views/Zaps/ProfileZapLinkView.swift | 2+-
Mdamus/Views/Zaps/ZapTypePicker.swift | 2+-
Mnostrdb/Ndb.swift | 31+++++++++++++++++++++----------
Mnostrdb/NdbTxn.swift | 11++++-------
42 files changed, 126 insertions(+), 107 deletions(-)

diff --git a/DamusNotificationService/NotificationFormatter.swift b/DamusNotificationService/NotificationFormatter.swift @@ -118,9 +118,9 @@ struct NotificationFormatter { let src = zap.request.ev let pk = zap.is_anon ? ANON_PUBKEY : src.pubkey - let name = profiles.lookup(id: pk).map { profile in - Profile.displayName(profile: profile, pubkey: pk).displayName.truncate(maxLength: 50) - }.value + let profile_txn = profiles.lookup(id: pk) + let profile = profile_txn?.unsafeUnownedValue + let name = Profile.displayName(profile: profile, pubkey: pk).displayName.truncate(maxLength: 50) let sats = NSNumber(value: (Double(zap.invoice.amount) / 1000.0)) let formattedSats = format_msats_abbrev(zap.invoice.amount) diff --git a/DamusNotificationService/NotificationService.swift b/DamusNotificationService/NotificationService.swift @@ -28,7 +28,7 @@ class NotificationService: UNNotificationServiceExtension { Log.debug("Got nostr event push notification from pubkey %s", for: .push_notifications, nostr_event.pubkey.hex()) guard let state = NotificationExtensionState(), - let display_name = state.ndb.lookup_profile(nostr_event.pubkey).unsafeUnownedValue?.profile?.display_name // We are not holding the txn here. + let display_name = state.ndb.lookup_profile(nostr_event.pubkey)?.unsafeUnownedValue?.profile?.display_name // We are not holding the txn here. else { // Something failed to initialize so let's go for the next best thing guard let improved_content = NotificationFormatter.shared.format_message(event: nostr_event) else { diff --git a/damus/Components/NIP05Badge.swift b/damus/Components/NIP05Badge.swift @@ -45,7 +45,7 @@ struct NIP05Badge: View { } var username_matches_nip05: Bool { - guard let name = profiles.lookup(id: pubkey).map({ p in p?.name }).value + guard let name = profiles.lookup(id: pubkey)?.map({ p in p?.name }).value else { return false } diff --git a/damus/ContentView.swift b/damus/ContentView.swift @@ -370,14 +370,9 @@ struct ContentView: View { // wallet with an associated guard let ds = self.damus_state, let lud16 = nwc.lud16, - let keypair = ds.keypair.to_full() - else { - return - } - - let profile_txn = ds.profiles.lookup(id: ds.pubkey) - - guard let profile = profile_txn.unsafeUnownedValue, + let keypair = ds.keypair.to_full(), + let profile_txn = ds.profiles.lookup(id: ds.pubkey), + let profile = profile_txn.unsafeUnownedValue, lud16 != profile.lud16 else { return } @@ -514,10 +509,9 @@ struct ContentView: View { .onReceive(handle_notify(.onlyzaps_mode)) { hide in home.filter_events() - guard let ds = damus_state else { return } - let profile_txn = ds.profiles.lookup(id: ds.pubkey) - - guard let profile = profile_txn.unsafeUnownedValue, + guard let ds = damus_state, + let profile_txn = ds.profiles.lookup(id: ds.pubkey), + let profile = profile_txn.unsafeUnownedValue, let keypair = ds.keypair.to_full() else { return @@ -534,9 +528,9 @@ struct ContentView: View { } }, message: { if let pubkey = self.muting { - let name = damus_state!.profiles.lookup(id: pubkey).map { profile in - Profile.displayName(profile: profile, pubkey: pubkey).username.truncate(maxLength: 50) - }.value + let profile_txn = damus_state!.profiles.lookup(id: pubkey) + let profile = profile_txn?.unsafeUnownedValue + let name = Profile.displayName(profile: profile, pubkey: pubkey).username.truncate(maxLength: 50) Text("\(name) has been muted", comment: "Alert message that informs a user was muted.") } else { Text("User has been muted", comment: "Alert message that informs a user was muted.") @@ -595,9 +589,9 @@ struct ContentView: View { } }, message: { if let pubkey = muting { - let name = damus_state?.profiles.lookup(id: pubkey).map({ profile in - Profile.displayName(profile: profile, pubkey: pubkey).username.truncate(maxLength: 50) - }).value ?? "unknown" + let profile_txn = damus_state?.profiles.lookup(id: pubkey) + let profile = profile_txn?.unsafeUnownedValue + let name = Profile.displayName(profile: profile, pubkey: pubkey).username.truncate(maxLength: 50) Text("Mute \(name)?", comment: "Alert message prompt to ask if a user should be muted.") } else { Text("Could not find user to mute...", comment: "Alert message to indicate that the muted user could not be found.") @@ -865,7 +859,8 @@ func find_event_with_subid(state: DamusState, query query_: FindEvent, subid: St switch query { case .profile(let pubkey): - if let record = state.ndb.lookup_profile(pubkey).unsafeUnownedValue, + if let profile_txn = state.ndb.lookup_profile(pubkey), + let record = profile_txn.unsafeUnownedValue, record.profile != nil { callback(.profile(pubkey)) diff --git a/damus/Models/EventsModel.swift b/damus/Models/EventsModel.swift @@ -66,7 +66,9 @@ class EventsModel: ObservableObject { case .auth: break case .eose: - let txn = NdbTxn(ndb: self.state.ndb) + guard let txn = NdbTxn(ndb: self.state.ndb) else { + return + } load_profiles(context: "events_model", profiles_subid: profiles_id, relay_id: relay_id, load: .from_events(events), damus_state: state, txn: txn) } } diff --git a/damus/Models/FollowersModel.swift b/damus/Models/FollowersModel.swift @@ -83,7 +83,7 @@ class FollowersModel: ObservableObject { case .eose(let sub_id): if sub_id == self.sub_id { - let txn = NdbTxn(ndb: self.damus_state.ndb) + guard let txn = NdbTxn(ndb: self.damus_state.ndb) else { return } load_profiles(relay_id: relay_id, txn: txn) } else if sub_id == self.profiles_id { damus_state.pool.unsubscribe(sub_id: profiles_id, to: [relay_id]) diff --git a/damus/Models/HomeModel.swift b/damus/Models/HomeModel.swift @@ -414,8 +414,10 @@ class HomeModel { print(msg) case .eose(let sub_id): - - let txn = NdbTxn(ndb: damus_state.ndb) + guard let txn = NdbTxn(ndb: damus_state.ndb) else { + return + } + if sub_id == dms_subid { var dms = dms.dms.flatMap { $0.events } dms.append(contentsOf: incoming_dms) diff --git a/damus/Models/NoteContent.swift b/damus/Models/NoteContent.swift @@ -187,7 +187,7 @@ func mention_str(_ m: Mention<MentionRef>, profiles: Profiles) -> CompatibleText case .pubkey(let pk): let npub = bech32_pubkey(pk) let profile_txn = profiles.lookup(id: pk) - let profile = profile_txn.unsafeUnownedValue + let profile = profile_txn?.unsafeUnownedValue let disp = Profile.displayName(profile: profile, pubkey: pk).username.truncate(maxLength: 50) var attributedString = AttributedString(stringLiteral: "@\(disp)") attributedString.link = URL(string: "damus:nostr:\(npub)") diff --git a/damus/Models/NotificationsManager.swift b/damus/Models/NotificationsManager.swift @@ -21,6 +21,7 @@ func process_local_notification(state: HeadlessDamusState, event ev: NostrEvent) guard let local_notification = generate_local_notification_object(from: ev, state: state) else { return } + create_local_notification(profiles: state.profiles, notify: local_notification) } @@ -76,7 +77,8 @@ func generate_local_notification_object(from ev: NostrEvent, state: HeadlessDamu } else if type == .like, state.settings.like_notification, let evid = ev.referenced_ids.last, - let liked_event = state.ndb.lookup_note(evid).unsafeUnownedValue // We are only accessing it temporarily to generate notification content + let txn = state.ndb.lookup_note(evid), + let liked_event = txn.unsafeUnownedValue // We are only accessing it temporarily to generate notification content { let content_preview = render_notification_content_preview(ev: liked_event, profiles: state.profiles, keypair: state.keypair) return LocalNotification(type: .like, event: ev, target: liked_event, content: content_preview) @@ -135,9 +137,9 @@ func render_notification_content_preview(ev: NostrEvent, profiles: Profiles, key } func event_author_name(profiles: Profiles, pubkey: Pubkey) -> String { - return profiles.lookup(id: pubkey).map({ profile in - Profile.displayName(profile: profile, pubkey: pubkey).username.truncate(maxLength: 50) - }).value + let profile_txn = profiles.lookup(id: pubkey) + let profile = profile_txn?.unsafeUnownedValue + return Profile.displayName(profile: profile, pubkey: pubkey).username.truncate(maxLength: 50) } @MainActor @@ -173,8 +175,8 @@ func process_zap_event(state: HeadlessDamusState, ev: NostrEvent, completion: @e return } - guard let lnurl = state.profiles.lookup_with_timestamp(ptag) - .map({ pr in pr?.lnurl }).value else { + guard let txn = state.profiles.lookup_with_timestamp(ptag), + let lnurl = txn.map({ pr in pr?.lnurl }).value else { completion(.failed) return } @@ -221,7 +223,8 @@ func get_zap_target_pubkey(ev: NostrEvent, ndb: Ndb) -> Pubkey? { } // we can't trust the p tag on note zaps because they can be faked - guard let pk = ndb.lookup_note(etag).unsafeUnownedValue?.pubkey else { + guard let txn = ndb.lookup_note(etag), + let pk = txn.unsafeUnownedValue?.pubkey else { // We don't have the event in cache so we can't check the pubkey. // We could return this as an invalid zap but that wouldn't be correct diff --git a/damus/Models/ProfileModel.swift b/damus/Models/ProfileModel.swift @@ -128,7 +128,7 @@ class ProfileModel: ObservableObject, Equatable { break //notify(.notice, notice) case .eose: - let txn = NdbTxn(ndb: damus.ndb) + guard let txn = NdbTxn(ndb: damus.ndb) else { return } if resp.subid == sub_id { load_profiles(context: "profile", profiles_subid: prof_subid, relay_id: relay_id, load: .from_events(events.events), damus_state: damus, txn: txn) } diff --git a/damus/Models/SearchHomeModel.swift b/damus/Models/SearchHomeModel.swift @@ -83,7 +83,7 @@ class SearchHomeModel: ObservableObject { // global events are not realtime unsubscribe(to: relay_id) - let txn = NdbTxn(ndb: damus_state.ndb) + guard let txn = NdbTxn(ndb: damus_state.ndb) else { return } load_profiles(context: "universe", profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(events.all_events), damus_state: damus_state, txn: txn) } diff --git a/damus/Models/SearchModel.swift b/damus/Models/SearchModel.swift @@ -80,7 +80,7 @@ class SearchModel: ObservableObject { self.loading = false if sub_id == self.sub_id { - let txn = NdbTxn(ndb: state.ndb) + guard let txn = NdbTxn(ndb: state.ndb) else { return } load_profiles(context: "search", profiles_subid: self.profiles_subid, relay_id: relay_id, load: .from_events(self.events.all_events), damus_state: state, txn: txn) } } diff --git a/damus/Models/ThreadModel.swift b/damus/Models/ThreadModel.swift @@ -120,7 +120,7 @@ class ThreadModel: ObservableObject { } if sub_id == self.base_subid { - let txn = NdbTxn(ndb: damus_state.ndb) + guard let txn = NdbTxn(ndb: damus_state.ndb) else { return } load_profiles(context: "thread", profiles_subid: self.profiles_subid, relay_id: relay_id, load: .from_events(Array(event_map)), damus_state: damus_state, txn: txn) } } diff --git a/damus/Models/ZapsModel.swift b/damus/Models/ZapsModel.swift @@ -55,7 +55,7 @@ class ZapsModel: ObservableObject { break case .eose: let events = state.events.lookup_zaps(target: target).map { $0.request.ev } - let txn = NdbTxn(ndb: state.ndb) + guard let txn = NdbTxn(ndb: state.ndb) else { return } load_profiles(context: "zaps_model", profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(events), damus_state: state, txn: txn) case .event(_, let ev): guard ev.kind == 9735, diff --git a/damus/Nostr/Profiles.swift b/damus/Nostr/Profiles.swift @@ -73,24 +73,27 @@ class Profiles { profile_data(pubkey).zapper } - func lookup_with_timestamp(_ pubkey: Pubkey) -> NdbTxn<ProfileRecord?> { - return ndb.lookup_profile(pubkey) + func lookup_with_timestamp(_ pubkey: Pubkey) -> NdbTxn<ProfileRecord?>? { + ndb.lookup_profile(pubkey) } - func lookup_by_key(key: ProfileKey) -> NdbTxn<ProfileRecord?> { - return ndb.lookup_profile_by_key(key: key) + func lookup_by_key(key: ProfileKey) -> NdbTxn<ProfileRecord?>? { + ndb.lookup_profile_by_key(key: key) } func search<Y>(_ query: String, limit: Int, txn: NdbTxn<Y>) -> [Pubkey] { - return ndb.search_profile(query, limit: limit, txn: txn) + ndb.search_profile(query, limit: limit, txn: txn) } - func lookup(id: Pubkey) -> NdbTxn<Profile?> { - return ndb.lookup_profile(id).map({ pr in pr?.profile }) + func lookup(id: Pubkey) -> NdbTxn<Profile?>? { + guard let txn = ndb.lookup_profile(id) else { + return nil + } + return txn.map({ pr in pr?.profile }) } func lookup_key_by_pubkey(_ pubkey: Pubkey) -> ProfileKey? { - return ndb.lookup_profile_key(pubkey) + ndb.lookup_profile_key(pubkey) } func has_fresh_profile<Y>(id: Pubkey, txn: NdbTxn<Y>) -> Bool { diff --git a/damus/Views/ActionBar/EventActionBar.swift b/damus/Views/ActionBar/EventActionBar.swift @@ -28,7 +28,9 @@ struct EventActionBar: View { } var lnurl: String? { - damus_state.profiles.lookup_with_timestamp(event.pubkey).map({ pr in pr?.lnurl }).value + damus_state.profiles.lookup_with_timestamp(event.pubkey)?.map({ pr in + pr?.lnurl + }).value } var show_like: Bool { diff --git a/damus/Views/BannerImageView.swift b/damus/Views/BannerImageView.swift @@ -78,11 +78,12 @@ struct BannerImageView: View { var body: some View { InnerBannerImageView(disable_animation: disable_animation, url: get_banner_url(banner: banner, pubkey: pubkey, profiles: profiles)) .onReceive(handle_notify(.profile_updated)) { updated in - guard updated.pubkey == self.pubkey else { + guard updated.pubkey == self.pubkey, + let profile_txn = profiles.lookup(id: updated.pubkey) + else { return } - let profile_txn = profiles.lookup(id: updated.pubkey) let profile = profile_txn.unsafeUnownedValue if let bannerImage = profile?.banner, bannerImage != self.banner { self.banner = bannerImage @@ -92,7 +93,7 @@ struct BannerImageView: View { } func get_banner_url(banner: String?, pubkey: Pubkey, profiles: Profiles) -> URL? { - let bannerUrlString = banner ?? profiles.lookup(id: pubkey).map({ p in p?.banner }).value ?? "" + let bannerUrlString = banner ?? profiles.lookup(id: pubkey)?.map({ p in p?.banner }).value ?? "" if let url = URL(string: bannerUrlString) { return url } diff --git a/damus/Views/Events/Components/ReplyDescription.swift b/damus/Views/Events/Components/ReplyDescription.swift @@ -38,7 +38,9 @@ func reply_desc(ndb: Ndb, event: NostrEvent, replying_to: NostrEvent?, locale: L return NSLocalizedString("Replying to self", bundle: bundle, comment: "Label to indicate that the user is replying to themself.") } - let profile_txn = NdbTxn(ndb: ndb) + guard let profile_txn = NdbTxn(ndb: ndb) else { + return "" + } let names: [String] = pubkeys.map { pk in let prof = ndb.lookup_profile_with_txn(pk, txn: profile_txn) diff --git a/damus/Views/FollowingView.swift b/damus/Views/FollowingView.swift @@ -151,7 +151,7 @@ struct FollowingView: View { } .tabViewStyle(.page(indexDisplayMode: .never)) .onAppear { - let txn = NdbTxn(ndb: self.damus_state.ndb) + guard let txn = NdbTxn(ndb: self.damus_state.ndb) else { return } following.subscribe(txn: txn) } .onDisappear { diff --git a/damus/Views/NoteContentView.swift b/damus/Views/NoteContentView.swift @@ -303,7 +303,7 @@ struct NoteContentView: View { class NoteArtifactsParts { var parts: [ArtifactPart] var words: Int - + init(parts: [ArtifactPart], words: Int) { self.parts = parts self.words = words diff --git a/damus/Views/Notifications/NotificationsView.swift b/damus/Views/Notifications/NotificationsView.swift @@ -61,7 +61,7 @@ struct NotificationsView: View { var mystery: some View { let profile_txn = state.profiles.lookup(id: state.pubkey) - let profile = profile_txn.unsafeUnownedValue + let profile = profile_txn?.unsafeUnownedValue return VStack(spacing: 20) { Text("Wake up, \(Profile.displayName(profile: profile, pubkey: state.pubkey).displayName.truncate(maxLength: 50))", comment: "Text telling the user to wake up, where the argument is their display name.") Text("You are dreaming...", comment: "Text telling the user that they are dreaming.") diff --git a/damus/Views/Onboarding/SuggestedUsersViewModel.swift b/damus/Views/Onboarding/SuggestedUsersViewModel.swift @@ -36,7 +36,7 @@ class SuggestedUsersViewModel: ObservableObject { func suggestedUser(pubkey: Pubkey) -> SuggestedUser? { let profile_txn = damus_state.profiles.lookup(id: pubkey) - if let profile = profile_txn.unsafeUnownedValue, + if let profile = profile_txn?.unsafeUnownedValue, let user = SuggestedUser(name: profile.name, about: profile.about, picture: profile.picture, pubkey: pubkey) { return user } diff --git a/damus/Views/PostView.swift b/damus/Views/PostView.swift @@ -181,7 +181,7 @@ struct PostView: View { } let profile_txn = damus_state.profiles.lookup(id: pubkey) - let profile = profile_txn.unsafeUnownedValue + let profile = profile_txn?.unsafeUnownedValue return user_tag_attr_string(profile: profile, pubkey: pubkey) } diff --git a/damus/Views/Posting/UserSearch.swift b/damus/Views/Posting/UserSearch.swift @@ -17,7 +17,7 @@ struct UserSearch: View { @EnvironmentObject var tagModel: TagModel var users: [Pubkey] { - let txn = NdbTxn(ndb: damus_state.ndb) + guard let txn = NdbTxn(ndb: damus_state.ndb) else { return [] } return search_profiles(profiles: damus_state.profiles, search: search, txn: txn).sorted { a, b in let aFriendTypePriority = get_friend_type(contacts: damus_state.contacts, pubkey: a)?.priority ?? 0 let bFriendTypePriority = get_friend_type(contacts: damus_state.contacts, pubkey: b)?.priority ?? 0 @@ -33,7 +33,7 @@ struct UserSearch: View { func on_user_tapped(pk: Pubkey) { let profile_txn = damus_state.profiles.lookup(id: pk) - let profile = profile_txn.unsafeUnownedValue + let profile = profile_txn?.unsafeUnownedValue let user_tag = user_tag_attr_string(profile: profile, pubkey: pk) appendUserTag(withTag: user_tag) diff --git a/damus/Views/Profile/EditMetadataView.swift b/damus/Views/Profile/EditMetadataView.swift @@ -30,7 +30,8 @@ struct EditMetadataView: View { init(damus_state: DamusState) { self.damus_state = damus_state - let data = damus_state.profiles.lookup(id: damus_state.pubkey).unsafeUnownedValue + let profile_txn = damus_state.profiles.lookup(id: damus_state.pubkey) + let data = profile_txn?.unsafeUnownedValue _name = State(initialValue: data?.name ?? "") _display_name = State(initialValue: data?.display_name ?? "") diff --git a/damus/Views/Profile/EventProfileName.swift b/damus/Views/Profile/EventProfileName.swift @@ -24,7 +24,7 @@ struct EventProfileName: View { self.damus_state = damus self.pubkey = pubkey self.size = size - let donation = damus.ndb.lookup_profile(pubkey).map({ p in p?.profile?.damus_donation }).value + let donation = damus.ndb.lookup_profile(pubkey)?.map({ p in p?.profile?.damus_donation }).value self._donation = State(wrappedValue: donation) is_purple_user = nil } @@ -65,7 +65,7 @@ struct EventProfileName: View { var body: some View { let profile_txn = damus_state.profiles.lookup(id: pubkey) - let profile = profile_txn.unsafeUnownedValue + let profile = profile_txn?.unsafeUnownedValue HStack(spacing: 2) { switch current_display_name(profile) { case .one(let one): @@ -109,7 +109,7 @@ struct EventProfileName: View { } let profile_txn = damus_state.profiles.lookup(id: update.pubkey) - guard let profile = profile_txn.unsafeUnownedValue else { return } + guard let profile = profile_txn?.unsafeUnownedValue else { return } let display_name = Profile.displayName(profile: profile, pubkey: pubkey) if display_name != self.display_name { diff --git a/damus/Views/Profile/ProfileName.swift b/damus/Views/Profile/ProfileName.swift @@ -87,7 +87,7 @@ struct ProfileName: View { var body: some View { let profile_txn = damus_state.profiles.lookup(id: pubkey) - let profile = profile_txn.unsafeUnownedValue + let profile = profile_txn?.unsafeUnownedValue HStack(spacing: 2) { Text(verbatim: "\(prefix)\(name_choice(profile: profile))") diff --git a/damus/Views/Profile/ProfileNameView.swift b/damus/Views/Profile/ProfileNameView.swift @@ -17,7 +17,7 @@ struct ProfileNameView: View { Group { VStack(alignment: .leading) { let profile_txn = self.damus.profiles.lookup(id: pubkey) - let profile = profile_txn.unsafeUnownedValue + let profile = profile_txn?.unsafeUnownedValue switch Profile.displayName(profile: profile, pubkey: pubkey) { case .one: diff --git a/damus/Views/Profile/ProfilePicView.swift b/damus/Views/Profile/ProfilePicView.swift @@ -84,7 +84,7 @@ struct ProfilePicView: View { } func get_lnurl() -> String? { - return profiles.lookup_with_timestamp(pubkey).unsafeUnownedValue?.lnurl + return profiles.lookup_with_timestamp(pubkey)?.unsafeUnownedValue?.lnurl } var body: some View { @@ -102,7 +102,7 @@ struct ProfilePicView: View { } case .remote(pubkey: let pk): let profile_txn = profiles.lookup(id: pk) - let profile = profile_txn.unsafeUnownedValue + let profile = profile_txn?.unsafeUnownedValue if let pic = profile?.picture { self.picture = pic } @@ -126,7 +126,7 @@ struct ProfilePicView: View { } func get_profile_url(picture: String?, pubkey: Pubkey, profiles: Profiles) -> URL { - let pic = picture ?? profiles.lookup(id: pubkey).map({ $0?.picture }).value ?? robohash(pubkey) + let pic = picture ?? profiles.lookup(id: pubkey)?.map({ $0?.picture }).value ?? robohash(pubkey) if let url = URL(string: pic) { return url } diff --git a/damus/Views/Profile/ProfilePictureSelector.swift b/damus/Views/Profile/ProfilePictureSelector.swift @@ -43,7 +43,7 @@ struct EditProfilePictureView: View { if let profile_url { return profile_url } else if let state = damus_state, - let picture = state.profiles.lookup(id: pubkey).map({ pr in pr?.picture }).value { + let picture = state.profiles.lookup(id: pubkey)?.map({ pr in pr?.picture }).value { return URL(string: picture) } else { return profile_url ?? URL(string: robohash(pubkey)) diff --git a/damus/Views/Profile/ProfileView.swift b/damus/Views/Profile/ProfileView.swift @@ -24,10 +24,10 @@ func follow_btn_txt(_ fs: FollowState, follows_you: Bool) -> String { } } -func followedByString<Y>(txn: NdbTxn<Y>, _ friend_intersection: [Pubkey], ndb: Ndb, locale: Locale = Locale.current) -> String { +func followedByString(_ friend_intersection: [Pubkey], ndb: Ndb, locale: Locale = Locale.current) -> String { let bundle = bundleForLocale(locale: locale) let names: [String] = friend_intersection.prefix(3).map { pk in - let profile = ndb.lookup_profile_with_txn(pk, txn: txn)?.profile + let profile = ndb.lookup_profile(pk)?.unsafeUnownedValue?.profile return Profile.displayName(profile: profile, pubkey: pk).username.truncate(maxLength: 20) } @@ -331,7 +331,7 @@ struct ProfileView: View { var aboutSection: some View { VStack(alignment: .leading, spacing: 8.0) { let profile_txn = damus_state.profiles.lookup_with_timestamp(profile.pubkey) - let profile_data = profile_txn.unsafeUnownedValue + let profile_data = profile_txn?.unsafeUnownedValue nameSection(profile_data: profile_data) @@ -398,7 +398,7 @@ struct ProfileView: View { NavigationLink(value: Route.FollowersYouKnow(friendedFollowers: friended_followers, followers: followers)) { HStack { CondensedProfilePicturesView(state: damus_state, pubkeys: friended_followers, maxPictures: 3) - let followedByString = followedByString(txn: profile_txn, friended_followers, ndb: damus_state.ndb) + let followedByString = followedByString(friended_followers, ndb: damus_state.ndb) Text(followedByString) .font(.subheadline).foregroundColor(.gray) .multilineTextAlignment(.leading) @@ -499,7 +499,7 @@ extension View { func check_nip05_validity(pubkey: Pubkey, profiles: Profiles) { let profile_txn = profiles.lookup(id: pubkey) - guard let profile = profile_txn.unsafeUnownedValue, + guard let profile = profile_txn?.unsafeUnownedValue, let nip05 = profile.nip05, profiles.is_validated(pubkey) == nil else { diff --git a/damus/Views/ProfileActionSheetView.swift b/damus/Views/ProfileActionSheetView.swift @@ -30,7 +30,7 @@ struct ProfileActionSheetView: View { func profile_data() -> ProfileRecord? { let profile_txn = damus_state.profiles.lookup_with_timestamp(profile.pubkey) - return profile_txn.unsafeUnownedValue + return profile_txn?.unsafeUnownedValue } func get_profile() -> Profile? { diff --git a/damus/Views/QRCodeView.swift b/damus/Views/QRCodeView.swift @@ -119,10 +119,11 @@ struct QRCodeView: View { var QRView: some View { VStack(alignment: .center) { let profile_txn = damus_state.profiles.lookup(id: pubkey) - let profile = profile_txn.unsafeUnownedValue - let our_profile = damus_state.ndb.lookup_profile_with_txn(damus_state.pubkey, txn: profile_txn) + let profile = profile_txn?.unsafeUnownedValue + let our_profile_txn = damus_state.profiles.lookup(id: damus_state.pubkey) + let our_profile = our_profile_txn?.unsafeUnownedValue - if our_profile?.profile?.picture != nil { + if our_profile?.picture != nil { ProfilePicView(pubkey: pubkey, size: 90.0, highlight: .custom(DamusColors.white, 3.0), profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation) .padding(.top, 50) } else { diff --git a/damus/Views/ReplyView.swift b/damus/Views/ReplyView.swift @@ -24,11 +24,10 @@ struct ReplyView: View { var ReplyingToSection: some View { HStack { Group { - let txn = NdbTxn(ndb: damus.ndb) let names = references .map { pubkey in let pk = pubkey - let prof = damus.ndb.lookup_profile_with_txn(pk, txn: txn)?.profile + let prof = damus.ndb.lookup_profile(pk)?.unsafeUnownedValue?.profile return "@" + Profile.displayName(profile: prof, pubkey: pk).username.truncate(maxLength: 50) } .joined(separator: " ") diff --git a/damus/Views/Search/PullDownSearch.swift b/damus/Views/Search/PullDownSearch.swift @@ -31,7 +31,7 @@ struct PullDownSearchView: View { } do { - let txn = NdbTxn(ndb: state.ndb) + 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 diff --git a/damus/Views/SearchResultsView.swift b/damus/Views/SearchResultsView.swift @@ -108,11 +108,11 @@ struct SearchResultsView: View { } .frame(maxHeight: .infinity) .onAppear { - let txn = NdbTxn.init(ndb: damus_state.ndb) + guard let txn = NdbTxn.init(ndb: damus_state.ndb) else { return } self.result = search_for_string(profiles: damus_state.profiles, search: search, txn: txn) } .onChange(of: search) { new in - let txn = NdbTxn.init(ndb: damus_state.ndb) + guard let txn = NdbTxn.init(ndb: damus_state.ndb) else { return } self.result = search_for_string(profiles: damus_state.profiles, search: search, txn: txn) } } diff --git a/damus/Views/SideMenuView.swift b/damus/Views/SideMenuView.swift @@ -90,7 +90,7 @@ struct SideMenuView: View { var TopProfile: some View { let profile_txn = damus_state.profiles.lookup(id: damus_state.pubkey) - let profile = profile_txn.unsafeUnownedValue + let profile = profile_txn?.unsafeUnownedValue return VStack(alignment: .leading, spacing: verticalSpacing) { HStack { ProfilePicView(pubkey: damus_state.pubkey, size: 60, highlight: .none, profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation) diff --git a/damus/Views/Wallet/WalletView.swift b/damus/Views/Wallet/WalletView.swift @@ -165,7 +165,7 @@ struct WalletView: View { } .onChange(of: settings.donation_percent) { p in let profile_txn = damus_state.profiles.lookup(id: damus_state.pubkey) - guard let profile = profile_txn.unsafeUnownedValue else { + guard let profile = profile_txn?.unsafeUnownedValue else { return } @@ -177,7 +177,7 @@ struct WalletView: View { let profile_txn = damus_state.profiles.lookup(id: damus_state.pubkey) guard let keypair = damus_state.keypair.to_full(), - let profile = profile_txn.unsafeUnownedValue, + let profile = profile_txn?.unsafeUnownedValue, model.initial_percent != profile.damus_donation else { return diff --git a/damus/Views/Zaps/ProfileZapLinkView.swift b/damus/Views/Zaps/ProfileZapLinkView.swift @@ -34,7 +34,7 @@ struct ProfileZapLinkView<Content: View>: View { self.action = action let profile_txn = damus_state.profiles.lookup_with_timestamp(pubkey) - let record = profile_txn.unsafeUnownedValue + let record = profile_txn?.unsafeUnownedValue self.reactions_enabled = record?.profile?.reactions ?? true self.lud16 = record?.profile?.lud06 self.lnurl = record?.lnurl diff --git a/damus/Views/Zaps/ZapTypePicker.swift b/damus/Views/Zaps/ZapTypePicker.swift @@ -98,7 +98,7 @@ func zap_type_desc(type: ZapType, profiles: Profiles, pubkey: Pubkey) -> String return NSLocalizedString("No one will see that you zapped", comment: "Description of anonymous zap type where the zap is sent anonymously and does not identify the user who sent it.") case .priv: let prof_txn = profiles.lookup(id: pubkey) - let prof = prof_txn.unsafeUnownedValue + let prof = prof_txn?.unsafeUnownedValue let name = Profile.displayName(profile: prof, pubkey: pubkey).username.truncate(maxLength: 50) return String.localizedStringWithFormat(NSLocalizedString("private_zap_description", value: "Only '%@' will see that you zapped them", comment: "Description of private zap type where the zap is sent privately and does not identify the user to the public."), name) case .non_zap: diff --git a/nostrdb/Ndb.swift b/nostrdb/Ndb.swift @@ -186,6 +186,7 @@ class Ndb { throw DatabaseError.failed_open } + self.closed = false self.ndb = db } @@ -198,7 +199,7 @@ class Ndb { } func text_search(query: String, limit: Int = 32, order: NdbSearchOrder = .newest_first) -> [NoteKey] { - let txn = NdbTxn(ndb: self) + guard let txn = NdbTxn(ndb: self) else { return [] } var results = ndb_text_search_results() let res = query.withCString { q in let order = order == .newest_first ? NDB_ORDER_DESCENDING : NDB_ORDER_ASCENDING @@ -243,7 +244,7 @@ class Ndb { return note_ids } - func lookup_note_by_key(_ key: NoteKey) -> NdbTxn<NdbNote?> { + func lookup_note_by_key(_ key: NoteKey) -> NdbTxn<NdbNote?>? { return NdbTxn(ndb: self) { txn in lookup_note_by_key_with_txn(key, txn: txn) } @@ -301,7 +302,7 @@ class Ndb { lookup_profile_by_key_inner(key, txn: txn) } - func lookup_profile_by_key(key: ProfileKey) -> NdbTxn<ProfileRecord?> { + func lookup_profile_by_key(key: ProfileKey) -> NdbTxn<ProfileRecord?>? { return NdbTxn(ndb: self) { txn in lookup_profile_by_key_inner(key, txn: txn) } @@ -312,9 +313,13 @@ class Ndb { } func lookup_profile_key(_ pubkey: Pubkey) -> ProfileKey? { - return NdbTxn(ndb: self) { txn in + guard let txn = NdbTxn(ndb: self, with: { txn in lookup_profile_key_with_txn(pubkey, txn: txn) - }.value + }) else { + return nil + } + + return txn.value } func lookup_profile_key_with_txn<Y>(_ pubkey: Pubkey, txn: NdbTxn<Y>) -> ProfileKey? { @@ -342,17 +347,23 @@ class Ndb { } func lookup_note_key(_ id: NoteId) -> NoteKey? { - NdbTxn(ndb: self, with: { txn in lookup_note_key_with_txn(id, txn: txn) }).value + guard let txn = NdbTxn(ndb: self, with: { txn in + lookup_note_key_with_txn(id, txn: txn) + }) else { + return nil + } + + return txn.value } - func lookup_note(_ id: NoteId) -> NdbTxn<NdbNote?> { - return NdbTxn(ndb: self) { txn in + func lookup_note(_ id: NoteId) -> NdbTxn<NdbNote?>? { + NdbTxn(ndb: self) { txn in lookup_note_with_txn_inner(id: id, txn: txn) } } - func lookup_profile(_ pubkey: Pubkey) -> NdbTxn<ProfileRecord?> { - return NdbTxn(ndb: self) { txn in + func lookup_profile(_ pubkey: Pubkey) -> NdbTxn<ProfileRecord?>? { + NdbTxn(ndb: self) { txn in lookup_profile_with_txn_inner(pubkey: pubkey, txn: txn) } } diff --git a/nostrdb/NdbTxn.swift b/nostrdb/NdbTxn.swift @@ -18,8 +18,9 @@ class NdbTxn<T> { var moved: Bool var inherited: Bool - init(ndb: Ndb, with: (NdbTxn<T>) -> T = { _ in () }) { - #if TXNDEBUG + init?(ndb: Ndb, with: (NdbTxn<T>) -> T = { _ in () }) { + guard !ndb.closed else { return nil } +#if TXNDEBUG txn_count += 1 print("opening transaction \(txn_count)") #endif @@ -31,11 +32,7 @@ class NdbTxn<T> { self.txn = ndb_txn() let ok = ndb_begin_query(ndb.ndb.ndb, &self.txn) != 0 if !ok { - self.moved = false - self.txn = ndb_txn() - self.inherited = true - self.val = with(self) - return + return nil } Thread.current.threadDictionary["ndb_txn"] = self.txn self.inherited = false