Fix infinite loading spinner regression

Root cause:
1. `lookup` looks up a note by its note id, and saving its note key
2. `lookup` then returns early (i.e. does not loading anything from the
   network) since it found the note
3. On the view, once it borrows the note from NostrDB (a query using its
   NoteKey), the query fails (Most likely due to transaction inheritance
   and the fact that the inherited transaction may be an older snapshot
   of the database without the note), causing the view loading logic to
   fail silently, leading to the infinite loading spinner

The issue was addressed by performing a single query during lookup and
copying the note contents directly at that point to avoid this
transaction inheritance issue.

In the future we should consider a more comprehensive fix to address
other instances where this may happen. I opened
https://github.com/damus-io/damus/issues/3607 for this future work.

Changelog-Fixed: Fixed an issue where notes would keep loading indefinitely in some cases
Closes: https://github.com/damus-io/damus/issues/3498
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
This commit is contained in:
Daniel D’Aquino
2026-02-04 19:02:47 -08:00
parent be6b0e2702
commit 7fa044d205
2 changed files with 19 additions and 31 deletions

View File

@@ -416,8 +416,8 @@ extension NostrNetworkManager {
/// to all connected relays. The `timeout` parameter is a total deadline for both phases.
func lookup(noteId: NoteId, to targetRelays: [RelayURL]? = nil, timeout: Duration? = nil) async throws -> NdbNoteLender? {
// Since note ids point to immutable objects, we can do a simple ndb lookup first
if let noteKey = try? self.ndb.lookup_note_key(noteId) {
return NdbNoteLender(ndb: self.ndb, noteKey: noteKey)
if let note = try? self.ndb.lookup_note_and_copy(noteId) {
return NdbNoteLender(ownedNdbNote: note)
}
// Not available in local ndb, stream from network
@@ -760,4 +760,4 @@ extension NostrNetworkManager {
/// Preload metadata for authors and referenced profiles
case preload
}
}
}

View File

@@ -16,7 +16,6 @@ struct EventLoaderView<Content: View>: View {
let relayHints: [RelayURL]
@State var event: NostrEvent?
@State var subscription_uuid: String = UUID().description
@State var loadingTask: Task<Void, Never>? = nil
let content: (NostrEvent) -> Content
/// Creates an event loader view.
@@ -34,32 +33,21 @@ struct EventLoaderView<Content: View>: View {
let event = damus_state.events.lookup(event_id)
_event = State(initialValue: event)
}
func unsubscribe() {
self.loadingTask?.cancel()
}
func subscribe() {
self.loadingTask?.cancel()
self.loadingTask = Task {
let targetRelays = relayHints.isEmpty ? nil : relayHints
#if DEBUG
if let targetRelays, !targetRelays.isEmpty {
print("[relay-hints] EventLoaderView: Loading event \(event_id.hex().prefix(8))... with \(targetRelays.count) relay hint(s): \(targetRelays.map { $0.absoluteString })")
}
#endif
let lender = try? await damus_state.nostrNetwork.reader.lookup(noteId: self.event_id, to: targetRelays)
lender?.justUseACopy({ event = $0 })
#if DEBUG
if let targetRelays, !targetRelays.isEmpty {
print("[relay-hints] EventLoaderView: Event \(event_id.hex().prefix(8))... loaded: \(event != nil)")
}
#endif
func load() async {
let targetRelays = relayHints.isEmpty ? nil : relayHints
#if DEBUG
if let targetRelays, !targetRelays.isEmpty {
print("[relay-hints] EventLoaderView: Loading event \(event_id.hex().prefix(8))... with \(targetRelays.count) relay hint(s): \(targetRelays.map { $0.absoluteString })")
}
}
func load() {
subscribe()
#endif
let lender = try? await damus_state.nostrNetwork.reader.lookup(noteId: self.event_id, to: targetRelays)
lender?.justUseACopy({ event = $0 })
#if DEBUG
if let targetRelays, !targetRelays.isEmpty {
print("[relay-hints] EventLoaderView: Event \(event_id.hex().prefix(8))... loaded: \(event != nil)")
}
#endif
}
var body: some View {
@@ -70,11 +58,11 @@ struct EventLoaderView<Content: View>: View {
ProgressView().padding()
}
}
.onAppear {
.task {
guard event == nil else {
return
}
self.load()
await self.load()
}
}
}