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)) }