From f844ed99318568e85f80e386161971cb0d205975 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20D=E2=80=99Aquino?= Date: Fri, 28 Nov 2025 19:17:35 -0800 Subject: [PATCH] Redesign Ndb.swift interface with build safety MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../NotificationFormatter.swift | 3 +- .../NotificationService.swift | 20 +- damus/ContentView.swift | 12 +- .../NostrNetworkManager/ProfilesManager.swift | 4 +- .../SubscriptionManager.swift | 13 +- .../UserRelayListManager.swift | 2 +- damus/Core/Nostr/Nostr.swift | 67 ++++++- damus/Core/Nostr/NostrEvent.swift | 55 +++--- damus/Core/Nostr/Profiles.swift | 40 ++-- .../ActionBar/Views/EventActionBar.swift | 4 +- damus/Features/Chat/ChatEventView.swift | 4 +- .../Events/Components/ReplyDescription.swift | 8 +- .../Features/Events/Models/NoteContent.swift | 8 +- damus/Features/Events/NoteContentView.swift | 111 ++++++----- .../FollowPack/Views/FollowPackPreview.swift | 3 +- .../FollowPack/Views/FollowPackView.swift | 3 +- .../Follows/Models/FollowingModel.swift | 8 +- .../Follows/Views/FollowingView.swift | 3 +- .../Highlight/Models/HighlightEvent.swift | 8 +- .../Highlight/Views/HighlightEventRef.swift | 3 +- .../Views/Components/LiveStreamProfile.swift | 3 +- .../NIP05/Models/NIP05DomainEventsModel.swift | 4 +- .../Views/NIP05DomainTimelineHeaderView.swift | 7 +- .../Models/NotificationsManager.swift | 134 +++++++------ .../Onboarding/SuggestedUsersViewModel.swift | 3 +- .../Features/Posting/Models/DraftsModel.swift | 12 +- damus/Features/Posting/Views/PostView.swift | 3 +- damus/Features/Posting/Views/ReplyView.swift | 2 +- damus/Features/Posting/Views/UserSearch.swift | 6 +- .../Profile/Views/EditMetadataView.swift | 6 +- .../Profile/Views/EventProfileName.swift | 8 +- .../Views/ProfileActionSheetView.swift | 17 +- .../Features/Profile/Views/ProfileName.swift | 6 +- .../Profile/Views/ProfileNameView.swift | 3 +- .../Profile/Views/ProfilePicView.swift | 12 +- .../Views/ProfilePictureSelector.swift | 2 +- .../Features/Profile/Views/ProfileView.swift | 37 ++-- .../Purple/Views/DamusPurpleAccountView.swift | 3 +- .../Search/Models/SearchHomeModel.swift | 16 +- .../Search/Views/PullDownSearch.swift | 21 +- .../Search/Views/SearchResultsView.swift | 35 ++-- .../Features/Timeline/Models/HomeModel.swift | 2 +- .../Timeline/Views/SideMenuView.swift | 3 +- damus/Features/Wallet/Views/NWCSettings.swift | 6 +- .../Wallet/Views/TransactionsView.swift | 3 +- .../Zaps/Views/ProfileZapLinkView.swift | 23 ++- damus/Features/Zaps/Views/ZapTypePicker.swift | 3 +- damus/Shared/Components/NIP05Badge.swift | 2 +- damus/Shared/Components/QRCodeView.swift | 3 +- .../Shared/Media/Images/BannerImageView.swift | 7 +- damus/Shared/Utilities/EventCache.swift | 2 +- .../NostrNetworkManagerTests.swift | 8 +- damusTests/NoteContentViewTests.swift | 14 +- nostrdb/Ndb+.swift | 4 +- nostrdb/Ndb.swift | 179 +++++++++++------- nostrdb/NdbBlock.swift | 82 ++++---- nostrdb/NdbNote.swift | 21 +- nostrdb/NdbTxn.swift | 2 +- nostrdb/Test/NdbTests.swift | 16 +- nostrdb/UnownedNdbNote.swift | 9 +- 60 files changed, 611 insertions(+), 497 deletions(-) diff --git a/DamusNotificationService/NotificationFormatter.swift b/DamusNotificationService/NotificationFormatter.swift index 3b0d9203..bfaebc67 100644 --- a/DamusNotificationService/NotificationFormatter.swift +++ b/DamusNotificationService/NotificationFormatter.swift @@ -125,8 +125,7 @@ struct NotificationFormatter { let src = zap.request.ev let pk = zap.is_anon ? ANON_PUBKEY : src.pubkey - let profile_txn = profiles.lookup(id: pk) - let profile = profile_txn?.unsafeUnownedValue + let profile = profiles.lookup(id: pk) let name = Profile.displayName(profile: profile, pubkey: pk).displayName.truncate(maxLength: 50) let sats = NSNumber(value: (Double(zap.invoice.amount) / 1000.0)) diff --git a/DamusNotificationService/NotificationService.swift b/DamusNotificationService/NotificationService.swift index 0eb395b1..fbdd0eb8 100644 --- a/DamusNotificationService/NotificationService.swift +++ b/DamusNotificationService/NotificationService.swift @@ -58,8 +58,7 @@ class NotificationService: UNNotificationServiceExtension { } let sender_profile = { - let txn = state.ndb.lookup_profile(nostr_event.pubkey) - let profile = txn?.unsafeUnownedValue?.profile + let profile = state.profiles.lookup(id: nostr_event.pubkey) let picture = ((profile?.picture.map { URL(string: $0) }) ?? URL(string: robohash(nostr_event.pubkey)))! return ProfileBuf(picture: picture, name: profile?.name, @@ -186,8 +185,13 @@ func message_intent_from_note(ndb: Ndb, sender_profile: ProfileBuf, content: Str // gather recipients if let recipient_note_id = note.direct_replies() { - let replying_to = ndb.lookup_note(recipient_note_id) - if let replying_to_pk = replying_to?.unsafeUnownedValue?.pubkey { + let replying_to_pk = ndb.lookup_note(recipient_note_id, borrow: { replying_to_note -> Pubkey? in + switch replying_to_note { + case .none: return nil + case .some(let note): return note.pubkey + } + }) + if let replying_to_pk { meta.isReplyToCurrentUser = replying_to_pk == our_pubkey if replying_to_pk != sender_pk { @@ -247,8 +251,12 @@ func message_intent_from_note(ndb: Ndb, sender_profile: ProfileBuf, content: Str } func pubkey_to_inperson(ndb: Ndb, pubkey: Pubkey, our_pubkey: Pubkey) async -> INPerson { - let profile_txn = ndb.lookup_profile(pubkey) - let profile = profile_txn?.unsafeUnownedValue?.profile + let profile = ndb.lookup_profile(pubkey, borrow: { profileRecord in + switch profileRecord { + case .some(let pr): return pr.profile + case .none: return nil + } + }) let name = profile?.name let display_name = profile?.display_name let nip05 = profile?.nip05 diff --git a/damus/ContentView.swift b/damus/ContentView.swift index da6121ea..88260424 100644 --- a/damus/ContentView.swift +++ b/damus/ContentView.swift @@ -397,8 +397,7 @@ struct ContentView: View { guard let ds = self.damus_state, let lud16 = nwc.lud16, let keypair = ds.keypair.to_full(), - let profile_txn = ds.profiles.lookup(id: ds.pubkey), - let profile = profile_txn.unsafeUnownedValue, + let profile = ds.profiles.lookup(id: ds.pubkey), lud16 != profile.lud16 else { return } @@ -561,8 +560,7 @@ struct ContentView: View { home.filter_events() guard let ds = damus_state, - let profile_txn = ds.profiles.lookup(id: ds.pubkey), - let profile = profile_txn.unsafeUnownedValue, + let profile = ds.profiles.lookup(id: ds.pubkey), let keypair = ds.keypair.to_full() else { return @@ -580,8 +578,7 @@ struct ContentView: View { } }, message: { if case let .user(pubkey, _) = self.muting { - let profile_txn = damus_state!.profiles.lookup(id: pubkey) - let profile = profile_txn?.unsafeUnownedValue + let profile = damus_state!.profiles.lookup(id: pubkey) 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 { @@ -643,8 +640,7 @@ struct ContentView: View { } }, message: { if case let .user(pubkey, _) = muting { - let profile_txn = damus_state?.profiles.lookup(id: pubkey) - let profile = profile_txn?.unsafeUnownedValue + let profile = damus_state?.profiles.lookup(id: pubkey) 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 { diff --git a/damus/Core/Networking/NostrNetworkManager/ProfilesManager.swift b/damus/Core/Networking/NostrNetworkManager/ProfilesManager.swift index ff3ac3f1..b59f7b12 100644 --- a/damus/Core/Networking/NostrNetworkManager/ProfilesManager.swift +++ b/damus/Core/Networking/NostrNetworkManager/ProfilesManager.swift @@ -96,7 +96,7 @@ extension NostrNetworkManager { 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(metadataEvent.pubkey) else { return } + guard let profile = ndb.lookup_profile_and_copy(metadataEvent.pubkey) else { return } for relevantStream in relevantStreams.values { relevantStream.continuation.yield(profile) } @@ -144,7 +144,7 @@ extension NostrNetworkManager { // MARK: - Helper types - typealias ProfileStreamItem = NdbTxn + typealias ProfileStreamItem = Profile struct ProfileStreamInfo { let id: UUID = UUID() diff --git a/damus/Core/Networking/NostrNetworkManager/SubscriptionManager.swift b/damus/Core/Networking/NostrNetworkManager/SubscriptionManager.swift index d5bf1218..6a200b80 100644 --- a/damus/Core/Networking/NostrNetworkManager/SubscriptionManager.swift +++ b/damus/Core/Networking/NostrNetworkManager/SubscriptionManager.swift @@ -413,15 +413,18 @@ extension NostrNetworkManager { switch query { case .profile(let pubkey): - if let profile_txn = self.ndb.lookup_profile(pubkey), - let record = profile_txn.unsafeUnownedValue, - record.profile != nil - { + let profileNotNil = self.ndb.lookup_profile(pubkey, borrow: { pr in + switch pr { + case .some(let pr): return pr.profile != nil + case .none: return true + } + }) + if profileNotNil { return .profile(pubkey) } filter = NostrFilter(kinds: [.metadata], limit: 1, authors: [pubkey]) case .event(let evid): - if let event = self.ndb.lookup_note(evid)?.unsafeUnownedValue?.to_owned() { + if let event = self.ndb.lookup_note_and_copy(evid) { return .event(event) } filter = NostrFilter(ids: [evid], limit: 1) diff --git a/damus/Core/Networking/NostrNetworkManager/UserRelayListManager.swift b/damus/Core/Networking/NostrNetworkManager/UserRelayListManager.swift index 8e40177e..05dd60e7 100644 --- a/damus/Core/Networking/NostrNetworkManager/UserRelayListManager.swift +++ b/damus/Core/Networking/NostrNetworkManager/UserRelayListManager.swift @@ -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(latestRelayListEventId)?.unsafeUnownedValue?.to_owned() + return delegate.ndb.lookup_note_and_copy(latestRelayListEventId) } /// Gets the latest `kind:3` relay list from NostrDB. diff --git a/damus/Core/Nostr/Nostr.swift b/damus/Core/Nostr/Nostr.swift index f0588c84..72905565 100644 --- a/damus/Core/Nostr/Nostr.swift +++ b/damus/Core/Nostr/Nostr.swift @@ -11,8 +11,8 @@ typealias Profile = NdbProfile typealias ProfileKey = UInt64 //typealias ProfileRecord = NdbProfileRecord -class ProfileRecord { - let data: NdbProfileRecord +struct ProfileRecord: ~Copyable { + private let data: NdbProfileRecord // Marked as private to make users access the safer `profile` property init(data: NdbProfileRecord, key: ProfileKey) { self.data = data @@ -20,7 +20,11 @@ class ProfileRecord { } let profileKey: ProfileKey - var profile: Profile? { return data.profile } + var profile: Profile? { + // Clone the data since `NdbProfile` can be unowned, but does not `~Copyable` semantics. + // This helps ensure the memory safety of this property + return data.profile?.clone() + } var receivedAt: UInt64 { data.receivedAt } var noteKey: UInt64 { data.noteKey } @@ -37,10 +41,7 @@ class ProfileRecord { } if addr.contains("@") { - // this is a heavy op and is used a lot in views, cache it! - let addr = lnaddress_to_lnurl(addr); - self._lnurl = addr - return addr + return lnaddress_to_lnurl(addr) } if !addr.lowercased().hasPrefix("lnurl") { @@ -81,6 +82,24 @@ extension NdbProfile { return URL(string: trim) } } + + + /// Clones this object. Useful for creating an owned copy from an unowned profile + func clone() -> Self { + return NdbProfile( + name: self.name, + display_name: self.display_name, + about: self.about, + picture: self.picture, + banner: self.banner, + website: self.website, + lud06: self.lud06, + lud16: self.lud16, + nip05: self.nip05, + damus_donation: self.damus_donation, + reactions: self.reactions + ) + } init(name: String? = nil, display_name: String? = nil, about: String? = nil, picture: String? = nil, banner: String? = nil, website: String? = nil, lud06: String? = nil, lud16: String? = nil, nip05: String? = nil, damus_donation: Int? = nil, reactions: Bool = true) { @@ -309,7 +328,40 @@ func make_ln_url(_ str: String?) -> URL? { return str.flatMap { URL(string: "lightning:" + $0) } } +import Synchronization + +@available(iOS 18.0, *) +class CachedLNAddressConverter { + static let shared: CachedLNAddressConverter = .init() + + private let cache: Mutex<[String: String?]> = .init([:]) // Using a mutex here to avoid race conditions without imposing actor isolation requirements. + + func lnaddress_to_lnurl(_ lnaddr: String) -> String? { + if let cachedValue = cache.withLock({ $0[lnaddr] }) { + return cachedValue + } + + let lnurl: String? = compute_lnaddress_to_lnurl(lnaddr) + + cache.withLock({ cache in + cache[lnaddr] = .some(lnurl) + }) + return lnurl + } +} + func lnaddress_to_lnurl(_ lnaddr: String) -> String? { + if #available(iOS 18.0, *) { + // This is a heavy op, use a cache if available! + return CachedLNAddressConverter.shared.lnaddress_to_lnurl(lnaddr) + } else { + // Fallback on earlier versions + return compute_lnaddress_to_lnurl(lnaddr) + } +} + + +func compute_lnaddress_to_lnurl(_ lnaddr: String) -> String? { let parts = lnaddr.split(separator: "@") guard parts.count == 2 else { return nil @@ -322,4 +374,3 @@ func lnaddress_to_lnurl(_ lnaddr: String) -> String? { return bech32_encode(hrp: "lnurl", Array(dat)) } - diff --git a/damus/Core/Nostr/NostrEvent.swift b/damus/Core/Nostr/NostrEvent.swift index c8afb0b0..ba68744f 100644 --- a/damus/Core/Nostr/NostrEvent.swift +++ b/damus/Core/Nostr/NostrEvent.swift @@ -810,42 +810,41 @@ func validate_event(ev: NostrEvent) -> ValidationResult { } func first_eref_mention(ndb: Ndb, ev: NostrEvent, keypair: Keypair) -> Mention? { - guard let blockGroup = try? NdbBlockGroup.from(event: ev, using: ndb, and: keypair) else { return nil } - - return blockGroup.forEachBlock({ index, block in - switch block { - case .mention(let mention): - guard let mention = MentionRef(block: mention) else { return .loopContinue } - switch mention.nip19 { - case .note(let noteId): - return .loopReturn(Mention.note(noteId, index: index)) - case .nevent(let nEvent): - return .loopReturn(Mention.note(nEvent.noteid, index: index)) + return try? NdbBlockGroup.borrowBlockGroup(event: ev, using: ndb, and: keypair, borrow: { blockGroup in + return blockGroup.forEachBlock({ index, block in + switch block { + case .mention(let mention): + guard let mention = MentionRef(block: mention) else { return .loopContinue } + switch mention.nip19 { + case .note(let noteId): + return .loopReturn(Mention.note(noteId, index: index)) + case .nevent(let nEvent): + return .loopReturn(Mention.note(nEvent.noteid, index: index)) + default: + return .loopContinue + } default: return .loopContinue } - default: - return .loopContinue - } + }) }) } func separate_invoices(ndb: Ndb, ev: NostrEvent, keypair: Keypair) -> [Invoice]? { - guard let blockGroup = try? NdbBlockGroup.from(event: ev, using: ndb, and: keypair) else { - return nil - } - let invoiceBlocks: [Invoice] = (try? blockGroup.reduce(initialResult: [Invoice](), { index, invoices, block in - switch block { - case .invoice(let invoice): - if let invoice = invoice.as_invoice() { - return .loopReturn(invoices + [invoice]) + return try? NdbBlockGroup.borrowBlockGroup(event: ev, using: ndb, and: keypair, borrow: { blockGroup in + let invoiceBlocks: [Invoice] = (try? blockGroup.reduce(initialResult: [Invoice](), { index, invoices, block in + switch block { + case .invoice(let invoice): + if let invoice = invoice.as_invoice() { + return .loopReturn(invoices + [invoice]) + } + default: + break } - default: - break - } - return .loopContinue - })) ?? [] - return invoiceBlocks.isEmpty ? nil : invoiceBlocks + return .loopContinue + })) ?? [] + return invoiceBlocks.isEmpty ? nil : invoiceBlocks + }) } /** diff --git a/damus/Core/Nostr/Profiles.swift b/damus/Core/Nostr/Profiles.swift index 37273f56..484ebae6 100644 --- a/damus/Core/Nostr/Profiles.swift +++ b/damus/Core/Nostr/Profiles.swift @@ -74,31 +74,45 @@ class Profiles { profile_data(pubkey).zapper } - func lookup_with_timestamp(_ pubkey: Pubkey) -> NdbTxn? { - ndb.lookup_profile(pubkey) + func lookup_with_timestamp(_ pubkey: Pubkey, borrow lendingFunction: (_: borrowing ProfileRecord?) throws -> T) rethrows -> T { + return try ndb.lookup_profile(pubkey, borrow: lendingFunction) + } + + func lookup_lnurl(_ pubkey: Pubkey) -> String? { + return lookup_with_timestamp(pubkey, borrow: { pr in + switch pr { + case .some(let pr): return pr.lnurl + case .none: return nil + } + }) } - func lookup_by_key(key: ProfileKey) -> NdbTxn? { - ndb.lookup_profile_by_key(key: key) + func lookup_by_key(key: ProfileKey, borrow lendingFunction: (_: borrowing ProfileRecord?) throws -> T) rethrows -> T { + return try ndb.lookup_profile_by_key(key: key, borrow: lendingFunction) } - func search(_ query: String, limit: Int, txn: NdbTxn) -> [Pubkey] { - ndb.search_profile(query, limit: limit, txn: txn) + func search(_ query: String, limit: Int) -> [Pubkey] { + ndb.search_profile(query, limit: limit) } - func lookup(id: Pubkey, txn_name: String? = nil) -> NdbTxn? { - guard let txn = ndb.lookup_profile(id, txn_name: txn_name) else { - return nil - } - return txn.map({ pr in pr?.profile }) + func lookup(id: Pubkey) -> Profile? { + return ndb.lookup_profile(id, borrow: { pr in + switch pr { + case .none: + return nil + case .some(let profileRecord): + // This will clone the value to make it owned and safe to return. + return profileRecord.profile + } + }) } func lookup_key_by_pubkey(_ pubkey: Pubkey) -> ProfileKey? { ndb.lookup_profile_key(pubkey) } - func has_fresh_profile(id: Pubkey, txn: NdbTxn) -> Bool { - guard let fetched_at = ndb.read_profile_last_fetched(txn: txn, pubkey: id) + func has_fresh_profile(id: Pubkey) -> Bool { + guard let fetched_at = ndb.read_profile_last_fetched(pubkey: id) else { return false } diff --git a/damus/Features/Actions/ActionBar/Views/EventActionBar.swift b/damus/Features/Actions/ActionBar/Views/EventActionBar.swift index 1aa4e8d6..10c93037 100644 --- a/damus/Features/Actions/ActionBar/Views/EventActionBar.swift +++ b/damus/Features/Actions/ActionBar/Views/EventActionBar.swift @@ -41,9 +41,7 @@ struct EventActionBar: View { // Fetching an LNURL is expensive enough that it can cause a hitch. Use a special backgroundable function to fetch the value. // Fetch on `.onAppear` nonisolated func fetchLNURL() { - let lnurl = damus_state.profiles.lookup_with_timestamp(event.pubkey)?.map({ pr in - pr?.lnurl - }).value + let lnurl = damus_state.profiles.lookup_lnurl(event.pubkey) DispatchQueue.main.async { self.lnurl = lnurl } diff --git a/damus/Features/Chat/ChatEventView.swift b/damus/Features/Chat/ChatEventView.swift index 2a1798c1..1c9f8208 100644 --- a/damus/Features/Chat/ChatEventView.swift +++ b/damus/Features/Chat/ChatEventView.swift @@ -115,9 +115,7 @@ struct ChatEventView: View { // MARK: Zapping properties var lnurl: String? { - damus_state.profiles.lookup_with_timestamp(event.pubkey)?.map({ pr in - pr?.lnurl - }).value + damus_state.profiles.lookup_lnurl(event.pubkey) } var zap_target: ZapTarget { ZapTarget.note(id: event.id, author: event.pubkey) diff --git a/damus/Features/Events/Components/ReplyDescription.swift b/damus/Features/Events/Components/ReplyDescription.swift index 1a1c529c..70ec3f06 100644 --- a/damus/Features/Events/Components/ReplyDescription.swift +++ b/damus/Features/Events/Components/ReplyDescription.swift @@ -38,14 +38,10 @@ 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.") } - 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) + let profile = ndb.lookup_profile_and_copy(pk) - return Profile.displayName(profile: prof?.profile, pubkey: pk).username.truncate(maxLength: 50) + return Profile.displayName(profile: profile, pubkey: pk).username.truncate(maxLength: 50) } let uniqueNames = NSOrderedSet(array: names).array as! [String] diff --git a/damus/Features/Events/Models/NoteContent.swift b/damus/Features/Events/Models/NoteContent.swift index 671a2f23..e2892090 100644 --- a/damus/Features/Events/Models/NoteContent.swift +++ b/damus/Features/Events/Models/NoteContent.swift @@ -72,8 +72,9 @@ func render_immediately_available_note_content(ndb: Ndb, ev: NostrEvent, profile } do { - let blocks = try NdbBlockGroup.from(event: ev, using: ndb, and: keypair) - return .separated(render_blocks(blocks: blocks, profiles: profiles, can_hide_last_previewable_refs: true)) + return try NdbBlockGroup.borrowBlockGroup(event: ev, using: ndb, and: keypair, borrow: { blocks in + return .separated(render_blocks(blocks: blocks, profiles: profiles, can_hide_last_previewable_refs: true)) + }) } catch { // TODO: Improve error handling in the future, bubbling it up so that the view can decide how display errors. Keep legacy behavior for now. @@ -327,8 +328,7 @@ func attributed_string_attach_icon(_ astr: inout AttributedString, img: UIImage) } func getDisplayName(pk: Pubkey, profiles: Profiles) -> String { - let profile_txn = profiles.lookup(id: pk, txn_name: "getDisplayName") - let profile = profile_txn?.unsafeUnownedValue + let profile = profiles.lookup(id: pk) return Profile.displayName(profile: profile, pubkey: pk).username.truncate(maxLength: 50) } diff --git a/damus/Features/Events/NoteContentView.swift b/damus/Features/Events/NoteContentView.swift index d3ff0fd3..28622201 100644 --- a/damus/Features/Events/NoteContentView.swift +++ b/damus/Features/Events/NoteContentView.swift @@ -275,17 +275,15 @@ struct NoteContentView: View { } func ensureMentionProfilesAreFetchingIfNeeded() { - guard let blockGroup = try? NdbBlockGroup.from(event: event, using: damus_state.ndb, and: damus_state.keypair) else { - return - } - var mentionPubkeys: Set = [] - let _: ()? = try? blockGroup.forEachBlock({ _, block in - guard let pubkey = block.mentionPubkey(tags: event.tags) else { + try? NdbBlockGroup.borrowBlockGroup(event: event, using: damus_state.ndb, and: damus_state.keypair, borrow: { blockGroup in + let _: ()? = try? blockGroup.forEachBlock({ _, block in + guard let pubkey = block.mentionPubkey(tags: event.tags) else { + return .loopContinue + } + mentionPubkeys.insert(pubkey) return .loopContinue - } - mentionPubkeys.insert(pubkey) - return .loopContinue + }) }) guard !mentionPubkeys.isEmpty else { return } @@ -297,8 +295,7 @@ struct NoteContentView: View { } requestedMentionProfiles.insert(pubkey) - if let txn = damus_state.ndb.lookup_profile(pubkey), - damus_state.profiles.has_fresh_profile(id: pubkey, txn: txn) { + if damus_state.profiles.has_fresh_profile(id: pubkey) { continue } @@ -397,38 +394,37 @@ struct NoteContentView: View { var body: some View { ArtifactContent .onReceive(handle_notify(.profile_updated)) { profile in - guard let blockGroup = try? NdbBlockGroup.from(event: event, using: damus_state.ndb, and: damus_state.keypair) else { - return - } - let _: Int? = try? blockGroup.forEachBlock { index, block in - switch block { - case .mention(let m): - guard let typ = m.bech32_type else { - return .loopContinue - } - switch typ { - case .nprofile: - if m.bech32.nprofile.matches_pubkey(pk: profile.pubkey) { - load(force_artifacts: true) + try? NdbBlockGroup.borrowBlockGroup(event: event, using: damus_state.ndb, and: damus_state.keypair, borrow: { blockGroup in + let _: Int? = blockGroup.forEachBlock { index, block in + switch block { + case .mention(let m): + guard let typ = m.bech32_type else { + return .loopContinue } - case .npub: - if m.bech32.npub.matches_pubkey(pk: profile.pubkey) { - load(force_artifacts: true) + switch typ { + case .nprofile: + if m.bech32.nprofile.matches_pubkey(pk: profile.pubkey) { + load(force_artifacts: true) + } + case .npub: + if m.bech32.npub.matches_pubkey(pk: profile.pubkey) { + load(force_artifacts: true) + } + case .nevent: return .loopContinue + case .nrelay: return .loopContinue + case .nsec: return .loopContinue + case .note: return .loopContinue + case .naddr: return .loopContinue } - case .nevent: return .loopContinue - case .nrelay: return .loopContinue - case .nsec: return .loopContinue - case .note: return .loopContinue - case .naddr: return .loopContinue + case .text: return .loopContinue + case .hashtag: return .loopContinue + case .url: return .loopContinue + case .invoice: return .loopContinue + case .mention_index(_): return .loopContinue } - case .text: return .loopContinue - case .hashtag: return .loopContinue - case .url: return .loopContinue - case .invoice: return .loopContinue - case .mention_index(_): return .loopContinue + return .loopContinue } - return .loopContinue - } + }) } .onAppear { load() @@ -583,26 +579,25 @@ struct NoteContentView_Previews: PreviewProvider { } func separate_images(ndb: Ndb, ev: NostrEvent, keypair: Keypair) -> [MediaUrl]? { - guard let blockGroup = try? NdbBlockGroup.from(event: ev, using: ndb, and: keypair) else { - return nil - } - let urlBlocks: [URL] = (try? blockGroup.reduce(initialResult: Array()) { index, urls, block in - switch block { - case .url(let url): - guard let parsed_url = URL(string: url.as_str()) else { - return .loopContinue + return try? NdbBlockGroup.borrowBlockGroup(event: ev, using: ndb, and: keypair, borrow: { blockGroup in + let urlBlocks: [URL] = (blockGroup.reduce(initialResult: Array()) { index, urls, block in + switch block { + case .url(let url): + guard let parsed_url = URL(string: url.as_str()) else { + return .loopContinue + } + + if classify_url(parsed_url).is_img != nil { + return .loopReturn(urls + [parsed_url]) + } + default: + break } - - if classify_url(parsed_url).is_img != nil { - return .loopReturn(urls + [parsed_url]) - } - default: - break - } - return .loopContinue - }) ?? [] - let mediaUrls = urlBlocks.map { MediaUrl.image($0) } - return mediaUrls.isEmpty ? nil : mediaUrls + return .loopContinue + }) ?? [] + let mediaUrls = urlBlocks.map { MediaUrl.image($0) } + return mediaUrls.isEmpty ? nil : mediaUrls + }) } extension NdbBlock { diff --git a/damus/Features/FollowPack/Views/FollowPackPreview.swift b/damus/Features/FollowPack/Views/FollowPackPreview.swift index c31dd6a3..a16ecec6 100644 --- a/damus/Features/FollowPack/Views/FollowPackPreview.swift +++ b/damus/Features/FollowPack/Views/FollowPackPreview.swift @@ -157,8 +157,7 @@ struct FollowPackPreviewBody: View { .onTapGesture { state.nav.push(route: Route.ProfileByKey(pubkey: event.event.pubkey)) } - let profile_txn = state.profiles.lookup(id: event.event.pubkey) - let profile = profile_txn?.unsafeUnownedValue + let profile = state.profiles.lookup(id: event.event.pubkey) let displayName = Profile.displayName(profile: profile, pubkey: event.event.pubkey) switch displayName { case .one(let one): diff --git a/damus/Features/FollowPack/Views/FollowPackView.swift b/damus/Features/FollowPack/Views/FollowPackView.swift index 22854a23..bdba1ddb 100644 --- a/damus/Features/FollowPack/Views/FollowPackView.swift +++ b/damus/Features/FollowPack/Views/FollowPackView.swift @@ -135,8 +135,7 @@ struct FollowPackView: View { .onTapGesture { state.nav.push(route: Route.ProfileByKey(pubkey: event.event.pubkey)) } - let profile_txn = state.profiles.lookup(id: event.event.pubkey) - let profile = profile_txn?.unsafeUnownedValue + let profile = state.profiles.lookup(id: event.event.pubkey) let displayName = Profile.displayName(profile: profile, pubkey: event.event.pubkey) switch displayName { case .one(let one): diff --git a/damus/Features/Follows/Models/FollowingModel.swift b/damus/Features/Follows/Models/FollowingModel.swift index 39e79b3b..342d69d6 100644 --- a/damus/Features/Follows/Models/FollowingModel.swift +++ b/damus/Features/Follows/Models/FollowingModel.swift @@ -22,11 +22,11 @@ class FollowingModel { self.hashtags = hashtags } - func get_filter(txn: NdbTxn) -> NostrFilter { + func get_filter() -> NostrFilter { var f = NostrFilter(kinds: [.metadata]) f.authors = self.contacts.reduce(into: Array()) { acc, pk in // don't fetch profiles we already have - if damus_state.profiles.has_fresh_profile(id: pk, txn: txn) { + if damus_state.profiles.has_fresh_profile(id: pk) { return } acc.append(pk) @@ -34,8 +34,8 @@ class FollowingModel { return f } - func subscribe(txn: NdbTxn) { - let filter = get_filter(txn: txn) + func subscribe() { + let filter = get_filter() if (filter.authors?.count ?? 0) == 0 { needs_sub = false return diff --git a/damus/Features/Follows/Views/FollowingView.swift b/damus/Features/Follows/Views/FollowingView.swift index d6aef101..6c824c3b 100644 --- a/damus/Features/Follows/Views/FollowingView.swift +++ b/damus/Features/Follows/Views/FollowingView.swift @@ -151,8 +151,7 @@ struct FollowingView: View { } .tabViewStyle(.page(indexDisplayMode: .never)) .onAppear { - guard let txn = NdbTxn(ndb: self.damus_state.ndb) else { return } - following.subscribe(txn: txn) + following.subscribe() } .onDisappear { following.unsubscribe() diff --git a/damus/Features/Highlight/Models/HighlightEvent.swift b/damus/Features/Highlight/Models/HighlightEvent.swift index 68ae38e3..420ceef0 100644 --- a/damus/Features/Highlight/Models/HighlightEvent.swift +++ b/damus/Features/Highlight/Models/HighlightEvent.swift @@ -100,14 +100,10 @@ struct HighlightEvent { return NSLocalizedString("Highlighted", bundle: bundle, comment: "Label to indicate that the user is highlighting their own post.") } - 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) + let profile = ndb.lookup_profile_and_copy(pk) - return Profile.displayName(profile: prof?.profile, pubkey: pk).username.truncate(maxLength: 50) + return Profile.displayName(profile: profile, pubkey: pk).username.truncate(maxLength: 50) } let uniqueNames: [String] = Array(Set(names)) diff --git a/damus/Features/Highlight/Views/HighlightEventRef.swift b/damus/Features/Highlight/Views/HighlightEventRef.swift index aa7b9d63..ff977ee7 100644 --- a/damus/Features/Highlight/Views/HighlightEventRef.swift +++ b/damus/Features/Highlight/Views/HighlightEventRef.swift @@ -63,8 +63,7 @@ struct HighlightEventRef: View { .font(.system(size: 14, weight: .bold)) .lineLimit(1) - let profile_txn = damus_state.profiles.lookup(id: longform_event.event.pubkey, txn_name: "highlight-profile") - let profile = profile_txn?.unsafeUnownedValue + let profile = damus_state.profiles.lookup(id: longform_event.event.pubkey) if let display_name = profile?.display_name { Text(display_name) diff --git a/damus/Features/Live/LiveStream/Views/Components/LiveStreamProfile.swift b/damus/Features/Live/LiveStream/Views/Components/LiveStreamProfile.swift index e9ee421b..cd034162 100644 --- a/damus/Features/Live/LiveStream/Views/Components/LiveStreamProfile.swift +++ b/damus/Features/Live/LiveStream/Views/Components/LiveStreamProfile.swift @@ -18,8 +18,7 @@ struct LiveStreamProfile: View { .onTapGesture { state.nav.push(route: Route.ProfileByKey(pubkey: pubkey)) } - let profile_txn = state.profiles.lookup(id: pubkey) - let profile = profile_txn?.unsafeUnownedValue + let profile = state.profiles.lookup(id: pubkey) let displayName = Profile.displayName(profile: profile, pubkey: pubkey) switch displayName { case .one(let one): diff --git a/damus/Features/NIP05/Models/NIP05DomainEventsModel.swift b/damus/Features/NIP05/Models/NIP05DomainEventsModel.swift index d5072b36..d9f5b8c4 100644 --- a/damus/Features/NIP05/Models/NIP05DomainEventsModel.swift +++ b/damus/Features/NIP05/Models/NIP05DomainEventsModel.swift @@ -47,9 +47,7 @@ class NIP05DomainEventsModel: ObservableObject { var authors = Set() for pubkey in state.contacts.get_friend_of_friends_list() { - let profile_txn = state.profiles.lookup(id: pubkey) - - guard let profile = profile_txn?.unsafeUnownedValue, + guard let profile = state.profiles.lookup(id: pubkey), let nip05_str = profile.nip05, let nip05 = NIP05.parse(nip05_str), nip05.host.caseInsensitiveCompare(domain) == .orderedSame else { diff --git a/damus/Features/NIP05/Views/NIP05DomainTimelineHeaderView.swift b/damus/Features/NIP05/Views/NIP05DomainTimelineHeaderView.swift index 7e8b7e6b..2665ba58 100644 --- a/damus/Features/NIP05/Views/NIP05DomainTimelineHeaderView.swift +++ b/damus/Features/NIP05/Views/NIP05DomainTimelineHeaderView.swift @@ -82,7 +82,12 @@ struct NIP05DomainTimelineHeaderView: View { func friendsOfFriendsString(_ friendsOfFriends: [Pubkey], ndb: Ndb, locale: Locale = Locale.current) -> String { let bundle = bundleForLocale(locale: locale) let names: [String] = friendsOfFriends.prefix(3).map { pk in - let profile = ndb.lookup_profile(pk)?.unsafeUnownedValue?.profile + let profile = ndb.lookup_profile(pk, borrow: { pr in + switch pr { + case .some(let pr): return pr.profile + case .none: return nil + } + }) return Profile.displayName(profile: profile, pubkey: pk).username.truncate(maxLength: 20) } diff --git a/damus/Features/Notifications/Models/NotificationsManager.swift b/damus/Features/Notifications/Models/NotificationsManager.swift index b266189b..43a7a6d3 100644 --- a/damus/Features/Notifications/Models/NotificationsManager.swift +++ b/damus/Features/Notifications/Models/NotificationsManager.swift @@ -65,63 +65,77 @@ func should_display_notification(state: HeadlessDamusState, event ev: NostrEvent return true } +func generate_text_mention_notification(ndb: Ndb, from ev: NostrEvent, state: HeadlessDamusState, blockGroup: borrowing NdbBlockGroup) -> LocalNotification? { + let notification: LocalNotification? = blockGroup.forEachBlock({ index, block in + switch block { + case .mention(let mention): + guard case .npub = mention.bech32_type, + (memcmp(state.keypair.pubkey.id.bytes, mention.bech32.npub.pubkey, 32) == 0) else { + return .loopContinue + } + let content_preview = render_notification_content_preview(ndb: ndb, ev: ev, profiles: state.profiles, keypair: state.keypair) + return .loopReturn(LocalNotification(type: .mention, event: ev, target: .note(ev), content: content_preview)) + default: + return .loopContinue + } + }) + if let notification { + return notification + } + + if ev.referenced_ids.contains(where: { note_id in + guard let note_author: Pubkey = state.ndb.lookup_note(note_id, borrow: { note in + switch note { + case .some(let note): return note.pubkey + case .none: return nil + } + }) else { return false } + guard note_author == state.keypair.pubkey else { return false } + return true + }) { + // This is a reply to one of our posts + let content_preview = render_notification_content_preview(ndb: state.ndb, ev: ev, profiles: state.profiles, keypair: state.keypair) + return LocalNotification(type: .reply, event: ev, target: .note(ev), content: content_preview) + } + + if ev.referenced_pubkeys.contains(state.keypair.pubkey) { + // not mentioned or replied to, just tagged + let content_preview = render_notification_content_preview(ndb: state.ndb, ev: ev, profiles: state.profiles, keypair: state.keypair) + return LocalNotification(type: .tagged, event: ev, target: .note(ev), content: content_preview) + } + + return nil +} + func generate_local_notification_object(ndb: Ndb, from ev: NostrEvent, state: HeadlessDamusState) -> LocalNotification? { guard let type = ev.known_kind else { return nil } if type == .text, - state.settings.mention_notification, - let blockGroup = try? NdbBlockGroup.from(event: ev, using: ndb, and: state.keypair) + state.settings.mention_notification { - let notification: LocalNotification? = try? blockGroup.forEachBlock({ index, block in - switch block { - case .mention(let mention): - guard case .npub = mention.bech32_type, - (memcmp(state.keypair.pubkey.id.bytes, mention.bech32.npub.pubkey, 32) == 0) else { - return .loopContinue - } - let content_preview = render_notification_content_preview(ndb: ndb, ev: ev, profiles: state.profiles, keypair: state.keypair) - return .loopReturn(LocalNotification(type: .mention, event: ev, target: .note(ev), content: content_preview)) - default: - return .loopContinue - } + return try? NdbBlockGroup.borrowBlockGroup(event: ev, using: ndb, and: state.keypair, borrow: { blockGroup in + return generate_text_mention_notification(ndb: ndb, from: ev, state: state, blockGroup: blockGroup) }) - if let notification { - return notification - } - - if ev.referenced_ids.contains(where: { note_id in - guard let note_author: Pubkey = state.ndb.lookup_note(note_id)?.unsafeUnownedValue?.pubkey else { return false } - guard note_author == state.keypair.pubkey else { return false } - return true - }) { - // This is a reply to one of our posts - let content_preview = render_notification_content_preview(ndb: state.ndb, ev: ev, profiles: state.profiles, keypair: state.keypair) - return LocalNotification(type: .reply, event: ev, target: .note(ev), content: content_preview) - } - - if ev.referenced_pubkeys.contains(state.keypair.pubkey) { - // not mentioned or replied to, just tagged - let content_preview = render_notification_content_preview(ndb: state.ndb, ev: ev, profiles: state.profiles, keypair: state.keypair) - return LocalNotification(type: .tagged, event: ev, target: .note(ev), content: content_preview) - } - } else if type == .boost, state.settings.repost_notification, let inner_ev = ev.get_inner_event() { + let content_preview = render_notification_content_preview(ndb: ndb, ev: inner_ev, profiles: state.profiles, keypair: state.keypair) return LocalNotification(type: .repost, event: ev, target: .note(inner_ev), content: content_preview) } else if type == .like, state.settings.like_notification, let evid = ev.referenced_ids.last { - if let txn = state.ndb.lookup_note(evid, txn_name: "local_notification_like"), - let liked_event = txn.unsafeUnownedValue - { - let content_preview = render_notification_content_preview(ndb: ndb, ev: liked_event, profiles: state.profiles, keypair: state.keypair) - return LocalNotification(type: .like, event: ev, target: .note(liked_event), content: content_preview) - } else { - return LocalNotification(type: .like, event: ev, target: .note_id(evid), content: "") - } + return state.ndb.lookup_note(evid, borrow: { liked_event in + switch liked_event { + case .none: + return LocalNotification(type: .like, event: ev, target: .note_id(evid), content: "") + case .some(let liked_event): + let owned_liked_event = liked_event.toOwned() + let content_preview = render_notification_content_preview(ndb: ndb, ev: owned_liked_event, profiles: state.profiles, keypair: state.keypair) + return LocalNotification(type: .like, event: ev, target: .note(owned_liked_event), content: content_preview) + } + }) } else if type == .dm, state.settings.dm_notification { @@ -177,8 +191,7 @@ func render_notification_content_preview(ndb: Ndb, ev: NostrEvent, profiles: Pro } func event_author_name(profiles: Profiles, pubkey: Pubkey) -> String { - let profile_txn = profiles.lookup(id: pubkey) - let profile = profile_txn?.unsafeUnownedValue + let profile = profiles.lookup(id: pubkey) return Profile.displayName(profile: profile, pubkey: pubkey).username.truncate(maxLength: 50) } @@ -214,9 +227,13 @@ func process_zap_event(state: HeadlessDamusState, ev: NostrEvent, completion: @e completion(.done(zap)) return } - - guard let txn = state.profiles.lookup_with_timestamp(ptag), - let lnurl = txn.map({ pr in pr?.lnurl }).value else { + + guard let lnurl = state.profiles.lookup_with_timestamp(ptag, borrow: { record -> String? in + switch record { + case .none: return nil + case .some(let record): return record.lnurl + } + }) else { completion(.failed) return } @@ -263,18 +280,19 @@ 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 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. + return ndb.lookup_note(etag, borrow: { note in + switch note { + case .none: + // 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 - // all of the time, and may reject valid zaps. What we need is a new - // unvalidated zap state, but for now we simply leak a bit of correctness... - - return ev.referenced_pubkeys.just_one() - } - - return pk + // We could return this as an invalid zap but that wouldn't be correct + // all of the time, and may reject valid zaps. What we need is a new + // unvalidated zap state, but for now we simply leak a bit of correctness... + return ev.referenced_pubkeys.just_one() + case .some(let note): + return note.pubkey + } + }) } fileprivate func process_zap_event_with_zapper(state: HeadlessDamusState, ev: NostrEvent, zapper: Pubkey) -> Zap? { diff --git a/damus/Features/Onboarding/SuggestedUsersViewModel.swift b/damus/Features/Onboarding/SuggestedUsersViewModel.swift index 65ab22fd..3eac32d1 100644 --- a/damus/Features/Onboarding/SuggestedUsersViewModel.swift +++ b/damus/Features/Onboarding/SuggestedUsersViewModel.swift @@ -79,8 +79,7 @@ class SuggestedUsersViewModel: ObservableObject { /// Gets suggested user information from a provided pubkey func suggestedUser(pubkey: Pubkey) -> SuggestedUser? { - let profile_txn = damus_state.profiles.lookup(id: pubkey) - if let profile = profile_txn?.unsafeUnownedValue, + if let profile = damus_state.profiles.lookup(id: pubkey), let user = SuggestedUser(name: profile.name, about: profile.about, picture: profile.picture, pubkey: pubkey) { return user } diff --git a/damus/Features/Posting/Models/DraftsModel.swift b/damus/Features/Posting/Models/DraftsModel.swift index de00ac1b..dc344bf8 100644 --- a/damus/Features/Posting/Models/DraftsModel.swift +++ b/damus/Features/Posting/Models/DraftsModel.swift @@ -116,7 +116,7 @@ class DraftArtifacts: Equatable { case .mention(let mention): if let pubkey = mention.ref.nip19.pubkey() { // A profile reference, format things properly. - let profile = damus_state.ndb.lookup_profile(pubkey)?.unsafeUnownedValue?.profile + let profile = damus_state.profiles.lookup(id: pubkey) let profile_name = DisplayName(profile: profile, pubkey: pubkey).username guard let url_address = URL(string: block.asString) else { rich_text_content.append(.init(string: block.asString)) @@ -173,8 +173,10 @@ class Drafts: ObservableObject { func load(from damus_state: DamusState) { guard let note_ids = damus_state.settings.draft_event_ids?.compactMap({ NoteId(hex: $0) }) else { return } for note_id in note_ids { - let txn = damus_state.ndb.lookup_note(note_id) - guard let note = txn?.unsafeUnownedValue else { continue } + let note = damus_state.ndb.lookup_note(note_id, borrow: { event in + return event?.toOwned() + }) + guard let note else { continue } // Implementation note: This currently fails silently, because: // 1. Errors are unlikely and not expected // 2. It is not mission critical to recover from this error @@ -232,13 +234,13 @@ class Drafts: ObservableObject { draft_events.append(wrapped_note) } for (replied_to_note_id, reply_artifacts) in self.replies { - guard let replied_to_note = damus_state.ndb.lookup_note(replied_to_note_id)?.unsafeUnownedValue?.to_owned() else { continue } + guard let replied_to_note = damus_state.ndb.lookup_note_and_copy(replied_to_note_id) else { continue } let nip37_draft = try? await reply_artifacts.to_nip37_draft(action: .replying_to(replied_to_note), damus_state: damus_state) guard let wrapped_note = nip37_draft?.wrapped_note else { continue } draft_events.append(wrapped_note) } for (quoted_note_id, quote_note_artifacts) in self.quotes { - guard let quoted_note = damus_state.ndb.lookup_note(quoted_note_id)?.unsafeUnownedValue?.to_owned() else { continue } + guard let quoted_note = damus_state.ndb.lookup_note_and_copy(quoted_note_id) else { continue } let nip37_draft = try? await quote_note_artifacts.to_nip37_draft(action: .quoting(quoted_note), damus_state: damus_state) guard let wrapped_note = nip37_draft?.wrapped_note else { continue } draft_events.append(wrapped_note) diff --git a/damus/Features/Posting/Views/PostView.swift b/damus/Features/Posting/Views/PostView.swift index c9c501bc..70789fde 100644 --- a/damus/Features/Posting/Views/PostView.swift +++ b/damus/Features/Posting/Views/PostView.swift @@ -212,8 +212,7 @@ struct PostView: View { return .init(string: "") } - let profile_txn = damus_state.profiles.lookup(id: pubkey) - let profile = profile_txn?.unsafeUnownedValue + let profile = damus_state.profiles.lookup(id: pubkey) return user_tag_attr_string(profile: profile, pubkey: pubkey) } diff --git a/damus/Features/Posting/Views/ReplyView.swift b/damus/Features/Posting/Views/ReplyView.swift index 7686017b..5d9ea6c6 100644 --- a/damus/Features/Posting/Views/ReplyView.swift +++ b/damus/Features/Posting/Views/ReplyView.swift @@ -27,7 +27,7 @@ struct ReplyView: View { let names = references .map { pubkey in let pk = pubkey - let prof = damus.ndb.lookup_profile(pk)?.unsafeUnownedValue?.profile + let prof = damus.profiles.lookup(id: pk) return "@" + Profile.displayName(profile: prof, pubkey: pk).username.truncate(maxLength: 50) } .joined(separator: " ") diff --git a/damus/Features/Posting/Views/UserSearch.swift b/damus/Features/Posting/Views/UserSearch.swift index 3957d166..e7a04919 100644 --- a/damus/Features/Posting/Views/UserSearch.swift +++ b/damus/Features/Posting/Views/UserSearch.swift @@ -17,13 +17,11 @@ struct UserSearch: View { @EnvironmentObject var tagModel: TagModel var users: [Pubkey] { - guard let txn = NdbTxn(ndb: damus_state.ndb) else { return [] } - return search_profiles(profiles: damus_state.profiles, contacts: damus_state.contacts, search: search, txn: txn) + return search_profiles(profiles: damus_state.profiles, contacts: damus_state.contacts, search: search) } func on_user_tapped(pk: Pubkey) { - let profile_txn = damus_state.profiles.lookup(id: pk) - let profile = profile_txn?.unsafeUnownedValue + let profile = damus_state.profiles.lookup(id: pk) let user_tag = user_tag_attr_string(profile: profile, pubkey: pk) appendUserTag(withTag: user_tag) diff --git a/damus/Features/Profile/Views/EditMetadataView.swift b/damus/Features/Profile/Views/EditMetadataView.swift index 6dc65b5b..6f19e8e4 100644 --- a/damus/Features/Profile/Views/EditMetadataView.swift +++ b/damus/Features/Profile/Views/EditMetadataView.swift @@ -33,8 +33,7 @@ struct EditMetadataView: View { init(damus_state: DamusState) { self.damus_state = damus_state - let profile_txn = damus_state.profiles.lookup(id: damus_state.pubkey) - let data = profile_txn?.unsafeUnownedValue + let data = damus_state.profiles.lookup(id: damus_state.pubkey) _name = State(initialValue: data?.name ?? "") _display_name = State(initialValue: data?.display_name ?? "") @@ -259,8 +258,7 @@ struct EditMetadataView: View { } func didChange() -> Bool { - let profile_txn = damus_state.profiles.lookup(id: damus_state.pubkey) - let data = profile_txn?.unsafeUnownedValue + let data = damus_state.profiles.lookup(id: damus_state.pubkey) if data?.name ?? "" != name { return true diff --git a/damus/Features/Profile/Views/EventProfileName.swift b/damus/Features/Profile/Views/EventProfileName.swift index 7afc2f81..c6dce13e 100644 --- a/damus/Features/Profile/Views/EventProfileName.swift +++ b/damus/Features/Profile/Views/EventProfileName.swift @@ -25,7 +25,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.profiles.lookup(id: pubkey)?.damus_donation self._donation = State(wrappedValue: donation) self.purple_account = nil self._profileObserver = StateObject.init(wrappedValue: ProfileObserver(pubkey: pubkey, damusState: damus)) @@ -61,8 +61,7 @@ struct EventProfileName: View { } var body: some View { - let profile_txn = damus_state.profiles.lookup(id: pubkey) - let profile = profile_txn?.unsafeUnownedValue + let profile = damus_state.profiles.lookup(id: pubkey) HStack(spacing: 2) { switch current_display_name(profile) { case .one(let one): @@ -108,8 +107,7 @@ struct EventProfileName: View { return } - let profile_txn = damus_state.profiles.lookup(id: update.pubkey) - guard let profile = profile_txn?.unsafeUnownedValue else { return } + guard let profile = damus_state.profiles.lookup(id: update.pubkey) else { return } let display_name = Profile.displayName(profile: profile, pubkey: pubkey) if display_name != self.display_name { diff --git a/damus/Features/Profile/Views/ProfileActionSheetView.swift b/damus/Features/Profile/Views/ProfileActionSheetView.swift index 495e75c6..48b9c452 100644 --- a/damus/Features/Profile/Views/ProfileActionSheetView.swift +++ b/damus/Features/Profile/Views/ProfileActionSheetView.swift @@ -33,13 +33,16 @@ struct ProfileActionSheetView: View { colorScheme == .light ? DamusColors.white : DamusColors.black } - func profile_data() -> ProfileRecord? { - let profile_txn = damus_state.profiles.lookup_with_timestamp(profile.pubkey) - return profile_txn?.unsafeUnownedValue + func profile_data(borrow lendingFunction: (_: borrowing ProfileRecord?) throws -> T) rethrows -> T { + return try damus_state.profiles.lookup_with_timestamp(profile.pubkey, borrow: lendingFunction) } func get_profile() -> Profile? { - return self.profile_data()?.profile + return damus_state.profiles.lookup(id: profile.pubkey) + } + + func get_lnurl() -> String? { + return damus_state.profiles.lookup_lnurl(profile.pubkey) } func navigate(route: Route) { @@ -115,7 +118,7 @@ struct ProfileActionSheetView: View { } var zapButton: some View { - if let lnurl = self.profile_data()?.lnurl, lnurl != "" { + if let lnurl = self.get_lnurl(), lnurl != "" { return AnyView(ProfileActionSheetZapButton(damus_state: damus_state, profile: profile, lnurl: lnurl)) } else { @@ -134,7 +137,7 @@ struct ProfileActionSheetView: View { var body: some View { VStack(alignment: .center) { ProfilePicView(pubkey: profile.pubkey, size: pfp_size, highlight: .custom(imageBorderColor(), 4.0), profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation, damusState: damus_state) - if let url = self.profile_data()?.profile?.website_url { + if let url = self.get_profile()?.website_url { WebsiteLink(url: url, style: .accent) .padding(.top, -15) } @@ -143,7 +146,7 @@ struct ProfileActionSheetView: View { PubkeyView(pubkey: profile.pubkey) - if let about = self.profile_data()?.profile?.about { + if let about = self.get_profile()?.about { AboutView(state: damus_state, about: about, max_about_length: 140, text_alignment: .center) .padding(.top) } diff --git a/damus/Features/Profile/Views/ProfileName.swift b/damus/Features/Profile/Views/ProfileName.swift index 2fce5982..a5ef4eda 100644 --- a/damus/Features/Profile/Views/ProfileName.swift +++ b/damus/Features/Profile/Views/ProfileName.swift @@ -96,8 +96,7 @@ struct ProfileName: View { } var body: some View { - let profile_txn = damus_state.profiles.lookup(id: pubkey) - let profile = profile_txn?.unsafeUnownedValue + let profile = damus_state.profiles.lookup(id: pubkey) HStack(spacing: 2) { Text(verbatim: "\(prefix)\(name_choice(profile: profile))") @@ -139,8 +138,7 @@ struct ProfileName: View { switch update { case .remote(let pubkey): - guard let profile_txn = damus_state.profiles.lookup(id: pubkey), - let prof = profile_txn.unsafeUnownedValue else { + guard let prof = damus_state.profiles.lookup(id: pubkey) else { return } handle_profile_update(profile: prof) diff --git a/damus/Features/Profile/Views/ProfileNameView.swift b/damus/Features/Profile/Views/ProfileNameView.swift index 56153381..61b6721b 100644 --- a/damus/Features/Profile/Views/ProfileNameView.swift +++ b/damus/Features/Profile/Views/ProfileNameView.swift @@ -16,8 +16,7 @@ struct ProfileNameView: View { var body: some View { Group { VStack(alignment: .leading) { - let profile_txn = self.damus.profiles.lookup(id: pubkey) - let profile = profile_txn?.unsafeUnownedValue + let profile = self.damus.profiles.lookup(id: pubkey) switch Profile.displayName(profile: profile, pubkey: pubkey) { case .one: diff --git a/damus/Features/Profile/Views/ProfilePicView.swift b/damus/Features/Profile/Views/ProfilePicView.swift index 12324e81..cfc17406 100644 --- a/damus/Features/Profile/Views/ProfilePicView.swift +++ b/damus/Features/Profile/Views/ProfilePicView.swift @@ -99,7 +99,12 @@ struct ProfilePicView: View { } func get_lnurl() -> String? { - return profiles.lookup_with_timestamp(pubkey)?.unsafeUnownedValue?.lnurl + return profiles.lookup_with_timestamp(pubkey, borrow: { pr in + switch pr { + case .some(let pr): return pr.lnurl + case .none: return nil + } + }) } var body: some View { @@ -116,8 +121,7 @@ struct ProfilePicView: View { self.picture = pic } case .remote(pubkey: let pk): - let profile_txn = profiles.lookup(id: pk) - let profile = profile_txn?.unsafeUnownedValue + let profile = profiles.lookup(id: pk) if let pic = profile?.picture { self.picture = pic } @@ -141,7 +145,7 @@ struct ProfilePicView: View { } func get_profile_url(picture: String?, pubkey: Pubkey, profiles: Profiles) -> URL { - let pic = picture ?? profiles.lookup(id: pubkey, txn_name: "get_profile_url")?.map({ $0?.picture }).value ?? robohash(pubkey) + let pic = picture ?? profiles.lookup(id: pubkey)?.picture ?? robohash(pubkey) if let url = URL(string: pic) { return url } diff --git a/damus/Features/Profile/Views/ProfilePictureSelector.swift b/damus/Features/Profile/Views/ProfilePictureSelector.swift index e7982bbd..0bc4e502 100644 --- a/damus/Features/Profile/Views/ProfilePictureSelector.swift +++ b/damus/Features/Profile/Views/ProfilePictureSelector.swift @@ -52,7 +52,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)?.picture { return URL(string: picture) } else { return profile_url ?? URL(string: robohash(pubkey)) diff --git a/damus/Features/Profile/Views/ProfileView.swift b/damus/Features/Profile/Views/ProfileView.swift index ffab68a3..e766de0c 100644 --- a/damus/Features/Profile/Views/ProfileView.swift +++ b/damus/Features/Profile/Views/ProfileView.swift @@ -27,7 +27,7 @@ func follow_btn_txt(_ fs: FollowState, follows_you: Bool) -> 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(pk)?.unsafeUnownedValue?.profile + let profile = ndb.lookup_profile_and_copy(pk) return Profile.displayName(profile: profile, pubkey: pk).username.truncate(maxLength: 20) } @@ -108,8 +108,7 @@ struct ProfileView: View { } func getProfileInfo() -> (String, String) { - let profile_txn = self.damus_state.profiles.lookup(id: profile.pubkey) - let ndbprofile = profile_txn?.unsafeUnownedValue + let ndbprofile = self.damus_state.profiles.lookup(id: profile.pubkey) let displayName = Profile.displayName(profile: ndbprofile, pubkey: profile.pubkey).displayName.truncate(maxLength: 25) let userName = Profile.displayName(profile: ndbprofile, pubkey: profile.pubkey).username.truncate(maxLength: 25) return (displayName, "@\(userName)") @@ -241,8 +240,8 @@ struct ProfileView: View { } } - func lnButton(unownedProfile: Profile?, record: ProfileRecord?) -> some View { - return ProfileZapLinkView(unownedProfileRecord: record, profileModel: self.profile) { reactions_enabled, lud16, lnurl in + func lnButton(profile: Profile?, lnurl: String?) -> some View { + return ProfileZapLinkView(profile: profile, lnurl: lnurl, profileModel: self.profile) { reactions_enabled, lud16, lnurl in Image(reactions_enabled ? "zap.fill" : "zap") .foregroundColor(reactions_enabled ? .orange : Color.primary) .profile_button_style(scheme: colorScheme) @@ -270,17 +269,16 @@ struct ProfileView: View { .font(.footnote) } - func actionSection(record: ProfileRecord?, pubkey: Pubkey) -> some View { + func actionSection(ndbprofile: Profile?, lnurl: String?, pubkey: Pubkey) -> some View { return Group { if damus_state.settings.enable_favourites_feature { FavoriteButtonView(pubkey: profile.pubkey, damus_state: damus_state) } - if let record, - let profile = record.profile, - let lnurl = record.lnurl, + if let profile = ndbprofile, + let lnurl, lnurl != "" { - lnButton(unownedProfile: profile, record: record) + lnButton(profile: ndbprofile, lnurl: lnurl) } dmButton @@ -312,7 +310,7 @@ struct ProfileView: View { return scale < 1 ? scale : 1 } - func nameSection(profile_data: ProfileRecord?) -> some View { + func nameSection(ndbprofile: Profile?, lnurl: String?) -> some View { return Group { let follows_you = profile.pubkey != damus_state.pubkey && profile.follows(pubkey: damus_state.pubkey) @@ -334,7 +332,7 @@ struct ProfileView: View { followsYouBadge } - actionSection(record: profile_data, pubkey: profile.pubkey) + actionSection(ndbprofile: ndbprofile, lnurl: lnurl, pubkey: profile.pubkey) } ProfileNameView(pubkey: profile.pubkey, damus: damus_state) @@ -360,16 +358,16 @@ 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 lnurl = damus_state.profiles.lookup_lnurl(profile.pubkey) + let ndbprofile = damus_state.profiles.lookup(id: profile.pubkey) - nameSection(profile_data: profile_data) + nameSection(ndbprofile: ndbprofile, lnurl: lnurl) - if let about = profile_data?.profile?.about { + if let about = ndbprofile?.about { AboutView(state: damus_state, about: about) } - if let url = profile_data?.profile?.website_url { + if let url = ndbprofile?.website_url { WebsiteLink(url: url) } @@ -571,10 +569,9 @@ extension View { @MainActor func check_nip05_validity(pubkey: Pubkey, profiles: Profiles) { - let profile_txn = profiles.lookup(id: pubkey) + let profile = profiles.lookup(id: pubkey) - guard let profile = profile_txn?.unsafeUnownedValue, - let nip05 = profile.nip05, + guard let nip05 = profile?.nip05, profiles.is_validated(pubkey) == nil else { return diff --git a/damus/Features/Purple/Views/DamusPurpleAccountView.swift b/damus/Features/Purple/Views/DamusPurpleAccountView.swift index bc959431..cafcd478 100644 --- a/damus/Features/Purple/Views/DamusPurpleAccountView.swift +++ b/damus/Features/Purple/Views/DamusPurpleAccountView.swift @@ -121,8 +121,7 @@ struct DamusPurpleAccountView: View { } func profile_display_name() -> String { - let profile_txn: NdbTxn? = damus_state.profiles.lookup_with_timestamp(account.pubkey) - let profile: NdbProfile? = profile_txn?.unsafeUnownedValue?.profile + let profile = damus_state.profiles.lookup(id: account.pubkey) let display_name = DisplayName(profile: profile, pubkey: account.pubkey).displayName return display_name } diff --git a/damus/Features/Search/Models/SearchHomeModel.swift b/damus/Features/Search/Models/SearchHomeModel.swift index a94c42e4..4a9f8563 100644 --- a/damus/Features/Search/Models/SearchHomeModel.swift +++ b/damus/Features/Search/Models/SearchHomeModel.swift @@ -110,29 +110,29 @@ class SearchHomeModel: ObservableObject { } } -func find_profiles_to_fetch(profiles: Profiles, load: PubkeysToLoad, cache: EventCache, txn: NdbTxn) -> [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(profiles: Profiles, pks: [Pubkey], txn: NdbTxn) -> [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(profiles: Profiles, events: [NostrEvent], cache: EventCache, txn: NdbTxn) -> [Pubkey] { +func find_profiles_to_fetch_from_events(profiles: Profiles, events: [NostrEvent], cache: EventCache) -> [Pubkey] { var pubkeys = Set() 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) } } diff --git a/damus/Features/Search/Views/PullDownSearch.swift b/damus/Features/Search/Views/PullDownSearch.swift index 2e332720..38d402e1 100644 --- a/damus/Features/Search/Views/PullDownSearch.swift +++ b/damus/Features/Search/Views/PullDownSearch.swift @@ -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) + } + } + }) } } diff --git a/damus/Features/Search/Views/SearchResultsView.swift b/damus/Features/Search/Views/SearchResultsView.swift index ce9a38b3..b11827e8 100644 --- a/damus/Features/Search/Views/SearchResultsView.swift +++ b/damus/Features/Search/Views/SearchResultsView.swift @@ -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(profiles: Profiles, contacts: Contacts, search new: String, txn: NdbTxn) -> 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(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(profiles: Profiles, contacts: Contacts, search: String, txn: NdbTxn) -> [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(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 diff --git a/damus/Features/Timeline/Models/HomeModel.swift b/damus/Features/Timeline/Models/HomeModel.swift index 8a9f07ea..3f7ea97d 100644 --- a/damus/Features/Timeline/Models/HomeModel.swift +++ b/damus/Features/Timeline/Models/HomeModel.swift @@ -120,7 +120,7 @@ class HomeModel: ContactsDelegate, ObservableObject { damus_state.contacts.delegate = self guard let latest_contact_event_id_hex = damus_state.settings.latest_contact_event_id_hex else { return } guard let latest_contact_event_id = NoteId(hex: latest_contact_event_id_hex) else { return } - guard let latest_contact_event: NdbNote = damus_state.ndb.lookup_note( latest_contact_event_id)?.unsafeUnownedValue?.to_owned() else { return } + guard let latest_contact_event: NdbNote = damus_state.ndb.lookup_note_and_copy(latest_contact_event_id) else { return } process_contact_event(state: damus_state, ev: latest_contact_event) } diff --git a/damus/Features/Timeline/Views/SideMenuView.swift b/damus/Features/Timeline/Views/SideMenuView.swift index 2b636301..6352a8ce 100644 --- a/damus/Features/Timeline/Views/SideMenuView.swift +++ b/damus/Features/Timeline/Views/SideMenuView.swift @@ -136,8 +136,7 @@ struct SideMenuView: View { var display_name: String? = nil do { - let profile_txn = damus_state.ndb.lookup_profile(damus_state.pubkey, txn_name: "top_profile") - let profile = profile_txn?.unsafeUnownedValue?.profile + let profile = damus_state.profiles.lookup(id: damus_state.pubkey) name = profile?.name display_name = profile?.display_name } diff --git a/damus/Features/Wallet/Views/NWCSettings.swift b/damus/Features/Wallet/Views/NWCSettings.swift index c72d8762..e2251161 100644 --- a/damus/Features/Wallet/Views/NWCSettings.swift +++ b/damus/Features/Wallet/Views/NWCSettings.swift @@ -244,8 +244,7 @@ struct NWCSettings: View { } } .onChange(of: model.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 = damus_state.profiles.lookup(id: damus_state.pubkey) else { return } @@ -254,10 +253,9 @@ struct NWCSettings: View { notify(.profile_updated(.manual(pubkey: self.damus_state.pubkey, profile: prof))) } .onDisappear { - 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 = damus_state.profiles.lookup(id: damus_state.pubkey), model.initial_percent != profile.damus_donation else { return diff --git a/damus/Features/Wallet/Views/TransactionsView.swift b/damus/Features/Wallet/Views/TransactionsView.swift index c71ccade..1a637977 100644 --- a/damus/Features/Wallet/Views/TransactionsView.swift +++ b/damus/Features/Wallet/Views/TransactionsView.swift @@ -104,8 +104,7 @@ struct TransactionView: View { return NSLocalizedString("Unknown", comment: "A name label for an unknown user") } - let profile_txn = damus_state.profiles.lookup(id: pubkey, txn_name: "txview-profile") - let profile = profile_txn?.unsafeUnownedValue + let profile = damus_state.profiles.lookup(id: pubkey) return Profile.displayName(profile: profile, pubkey: pubkey).displayName } diff --git a/damus/Features/Zaps/Views/ProfileZapLinkView.swift b/damus/Features/Zaps/Views/ProfileZapLinkView.swift index a09fecf2..1866c63d 100644 --- a/damus/Features/Zaps/Views/ProfileZapLinkView.swift +++ b/damus/Features/Zaps/Views/ProfileZapLinkView.swift @@ -33,21 +33,26 @@ struct ProfileZapLinkView: View { self.label = label self.action = action - let profile_txn = damus_state.profiles.lookup_with_timestamp(pubkey) - let record = profile_txn?.unsafeUnownedValue - self.reactions_enabled = record?.profile?.reactions ?? true - self.lud16 = record?.profile?.lud06?.trimmingCharacters(in: .whitespaces) - self.lnurl = record?.lnurl?.trimmingCharacters(in: .whitespaces) + let profile = damus_state.profiles.lookup(id: pubkey) + let lnurl = damus_state.profiles.lookup_with_timestamp(pubkey, borrow: { pr -> String? in + switch pr { + case .some(let pr): return pr.lnurl + case .none: return nil + } + }) + self.reactions_enabled = profile?.reactions ?? true + self.lud16 = profile?.lud06?.trimmingCharacters(in: .whitespaces) + self.lnurl = lnurl?.trimmingCharacters(in: .whitespaces) } - init(unownedProfileRecord: ProfileRecord?, profileModel: ProfileModel, action: ActionFunction? = nil, @ViewBuilder label: @escaping ContentViewFunction) { + init(profile: Profile?, lnurl: String?, profileModel: ProfileModel, action: ActionFunction? = nil, @ViewBuilder label: @escaping ContentViewFunction) { self.pubkey = profileModel.pubkey self.label = label self.action = action - self.reactions_enabled = unownedProfileRecord?.profile?.reactions ?? true - self.lud16 = unownedProfileRecord?.profile?.lud16?.trimmingCharacters(in: .whitespaces) - self.lnurl = unownedProfileRecord?.lnurl?.trimmingCharacters(in: .whitespaces) + self.reactions_enabled = profile?.reactions ?? true + self.lud16 = profile?.lud16?.trimmingCharacters(in: .whitespaces) + self.lnurl = lnurl?.trimmingCharacters(in: .whitespaces) } var body: some View { diff --git a/damus/Features/Zaps/Views/ZapTypePicker.swift b/damus/Features/Zaps/Views/ZapTypePicker.swift index a7e3b3db..dab93d33 100644 --- a/damus/Features/Zaps/Views/ZapTypePicker.swift +++ b/damus/Features/Zaps/Views/ZapTypePicker.swift @@ -97,8 +97,7 @@ func zap_type_desc(type: ZapType, profiles: Profiles, pubkey: Pubkey) -> String case .anon: 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 = profiles.lookup(id: pubkey) 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/damus/Shared/Components/NIP05Badge.swift b/damus/Shared/Components/NIP05Badge.swift index e5eb62f4..b2f190cc 100644 --- a/damus/Shared/Components/NIP05Badge.swift +++ b/damus/Shared/Components/NIP05Badge.swift @@ -60,7 +60,7 @@ struct NIP05Badge: View { } var username_matches_nip05: Bool { - guard let name = damus_state.profiles.lookup(id: pubkey)?.map({ p in p?.name }).value + guard let name = damus_state.profiles.lookup(id: pubkey)?.name else { return false } diff --git a/damus/Shared/Components/QRCodeView.swift b/damus/Shared/Components/QRCodeView.swift index 03aa2a26..127bb316 100644 --- a/damus/Shared/Components/QRCodeView.swift +++ b/damus/Shared/Components/QRCodeView.swift @@ -73,8 +73,7 @@ struct QRCodeView: View { var QRView: some View { VStack(alignment: .center) { - let profile_txn = damus_state.profiles.lookup(id: pubkey, txn_name: "qrview-profile") - let profile = profile_txn?.unsafeUnownedValue + let profile = damus_state.profiles.lookup(id: pubkey) ProfilePicView(pubkey: pubkey, size: 90.0, highlight: .custom(DamusColors.white, 3.0), profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation, damusState: damus_state) .padding(.top, 20) diff --git a/damus/Shared/Media/Images/BannerImageView.swift b/damus/Shared/Media/Images/BannerImageView.swift index 4adea6d9..04d29f9b 100644 --- a/damus/Shared/Media/Images/BannerImageView.swift +++ b/damus/Shared/Media/Images/BannerImageView.swift @@ -104,13 +104,12 @@ struct BannerImageView: 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, - let profile_txn = profiles.lookup(id: updated.pubkey) + let profile = profiles.lookup(id: updated.pubkey) else { return } - let profile = profile_txn.unsafeUnownedValue - if let bannerImage = profile?.banner, bannerImage != self.banner { + if let bannerImage = profile.banner, bannerImage != self.banner { self.banner = bannerImage } } @@ -118,7 +117,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)?.banner ?? "" if let url = URL(string: bannerUrlString) { return url } diff --git a/damus/Shared/Utilities/EventCache.swift b/damus/Shared/Utilities/EventCache.swift index 266e00af..465c9e1d 100644 --- a/damus/Shared/Utilities/EventCache.swift +++ b/damus/Shared/Utilities/EventCache.swift @@ -222,7 +222,7 @@ class EventCache { return ev } - if let ev = self.ndb.lookup_note(evid)?.unsafeUnownedValue?.to_owned() { + if let ev = self.ndb.lookup_note_and_copy(evid) { events[ev.id] = ev return ev } diff --git a/damusTests/NostrNetworkManagerTests/NostrNetworkManagerTests.swift b/damusTests/NostrNetworkManagerTests/NostrNetworkManagerTests.swift index 44e25c24..64cff8b7 100644 --- a/damusTests/NostrNetworkManagerTests/NostrNetworkManagerTests.swift +++ b/damusTests/NostrNetworkManagerTests/NostrNetworkManagerTests.swift @@ -137,11 +137,9 @@ class NostrNetworkManagerTests: XCTestCase { switch item { case .event(let noteKey): // Lookup the note to verify it exists - if let txn = NdbTxn(ndb: ndb) { - if let note = ndb.lookup_note_by_key_with_txn(noteKey, txn: txn) { - count += 1 - receivedIds.insert(note.id) - } + if let note = ndb.lookup_note_by_key_and_copy(noteKey) { + count += 1 + receivedIds.insert(note.id) } if count >= expectedCount { atLeastXNotes.fulfill() diff --git a/damusTests/NoteContentViewTests.swift b/damusTests/NoteContentViewTests.swift index 1939470c..398f61a5 100644 --- a/damusTests/NoteContentViewTests.swift +++ b/damusTests/NoteContentViewTests.swift @@ -347,15 +347,17 @@ class NoteContentViewTests: XCTestCase { func testDirectBlockParsing() { let kp = test_keypair_full let dm: NdbNote = NIP04.create_dm("Test", to_pk: kp.pubkey, tags: [], keypair: kp.to_keypair())! - let blocks = try! NdbBlockGroup.from(event: dm, using: test_damus_state.ndb, and: kp.to_keypair()) - let blockCount1 = try? blocks.withList({ $0.count }) - XCTAssertEqual(blockCount1, 1) + try! NdbBlockGroup.borrowBlockGroup(event: dm, using: test_damus_state.ndb, and: kp.to_keypair(), borrow: { blocks in + let blockCount = blocks.withList({ $0.count }) + XCTAssertEqual(blockCount, 1) + }) let post = NostrPost(content: "Test", kind: .text) let event = post.to_event(keypair: kp)! - let blocks2 = try! NdbBlockGroup.from(event: event, using: test_damus_state.ndb, and: kp.to_keypair()) - let blockCount2 = try? blocks2.withList({ $0.count }) - XCTAssertEqual(blockCount2, 1) + try! NdbBlockGroup.borrowBlockGroup(event: event, using: test_damus_state.ndb, and: kp.to_keypair(), borrow: { blocks in + let blockCount = blocks.withList({ $0.count }) + XCTAssertEqual(blockCount, 1) + }) } func testMentionStr_Pubkey_ContainsAbbreviated() throws { diff --git a/nostrdb/Ndb+.swift b/nostrdb/Ndb+.swift index 00b22beb..ed901e0f 100644 --- a/nostrdb/Ndb+.swift +++ b/nostrdb/Ndb+.swift @@ -29,8 +29,8 @@ extension Ndb { } /// Determines if a given note was seen on any of the listed relay URLs - func was(noteKey: NoteKey, seenOnAnyOf relayUrls: [RelayURL], txn: SafeNdbTxn<()>? = nil) throws -> Bool { - return try self.was(noteKey: noteKey, seenOnAnyOf: relayUrls.map({ $0.absoluteString }), txn: txn) + func was(noteKey: NoteKey, seenOnAnyOf relayUrls: [RelayURL]) throws -> Bool { + return try self.was(noteKey: noteKey, seenOnAnyOf: relayUrls.map({ $0.absoluteString })) } func processEvent(_ str: String, originRelayURL: RelayURL? = nil) -> Bool { diff --git a/nostrdb/Ndb.swift b/nostrdb/Ndb.swift index 97347658..6b165f09 100644 --- a/nostrdb/Ndb.swift +++ b/nostrdb/Ndb.swift @@ -235,7 +235,8 @@ class Ndb { return true } - func lookup_blocks_by_key_with_txn(_ key: NoteKey, txn: RawNdbTxnAccessible) -> NdbBlockGroup.BlocksMetadata? { + // GH_3245 TODO: This is a low level call, make it hidden from outside Ndb + internal func lookup_blocks_by_key_with_txn(_ key: NoteKey, txn: RawNdbTxnAccessible) -> NdbBlockGroup.BlocksMetadata? { guard let blocks = ndb_get_blocks_by_key(self.ndb.ndb, &txn.txn, key) else { return nil } @@ -243,13 +244,17 @@ class Ndb { return NdbBlockGroup.BlocksMetadata(ptr: blocks) } - func lookup_blocks_by_key(_ key: NoteKey) -> SafeNdbTxn? { - SafeNdbTxn.new(on: self) { txn in + func lookup_blocks_by_key(_ key: NoteKey, borrow lendingFunction: (_: borrowing NdbBlockGroup.BlocksMetadata?) throws -> T) rethrows -> T { + let txn = SafeNdbTxn.new(on: self) { txn in lookup_blocks_by_key_with_txn(key, txn: txn) } + guard let txn else { + return try lendingFunction(nil) + } + return try lendingFunction(txn.val) } - func lookup_note_by_key_with_txn(_ key: NoteKey, txn: NdbTxn) -> NdbNote? { + private func lookup_note_by_key_with_txn(_ key: NoteKey, txn: NdbTxn) -> NdbNote? { var size: Int = 0 guard let note_p = ndb_get_note_by_key(&txn.txn, key, &size) else { return nil @@ -411,13 +416,25 @@ class Ndb { return note_ids } - func lookup_note_by_key(_ key: NoteKey) -> NdbTxn? { - return NdbTxn(ndb: self) { txn in + func lookup_note_by_key(_ key: NoteKey, borrow lendingFunction: (_: borrowing UnownedNdbNote?) throws -> T) rethrows -> T { + let txn = NdbTxn(ndb: self) { txn in lookup_note_by_key_with_txn(key, txn: txn) } + guard let rawNote = txn?.unsafeUnownedValue else { return try lendingFunction(nil) } + let unownedNote = UnownedNdbNote(rawNote) + return try lendingFunction(.some(unownedNote)) + } + + func lookup_note_by_key_and_copy(_ key: NoteKey) -> NdbNote? { + return lookup_note_by_key(key, borrow: { maybeUnownedNote -> NdbNote? in + switch maybeUnownedNote { + case .none: return nil + case .some(let unownedNote): return unownedNote.toOwned() + } + }) } - private func lookup_profile_by_key_inner(_ key: ProfileKey, txn: NdbTxn) -> ProfileRecord? { + private func lookup_profile_by_key_inner(_ key: ProfileKey, txn: RawNdbTxnAccessible) -> ProfileRecord? { var size: Int = 0 guard let profile_p = ndb_get_profile_by_key(&txn.txn, key, &size) else { return nil @@ -451,32 +468,36 @@ class Ndb { } } - private func lookup_profile_with_txn_inner(pubkey: Pubkey, txn: NdbTxn) -> ProfileRecord? { - return pubkey.id.withUnsafeBytes { (ptr: UnsafeRawBufferPointer) -> ProfileRecord? in + private func lookup_profile_with_txn_inner(pubkey: Pubkey, txn: some RawNdbTxnAccessible) -> ProfileRecord? { + var record: ProfileRecord? = nil + pubkey.id.withUnsafeBytes { (ptr: UnsafeRawBufferPointer) in var size: Int = 0 var key: UInt64 = 0 guard let baseAddress = ptr.baseAddress, let profile_p = ndb_get_profile_by_pubkey(&txn.txn, baseAddress, &size, &key) else { - return nil + return } - return profile_flatbuf_to_record(ptr: profile_p, size: size, key: key) + record = profile_flatbuf_to_record(ptr: profile_p, size: size, key: key) } + return record } - func lookup_profile_by_key_with_txn(key: ProfileKey, txn: NdbTxn) -> ProfileRecord? { + private func lookup_profile_by_key_with_txn(key: ProfileKey, txn: RawNdbTxnAccessible) -> ProfileRecord? { lookup_profile_by_key_inner(key, txn: txn) } - func lookup_profile_by_key(key: ProfileKey) -> NdbTxn? { - return NdbTxn(ndb: self) { txn in - lookup_profile_by_key_inner(key, txn: txn) + func lookup_profile_by_key(key: ProfileKey, borrow lendingFunction: (_: borrowing ProfileRecord?) throws -> T) rethrows -> T { + let txn = SafeNdbTxn.new(on: self) { txn in + return lookup_profile_by_key_inner(key, txn: txn) } + guard let txn else { return try lendingFunction(nil) } + return try lendingFunction(txn.val) } - func lookup_note_with_txn(id: NoteId, txn: NdbTxn) -> NdbNote? { + private func lookup_note_with_txn(id: NoteId, txn: NdbTxn) -> NdbNote? { lookup_note_with_txn_inner(id: id, txn: txn) } @@ -490,7 +511,7 @@ class Ndb { return txn.value } - func lookup_profile_key_with_txn(_ pubkey: Pubkey, txn: NdbTxn) -> ProfileKey? { + private func lookup_profile_key_with_txn(_ pubkey: Pubkey, txn: NdbTxn) -> ProfileKey? { return pubkey.id.withUnsafeBytes { (ptr: UnsafeRawBufferPointer) -> NoteKey? in guard let p = ptr.baseAddress else { return nil } let r = ndb_get_profilekey_by_pubkey(&txn.txn, p) @@ -501,7 +522,8 @@ class Ndb { } } - func lookup_note_key_with_txn(_ id: NoteId, txn: some RawNdbTxnAccessible) -> NoteKey? { + // GH_3245 TODO: This is a low level call, make it hidden from outside Ndb + internal func lookup_note_key_with_txn(_ id: NoteId, txn: some RawNdbTxnAccessible) -> NoteKey? { guard !closed else { return nil } return id.id.withUnsafeBytes { (ptr: UnsafeRawBufferPointer) -> NoteKey? in guard let p = ptr.baseAddress else { @@ -525,19 +547,47 @@ class Ndb { return txn.value } - func lookup_note(_ id: NoteId, txn_name: String? = nil) -> NdbTxn? { - NdbTxn(ndb: self, name: txn_name) { txn in + func lookup_note(_ id: NoteId, borrow lendingFunction: (_: borrowing UnownedNdbNote?) throws -> T) rethrows -> T { + let txn = NdbTxn(ndb: self) { txn in lookup_note_with_txn_inner(id: id, txn: txn) } + guard let rawNote = txn?.unsafeUnownedValue else { return try lendingFunction(nil) } + return try lendingFunction(UnownedNdbNote(rawNote)) } - - func lookup_profile(_ pubkey: Pubkey, txn_name: String? = nil) -> NdbTxn? { - NdbTxn(ndb: self, name: txn_name) { txn in + + func lookup_note_and_copy(_ id: NoteId) -> NdbNote? { + return self.lookup_note(id, borrow: { unownedNote in + return unownedNote?.toOwned() + }) + } + + func lookup_profile(_ pubkey: Pubkey, borrow lendingFunction: (_: borrowing ProfileRecord?) throws -> T) rethrows -> T { + let txn = SafeNdbTxn.new(on: self) { txn in lookup_profile_with_txn_inner(pubkey: pubkey, txn: txn) } + guard let txn else { return try lendingFunction(nil) } + return try lendingFunction(txn.val) + } + + func lookup_profile_lnurl(_ pubkey: Pubkey) -> String? { + return lookup_profile(pubkey, borrow: { pr in + switch pr { + case .none: return nil + case .some(let pr): return pr.lnurl + } + }) + } + + func lookup_profile_and_copy(_ pubkey: Pubkey) -> Profile? { + return self.lookup_profile(pubkey, borrow: { pr in + switch pr { + case .some(let pr): return pr.profile + case .none: return nil + } + }) } - func lookup_profile_with_txn(_ pubkey: Pubkey, txn: NdbTxn) -> ProfileRecord? { + private func lookup_profile_with_txn(_ pubkey: Pubkey, txn: NdbTxn) -> ProfileRecord? { lookup_profile_with_txn_inner(pubkey: pubkey, txn: txn) } @@ -556,7 +606,7 @@ class Ndb { } } - func read_profile_last_fetched(txn: NdbTxn, pubkey: Pubkey) -> UInt64? { + private func read_profile_last_fetched(txn: NdbTxn, pubkey: Pubkey) -> UInt64? { guard !closed else { return nil } return pubkey.id.withUnsafeBytes { (ptr: UnsafeRawBufferPointer) -> UInt64? in guard let p = ptr.baseAddress else { return nil } @@ -568,6 +618,14 @@ class Ndb { return res } } + + func read_profile_last_fetched(pubkey: Pubkey) -> UInt64? { + var last_fetched: UInt64? = nil + let _ = NdbTxn(ndb: self) { txn in + last_fetched = read_profile_last_fetched(txn: txn, pubkey: pubkey) + } + return last_fetched + } func process_event(_ str: String, originRelayURL: String? = nil) -> Bool { guard !is_closed else { return false } @@ -592,8 +650,13 @@ class Ndb { return ndb_process_events(ndb.ndb, cstr, str.utf8.count) != 0 } } + + func search_profile(_ search: String, limit: Int) -> [Pubkey] { + guard let txn = NdbTxn<()>.init(ndb: self) else { return [] } + return search_profile(search, limit: limit, txn: txn) + } - func search_profile(_ search: String, limit: Int, txn: NdbTxn) -> [Pubkey] { + private func search_profile(_ search: String, limit: Int, txn: NdbTxn) -> [Pubkey] { var pks = Array() return search.withCString { q in @@ -621,6 +684,11 @@ class Ndb { // MARK: NdbFilter queries and subscriptions + func query(filters: [NdbFilter], maxResults: Int) throws(NdbStreamError) -> [NoteKey] { + guard let txn = NdbTxn(ndb: self) else { return [] } + return try query(with: txn, filters: filters, maxResults: maxResults) + } + /// Safe wrapper around the `ndb_query` C function /// - Parameters: /// - txn: Database transaction @@ -628,7 +696,7 @@ class Ndb { /// - maxResults: Maximum number of results to return /// - Returns: Array of note keys matching the filters /// - Throws: NdbStreamError if the query fails - func query(with txn: NdbTxn, filters: [NdbFilter], maxResults: Int) throws(NdbStreamError) -> [NoteKey] { + private func query(with txn: NdbTxn, filters: [NdbFilter], maxResults: Int) throws(NdbStreamError) -> [NoteKey] { guard !self.is_closed else { throw .ndbClosed } let filtersPointer = UnsafeMutablePointer.allocate(capacity: filters.count) defer { filtersPointer.deallocate() } @@ -784,60 +852,20 @@ class Ndb { return nil } - func waitFor(noteId: NoteId, timeout: TimeInterval = 10) async throws(NdbLookupError) -> NdbTxn? { - do { - return try await withCheckedThrowingContinuation({ continuation in - var done = false - let waitTask = Task { - do { - Log.debug("ndb_wait: Waiting for %s", for: .ndb, noteId.hex()) - let result = try await self.waitWithoutTimeout(for: noteId) - if !done { - Log.debug("ndb_wait: Found %s", for: .ndb, noteId.hex()) - continuation.resume(returning: result) - done = true - } - } - catch { - if Task.isCancelled { - return // the timeout task will handle throwing the timeout error - } - if !done { - Log.debug("ndb_wait: Error on %s: %s", for: .ndb, noteId.hex(), error.localizedDescription) - continuation.resume(throwing: error) - done = true - } - } - } - - let timeoutTask = Task { - try await Task.sleep(for: .seconds(Int(timeout))) - if !done { - Log.debug("ndb_wait: Timeout on %s. Cancelling wait task…", for: .ndb, noteId.hex()) - done = true - print("ndb_wait: throwing timeout error") - continuation.resume(throwing: NdbLookupError.timeout) - } - waitTask.cancel() - } - }) - } - catch { - if let error = error as? NdbLookupError { throw error } - else { throw .internalInconsistency } - } - } - /// Determines if a given note was seen on a specific relay URL - func was(noteKey: NoteKey, seenOn relayUrl: String, txn: SafeNdbTxn<()>? = nil) throws -> Bool { + private func was(noteKey: NoteKey, seenOn relayUrl: String, txn: SafeNdbTxn<()>?) throws -> Bool { guard let txn = txn ?? SafeNdbTxn.new(on: self) else { throw NdbLookupError.cannotOpenTransaction } return relayUrl.withCString({ relayCString in return ndb_note_seen_on_relay(&txn.txn, noteKey, relayCString) == 1 }) } + func was(noteKey: NoteKey, seenOn relayUrl: String) throws -> Bool { + return try self.was(noteKey: noteKey, seenOn: relayUrl, txn: nil) + } + /// Determines if a given note was seen on any of the listed relay URLs - func was(noteKey: NoteKey, seenOnAnyOf relayUrls: [String], txn: SafeNdbTxn<()>? = nil) throws -> Bool { + private func was(noteKey: NoteKey, seenOnAnyOf relayUrls: [String], txn: SafeNdbTxn<()>? = nil) throws -> Bool { guard let txn = txn ?? SafeNdbTxn.new(on: self) else { throw NdbLookupError.cannotOpenTransaction } for relayUrl in relayUrls { if try self.was(noteKey: noteKey, seenOn: relayUrl, txn: txn) { @@ -847,6 +875,11 @@ class Ndb { return false } + /// Determines if a given note was seen on any of the listed relay URLs + func was(noteKey: NoteKey, seenOnAnyOf relayUrls: [String]) throws -> Bool { + return try self.was(noteKey: noteKey, seenOnAnyOf: relayUrls, txn: nil) + } + // MARK: Internal ndb callback interfaces internal func setContinuation(for subscriptionId: UInt64, continuation: AsyncStream.Continuation) async { diff --git a/nostrdb/NdbBlock.swift b/nostrdb/NdbBlock.swift index 08b71bd6..ca10a305 100644 --- a/nostrdb/NdbBlock.swift +++ b/nostrdb/NdbBlock.swift @@ -104,11 +104,16 @@ enum NdbBlock: ~Copyable { /// Represents a group of blocks struct NdbBlockGroup: ~Copyable { /// The block offsets - fileprivate let metadata: MaybeTxn + fileprivate let metadata: BlocksMetadata /// The raw text content of the note fileprivate let rawTextContent: String var words: Int { - return metadata.borrow { $0.words } + return metadata.words + } + + init(metadata: consuming BlocksMetadata, rawTextContent: String) { + self.metadata = metadata + self.rawTextContent = rawTextContent } /// Gets the parsed blocks from a specific note. @@ -116,18 +121,20 @@ struct NdbBlockGroup: ~Copyable { /// This function will: /// - fetch blocks information from NostrDB if possible _and_ available, or /// - parse blocks on-demand. - static func from(event: NdbNote, using ndb: Ndb, and keypair: Keypair) throws(NdbBlocksError) -> Self { + static func borrowBlockGroup(event: NdbNote, using ndb: Ndb, and keypair: Keypair, borrow lendingFunction: (_: borrowing Self) throws -> T) throws -> T { if event.is_content_encrypted() { - return try parse(event: event, keypair: keypair) + return try lendingFunction(parse(event: event, keypair: keypair)) } else if event.known_kind == .highlight { - return try parse(event: event, keypair: keypair) + return try lendingFunction(parse(event: event, keypair: keypair)) } else { - guard let offsets = event.block_offsets(ndb: ndb) else { - return try parse(event: event, keypair: keypair) - } - return .init(metadata: .txn(offsets), rawTextContent: event.content) + return try ndb.lookup_block_group_by_key(event: event, borrow: { group in + switch group { + case .none: return try lendingFunction(parse(event: event, keypair: keypair)) + case .some(let group): return try lendingFunction(group) + } + }) } } @@ -136,34 +143,44 @@ struct NdbBlockGroup: ~Copyable { /// Prioritize using `from(event: NdbNote, using ndb: Ndb, and keypair: Keypair)` when possible. static func parse(event: NdbNote, keypair: Keypair) throws(NdbBlocksError) -> Self { guard let content = event.maybe_get_content(keypair) else { throw NdbBlocksError.decryptionError } - guard let metadata = BlocksMetadata.parseContent(content: content) else { throw NdbBlocksError.parseError } + guard var metadata = BlocksMetadata.parseContent(content: content) else { throw NdbBlocksError.parseError } return self.init( - metadata: .pure(metadata), + metadata: metadata, rawTextContent: content ) } /// Parses the note contents on-demand from a specific text. static func parse(content: String) throws(NdbBlocksError) -> Self { - guard let metadata = BlocksMetadata.parseContent(content: content) else { throw NdbBlocksError.parseError } + guard var metadata = BlocksMetadata.parseContent(content: content) else { throw NdbBlocksError.parseError } return self.init( - metadata: .pure(metadata), + metadata: metadata, rawTextContent: content ) } } -enum MaybeTxn: ~Copyable { - case pure(T) - case txn(SafeNdbTxn) - - func borrow(_ borrowFunction: (borrowing T) throws -> Y) rethrows -> Y { - switch self { - case .pure(let item): - return try borrowFunction(item) - case .txn(let txn): - return try borrowFunction(txn.val) +// MARK: - Extensions enabling low-level control + +fileprivate extension Ndb { + func lookup_block_group_by_key(event: NdbNote, borrow lendingFunction: sending (_: borrowing NdbBlockGroup?) throws -> T) rethrows -> T { + let txn = SafeNdbTxn.new(on: self) { txn in + guard let key = lookup_note_key_with_txn(event.id, txn: txn) else { return nil } + return lookup_block_group_by_key_with_txn(key, event: event, txn: txn) } + guard let txn else { + return try lendingFunction(nil) + } + return try lendingFunction(txn.val) + } + + func lookup_block_group_by_key_with_txn(_ key: NoteKey, event: NdbNote, txn: RawNdbTxnAccessible) -> NdbBlockGroup? { + guard let blocks = ndb_get_blocks_by_key(self.ndb.ndb, &txn.txn, key) else { + return nil + } + + let metadata = NdbBlockGroup.BlocksMetadata(ptr: blocks) + return NdbBlockGroup(metadata: metadata, rawTextContent: event.content) } } @@ -174,8 +191,6 @@ extension NdbBlockGroup { /// Wrapper for the `ndb_blocks` C struct /// /// This does not store the actual block contents, only the offsets on the content string and block metadata. - /// - /// **Implementation note:** This would be better as `~Copyable`, but `NdbTxn` does not support `~Copyable` yet. struct BlocksMetadata: ~Copyable { private let blocks_ptr: ndb_blocks_ptr private let buffer: UnsafeMutableRawPointer? @@ -290,17 +305,14 @@ extension NdbBlockGroup { var iter = ndb_block_iterator(content: cptr, blocks: nil, block: ndb_block(), p: nil) // Start the iteration - return try self.metadata.borrow { value in - ndb_blocks_iterate_start(cptr, value.as_ptr(), &iter) - - // Collect blocks into array - outerLoop: while let ptr = ndb_blocks_iterate_next(&iter), - let block = NdbBlock(ndb_block_ptr(ptr: ptr)) { - linkedList.add(item: block) - } - - return try borrowingFunction(linkedList) + ndb_blocks_iterate_start(cptr, self.metadata.as_ptr(), &iter) + + // Collect blocks into array + outerLoop: while let ptr = ndb_blocks_iterate_next(&iter), + let block = NdbBlock(ndb_block_ptr(ptr: ptr)) { + linkedList.add(item: block) } + return try borrowingFunction(linkedList) } } } diff --git a/nostrdb/NdbNote.swift b/nostrdb/NdbNote.swift index 1291b46a..fa4e5aef 100644 --- a/nostrdb/NdbNote.swift +++ b/nostrdb/NdbNote.swift @@ -73,6 +73,10 @@ class NdbNote: Codable, Equatable, Hashable { } #endif } + + func clone() -> NdbNote { + return self.to_owned() + } func to_owned() -> NdbNote { if self.owned { @@ -474,17 +478,12 @@ extension NdbNote { return ThreadReply(tags: self.tags)?.reply.note_id } - func block_offsets(ndb: Ndb) -> SafeNdbTxn? { - let blocks_txn: SafeNdbTxn? = .new(on: ndb) { txn -> NdbBlockGroup.BlocksMetadata? in - guard let key = ndb.lookup_note_key_with_txn(self.id, txn: txn) else { - return nil - } - return ndb.lookup_blocks_by_key_with_txn(key, txn: txn) - } - - guard let blocks_txn else { return nil } - - return blocks_txn + func block_offsets(ndb: Ndb, borrow lendingFunction: (_: borrowing NdbBlockGroup.BlocksMetadata?) throws -> T) rethrows -> T { + guard let key = ndb.lookup_note_key(self.id) else { return try lendingFunction(nil) } + + return try ndb.lookup_blocks_by_key(key, borrow: { blocks in + return try lendingFunction(blocks) + }) } func is_content_encrypted() -> Bool { diff --git a/nostrdb/NdbTxn.swift b/nostrdb/NdbTxn.swift index e4130fe6..4e32b2e0 100644 --- a/nostrdb/NdbTxn.swift +++ b/nostrdb/NdbTxn.swift @@ -78,7 +78,7 @@ class NdbTxn: RawNdbTxnAccessible { /// Only access temporarily! Do not store database references for longterm use. If it's a primitive type you /// can retrieve this value with `.value` - var unsafeUnownedValue: T { + internal var unsafeUnownedValue: T { precondition(!moved) return val } diff --git a/nostrdb/Test/NdbTests.swift b/nostrdb/Test/NdbTests.swift index e4f82c05..87c17b68 100644 --- a/nostrdb/Test/NdbTests.swift +++ b/nostrdb/Test/NdbTests.swift @@ -64,18 +64,19 @@ final class NdbTests: XCTestCase { let ndb = Ndb(path: db_dir)! let id = NoteId(hex: "d12c17bde3094ad32f4ab862a6cc6f5c289cfe7d5802270bdf34904df585f349")! guard let txn = NdbTxn(ndb: ndb) else { return XCTAssert(false) } - let note = ndb.lookup_note_with_txn(id: id, txn: txn) + let note = ndb.lookup_note_and_copy(id) XCTAssertNotNil(note) guard let note else { return } let pk = Pubkey(hex: "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245")! XCTAssertEqual(note.pubkey, pk) - let profile = ndb.lookup_profile_with_txn(pk, txn: txn) + let profile = ndb.lookup_profile_and_copy(pk) + let lnurl = ndb.lookup_profile_lnurl(pk) XCTAssertNotNil(profile) guard let profile else { return } - XCTAssertEqual(profile.profile?.name, "jb55") - XCTAssertEqual(profile.lnurl, nil) + XCTAssertEqual(profile.name, "jb55") + XCTAssertEqual(lnurl, nil) } @@ -97,7 +98,12 @@ final class NdbTests: XCTestCase { XCTFail("Expected at least one note to be found") return } - let note_id = ndb.lookup_note_by_key(note_ids[0])?.map({ n in n?.id }).value + let note_id = ndb.lookup_note_by_key(note_ids[0], borrow: { maybeUnownedNote -> NoteId? in + switch maybeUnownedNote { + case .none: return nil + case .some(let unownedNote): return unownedNote.id + } + }) XCTAssertEqual(note_id, .some(expected_note_id)) } } diff --git a/nostrdb/UnownedNdbNote.swift b/nostrdb/UnownedNdbNote.swift index e3d8c418..bd0a68bf 100644 --- a/nostrdb/UnownedNdbNote.swift +++ b/nostrdb/UnownedNdbNote.swift @@ -58,9 +58,12 @@ enum NdbNoteLender: Sendable { switch self { case .ndbNoteKey(let ndb, let noteKey): guard !ndb.is_closed else { throw LendingError.ndbClosed } - guard let ndbNoteTxn = ndb.lookup_note_by_key(noteKey) else { throw LendingError.errorLoadingNote } - guard let unownedNote = UnownedNdbNote(ndbNoteTxn) else { throw LendingError.errorLoadingNote } - return try lendingFunction(unownedNote) + return try ndb.lookup_note_by_key(noteKey, borrow: { maybeUnownedNote in + switch maybeUnownedNote { + case .none: throw LendingError.errorLoadingNote + case .some(let unownedNote): return try lendingFunction(unownedNote) + } + }) case .owned(let note): return try lendingFunction(UnownedNdbNote(note)) }