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

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 {

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 {

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

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 {

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
}

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

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