Create NostrDB streaming and async lookup interfaces
This commit introduces new interfaces for working with NostrDB from Swift, including `NostrFilter` conversion, subscription streaming via AsyncStreams and lookup/wait functions. No user-facing changes. Changelog-None Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
This commit is contained in:
30
nostrdb/Ndb+.swift
Normal file
30
nostrdb/Ndb+.swift
Normal file
@@ -0,0 +1,30 @@
|
||||
//
|
||||
// Ndb+.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Daniel D’Aquino on 2025-04-04.
|
||||
//
|
||||
|
||||
/// ## Implementation notes
|
||||
///
|
||||
/// 1. This was created as a separate file because it contains dependencies to damus-specific structures such as `NostrFilter`, which is not yet available inside the NostrDB codebase.
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Ndb {
|
||||
/// Subscribe to events matching the provided NostrFilters
|
||||
/// - Parameters:
|
||||
/// - filters: Array of NostrFilter objects
|
||||
/// - maxSimultaneousResults: Maximum number of initial results to return
|
||||
/// - Returns: AsyncStream of StreamItem events
|
||||
/// - Throws: NdbStreamError if subscription fails
|
||||
func subscribe(filters: [NostrFilter], maxSimultaneousResults: Int = 1000) throws(NdbStreamError) -> AsyncStream<StreamItem> {
|
||||
let ndbFilters: [NdbFilter]
|
||||
do {
|
||||
ndbFilters = try filters.toNdbFilters()
|
||||
} catch {
|
||||
throw .cannotConvertFilter(error)
|
||||
}
|
||||
return try self.subscribe(filters: ndbFilters, maxSimultaneousResults: maxSimultaneousResults)
|
||||
}
|
||||
}
|
||||
@@ -33,11 +33,12 @@ class Ndb {
|
||||
let owns_db: Bool
|
||||
var generation: Int
|
||||
private var closed: Bool
|
||||
private var callbackHandler: Ndb.CallbackHandler
|
||||
|
||||
var is_closed: Bool {
|
||||
self.closed || self.ndb.ndb == nil
|
||||
}
|
||||
|
||||
|
||||
static func safemode() -> Ndb? {
|
||||
guard let path = db_path ?? old_db_path else { return nil }
|
||||
|
||||
@@ -80,7 +81,7 @@ class Ndb {
|
||||
return Ndb(ndb: ndb_t(ndb: nil))
|
||||
}
|
||||
|
||||
static func open(path: String? = nil, owns_db_file: Bool = true) -> ndb_t? {
|
||||
static func open(path: String? = nil, owns_db_file: Bool = true, callbackHandler: Ndb.CallbackHandler) -> ndb_t? {
|
||||
var ndb_p: OpaquePointer? = nil
|
||||
|
||||
let ingest_threads: Int32 = 4
|
||||
@@ -111,6 +112,19 @@ class Ndb {
|
||||
var ok = false
|
||||
while !ok && mapsize > 1024 * 1024 * 700 {
|
||||
var cfg = ndb_config(flags: 0, ingester_threads: ingest_threads, mapsize: mapsize, filter_context: nil, ingest_filter: nil, sub_cb_ctx: nil, sub_cb: nil)
|
||||
|
||||
// Here we hook up the global callback function for subscription callbacks.
|
||||
// We do an "unretained" pass here because the lifetime of the callback handler is larger than the lifetime of the nostrdb monitor in the C code.
|
||||
// The NostrDB monitor that makes the callbacks should in theory _never_ outlive the callback handler.
|
||||
//
|
||||
// This means that:
|
||||
// - for as long as nostrdb is running, its parent Ndb instance will be alive, keeping the callback handler alive.
|
||||
// - when the Ndb instance is deinitialized — and the callback handler comes down with it — the `deinit` function will destroy the nostrdb monitor, preventing it from accessing freed memory.
|
||||
//
|
||||
// Therefore, we do not need to increase reference count to callbackHandler. The tightly coupled lifetimes will ensure that it is always alive when the ndb_monitor is alive.
|
||||
let ctx: UnsafeMutableRawPointer = Unmanaged.passUnretained(callbackHandler).toOpaque()
|
||||
ndb_config_set_subscription_callback(&cfg, subscription_callback, ctx)
|
||||
|
||||
let res = ndb_init(&ndb_p, testdir, &cfg);
|
||||
ok = res != 0;
|
||||
if !ok {
|
||||
@@ -124,12 +138,15 @@ class Ndb {
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
return ndb_t(ndb: ndb_p)
|
||||
|
||||
let ndb_instance = ndb_t(ndb: ndb_p)
|
||||
Task { await callbackHandler.set(ndb: ndb_instance) }
|
||||
return ndb_instance
|
||||
}
|
||||
|
||||
init?(path: String? = nil, owns_db_file: Bool = true) {
|
||||
guard let db = Self.open(path: path, owns_db_file: owns_db_file) else {
|
||||
let callbackHandler = Ndb.CallbackHandler()
|
||||
guard let db = Self.open(path: path, owns_db_file: owns_db_file, callbackHandler: callbackHandler) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -138,6 +155,7 @@ class Ndb {
|
||||
self.owns_db = owns_db_file
|
||||
self.ndb = db
|
||||
self.closed = false
|
||||
self.callbackHandler = callbackHandler
|
||||
}
|
||||
|
||||
private static func migrate_db_location_if_needed() throws {
|
||||
@@ -183,6 +201,8 @@ class Ndb {
|
||||
self.path = nil
|
||||
self.owns_db = true
|
||||
self.closed = false
|
||||
// This simple initialization will cause subscriptions not to be ever called. Probably fine because this initializer is used only for empty example ndb instances.
|
||||
self.callbackHandler = Ndb.CallbackHandler()
|
||||
}
|
||||
|
||||
func close() {
|
||||
@@ -196,7 +216,7 @@ class Ndb {
|
||||
|
||||
func reopen() -> Bool {
|
||||
guard self.is_closed,
|
||||
let db = Self.open(path: self.path, owns_db_file: self.owns_db) else {
|
||||
let db = Self.open(path: self.path, owns_db_file: self.owns_db, callbackHandler: self.callbackHandler) else {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -581,10 +601,220 @@ class Ndb {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: NdbFilter queries and subscriptions
|
||||
|
||||
/// Safe wrapper around the `ndb_query` C function
|
||||
/// - Parameters:
|
||||
/// - txn: Database transaction
|
||||
/// - filters: Array of NdbFilter objects
|
||||
/// - 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] {
|
||||
let filtersPointer = UnsafeMutablePointer<ndb_filter>.allocate(capacity: filters.count)
|
||||
defer { filtersPointer.deallocate() }
|
||||
|
||||
for (index, ndbFilter) in filters.enumerated() {
|
||||
filtersPointer.advanced(by: index).pointee = ndbFilter.ndbFilter
|
||||
}
|
||||
|
||||
let count = UnsafeMutablePointer<Int32>.allocate(capacity: 1)
|
||||
defer { count.deallocate() }
|
||||
|
||||
let results = UnsafeMutablePointer<ndb_query_result>.allocate(capacity: maxResults)
|
||||
defer { results.deallocate() }
|
||||
|
||||
guard ndb_query(&txn.txn, filtersPointer, Int32(filters.count), results, Int32(maxResults), count) == 1 else {
|
||||
throw NdbStreamError.initialQueryFailed
|
||||
}
|
||||
|
||||
var noteIds: [NoteKey] = []
|
||||
for i in 0..<count.pointee {
|
||||
noteIds.append(results.advanced(by: Int(i)).pointee.note_id)
|
||||
}
|
||||
|
||||
return noteIds
|
||||
}
|
||||
|
||||
/// Safe wrapper around `ndb_subscribe` that handles all pointer management
|
||||
/// - Parameters:
|
||||
/// - filters: Array of NdbFilter objects
|
||||
/// - Returns: AsyncStream of StreamItem events for new matches only
|
||||
private func ndbSubscribe(filters: [NdbFilter]) -> AsyncStream<StreamItem> {
|
||||
return AsyncStream<StreamItem> { continuation in
|
||||
// Allocate filters pointer - will be deallocated when subscription ends
|
||||
// Cannot use `defer` to deallocate `filtersPointer` because it needs to remain valid for the lifetime of the subscription, which extends beyond this block's scope.
|
||||
let filtersPointer = UnsafeMutablePointer<ndb_filter>.allocate(capacity: filters.count)
|
||||
for (index, ndbFilter) in filters.enumerated() {
|
||||
filtersPointer.advanced(by: index).pointee = ndbFilter.ndbFilter
|
||||
}
|
||||
|
||||
var streaming = true
|
||||
var subid: UInt64 = 0
|
||||
var terminationStarted = false
|
||||
|
||||
// Set up termination handler
|
||||
continuation.onTermination = { @Sendable _ in
|
||||
guard !terminationStarted else { return } // Avoid race conditions between two termination closures
|
||||
terminationStarted = true
|
||||
Log.debug("ndb_wait: stream: Terminated early", for: .ndb)
|
||||
streaming = false
|
||||
// Clean up resources on early termination
|
||||
if subid != 0 {
|
||||
ndb_unsubscribe(self.ndb.ndb, subid)
|
||||
Task { await self.unsetCallback(subscriptionId: subid) }
|
||||
}
|
||||
filtersPointer.deallocate()
|
||||
}
|
||||
|
||||
if !streaming {
|
||||
return
|
||||
}
|
||||
|
||||
// Set up subscription
|
||||
subid = ndb_subscribe(self.ndb.ndb, filtersPointer, Int32(filters.count))
|
||||
|
||||
// Set the subscription callback
|
||||
Task {
|
||||
await self.setCallback(for: subid, callback: { noteKey in
|
||||
continuation.yield(.event(noteKey))
|
||||
})
|
||||
}
|
||||
|
||||
// Update termination handler to include subscription cleanup
|
||||
continuation.onTermination = { @Sendable _ in
|
||||
guard !terminationStarted else { return } // Avoid race conditions between two termination closures
|
||||
terminationStarted = true
|
||||
Log.debug("ndb_wait: stream: Terminated early", for: .ndb)
|
||||
streaming = false
|
||||
ndb_unsubscribe(self.ndb.ndb, subid)
|
||||
Task { await self.unsetCallback(subscriptionId: subid) }
|
||||
filtersPointer.deallocate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func subscribe(filters: [NdbFilter], maxSimultaneousResults: Int = 1000) throws(NdbStreamError) -> AsyncStream<StreamItem> {
|
||||
// Fetch initial results
|
||||
guard let txn = NdbTxn(ndb: self) else { throw .cannotOpenTransaction }
|
||||
|
||||
// Use our safe wrapper instead of direct C function call
|
||||
let noteIds = try query(with: txn, filters: filters, maxResults: maxSimultaneousResults)
|
||||
|
||||
// Create a subscription for new events
|
||||
let newEventsStream = ndbSubscribe(filters: filters)
|
||||
|
||||
// Create a cascading stream that combines initial results with new events
|
||||
return AsyncStream<StreamItem> { continuation in
|
||||
// Stream all results already present in the database
|
||||
for noteId in noteIds {
|
||||
continuation.yield(.event(noteId))
|
||||
}
|
||||
|
||||
// Indicate this is the end of the results currently present in the database
|
||||
continuation.yield(.eose)
|
||||
|
||||
// Create a task to forward events from the subscription stream
|
||||
let forwardingTask = Task {
|
||||
for await item in newEventsStream {
|
||||
continuation.yield(item)
|
||||
}
|
||||
continuation.finish()
|
||||
}
|
||||
|
||||
// Handle termination by canceling the forwarding task
|
||||
continuation.onTermination = { @Sendable _ in
|
||||
forwardingTask.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func waitWithoutTimeout(for noteId: NoteId) async throws(NdbLookupError) -> NdbTxn<NdbNote>? {
|
||||
do {
|
||||
for try await item in try self.subscribe(filters: [NostrFilter(ids: [noteId])]) {
|
||||
switch item {
|
||||
case .eose:
|
||||
continue
|
||||
case .event(let noteKey):
|
||||
guard let txn = NdbTxn(ndb: self) else { throw NdbLookupError.cannotOpenTransaction }
|
||||
guard let note = self.lookup_note_by_key_with_txn(noteKey, txn: txn) else { throw NdbLookupError.internalInconsistency }
|
||||
if note.id == noteId {
|
||||
Log.debug("ndb wait: %d has matching id %s. Returning transaction", for: .ndb, noteKey, noteId.hex())
|
||||
return NdbTxn<NdbNote>.pure(ndb: self, val: note)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
if let error = error as? NdbStreamError { throw NdbLookupError.streamError(error) }
|
||||
else if let error = error as? NdbLookupError { throw error }
|
||||
else { throw .internalInconsistency }
|
||||
}
|
||||
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 }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Internal ndb callback interfaces
|
||||
|
||||
internal func setCallback(for subscriptionId: UInt64, callback: @escaping (NoteKey) -> Void) async {
|
||||
await self.callbackHandler.set(callback: callback, for: subscriptionId)
|
||||
}
|
||||
|
||||
internal func unsetCallback(subscriptionId: UInt64) async {
|
||||
await self.callbackHandler.unset(subid: subscriptionId)
|
||||
}
|
||||
|
||||
// MARK: Helpers
|
||||
|
||||
enum Errors: Error {
|
||||
case cannot_find_db_path
|
||||
case db_file_migration_error
|
||||
}
|
||||
|
||||
// MARK: Deinitialization
|
||||
|
||||
deinit {
|
||||
print("txn: Ndb de-init")
|
||||
@@ -592,6 +822,87 @@ class Ndb {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Extensions and helper structures and functions
|
||||
|
||||
extension Ndb {
|
||||
/// A class that is used to handle callbacks from nostrdb
|
||||
///
|
||||
/// This is a separate class from `Ndb` because it simplifies the initialization logic
|
||||
actor CallbackHandler {
|
||||
/// Holds the ndb instance in the C codebase. Should be shared with `Ndb`
|
||||
var ndb: ndb_t? = nil
|
||||
/// A map from nostrdb subscription ids to callbacks
|
||||
var subscriptionCallbackMap: [UInt64: (NoteKey) -> Void] = [:]
|
||||
|
||||
func set(callback: @escaping (NoteKey) -> Void, for subid: UInt64) {
|
||||
subscriptionCallbackMap[subid] = callback
|
||||
}
|
||||
|
||||
func unset(subid: UInt64) {
|
||||
subscriptionCallbackMap[subid] = nil
|
||||
}
|
||||
|
||||
func set(ndb: ndb_t?) {
|
||||
self.ndb = ndb
|
||||
}
|
||||
|
||||
/// Handles callbacks from nostrdb subscriptions, and routes them to the correct callback
|
||||
func handleSubscriptionCallback(subId: UInt64, maxCapacity: Int32 = 1000) {
|
||||
if let callback = subscriptionCallbackMap[subId] {
|
||||
let result = UnsafeMutablePointer<UInt64>.allocate(capacity: Int(maxCapacity))
|
||||
defer { result.deallocate() } // Ensure we deallocate memory before leaving the function to avoid memory leaks
|
||||
if let ndb {
|
||||
let numberOfNotes = ndb_poll_for_notes(ndb.ndb, subId, result, maxCapacity)
|
||||
for i in 0..<numberOfNotes {
|
||||
callback(result.advanced(by: Int(i)).pointee)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An item that comes out of a subscription stream
|
||||
enum StreamItem {
|
||||
/// End of currently stored events
|
||||
case eose
|
||||
/// An event in NostrDB available at the given note key
|
||||
case event(NoteKey)
|
||||
}
|
||||
|
||||
/// An error that may happen during nostrdb streaming
|
||||
enum NdbStreamError: Error {
|
||||
case cannotOpenTransaction
|
||||
case cannotConvertFilter(any Error)
|
||||
case initialQueryFailed
|
||||
case timeout
|
||||
}
|
||||
|
||||
/// An error that may happen when looking something up
|
||||
enum NdbLookupError: Error {
|
||||
case cannotOpenTransaction
|
||||
case streamError(NdbStreamError)
|
||||
case internalInconsistency
|
||||
case timeout
|
||||
}
|
||||
}
|
||||
|
||||
/// This callback "trampoline" function will be called when new notes arrive for NostrDB subscriptions.
|
||||
///
|
||||
/// This is needed as a separate global function in order to allow us to pass it to the C code as a callback (We can't pass native Swift fuctions directly as callbacks).
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - ctx: A pointer to a context object setup during initialization. This allows this function to "find" the correct place to call. MUST be a pointer to a `CallbackHandler`, otherwise this will trigger a crash
|
||||
/// - subid: The NostrDB subscription ID, which identifies the subscription that is being called back
|
||||
@_cdecl("subscription_callback")
|
||||
public func subscription_callback(ctx: UnsafeMutableRawPointer?, subid: UInt64) {
|
||||
guard let ctx else { return }
|
||||
let handler = Unmanaged<Ndb.CallbackHandler>.fromOpaque(ctx).takeUnretainedValue()
|
||||
Task {
|
||||
await handler.handleSubscriptionCallback(subId: subid)
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
func getDebugCheckedRoot<T: FlatBufferObject>(byteBuffer: inout ByteBuffer) throws -> T {
|
||||
return getRoot(byteBuffer: &byteBuffer)
|
||||
|
||||
356
nostrdb/NdbFilter.swift
Normal file
356
nostrdb/NdbFilter.swift
Normal file
@@ -0,0 +1,356 @@
|
||||
//
|
||||
// NdbFilter.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Daniel D'Aquino on 2025-06-02.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// A safe Swift wrapper around `UnsafeMutablePointer<ndb_filter>` that manages memory automatically.
|
||||
///
|
||||
/// This class provides a safe interface to the underlying C `ndb_filter` structure, handling
|
||||
/// memory allocation and deallocation automatically. It eliminates the need for manual memory
|
||||
/// management when working with NostrDB filters.
|
||||
///
|
||||
/// ## Usage
|
||||
/// ```swift
|
||||
/// let nostrFilter = NostrFilter(kinds: [.text_note])
|
||||
/// let ndbFilter = try NdbFilter(from: nostrFilter)
|
||||
/// // Use ndbFilter.ndbFilter or ndbFilter.unsafePointer as needed
|
||||
/// // Memory is automatically cleaned up when ndbFilter goes out of scope
|
||||
/// ```
|
||||
class NdbFilter {
|
||||
private let filterPointer: UnsafeMutablePointer<ndb_filter>
|
||||
|
||||
/// Creates a new NdbFilter from a NostrFilter.
|
||||
/// - Parameter nostrFilter: The NostrFilter to convert
|
||||
/// - Throws: `NdbFilterError.conversionFailed` if the underlying conversion fails
|
||||
init(from nostrFilter: NostrFilter) throws {
|
||||
do {
|
||||
self.filterPointer = try Self.from(nostrFilter: nostrFilter)
|
||||
} catch {
|
||||
throw NdbFilterError.conversionFailed(error)
|
||||
}
|
||||
}
|
||||
|
||||
/// Provides access to the underlying `ndb_filter` structure.
|
||||
/// - Returns: The underlying `ndb_filter` value (not a pointer)
|
||||
var ndbFilter: ndb_filter {
|
||||
return filterPointer.pointee
|
||||
}
|
||||
|
||||
/// Provides access to the underlying unsafe pointer when needed for C interop.
|
||||
/// - Warning: The caller must not deallocate this pointer. It will be automatically
|
||||
/// deallocated when this NdbFilter is destroyed.
|
||||
/// - Returns: The unsafe mutable pointer to the underlying ndb_filter
|
||||
var unsafePointer: UnsafeMutablePointer<ndb_filter> {
|
||||
return filterPointer
|
||||
}
|
||||
|
||||
/// Creates multiple NdbFilter instances from an array of NostrFilters.
|
||||
/// - Parameter nostrFilters: Array of NostrFilter instances to convert
|
||||
/// - Returns: Array of NdbFilter instances
|
||||
/// - Throws: `NdbFilterError.conversionFailed` if any conversion fails
|
||||
static func create(from nostrFilters: [NostrFilter]) throws -> [NdbFilter] {
|
||||
return try nostrFilters.map { try NdbFilter(from: $0) }
|
||||
}
|
||||
|
||||
// MARK: - Conversion to/from ndb_filter
|
||||
|
||||
// TODO: This function is long and repetitive, refactor it into something cleaner.
|
||||
private static func from(nostrFilter: NostrFilter) throws(NdbFilterConversionError) -> UnsafeMutablePointer<ndb_filter> {
|
||||
let filterPointer = UnsafeMutablePointer<ndb_filter>.allocate(capacity: 1)
|
||||
|
||||
guard ndb_filter_init(filterPointer) == 1 else {
|
||||
filterPointer.deallocate()
|
||||
throw NdbFilterConversionError.failedToInitialize
|
||||
}
|
||||
|
||||
// Handle `ids` field
|
||||
if let ids = nostrFilter.ids {
|
||||
guard ndb_filter_start_field(filterPointer, NDB_FILTER_IDS) == 1 else {
|
||||
ndb_filter_destroy(filterPointer)
|
||||
filterPointer.deallocate()
|
||||
throw NdbFilterConversionError.failedToStartField
|
||||
}
|
||||
|
||||
for noteId in ids {
|
||||
do {
|
||||
try noteId.withUnsafePointer({ idPointer in
|
||||
if ndb_filter_add_id_element(filterPointer, idPointer) != 1 {
|
||||
ndb_filter_destroy(filterPointer)
|
||||
filterPointer.deallocate()
|
||||
throw NdbFilterConversionError.failedToAddElement
|
||||
}
|
||||
})
|
||||
}
|
||||
catch {
|
||||
ndb_filter_destroy(filterPointer)
|
||||
filterPointer.deallocate()
|
||||
throw NdbFilterConversionError.failedToAddElement
|
||||
}
|
||||
}
|
||||
|
||||
ndb_filter_end_field(filterPointer)
|
||||
}
|
||||
|
||||
// Handle `kinds` field
|
||||
if let kinds = nostrFilter.kinds {
|
||||
guard ndb_filter_start_field(filterPointer, NDB_FILTER_KINDS) == 1 else {
|
||||
ndb_filter_destroy(filterPointer)
|
||||
filterPointer.deallocate()
|
||||
throw NdbFilterConversionError.failedToStartField
|
||||
}
|
||||
|
||||
for kind in kinds {
|
||||
if ndb_filter_add_int_element(filterPointer, UInt64(kind.rawValue)) != 1 {
|
||||
ndb_filter_destroy(filterPointer)
|
||||
filterPointer.deallocate()
|
||||
throw NdbFilterConversionError.failedToAddElement
|
||||
}
|
||||
}
|
||||
|
||||
ndb_filter_end_field(filterPointer)
|
||||
}
|
||||
|
||||
// Handle `referenced_ids` field
|
||||
if let referencedIds = nostrFilter.referenced_ids {
|
||||
guard ndb_filter_start_tag_field(filterPointer, CChar(UnicodeScalar("e").value)) == 1 else {
|
||||
ndb_filter_destroy(filterPointer)
|
||||
filterPointer.deallocate()
|
||||
throw NdbFilterConversionError.failedToStartField
|
||||
}
|
||||
|
||||
for refId in referencedIds {
|
||||
do {
|
||||
try refId.withUnsafePointer({ refPointer in
|
||||
if ndb_filter_add_id_element(filterPointer, refPointer) != 1 {
|
||||
ndb_filter_destroy(filterPointer)
|
||||
filterPointer.deallocate()
|
||||
throw NdbFilterConversionError.failedToAddElement
|
||||
}
|
||||
})
|
||||
}
|
||||
catch {
|
||||
ndb_filter_destroy(filterPointer)
|
||||
filterPointer.deallocate()
|
||||
throw NdbFilterConversionError.failedToAddElement
|
||||
}
|
||||
}
|
||||
|
||||
ndb_filter_end_field(filterPointer)
|
||||
}
|
||||
|
||||
// Handle `pubkeys`
|
||||
if let pubkeys = nostrFilter.pubkeys {
|
||||
guard ndb_filter_start_tag_field(filterPointer, CChar(UnicodeScalar("p").value)) == 1 else {
|
||||
ndb_filter_destroy(filterPointer)
|
||||
filterPointer.deallocate()
|
||||
throw NdbFilterConversionError.failedToStartField
|
||||
}
|
||||
|
||||
for pubkey in pubkeys {
|
||||
do {
|
||||
try pubkey.withUnsafePointer({ pubkeyPointer in
|
||||
if ndb_filter_add_id_element(filterPointer, pubkeyPointer) != 1 {
|
||||
ndb_filter_destroy(filterPointer)
|
||||
filterPointer.deallocate()
|
||||
throw NdbFilterConversionError.failedToAddElement
|
||||
}
|
||||
})
|
||||
}
|
||||
catch {
|
||||
ndb_filter_destroy(filterPointer)
|
||||
filterPointer.deallocate()
|
||||
throw NdbFilterConversionError.failedToAddElement
|
||||
}
|
||||
}
|
||||
|
||||
ndb_filter_end_field(filterPointer)
|
||||
}
|
||||
|
||||
// Handle `since`
|
||||
if let since = nostrFilter.since {
|
||||
if ndb_filter_start_field(filterPointer, NDB_FILTER_SINCE) != 1 {
|
||||
ndb_filter_destroy(filterPointer)
|
||||
filterPointer.deallocate()
|
||||
throw NdbFilterConversionError.failedToAddElement
|
||||
}
|
||||
|
||||
if ndb_filter_add_int_element(filterPointer, UInt64(since)) != 1 {
|
||||
ndb_filter_destroy(filterPointer)
|
||||
filterPointer.deallocate()
|
||||
throw NdbFilterConversionError.failedToAddElement
|
||||
}
|
||||
|
||||
ndb_filter_end_field(filterPointer)
|
||||
}
|
||||
|
||||
// Handle `until`
|
||||
if let until = nostrFilter.until {
|
||||
if ndb_filter_start_field(filterPointer, NDB_FILTER_UNTIL) != 1 {
|
||||
ndb_filter_destroy(filterPointer)
|
||||
filterPointer.deallocate()
|
||||
throw NdbFilterConversionError.failedToAddElement
|
||||
}
|
||||
|
||||
if ndb_filter_add_int_element(filterPointer, UInt64(until)) != 1 {
|
||||
ndb_filter_destroy(filterPointer)
|
||||
filterPointer.deallocate()
|
||||
throw NdbFilterConversionError.failedToAddElement
|
||||
}
|
||||
|
||||
ndb_filter_end_field(filterPointer)
|
||||
}
|
||||
|
||||
// Handle `limit`
|
||||
if let limit = nostrFilter.limit {
|
||||
if ndb_filter_start_field(filterPointer, NDB_FILTER_LIMIT) != 1 {
|
||||
ndb_filter_destroy(filterPointer)
|
||||
filterPointer.deallocate()
|
||||
throw NdbFilterConversionError.failedToAddElement
|
||||
}
|
||||
|
||||
if ndb_filter_add_int_element(filterPointer, UInt64(limit)) != 1 {
|
||||
ndb_filter_destroy(filterPointer)
|
||||
filterPointer.deallocate()
|
||||
throw NdbFilterConversionError.failedToAddElement
|
||||
}
|
||||
|
||||
ndb_filter_end_field(filterPointer)
|
||||
}
|
||||
|
||||
// Handle `authors`
|
||||
if let authors = nostrFilter.authors {
|
||||
guard ndb_filter_start_field(filterPointer, NDB_FILTER_AUTHORS) == 1 else {
|
||||
ndb_filter_destroy(filterPointer)
|
||||
filterPointer.deallocate()
|
||||
throw NdbFilterConversionError.failedToStartField
|
||||
}
|
||||
|
||||
for author in authors {
|
||||
do {
|
||||
try author.withUnsafePointer({ authorPointer in
|
||||
if ndb_filter_add_id_element(filterPointer, authorPointer) != 1 {
|
||||
ndb_filter_destroy(filterPointer)
|
||||
filterPointer.deallocate()
|
||||
throw NdbFilterConversionError.failedToAddElement
|
||||
}
|
||||
})
|
||||
}
|
||||
catch {
|
||||
ndb_filter_destroy(filterPointer)
|
||||
filterPointer.deallocate()
|
||||
throw NdbFilterConversionError.failedToAddElement
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ndb_filter_end_field(filterPointer)
|
||||
}
|
||||
|
||||
// Handle `hashtag`
|
||||
if let hashtags = nostrFilter.hashtag {
|
||||
guard ndb_filter_start_tag_field(filterPointer, CChar(UnicodeScalar("t").value)) == 1 else {
|
||||
ndb_filter_destroy(filterPointer)
|
||||
filterPointer.deallocate()
|
||||
throw NdbFilterConversionError.failedToStartField
|
||||
}
|
||||
|
||||
for tag in hashtags {
|
||||
if ndb_filter_add_str_element(filterPointer, tag.cString(using: .utf8)) != 1 {
|
||||
ndb_filter_destroy(filterPointer)
|
||||
filterPointer.deallocate()
|
||||
throw NdbFilterConversionError.failedToAddElement
|
||||
}
|
||||
}
|
||||
ndb_filter_end_field(filterPointer)
|
||||
}
|
||||
|
||||
// Handle `parameter`
|
||||
if let parameters = nostrFilter.parameter {
|
||||
guard ndb_filter_start_tag_field(filterPointer, CChar(UnicodeScalar("d").value)) == 1 else {
|
||||
ndb_filter_destroy(filterPointer)
|
||||
filterPointer.deallocate()
|
||||
throw NdbFilterConversionError.failedToStartField
|
||||
}
|
||||
|
||||
for parameter in parameters {
|
||||
if ndb_filter_add_str_element(filterPointer, parameter.cString(using: .utf8)) != 1 {
|
||||
ndb_filter_destroy(filterPointer)
|
||||
filterPointer.deallocate()
|
||||
throw NdbFilterConversionError.failedToAddElement
|
||||
}
|
||||
}
|
||||
ndb_filter_end_field(filterPointer)
|
||||
}
|
||||
|
||||
// Handle `quotes`
|
||||
if let quotes = nostrFilter.quotes {
|
||||
guard ndb_filter_start_tag_field(filterPointer, CChar(UnicodeScalar("q").value)) == 1 else {
|
||||
ndb_filter_destroy(filterPointer)
|
||||
filterPointer.deallocate()
|
||||
throw NdbFilterConversionError.failedToStartField
|
||||
}
|
||||
|
||||
for quote in quotes {
|
||||
do {
|
||||
try quote.withUnsafePointer({ quotePointer in
|
||||
if ndb_filter_add_id_element(filterPointer, quotePointer) != 1 {
|
||||
ndb_filter_destroy(filterPointer)
|
||||
filterPointer.deallocate()
|
||||
throw NdbFilterConversionError.failedToAddElement
|
||||
}
|
||||
})
|
||||
}
|
||||
catch {
|
||||
ndb_filter_destroy(filterPointer)
|
||||
filterPointer.deallocate()
|
||||
throw NdbFilterConversionError.failedToAddElement
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ndb_filter_end_field(filterPointer)
|
||||
}
|
||||
|
||||
// Finalize the filter
|
||||
guard ndb_filter_end(filterPointer) == 1 else {
|
||||
ndb_filter_destroy(filterPointer)
|
||||
filterPointer.deallocate()
|
||||
throw NdbFilterConversionError.failedToFinalize
|
||||
}
|
||||
|
||||
return filterPointer
|
||||
}
|
||||
|
||||
enum NdbFilterConversionError: Error {
|
||||
case failedToInitialize
|
||||
case failedToStartField
|
||||
case failedToAddElement
|
||||
case failedToFinalize
|
||||
}
|
||||
|
||||
deinit {
|
||||
ndb_filter_destroy(filterPointer)
|
||||
filterPointer.deallocate()
|
||||
}
|
||||
}
|
||||
|
||||
/// Errors that can occur when working with NdbFilter.
|
||||
enum NdbFilterError: Error {
|
||||
/// Thrown when conversion from NostrFilter to NdbFilter fails.
|
||||
/// - Parameter Error: The underlying error that caused the conversion to fail
|
||||
case conversionFailed(Error)
|
||||
}
|
||||
|
||||
/// Extension to create multiple NdbFilters safely from an array of NostrFilters.
|
||||
extension Array where Element == NostrFilter {
|
||||
/// Converts an array of NostrFilters to NdbFilters.
|
||||
/// - Returns: Array of NdbFilter instances
|
||||
/// - Throws: `NdbFilterError.conversionFailed` if any conversion fails
|
||||
func toNdbFilters() throws -> [NdbFilter] {
|
||||
return try self.map { try NdbFilter(from: $0) }
|
||||
}
|
||||
}
|
||||
@@ -263,27 +263,8 @@ class NdbNote: Codable, Equatable, Hashable {
|
||||
}
|
||||
|
||||
var n = ndb_note_ptr()
|
||||
|
||||
var the_kp: ndb_keypair? = nil
|
||||
|
||||
if let sec = keypair.privkey {
|
||||
var kp = ndb_keypair()
|
||||
memcpy(&kp.secret.0, sec.id.bytes, 32);
|
||||
|
||||
if ndb_create_keypair(&kp) <= 0 {
|
||||
print("bad keypair")
|
||||
} else {
|
||||
the_kp = kp
|
||||
}
|
||||
}
|
||||
|
||||
var len: Int32 = 0
|
||||
if var the_kp {
|
||||
len = ndb_builder_finalize(&builder, &n.ptr, &the_kp)
|
||||
} else {
|
||||
len = ndb_builder_finalize(&builder, &n.ptr, nil)
|
||||
}
|
||||
|
||||
|
||||
switch noteConstructionMaterial {
|
||||
case .keypair(let keypair):
|
||||
var the_kp: ndb_keypair? = nil
|
||||
@@ -300,9 +281,9 @@ class NdbNote: Codable, Equatable, Hashable {
|
||||
}
|
||||
|
||||
if var the_kp {
|
||||
len = ndb_builder_finalize(&builder, &n, &the_kp)
|
||||
len = ndb_builder_finalize(&builder, &n.ptr, &the_kp)
|
||||
} else {
|
||||
len = ndb_builder_finalize(&builder, &n, nil)
|
||||
len = ndb_builder_finalize(&builder, &n.ptr, nil)
|
||||
}
|
||||
|
||||
if len <= 0 {
|
||||
@@ -315,7 +296,7 @@ class NdbNote: Codable, Equatable, Hashable {
|
||||
|
||||
do {
|
||||
// Finalize note, save length, and ensure it is higher than zero (which signals finalization has succeeded)
|
||||
len = ndb_builder_finalize(&builder, &n, nil)
|
||||
len = ndb_builder_finalize(&builder, &n.ptr, nil)
|
||||
guard len > 0 else { throw InitError.generic }
|
||||
|
||||
let scratch_buf_len = MAX_NOTE_SIZE
|
||||
@@ -323,11 +304,11 @@ class NdbNote: Codable, Equatable, Hashable {
|
||||
defer { free(scratch_buf) } // Ensure we deallocate as soon as we leave this scope, regardless of the outcome
|
||||
|
||||
// Calculate the ID based on the content
|
||||
guard ndb_calculate_id(n, scratch_buf, Int32(scratch_buf_len)) == 1 else { throw InitError.generic }
|
||||
guard ndb_calculate_id(n.ptr, scratch_buf, Int32(scratch_buf_len)) == 1 else { throw InitError.generic }
|
||||
|
||||
// Verify the signature against the pubkey and the computed ID, to verify the validity of the whole note
|
||||
var ctx = secp256k1_context_create(UInt32(SECP256K1_CONTEXT_VERIFY))
|
||||
guard ndb_note_verify(&ctx, ndb_note_pubkey(n), ndb_note_id(n), ndb_note_sig(n)) == 1 else { throw InitError.generic }
|
||||
guard ndb_note_verify(&ctx, ndb_note_pubkey(n.ptr), ndb_note_id(n.ptr), ndb_note_sig(n.ptr)) == 1 else { throw InitError.generic }
|
||||
}
|
||||
catch {
|
||||
free(buf)
|
||||
|
||||
Reference in New Issue
Block a user