damus

nostr ios client
git clone git://jb55.com/damus
Log | Files | Refs | README | LICENSE

commit 0ec2b050706ec508b355a134fd2fdd859f802ce7
parent 130bbfafb4aa47c327e15e6bb99e0eac91396cf6
Author: Daniel D’Aquino <daniel@daquino.me>
Date:   Wed, 26 Mar 2025 10:27:56 -0300

Implement safe interface for unowned NdbNotes

This commit introduces a new interface that makes it easier and safer to
handle unowned NostrDB notes, by leveraging new non-copyable and borrow
features from modern Swift.

Changelog-None
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>

Diffstat:
Mdamus.xcodeproj/project.pbxproj | 10++++++++++
Mdamus/NIP65/NIP65.swift | 4++++
Anostrdb/UnownedNdbNote.swift | 78++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 92 insertions(+), 0 deletions(-)

diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj @@ -1090,6 +1090,10 @@ D72E127A2BEEEED000F4F781 /* NostrFilterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72E12792BEEEED000F4F781 /* NostrFilterTests.swift */; }; D7315A2A2ACDF3B70036E30A /* DamusCacheManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7315A292ACDF3B70036E30A /* DamusCacheManager.swift */; }; D7315A2C2ACDF4DA0036E30A /* DamusCacheManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7315A2B2ACDF4DA0036E30A /* DamusCacheManagerTests.swift */; }; + D733F9E52D92C76100317B11 /* UnownedNdbNote.swift in Sources */ = {isa = PBXBuildFile; fileRef = D733F9E42D92C75C00317B11 /* UnownedNdbNote.swift */; }; + D733F9E62D92C76100317B11 /* UnownedNdbNote.swift in Sources */ = {isa = PBXBuildFile; fileRef = D733F9E42D92C75C00317B11 /* UnownedNdbNote.swift */; }; + D733F9E72D92C76100317B11 /* UnownedNdbNote.swift in Sources */ = {isa = PBXBuildFile; fileRef = D733F9E42D92C75C00317B11 /* UnownedNdbNote.swift */; }; + D733F9E82D92C76100317B11 /* UnownedNdbNote.swift in Sources */ = {isa = PBXBuildFile; fileRef = D733F9E42D92C75C00317B11 /* UnownedNdbNote.swift */; }; D734B1452CCC19B1000B5C97 /* DamusFullScreenCover.swift in Sources */ = {isa = PBXBuildFile; fileRef = D734B1442CCC19B1000B5C97 /* DamusFullScreenCover.swift */; }; D734B1462CCC19B1000B5C97 /* DamusFullScreenCover.swift in Sources */ = {isa = PBXBuildFile; fileRef = D734B1442CCC19B1000B5C97 /* DamusFullScreenCover.swift */; }; D7373BA62B688EA300F7783D /* DamusPurpleTranslationSetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7373BA52B688EA200F7783D /* DamusPurpleTranslationSetupView.swift */; }; @@ -2477,6 +2481,7 @@ D72E12792BEEEED000F4F781 /* NostrFilterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrFilterTests.swift; sourceTree = "<group>"; }; D7315A292ACDF3B70036E30A /* DamusCacheManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusCacheManager.swift; sourceTree = "<group>"; }; D7315A2B2ACDF4DA0036E30A /* DamusCacheManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusCacheManagerTests.swift; sourceTree = "<group>"; }; + D733F9E42D92C75C00317B11 /* UnownedNdbNote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnownedNdbNote.swift; sourceTree = "<group>"; }; D734B1442CCC19B1000B5C97 /* DamusFullScreenCover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusFullScreenCover.swift; sourceTree = "<group>"; }; D7373BA52B688EA200F7783D /* DamusPurpleTranslationSetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleTranslationSetupView.swift; sourceTree = "<group>"; }; D7373BA72B68974500F7783D /* DamusPurpleNewUserOnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleNewUserOnboardingView.swift; sourceTree = "<group>"; }; @@ -3359,6 +3364,7 @@ 4C9054862A6AEB4500811EEC /* nostrdb */ = { isa = PBXGroup; children = ( + D733F9E42D92C75C00317B11 /* UnownedNdbNote.swift */, 4C47928D2A9939BD00489948 /* flatcc */, 4C478E2A2A9935D300489948 /* bindings */, 4CE9FBBB2A6B3D9C007E485C /* Test */, @@ -4927,6 +4933,7 @@ 4C9AA14A2A4587A6003F49FD /* NotificationStatusModel.swift in Sources */, D7100C5C2B77016700C59298 /* IAPProductStateView.swift in Sources */, 4CB9D4A72992D02B00A9A7E4 /* ProfileNameView.swift in Sources */, + D733F9E82D92C76100317B11 /* UnownedNdbNote.swift in Sources */, D74EA0902D2E271E002290DD /* ErrorView.swift in Sources */, 4CE4F0F429D779B5005914DB /* PostBox.swift in Sources */, BA37598E2ABCCE500018D73B /* VideoCaptureProcessor.swift in Sources */, @@ -5292,6 +5299,7 @@ 82D6FBBA2CD99F7900C925F4 /* NostrRequest.swift in Sources */, 82D6FBBB2CD99F7900C925F4 /* Profiles.swift in Sources */, 82D6FBBC2CD99F7900C925F4 /* NostrKind.swift in Sources */, + D733F9E62D92C76100317B11 /* UnownedNdbNote.swift in Sources */, 82D6FBBD2CD99F7900C925F4 /* NostrLink.swift in Sources */, 82D6FBBE2CD99F7900C925F4 /* WebSocket.swift in Sources */, 82D6FBBF2CD99F7900C925F4 /* ReferencedId.swift in Sources */, @@ -5604,6 +5612,7 @@ D73E5E882C6A97F4007EB227 /* StoreObserver.swift in Sources */, D73E5E892C6A97F4007EB227 /* DamusPurpleURL.swift in Sources */, D73E5E8A2C6A97F4007EB227 /* PurpleStoreKitManager.swift in Sources */, + D733F9E72D92C76100317B11 /* UnownedNdbNote.swift in Sources */, D73E5E8E2C6A97F4007EB227 /* ImageResizer.swift in Sources */, D78F080E2D7F78EF00FC6C75 /* Request.swift in Sources */, D73E5E8F2C6A97F4007EB227 /* PhotoCaptureProcessor.swift in Sources */, @@ -5989,6 +5998,7 @@ buildActionMask = 2147483647; files = ( 4C8FA7242BED58A900798A6A /* ThreadReply.swift in Sources */, + D733F9E52D92C76100317B11 /* UnownedNdbNote.swift in Sources */, D798D21F2B0858D600234419 /* MigratedTypes.swift in Sources */, D7CE1B472B0BE719002EDAD4 /* NativeObject.swift in Sources */, D71AD9002CEC176A002E2C3C /* AppAccessibilityIdentifiers.swift in Sources */, diff --git a/damus/NIP65/NIP65.swift b/damus/NIP65/NIP65.swift @@ -20,6 +20,10 @@ extension NIP65 { // MARK: - Initialization init(event: NdbNote) throws(NIP65DecodingError) { + try self.init(event: UnownedNdbNote(event)) + } + + init(event: borrowing UnownedNdbNote) throws(NIP65DecodingError) { guard event.known_kind == .relay_list else { throw .notRelayList } var relays: [RelayItem] = [] for tag in event.tags { diff --git a/nostrdb/UnownedNdbNote.swift b/nostrdb/UnownedNdbNote.swift @@ -0,0 +1,78 @@ +// +// UnownedNdbNote.swift +// damus +// +// Created by Daniel D’Aquino on 2025-03-25. +// + +/// A function that allows an unowned NdbNote to be lent out temporarily +/// +/// Use this to provide access to NostrDB unowned notes in a way that has much better compile-time safety guarantees. +/// +/// # Usage examples +/// +/// ## Lending out or providing Ndb notes +/// +/// ```swift +/// // Define the lender +/// let lender: NdbNoteLender = { lend in +/// guard let ndbNoteTxn = ndb.lookup_note(noteId) else { // Note: Must have access to `Ndb` +/// throw NdbNoteLenderError.errorLoadingNote // Throw errors if loading fails +/// } +/// guard let unownedNote = UnownedNdbNote(ndbNoteTxn) else { +/// throw NdbNoteLenderError.errorLoadingNote +/// } +/// lend(unownedNote) // Lend out the Unowned Ndb note +/// } +/// return lender // Return or pass the lender to another class +/// ``` +/// +/// ## Borrowing Ndb notes +/// +/// Assuming you are given a lender, here is how you can use it: +/// +/// ```swift +/// let borrow: NdbNoteLender = functionThatProvidesALender() +/// try? borrow { note in // You can optionally handle errors if borrowing fails +/// self.date = note.createdAt // You can do things with the note without copying it over +/// // self.note = note // Not allowed by the compiler +/// self.note = note.toOwned() // You can copy the note if needed +/// } +/// ``` +typealias NdbNoteLender = ((_: borrowing UnownedNdbNote) -> Void) throws -> Void + +enum NdbNoteLenderError: Error { + case errorLoadingNote +} + + +/// A wrapper to NdbNote that allows unowned NdbNotes to be safely handled +struct UnownedNdbNote: ~Copyable { + private let _ndbNote: NdbNote + + init(_ txn: NdbTxn<NdbNote>) { + self._ndbNote = txn.unsafeUnownedValue + } + + init?(_ txn: NdbTxn<NdbNote?>) { + guard let note = txn.unsafeUnownedValue else { return nil } + self._ndbNote = note + } + + init(_ ndbNote: NdbNote) { + self._ndbNote = ndbNote + } + + var kind: UInt32 { _ndbNote.kind } + var known_kind: NostrKind? { _ndbNote.known_kind } + var content: String { _ndbNote.content } + var tags: TagsSequence { _ndbNote.tags } + var pubkey: Pubkey { _ndbNote.pubkey } + var createdAt: UInt32 { _ndbNote.created_at } + var id: NoteId { _ndbNote.id } + var sig: Signature { _ndbNote.sig } + + func toOwned() -> NdbNote { + return _ndbNote.to_owned() + } +}