ndb/txn: make transactions failable

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

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

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

Fixes: da2bdad18d ("nostrdb: close database when backgrounded")
This commit is contained in:
William Casarin
2023-12-12 12:45:56 -08:00
parent 227734d286
commit bfad2ab42d
42 changed files with 126 additions and 107 deletions

View File

@@ -118,9 +118,9 @@ struct NotificationFormatter {
let src = zap.request.ev let src = zap.request.ev
let pk = zap.is_anon ? ANON_PUBKEY : src.pubkey let pk = zap.is_anon ? ANON_PUBKEY : src.pubkey
let name = profiles.lookup(id: pk).map { profile in let profile_txn = profiles.lookup(id: pk)
Profile.displayName(profile: profile, pubkey: pk).displayName.truncate(maxLength: 50) let profile = profile_txn?.unsafeUnownedValue
}.value let name = Profile.displayName(profile: profile, pubkey: pk).displayName.truncate(maxLength: 50)
let sats = NSNumber(value: (Double(zap.invoice.amount) / 1000.0)) let sats = NSNumber(value: (Double(zap.invoice.amount) / 1000.0))
let formattedSats = format_msats_abbrev(zap.invoice.amount) let formattedSats = format_msats_abbrev(zap.invoice.amount)

View File

@@ -28,7 +28,7 @@ class NotificationService: UNNotificationServiceExtension {
Log.debug("Got nostr event push notification from pubkey %s", for: .push_notifications, nostr_event.pubkey.hex()) Log.debug("Got nostr event push notification from pubkey %s", for: .push_notifications, nostr_event.pubkey.hex())
guard let state = NotificationExtensionState(), guard let state = NotificationExtensionState(),
let display_name = state.ndb.lookup_profile(nostr_event.pubkey).unsafeUnownedValue?.profile?.display_name // We are not holding the txn here. let display_name = state.ndb.lookup_profile(nostr_event.pubkey)?.unsafeUnownedValue?.profile?.display_name // We are not holding the txn here.
else { else {
// Something failed to initialize so let's go for the next best thing // Something failed to initialize so let's go for the next best thing
guard let improved_content = NotificationFormatter.shared.format_message(event: nostr_event) else { guard let improved_content = NotificationFormatter.shared.format_message(event: nostr_event) else {

View File

@@ -45,7 +45,7 @@ struct NIP05Badge: View {
} }
var username_matches_nip05: Bool { var username_matches_nip05: Bool {
guard let name = profiles.lookup(id: pubkey).map({ p in p?.name }).value guard let name = profiles.lookup(id: pubkey)?.map({ p in p?.name }).value
else { else {
return false return false
} }

View File

@@ -370,14 +370,9 @@ struct ContentView: View {
// wallet with an associated // wallet with an associated
guard let ds = self.damus_state, guard let ds = self.damus_state,
let lud16 = nwc.lud16, let lud16 = nwc.lud16,
let keypair = ds.keypair.to_full() let keypair = ds.keypair.to_full(),
else { let profile_txn = ds.profiles.lookup(id: ds.pubkey),
return let profile = profile_txn.unsafeUnownedValue,
}
let profile_txn = ds.profiles.lookup(id: ds.pubkey)
guard let profile = profile_txn.unsafeUnownedValue,
lud16 != profile.lud16 else { lud16 != profile.lud16 else {
return return
} }
@@ -514,10 +509,9 @@ struct ContentView: View {
.onReceive(handle_notify(.onlyzaps_mode)) { hide in .onReceive(handle_notify(.onlyzaps_mode)) { hide in
home.filter_events() home.filter_events()
guard let ds = damus_state else { return } guard let ds = damus_state,
let profile_txn = ds.profiles.lookup(id: ds.pubkey) let profile_txn = ds.profiles.lookup(id: ds.pubkey),
let profile = profile_txn.unsafeUnownedValue,
guard let profile = profile_txn.unsafeUnownedValue,
let keypair = ds.keypair.to_full() let keypair = ds.keypair.to_full()
else { else {
return return
@@ -534,9 +528,9 @@ struct ContentView: View {
} }
}, message: { }, message: {
if let pubkey = self.muting { if let pubkey = self.muting {
let name = damus_state!.profiles.lookup(id: pubkey).map { profile in let profile_txn = damus_state!.profiles.lookup(id: pubkey)
Profile.displayName(profile: profile, pubkey: pubkey).username.truncate(maxLength: 50) let profile = profile_txn?.unsafeUnownedValue
}.value 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.") Text("\(name) has been muted", comment: "Alert message that informs a user was muted.")
} else { } else {
Text("User has been muted", comment: "Alert message that informs a user was muted.") Text("User has been muted", comment: "Alert message that informs a user was muted.")
@@ -595,9 +589,9 @@ struct ContentView: View {
} }
}, message: { }, message: {
if let pubkey = muting { if let pubkey = muting {
let name = damus_state?.profiles.lookup(id: pubkey).map({ profile in let profile_txn = damus_state?.profiles.lookup(id: pubkey)
Profile.displayName(profile: profile, pubkey: pubkey).username.truncate(maxLength: 50) let profile = profile_txn?.unsafeUnownedValue
}).value ?? "unknown" 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.") Text("Mute \(name)?", comment: "Alert message prompt to ask if a user should be muted.")
} else { } else {
Text("Could not find user to mute...", comment: "Alert message to indicate that the muted user could not be found.") Text("Could not find user to mute...", comment: "Alert message to indicate that the muted user could not be found.")
@@ -865,7 +859,8 @@ func find_event_with_subid(state: DamusState, query query_: FindEvent, subid: St
switch query { switch query {
case .profile(let pubkey): case .profile(let pubkey):
if let record = state.ndb.lookup_profile(pubkey).unsafeUnownedValue, if let profile_txn = state.ndb.lookup_profile(pubkey),
let record = profile_txn.unsafeUnownedValue,
record.profile != nil record.profile != nil
{ {
callback(.profile(pubkey)) callback(.profile(pubkey))

View File

@@ -66,7 +66,9 @@ class EventsModel: ObservableObject {
case .auth: case .auth:
break break
case .eose: case .eose:
let txn = NdbTxn(ndb: self.state.ndb) guard let txn = NdbTxn(ndb: self.state.ndb) else {
return
}
load_profiles(context: "events_model", profiles_subid: profiles_id, relay_id: relay_id, load: .from_events(events), damus_state: state, txn: txn) load_profiles(context: "events_model", profiles_subid: profiles_id, relay_id: relay_id, load: .from_events(events), damus_state: state, txn: txn)
} }
} }

View File

@@ -83,7 +83,7 @@ class FollowersModel: ObservableObject {
case .eose(let sub_id): case .eose(let sub_id):
if sub_id == self.sub_id { if sub_id == self.sub_id {
let txn = NdbTxn(ndb: self.damus_state.ndb) guard let txn = NdbTxn(ndb: self.damus_state.ndb) else { return }
load_profiles(relay_id: relay_id, txn: txn) load_profiles(relay_id: relay_id, txn: txn)
} else if sub_id == self.profiles_id { } else if sub_id == self.profiles_id {
damus_state.pool.unsubscribe(sub_id: profiles_id, to: [relay_id]) damus_state.pool.unsubscribe(sub_id: profiles_id, to: [relay_id])

View File

@@ -414,8 +414,10 @@ class HomeModel {
print(msg) print(msg)
case .eose(let sub_id): case .eose(let sub_id):
guard let txn = NdbTxn(ndb: damus_state.ndb) else {
return
}
let txn = NdbTxn(ndb: damus_state.ndb)
if sub_id == dms_subid { if sub_id == dms_subid {
var dms = dms.dms.flatMap { $0.events } var dms = dms.dms.flatMap { $0.events }
dms.append(contentsOf: incoming_dms) dms.append(contentsOf: incoming_dms)

View File

@@ -187,7 +187,7 @@ func mention_str(_ m: Mention<MentionRef>, profiles: Profiles) -> CompatibleText
case .pubkey(let pk): case .pubkey(let pk):
let npub = bech32_pubkey(pk) let npub = bech32_pubkey(pk)
let profile_txn = profiles.lookup(id: pk) let profile_txn = profiles.lookup(id: pk)
let profile = profile_txn.unsafeUnownedValue let profile = profile_txn?.unsafeUnownedValue
let disp = Profile.displayName(profile: profile, pubkey: pk).username.truncate(maxLength: 50) let disp = Profile.displayName(profile: profile, pubkey: pk).username.truncate(maxLength: 50)
var attributedString = AttributedString(stringLiteral: "@\(disp)") var attributedString = AttributedString(stringLiteral: "@\(disp)")
attributedString.link = URL(string: "damus:nostr:\(npub)") attributedString.link = URL(string: "damus:nostr:\(npub)")

View File

@@ -21,6 +21,7 @@ func process_local_notification(state: HeadlessDamusState, event ev: NostrEvent)
guard let local_notification = generate_local_notification_object(from: ev, state: state) else { guard let local_notification = generate_local_notification_object(from: ev, state: state) else {
return return
} }
create_local_notification(profiles: state.profiles, notify: local_notification) create_local_notification(profiles: state.profiles, notify: local_notification)
} }
@@ -76,7 +77,8 @@ func generate_local_notification_object(from ev: NostrEvent, state: HeadlessDamu
} else if type == .like, } else if type == .like,
state.settings.like_notification, state.settings.like_notification,
let evid = ev.referenced_ids.last, let evid = ev.referenced_ids.last,
let liked_event = state.ndb.lookup_note(evid).unsafeUnownedValue // We are only accessing it temporarily to generate notification content let txn = state.ndb.lookup_note(evid),
let liked_event = txn.unsafeUnownedValue // We are only accessing it temporarily to generate notification content
{ {
let content_preview = render_notification_content_preview(ev: liked_event, profiles: state.profiles, keypair: state.keypair) let content_preview = render_notification_content_preview(ev: liked_event, profiles: state.profiles, keypair: state.keypair)
return LocalNotification(type: .like, event: ev, target: liked_event, content: content_preview) return LocalNotification(type: .like, event: ev, target: liked_event, content: content_preview)
@@ -135,9 +137,9 @@ func render_notification_content_preview(ev: NostrEvent, profiles: Profiles, key
} }
func event_author_name(profiles: Profiles, pubkey: Pubkey) -> String { func event_author_name(profiles: Profiles, pubkey: Pubkey) -> String {
return profiles.lookup(id: pubkey).map({ profile in let profile_txn = profiles.lookup(id: pubkey)
Profile.displayName(profile: profile, pubkey: pubkey).username.truncate(maxLength: 50) let profile = profile_txn?.unsafeUnownedValue
}).value return Profile.displayName(profile: profile, pubkey: pubkey).username.truncate(maxLength: 50)
} }
@MainActor @MainActor
@@ -173,8 +175,8 @@ func process_zap_event(state: HeadlessDamusState, ev: NostrEvent, completion: @e
return return
} }
guard let lnurl = state.profiles.lookup_with_timestamp(ptag) guard let txn = state.profiles.lookup_with_timestamp(ptag),
.map({ pr in pr?.lnurl }).value else { let lnurl = txn.map({ pr in pr?.lnurl }).value else {
completion(.failed) completion(.failed)
return return
} }
@@ -221,7 +223,8 @@ func get_zap_target_pubkey(ev: NostrEvent, ndb: Ndb) -> Pubkey? {
} }
// we can't trust the p tag on note zaps because they can be faked // we can't trust the p tag on note zaps because they can be faked
guard let pk = ndb.lookup_note(etag).unsafeUnownedValue?.pubkey else { guard let txn = ndb.lookup_note(etag),
let pk = txn.unsafeUnownedValue?.pubkey else {
// We don't have the event in cache so we can't check the pubkey. // We 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 // We could return this as an invalid zap but that wouldn't be correct

View File

@@ -128,7 +128,7 @@ class ProfileModel: ObservableObject, Equatable {
break break
//notify(.notice, notice) //notify(.notice, notice)
case .eose: case .eose:
let txn = NdbTxn(ndb: damus.ndb) guard let txn = NdbTxn(ndb: damus.ndb) else { return }
if resp.subid == sub_id { if resp.subid == sub_id {
load_profiles(context: "profile", profiles_subid: prof_subid, relay_id: relay_id, load: .from_events(events.events), damus_state: damus, txn: txn) load_profiles(context: "profile", profiles_subid: prof_subid, relay_id: relay_id, load: .from_events(events.events), damus_state: damus, txn: txn)
} }

View File

@@ -83,7 +83,7 @@ class SearchHomeModel: ObservableObject {
// global events are not realtime // global events are not realtime
unsubscribe(to: relay_id) unsubscribe(to: relay_id)
let txn = NdbTxn(ndb: damus_state.ndb) guard let txn = NdbTxn(ndb: damus_state.ndb) else { return }
load_profiles(context: "universe", profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(events.all_events), damus_state: damus_state, txn: txn) load_profiles(context: "universe", profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(events.all_events), damus_state: damus_state, txn: txn)
} }

View File

@@ -80,7 +80,7 @@ class SearchModel: ObservableObject {
self.loading = false self.loading = false
if sub_id == self.sub_id { if sub_id == self.sub_id {
let txn = NdbTxn(ndb: state.ndb) guard let txn = NdbTxn(ndb: state.ndb) else { return }
load_profiles(context: "search", profiles_subid: self.profiles_subid, relay_id: relay_id, load: .from_events(self.events.all_events), damus_state: state, txn: txn) load_profiles(context: "search", profiles_subid: self.profiles_subid, relay_id: relay_id, load: .from_events(self.events.all_events), damus_state: state, txn: txn)
} }
} }

View File

@@ -120,7 +120,7 @@ class ThreadModel: ObservableObject {
} }
if sub_id == self.base_subid { if sub_id == self.base_subid {
let txn = NdbTxn(ndb: damus_state.ndb) guard let txn = NdbTxn(ndb: damus_state.ndb) else { return }
load_profiles(context: "thread", profiles_subid: self.profiles_subid, relay_id: relay_id, load: .from_events(Array(event_map)), damus_state: damus_state, txn: txn) load_profiles(context: "thread", profiles_subid: self.profiles_subid, relay_id: relay_id, load: .from_events(Array(event_map)), damus_state: damus_state, txn: txn)
} }
} }

View File

@@ -55,7 +55,7 @@ class ZapsModel: ObservableObject {
break break
case .eose: case .eose:
let events = state.events.lookup_zaps(target: target).map { $0.request.ev } let events = state.events.lookup_zaps(target: target).map { $0.request.ev }
let txn = NdbTxn(ndb: state.ndb) guard let txn = NdbTxn(ndb: state.ndb) else { return }
load_profiles(context: "zaps_model", profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(events), damus_state: state, txn: txn) load_profiles(context: "zaps_model", profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(events), damus_state: state, txn: txn)
case .event(_, let ev): case .event(_, let ev):
guard ev.kind == 9735, guard ev.kind == 9735,

View File

@@ -73,24 +73,27 @@ class Profiles {
profile_data(pubkey).zapper profile_data(pubkey).zapper
} }
func lookup_with_timestamp(_ pubkey: Pubkey) -> NdbTxn<ProfileRecord?> { func lookup_with_timestamp(_ pubkey: Pubkey) -> NdbTxn<ProfileRecord?>? {
return ndb.lookup_profile(pubkey) ndb.lookup_profile(pubkey)
} }
func lookup_by_key(key: ProfileKey) -> NdbTxn<ProfileRecord?> { func lookup_by_key(key: ProfileKey) -> NdbTxn<ProfileRecord?>? {
return ndb.lookup_profile_by_key(key: key) ndb.lookup_profile_by_key(key: key)
} }
func search<Y>(_ query: String, limit: Int, txn: NdbTxn<Y>) -> [Pubkey] { func search<Y>(_ query: String, limit: Int, txn: NdbTxn<Y>) -> [Pubkey] {
return ndb.search_profile(query, limit: limit, txn: txn) ndb.search_profile(query, limit: limit, txn: txn)
} }
func lookup(id: Pubkey) -> NdbTxn<Profile?> { func lookup(id: Pubkey) -> NdbTxn<Profile?>? {
return ndb.lookup_profile(id).map({ pr in pr?.profile }) guard let txn = ndb.lookup_profile(id) else {
return nil
}
return txn.map({ pr in pr?.profile })
} }
func lookup_key_by_pubkey(_ pubkey: Pubkey) -> ProfileKey? { func lookup_key_by_pubkey(_ pubkey: Pubkey) -> ProfileKey? {
return ndb.lookup_profile_key(pubkey) ndb.lookup_profile_key(pubkey)
} }
func has_fresh_profile<Y>(id: Pubkey, txn: NdbTxn<Y>) -> Bool { func has_fresh_profile<Y>(id: Pubkey, txn: NdbTxn<Y>) -> Bool {

View File

@@ -28,7 +28,9 @@ struct EventActionBar: View {
} }
var lnurl: String? { var lnurl: String? {
damus_state.profiles.lookup_with_timestamp(event.pubkey).map({ pr in pr?.lnurl }).value damus_state.profiles.lookup_with_timestamp(event.pubkey)?.map({ pr in
pr?.lnurl
}).value
} }
var show_like: Bool { var show_like: Bool {

View File

@@ -78,11 +78,12 @@ struct BannerImageView: View {
var body: some View { var body: some View {
InnerBannerImageView(disable_animation: disable_animation, url: get_banner_url(banner: banner, pubkey: pubkey, profiles: profiles)) InnerBannerImageView(disable_animation: disable_animation, url: get_banner_url(banner: banner, pubkey: pubkey, profiles: profiles))
.onReceive(handle_notify(.profile_updated)) { updated in .onReceive(handle_notify(.profile_updated)) { updated in
guard updated.pubkey == self.pubkey else { guard updated.pubkey == self.pubkey,
let profile_txn = profiles.lookup(id: updated.pubkey)
else {
return return
} }
let profile_txn = profiles.lookup(id: updated.pubkey)
let profile = profile_txn.unsafeUnownedValue let profile = profile_txn.unsafeUnownedValue
if let bannerImage = profile?.banner, bannerImage != self.banner { if let bannerImage = profile?.banner, bannerImage != self.banner {
self.banner = bannerImage self.banner = bannerImage
@@ -92,7 +93,7 @@ struct BannerImageView: View {
} }
func get_banner_url(banner: String?, pubkey: Pubkey, profiles: Profiles) -> URL? { func get_banner_url(banner: String?, pubkey: Pubkey, profiles: Profiles) -> URL? {
let bannerUrlString = banner ?? profiles.lookup(id: pubkey).map({ p in p?.banner }).value ?? "" let bannerUrlString = banner ?? profiles.lookup(id: pubkey)?.map({ p in p?.banner }).value ?? ""
if let url = URL(string: bannerUrlString) { if let url = URL(string: bannerUrlString) {
return url return url
} }

View File

@@ -38,7 +38,9 @@ func reply_desc(ndb: Ndb, event: NostrEvent, replying_to: NostrEvent?, locale: L
return NSLocalizedString("Replying to self", bundle: bundle, comment: "Label to indicate that the user is replying to themself.") return NSLocalizedString("Replying to self", bundle: bundle, comment: "Label to indicate that the user is replying to themself.")
} }
let profile_txn = NdbTxn(ndb: ndb) guard let profile_txn = NdbTxn(ndb: ndb) else {
return ""
}
let names: [String] = pubkeys.map { pk in let names: [String] = pubkeys.map { pk in
let prof = ndb.lookup_profile_with_txn(pk, txn: profile_txn) let prof = ndb.lookup_profile_with_txn(pk, txn: profile_txn)

View File

@@ -151,7 +151,7 @@ struct FollowingView: View {
} }
.tabViewStyle(.page(indexDisplayMode: .never)) .tabViewStyle(.page(indexDisplayMode: .never))
.onAppear { .onAppear {
let txn = NdbTxn(ndb: self.damus_state.ndb) guard let txn = NdbTxn(ndb: self.damus_state.ndb) else { return }
following.subscribe(txn: txn) following.subscribe(txn: txn)
} }
.onDisappear { .onDisappear {

View File

@@ -61,7 +61,7 @@ struct NotificationsView: View {
var mystery: some View { var mystery: some View {
let profile_txn = state.profiles.lookup(id: state.pubkey) let profile_txn = state.profiles.lookup(id: state.pubkey)
let profile = profile_txn.unsafeUnownedValue let profile = profile_txn?.unsafeUnownedValue
return VStack(spacing: 20) { return VStack(spacing: 20) {
Text("Wake up, \(Profile.displayName(profile: profile, pubkey: state.pubkey).displayName.truncate(maxLength: 50))", comment: "Text telling the user to wake up, where the argument is their display name.") Text("Wake up, \(Profile.displayName(profile: profile, pubkey: state.pubkey).displayName.truncate(maxLength: 50))", comment: "Text telling the user to wake up, where the argument is their display name.")
Text("You are dreaming...", comment: "Text telling the user that they are dreaming.") Text("You are dreaming...", comment: "Text telling the user that they are dreaming.")

View File

@@ -36,7 +36,7 @@ class SuggestedUsersViewModel: ObservableObject {
func suggestedUser(pubkey: Pubkey) -> SuggestedUser? { func suggestedUser(pubkey: Pubkey) -> SuggestedUser? {
let profile_txn = damus_state.profiles.lookup(id: pubkey) let profile_txn = damus_state.profiles.lookup(id: pubkey)
if let profile = profile_txn.unsafeUnownedValue, if let profile = profile_txn?.unsafeUnownedValue,
let user = SuggestedUser(name: profile.name, about: profile.about, picture: profile.picture, pubkey: pubkey) { let user = SuggestedUser(name: profile.name, about: profile.about, picture: profile.picture, pubkey: pubkey) {
return user return user
} }

View File

@@ -181,7 +181,7 @@ struct PostView: View {
} }
let profile_txn = damus_state.profiles.lookup(id: pubkey) let profile_txn = damus_state.profiles.lookup(id: pubkey)
let profile = profile_txn.unsafeUnownedValue let profile = profile_txn?.unsafeUnownedValue
return user_tag_attr_string(profile: profile, pubkey: pubkey) return user_tag_attr_string(profile: profile, pubkey: pubkey)
} }

View File

@@ -17,7 +17,7 @@ struct UserSearch: View {
@EnvironmentObject var tagModel: TagModel @EnvironmentObject var tagModel: TagModel
var users: [Pubkey] { var users: [Pubkey] {
let txn = NdbTxn(ndb: damus_state.ndb) guard let txn = NdbTxn(ndb: damus_state.ndb) else { return [] }
return search_profiles(profiles: damus_state.profiles, search: search, txn: txn).sorted { a, b in return search_profiles(profiles: damus_state.profiles, search: search, txn: txn).sorted { a, b in
let aFriendTypePriority = get_friend_type(contacts: damus_state.contacts, pubkey: a)?.priority ?? 0 let aFriendTypePriority = get_friend_type(contacts: damus_state.contacts, pubkey: a)?.priority ?? 0
let bFriendTypePriority = get_friend_type(contacts: damus_state.contacts, pubkey: b)?.priority ?? 0 let bFriendTypePriority = get_friend_type(contacts: damus_state.contacts, pubkey: b)?.priority ?? 0
@@ -33,7 +33,7 @@ struct UserSearch: View {
func on_user_tapped(pk: Pubkey) { func on_user_tapped(pk: Pubkey) {
let profile_txn = damus_state.profiles.lookup(id: pk) let profile_txn = damus_state.profiles.lookup(id: pk)
let profile = profile_txn.unsafeUnownedValue let profile = profile_txn?.unsafeUnownedValue
let user_tag = user_tag_attr_string(profile: profile, pubkey: pk) let user_tag = user_tag_attr_string(profile: profile, pubkey: pk)
appendUserTag(withTag: user_tag) appendUserTag(withTag: user_tag)

View File

@@ -30,7 +30,8 @@ struct EditMetadataView: View {
init(damus_state: DamusState) { init(damus_state: DamusState) {
self.damus_state = damus_state self.damus_state = damus_state
let data = damus_state.profiles.lookup(id: damus_state.pubkey).unsafeUnownedValue let profile_txn = damus_state.profiles.lookup(id: damus_state.pubkey)
let data = profile_txn?.unsafeUnownedValue
_name = State(initialValue: data?.name ?? "") _name = State(initialValue: data?.name ?? "")
_display_name = State(initialValue: data?.display_name ?? "") _display_name = State(initialValue: data?.display_name ?? "")

View File

@@ -24,7 +24,7 @@ struct EventProfileName: View {
self.damus_state = damus self.damus_state = damus
self.pubkey = pubkey self.pubkey = pubkey
self.size = size self.size = size
let donation = damus.ndb.lookup_profile(pubkey).map({ p in p?.profile?.damus_donation }).value let donation = damus.ndb.lookup_profile(pubkey)?.map({ p in p?.profile?.damus_donation }).value
self._donation = State(wrappedValue: donation) self._donation = State(wrappedValue: donation)
is_purple_user = nil is_purple_user = nil
} }
@@ -65,7 +65,7 @@ struct EventProfileName: View {
var body: some View { var body: some View {
let profile_txn = damus_state.profiles.lookup(id: pubkey) let profile_txn = damus_state.profiles.lookup(id: pubkey)
let profile = profile_txn.unsafeUnownedValue let profile = profile_txn?.unsafeUnownedValue
HStack(spacing: 2) { HStack(spacing: 2) {
switch current_display_name(profile) { switch current_display_name(profile) {
case .one(let one): case .one(let one):
@@ -109,7 +109,7 @@ struct EventProfileName: View {
} }
let profile_txn = damus_state.profiles.lookup(id: update.pubkey) let profile_txn = damus_state.profiles.lookup(id: update.pubkey)
guard let profile = profile_txn.unsafeUnownedValue else { return } guard let profile = profile_txn?.unsafeUnownedValue else { return }
let display_name = Profile.displayName(profile: profile, pubkey: pubkey) let display_name = Profile.displayName(profile: profile, pubkey: pubkey)
if display_name != self.display_name { if display_name != self.display_name {

View File

@@ -87,7 +87,7 @@ struct ProfileName: View {
var body: some View { var body: some View {
let profile_txn = damus_state.profiles.lookup(id: pubkey) let profile_txn = damus_state.profiles.lookup(id: pubkey)
let profile = profile_txn.unsafeUnownedValue let profile = profile_txn?.unsafeUnownedValue
HStack(spacing: 2) { HStack(spacing: 2) {
Text(verbatim: "\(prefix)\(name_choice(profile: profile))") Text(verbatim: "\(prefix)\(name_choice(profile: profile))")

View File

@@ -17,7 +17,7 @@ struct ProfileNameView: View {
Group { Group {
VStack(alignment: .leading) { VStack(alignment: .leading) {
let profile_txn = self.damus.profiles.lookup(id: pubkey) let profile_txn = self.damus.profiles.lookup(id: pubkey)
let profile = profile_txn.unsafeUnownedValue let profile = profile_txn?.unsafeUnownedValue
switch Profile.displayName(profile: profile, pubkey: pubkey) { switch Profile.displayName(profile: profile, pubkey: pubkey) {
case .one: case .one:

View File

@@ -84,7 +84,7 @@ struct ProfilePicView: View {
} }
func get_lnurl() -> String? { func get_lnurl() -> String? {
return profiles.lookup_with_timestamp(pubkey).unsafeUnownedValue?.lnurl return profiles.lookup_with_timestamp(pubkey)?.unsafeUnownedValue?.lnurl
} }
var body: some View { var body: some View {
@@ -102,7 +102,7 @@ struct ProfilePicView: View {
} }
case .remote(pubkey: let pk): case .remote(pubkey: let pk):
let profile_txn = profiles.lookup(id: pk) let profile_txn = profiles.lookup(id: pk)
let profile = profile_txn.unsafeUnownedValue let profile = profile_txn?.unsafeUnownedValue
if let pic = profile?.picture { if let pic = profile?.picture {
self.picture = pic self.picture = pic
} }
@@ -126,7 +126,7 @@ struct ProfilePicView: View {
} }
func get_profile_url(picture: String?, pubkey: Pubkey, profiles: Profiles) -> URL { func get_profile_url(picture: String?, pubkey: Pubkey, profiles: Profiles) -> URL {
let pic = picture ?? profiles.lookup(id: pubkey).map({ $0?.picture }).value ?? robohash(pubkey) let pic = picture ?? profiles.lookup(id: pubkey)?.map({ $0?.picture }).value ?? robohash(pubkey)
if let url = URL(string: pic) { if let url = URL(string: pic) {
return url return url
} }

View File

@@ -43,7 +43,7 @@ struct EditProfilePictureView: View {
if let profile_url { if let profile_url {
return profile_url return profile_url
} else if let state = damus_state, } else if let state = damus_state,
let picture = state.profiles.lookup(id: pubkey).map({ pr in pr?.picture }).value { let picture = state.profiles.lookup(id: pubkey)?.map({ pr in pr?.picture }).value {
return URL(string: picture) return URL(string: picture)
} else { } else {
return profile_url ?? URL(string: robohash(pubkey)) return profile_url ?? URL(string: robohash(pubkey))

View File

@@ -24,10 +24,10 @@ func follow_btn_txt(_ fs: FollowState, follows_you: Bool) -> String {
} }
} }
func followedByString<Y>(txn: NdbTxn<Y>, _ friend_intersection: [Pubkey], ndb: Ndb, locale: Locale = Locale.current) -> String { func followedByString(_ friend_intersection: [Pubkey], ndb: Ndb, locale: Locale = Locale.current) -> String {
let bundle = bundleForLocale(locale: locale) let bundle = bundleForLocale(locale: locale)
let names: [String] = friend_intersection.prefix(3).map { pk in let names: [String] = friend_intersection.prefix(3).map { pk in
let profile = ndb.lookup_profile_with_txn(pk, txn: txn)?.profile let profile = ndb.lookup_profile(pk)?.unsafeUnownedValue?.profile
return Profile.displayName(profile: profile, pubkey: pk).username.truncate(maxLength: 20) return Profile.displayName(profile: profile, pubkey: pk).username.truncate(maxLength: 20)
} }
@@ -331,7 +331,7 @@ struct ProfileView: View {
var aboutSection: some View { var aboutSection: some View {
VStack(alignment: .leading, spacing: 8.0) { VStack(alignment: .leading, spacing: 8.0) {
let profile_txn = damus_state.profiles.lookup_with_timestamp(profile.pubkey) let profile_txn = damus_state.profiles.lookup_with_timestamp(profile.pubkey)
let profile_data = profile_txn.unsafeUnownedValue let profile_data = profile_txn?.unsafeUnownedValue
nameSection(profile_data: profile_data) nameSection(profile_data: profile_data)
@@ -398,7 +398,7 @@ struct ProfileView: View {
NavigationLink(value: Route.FollowersYouKnow(friendedFollowers: friended_followers, followers: followers)) { NavigationLink(value: Route.FollowersYouKnow(friendedFollowers: friended_followers, followers: followers)) {
HStack { HStack {
CondensedProfilePicturesView(state: damus_state, pubkeys: friended_followers, maxPictures: 3) CondensedProfilePicturesView(state: damus_state, pubkeys: friended_followers, maxPictures: 3)
let followedByString = followedByString(txn: profile_txn, friended_followers, ndb: damus_state.ndb) let followedByString = followedByString(friended_followers, ndb: damus_state.ndb)
Text(followedByString) Text(followedByString)
.font(.subheadline).foregroundColor(.gray) .font(.subheadline).foregroundColor(.gray)
.multilineTextAlignment(.leading) .multilineTextAlignment(.leading)
@@ -499,7 +499,7 @@ extension View {
func check_nip05_validity(pubkey: Pubkey, profiles: Profiles) { func check_nip05_validity(pubkey: Pubkey, profiles: Profiles) {
let profile_txn = profiles.lookup(id: pubkey) let profile_txn = profiles.lookup(id: pubkey)
guard let profile = profile_txn.unsafeUnownedValue, guard let profile = profile_txn?.unsafeUnownedValue,
let nip05 = profile.nip05, let nip05 = profile.nip05,
profiles.is_validated(pubkey) == nil profiles.is_validated(pubkey) == nil
else { else {

View File

@@ -30,7 +30,7 @@ struct ProfileActionSheetView: View {
func profile_data() -> ProfileRecord? { func profile_data() -> ProfileRecord? {
let profile_txn = damus_state.profiles.lookup_with_timestamp(profile.pubkey) let profile_txn = damus_state.profiles.lookup_with_timestamp(profile.pubkey)
return profile_txn.unsafeUnownedValue return profile_txn?.unsafeUnownedValue
} }
func get_profile() -> Profile? { func get_profile() -> Profile? {

View File

@@ -119,10 +119,11 @@ struct QRCodeView: View {
var QRView: some View { var QRView: some View {
VStack(alignment: .center) { VStack(alignment: .center) {
let profile_txn = damus_state.profiles.lookup(id: pubkey) let profile_txn = damus_state.profiles.lookup(id: pubkey)
let profile = profile_txn.unsafeUnownedValue let profile = profile_txn?.unsafeUnownedValue
let our_profile = damus_state.ndb.lookup_profile_with_txn(damus_state.pubkey, txn: profile_txn) let our_profile_txn = damus_state.profiles.lookup(id: damus_state.pubkey)
let our_profile = our_profile_txn?.unsafeUnownedValue
if our_profile?.profile?.picture != nil { if our_profile?.picture != nil {
ProfilePicView(pubkey: pubkey, size: 90.0, highlight: .custom(DamusColors.white, 3.0), profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation) ProfilePicView(pubkey: pubkey, size: 90.0, highlight: .custom(DamusColors.white, 3.0), profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation)
.padding(.top, 50) .padding(.top, 50)
} else { } else {

View File

@@ -24,11 +24,10 @@ struct ReplyView: View {
var ReplyingToSection: some View { var ReplyingToSection: some View {
HStack { HStack {
Group { Group {
let txn = NdbTxn(ndb: damus.ndb)
let names = references let names = references
.map { pubkey in .map { pubkey in
let pk = pubkey let pk = pubkey
let prof = damus.ndb.lookup_profile_with_txn(pk, txn: txn)?.profile let prof = damus.ndb.lookup_profile(pk)?.unsafeUnownedValue?.profile
return "@" + Profile.displayName(profile: prof, pubkey: pk).username.truncate(maxLength: 50) return "@" + Profile.displayName(profile: prof, pubkey: pk).username.truncate(maxLength: 50)
} }
.joined(separator: " ") .joined(separator: " ")

View File

@@ -31,7 +31,7 @@ struct PullDownSearchView: View {
} }
do { do {
let txn = NdbTxn(ndb: state.ndb) guard let txn = NdbTxn(ndb: state.ndb) else { return }
for note_key in note_keys { for note_key in note_keys {
guard let note = state.ndb.lookup_note_by_key_with_txn(note_key, txn: txn) else { guard let note = state.ndb.lookup_note_by_key_with_txn(note_key, txn: txn) else {
continue continue

View File

@@ -108,11 +108,11 @@ struct SearchResultsView: View {
} }
.frame(maxHeight: .infinity) .frame(maxHeight: .infinity)
.onAppear { .onAppear {
let txn = NdbTxn.init(ndb: damus_state.ndb) guard let txn = NdbTxn.init(ndb: damus_state.ndb) else { return }
self.result = search_for_string(profiles: damus_state.profiles, search: search, txn: txn) self.result = search_for_string(profiles: damus_state.profiles, search: search, txn: txn)
} }
.onChange(of: search) { new in .onChange(of: search) { new in
let txn = NdbTxn.init(ndb: damus_state.ndb) guard let txn = NdbTxn.init(ndb: damus_state.ndb) else { return }
self.result = search_for_string(profiles: damus_state.profiles, search: search, txn: txn) self.result = search_for_string(profiles: damus_state.profiles, search: search, txn: txn)
} }
} }

View File

@@ -90,7 +90,7 @@ struct SideMenuView: View {
var TopProfile: some View { var TopProfile: some View {
let profile_txn = damus_state.profiles.lookup(id: damus_state.pubkey) let profile_txn = damus_state.profiles.lookup(id: damus_state.pubkey)
let profile = profile_txn.unsafeUnownedValue let profile = profile_txn?.unsafeUnownedValue
return VStack(alignment: .leading, spacing: verticalSpacing) { return VStack(alignment: .leading, spacing: verticalSpacing) {
HStack { HStack {
ProfilePicView(pubkey: damus_state.pubkey, size: 60, highlight: .none, profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation) ProfilePicView(pubkey: damus_state.pubkey, size: 60, highlight: .none, profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation)

View File

@@ -165,7 +165,7 @@ struct WalletView: View {
} }
.onChange(of: settings.donation_percent) { p in .onChange(of: settings.donation_percent) { p in
let profile_txn = damus_state.profiles.lookup(id: damus_state.pubkey) let profile_txn = damus_state.profiles.lookup(id: damus_state.pubkey)
guard let profile = profile_txn.unsafeUnownedValue else { guard let profile = profile_txn?.unsafeUnownedValue else {
return return
} }
@@ -177,7 +177,7 @@ struct WalletView: View {
let profile_txn = damus_state.profiles.lookup(id: damus_state.pubkey) let profile_txn = damus_state.profiles.lookup(id: damus_state.pubkey)
guard let keypair = damus_state.keypair.to_full(), guard let keypair = damus_state.keypair.to_full(),
let profile = profile_txn.unsafeUnownedValue, let profile = profile_txn?.unsafeUnownedValue,
model.initial_percent != profile.damus_donation model.initial_percent != profile.damus_donation
else { else {
return return

View File

@@ -34,7 +34,7 @@ struct ProfileZapLinkView<Content: View>: View {
self.action = action self.action = action
let profile_txn = damus_state.profiles.lookup_with_timestamp(pubkey) let profile_txn = damus_state.profiles.lookup_with_timestamp(pubkey)
let record = profile_txn.unsafeUnownedValue let record = profile_txn?.unsafeUnownedValue
self.reactions_enabled = record?.profile?.reactions ?? true self.reactions_enabled = record?.profile?.reactions ?? true
self.lud16 = record?.profile?.lud06 self.lud16 = record?.profile?.lud06
self.lnurl = record?.lnurl self.lnurl = record?.lnurl

View File

@@ -98,7 +98,7 @@ func zap_type_desc(type: ZapType, profiles: Profiles, pubkey: Pubkey) -> String
return NSLocalizedString("No one will see that you zapped", comment: "Description of anonymous zap type where the zap is sent anonymously and does not identify the user who sent it.") 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: case .priv:
let prof_txn = profiles.lookup(id: pubkey) let prof_txn = profiles.lookup(id: pubkey)
let prof = prof_txn.unsafeUnownedValue let prof = prof_txn?.unsafeUnownedValue
let name = Profile.displayName(profile: prof, pubkey: pubkey).username.truncate(maxLength: 50) 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) 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: case .non_zap:

View File

@@ -186,6 +186,7 @@ class Ndb {
throw DatabaseError.failed_open throw DatabaseError.failed_open
} }
self.closed = false
self.ndb = db self.ndb = db
} }
@@ -198,7 +199,7 @@ class Ndb {
} }
func text_search(query: String, limit: Int = 32, order: NdbSearchOrder = .newest_first) -> [NoteKey] { func text_search(query: String, limit: Int = 32, order: NdbSearchOrder = .newest_first) -> [NoteKey] {
let txn = NdbTxn(ndb: self) guard let txn = NdbTxn(ndb: self) else { return [] }
var results = ndb_text_search_results() var results = ndb_text_search_results()
let res = query.withCString { q in let res = query.withCString { q in
let order = order == .newest_first ? NDB_ORDER_DESCENDING : NDB_ORDER_ASCENDING let order = order == .newest_first ? NDB_ORDER_DESCENDING : NDB_ORDER_ASCENDING
@@ -243,7 +244,7 @@ class Ndb {
return note_ids return note_ids
} }
func lookup_note_by_key(_ key: NoteKey) -> NdbTxn<NdbNote?> { func lookup_note_by_key(_ key: NoteKey) -> NdbTxn<NdbNote?>? {
return NdbTxn(ndb: self) { txn in return NdbTxn(ndb: self) { txn in
lookup_note_by_key_with_txn(key, txn: txn) lookup_note_by_key_with_txn(key, txn: txn)
} }
@@ -301,7 +302,7 @@ class Ndb {
lookup_profile_by_key_inner(key, txn: txn) lookup_profile_by_key_inner(key, txn: txn)
} }
func lookup_profile_by_key(key: ProfileKey) -> NdbTxn<ProfileRecord?> { func lookup_profile_by_key(key: ProfileKey) -> NdbTxn<ProfileRecord?>? {
return NdbTxn(ndb: self) { txn in return NdbTxn(ndb: self) { txn in
lookup_profile_by_key_inner(key, txn: txn) lookup_profile_by_key_inner(key, txn: txn)
} }
@@ -312,9 +313,13 @@ class Ndb {
} }
func lookup_profile_key(_ pubkey: Pubkey) -> ProfileKey? { func lookup_profile_key(_ pubkey: Pubkey) -> ProfileKey? {
return NdbTxn(ndb: self) { txn in guard let txn = NdbTxn(ndb: self, with: { txn in
lookup_profile_key_with_txn(pubkey, txn: txn) lookup_profile_key_with_txn(pubkey, txn: txn)
}.value }) else {
return nil
}
return txn.value
} }
func lookup_profile_key_with_txn<Y>(_ pubkey: Pubkey, txn: NdbTxn<Y>) -> ProfileKey? { func lookup_profile_key_with_txn<Y>(_ pubkey: Pubkey, txn: NdbTxn<Y>) -> ProfileKey? {
@@ -342,17 +347,23 @@ class Ndb {
} }
func lookup_note_key(_ id: NoteId) -> NoteKey? { func lookup_note_key(_ id: NoteId) -> NoteKey? {
NdbTxn(ndb: self, with: { txn in lookup_note_key_with_txn(id, txn: txn) }).value guard let txn = NdbTxn(ndb: self, with: { txn in
lookup_note_key_with_txn(id, txn: txn)
}) else {
return nil
}
return txn.value
} }
func lookup_note(_ id: NoteId) -> NdbTxn<NdbNote?> { func lookup_note(_ id: NoteId) -> NdbTxn<NdbNote?>? {
return NdbTxn(ndb: self) { txn in NdbTxn(ndb: self) { txn in
lookup_note_with_txn_inner(id: id, txn: txn) lookup_note_with_txn_inner(id: id, txn: txn)
} }
} }
func lookup_profile(_ pubkey: Pubkey) -> NdbTxn<ProfileRecord?> { func lookup_profile(_ pubkey: Pubkey) -> NdbTxn<ProfileRecord?>? {
return NdbTxn(ndb: self) { txn in NdbTxn(ndb: self) { txn in
lookup_profile_with_txn_inner(pubkey: pubkey, txn: txn) lookup_profile_with_txn_inner(pubkey: pubkey, txn: txn)
} }
} }

View File

@@ -18,8 +18,9 @@ class NdbTxn<T> {
var moved: Bool var moved: Bool
var inherited: Bool var inherited: Bool
init(ndb: Ndb, with: (NdbTxn<T>) -> T = { _ in () }) { init?(ndb: Ndb, with: (NdbTxn<T>) -> T = { _ in () }) {
#if TXNDEBUG guard !ndb.closed else { return nil }
#if TXNDEBUG
txn_count += 1 txn_count += 1
print("opening transaction \(txn_count)") print("opening transaction \(txn_count)")
#endif #endif
@@ -31,11 +32,7 @@ class NdbTxn<T> {
self.txn = ndb_txn() self.txn = ndb_txn()
let ok = ndb_begin_query(ndb.ndb.ndb, &self.txn) != 0 let ok = ndb_begin_query(ndb.ndb.ndb, &self.txn) != 0
if !ok { if !ok {
self.moved = false return nil
self.txn = ndb_txn()
self.inherited = true
self.val = with(self)
return
} }
Thread.current.threadDictionary["ndb_txn"] = self.txn Thread.current.threadDictionary["ndb_txn"] = self.txn
self.inherited = false self.inherited = false