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:
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user