Redesign Ndb.swift interface with build safety

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

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

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

Closes: https://github.com/damus-io/damus/issues/3364
Changelog-Fixed: Fixed several crashes throughout the app
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
This commit is contained in:
Daniel D’Aquino
2025-11-28 19:17:35 -08:00
parent b562b930cc
commit f844ed9931
60 changed files with 611 additions and 497 deletions
@@ -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))
@@ -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
+4 -8
View File
@@ -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 {
@@ -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<ProfileRecord?>
typealias ProfileStreamItem = Profile
struct ProfileStreamInfo {
let id: UUID = UUID()
@@ -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)
@@ -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.
+59 -8
View File
@@ -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))
}
+27 -28
View File
@@ -810,42 +810,41 @@ func validate_event(ev: NostrEvent) -> ValidationResult {
}
func first_eref_mention(ndb: Ndb, ev: NostrEvent, keypair: Keypair) -> Mention<NoteId>? {
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<NoteId>.note(noteId, index: index))
case .nevent(let nEvent):
return .loopReturn(Mention<NoteId>.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<NoteId>.note(noteId, index: index))
case .nevent(let nEvent):
return .loopReturn(Mention<NoteId>.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
})
}
/**
+27 -13
View File
@@ -74,31 +74,45 @@ class Profiles {
profile_data(pubkey).zapper
}
func lookup_with_timestamp(_ pubkey: Pubkey) -> NdbTxn<ProfileRecord?>? {
ndb.lookup_profile(pubkey)
func lookup_with_timestamp<T>(_ 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<ProfileRecord?>? {
ndb.lookup_profile_by_key(key: key)
func lookup_by_key<T>(key: ProfileKey, borrow lendingFunction: (_: borrowing ProfileRecord?) throws -> T) rethrows -> T {
return try ndb.lookup_profile_by_key(key: key, borrow: lendingFunction)
}
func search<Y>(_ query: String, limit: Int, txn: NdbTxn<Y>) -> [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<Profile?>? {
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<Y>(id: Pubkey, txn: NdbTxn<Y>) -> 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
}
@@ -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
}
+1 -3
View File
@@ -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)
@@ -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]
@@ -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)
}
+53 -58
View File
@@ -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<Pubkey> = []
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<URL>()) { 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<URL>()) { 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 {
@@ -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):
@@ -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):
@@ -22,11 +22,11 @@ class FollowingModel {
self.hashtags = hashtags
}
func get_filter<Y>(txn: NdbTxn<Y>) -> NostrFilter {
func get_filter() -> NostrFilter {
var f = NostrFilter(kinds: [.metadata])
f.authors = self.contacts.reduce(into: Array<Pubkey>()) { 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<Y>(txn: NdbTxn<Y>) {
let filter = get_filter(txn: txn)
func subscribe() {
let filter = get_filter()
if (filter.authors?.count ?? 0) == 0 {
needs_sub = false
return
@@ -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()
@@ -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))
@@ -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)
@@ -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):
@@ -47,9 +47,7 @@ class NIP05DomainEventsModel: ObservableObject {
var authors = Set<Pubkey>()
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 {
@@ -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)
}
@@ -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? {
@@ -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
}
@@ -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)
+1 -2
View File
@@ -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)
}
+1 -1
View File
@@ -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: " ")
@@ -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)
@@ -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
@@ -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 {
@@ -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<T>(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)
}
@@ -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)
@@ -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:
@@ -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
}
@@ -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))
+17 -20
View File
@@ -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
@@ -121,8 +121,7 @@ struct DamusPurpleAccountView: View {
}
func profile_display_name() -> String {
let profile_txn: NdbTxn<ProfileRecord?>? = 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
}
@@ -110,29 +110,29 @@ class SearchHomeModel: ObservableObject {
}
}
func find_profiles_to_fetch<Y>(profiles: Profiles, load: PubkeysToLoad, cache: EventCache, txn: NdbTxn<Y>) -> [Pubkey] {
func find_profiles_to_fetch(profiles: Profiles, load: PubkeysToLoad, cache: EventCache) -> [Pubkey] {
switch load {
case .from_events(let events):
return find_profiles_to_fetch_from_events(profiles: profiles, events: events, cache: cache, txn: txn)
return find_profiles_to_fetch_from_events(profiles: profiles, events: events, cache: cache)
case .from_keys(let pks):
return find_profiles_to_fetch_from_keys(profiles: profiles, pks: pks, txn: txn)
return find_profiles_to_fetch_from_keys(profiles: profiles, pks: pks)
}
}
func find_profiles_to_fetch_from_keys<Y>(profiles: Profiles, pks: [Pubkey], txn: NdbTxn<Y>) -> [Pubkey] {
Array(Set(pks.filter { pk in !profiles.has_fresh_profile(id: pk, txn: txn) }))
func find_profiles_to_fetch_from_keys(profiles: Profiles, pks: [Pubkey]) -> [Pubkey] {
Array(Set(pks.filter { pk in !profiles.has_fresh_profile(id: pk) }))
}
func find_profiles_to_fetch_from_events<Y>(profiles: Profiles, events: [NostrEvent], cache: EventCache, txn: NdbTxn<Y>) -> [Pubkey] {
func find_profiles_to_fetch_from_events(profiles: Profiles, events: [NostrEvent], cache: EventCache) -> [Pubkey] {
var pubkeys = Set<Pubkey>()
for ev in events {
// lookup profiles from boosted events
if ev.known_kind == .boost, let bev = ev.get_inner_event(cache: cache), !profiles.has_fresh_profile(id: bev.pubkey, txn: txn) {
if ev.known_kind == .boost, let bev = ev.get_inner_event(cache: cache), !profiles.has_fresh_profile(id: bev.pubkey) {
pubkeys.insert(bev.pubkey)
}
if !profiles.has_fresh_profile(id: ev.pubkey, txn: txn) {
if !profiles.has_fresh_profile(id: ev.pubkey) {
pubkeys.insert(ev.pubkey)
}
}
@@ -31,17 +31,18 @@ struct PullDownSearchView: View {
}
do {
guard let txn = NdbTxn(ndb: state.ndb) else { return }
for note_key in note_keys {
guard let note = state.ndb.lookup_note_by_key_with_txn(note_key, txn: txn) else {
continue
}
if !keyset.contains(note_key) {
let owned_note = note.to_owned()
res.append(owned_note)
keyset.insert(note_key)
}
state.ndb.lookup_note_by_key(note_key, borrow: { maybeUnownedNote in
switch maybeUnownedNote {
case .none: return // Skip this
case .some(let unownedNote):
if !keyset.contains(note_key) {
let owned_note = unownedNote.toOwned()
res.append(owned_note)
keyset.insert(note_key)
}
}
})
}
}
@@ -154,17 +154,18 @@ struct SearchResultsView: View {
}
do {
guard let txn = NdbTxn(ndb: damus_state.ndb) else { return }
for note_key in note_keys {
guard let note = damus_state.ndb.lookup_note_by_key_with_txn(note_key, txn: txn) else {
continue
}
if !keyset.contains(note_key) {
let owned_note = note.to_owned()
res.append(owned_note)
keyset.insert(note_key)
}
damus_state.ndb.lookup_note_by_key(note_key, borrow: { maybeUnownedNote in
switch maybeUnownedNote {
case .none: return
case .some(let unownedNote):
if !keyset.contains(note_key) {
let owned_note = unownedNote.toOwned()
res.append(owned_note)
keyset.insert(note_key)
}
}
})
}
}
@@ -182,12 +183,10 @@ struct SearchResultsView: View {
}
.frame(maxHeight: .infinity)
.onAppear {
guard let txn = NdbTxn.init(ndb: damus_state.ndb) else { return }
self.result = search_for_string(profiles: damus_state.profiles, contacts: damus_state.contacts, search: search, txn: txn)
self.result = search_for_string(profiles: damus_state.profiles, contacts: damus_state.contacts, search: search)
}
.onChange(of: search) { new in
guard let txn = NdbTxn.init(ndb: damus_state.ndb) else { return }
self.result = search_for_string(profiles: damus_state.profiles, contacts: damus_state.contacts, search: search, txn: txn)
self.result = search_for_string(profiles: damus_state.profiles, contacts: damus_state.contacts, search: search)
}
.onChange(of: search) { query in
debouncer.debounce {
@@ -208,7 +207,7 @@ struct SearchResultsView_Previews: PreviewProvider {
*/
func search_for_string<Y>(profiles: Profiles, contacts: Contacts, search new: String, txn: NdbTxn<Y>) -> Search? {
func search_for_string(profiles: Profiles, contacts: Contacts, search new: String) -> Search? {
guard new.count != 0 else {
return nil
}
@@ -251,7 +250,7 @@ func search_for_string<Y>(profiles: Profiles, contacts: Contacts, search new: St
return .naddr(naddr)
}
let multisearch = MultiSearch(text: new, hashtag: make_hashtagable(searchQuery), profiles: search_profiles(profiles: profiles, contacts: contacts, search: new, txn: txn))
let multisearch = MultiSearch(text: new, hashtag: make_hashtagable(searchQuery), profiles: search_profiles(profiles: profiles, contacts: contacts, search: new))
return .multi(multisearch)
}
@@ -268,7 +267,7 @@ func make_hashtagable(_ str: String) -> String {
return String(new.filter{$0 != " "})
}
func search_profiles<Y>(profiles: Profiles, contacts: Contacts, search: String, txn: NdbTxn<Y>) -> [Pubkey] {
func search_profiles(profiles: Profiles, contacts: Contacts, search: String) -> [Pubkey] {
// Search by hex pubkey.
if let pubkey = hex_decode_pubkey(search),
profiles.lookup_key_by_pubkey(pubkey) != nil
@@ -285,7 +284,7 @@ func search_profiles<Y>(profiles: Profiles, contacts: Contacts, search: String,
return [pk]
}
return profiles.search(search, limit: 128, txn: txn).sorted { a, b in
return profiles.search(search, limit: 128).sorted { a, b in
let aFriendTypePriority = get_friend_type(contacts: contacts, pubkey: a)?.priority ?? 0
let bFriendTypePriority = get_friend_type(contacts: contacts, pubkey: b)?.priority ?? 0
@@ -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)
}
@@ -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
}
@@ -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
@@ -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
}
@@ -33,21 +33,26 @@ struct ProfileZapLinkView<Content: View>: 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 {
@@ -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:
+1 -1
View File
@@ -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
}
+1 -2
View File
@@ -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)
@@ -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
}
+1 -1
View File
@@ -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
}
@@ -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()
+8 -6
View File
@@ -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 {
+2 -2
View File
@@ -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 {
+106 -73
View File
@@ -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<NdbBlockGroup.BlocksMetadata?>? {
SafeNdbTxn<NdbBlockGroup.BlocksMetadata?>.new(on: self) { txn in
func lookup_blocks_by_key<T>(_ key: NoteKey, borrow lendingFunction: (_: borrowing NdbBlockGroup.BlocksMetadata?) throws -> T) rethrows -> T {
let txn = SafeNdbTxn<NdbBlockGroup.BlocksMetadata?>.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<Y>(_ key: NoteKey, txn: NdbTxn<Y>) -> NdbNote? {
private func lookup_note_by_key_with_txn<Y>(_ key: NoteKey, txn: NdbTxn<Y>) -> 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<NdbNote?>? {
return NdbTxn(ndb: self) { txn in
func lookup_note_by_key<T>(_ 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<Y>(_ key: ProfileKey, txn: NdbTxn<Y>) -> 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<Y>(pubkey: Pubkey, txn: NdbTxn<Y>) -> 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<Y>(key: ProfileKey, txn: NdbTxn<Y>) -> 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<ProfileRecord?>? {
return NdbTxn(ndb: self) { txn in
lookup_profile_by_key_inner(key, txn: txn)
func lookup_profile_by_key<T>(key: ProfileKey, borrow lendingFunction: (_: borrowing ProfileRecord?) throws -> T) rethrows -> T {
let txn = SafeNdbTxn<ProfileRecord?>.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<Y>(id: NoteId, txn: NdbTxn<Y>) -> NdbNote? {
private func lookup_note_with_txn<Y>(id: NoteId, txn: NdbTxn<Y>) -> 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<Y>(_ pubkey: Pubkey, txn: NdbTxn<Y>) -> ProfileKey? {
private func lookup_profile_key_with_txn<Y>(_ pubkey: Pubkey, txn: NdbTxn<Y>) -> 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<NdbNote?>? {
NdbTxn(ndb: self, name: txn_name) { txn in
func lookup_note<T>(_ 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<ProfileRecord?>? {
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<T>(_ pubkey: Pubkey, borrow lendingFunction: (_: borrowing ProfileRecord?) throws -> T) rethrows -> T {
let txn = SafeNdbTxn<ProfileRecord?>.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<Y>(_ pubkey: Pubkey, txn: NdbTxn<Y>) -> ProfileRecord? {
private func lookup_profile_with_txn<Y>(_ pubkey: Pubkey, txn: NdbTxn<Y>) -> ProfileRecord? {
lookup_profile_with_txn_inner(pubkey: pubkey, txn: txn)
}
@@ -556,7 +606,7 @@ class Ndb {
}
}
func read_profile_last_fetched<Y>(txn: NdbTxn<Y>, pubkey: Pubkey) -> UInt64? {
private func read_profile_last_fetched<Y>(txn: NdbTxn<Y>, 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<Y>(_ search: String, limit: Int, txn: NdbTxn<Y>) -> [Pubkey] {
private func search_profile<Y>(_ search: String, limit: Int, txn: NdbTxn<Y>) -> [Pubkey] {
var pks = Array<Pubkey>()
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<Y>(with txn: NdbTxn<Y>, filters: [NdbFilter], maxResults: Int) throws(NdbStreamError) -> [NoteKey] {
private func query<Y>(with txn: NdbTxn<Y>, filters: [NdbFilter], maxResults: Int) throws(NdbStreamError) -> [NoteKey] {
guard !self.is_closed else { throw .ndbClosed }
let filtersPointer = UnsafeMutablePointer<ndb_filter>.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<NdbNote>? {
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<NoteKey>.Continuation) async {
+47 -35
View File
@@ -104,11 +104,16 @@ enum NdbBlock: ~Copyable {
/// Represents a group of blocks
struct NdbBlockGroup: ~Copyable {
/// The block offsets
fileprivate let metadata: MaybeTxn<BlocksMetadata>
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<T>(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<T: ~Copyable>: ~Copyable {
case pure(T)
case txn(SafeNdbTxn<T>)
func borrow<Y>(_ 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<T>(event: NdbNote, borrow lendingFunction: sending (_: borrowing NdbBlockGroup?) throws -> T) rethrows -> T {
let txn = SafeNdbTxn<NdbBlockGroup?>.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)
}
}
}
+10 -11
View File
@@ -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<NdbBlockGroup.BlocksMetadata>? {
let blocks_txn: SafeNdbTxn<NdbBlockGroup.BlocksMetadata>? = .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<T>(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 {
+1 -1
View File
@@ -78,7 +78,7 @@ class NdbTxn<T>: 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
}
+11 -5
View File
@@ -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))
}
}
+6 -3
View File
@@ -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))
}