diff --git a/.gitignore b/.gitignore index be82d5f..7d9a11f 100644 --- a/.gitignore +++ b/.gitignore @@ -81,4 +81,7 @@ lib/ cbl-reactnative-docs/ couchbase-lite-ios/ couchbase-lite-java-common-android-release-*/ -ios-swift-quickstart/ \ No newline at end of file +ios-swift-quickstart/ + +# Local planning notes (not for version control) +TURBO_MIGRATION_PLAN.md \ No newline at end of file diff --git a/.npmignore b/.npmignore index 4ba7750..8261c59 100644 --- a/.npmignore +++ b/.npmignore @@ -2,4 +2,4 @@ src/cblite-js/cblite-tests expo-example couchbase-lite-ios ios-swift-quickstart -cbl-reactnative-docs +cbl-reactnative-docs \ No newline at end of file diff --git a/cbl-reactnative.podspec b/cbl-reactnative.podspec index 53ab0bb..421a7fd 100644 --- a/cbl-reactnative.podspec +++ b/cbl-reactnative.podspec @@ -1,7 +1,6 @@ require "json" package = JSON.parse(File.read(File.join(__dir__, "package.json"))) -folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32' Pod::Spec.new do |s| s.name = "cbl-reactnative" @@ -18,26 +17,5 @@ Pod::Spec.new do |s| s.dependency 'CouchbaseLite-Swift-Enterprise', '3.3.0' s.source_files = "ios/**/*.{h,m,mm,swift}" - # Use install_modules_dependencies helper to install the dependencies if React Native version >=0.71.0. - # See https://github.com/facebook/react-native/blob/febf6b7f33fdb4904669f99d795eba4c0f95d7bf/scripts/cocoapods/new_architecture.rb#L79. - if respond_to?(:install_modules_dependencies, true) - install_modules_dependencies(s) - else - s.dependency "React-Core" - - # Don't install the dependencies when we run `pod install` in the old architecture. - if ENV['RCT_NEW_ARCH_ENABLED'] == '1' then - s.compiler_flags = folly_compiler_flags + " -DRCT_NEW_ARCH_ENABLED=1" - s.pod_target_xcconfig = { - "HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\"", - "OTHER_CPLUSPLUSFLAGS" => "-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1", - "CLANG_CXX_LANGUAGE_STANDARD" => "c++17" - } - s.dependency "React-Codegen" - s.dependency "RCT-Folly" - s.dependency "RCTRequired" - s.dependency "RCTTypeSafety" - s.dependency "ReactCommon/turbomodule/core" - end - end + install_modules_dependencies(s) end diff --git a/ios/CblCollectionModule.swift b/ios/CblCollectionModule.swift new file mode 100644 index 0000000..f803f5e --- /dev/null +++ b/ios/CblCollectionModule.swift @@ -0,0 +1,424 @@ +import Foundation +import CouchbaseLiteSwift + +@objcMembers public class CblCollectionModule: NSObject { + + private let backgroundQueue = CblNativeQueue.shared + private let sendEventClosure: (String, Any?) -> Void + + @objc public init(sendEvent: @escaping (String, Any?) -> Void) { + self.sendEventClosure = sendEvent + super.init() + } + + public func collection_CreateCollection( + collectionName: String, name: String, scopeName: String, + resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) { + let (isError, args) = DataAdapter.shared.adaptCollectionArgs( + name: name as NSString, collectionName: collectionName as NSString, + scopeName: scopeName as NSString, reject: reject + ) + if isError { return } + backgroundQueue.async { + do { + guard let collection = try DatabaseManager.shared.createCollection( + args.collectionName, scopeName: args.scopeName, + databaseName: args.databaseName + ) else { + reject("DATABASE_ERROR", + "Unable to create collection <\(args.scopeName)." + + "\(args.collectionName)> in database <\(args.databaseName)>", nil) + return + } + let dict = DataAdapter.shared.adaptCollectionToNSDictionary( + collection, databaseName: args.databaseName + ) + resolve(dict) + } catch let error as NSError { + reject("DATABASE_ERROR", error.localizedDescription, nil) + } catch { + reject("DATABASE_ERROR", error.localizedDescription, nil) + } + } + } + + public func collection_DeleteCollection( + collectionName: String, name: String, scopeName: String, + resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) { + let (isError, args) = DataAdapter.shared.adaptCollectionArgs( + name: name as NSString, collectionName: collectionName as NSString, + scopeName: scopeName as NSString, reject: reject + ) + if isError { return } + backgroundQueue.async { + do { + try DatabaseManager.shared.deleteCollection( + args.collectionName, scopeName: args.scopeName, + databaseName: args.databaseName + ) + resolve(nil) + } catch let error as NSError { + reject("DATABASE_ERROR", error.localizedDescription, nil) + } catch { + reject("DATABASE_ERROR", error.localizedDescription, nil) + } + } + } + + public func collection_GetCollection( + collectionName: String, name: String, scopeName: String, + resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) { + let (isError, args) = DataAdapter.shared.adaptCollectionArgs( + name: name as NSString, collectionName: collectionName as NSString, + scopeName: scopeName as NSString, reject: reject + ) + if isError { return } + backgroundQueue.async { + do { + guard let collection = try DatabaseManager.shared.collection( + args.collectionName, scopeName: args.scopeName, + databaseName: args.databaseName + ) else { + reject("DATABASE_ERROR", + "Unable to get collection <\(args.scopeName)." + + "\(args.collectionName)> in database <\(args.databaseName)>", nil) + return + } + let dict = DataAdapter.shared.adaptCollectionToNSDictionary( + collection, databaseName: args.databaseName + ) + resolve(dict) + } catch let error as NSError { + reject("DATABASE_ERROR", error.localizedDescription, nil) + } catch { + reject("DATABASE_ERROR", error.localizedDescription, nil) + } + } + } + + public func collection_GetCollections( + name: String, scopeName: String, + resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) { + let (isError, args) = DataAdapter.shared.adaptScopeArgs( + name: name as NSString, scopeName: scopeName as NSString, reject: reject + ) + if isError { return } + backgroundQueue.async { + do { + if let collections = try DatabaseManager.shared.collections( + args.scopeName, databaseName: args.databaseName + ) { + let collectionsArray = DataAdapter.shared.adaptCollectionsToNSDictionaryString( + collections, databaseName: args.databaseName + ) + resolve(["collections": collectionsArray]) + } else { + reject("DATABASE_ERROR", + "Unable to get collections for scope <\(args.scopeName)> " + + "in database <\(args.databaseName)>", nil) + return + } + } catch let error as NSError { + reject("DATABASE_ERROR", error.localizedDescription, nil) + } catch { + reject("DATABASE_ERROR", error.localizedDescription, nil) + } + } + } + + public func collection_GetDefault( + name: String, + resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) { + let (isError, databaseName) = DataAdapter.shared.adaptDatabaseName( + name: name as NSString, reject: reject + ) + if isError { return } + backgroundQueue.async { + do { + guard let collection = try DatabaseManager.shared.defaultCollection(databaseName) + else { + reject("DATABASE_ERROR", + "Unable to get default collection for database \(databaseName)", nil) + return + } + let dict = DataAdapter.shared.adaptCollectionToNSDictionary( + collection, databaseName: databaseName + ) + resolve(dict) + } catch let error as NSError { + reject("DATABASE_ERROR", error.localizedDescription, nil) + } catch { + reject("DATABASE_ERROR", error.localizedDescription, nil) + } + } + } + + public func collection_GetCount( + collectionName: String, name: String, scopeName: String, + resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) { + let (isError, args) = DataAdapter.shared.adaptCollectionArgs( + name: name as NSString, collectionName: collectionName as NSString, + scopeName: scopeName as NSString, reject: reject + ) + if isError { return } + backgroundQueue.async { + do { + let count = try CollectionManager.shared.documentsCount( + args.collectionName, scopeName: args.scopeName, + databaseName: args.databaseName + ) + resolve(["count": count]) + } catch let error as NSError { + reject("DATABASE_ERROR", error.localizedDescription, nil) + } catch { + reject("DATABASE_ERROR", error.localizedDescription, nil) + } + } + } + + public func collection_GetFullName( + collectionName: String, name: String, scopeName: String, + resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) { + let (isError, args) = DataAdapter.shared.adaptCollectionArgs( + name: name as NSString, collectionName: collectionName as NSString, + scopeName: scopeName as NSString, reject: reject + ) + if isError { return } + backgroundQueue.async { + do { + let fullName = try CollectionManager.shared.fullName( + args.collectionName, scopeName: args.scopeName, + databaseName: args.databaseName + ) + resolve(["fullName": fullName]) + } catch let error as NSError { + reject("DATABASE_ERROR", error.localizedDescription, nil) + } catch { + reject("DATABASE_ERROR", error.localizedDescription, nil) + } + } + } + + public func collection_CreateIndex( + indexName: String, index: Any, collectionName: String, + scopeName: String, name: String, + resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) { + let (isError, args) = DataAdapter.shared.adaptCollectionArgs( + name: name as NSString, collectionName: collectionName as NSString, + scopeName: scopeName as NSString, reject: reject + ) + let (isIdxNameError, idxName) = DataAdapter.shared.adaptNonEmptyString( + value: indexName as NSString, propertyName: "indexName", reject: reject + ) + let indexDict = index as? NSDictionary ?? NSDictionary() + let (isIdxError, indexData) = DataAdapter.shared.adaptIndexToArrayAny( + dict: indexDict, reject: reject + ) + if isError || isIdxNameError || isIdxError { return } + backgroundQueue.async { + do { + try CollectionManager.shared.createIndex( + idxName, indexType: indexData.indexType, items: indexData.indexes, + collectionName: args.collectionName, scopeName: args.scopeName, + databaseName: args.databaseName + ) + resolve(nil) + } catch let error as NSError { + reject("DATABASE_ERROR", error.localizedDescription, nil) + } catch { + reject("DATABASE_ERROR", error.localizedDescription, nil) + } + } + } + + public func collection_DeleteIndex( + indexName: String, collectionName: String, scopeName: String, name: String, + resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) { + let (isError, args) = DataAdapter.shared.adaptCollectionArgs( + name: name as NSString, collectionName: collectionName as NSString, + scopeName: scopeName as NSString, reject: reject + ) + let (isIdxNameError, idxName) = DataAdapter.shared.adaptNonEmptyString( + value: indexName as NSString, propertyName: "indexName", reject: reject + ) + if isError || isIdxNameError { return } + backgroundQueue.async { + do { + try CollectionManager.shared.deleteIndex( + idxName, collectionName: args.collectionName, + scopeName: args.scopeName, databaseName: args.databaseName + ) + resolve(nil) + } catch let error as NSError { + reject("DATABASE_ERROR", error.localizedDescription, nil) + } catch { + reject("DATABASE_ERROR", error.localizedDescription, nil) + } + } + } + + public func collection_GetIndexes( + collectionName: String, scopeName: String, name: String, + resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) { + let (isError, args) = DataAdapter.shared.adaptCollectionArgs( + name: name as NSString, collectionName: collectionName as NSString, + scopeName: scopeName as NSString, reject: reject + ) + if isError { return } + backgroundQueue.async { + do { + let indexes = try CollectionManager.shared.indexes( + args.collectionName, scopeName: args.scopeName, + databaseName: args.databaseName + ) + resolve(["indexes": indexes]) + } catch let error as NSError { + reject("DATABASE_ERROR", error.localizedDescription, nil) + } catch { + reject("DATABASE_ERROR", error.localizedDescription, nil) + } + } + } + + public func collection_AddChangeListener( + changeListenerToken: String, collectionName: String, + name: String, scopeName: String, + resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) { + let (isError, args) = DataAdapter.shared.adaptCollectionArgs( + name: name as NSString, collectionName: collectionName as NSString, + scopeName: scopeName as NSString, reject: reject + ) + let (isTokenError, uuidToken) = DataAdapter.shared.adaptNonEmptyString( + value: changeListenerToken as NSString, + propertyName: "changeListenerToken", reject: reject + ) + if isError || isTokenError { return } + backgroundQueue.async { + do { + guard let collection = try CollectionManager.shared.getCollection( + args.collectionName, scopeName: args.scopeName, + databaseName: args.databaseName + ) else { + reject("DATABASE_ERROR", "Could not find collection", nil) + return + } + let listener = collection.addChangeListener( + queue: self.backgroundQueue + ) { [weak self] change in + guard let self = self else { return } + let resultData = NSMutableDictionary() + resultData["token"] = uuidToken + resultData["documentIDs"] = change.documentIDs + resultData["collection"] = DataAdapter.shared.adaptCollectionToNSDictionary( + collection, databaseName: args.databaseName + ) + self.sendEventClosure("collectionChange", resultData) + } + ListenerTokenStore.shared.add( + token: uuidToken, + record: ChangeListenerRecord( + nativeListenerToken: listener, listenerType: .collection + ) + ) + resolve(nil) + } catch let error as NSError { + reject("DATABASE_ERROR", error.localizedDescription, nil) + } catch { + reject("DATABASE_ERROR", error.localizedDescription, nil) + } + } + } + + public func collection_AddDocumentChangeListener( + changeListenerToken: String, documentId: String, collectionName: String, + name: String, scopeName: String, + resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) { + let (isError, args) = DataAdapter.shared.adaptCollectionArgs( + name: name as NSString, collectionName: collectionName as NSString, + scopeName: scopeName as NSString, reject: reject + ) + let (isTokenError, uuidToken) = DataAdapter.shared.adaptNonEmptyString( + value: changeListenerToken as NSString, + propertyName: "changeListenerToken", reject: reject + ) + let (isDocError, docId) = DataAdapter.shared.adaptNonEmptyString( + value: documentId as NSString, propertyName: "documentId", reject: reject + ) + if isError || isTokenError || isDocError { return } + backgroundQueue.async { + do { + guard let collection = try CollectionManager.shared.getCollection( + args.collectionName, scopeName: args.scopeName, + databaseName: args.databaseName + ) else { + reject("DATABASE_ERROR", "Could not find collection", nil) + return + } + let listener = collection.addDocumentChangeListener( + id: docId, queue: self.backgroundQueue + ) { [weak self] change in + guard let self = self else { return } + let resultData = NSMutableDictionary() + resultData["token"] = uuidToken + resultData["documentId"] = change.documentID + resultData["database"] = change.database.name + resultData["collection"] = DataAdapter.shared.adaptCollectionToNSDictionary( + collection, databaseName: args.databaseName + ) + self.sendEventClosure("collectionDocumentChange", resultData) + } + ListenerTokenStore.shared.add( + token: uuidToken, + record: ChangeListenerRecord( + nativeListenerToken: listener, listenerType: .collectionDocument + ) + ) + resolve(nil) + } catch let error as NSError { + reject("DATABASE_ERROR", error.localizedDescription, nil) + } catch { + reject("DATABASE_ERROR", error.localizedDescription, nil) + } + } + } + + public func collection_RemoveChangeListener( + changeListenerToken: String, + resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) { + backgroundQueue.async { + guard let record = ListenerTokenStore.shared.remove(token: changeListenerToken) else { + reject("LISTENER_ERROR", + "No listener found for token \(changeListenerToken)", nil) + return + } + record.nativeListenerToken.remove() + resolve(nil) + } + } +} diff --git a/ios/CblDatabaseModule.swift b/ios/CblDatabaseModule.swift new file mode 100644 index 0000000..026e83a --- /dev/null +++ b/ios/CblDatabaseModule.swift @@ -0,0 +1,234 @@ +import Foundation +import CouchbaseLiteSwift + +@objcMembers public class CblDatabaseModule: NSObject { + + private let backgroundQueue = CblNativeQueue.shared + + public func database_Open( + name: String, + directory: String?, + encryptionKey: String?, + resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) { + let (isError, databaseName) = DataAdapter.shared.adaptDatabaseName( + name: name as NSString, reject: reject + ) + if isError { return } + + var config: [AnyHashable: Any] = [:] + if let dir = directory, !dir.isEmpty, dir != "null", dir != "undefined" { + config["directory"] = dir + } + if let key = encryptionKey, !key.isEmpty, key != "null", key != "undefined" { + config["encryptionKey"] = key + } + + backgroundQueue.async { + do { + let uniqueName = try DatabaseManager.shared.open( + databaseName, databaseConfig: config + ) + resolve(["databaseUniqueName": uniqueName]) + } catch let error as NSError { + reject("DATABASE_ERROR", error.localizedDescription, nil) + } catch { + reject("DATABASE_ERROR", error.localizedDescription, nil) + } + } + } + + public func database_Close( + name: String, + resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) { + let (isError, databaseName) = DataAdapter.shared.adaptDatabaseName( + name: name as NSString, reject: reject + ) + if isError { return } + backgroundQueue.async { + do { + try DatabaseManager.shared.close(databaseName) + resolve(nil) + } catch let error as NSError { + reject("DATABASE_ERROR", error.localizedDescription, nil) + } catch { + reject("DATABASE_ERROR", error.localizedDescription, nil) + } + } + } + + public func database_Delete( + name: String, + resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) { + let (isError, databaseName) = DataAdapter.shared.adaptDatabaseName( + name: name as NSString, reject: reject + ) + if isError { return } + backgroundQueue.async { + do { + try DatabaseManager.shared.delete(databaseName) + resolve(nil) + } catch let error as NSError { + reject("DATABASE_ERROR", error.localizedDescription, nil) + } catch { + reject("DATABASE_ERROR", error.localizedDescription, nil) + } + } + } + + public func database_DeleteWithPath( + path: String, + name: String, + resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) { + let (isPathError, databasePath) = DataAdapter.shared.adaptNonEmptyString( + value: path as NSString, propertyName: "path", reject: reject + ) + let (isError, databaseName) = DataAdapter.shared.adaptDatabaseName( + name: name as NSString, reject: reject + ) + if isPathError || isError { return } + backgroundQueue.async { + do { + try DatabaseManager.shared.delete(databasePath, databaseName: databaseName) + resolve(nil) + } catch let error as NSError { + reject("DATABASE_ERROR", error.localizedDescription, nil) + } catch { + reject("DATABASE_ERROR", error.localizedDescription, nil) + } + } + } + + public func database_Copy( + path: String, + newName: String, + directory: String?, + encryptionKey: String?, + resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) { + let (isError, databaseName) = DataAdapter.shared.adaptDatabaseName( + name: newName as NSString, reject: reject + ) + let (isPathError, databasePath) = DataAdapter.shared.adaptNonEmptyString( + value: path as NSString, propertyName: "path", reject: reject + ) + if isError || isPathError { return } + + var config: [AnyHashable: Any] = [:] + if let dir = directory, !dir.isEmpty, dir != "null", dir != "undefined" { + config["directory"] = dir + } + if let key = encryptionKey, !key.isEmpty, key != "null", key != "undefined" { + config["encryptionKey"] = key + } + + backgroundQueue.async { + do { + try DatabaseManager.shared.copy( + databasePath, newName: databaseName, databaseConfig: config + ) + resolve(nil) + } catch let error as NSError { + reject("DATABASE_ERROR", error.localizedDescription, nil) + } catch { + reject("DATABASE_ERROR", error.localizedDescription, nil) + } + } + } + + public func database_Exists( + name: String, + directory: String, + resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) { + let (isNameError, databaseName) = DataAdapter.shared.adaptDatabaseName( + name: name as NSString, reject: reject + ) + let (isDirError, path) = DataAdapter.shared.adaptNonEmptyString( + value: directory as NSString, propertyName: "directory", reject: reject + ) + if isNameError || isDirError { return } + backgroundQueue.async { + let exists = DatabaseManager.shared.exists(databaseName, directoryPath: path) + resolve(exists) + } + } + + public func database_GetPath( + name: String, + resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) { + let (isError, databaseName) = DataAdapter.shared.adaptDatabaseName( + name: name as NSString, reject: reject + ) + if isError { return } + backgroundQueue.async { + do { + guard let path = try DatabaseManager.shared.getPath(databaseName) else { + reject("DATABASE_ERROR", + "Unable to get path for database \(databaseName)", nil) + return + } + resolve(path) + } catch let error as NSError { + reject("DATABASE_ERROR", error.localizedDescription, nil) + } catch { + reject("DATABASE_ERROR", error.localizedDescription, nil) + } + } + } + + public func database_PerformMaintenance( + maintenanceType: Double, + databaseName: String, + resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) { + let mType = DataAdapter.shared.adaptMaintenanceTypeFromInt(intValue: Int(maintenanceType)) + backgroundQueue.async { + do { + try DatabaseManager.shared.performMaintenance( + databaseName, maintenanceType: mType + ) + resolve(nil) + } catch let error as NSError { + reject("DATABASE_ERROR", error.localizedDescription, nil) + } catch { + reject("DATABASE_ERROR", error.localizedDescription, nil) + } + } + } + + public func database_ChangeEncryptionKey( + newKey: String, + name: String, + resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) { + let (isError, databaseName) = DataAdapter.shared.adaptDatabaseName( + name: name as NSString, reject: reject + ) + if isError { return } + let keyToUse = newKey.isEmpty ? nil : newKey + backgroundQueue.async { + do { + try DatabaseManager.shared.changeEncryptionKey(databaseName, newKey: keyToUse) + resolve(nil) + } catch let error as NSError { + reject("DATABASE_ERROR", error.localizedDescription, nil) + } catch { + reject("DATABASE_ERROR", error.localizedDescription, nil) + } + } + } +} diff --git a/ios/CblDocumentModule.swift b/ios/CblDocumentModule.swift new file mode 100644 index 0000000..cb7f7ac --- /dev/null +++ b/ios/CblDocumentModule.swift @@ -0,0 +1,317 @@ +import Foundation +import CouchbaseLiteSwift + +@objcMembers public class CblDocumentModule: NSObject { + + private let backgroundQueue = CblNativeQueue.shared + + public func collection_GetDocument( + docId: String, + name: String, + scopeName: String, + collectionName: String, + resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) { + let (isError, args) = DataAdapter.shared.adaptCollectionArgs( + name: name as NSString, + collectionName: collectionName as NSString, + scopeName: scopeName as NSString, + reject: reject + ) + let (isDocError, documentId) = DataAdapter.shared.adaptNonEmptyString( + value: docId as NSString, propertyName: "docId", reject: reject + ) + if isError || isDocError { return } + backgroundQueue.async { + do { + guard let doc = try CollectionManager.shared.document( + documentId, + collectionName: args.collectionName, + scopeName: args.scopeName, + databaseName: args.databaseName + ) else { + resolve(NSDictionary()) + return + } + var data: [String: Any] = [:] + let documentJson = doc.toJSON() + if !documentJson.isEmpty { + guard let jsonData = documentJson.data(using: .utf8), + let jsonDict = try JSONSerialization.jsonObject( + with: jsonData, options: [] + ) as? [String: Any] else { + reject("DOCUMENT_ERROR", "Failed to parse document JSON", nil) + return + } + data["_data"] = jsonDict + } else { + data["_data"] = [:] + } + data["_id"] = documentId + data["_sequence"] = doc.sequence + resolve(data as NSDictionary) + } catch let error as NSError { + reject("DATABASE_ERROR", error.localizedDescription, nil) + } catch { + reject("DATABASE_ERROR", error.localizedDescription, nil) + } + } + } + + public func collection_Save( + document: String, + blobs: String, + docId: String, + name: String, + scopeName: String, + collectionName: String, + concurrencyControlValue: Double, + resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) { + let (isError, args) = DataAdapter.shared.adaptCollectionArgs( + name: name as NSString, + collectionName: collectionName as NSString, + scopeName: scopeName as NSString, + reject: reject + ) + let (isDocError, documentArgs) = DataAdapter.shared.adaptDocumentArgs( + docId: docId as NSString, + concurrencyControlValue: NSNumber(value: concurrencyControlValue), + reject: reject + ) + if isError || isDocError { return } + let (isBlobError, documentBlobArgs) = DataAdapter.shared.adaptDocumentBlobStrings( + document: document as NSString, blobs: blobs as NSString, reject: reject + ) + if isBlobError { return } + backgroundQueue.async { + do { + let blobMap = try CollectionManager.shared.blobsFromJsonString( + documentBlobArgs.blobs + ) + let result = try CollectionManager.shared.saveDocument( + documentArgs.documentId, + document: documentBlobArgs.document, + blobs: blobMap, + concurrencyControl: documentArgs.concurrencyControlValue, + collectionName: args.collectionName, + scopeName: args.scopeName, + databaseName: args.databaseName + ) + resolve([ + "_id": result.id, + "_revId": result.revId ?? "", + "_sequence": result.sequence, + "concurrencyControlResult": result.concurrencyControl as Any + ]) + } catch let error as NSError { + reject("DATABASE_ERROR", error.localizedDescription, nil) + } catch { + reject("DATABASE_ERROR", error.localizedDescription, nil) + } + } + } + + public func collection_DeleteDocument( + docId: String, + name: String, + scopeName: String, + collectionName: String, + concurrencyControl: Double, + resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) { + let (isError, args) = DataAdapter.shared.adaptCollectionArgs( + name: name as NSString, + collectionName: collectionName as NSString, + scopeName: scopeName as NSString, + reject: reject + ) + let (isDocError, documentArgs) = DataAdapter.shared.adaptDocumentArgs( + docId: docId as NSString, + concurrencyControlValue: NSNumber(value: concurrencyControl), + reject: reject + ) + if isError || isDocError { return } + backgroundQueue.async { + do { + let result = try CollectionManager.shared.deleteDocument( + documentArgs.documentId, + concurrencyControl: documentArgs.concurrencyControlValue, + collectionName: args.collectionName, + scopeName: args.scopeName, + databaseName: args.databaseName + ) + resolve(["concurrencyControlResult": result]) + } catch let error as NSError { + reject("DATABASE_ERROR", error.localizedDescription, nil) + } catch { + reject("DATABASE_ERROR", error.localizedDescription, nil) + } + } + } + + public func collection_PurgeDocument( + docId: String, + name: String, + scopeName: String, + collectionName: String, + resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) { + let (isError, args) = DataAdapter.shared.adaptCollectionArgs( + name: name as NSString, + collectionName: collectionName as NSString, + scopeName: scopeName as NSString, + reject: reject + ) + let (isDocError, documentId) = DataAdapter.shared.adaptNonEmptyString( + value: docId as NSString, propertyName: "docId", reject: reject + ) + if isError || isDocError { return } + backgroundQueue.async { + do { + try CollectionManager.shared.purgeDocument( + documentId, + collectionName: args.collectionName, + scopeName: args.scopeName, + databaseName: args.databaseName + ) + resolve(nil) + } catch let error as NSError { + reject("DATABASE_ERROR", error.localizedDescription, nil) + } catch { + reject("DATABASE_ERROR", error.localizedDescription, nil) + } + } + } + + public func collection_GetDocumentExpiration( + docId: String, + name: String, + scopeName: String, + collectionName: String, + resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) { + let (isError, args) = DataAdapter.shared.adaptCollectionArgs( + name: name as NSString, + collectionName: collectionName as NSString, + scopeName: scopeName as NSString, + reject: reject + ) + let (isDocError, documentId) = DataAdapter.shared.adaptNonEmptyString( + value: docId as NSString, propertyName: "docId", reject: reject + ) + if isError || isDocError { return } + backgroundQueue.async { + do { + if let date = try CollectionManager.shared.getDocumentExpiration( + documentId, + collectionName: args.collectionName, + scopeName: args.scopeName, + databaseName: args.databaseName + ) { + let formatter = ISO8601DateFormatter() + resolve(["date": formatter.string(from: date)]) + } else { + resolve(nil) + } + } catch let error as NSError { + reject("DATABASE_ERROR", error.localizedDescription, nil) + } catch { + reject("DATABASE_ERROR", error.localizedDescription, nil) + } + } + } + + public func collection_SetDocumentExpiration( + expiration: String, + docId: String, + name: String, + scopeName: String, + collectionName: String, + resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) { + let (isError, args) = DataAdapter.shared.adaptCollectionArgs( + name: name as NSString, + collectionName: collectionName as NSString, + scopeName: scopeName as NSString, + reject: reject + ) + let (isDocError, documentId) = DataAdapter.shared.adaptNonEmptyString( + value: docId as NSString, propertyName: "docId", reject: reject + ) + if isError || isDocError { return } + backgroundQueue.async { + do { + let formatter = ISO8601DateFormatter() + formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + guard let date = formatter.date(from: expiration) else { + reject("DATABASE_ERROR", + "Unable to convert date to ISO8601. " + + "Validate expiration is in ISO8601 format.", nil) + return + } + try CollectionManager.shared.setDocumentExpiration( + documentId, + expiration: date, + collectionName: args.collectionName, + scopeName: args.scopeName, + databaseName: args.databaseName + ) + resolve(nil) + } catch let error as NSError { + reject("DATABASE_ERROR", error.localizedDescription, nil) + } catch { + reject("DATABASE_ERROR", error.localizedDescription, nil) + } + } + } + + public func collection_GetBlobContent( + key: String, + documentId: String, + name: String, + scopeName: String, + collectionName: String, + resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) { + let (isError, args) = DataAdapter.shared.adaptCollectionArgs( + name: name as NSString, + collectionName: collectionName as NSString, + scopeName: scopeName as NSString, + reject: reject + ) + let (isDocError, docId) = DataAdapter.shared.adaptNonEmptyString( + value: documentId as NSString, propertyName: "docId", reject: reject + ) + let (isKeyError, keyValue) = DataAdapter.shared.adaptNonEmptyString( + value: key as NSString, propertyName: "key", reject: reject + ) + if isError || isDocError || isKeyError { return } + backgroundQueue.async { + do { + guard let blob = try CollectionManager.shared.getBlobContent( + keyValue, + documentId: docId, + collectionName: args.collectionName, + scopeName: args.scopeName, + databaseName: args.databaseName + ) else { + resolve(["data": []]) + return + } + resolve(["data": blob]) + } catch let error as NSError { + reject("DATABASE_ERROR", error.localizedDescription, nil) + } catch { + reject("DATABASE_ERROR", error.localizedDescription, nil) + } + } + } +} diff --git a/ios/CblEngineModule.swift b/ios/CblEngineModule.swift new file mode 100644 index 0000000..8390509 --- /dev/null +++ b/ios/CblEngineModule.swift @@ -0,0 +1,35 @@ +import Foundation +import CouchbaseLiteSwift + +@objcMembers public class CblEngineModule: NSObject { + + private let backgroundQueue = CblNativeQueue.shared + + public func file_GetDefaultPath( + resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) { + backgroundQueue.async { + let paths = NSSearchPathForDirectoriesInDomains( + .applicationSupportDirectory, .userDomainMask, true + ) + resolve(paths.first ?? "") + } + } + + public func listenerToken_Remove( + changeListenerToken: String, + resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) { + backgroundQueue.async { + guard let record = ListenerTokenStore.shared.remove(token: changeListenerToken) else { + reject("LISTENER_ERROR", + "No listener found for token \(changeListenerToken)", nil) + return + } + record.nativeListenerToken.remove() + resolve(nil) + } + } +} diff --git a/ios/CblLoggingModule.swift b/ios/CblLoggingModule.swift new file mode 100644 index 0000000..e32c4d5 --- /dev/null +++ b/ios/CblLoggingModule.swift @@ -0,0 +1,140 @@ +import Foundation +import CouchbaseLiteSwift + +@objcMembers public class CblLoggingModule: NSObject { + + private let backgroundQueue = CblNativeQueue.shared + private let sendEventClosure: (String, Any?) -> Void + + @objc public init(sendEvent: @escaping (String, Any?) -> Void) { + self.sendEventClosure = sendEvent + super.init() + } + + public func database_SetLogLevel( + domain: String, logLevel: Double, + resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) { + backgroundQueue.async { + do { + try LoggingManager.shared.setLogLevel(domain, logLevel: Int(logLevel)) + resolve(nil) + } catch let error as NSError { + reject("DATABASE_ERROR", error.localizedDescription, nil) + } catch { + reject("DATABASE_ERROR", error.localizedDescription, nil) + } + } + } + + public func database_SetFileLoggingConfig( + name: String, directory: String, logLevel: Double, + maxSize: Double, maxRotateCount: Double, shouldUsePlainText: Bool, + resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) { + var config: [String: Any] = [:] + config["level"] = Int(logLevel) + config["directory"] = directory + config["maxRotateCount"] = Int(maxRotateCount) + config["maxSize"] = Int64(maxSize) + config["usePlainText"] = shouldUsePlainText + backgroundQueue.async { + do { + try LoggingManager.shared.setFileLogging(name, config: config) + resolve(nil) + } catch let error as NSError { + reject("DATABASE_ERROR", error.localizedDescription, nil) + } catch { + reject("DATABASE_ERROR", error.localizedDescription, nil) + } + } + } + + // With Turbo Modules, codegen enforces `double` and `[String]` — the JS layer + // sends -1 for "disable" and an empty array for "all domains". The legacy code used + // Any? and NSNumber? which allowed nil, but Turbo Modules never send nil for non-optional params. + public func logsinks_SetConsole( + level: Double, domains: [String], + resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) { + backgroundQueue.async { + do { + let intLevel = Int(level) == -1 ? nil : Int(level) + let domainsArray: [String]? = domains.isEmpty ? nil : domains + try LogSinksManager.shared.setConsoleSink(level: intLevel, domains: domainsArray) + resolve(nil) + } catch let error as NSError { + reject("LOGSINKS_ERROR", error.localizedDescription, error) + } catch { + reject("LOGSINKS_ERROR", error.localizedDescription, nil) + } + } + } + + public func logsinks_SetFile( + level: Double, config: Any, + resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) { + backgroundQueue.async { + do { + let intLevel = Int(level) == -1 ? nil : Int(level) + let configDict = config as? [String: Any] + try LogSinksManager.shared.setFileSink(level: intLevel, config: configDict) + resolve(nil) + } catch let error as NSError { + reject("LOGSINKS_ERROR", error.localizedDescription, error) + } catch { + reject("LOGSINKS_ERROR", error.localizedDescription, nil) + } + } + } + + public func logsinks_SetCustom( + level: Double, domains: [String], token: String, + resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) { + backgroundQueue.async { + do { + let intLevel = Int(level) == -1 ? nil : Int(level) + let domainsArray = domains.isEmpty ? nil : domains + let tokenValue = token.isEmpty ? nil : token + let callback: ((LogLevel, LogDomain, String) -> Void)? = + (intLevel != nil && tokenValue != nil) ? + { [weak self] logLevel, logDomain, message in + guard let self = self else { return } + let eventData: [String: Any] = [ + "token": tokenValue!, + "level": logLevel.rawValue, + "domain": self.logDomainToString(logDomain), + "message": message + ] + self.sendEventClosure("customLogMessage", eventData) + } : nil + try LogSinksManager.shared.setCustomSink( + level: intLevel, domains: domainsArray, callback: callback + ) + resolve(nil) + } catch let error as NSError { + reject("LOGSINKS_ERROR", error.localizedDescription, error) + } catch { + reject("LOGSINKS_ERROR", error.localizedDescription, nil) + } + } + } + + private func logDomainToString(_ domain: LogDomain) -> String { + switch domain { + case .database: return "DATABASE" + case .query: return "QUERY" + case .replicator: return "REPLICATOR" + case .network: return "NETWORK" + case .listener: return "LISTENER" + default: return "UNKNOWN" + } + } +} diff --git a/ios/CblNativeQueue.swift b/ios/CblNativeQueue.swift new file mode 100644 index 0000000..233bb95 --- /dev/null +++ b/ios/CblNativeQueue.swift @@ -0,0 +1,8 @@ +import Foundation + +enum CblNativeQueue { + static let shared = DispatchQueue( + label: "com.cblite.reactnative.backgroundQueue", + qos: .userInitiated + ) +} diff --git a/ios/CblQueryModule.swift b/ios/CblQueryModule.swift new file mode 100644 index 0000000..b0ab6ec --- /dev/null +++ b/ios/CblQueryModule.swift @@ -0,0 +1,157 @@ +import Foundation +import CouchbaseLiteSwift + +@objcMembers public class CblQueryModule: NSObject { + + private let backgroundQueue = CblNativeQueue.shared + private let sendEventClosure: (String, Any?) -> Void + + @objc public init(sendEvent: @escaping (String, Any?) -> Void) { + self.sendEventClosure = sendEvent + super.init() + } + + public func query_Execute( + query: String, parameters: Any, name: String, + resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) { + let (isError, databaseName) = DataAdapter.shared.adaptDatabaseName( + name: name as NSString, reject: reject + ) + let parametersDict = parameters as? NSDictionary ?? NSDictionary() + let (isQueryError, queryArgs) = DataAdapter.shared.adaptQueryParameter( + query: query as NSString, parameters: parametersDict, reject: reject + ) + if isError || isQueryError { return } + backgroundQueue.async { + do { + let results: String + if let params = queryArgs.parameters { + results = try DatabaseManager.shared.executeQuery( + queryArgs.query, parameters: params, databaseName: databaseName + ) + } else { + results = try DatabaseManager.shared.executeQuery( + queryArgs.query, databaseName: databaseName + ) + } + resolve(["data": results]) + } catch let error as NSError { + reject("DATABASE_ERROR", error.localizedDescription, nil) + } catch { + reject("DATABASE_ERROR", error.localizedDescription, nil) + } + } + } + + public func query_Explain( + query: String, parameters: Any, name: String, + resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) { + let (isError, databaseName) = DataAdapter.shared.adaptDatabaseName( + name: name as NSString, reject: reject + ) + let parametersDict = parameters as? NSDictionary ?? NSDictionary() + let (isQueryError, queryArgs) = DataAdapter.shared.adaptQueryParameter( + query: query as NSString, parameters: parametersDict, reject: reject + ) + if isError || isQueryError { return } + backgroundQueue.async { + do { + let results: String + if let params = queryArgs.parameters { + results = try DatabaseManager.shared.queryExplain( + queryArgs.query, parameters: params, databaseName: databaseName + ) + } else { + results = try DatabaseManager.shared.queryExplain( + queryArgs.query, databaseName: databaseName + ) + } + resolve(["data": results]) + } catch let error as NSError { + reject("DATABASE_ERROR", error.localizedDescription, nil) + } catch { + reject("DATABASE_ERROR", error.localizedDescription, nil) + } + } + } + + public func query_AddChangeListener( + changeListenerToken: String, query: String, parameters: Any, name: String, + resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) { + let (isError, databaseName) = DataAdapter.shared.adaptDatabaseName( + name: name as NSString, reject: reject + ) + let (isTokenError, uuidToken) = DataAdapter.shared.adaptNonEmptyString( + value: changeListenerToken as NSString, + propertyName: "changeListenerToken", reject: reject + ) + let (isQueryError, queryString) = DataAdapter.shared.adaptNonEmptyString( + value: query as NSString, propertyName: "query", reject: reject + ) + if isError || isTokenError || isQueryError { return } + backgroundQueue.async { + do { + guard let database = DatabaseManager.shared.getDatabase(databaseName) else { + reject("DATABASE_ERROR", + "Could not find database with name \(databaseName)", nil) + return + } + let q = try database.createQuery(queryString) + let parametersDict = parameters as? [String: Any] ?? [:] + if !parametersDict.isEmpty { + let params = try QueryHelper.getParamatersFromJson(parametersDict) + q.parameters = params + } + let listener = q.addChangeListener( + withQueue: self.backgroundQueue + ) { [weak self] change in + guard let self = self else { return } + let resultData = NSMutableDictionary() + resultData["token"] = uuidToken + if let results = change.results { + let jsonArray = "[" + + results.map { $0.toJSON() }.joined(separator: ",") + "]" + resultData["data"] = jsonArray + } + if let error = change.error { + resultData["error"] = error.localizedDescription + } + self.sendEventClosure("queryChange", resultData) + } + ListenerTokenStore.shared.add( + token: uuidToken, + record: ChangeListenerRecord( + nativeListenerToken: listener, listenerType: .query + ) + ) + resolve(nil) + } catch let error as NSError { + reject("DATABASE_ERROR", error.localizedDescription, nil) + } catch { + reject("DATABASE_ERROR", error.localizedDescription, nil) + } + } + } + + public func query_RemoveChangeListener( + changeListenerToken: String, + resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) { + backgroundQueue.async { + guard let record = ListenerTokenStore.shared.remove(token: changeListenerToken) else { + reject("LISTENER_ERROR", + "No listener found for token \(changeListenerToken)", nil) + return + } + record.nativeListenerToken.remove() + resolve(nil) + } + } +} diff --git a/ios/CblReactnative-Bridging-Header.h b/ios/CblReactnative-Bridging-Header.h index e5bc832..ad284d2 100644 --- a/ios/CblReactnative-Bridging-Header.h +++ b/ios/CblReactnative-Bridging-Header.h @@ -1,3 +1 @@ -#import #import -#import diff --git a/ios/CblReplicatorModule.swift b/ios/CblReplicatorModule.swift new file mode 100644 index 0000000..1f482d2 --- /dev/null +++ b/ios/CblReplicatorModule.swift @@ -0,0 +1,303 @@ +import Foundation +import CouchbaseLiteSwift + +@objcMembers public class CblReplicatorModule: NSObject { + + private let backgroundQueue = CblNativeQueue.shared + private let sendEventClosure: (String, Any?) -> Void + + @objc public init(sendEvent: @escaping (String, Any?) -> Void) { + self.sendEventClosure = sendEvent + super.init() + } + + public func replicator_Create( + config: Any, + resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) { + guard let repConfig = config as? [String: Any], + let collectionConfigJson = repConfig["collectionConfig"] as? String else { + reject("REPLICATOR_ERROR", "Couldn't parse replicator config from dictionary", nil) + return + } + backgroundQueue.async { + do { + let replicatorId = try ReplicatorManager.shared.replicator( + repConfig, collectionConfigJson: collectionConfigJson + ) + resolve(["replicatorId": replicatorId]) + } catch let error as NSError { + reject("REPLICATOR_ERROR", error.localizedDescription, nil) + } catch { + reject("REPLICATOR_ERROR", error.localizedDescription, nil) + } + } + } + + public func replicator_Start( + replicatorId: String, + resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) { + let (isError, repId) = DataAdapter.shared.adaptReplicatorId( + replicatorId: replicatorId as NSString, reject: reject + ) + if isError { return } + backgroundQueue.async { + do { + try ReplicatorManager.shared.start(repId) + resolve(nil) + } catch let error as NSError { + reject("REPLICATOR_ERROR", error.localizedDescription, nil) + } catch { + reject("REPLICATOR_ERROR", error.localizedDescription, nil) + } + } + } + + public func replicator_Stop( + replicatorId: String, + resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) { + let (isError, repId) = DataAdapter.shared.adaptReplicatorId( + replicatorId: replicatorId as NSString, reject: reject + ) + if isError { return } + backgroundQueue.async { + do { + try ReplicatorManager.shared.stop(repId) + resolve(nil) + } catch let error as NSError { + reject("REPLICATOR_ERROR", error.localizedDescription, nil) + } catch { + reject("REPLICATOR_ERROR", error.localizedDescription, nil) + } + } + } + + public func replicator_Cleanup( + replicatorId: String, + resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) { + let (isError, repId) = DataAdapter.shared.adaptReplicatorId( + replicatorId: replicatorId as NSString, reject: reject + ) + if isError { return } + backgroundQueue.async { + do { + try ReplicatorManager.shared.cleanUp(repId) + resolve(nil) + } catch let error as NSError { + reject("REPLICATOR_ERROR", error.localizedDescription, nil) + } catch { + reject("REPLICATOR_ERROR", error.localizedDescription, nil) + } + } + } + + public func replicator_GetStatus( + replicatorId: String, + resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) { + let (isError, repId) = DataAdapter.shared.adaptReplicatorId( + replicatorId: replicatorId as NSString, reject: reject + ) + if isError { return } + backgroundQueue.async { + do { + let status = try ReplicatorManager.shared.getStatus(repId) + resolve(NSDictionary(dictionary: status)) + } catch let error as NSError { + reject("REPLICATOR_ERROR", error.localizedDescription, nil) + } catch { + reject("REPLICATOR_ERROR", error.localizedDescription, nil) + } + } + } + + public func replicator_ResetCheckpoint( + replicatorId: String, + resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) { + let (isError, repId) = DataAdapter.shared.adaptReplicatorId( + replicatorId: replicatorId as NSString, reject: reject + ) + if isError { return } + backgroundQueue.async { + do { + try ReplicatorManager.shared.resetCheckpoint(repId) + resolve(nil) + } catch let error as NSError { + reject("REPLICATOR_ERROR", error.localizedDescription, nil) + } catch { + reject("REPLICATOR_ERROR", error.localizedDescription, nil) + } + } + } + + public func replicator_GetPendingDocumentIds( + replicatorId: String, name: String, scopeName: String, collectionName: String, + resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) { + let (isError, repId) = DataAdapter.shared.adaptReplicatorId( + replicatorId: replicatorId as NSString, reject: reject + ) + let (isCollError, args) = DataAdapter.shared.adaptCollectionArgs( + name: name as NSString, collectionName: collectionName as NSString, + scopeName: scopeName as NSString, reject: reject + ) + if isError || isCollError { return } + backgroundQueue.async { + do { + guard let collection = try CollectionManager.shared.getCollection( + args.collectionName, scopeName: args.scopeName, + databaseName: args.databaseName + ) else { + reject("REPLICATOR_ERROR", "Couldn't resolve collection passed in", nil) + return + } + let pendingIds = try ReplicatorManager.shared.getPendingDocumentIds( + repId, collection: collection + ) + resolve(["pendingDocumentIds": pendingIds]) + } catch let error as NSError { + reject("REPLICATOR_ERROR", error.localizedDescription, nil) + } catch { + reject("REPLICATOR_ERROR", error.localizedDescription, nil) + } + } + } + + public func replicator_IsDocumentPending( + documentId: String, replicatorId: String, + name: String, scopeName: String, collectionName: String, + resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) { + let (isError, repId) = DataAdapter.shared.adaptReplicatorId( + replicatorId: replicatorId as NSString, reject: reject + ) + let (isCollError, args) = DataAdapter.shared.adaptCollectionArgs( + name: name as NSString, collectionName: collectionName as NSString, + scopeName: scopeName as NSString, reject: reject + ) + let (isDocError, docId) = DataAdapter.shared.adaptNonEmptyString( + value: documentId as NSString, propertyName: "docId", reject: reject + ) + if isError || isCollError || isDocError { return } + backgroundQueue.async { + do { + guard let collection = try CollectionManager.shared.getCollection( + args.collectionName, scopeName: args.scopeName, + databaseName: args.databaseName + ) else { + reject("REPLICATOR_ERROR", "Couldn't resolve collection passed in", nil) + return + } + let isPending = try ReplicatorManager.shared.isDocumentPending( + repId, documentId: docId, collection: collection + ) + resolve(NSDictionary(dictionary: isPending)) + } catch let error as NSError { + reject("REPLICATOR_ERROR", error.localizedDescription, nil) + } catch { + reject("REPLICATOR_ERROR", error.localizedDescription, nil) + } + } + } + + public func replicator_AddChangeListener( + changeListenerToken: String, replicatorId: String, + resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) { + let replId = replicatorId + let uuidToken = changeListenerToken + backgroundQueue.async { + guard let replicator = ReplicatorManager.shared.getReplicator(replicatorId: replId) + else { + reject("REPLICATOR_ERROR", + "No such replicator found for id \(replId)", nil) + return + } + let listener = replicator.addChangeListener( + withQueue: self.backgroundQueue + ) { [weak self] change in + guard let self = self else { return } + let statusJson = ReplicatorHelper.generateReplicatorStatusJson(change.status) + let resultData = NSMutableDictionary() + resultData["token"] = uuidToken + resultData["status"] = statusJson + self.sendEventClosure("replicatorStatusChange", resultData) + } + ListenerTokenStore.shared.add( + token: uuidToken, + record: ChangeListenerRecord( + nativeListenerToken: listener, listenerType: .replicator + ) + ) + resolve(nil) + } + } + + public func replicator_AddDocumentChangeListener( + changeListenerToken: String, replicatorId: String, + resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) { + let replId = replicatorId + let uuidToken = changeListenerToken + backgroundQueue.async { + guard let replicator = ReplicatorManager.shared.getReplicator(replicatorId: replId) + else { + reject("REPLICATOR_ERROR", + "No such replicator found for id \(replId)", nil) + return + } + let listener = replicator.addDocumentReplicationListener( + withQueue: self.backgroundQueue + ) { [weak self] change in + guard let self = self else { return } + let documentJson = ReplicatorHelper.generateReplicationJson( + change.documents, isPush: change.isPush + ) + let resultData = NSMutableDictionary() + resultData["token"] = uuidToken + resultData["documents"] = documentJson + self.sendEventClosure("replicatorDocumentChange", resultData) + } + ListenerTokenStore.shared.add( + token: uuidToken, + record: ChangeListenerRecord( + nativeListenerToken: listener, listenerType: .replicatorDocument + ) + ) + resolve(nil) + } + } + + // replicatorId is present in the TypeScript spec but intentionally unused — + // matches legacy behaviour at CblReactnative.swift line 1622 + public func replicator_RemoveChangeListener( + changeListenerToken: String, + replicatorId: String, + resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) { + backgroundQueue.async { + guard let record = ListenerTokenStore.shared.remove(token: changeListenerToken) else { + reject("LISTENER_ERROR", + "No listener found for token \(changeListenerToken)", nil) + return + } + record.nativeListenerToken.remove() + resolve(nil) + } + } +} diff --git a/ios/CblScopeModule.swift b/ios/CblScopeModule.swift new file mode 100644 index 0000000..ea26e5e --- /dev/null +++ b/ios/CblScopeModule.swift @@ -0,0 +1,95 @@ +import Foundation +import CouchbaseLiteSwift + +@objcMembers public class CblScopeModule: NSObject { + + private let backgroundQueue = CblNativeQueue.shared + + public func scope_GetDefault( + name: String, + resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) { + let (isError, databaseName) = DataAdapter.shared.adaptDatabaseName( + name: name as NSString, reject: reject + ) + if isError { return } + backgroundQueue.async { + do { + guard let scope = try DatabaseManager.shared.defaultScope(databaseName) else { + reject("DATABASE_ERROR", + "Unable to get default scope in database <\(databaseName)>", nil) + return + } + let dict = DataAdapter.shared.adaptScopeToNSDictionary( + scope, databaseName: databaseName + ) + resolve(dict) + } catch let error as NSError { + reject("DATABASE_ERROR", error.localizedDescription, nil) + } catch { + reject("DATABASE_ERROR", error.localizedDescription, nil) + } + } + } + + public func scope_GetScope( + scopeName: String, + name: String, + resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) { + let (isError, args) = DataAdapter.shared.adaptScopeArgs( + name: name as NSString, scopeName: scopeName as NSString, reject: reject + ) + if isError { return } + backgroundQueue.async { + do { + guard let scope = try DatabaseManager.shared.scope( + args.scopeName, databaseName: args.databaseName + ) else { + reject("DATABASE_ERROR", + "Unable to get scope <\(args.scopeName)> in database " + + "<\(args.databaseName)>", nil) + return + } + let dict = DataAdapter.shared.adaptScopeToNSDictionary( + scope, databaseName: args.databaseName + ) + resolve(dict) + } catch let error as NSError { + reject("DATABASE_ERROR", error.localizedDescription, nil) + } catch { + reject("DATABASE_ERROR", error.localizedDescription, nil) + } + } + } + + public func scope_GetScopes( + name: String, + resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) { + let (isError, databaseName) = DataAdapter.shared.adaptDatabaseName( + name: name as NSString, reject: reject + ) + if isError { return } + backgroundQueue.async { + do { + guard let scopes = try DatabaseManager.shared.scopes(databaseName) else { + reject("DATABASE_ERROR", + "Unable to get scopes for database \(databaseName)", nil) + return + } + let scopesArray = DataAdapter.shared.adaptScopesToNSDictionary( + scopes, databaseName: databaseName + ) + resolve(["scopes": scopesArray]) + } catch let error as NSError { + reject("DATABASE_ERROR", error.localizedDescription, nil) + } catch { + reject("DATABASE_ERROR", error.localizedDescription, nil) + } + } + } +} diff --git a/ios/ListenerTokenStore.swift b/ios/ListenerTokenStore.swift new file mode 100644 index 0000000..b2bf643 --- /dev/null +++ b/ios/ListenerTokenStore.swift @@ -0,0 +1,47 @@ +import Foundation +import CouchbaseLiteSwift + +struct ChangeListenerRecord { + let nativeListenerToken: ListenerToken + let listenerType: ChangeListenerType +} + +enum ChangeListenerType: String { + case collection + case collectionDocument + case query + case replicator + case replicatorDocument +} + +public class ListenerTokenStore { + + public static let shared: ListenerTokenStore = ListenerTokenStore() + private init() {} + + private let queue = DispatchQueue( + label: "com.cblite.ListenerTokenStore", + attributes: .concurrent + ) + private var store: [String: ChangeListenerRecord] = [:] + + public func add(token: String, record: ChangeListenerRecord) { + queue.async(flags: .barrier) { + self.store[token] = record + } + } + + public func remove(token: String) -> ChangeListenerRecord? { + var removed: ChangeListenerRecord? + queue.sync(flags: .barrier) { + removed = self.store.removeValue(forKey: token) + } + return removed + } + + public func get(token: String) -> ChangeListenerRecord? { + queue.sync { + return self.store[token] + } + } +} diff --git a/ios/RCTCblModules.h b/ios/RCTCblModules.h new file mode 100644 index 0000000..55f6508 --- /dev/null +++ b/ios/RCTCblModules.h @@ -0,0 +1,17 @@ +#import +#import +#import + +// MARK: - Non-event modules (NSObject) + +@interface RCTCblDatabase : NSObject @end +@interface RCTCblScope : NSObject @end +@interface RCTCblDocument : NSObject @end +@interface RCTCblEngine : NSObject @end + +// MARK: - Event-emitting modules (RCTEventEmitter) + +@interface RCTCblCollection : RCTEventEmitter @end +@interface RCTCblQuery : RCTEventEmitter @end +@interface RCTCblLogging : RCTEventEmitter @end +@interface RCTCblReplicator : RCTEventEmitter @end diff --git a/ios/RCTCblModules.mm b/ios/RCTCblModules.mm new file mode 100644 index 0000000..c8e5c11 --- /dev/null +++ b/ios/RCTCblModules.mm @@ -0,0 +1,645 @@ +#import "RCTCblModules.h" +#import "cbl_reactnative-Swift.h" + +// ============================================================ +// MARK: - RCTCblDatabase +// ============================================================ + +@implementation RCTCblDatabase { + CblDatabaseModule *_impl; +} + +- (id)init { + if (self = [super init]) { + _impl = [CblDatabaseModule new]; + } + return self; +} + ++ (NSString *)moduleName { return @"CblDatabase"; } + +- (std::shared_ptr)getTurboModule: + (const facebook::react::ObjCTurboModule::InitParams &)params { + return std::make_shared(params); +} + +- (void)database_Open:(NSString *)name + directory:(NSString *)directory + encryptionKey:(NSString *)encryptionKey + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + [_impl database_OpenWithName:name directory:directory encryptionKey:encryptionKey + resolve:resolve reject:reject]; +} + +- (void)database_Close:(NSString *)name + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + [_impl database_CloseWithName:name resolve:resolve reject:reject]; +} + +- (void)database_Delete:(NSString *)name + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + [_impl database_DeleteWithName:name resolve:resolve reject:reject]; +} + +- (void)database_DeleteWithPath:(NSString *)path + name:(NSString *)name + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + [_impl database_DeleteWithPathWithPath:path name:name resolve:resolve reject:reject]; +} + +- (void)database_Copy:(NSString *)path + newName:(NSString *)newName + directory:(NSString *)directory + encryptionKey:(NSString *)encryptionKey + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + [_impl database_CopyWithPath:path newName:newName directory:directory + encryptionKey:encryptionKey resolve:resolve reject:reject]; +} + +- (void)database_Exists:(NSString *)name + directory:(NSString *)directory + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + [_impl database_ExistsWithName:name directory:directory resolve:resolve reject:reject]; +} + +- (void)database_GetPath:(NSString *)name + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + [_impl database_GetPathWithName:name resolve:resolve reject:reject]; +} + +- (void)database_PerformMaintenance:(double)maintenanceType + databaseName:(NSString *)databaseName + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + [_impl database_PerformMaintenanceWithMaintenanceType:maintenanceType + databaseName:databaseName + resolve:resolve reject:reject]; +} + +- (void)database_ChangeEncryptionKey:(NSString *)newKey + name:(NSString *)name + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + [_impl database_ChangeEncryptionKeyWithNewKey:newKey name:name resolve:resolve reject:reject]; +} + +@end + +// ============================================================ +// MARK: - RCTCblScope +// ============================================================ + +@implementation RCTCblScope { + CblScopeModule *_impl; +} + +- (id)init { + if (self = [super init]) { + _impl = [CblScopeModule new]; + } + return self; +} + ++ (NSString *)moduleName { return @"CblScope"; } + +- (std::shared_ptr)getTurboModule: + (const facebook::react::ObjCTurboModule::InitParams &)params { + return std::make_shared(params); +} + +- (void)scope_GetDefault:(NSString *)name + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + [_impl scope_GetDefaultWithName:name resolve:resolve reject:reject]; +} + +- (void)scope_GetScope:(NSString *)scopeName + name:(NSString *)name + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + [_impl scope_GetScopeWithScopeName:scopeName name:name resolve:resolve reject:reject]; +} + +- (void)scope_GetScopes:(NSString *)name + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + [_impl scope_GetScopesWithName:name resolve:resolve reject:reject]; +} + +@end + +// ============================================================ +// MARK: - RCTCblDocument +// ============================================================ + +@implementation RCTCblDocument { + CblDocumentModule *_impl; +} + +- (id)init { + if (self = [super init]) { + _impl = [CblDocumentModule new]; + } + return self; +} + ++ (NSString *)moduleName { return @"CblDocument"; } + +- (std::shared_ptr)getTurboModule: + (const facebook::react::ObjCTurboModule::InitParams &)params { + return std::make_shared(params); +} + +- (void)collection_GetDocument:(NSString *)docId + name:(NSString *)name + scopeName:(NSString *)scopeName + collectionName:(NSString *)collectionName + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + [_impl collection_GetDocumentWithDocId:docId name:name scopeName:scopeName + collectionName:collectionName resolve:resolve reject:reject]; +} + +- (void)collection_Save:(NSString *)document + blobs:(NSString *)blobs + docId:(NSString *)docId + name:(NSString *)name + scopeName:(NSString *)scopeName + collectionName:(NSString *)collectionName +concurrencyControlValue:(double)concurrencyControlValue + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + [_impl collection_SaveWithDocument:document blobs:blobs docId:docId name:name + scopeName:scopeName collectionName:collectionName + concurrencyControlValue:concurrencyControlValue resolve:resolve reject:reject]; +} + +- (void)collection_DeleteDocument:(NSString *)docId + name:(NSString *)name + scopeName:(NSString *)scopeName + collectionName:(NSString *)collectionName + concurrencyControl:(double)concurrencyControl + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + [_impl collection_DeleteDocumentWithDocId:docId name:name scopeName:scopeName + collectionName:collectionName + concurrencyControl:concurrencyControl + resolve:resolve reject:reject]; +} + +- (void)collection_PurgeDocument:(NSString *)docId + name:(NSString *)name + scopeName:(NSString *)scopeName + collectionName:(NSString *)collectionName + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + [_impl collection_PurgeDocumentWithDocId:docId name:name scopeName:scopeName + collectionName:collectionName resolve:resolve reject:reject]; +} + +- (void)collection_GetDocumentExpiration:(NSString *)docId + name:(NSString *)name + scopeName:(NSString *)scopeName + collectionName:(NSString *)collectionName + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + [_impl collection_GetDocumentExpirationWithDocId:docId name:name scopeName:scopeName + collectionName:collectionName resolve:resolve reject:reject]; +} + +- (void)collection_SetDocumentExpiration:(NSString *)expiration + docId:(NSString *)docId + name:(NSString *)name + scopeName:(NSString *)scopeName + collectionName:(NSString *)collectionName + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + [_impl collection_SetDocumentExpirationWithExpiration:expiration docId:docId name:name + scopeName:scopeName collectionName:collectionName + resolve:resolve reject:reject]; +} + +- (void)collection_GetBlobContent:(NSString *)key + documentId:(NSString *)documentId + name:(NSString *)name + scopeName:(NSString *)scopeName + collectionName:(NSString *)collectionName + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + [_impl collection_GetBlobContentWithKey:key documentId:documentId name:name + scopeName:scopeName collectionName:collectionName + resolve:resolve reject:reject]; +} + +@end + +// ============================================================ +// MARK: - RCTCblEngine +// ============================================================ + +@implementation RCTCblEngine { + CblEngineModule *_impl; +} + +- (id)init { + if (self = [super init]) { + _impl = [CblEngineModule new]; + } + return self; +} + ++ (NSString *)moduleName { return @"CblEngine"; } + +- (std::shared_ptr)getTurboModule: + (const facebook::react::ObjCTurboModule::InitParams &)params { + return std::make_shared(params); +} + +- (void)file_GetDefaultPath:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + [_impl file_GetDefaultPathWithResolve:resolve reject:reject]; +} + +- (void)listenerToken_Remove:(NSString *)changeListenerToken + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + [_impl listenerToken_RemoveWithChangeListenerToken:changeListenerToken + resolve:resolve reject:reject]; +} + +@end + +// ============================================================ +// MARK: - RCTCblCollection +// ============================================================ + +@implementation RCTCblCollection { + CblCollectionModule *_impl; +} + +- (id)init { + if (self = [super init]) { + __weak typeof(self) weakSelf = self; + _impl = [[CblCollectionModule alloc] initWithSendEvent:^(NSString *name, id body) { + [weakSelf sendEventWithName:name body:body]; + }]; + } + return self; +} + ++ (NSString *)moduleName { return @"CblCollection"; } + +- (NSArray *)supportedEvents { + return @[@"collectionChange", @"collectionDocumentChange"]; +} + +- (std::shared_ptr)getTurboModule: + (const facebook::react::ObjCTurboModule::InitParams &)params { + return std::make_shared(params); +} + +- (void)collection_CreateCollection:(NSString *)collectionName name:(NSString *)name + scopeName:(NSString *)scopeName resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + [_impl collection_CreateCollectionWithCollectionName:collectionName name:name + scopeName:scopeName resolve:resolve reject:reject]; +} + +- (void)collection_DeleteCollection:(NSString *)collectionName name:(NSString *)name + scopeName:(NSString *)scopeName resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + [_impl collection_DeleteCollectionWithCollectionName:collectionName name:name + scopeName:scopeName resolve:resolve reject:reject]; +} + +- (void)collection_GetCollection:(NSString *)collectionName name:(NSString *)name + scopeName:(NSString *)scopeName resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + [_impl collection_GetCollectionWithCollectionName:collectionName name:name + scopeName:scopeName resolve:resolve reject:reject]; +} + +- (void)collection_GetCollections:(NSString *)name scopeName:(NSString *)scopeName + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + [_impl collection_GetCollectionsWithName:name scopeName:scopeName + resolve:resolve reject:reject]; +} + +- (void)collection_GetDefault:(NSString *)name resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + [_impl collection_GetDefaultWithName:name resolve:resolve reject:reject]; +} + +- (void)collection_GetCount:(NSString *)collectionName name:(NSString *)name + scopeName:(NSString *)scopeName resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + [_impl collection_GetCountWithCollectionName:collectionName name:name + scopeName:scopeName resolve:resolve reject:reject]; +} + +- (void)collection_GetFullName:(NSString *)collectionName name:(NSString *)name + scopeName:(NSString *)scopeName resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + [_impl collection_GetFullNameWithCollectionName:collectionName name:name + scopeName:scopeName resolve:resolve reject:reject]; +} + +- (void)collection_CreateIndex:(NSString *)indexName index:(id)index + collectionName:(NSString *)collectionName scopeName:(NSString *)scopeName + name:(NSString *)name resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + [_impl collection_CreateIndexWithIndexName:indexName index:index + collectionName:collectionName scopeName:scopeName + name:name resolve:resolve reject:reject]; +} + +- (void)collection_DeleteIndex:(NSString *)indexName collectionName:(NSString *)collectionName + scopeName:(NSString *)scopeName name:(NSString *)name + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + [_impl collection_DeleteIndexWithIndexName:indexName collectionName:collectionName + scopeName:scopeName name:name resolve:resolve reject:reject]; +} + +- (void)collection_GetIndexes:(NSString *)collectionName scopeName:(NSString *)scopeName + name:(NSString *)name resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + [_impl collection_GetIndexesWithCollectionName:collectionName scopeName:scopeName + name:name resolve:resolve reject:reject]; +} + +- (void)collection_AddChangeListener:(NSString *)changeListenerToken + collectionName:(NSString *)collectionName name:(NSString *)name + scopeName:(NSString *)scopeName resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + [_impl collection_AddChangeListenerWithChangeListenerToken:changeListenerToken + collectionName:collectionName name:name + scopeName:scopeName + resolve:resolve reject:reject]; +} + +- (void)collection_AddDocumentChangeListener:(NSString *)changeListenerToken + documentId:(NSString *)documentId + collectionName:(NSString *)collectionName + name:(NSString *)name scopeName:(NSString *)scopeName + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + [_impl collection_AddDocumentChangeListenerWithChangeListenerToken:changeListenerToken + documentId:documentId + collectionName:collectionName + name:name scopeName:scopeName + resolve:resolve reject:reject]; +} + +- (void)collection_RemoveChangeListener:(NSString *)changeListenerToken + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + [_impl collection_RemoveChangeListenerWithChangeListenerToken:changeListenerToken + resolve:resolve reject:reject]; +} + +@end + +// ============================================================ +// MARK: - RCTCblQuery +// ============================================================ + +@implementation RCTCblQuery { + CblQueryModule *_impl; +} + +- (id)init { + if (self = [super init]) { + __weak typeof(self) weakSelf = self; + _impl = [[CblQueryModule alloc] initWithSendEvent:^(NSString *name, id body) { + [weakSelf sendEventWithName:name body:body]; + }]; + } + return self; +} + ++ (NSString *)moduleName { return @"CblQuery"; } + +- (NSArray *)supportedEvents { return @[@"queryChange"]; } + +- (std::shared_ptr)getTurboModule: + (const facebook::react::ObjCTurboModule::InitParams &)params { + return std::make_shared(params); +} + +- (void)query_Execute:(NSString *)query parameters:(id)parameters name:(NSString *)name + resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [_impl query_ExecuteWithQuery:query parameters:parameters name:name + resolve:resolve reject:reject]; +} + +- (void)query_Explain:(NSString *)query parameters:(id)parameters name:(NSString *)name + resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [_impl query_ExplainWithQuery:query parameters:parameters name:name + resolve:resolve reject:reject]; +} + +- (void)query_AddChangeListener:(NSString *)changeListenerToken query:(NSString *)query + parameters:(id)parameters name:(NSString *)name + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + [_impl query_AddChangeListenerWithChangeListenerToken:changeListenerToken query:query + parameters:parameters name:name + resolve:resolve reject:reject]; +} + +- (void)query_RemoveChangeListener:(NSString *)changeListenerToken + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + [_impl query_RemoveChangeListenerWithChangeListenerToken:changeListenerToken + resolve:resolve reject:reject]; +} + +@end + +// ============================================================ +// MARK: - RCTCblLogging +// ============================================================ + +@implementation RCTCblLogging { + CblLoggingModule *_impl; +} + +- (id)init { + if (self = [super init]) { + __weak typeof(self) weakSelf = self; + _impl = [[CblLoggingModule alloc] initWithSendEvent:^(NSString *name, id body) { + [weakSelf sendEventWithName:name body:body]; + }]; + } + return self; +} + ++ (NSString *)moduleName { return @"CblLogging"; } + +- (NSArray *)supportedEvents { return @[@"customLogMessage"]; } + +- (std::shared_ptr)getTurboModule: + (const facebook::react::ObjCTurboModule::InitParams &)params { + return std::make_shared(params); +} + +- (void)database_SetLogLevel:(NSString *)domain logLevel:(double)logLevel + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + [_impl database_SetLogLevelWithDomain:domain logLevel:logLevel + resolve:resolve reject:reject]; +} + +- (void)database_SetFileLoggingConfig:(NSString *)name directory:(NSString *)directory + logLevel:(double)logLevel maxSize:(double)maxSize + maxRotateCount:(double)maxRotateCount + shouldUsePlainText:(BOOL)shouldUsePlainText + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + [_impl database_SetFileLoggingConfigWithName:name directory:directory logLevel:logLevel + maxSize:maxSize maxRotateCount:maxRotateCount + shouldUsePlainText:shouldUsePlainText + resolve:resolve reject:reject]; +} + +- (void)logsinks_SetConsole:(double)level domains:(NSArray *)domains + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + [_impl logsinks_SetConsoleWithLevel:level domains:domains resolve:resolve reject:reject]; +} + +- (void)logsinks_SetFile:(double)level config:(id)config + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + [_impl logsinks_SetFileWithLevel:level config:config resolve:resolve reject:reject]; +} + +- (void)logsinks_SetCustom:(double)level domains:(NSArray *)domains + token:(NSString *)token resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + [_impl logsinks_SetCustomWithLevel:level domains:domains token:token + resolve:resolve reject:reject]; +} + +@end + +// ============================================================ +// MARK: - RCTCblReplicator +// ============================================================ + +@implementation RCTCblReplicator { + CblReplicatorModule *_impl; +} + +- (id)init { + if (self = [super init]) { + __weak typeof(self) weakSelf = self; + _impl = [[CblReplicatorModule alloc] initWithSendEvent:^(NSString *name, id body) { + [weakSelf sendEventWithName:name body:body]; + }]; + } + return self; +} + ++ (NSString *)moduleName { return @"CblReplicator"; } + +- (NSArray *)supportedEvents { + return @[@"replicatorStatusChange", @"replicatorDocumentChange"]; +} + +- (std::shared_ptr)getTurboModule: + (const facebook::react::ObjCTurboModule::InitParams &)params { + return std::make_shared(params); +} + +- (void)replicator_Create:(id)config resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + [_impl replicator_CreateWithConfig:config resolve:resolve reject:reject]; +} + +- (void)replicator_Start:(NSString *)replicatorId resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + [_impl replicator_StartWithReplicatorId:replicatorId resolve:resolve reject:reject]; +} + +- (void)replicator_Stop:(NSString *)replicatorId resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + [_impl replicator_StopWithReplicatorId:replicatorId resolve:resolve reject:reject]; +} + +- (void)replicator_Cleanup:(NSString *)replicatorId resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + [_impl replicator_CleanupWithReplicatorId:replicatorId resolve:resolve reject:reject]; +} + +- (void)replicator_GetStatus:(NSString *)replicatorId resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + [_impl replicator_GetStatusWithReplicatorId:replicatorId resolve:resolve reject:reject]; +} + +- (void)replicator_ResetCheckpoint:(NSString *)replicatorId + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + [_impl replicator_ResetCheckpointWithReplicatorId:replicatorId resolve:resolve reject:reject]; +} + +- (void)replicator_GetPendingDocumentIds:(NSString *)replicatorId name:(NSString *)name + scopeName:(NSString *)scopeName + collectionName:(NSString *)collectionName + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + [_impl replicator_GetPendingDocumentIdsWithReplicatorId:replicatorId name:name + scopeName:scopeName + collectionName:collectionName + resolve:resolve reject:reject]; +} + +- (void)replicator_IsDocumentPending:(NSString *)documentId + replicatorId:(NSString *)replicatorId name:(NSString *)name + scopeName:(NSString *)scopeName + collectionName:(NSString *)collectionName + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + [_impl replicator_IsDocumentPendingWithDocumentId:documentId replicatorId:replicatorId + name:name scopeName:scopeName + collectionName:collectionName + resolve:resolve reject:reject]; +} + +- (void)replicator_AddChangeListener:(NSString *)changeListenerToken + replicatorId:(NSString *)replicatorId + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + [_impl replicator_AddChangeListenerWithChangeListenerToken:changeListenerToken + replicatorId:replicatorId + resolve:resolve reject:reject]; +} + +- (void)replicator_AddDocumentChangeListener:(NSString *)changeListenerToken + replicatorId:(NSString *)replicatorId + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + [_impl replicator_AddDocumentChangeListenerWithChangeListenerToken:changeListenerToken + replicatorId:replicatorId + resolve:resolve reject:reject]; +} + +- (void)replicator_RemoveChangeListener:(NSString *)changeListenerToken + replicatorId:(NSString *)replicatorId + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + [_impl replicator_RemoveChangeListenerWithChangeListenerToken:changeListenerToken + replicatorId:replicatorId + resolve:resolve reject:reject]; +} + +@end diff --git a/ios/legacy_CblReactnative-Bridging-Header.h b/ios/legacy_CblReactnative-Bridging-Header.h new file mode 100644 index 0000000..6486587 --- /dev/null +++ b/ios/legacy_CblReactnative-Bridging-Header.h @@ -0,0 +1,28 @@ +/* + * ============================================================ + * LEGACY FILE — DO NOT USE + * ============================================================ + * This file was the original Objective-C bridging header for the + * legacy React Native bridge (RCT_EXTERN_MODULE / RCTEventEmitter). + * + * It has been superseded by the new Turbo Module adapter pattern + * implemented in Phase 3 of the turbo migration. + * + * Original filename: CblReactnative-Bridging-Header.h + * Renamed to: legacy_CblReactnative-Bridging-Header.h + * + * All content below is commented out for historical reference. + * ============================================================ + */ + +/* +#import +#import +#import +*/ + +/* + * ============================================================ + * END OF LEGACY FILE + * ============================================================ + */ diff --git a/ios/CblReactnative.mm b/ios/legacy_CblReactnative.mm similarity index 95% rename from ios/CblReactnative.mm rename to ios/legacy_CblReactnative.mm index 86b6c9c..dc38ed5 100644 --- a/ios/CblReactnative.mm +++ b/ios/legacy_CblReactnative.mm @@ -1,3 +1,20 @@ +/** + * LEGACY FILE — DO NOT USE + * + * This RCT_EXTERN_MODULE bridge file has been replaced by 8 domain-specific + * Obj-C++ adapter files as part of Phase 3 of the Turbo Module migration. + * + * Replacement files: + * ios/RCTCblDatabase.mm ios/RCTCblCollection.mm + * ios/RCTCblDocument.mm ios/RCTCblQuery.mm + * ios/RCTCblReplicator.mm ios/RCTCblScope.mm + * ios/RCTCblLogging.mm ios/RCTCblEngine.mm + * + * Original file: ios/CblReactnative.mm + * Renamed as part of: Turbo Module Migration — Phase 3 + */ + +#if 0 // ---- LEGACY CONTENT START (inactive) ---- #import #import @@ -386,3 +403,4 @@ + (BOOL)requiresMainQueueSetup } @end +#endif // ---- LEGACY CONTENT END ---- diff --git a/ios/CblReactnative.swift b/ios/legacy_CblReactnative.swift similarity index 99% rename from ios/CblReactnative.swift rename to ios/legacy_CblReactnative.swift index f511c2a..fb57991 100644 --- a/ios/CblReactnative.swift +++ b/ios/legacy_CblReactnative.swift @@ -1,3 +1,23 @@ +/** + * LEGACY FILE — DO NOT USE + * + * This monolithic RCTEventEmitter class has been replaced by 8 domain-specific + * Turbo Module Swift implementation classes as part of Phase 3 of the Turbo + * Module migration. + * + * Replacement files: + * ios/CblDatabaseModule.swift ios/CblCollectionModule.swift + * ios/CblDocumentModule.swift ios/CblQueryModule.swift + * ios/CblReplicatorModule.swift ios/CblScopeModule.swift + * ios/CblLoggingModule.swift ios/CblEngineModule.swift + * + * Shared state extracted to: ios/ListenerTokenStore.swift + * + * Original file: ios/CblReactnative.swift + * Renamed as part of: Turbo Module Migration — Phase 3 + */ + +/* import Foundation import CouchbaseLiteSwift import os @@ -1912,3 +1932,4 @@ extension Notification.Name { static let replicatorStatusChange = Notification.Name("replicatorStatusChange") static let replicatorDocumentChange = Notification.Name("replicatorDocumentChange") } +*/ // END LEGACY FILE diff --git a/package.json b/package.json index 383ce0e..2dcf3df 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,6 @@ "lib", "android", "ios", - "cpp", "*.podspec", "!ios/build", "!android/build", @@ -194,8 +193,28 @@ ] }, "create-react-native-library": { - "type": "module-legacy", + "type": "module-new", "languages": "kotlin-swift", "version": "0.38.1" + }, + "codegenConfig": { + "name": "CblReactnativeSpecs", + "type": "modules", + "jsSrcsDir": "src", + "android": { + "javaPackageName": "com.cblreactnative" + }, + "ios": { + "modulesProvider": { + "CblDatabase": "RCTCblDatabase", + "CblCollection": "RCTCblCollection", + "CblDocument": "RCTCblDocument", + "CblQuery": "RCTCblQuery", + "CblReplicator": "RCTCblReplicator", + "CblScope": "RCTCblScope", + "CblLogging": "RCTCblLogging", + "CblEngine": "RCTCblEngine" + } + } } } diff --git a/src/CblReactNativeEngine.tsx b/src/CblReactNativeEngine.tsx index 1afe9c9..c70c73a 100644 --- a/src/CblReactNativeEngine.tsx +++ b/src/CblReactNativeEngine.tsx @@ -1,7 +1,6 @@ import { EmitterSubscription, NativeEventEmitter, - NativeModules, Platform, } from 'react-native'; import { @@ -68,6 +67,15 @@ import type { import uuid from 'react-native-uuid'; +import NativeCblDatabase from './NativeCblDatabase'; +import NativeCblCollection from './NativeCblCollection'; +import NativeCblDocument from './NativeCblDocument'; +import NativeCblQuery from './NativeCblQuery'; +import NativeCblReplicator from './NativeCblReplicator'; +import NativeCblScope from './NativeCblScope'; +import NativeCblLogging from './NativeCblLogging'; +import NativeCblEngine from './NativeCblEngine'; + export class CblReactNativeEngine implements ICoreEngine { _defaultCollectionName = '_default'; _defaultScopeName = '_default'; @@ -75,14 +83,12 @@ export class CblReactNativeEngine implements ICoreEngine { platform = Platform.OS; //event name mapping for the native side of the module - _eventReplicatorStatusChange = 'replicatorStatusChange'; _eventReplicatorDocumentChange = 'replicatorDocumentChange'; _eventCollectionChange = 'collectionChange'; _eventCollectionDocumentChange = 'collectionDocumentChange'; _eventQueryChange = 'queryChange'; - //used to listen to replicator change events for both status and document changes private _replicatorChangeListeners: Map = new Map(); private _emitterSubscriptions: Map = new Map(); @@ -104,23 +110,6 @@ export class CblReactNativeEngine implements ICoreEngine { (level: LogLevel, domain: LogDomain, message: string) => void > = new Map(); - private static readonly LINKING_ERROR = - `The package 'cbl-reactnative' doesn't seem to be linked. Make sure: \n\n` + - Platform.select({ ios: "- You have run 'pod install'\n", default: '' }) + - '- You rebuilt the app after installing the package\n' + - '- You are not using Expo Go\n'; - - CblReactNative = NativeModules.CblReactnative - ? NativeModules.CblReactnative - : new Proxy( - {}, - { - get() { - throw new Error(CblReactNativeEngine.LINKING_ERROR); - }, - } - ); - _eventEmitter: NativeEventEmitter; constructor(customEventEmitter?: NativeEventEmitter) { @@ -130,7 +119,8 @@ export class CblReactNativeEngine implements ICoreEngine { this.debugLog('Using provided custom event emitter'); this._eventEmitter = customEventEmitter; } else { - this._eventEmitter = new NativeEventEmitter(this.CblReactNative); + // New arch: all events flow through the global JSI event bus — no module arg needed + this._eventEmitter = new NativeEventEmitter(); } // Always add the customLogMessage listener regardless of emitter source @@ -155,14 +145,12 @@ export class CblReactNativeEngine implements ICoreEngine { ); } - //private logging function private debugLog(message: string) { if (this.debugConsole) { console.log(message); } } - //startListeningEvents - used to listen to events from the native side of the module. Implements Native change listeners for Couchbase Lite // eslint-disable-next-line @typescript-eslint/no-explicit-any startListeningEvents = (event: string, callback: any) => { console.log(`::DEBUG:: Registering listener for event: ${event}`); @@ -178,195 +166,102 @@ export class CblReactNativeEngine implements ICoreEngine { ); }; - collection_AddChangeListener( - args: CollectionChangeListenerArgs, - lcb: ListenerCallback - ): Promise { - return new Promise((resolve, reject) => { - const token = args.changeListenerToken; - - if (this._collectionChangeListeners.has(token)) { - reject(new Error('Change listener token already exists')); - return; - } - - const subscription = this.startListeningEvents( - this._eventCollectionChange, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (results: any) => { - if (results.token === token) { - this.debugLog( - `::DEBUG:: Received collection change event for token: ${token}` - ); - lcb(results); - } - } - ); - - this._emitterSubscriptions.set(token, subscription); - this._collectionChangeListeners.set(token, lcb); - - this.CblReactNative.collection_AddChangeListener( - token, - args.collectionName, - args.name, - args.scopeName - ).then( - () => resolve(), - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (error: any) => { - this._emitterSubscriptions.delete(token); - this._collectionChangeListeners.delete(token); - subscription.remove(); - reject(error); - } - ); - }); - } - - collection_AddDocumentChangeListener( - args: DocumentChangeListenerArgs, - lcb: ListenerCallback - ): Promise { - return new Promise((resolve, reject) => { - const token = args.changeListenerToken; - - if (this._collectionDocumentChangeListeners.has(token)) { - reject(new Error('Document change listener token already exists')); - return; - } - - const subscription = this.startListeningEvents( - this._eventCollectionDocumentChange, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (results: any) => { - if (results.token === token) { - this.debugLog( - `::DEBUG:: Received document change event for token: ${token}` - ); - lcb(results); - } - } - ); - - this._emitterSubscriptions.set(token, subscription); - this._collectionDocumentChangeListeners.set(token, lcb); - - this.CblReactNative.collection_AddDocumentChangeListener( - token, - args.documentId, - args.collectionName, - args.name, - args.scopeName - ).then( - () => resolve(), - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (error: any) => { - this._emitterSubscriptions.delete(token); - this._collectionDocumentChangeListeners.delete(token); - subscription.remove(); - reject(error); - } - ); - }); - } + // ─── NativeCblDatabase ──────────────────────────────────────────────────── - collection_CreateCollection(args: CollectionArgs): Promise { + database_Open( + args: DatabaseOpenArgs + ): Promise<{ databaseUniqueName: string }> { + this.debugLog( + `::DEBUG:: database_Open: ${args.name} ${args.config.directory} ${args.config.encryptionKey}` + ); return new Promise((resolve, reject) => { - this.CblReactNative.collection_CreateCollection( - args.collectionName, + NativeCblDatabase.database_Open( args.name, - args.scopeName + args.config.directory, + args.config.encryptionKey ).then( - (result: Collection) => { - resolve(result); + (databaseUniqueName) => { + this.debugLog(`::DEBUG:: database_Open completed`); + resolve( + databaseUniqueName as unknown as { databaseUniqueName: string } + ); }, // eslint-disable-next-line @typescript-eslint/no-explicit-any (error: any) => { + this.debugLog(`::DEBUG:: database_Open Error: ${error}`); reject(error); } ); }); } - collection_CreateIndex(args: CollectionCreateIndexArgs): Promise { + database_Close(args: DatabaseArgs): Promise { + this.debugLog(`::DEBUG:: database_Close: ${args.name}`); return new Promise((resolve, reject) => { - this.CblReactNative.collection_CreateIndex( - args.indexName, - args.index, - args.collectionName, - args.scopeName, - args.name - ).then( + NativeCblDatabase.database_Close(args.name).then( () => { + this.debugLog(`::DEBUG:: database_Close completed`); resolve(); }, // eslint-disable-next-line @typescript-eslint/no-explicit-any (error: any) => { + this.debugLog(`::DEBUG:: database_Close Error: ${error}`); reject(error); } ); }); } - collection_DeleteCollection(args: CollectionArgs): Promise { + database_Delete(args: DatabaseArgs): Promise { + if (this.debugConsole) { + console.log(`::DEBUG:: database_Delete: ${args.name}`); + } return new Promise((resolve, reject) => { - this.CblReactNative.collection_DeleteCollection( - args.collectionName, - args.name, - args.scopeName - ).then( + NativeCblDatabase.database_Delete(args.name).then( () => { + this.debugLog(`::DEBUG:: database_Delete completed`); resolve(); }, // eslint-disable-next-line @typescript-eslint/no-explicit-any (error: any) => { + console.log(`::DEBUG:: database_Delete Error: ${error}`); reject(error); } ); }); } - collection_DeleteDocument(args: CollectionDeleteDocumentArgs): Promise { - const concurrencyControl = - args.concurrencyControl !== null - ? (args.concurrencyControl as number) - : -9999; + database_DeleteWithPath(args: DatabaseExistsArgs): Promise { this.debugLog( - `::DEBUG:: collection_DeleteDocument: ${args.docId} ${args.name} ${args.scopeName} ${args.collectionName} ${concurrencyControl}` + `::DEBUG:: database_DeleteWithPath: ${args.directory} ${args.databaseName}` ); return new Promise((resolve, reject) => { - this.CblReactNative.collection_DeleteDocument( - args.docId, - args.name, - args.scopeName, - args.collectionName, - concurrencyControl + NativeCblDatabase.database_DeleteWithPath( + args.directory, + args.databaseName ).then( () => { - this.debugLog(`::DEBUG:: collection_DeleteDocument completed`); + this.debugLog(`::DEBUG:: database_DeleteWithPath completed`); resolve(); }, // eslint-disable-next-line @typescript-eslint/no-explicit-any (error: any) => { - this.debugLog(`::DEBUG:: collection_DeleteDocument Error: ${error}`); + this.debugLog(`::DEBUG:: database_DeleteWithPath Error: ${error}`); reject(error); } ); }); } - collection_DeleteIndex(args: CollectionDeleteIndexArgs): Promise { + database_Copy(args: DatabaseCopyArgs): Promise { return new Promise((resolve, reject) => { - this.CblReactNative.collection_DeleteIndex( - args.indexName, - args.collectionName, - args.scopeName, - args.name + NativeCblDatabase.database_Copy( + args.path, + args.newName, + args.config.directory, + args.config.encryptionKey ).then( - () => { - resolve(); - }, + () => resolve(), // eslint-disable-next-line @typescript-eslint/no-explicit-any (error: any) => { reject(error); @@ -375,20 +270,10 @@ export class CblReactNativeEngine implements ICoreEngine { }); } - collection_GetBlobContent( - args: CollectionDocumentGetBlobContentArgs - ): Promise<{ data: ArrayBuffer }> { + database_Exists(args: DatabaseExistsArgs): Promise<{ exists: boolean }> { return new Promise((resolve, reject) => { - this.CblReactNative.collection_GetBlobContent( - args.key, - args.documentId, - args.name, - args.scopeName, - args.collectionName - ).then( - (resultsData: { data: Iterable }) => { - resolve({ data: new Uint8Array(resultsData.data).buffer }); - }, + NativeCblDatabase.database_Exists(args.databaseName, args.directory).then( + (result: boolean) => resolve({ exists: result }), // eslint-disable-next-line @typescript-eslint/no-explicit-any (error: any) => { reject(error); @@ -397,16 +282,10 @@ export class CblReactNativeEngine implements ICoreEngine { }); } - collection_GetCollection(args: CollectionArgs): Promise { + database_GetPath(args: DatabaseArgs): Promise<{ path: string }> { return new Promise((resolve, reject) => { - this.CblReactNative.collection_GetCollection( - args.collectionName, - args.name, - args.scopeName - ).then( - (result: Collection) => { - resolve(result); - }, + NativeCblDatabase.database_GetPath(args.name).then( + (result: string) => resolve({ path: result }), // eslint-disable-next-line @typescript-eslint/no-explicit-any (error: any) => { reject(error); @@ -415,15 +294,13 @@ export class CblReactNativeEngine implements ICoreEngine { }); } - collection_GetCollections(args: ScopeArgs): Promise { + database_PerformMaintenance( + args: DatabasePerformMaintenanceArgs + ): Promise { + const numValue = args.maintenanceType.valueOf(); return new Promise((resolve, reject) => { - this.CblReactNative.collection_GetCollections( - args.name, - args.scopeName - ).then( - (result: CollectionsResult) => { - resolve(result); - }, + NativeCblDatabase.database_PerformMaintenance(numValue, args.name).then( + () => resolve(), // eslint-disable-next-line @typescript-eslint/no-explicit-any (error: any) => { reject(error); @@ -432,111 +309,160 @@ export class CblReactNativeEngine implements ICoreEngine { }); } - collection_GetCount(args: CollectionArgs): Promise<{ count: number }> { - this.debugLog( - `::DEBUG:: collection_GetCount: ${args.collectionName} ${args.name} ${args.scopeName}` - ); + database_ChangeEncryptionKey(args: DatabaseEncryptionKeyArgs): Promise { return new Promise((resolve, reject) => { - this.CblReactNative.collection_GetCount( - args.collectionName, - args.name, - args.scopeName + NativeCblDatabase.database_ChangeEncryptionKey( + args.newKey, + args.name ).then( - (result: { count: number }) => { - this.debugLog( - `::DEBUG:: collection_GetCount completed with result: ${JSON.stringify(result)}` - ); - resolve(result); - }, + () => resolve(), // eslint-disable-next-line @typescript-eslint/no-explicit-any (error: any) => { - this.debugLog(`::DEBUG:: collection_GetCount Error: ${error}`); reject(error); } ); }); } - async collection_GetFullName( - args: CollectionArgs - ): Promise<{ fullName: string }> { + /** + * @deprecated This function will be removed in future versions. Use collection_CreateIndex instead. + */ + database_CreateIndex(args: DatabaseCreateIndexArgs): Promise { + const colArgs: CollectionCreateIndexArgs = { + name: args.name, + collectionName: this._defaultCollectionName, + scopeName: this._defaultScopeName, + indexName: args.indexName, + index: args.index, + }; + return this.collection_CreateIndex(colArgs); + } + + /** + * @deprecated This will be removed in future versions. Use collection_DeleteDocument instead. + */ + database_DeleteDocument(args: DatabaseDeleteDocumentArgs): Promise { + const colArgs: CollectionDeleteDocumentArgs = { + name: args.name, + collectionName: this._defaultCollectionName, + scopeName: this._defaultScopeName, + docId: args.docId, + concurrencyControl: args.concurrencyControl, + }; this.debugLog( - `::DEBUG:: collection_GetFullName: ${args.collectionName} ${args.name} ${args.scopeName}` + `::DEBUG:: database_DeleteDocument: ${args.docId} ${args.name} ${this._defaultScopeName} ${this._defaultCollectionName} ${args.concurrencyControl}` ); - - try { - const result = await this.CblReactNative.collection_GetFullName( - args.collectionName, - args.name, - args.scopeName - ); - - this.debugLog( - `::DEBUG:: collection_GetFullName completed with result: ${JSON.stringify(result)}` - ); - - return result; - } catch (error: unknown) { - this.debugLog(`::DEBUG:: collection_GetFullName Error: ${error}`); - throw error; // Re-throw to maintain error propagation - } + return this.collection_DeleteDocument(colArgs); } - collection_GetDefault(args: DatabaseArgs): Promise { - return new Promise((resolve, reject) => { - this.CblReactNative.collection_GetDefault(args.name).then( - (result: Collection) => { - resolve(result); - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (error: any) => { - reject(error); - } - ); - }); + /** + * @deprecated This function will be removed in future versions. Use collection_DeleteIndex instead. + */ + database_DeleteIndex(args: DatabaseDeleteIndexArgs): Promise { + const colArgs: CollectionDeleteIndexArgs = { + name: args.name, + collectionName: this._defaultCollectionName, + scopeName: this._defaultScopeName, + indexName: args.indexName, + }; + return this.collection_DeleteIndex(colArgs); } - collection_GetDocument( - args: CollectionGetDocumentArgs - ): Promise { - this.debugLog( - `::DEBUG:: collection_GetDocument: ${args.docId} ${args.name} ${args.scopeName} ${args.collectionName}` - ); + /** + * @deprecated This will be removed in future versions. Use collection_GetCount instead. + */ + database_GetCount(args: DatabaseArgs): Promise<{ count: number }> { + const colArgs: CollectionArgs = { + name: args.name, + collectionName: this._defaultCollectionName, + scopeName: this._defaultScopeName, + }; + return this.collection_GetCount(colArgs); + } + + /** + * @deprecated This will be removed in future versions. Use collection_GetDocument instead. + */ + database_GetDocument(args: DatabaseGetDocumentArgs): Promise { + const colArgs: CollectionGetDocumentArgs = { + name: args.name, + collectionName: this._defaultCollectionName, + scopeName: this._defaultScopeName, + docId: args.docId, + }; + return this.collection_GetDocument(colArgs); + } + + /** + * @deprecated This function will be removed in future versions. Use collection_GetIndexes instead. + */ + database_GetIndexes(args: DatabaseArgs): Promise<{ indexes: string[] }> { + const colArgs: CollectionArgs = { + name: args.name, + collectionName: this._defaultCollectionName, + scopeName: this._defaultScopeName, + }; + return this.collection_GetIndexes(colArgs); + } + + /** + * @deprecated This will be removed in future versions. Use collection_PurgeDocument instead. + */ + database_PurgeDocument(args: DatabasePurgeDocumentArgs): Promise { + const colArgs: CollectionPurgeDocumentArgs = { + name: args.name, + collectionName: this._defaultCollectionName, + scopeName: this._defaultScopeName, + docId: args.docId, + }; + return this.collection_PurgeDocument(colArgs); + } + + /** + * @deprecated This function will be removed in future versions. Use collection_Save instead. + */ + database_Save(args: DatabaseSaveArgs): Promise<{ _id: string }> { + const colArgs: CollectionSaveStringArgs = { + name: args.name, + collectionName: this._defaultCollectionName, + scopeName: this._defaultScopeName, + id: args.id, + document: JSON.stringify(args.document), + blobs: JSON.stringify(args.blobs), + concurrencyControl: args.concurrencyControl, + }; + return this.collection_Save(colArgs); + } + + // ─── NativeCblCollection ────────────────────────────────────────────────── + collection_CreateCollection(args: CollectionArgs): Promise { return new Promise((resolve, reject) => { - this.CblReactNative.collection_GetDocument( - args.docId, + NativeCblCollection.collection_CreateCollection( + args.collectionName, args.name, - args.scopeName, - args.collectionName + args.scopeName ).then( - (dr: DocumentResult) => { - this.debugLog( - `::DEBUG:: collection_GetDocument completed with result: ${JSON.stringify(dr)}` - ); - resolve(dr); + (result) => { + resolve(result as unknown as Collection); }, // eslint-disable-next-line @typescript-eslint/no-explicit-any (error: any) => { - this.debugLog(`::DEBUG:: collection_GetDocument Error: ${error}`); reject(error); } ); }); } - collection_GetDocumentExpiration( - args: CollectionGetDocumentArgs - ): Promise { + collection_DeleteCollection(args: CollectionArgs): Promise { return new Promise((resolve, reject) => { - this.CblReactNative.collection_GetDocumentExpiration( - args.docId, + NativeCblCollection.collection_DeleteCollection( + args.collectionName, args.name, - args.scopeName, - args.collectionName + args.scopeName ).then( - (der: DocumentExpirationResult) => { - resolve(der); + () => { + resolve(); }, // eslint-disable-next-line @typescript-eslint/no-explicit-any (error: any) => { @@ -546,15 +472,15 @@ export class CblReactNativeEngine implements ICoreEngine { }); } - collection_GetIndexes(args: CollectionArgs): Promise<{ indexes: string[] }> { + collection_GetCollection(args: CollectionArgs): Promise { return new Promise((resolve, reject) => { - this.CblReactNative.collection_GetIndexes( + NativeCblCollection.collection_GetCollection( args.collectionName, - args.scopeName, - args.name + args.name, + args.scopeName ).then( - (items: { indexes: string[] }) => { - resolve(items); + (result) => { + resolve(result as unknown as Collection); }, // eslint-disable-next-line @typescript-eslint/no-explicit-any (error: any) => { @@ -564,16 +490,14 @@ export class CblReactNativeEngine implements ICoreEngine { }); } - collection_PurgeDocument(args: CollectionPurgeDocumentArgs): Promise { + collection_GetCollections(args: ScopeArgs): Promise { return new Promise((resolve, reject) => { - this.CblReactNative.collection_PurgeDocument( - args.docId, + NativeCblCollection.collection_GetCollections( args.name, - args.scopeName, - args.collectionName + args.scopeName ).then( - () => { - resolve(); + (result) => { + resolve(result as unknown as CollectionsResult); }, // eslint-disable-next-line @typescript-eslint/no-explicit-any (error: any) => { @@ -583,158 +507,97 @@ export class CblReactNativeEngine implements ICoreEngine { }); } - collection_RemoveChangeListener( - // eslint-disable-next-line - args: CollectionChangeListenerArgs - ): Promise { + collection_GetDefault(args: DatabaseArgs): Promise { return new Promise((resolve, reject) => { - const token = args.changeListenerToken; - - // Remove the subscription - if (this._emitterSubscriptions.has(token)) { - this._emitterSubscriptions.get(token)?.remove(); - this._emitterSubscriptions.delete(token); - } - - // Remove the listener from the collection listeners map - if (this._collectionChangeListeners.has(token)) { - this._collectionChangeListeners.delete(token); - } else { - reject(new Error(`No listener found with token: ${token}`)); - return; - } - - // Remove the listener from the native side - this.CblReactNative.collection_RemoveChangeListener(token).then( - () => { - this.debugLog( - `::DEBUG:: collection_RemoveChangeListener completed for token: ${token}` - ); - resolve(); + NativeCblCollection.collection_GetDefault(args.name).then( + (result) => { + resolve(result as unknown as Collection); }, // eslint-disable-next-line @typescript-eslint/no-explicit-any (error: any) => { - this.debugLog( - `::DEBUG:: collection_RemoveChangeListener Error: ${error}` - ); reject(error); } ); }); } - collection_RemoveDocumentChangeListener( - // eslint-disable-next-line - args: CollectionChangeListenerArgs - ): Promise { + collection_GetCount(args: CollectionArgs): Promise<{ count: number }> { + this.debugLog( + `::DEBUG:: collection_GetCount: ${args.collectionName} ${args.name} ${args.scopeName}` + ); return new Promise((resolve, reject) => { - const token = args.changeListenerToken; - - // Remove the subscription - if (this._emitterSubscriptions.has(token)) { - this._emitterSubscriptions.get(token)?.remove(); - this._emitterSubscriptions.delete(token); - } - - // Remove the listener from the document listeners map - if (this._collectionDocumentChangeListeners.has(token)) { - this._collectionDocumentChangeListeners.delete(token); - } else { - reject(new Error(`No document listener found with token: ${token}`)); - return; - } - - // Remove the listener from the native side - this.CblReactNative.collection_RemoveChangeListener(token).then( - () => { + NativeCblCollection.collection_GetCount( + args.collectionName, + args.name, + args.scopeName + ).then( + (result) => { this.debugLog( - `::DEBUG:: collection_RemoveDocumentChangeListener completed for token: ${token}` + `::DEBUG:: collection_GetCount completed with result: ${JSON.stringify(result)}` ); - resolve(); + resolve(result as unknown as { count: number }); }, // eslint-disable-next-line @typescript-eslint/no-explicit-any (error: any) => { - this.debugLog( - `::DEBUG:: collection_RemoveDocumentChangeListener Error: ${error}` - ); + this.debugLog(`::DEBUG:: collection_GetCount Error: ${error}`); reject(error); } ); }); } - /** - * Generic method to remove any listener by its UUID token. - * Calls the native listenerToken_Remove bridge method. - */ - listenerToken_Remove(args: { changeListenerToken: string }): Promise { - return this.CblReactNative.listenerToken_Remove( - args.changeListenerToken - ).then( - () => { - this.debugLog( - `::DEBUG:: Successfully removed listener with token ${args.changeListenerToken}` - ); - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (error: any) => { - this.debugLog( - `::ERROR:: Failed to remove listener with token ${args.changeListenerToken}: ${error}` - ); - throw error; - } - ); - } - - collection_Save( - args: CollectionSaveStringArgs - ): Promise { - //deal with react native passing nulls - const concurrencyControl = - args.concurrencyControl !== null - ? (args.concurrencyControl as number) - : -9999; + async collection_GetFullName( + args: CollectionArgs + ): Promise<{ fullName: string }> { this.debugLog( - `::DEBUG:: collection_Save: ${args.document} ${args.blobs} ${args.id} ${args.name} ${args.scopeName} ${args.collectionName} ${concurrencyControl}` + `::DEBUG:: collection_GetFullName: ${args.collectionName} ${args.name} ${args.scopeName}` ); - return new Promise((resolve, reject) => { - this.CblReactNative.collection_Save( - args.document, - args.blobs, - args.id, + try { + const result = await NativeCblCollection.collection_GetFullName( + args.collectionName, args.name, - args.scopeName, + args.scopeName + ); + + this.debugLog( + `::DEBUG:: collection_GetFullName completed with result: ${JSON.stringify(result)}` + ); + + return result as unknown as { fullName: string }; + } catch (error: unknown) { + this.debugLog(`::DEBUG:: collection_GetFullName Error: ${error}`); + throw error; + } + } + + collection_CreateIndex(args: CollectionCreateIndexArgs): Promise { + return new Promise((resolve, reject) => { + NativeCblCollection.collection_CreateIndex( + args.indexName, + args.index, args.collectionName, - concurrencyControl + args.scopeName, + args.name ).then( - (resultsData: CollectionDocumentSaveResult) => { - if (this.debugConsole) { - console.log( - `::DEBUG:: collection_Save completed with result: ${JSON.stringify(resultsData)}` - ); - } - resolve(resultsData); + () => { + resolve(); }, // eslint-disable-next-line @typescript-eslint/no-explicit-any (error: any) => { - console.log(`::DEBUG:: collection_Save Error: ${error}`); reject(error); } ); }); } - collection_SetDocumentExpiration( - args: CollectionDocumentExpirationArgs - ): Promise { + collection_DeleteIndex(args: CollectionDeleteIndexArgs): Promise { return new Promise((resolve, reject) => { - this.CblReactNative.collection_SetDocumentExpiration( - args.expiration.toISOString(), - args.docId, - args.name, + NativeCblCollection.collection_DeleteIndex( + args.indexName, + args.collectionName, args.scopeName, - args.collectionName + args.name ).then( () => { resolve(); @@ -747,13 +610,16 @@ export class CblReactNativeEngine implements ICoreEngine { }); } - database_ChangeEncryptionKey(args: DatabaseEncryptionKeyArgs): Promise { + collection_GetIndexes(args: CollectionArgs): Promise<{ indexes: string[] }> { return new Promise((resolve, reject) => { - this.CblReactNative.database_ChangeEncryptionKey( - args.newKey, + NativeCblCollection.collection_GetIndexes( + args.collectionName, + args.scopeName, args.name ).then( - () => resolve(), + (items) => { + resolve(items as unknown as { indexes: string[] }); + }, // eslint-disable-next-line @typescript-eslint/no-explicit-any (error: any) => { reject(error); @@ -762,221 +628,283 @@ export class CblReactNativeEngine implements ICoreEngine { }); } - database_Close(args: DatabaseArgs): Promise { - this.debugLog(`::DEBUG:: database_Close: ${args.name}`); + collection_AddChangeListener( + args: CollectionChangeListenerArgs, + lcb: ListenerCallback + ): Promise { return new Promise((resolve, reject) => { - this.CblReactNative.database_Close(args.name).then( - () => { - this.debugLog(`::DEBUG:: database_Close completed`); - resolve(); - }, + const token = args.changeListenerToken; + + if (this._collectionChangeListeners.has(token)) { + reject(new Error('Change listener token already exists')); + return; + } + + const subscription = this.startListeningEvents( + this._eventCollectionChange, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (results: any) => { + if (results.token === token) { + this.debugLog( + `::DEBUG:: Received collection change event for token: ${token}` + ); + lcb(results); + } + } + ); + + this._emitterSubscriptions.set(token, subscription); + this._collectionChangeListeners.set(token, lcb); + + NativeCblCollection.collection_AddChangeListener( + token, + args.collectionName, + args.name, + args.scopeName + ).then( + () => resolve(), // eslint-disable-next-line @typescript-eslint/no-explicit-any (error: any) => { - this.debugLog(`::DEBUG:: database_Close Error: ${error}`); + this._emitterSubscriptions.delete(token); + this._collectionChangeListeners.delete(token); + subscription.remove(); reject(error); } ); }); } - database_Copy(args: DatabaseCopyArgs): Promise { + collection_AddDocumentChangeListener( + args: DocumentChangeListenerArgs, + lcb: ListenerCallback + ): Promise { return new Promise((resolve, reject) => { - this.CblReactNative.database_Copy( - args.path, - args.newName, - args.config.directory, - args.config.encryptionKey + const token = args.changeListenerToken; + + if (this._collectionDocumentChangeListeners.has(token)) { + reject(new Error('Document change listener token already exists')); + return; + } + + const subscription = this.startListeningEvents( + this._eventCollectionDocumentChange, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (results: any) => { + if (results.token === token) { + this.debugLog( + `::DEBUG:: Received document change event for token: ${token}` + ); + lcb(results); + } + } + ); + + this._emitterSubscriptions.set(token, subscription); + this._collectionDocumentChangeListeners.set(token, lcb); + + NativeCblCollection.collection_AddDocumentChangeListener( + token, + args.documentId, + args.collectionName, + args.name, + args.scopeName ).then( () => resolve(), // eslint-disable-next-line @typescript-eslint/no-explicit-any (error: any) => { + this._emitterSubscriptions.delete(token); + this._collectionDocumentChangeListeners.delete(token); + subscription.remove(); reject(error); } ); }); } - /** - * @deprecated This function will be removed in future versions. Use collection_CreateIndex instead. - */ - database_CreateIndex(args: DatabaseCreateIndexArgs): Promise { - const colArgs: CollectionCreateIndexArgs = { - name: args.name, - collectionName: this._defaultCollectionName, - scopeName: this._defaultScopeName, - indexName: args.indexName, - index: args.index, - }; - return this.collection_CreateIndex(colArgs); - } - - database_Delete(args: DatabaseArgs): Promise { - if (this.debugConsole) { - console.log(`::DEBUG:: database_Delete: ${args.name}`); - } + collection_RemoveChangeListener( + // eslint-disable-next-line + args: CollectionChangeListenerArgs + ): Promise { return new Promise((resolve, reject) => { - this.CblReactNative.database_Delete(args.name).then( + const token = args.changeListenerToken; + + if (this._emitterSubscriptions.has(token)) { + this._emitterSubscriptions.get(token)?.remove(); + this._emitterSubscriptions.delete(token); + } + + if (this._collectionChangeListeners.has(token)) { + this._collectionChangeListeners.delete(token); + } else { + reject(new Error(`No listener found with token: ${token}`)); + return; + } + + NativeCblCollection.collection_RemoveChangeListener(token).then( () => { - this.debugLog(`::DEBUG:: database_Delete completed`); + this.debugLog( + `::DEBUG:: collection_RemoveChangeListener completed for token: ${token}` + ); resolve(); }, // eslint-disable-next-line @typescript-eslint/no-explicit-any (error: any) => { - console.log(`::DEBUG:: database_Delete Error: ${error}`); + this.debugLog( + `::DEBUG:: collection_RemoveChangeListener Error: ${error}` + ); reject(error); } ); }); } - database_DeleteWithPath(args: DatabaseExistsArgs): Promise { - this.debugLog( - `::DEBUG:: database_DeleteWithPath: ${args.directory} ${args.databaseName}` - ); + collection_RemoveDocumentChangeListener( + // eslint-disable-next-line + args: CollectionChangeListenerArgs + ): Promise { return new Promise((resolve, reject) => { - this.CblReactNative.database_DeleteWithPath( - args.directory, - args.databaseName - ).then( + const token = args.changeListenerToken; + + if (this._emitterSubscriptions.has(token)) { + this._emitterSubscriptions.get(token)?.remove(); + this._emitterSubscriptions.delete(token); + } + + if (this._collectionDocumentChangeListeners.has(token)) { + this._collectionDocumentChangeListeners.delete(token); + } else { + reject(new Error(`No document listener found with token: ${token}`)); + return; + } + + NativeCblCollection.collection_RemoveChangeListener(token).then( () => { - this.debugLog(`::DEBUG:: database_DeleteWithPath completed`); + this.debugLog( + `::DEBUG:: collection_RemoveDocumentChangeListener completed for token: ${token}` + ); resolve(); }, // eslint-disable-next-line @typescript-eslint/no-explicit-any (error: any) => { - this.debugLog(`::DEBUG:: database_DeleteWithPath Error: ${error}`); + this.debugLog( + `::DEBUG:: collection_RemoveDocumentChangeListener Error: ${error}` + ); reject(error); } ); }); } - /** - * @deprecated This will be removed in future versions. Use collection_DeleteDocument instead. - */ - database_DeleteDocument(args: DatabaseDeleteDocumentArgs): Promise { - const colArgs: CollectionDeleteDocumentArgs = { - name: args.name, - collectionName: this._defaultCollectionName, - scopeName: this._defaultScopeName, - docId: args.docId, - concurrencyControl: args.concurrencyControl, - }; + // ─── NativeCblDocument ──────────────────────────────────────────────────── + + collection_GetDocument( + args: CollectionGetDocumentArgs + ): Promise { this.debugLog( - `::DEBUG:: database_DeleteDocument: ${args.docId} ${args.name} ${this._defaultScopeName} ${this._defaultCollectionName} ${args.concurrencyControl}` + `::DEBUG:: collection_GetDocument: ${args.docId} ${args.name} ${args.scopeName} ${args.collectionName}` ); - return this.collection_DeleteDocument(colArgs); - } - - /** - * @deprecated This function will be removed in future versions. Use collection_DeleteIndex instead. - */ - database_DeleteIndex(args: DatabaseDeleteIndexArgs): Promise { - const colArgs: CollectionDeleteIndexArgs = { - name: args.name, - collectionName: this._defaultCollectionName, - scopeName: this._defaultScopeName, - indexName: args.indexName, - }; - return this.collection_DeleteIndex(colArgs); - } - database_Exists(args: DatabaseExistsArgs): Promise<{ exists: boolean }> { return new Promise((resolve, reject) => { - this.CblReactNative.database_Exists( - args.databaseName, - args.directory + NativeCblDocument.collection_GetDocument( + args.docId, + args.name, + args.scopeName, + args.collectionName ).then( - (result: boolean) => resolve({ exists: result }), + (dr) => { + this.debugLog( + `::DEBUG:: collection_GetDocument completed with result: ${JSON.stringify(dr)}` + ); + resolve(dr as unknown as DocumentResult); + }, // eslint-disable-next-line @typescript-eslint/no-explicit-any (error: any) => { + this.debugLog(`::DEBUG:: collection_GetDocument Error: ${error}`); reject(error); } ); }); } - /** - * @deprecated This will be removed in future versions. Use collection_GetCount instead. - */ - database_GetCount(args: DatabaseArgs): Promise<{ count: number }> { - const colArgs: CollectionArgs = { - name: args.name, - collectionName: this._defaultCollectionName, - scopeName: this._defaultScopeName, - }; - return this.collection_GetCount(colArgs); - } - - /** - * @deprecated This will be removed in future versions. Use collection_GetDocument instead. - */ - database_GetDocument(args: DatabaseGetDocumentArgs): Promise { - const colArgs: CollectionGetDocumentArgs = { - name: args.name, - collectionName: this._defaultCollectionName, - scopeName: this._defaultScopeName, - docId: args.docId, - }; - return this.collection_GetDocument(colArgs); - } - - /** - * @deprecated This function will be removed in future versions. Use collection_GetIndexes instead. - */ - database_GetIndexes(args: DatabaseArgs): Promise<{ indexes: string[] }> { - const colArgs: CollectionArgs = { - name: args.name, - collectionName: this._defaultCollectionName, - scopeName: this._defaultScopeName, - }; - return this.collection_GetIndexes(colArgs); - } + collection_Save( + args: CollectionSaveStringArgs + ): Promise { + //deal with react native passing nulls + const concurrencyControl = + args.concurrencyControl !== null + ? (args.concurrencyControl as number) + : -9999; + this.debugLog( + `::DEBUG:: collection_Save: ${args.document} ${args.blobs} ${args.id} ${args.name} ${args.scopeName} ${args.collectionName} ${concurrencyControl}` + ); - database_GetPath(args: DatabaseArgs): Promise<{ path: string }> { return new Promise((resolve, reject) => { - this.CblReactNative.database_GetPath(args.name).then( - (result: string) => resolve({ path: result }), + NativeCblDocument.collection_Save( + args.document, + args.blobs, + args.id, + args.name, + args.scopeName, + args.collectionName, + concurrencyControl + ).then( + (resultsData) => { + if (this.debugConsole) { + console.log( + `::DEBUG:: collection_Save completed with result: ${JSON.stringify(resultsData)}` + ); + } + resolve(resultsData as unknown as CollectionDocumentSaveResult); + }, // eslint-disable-next-line @typescript-eslint/no-explicit-any (error: any) => { + console.log(`::DEBUG:: collection_Save Error: ${error}`); reject(error); } ); }); } - database_Open( - args: DatabaseOpenArgs - ): Promise<{ databaseUniqueName: string }> { + collection_DeleteDocument(args: CollectionDeleteDocumentArgs): Promise { + const concurrencyControl = + args.concurrencyControl !== null + ? (args.concurrencyControl as number) + : -9999; this.debugLog( - `::DEBUG:: database_Open: ${args.name} ${args.config.directory} ${args.config.encryptionKey}` + `::DEBUG:: collection_DeleteDocument: ${args.docId} ${args.name} ${args.scopeName} ${args.collectionName} ${concurrencyControl}` ); return new Promise((resolve, reject) => { - this.CblReactNative.database_Open( + NativeCblDocument.collection_DeleteDocument( + args.docId, args.name, - args.config.directory, - args.config.encryptionKey + args.scopeName, + args.collectionName, + concurrencyControl ).then( - (databaseUniqueName) => { - this.debugLog(`::DEBUG:: database_Open completed`); - resolve(databaseUniqueName); + () => { + this.debugLog(`::DEBUG:: collection_DeleteDocument completed`); + resolve(); }, // eslint-disable-next-line @typescript-eslint/no-explicit-any (error: any) => { - this.debugLog(`::DEBUG:: database_Open Error: ${error}`); + this.debugLog(`::DEBUG:: collection_DeleteDocument Error: ${error}`); reject(error); } ); }); } - database_PerformMaintenance( - args: DatabasePerformMaintenanceArgs - ): Promise { - const numValue = args.maintenanceType.valueOf(); + collection_PurgeDocument(args: CollectionPurgeDocumentArgs): Promise { return new Promise((resolve, reject) => { - this.CblReactNative.database_PerformMaintenance(numValue, args.name).then( - () => resolve(), + NativeCblDocument.collection_PurgeDocument( + args.docId, + args.name, + args.scopeName, + args.collectionName + ).then( + () => { + resolve(); + }, // eslint-disable-next-line @typescript-eslint/no-explicit-any (error: any) => { reject(error); @@ -985,48 +913,41 @@ export class CblReactNativeEngine implements ICoreEngine { }); } - /** - * @deprecated This will be removed in future versions. Use collection_PurgeDocument instead. - */ - database_PurgeDocument(args: DatabasePurgeDocumentArgs): Promise { - const colArgs: CollectionPurgeDocumentArgs = { - name: args.name, - collectionName: this._defaultCollectionName, - scopeName: this._defaultScopeName, - docId: args.docId, - }; - return this.collection_PurgeDocument(colArgs); - } - - /** - * @deprecated This function will be removed in future versions. Use collection_Save instead. - */ - database_Save(args: DatabaseSaveArgs): Promise<{ _id: string }> { - const colArgs: CollectionSaveStringArgs = { - name: args.name, - collectionName: this._defaultCollectionName, - scopeName: this._defaultScopeName, - id: args.id, - document: JSON.stringify(args.document), - blobs: JSON.stringify(args.blobs), - concurrencyControl: args.concurrencyControl, - }; - return this.collection_Save(colArgs); + collection_GetDocumentExpiration( + args: CollectionGetDocumentArgs + ): Promise { + return new Promise((resolve, reject) => { + NativeCblDocument.collection_GetDocumentExpiration( + args.docId, + args.name, + args.scopeName, + args.collectionName + ).then( + (der) => { + resolve(der as unknown as DocumentExpirationResult); + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (error: any) => { + reject(error); + } + ); + }); } - database_SetFileLoggingConfig( - args: DatabaseSetFileLoggingConfigArgs + collection_SetDocumentExpiration( + args: CollectionDocumentExpirationArgs ): Promise { return new Promise((resolve, reject) => { - this.CblReactNative.database_SetFileLoggingConfig( + NativeCblDocument.collection_SetDocumentExpiration( + args.expiration.toISOString(), + args.docId, args.name, - args.config.directory, - args.config.level, - args.config.maxSize, - args.config.maxRotateCount, - args.config.usePlaintext + args.scopeName, + args.collectionName ).then( - () => resolve(), + () => { + resolve(); + }, // eslint-disable-next-line @typescript-eslint/no-explicit-any (error: any) => { reject(error); @@ -1035,10 +956,24 @@ export class CblReactNativeEngine implements ICoreEngine { }); } - database_SetLogLevel(args: DatabaseSetLogLevelArgs): Promise { + collection_GetBlobContent( + args: CollectionDocumentGetBlobContentArgs + ): Promise<{ data: ArrayBuffer }> { return new Promise((resolve, reject) => { - this.CblReactNative.database_SetLogLevel(args.domain, args.logLevel).then( - () => resolve(), + NativeCblDocument.collection_GetBlobContent( + args.key, + args.documentId, + args.name, + args.scopeName, + args.collectionName + ).then( + (resultsData) => { + resolve({ + data: new Uint8Array( + (resultsData as unknown as { data: Iterable }).data + ).buffer, + }); + }, // eslint-disable-next-line @typescript-eslint/no-explicit-any (error: any) => { reject(error); @@ -1063,11 +998,13 @@ export class CblReactNativeEngine implements ICoreEngine { return this.collection_GetBlobContent(colArgs); } - file_GetDefaultPath(): Promise<{ path: string }> { + // ─── NativeCblQuery ─────────────────────────────────────────────────────── + + query_Execute(args: QueryExecuteArgs): Promise { return new Promise((resolve, reject) => { - this.CblReactNative.file_GetDefaultPath().then( - (result: string) => { - resolve({ path: result }); + NativeCblQuery.query_Execute(args.query, args.parameters, args.name).then( + (result) => { + resolve(result as unknown as Result); }, // eslint-disable-next-line @typescript-eslint/no-explicit-any (error: any) => { @@ -1077,11 +1014,18 @@ export class CblReactNativeEngine implements ICoreEngine { }); } - // eslint-disable-next-line - file_GetFileNamesInDirectory(args: { - path: string; - }): Promise<{ files: string[] }> { - return Promise.resolve({ files: [] }); + query_Explain(args: QueryExecuteArgs): Promise<{ data: string }> { + return new Promise((resolve, reject) => { + NativeCblQuery.query_Explain(args.query, args.parameters, args.name).then( + (result) => { + resolve(result as unknown as { data: string }); + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (error: any) => { + reject(error); + } + ); + }); } query_AddChangeListener( @@ -1109,36 +1053,138 @@ export class CblReactNativeEngine implements ICoreEngine { } ); - this._emitterSubscriptions.set(token, subscription); - this._queryChangeListeners.set(token, lcb); - - this.CblReactNative.query_AddChangeListener( - token, - args.query, - args.parameters, - args.name - ).then( - () => resolve(), + this._emitterSubscriptions.set(token, subscription); + this._queryChangeListeners.set(token, lcb); + + NativeCblQuery.query_AddChangeListener( + token, + args.query, + args.parameters, + args.name + ).then( + () => resolve(), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (error: any) => { + this._emitterSubscriptions.delete(token); + this._queryChangeListeners.delete(token); + subscription.remove(); + reject(error); + } + ); + }); + } + + query_RemoveChangeListener( + args: QueryRemoveChangeListenerArgs + ): Promise { + return new Promise((resolve, reject) => { + const token = args.changeListenerToken; + + if (this._emitterSubscriptions.has(token)) { + this._emitterSubscriptions.get(token)?.remove(); + this._emitterSubscriptions.delete(token); + } + + if (this._queryChangeListeners.has(token)) { + this._queryChangeListeners.delete(token); + } else { + reject(new Error(`No query listener found with token: ${token}`)); + return; + } + + NativeCblQuery.query_RemoveChangeListener(token).then( + () => { + this.debugLog( + `::DEBUG:: query_RemoveChangeListener completed for token: ${token}` + ); + resolve(); + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (error: any) => { + this.debugLog(`::DEBUG:: query_RemoveChangeListener Error: ${error}`); + reject(error); + } + ); + }); + } + + // ─── NativeCblReplicator ────────────────────────────────────────────────── + + replicator_Create(args: ReplicatorCreateArgs): Promise { + return new Promise((resolve, reject) => { + NativeCblReplicator.replicator_Create(args.config).then( + (results) => { + resolve(results as unknown as ReplicatorArgs); + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (error: any) => { + reject(error); + } + ); + }); + } + + replicator_Start(args: ReplicatorArgs): Promise { + return new Promise((resolve, reject) => { + NativeCblReplicator.replicator_Start(args.replicatorId).then( + () => { + resolve(); + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (error: any) => { + reject(error); + } + ); + }); + } + + replicator_Stop(args: ReplicatorArgs): Promise { + return new Promise((resolve, reject) => { + NativeCblReplicator.replicator_Stop(args.replicatorId).then( + () => { + resolve(); + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (error: any) => { + reject(error); + } + ); + }); + } + + replicator_Cleanup(args: ReplicatorArgs): Promise { + return new Promise((resolve, reject) => { + NativeCblReplicator.replicator_Cleanup(args.replicatorId).then( + () => { + resolve(); + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (error: any) => { + reject(error); + } + ); + }); + } + + replicator_GetStatus(args: ReplicatorArgs): Promise { + return new Promise((resolve, reject) => { + NativeCblReplicator.replicator_GetStatus(args.replicatorId).then( + (results) => { + resolve(results as unknown as ReplicatorStatus); + }, // eslint-disable-next-line @typescript-eslint/no-explicit-any (error: any) => { - this._emitterSubscriptions.delete(token); - this._queryChangeListeners.delete(token); - subscription.remove(); reject(error); } ); }); } - query_Execute(args: QueryExecuteArgs): Promise { + replicator_ResetCheckpoint(args: ReplicatorArgs): Promise { return new Promise((resolve, reject) => { - this.CblReactNative.query_Execute( - args.query, - args.parameters, - args.name - ).then( - (result: Result) => { - resolve(result); + NativeCblReplicator.replicator_ResetCheckpoint(args.replicatorId).then( + () => { + resolve(); }, // eslint-disable-next-line @typescript-eslint/no-explicit-any (error: any) => { @@ -1148,15 +1194,18 @@ export class CblReactNativeEngine implements ICoreEngine { }); } - query_Explain(args: QueryExecuteArgs): Promise<{ data: string }> { + replicator_GetPendingDocumentIds( + args: ReplicatorCollectionArgs + ): Promise<{ pendingDocumentIds: string[] }> { return new Promise((resolve, reject) => { - this.CblReactNative.query_Explain( - args.query, - args.parameters, - args.name + NativeCblReplicator.replicator_GetPendingDocumentIds( + args.replicatorId, + args.name, + args.scopeName, + args.collectionName ).then( - (result: { data: string }) => { - resolve(result); + (results) => { + resolve(results as unknown as { pendingDocumentIds: string[] }); }, // eslint-disable-next-line @typescript-eslint/no-explicit-any (error: any) => { @@ -1166,34 +1215,22 @@ export class CblReactNativeEngine implements ICoreEngine { }); } - query_RemoveChangeListener( - args: QueryRemoveChangeListenerArgs - ): Promise { + replicator_IsDocumentPending( + args: ReplicatorDocumentPendingArgs + ): Promise<{ isPending: boolean }> { return new Promise((resolve, reject) => { - const token = args.changeListenerToken; - - if (this._emitterSubscriptions.has(token)) { - this._emitterSubscriptions.get(token)?.remove(); - this._emitterSubscriptions.delete(token); - } - - if (this._queryChangeListeners.has(token)) { - this._queryChangeListeners.delete(token); - } else { - reject(new Error(`No query listener found with token: ${token}`)); - return; - } - - this.CblReactNative.query_RemoveChangeListener(token).then( - () => { - this.debugLog( - `::DEBUG:: query_RemoveChangeListener completed for token: ${token}` - ); - resolve(); + NativeCblReplicator.replicator_IsDocumentPending( + args.documentId, + args.replicatorId, + args.name, + args.scopeName, + args.collectionName + ).then( + (results) => { + resolve(results as unknown as { isPending: boolean }); }, // eslint-disable-next-line @typescript-eslint/no-explicit-any (error: any) => { - this.debugLog(`::DEBUG:: query_RemoveChangeListener Error: ${error}`); reject(error); } ); @@ -1248,7 +1285,7 @@ export class CblReactNativeEngine implements ICoreEngine { } ); return new Promise((resolve, reject) => { - this.CblReactNative.replicator_AddChangeListener( + NativeCblReplicator.replicator_AddChangeListener( args.changeListenerToken, args.replicatorId ).then( @@ -1332,7 +1369,7 @@ export class CblReactNativeEngine implements ICoreEngine { } return new Promise((resolve, reject) => { - this.CblReactNative.replicator_AddDocumentChangeListener( + NativeCblReplicator.replicator_AddDocumentChangeListener( args.changeListenerToken, args.replicatorId ).then( @@ -1357,91 +1394,6 @@ export class CblReactNativeEngine implements ICoreEngine { }); } - replicator_Cleanup(args: ReplicatorArgs): Promise { - return new Promise((resolve, reject) => { - this.CblReactNative.replicator_Cleanup(args.replicatorId).then( - () => { - resolve(); - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (error: any) => { - reject(error); - } - ); - }); - } - - replicator_Create(args: ReplicatorCreateArgs): Promise { - return new Promise((resolve, reject) => { - this.CblReactNative.replicator_Create(args.config).then( - (results: ReplicatorArgs) => { - resolve(results); - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (error: any) => { - reject(error); - } - ); - }); - } - - replicator_GetPendingDocumentIds( - args: ReplicatorCollectionArgs - ): Promise<{ pendingDocumentIds: string[] }> { - return new Promise((resolve, reject) => { - this.CblReactNative.replicator_GetPendingDocumentIds( - args.replicatorId, - args.name, - args.scopeName, - args.collectionName - ).then( - (results: { pendingDocumentIds: string[] }) => { - resolve(results); - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (error: any) => { - reject(error); - } - ); - }); - } - - replicator_GetStatus(args: ReplicatorArgs): Promise { - return new Promise((resolve, reject) => { - this.CblReactNative.replicator_GetStatus(args.replicatorId).then( - (results: ReplicatorStatus) => { - resolve(results); - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (error: any) => { - reject(error); - } - ); - }); - } - - replicator_IsDocumentPending( - args: ReplicatorDocumentPendingArgs - ): Promise<{ isPending: boolean }> { - return new Promise((resolve, reject) => { - this.CblReactNative.replicator_IsDocumentPending( - args.documentId, - args.replicatorId, - args.name, - args.scopeName, - args.collectionName - ).then( - (results: { isPending: boolean }) => { - resolve(results); - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (error: any) => { - reject(error); - } - ); - }); - } - replicator_RemoveChangeListener( args: ReplicationChangeListenerArgs ): Promise { @@ -1462,7 +1414,7 @@ export class CblReactNativeEngine implements ICoreEngine { this._emitterSubscriptions.delete(args.changeListenerToken); } return new Promise((resolve, reject) => { - this.CblReactNative.replicator_RemoveChangeListener( + NativeCblReplicator.replicator_RemoveChangeListener( args.changeListenerToken, args.replicatorId ).then( @@ -1481,11 +1433,13 @@ export class CblReactNativeEngine implements ICoreEngine { }); } - replicator_ResetCheckpoint(args: ReplicatorArgs): Promise { + // ─── NativeCblScope ─────────────────────────────────────────────────────── + + scope_GetDefault(args: DatabaseArgs): Promise { return new Promise((resolve, reject) => { - this.CblReactNative.replicator_ResetCheckpoint(args.replicatorId).then( - () => { - resolve(); + NativeCblScope.scope_GetDefault(args.name).then( + (result) => { + resolve(result as unknown as Scope); }, // eslint-disable-next-line @typescript-eslint/no-explicit-any (error: any) => { @@ -1495,11 +1449,11 @@ export class CblReactNativeEngine implements ICoreEngine { }); } - replicator_Start(args: ReplicatorArgs): Promise { + scope_GetScope(args: ScopeArgs): Promise { return new Promise((resolve, reject) => { - this.CblReactNative.replicator_Start(args.replicatorId).then( - () => { - resolve(); + NativeCblScope.scope_GetScope(args.scopeName, args.name).then( + (result) => { + resolve(result as unknown as Scope); }, // eslint-disable-next-line @typescript-eslint/no-explicit-any (error: any) => { @@ -1509,11 +1463,11 @@ export class CblReactNativeEngine implements ICoreEngine { }); } - replicator_Stop(args: ReplicatorArgs): Promise { + scope_GetScopes(args: DatabaseArgs): Promise { return new Promise((resolve, reject) => { - this.CblReactNative.replicator_Stop(args.replicatorId).then( - () => { - resolve(); + NativeCblScope.scope_GetScopes(args.name).then( + (result) => { + resolve(result as unknown as ScopesResult); }, // eslint-disable-next-line @typescript-eslint/no-explicit-any (error: any) => { @@ -1523,12 +1477,12 @@ export class CblReactNativeEngine implements ICoreEngine { }); } - scope_GetDefault(args: DatabaseArgs): Promise { + // ─── NativeCblLogging ───────────────────────────────────────────────────── + + database_SetLogLevel(args: DatabaseSetLogLevelArgs): Promise { return new Promise((resolve, reject) => { - this.CblReactNative.scope_GetDefault(args.name).then( - (result: Scope) => { - resolve(result); - }, + NativeCblLogging.database_SetLogLevel(args.domain, args.logLevel).then( + () => resolve(), // eslint-disable-next-line @typescript-eslint/no-explicit-any (error: any) => { reject(error); @@ -1537,12 +1491,19 @@ export class CblReactNativeEngine implements ICoreEngine { }); } - scope_GetScope(args: ScopeArgs): Promise { + database_SetFileLoggingConfig( + args: DatabaseSetFileLoggingConfigArgs + ): Promise { return new Promise((resolve, reject) => { - this.CblReactNative.scope_GetScope(args.scopeName, args.name).then( - (result: Scope) => { - resolve(result); - }, + NativeCblLogging.database_SetFileLoggingConfig( + args.name, + args.config.directory, + args.config.level, + args.config.maxSize, + args.config.maxRotateCount, + args.config.usePlaintext + ).then( + () => resolve(), // eslint-disable-next-line @typescript-eslint/no-explicit-any (error: any) => { reject(error); @@ -1551,11 +1512,41 @@ export class CblReactNativeEngine implements ICoreEngine { }); } - scope_GetScopes(args: DatabaseArgs): Promise { + /** + * Sets or disables the console log sink + * @param args Arguments containing level and domains, or null to disable + */ + async logsinks_SetConsole(args: LogSinksSetConsoleArgs): Promise { + return NativeCblLogging.logsinks_SetConsole(args.level, args.domains); + } + + /** + * Sets or disables the file log sink + * @param args Arguments containing level and config, or null to disable + */ + async logsinks_SetFile(args: LogSinksSetFileArgs): Promise { + return NativeCblLogging.logsinks_SetFile(args.level, args.config); + } + + /** + * Sets or disables the custom log sink + * @param args Arguments containing level, domains, and token, or null to disable + */ + async logsinks_SetCustom(args: LogSinksSetCustomArgs): Promise { + return NativeCblLogging.logsinks_SetCustom( + args.level, + args.domains, + args.token + ); + } + + // ─── NativeCblEngine ────────────────────────────────────────────────────── + + file_GetDefaultPath(): Promise<{ path: string }> { return new Promise((resolve, reject) => { - this.CblReactNative.scope_GetScopes(args.name).then( - (result: ScopesResult) => { - resolve(result); + NativeCblEngine.file_GetDefaultPath().then( + (result: string) => { + resolve({ path: result }); }, // eslint-disable-next-line @typescript-eslint/no-explicit-any (error: any) => { @@ -1565,6 +1556,36 @@ export class CblReactNativeEngine implements ICoreEngine { }); } + // eslint-disable-next-line + file_GetFileNamesInDirectory(args: { + path: string; + }): Promise<{ files: string[] }> { + return Promise.resolve({ files: [] }); + } + + /** + * Generic method to remove any listener by its UUID token. + * Calls the native listenerToken_Remove method via NativeCblEngine. + */ + listenerToken_Remove(args: { changeListenerToken: string }): Promise { + return NativeCblEngine.listenerToken_Remove(args.changeListenerToken).then( + () => { + this.debugLog( + `::DEBUG:: Successfully removed listener with token ${args.changeListenerToken}` + ); + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (error: any) => { + this.debugLog( + `::ERROR:: Failed to remove listener with token ${args.changeListenerToken}: ${error}` + ); + throw error; + } + ); + } + + // ─── URLEndpointListener (not yet implemented) ──────────────────────────── + URLEndpointListener_createListener( // eslint-disable-next-line @typescript-eslint/no-unused-vars args: URLEndpointListenerCreateArgs @@ -1603,36 +1624,4 @@ export class CblReactNativeEngine implements ICoreEngine { getUUID(): string { return uuid.v4().toString(); } - - // ============================================================================= - // LOG SINKS API - // ============================================================================= - - /** - * Sets or disables the console log sink - * @param args Arguments containing level and domains, or null to disable - */ - async logsinks_SetConsole(args: LogSinksSetConsoleArgs): Promise { - return this.CblReactNative.logsinks_SetConsole(args.level, args.domains); - } - - /** - * Sets or disables the file log sink - * @param args Arguments containing level and config, or null to disable - */ - async logsinks_SetFile(args: LogSinksSetFileArgs): Promise { - return this.CblReactNative.logsinks_SetFile(args.level, args.config); - } - - /** - * Sets or disables the custom log sink - * @param args Arguments containing level, domains, and token, or null to disable - */ - async logsinks_SetCustom(args: LogSinksSetCustomArgs): Promise { - return this.CblReactNative.logsinks_SetCustom( - args.level, - args.domains, - args.token - ); - } } diff --git a/src/NativeCblCollection.ts b/src/NativeCblCollection.ts new file mode 100644 index 0000000..dda5676 --- /dev/null +++ b/src/NativeCblCollection.ts @@ -0,0 +1,83 @@ +/* eslint-disable @typescript-eslint/ban-types */ +import type { TurboModule } from 'react-native'; +import { TurboModuleRegistry } from 'react-native'; + +export interface Spec extends TurboModule { + // Event emitter infrastructure (emits: collectionChange, collectionDocumentChange) + addListener(eventType: string): void; + removeListeners(count: number): void; + + collection_CreateCollection( + collectionName: string, + name: string, + scopeName: string + ): Promise; + + collection_DeleteCollection( + collectionName: string, + name: string, + scopeName: string + ): Promise; + + collection_GetCollection( + collectionName: string, + name: string, + scopeName: string + ): Promise; + + collection_GetCollections(name: string, scopeName: string): Promise; + + collection_GetDefault(name: string): Promise; + + collection_GetCount( + collectionName: string, + name: string, + scopeName: string + ): Promise; + + collection_GetFullName( + collectionName: string, + name: string, + scopeName: string + ): Promise; + + collection_CreateIndex( + indexName: string, + index: Object, + collectionName: string, + scopeName: string, + name: string + ): Promise; + + collection_DeleteIndex( + indexName: string, + collectionName: string, + scopeName: string, + name: string + ): Promise; + + collection_GetIndexes( + collectionName: string, + scopeName: string, + name: string + ): Promise; + + collection_AddChangeListener( + changeListenerToken: string, + collectionName: string, + name: string, + scopeName: string + ): Promise; + + collection_AddDocumentChangeListener( + changeListenerToken: string, + documentId: string, + collectionName: string, + name: string, + scopeName: string + ): Promise; + + collection_RemoveChangeListener(changeListenerToken: string): Promise; +} + +export default TurboModuleRegistry.getEnforcing('CblCollection'); diff --git a/src/NativeCblDatabase.ts b/src/NativeCblDatabase.ts new file mode 100644 index 0000000..64cdb70 --- /dev/null +++ b/src/NativeCblDatabase.ts @@ -0,0 +1,37 @@ +/* eslint-disable @typescript-eslint/ban-types */ +import type { TurboModule } from 'react-native'; +import { TurboModuleRegistry } from 'react-native'; + +export interface Spec extends TurboModule { + database_Open( + name: string, + directory: string | null, + encryptionKey: string | null + ): Promise; + + database_Close(name: string): Promise; + + database_Delete(name: string): Promise; + + database_DeleteWithPath(path: string, name: string): Promise; + + database_Copy( + path: string, + newName: string, + directory: string | null, + encryptionKey: string | null + ): Promise; + + database_Exists(name: string, directory: string): Promise; + + database_GetPath(name: string): Promise; + + database_PerformMaintenance( + maintenanceType: number, + databaseName: string + ): Promise; + + database_ChangeEncryptionKey(newKey: string, name: string): Promise; +} + +export default TurboModuleRegistry.getEnforcing('CblDatabase'); diff --git a/src/NativeCblDocument.ts b/src/NativeCblDocument.ts new file mode 100644 index 0000000..6834037 --- /dev/null +++ b/src/NativeCblDocument.ts @@ -0,0 +1,62 @@ +/* eslint-disable @typescript-eslint/ban-types */ +import type { TurboModule } from 'react-native'; +import { TurboModuleRegistry } from 'react-native'; + +export interface Spec extends TurboModule { + collection_GetDocument( + docId: string, + name: string, + scopeName: string, + collectionName: string + ): Promise; + + collection_Save( + document: string, + blobs: string, + docId: string, + name: string, + scopeName: string, + collectionName: string, + concurrencyControlValue: number + ): Promise; + + collection_DeleteDocument( + docId: string, + name: string, + scopeName: string, + collectionName: string, + concurrencyControl: number + ): Promise; + + collection_PurgeDocument( + docId: string, + name: string, + scopeName: string, + collectionName: string + ): Promise; + + collection_GetDocumentExpiration( + docId: string, + name: string, + scopeName: string, + collectionName: string + ): Promise; + + collection_SetDocumentExpiration( + expiration: string, + docId: string, + name: string, + scopeName: string, + collectionName: string + ): Promise; + + collection_GetBlobContent( + key: string, + documentId: string, + name: string, + scopeName: string, + collectionName: string + ): Promise; +} + +export default TurboModuleRegistry.getEnforcing('CblDocument'); diff --git a/src/NativeCblEngine.ts b/src/NativeCblEngine.ts new file mode 100644 index 0000000..99c88d5 --- /dev/null +++ b/src/NativeCblEngine.ts @@ -0,0 +1,10 @@ +import type { TurboModule } from 'react-native'; +import { TurboModuleRegistry } from 'react-native'; + +export interface Spec extends TurboModule { + file_GetDefaultPath(): Promise; + + listenerToken_Remove(changeListenerToken: string): Promise; +} + +export default TurboModuleRegistry.getEnforcing('CblEngine'); diff --git a/src/NativeCblLogging.ts b/src/NativeCblLogging.ts new file mode 100644 index 0000000..380f83a --- /dev/null +++ b/src/NativeCblLogging.ts @@ -0,0 +1,34 @@ +/* eslint-disable @typescript-eslint/ban-types */ +import type { TurboModule } from 'react-native'; +import { TurboModuleRegistry } from 'react-native'; + +export interface Spec extends TurboModule { + // Event emitter infrastructure (emits: customLogMessage) + addListener(eventType: string): void; + removeListeners(count: number): void; + + // Legacy logging API + database_SetLogLevel(domain: string, logLevel: number): Promise; + + database_SetFileLoggingConfig( + name: string, + directory: string, + logLevel: number, + maxSize: number, + maxRotateCount: number, + shouldUsePlainText: boolean + ): Promise; + + // New LogSinks API + logsinks_SetConsole(level: number, domains: string[]): Promise; + + logsinks_SetFile(level: number, config: Object): Promise; + + logsinks_SetCustom( + level: number, + domains: string[], + token: string + ): Promise; +} + +export default TurboModuleRegistry.getEnforcing('CblLogging'); diff --git a/src/NativeCblQuery.ts b/src/NativeCblQuery.ts new file mode 100644 index 0000000..2627689 --- /dev/null +++ b/src/NativeCblQuery.ts @@ -0,0 +1,32 @@ +/* eslint-disable @typescript-eslint/ban-types */ +import type { TurboModule } from 'react-native'; +import { TurboModuleRegistry } from 'react-native'; + +export interface Spec extends TurboModule { + // Event emitter infrastructure (emits: queryChange) + addListener(eventType: string): void; + removeListeners(count: number): void; + + query_Execute( + query: string, + parameters: Object, + name: string + ): Promise; + + query_Explain( + query: string, + parameters: Object, + name: string + ): Promise; + + query_AddChangeListener( + changeListenerToken: string, + query: string, + parameters: Object, + name: string + ): Promise; + + query_RemoveChangeListener(changeListenerToken: string): Promise; +} + +export default TurboModuleRegistry.getEnforcing('CblQuery'); diff --git a/src/NativeCblReplicator.ts b/src/NativeCblReplicator.ts new file mode 100644 index 0000000..d73df96 --- /dev/null +++ b/src/NativeCblReplicator.ts @@ -0,0 +1,53 @@ +/* eslint-disable @typescript-eslint/ban-types */ +import type { TurboModule } from 'react-native'; +import { TurboModuleRegistry } from 'react-native'; + +export interface Spec extends TurboModule { + // Event emitter infrastructure (emits: replicatorStatusChange, replicatorDocumentChange) + addListener(eventType: string): void; + removeListeners(count: number): void; + + replicator_Create(config: Object): Promise; + + replicator_Start(replicatorId: string): Promise; + + replicator_Stop(replicatorId: string): Promise; + + replicator_Cleanup(replicatorId: string): Promise; + + replicator_GetStatus(replicatorId: string): Promise; + + replicator_ResetCheckpoint(replicatorId: string): Promise; + + replicator_GetPendingDocumentIds( + replicatorId: string, + name: string, + scopeName: string, + collectionName: string + ): Promise; + + replicator_IsDocumentPending( + documentId: string, + replicatorId: string, + name: string, + scopeName: string, + collectionName: string + ): Promise; + + replicator_AddChangeListener( + changeListenerToken: string, + replicatorId: string + ): Promise; + + replicator_AddDocumentChangeListener( + changeListenerToken: string, + replicatorId: string + ): Promise; + + replicator_RemoveChangeListener( + changeListenerToken: string, + replicatorId: string + ): Promise; +} + +export default TurboModuleRegistry.getEnforcing('CblReplicator'); diff --git a/src/NativeCblScope.ts b/src/NativeCblScope.ts new file mode 100644 index 0000000..8f9cdd4 --- /dev/null +++ b/src/NativeCblScope.ts @@ -0,0 +1,13 @@ +/* eslint-disable @typescript-eslint/ban-types */ +import type { TurboModule } from 'react-native'; +import { TurboModuleRegistry } from 'react-native'; + +export interface Spec extends TurboModule { + scope_GetDefault(name: string): Promise; + + scope_GetScope(scopeName: string, name: string): Promise; + + scope_GetScopes(name: string): Promise; +} + +export default TurboModuleRegistry.getEnforcing('CblScope'); diff --git a/src/legacy_CblReactNativeEngine.tsx b/src/legacy_CblReactNativeEngine.tsx new file mode 100644 index 0000000..df88826 --- /dev/null +++ b/src/legacy_CblReactNativeEngine.tsx @@ -0,0 +1,1648 @@ +// /** +// * @deprecated LEGACY BRIDGE — DO NOT USE +// * +// * This file is the original single-module React Native bridge implementation. +// * It uses NativeModules.CblReactnative (the old arch bridge) and will be +// * removed once the Turbo Module migration (Phase 2–7) is complete. +// * +// * The active implementation is: src/CblReactNativeEngine.tsx +// * +// */ +// import { +// EmitterSubscription, +// NativeEventEmitter, +// NativeModules, +// Platform, +// } from 'react-native'; +// import { +// CollectionChangeListenerArgs, +// ICoreEngine, +// ListenerCallback, +// CollectionArgs, +// CollectionCreateIndexArgs, +// CollectionDeleteDocumentArgs, +// CollectionDeleteIndexArgs, +// CollectionDocumentExpirationArgs, +// CollectionDocumentGetBlobContentArgs, +// CollectionDocumentSaveResult, +// CollectionGetDocumentArgs, +// CollectionPurgeDocumentArgs, +// CollectionSaveStringArgs, +// CollectionsResult, +// DatabaseArgs, +// DatabaseCopyArgs, +// DatabaseCreateIndexArgs, +// DatabaseDeleteDocumentArgs, +// DatabaseDeleteIndexArgs, +// DatabaseEncryptionKeyArgs, +// DatabaseExistsArgs, +// DatabaseGetDocumentArgs, +// DatabaseOpenArgs, +// DatabasePerformMaintenanceArgs, +// DatabasePurgeDocumentArgs, +// DatabaseSaveArgs, +// DatabaseSetFileLoggingConfigArgs, +// DatabaseSetLogLevelArgs, +// DocumentChangeListenerArgs, +// DocumentExpirationResult, +// DocumentResult, +// QueryChangeListenerArgs, +// QueryExecuteArgs, +// QueryRemoveChangeListenerArgs, +// ReplicationChangeListenerArgs, +// ReplicatorArgs, +// ReplicatorCollectionArgs, +// ReplicatorCreateArgs, +// ReplicatorDocumentPendingArgs, +// ScopeArgs, +// ScopesResult, +// DocumentGetBlobContentArgs, +// URLEndpointListenerCreateArgs, +// URLEndpointListenerArgs, +// URLEndpointListenerTLSIdentityArgs, +// URLEndpointListenerStatus, +// } from './cblite-js/cblite/core-types'; + +// import { EngineLocator } from './cblite-js/cblite/src/engine-locator'; +// import { Collection } from './cblite-js/cblite/src/collection'; +// import { Result } from './cblite-js/cblite/src/result'; +// import { ReplicatorStatus } from './cblite-js/cblite/src/replicator-status'; +// import { Scope } from './cblite-js/cblite/src/scope'; + +// import { LogLevel, LogDomain } from './cblite-js/cblite/src/log-sinks-enums'; +// import type { +// LogSinksSetConsoleArgs, +// LogSinksSetFileArgs, +// LogSinksSetCustomArgs, +// } from './cblite-js/cblite/src/log-sinks-types'; + +// import uuid from 'react-native-uuid'; + +// export class CblReactNativeEngine implements ICoreEngine { +// _defaultCollectionName = '_default'; +// _defaultScopeName = '_default'; +// debugConsole = false; +// platform = Platform.OS; + +// //event name mapping for the native side of the module + +// _eventReplicatorStatusChange = 'replicatorStatusChange'; +// _eventReplicatorDocumentChange = 'replicatorDocumentChange'; +// _eventCollectionChange = 'collectionChange'; +// _eventCollectionDocumentChange = 'collectionDocumentChange'; +// _eventQueryChange = 'queryChange'; + +// //used to listen to replicator change events for both status and document changes +// private _replicatorChangeListeners: Map = new Map(); +// private _emitterSubscriptions: Map = new Map(); + +// private _replicatorDocumentChangeListeners: Map = +// new Map(); +// private _isReplicatorDocumentChangeEventSetup: boolean = false; + +// private _collectionChangeListeners: Map = new Map(); +// private _collectionDocumentChangeListeners: Map = +// new Map(); + +// private _queryChangeListeners: Map = new Map(); + +// // Storage for custom log sink callbacks, users can have multiple custom logs +// // Key : unique token +// // value: callback function +// private customLogCallbacksMap: Map< +// string, +// (level: LogLevel, domain: LogDomain, message: string) => void +// > = new Map(); + +// private static readonly LINKING_ERROR = +// `The package 'cbl-reactnative' doesn't seem to be linked. Make sure: \n\n` + +// Platform.select({ ios: "- You have run 'pod install'\n", default: '' }) + +// '- You rebuilt the app after installing the package\n' + +// '- You are not using Expo Go\n'; + +// CblReactNative = NativeModules.CblReactnative +// ? NativeModules.CblReactnative +// : new Proxy( +// {}, +// { +// get() { +// throw new Error(CblReactNativeEngine.LINKING_ERROR); +// }, +// } +// ); + +// _eventEmitter: NativeEventEmitter; + +// constructor(customEventEmitter?: NativeEventEmitter) { +// EngineLocator.registerEngine(EngineLocator.key, this); + +// if (customEventEmitter) { +// this.debugLog('Using provided custom event emitter'); +// this._eventEmitter = customEventEmitter; +// } else { +// this._eventEmitter = new NativeEventEmitter(this.CblReactNative); +// } + +// // Always add the customLogMessage listener regardless of emitter source +// this._eventEmitter.addListener( +// 'customLogMessage', +// (data: { +// token: string; +// level: LogLevel; +// domain: LogDomain; +// message: string; +// }) => { +// const callback = this.customLogCallbacksMap.get(data.token); + +// if (callback) { +// callback( +// data.level as LogLevel, +// data.domain as LogDomain, +// data.message +// ); +// } +// } +// ); +// } + +// //private logging function +// private debugLog(message: string) { +// if (this.debugConsole) { +// console.log(message); +// } +// } + +// //startListeningEvents - used to listen to events from the native side of the module. Implements Native change listeners for Couchbase Lite +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// startListeningEvents = (event: string, callback: any) => { +// console.log(`::DEBUG:: Registering listener for event: ${event}`); +// return this._eventEmitter.addListener( +// event, +// (data) => { +// this.debugLog( +// `::DEBUG:: Received event: ${event} with data: ${JSON.stringify(data)}` +// ); +// callback(data); +// }, +// this +// ); +// }; + +// collection_AddChangeListener( +// args: CollectionChangeListenerArgs, +// lcb: ListenerCallback +// ): Promise { +// return new Promise((resolve, reject) => { +// const token = args.changeListenerToken; + +// if (this._collectionChangeListeners.has(token)) { +// reject(new Error('Change listener token already exists')); +// return; +// } + +// const subscription = this.startListeningEvents( +// this._eventCollectionChange, +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// (results: any) => { +// if (results.token === token) { +// this.debugLog( +// `::DEBUG:: Received collection change event for token: ${token}` +// ); +// lcb(results); +// } +// } +// ); + +// this._emitterSubscriptions.set(token, subscription); +// this._collectionChangeListeners.set(token, lcb); + +// this.CblReactNative.collection_AddChangeListener( +// token, +// args.collectionName, +// args.name, +// args.scopeName +// ).then( +// () => resolve(), +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// (error: any) => { +// this._emitterSubscriptions.delete(token); +// this._collectionChangeListeners.delete(token); +// subscription.remove(); +// reject(error); +// } +// ); +// }); +// } + +// collection_AddDocumentChangeListener( +// args: DocumentChangeListenerArgs, +// lcb: ListenerCallback +// ): Promise { +// return new Promise((resolve, reject) => { +// const token = args.changeListenerToken; + +// if (this._collectionDocumentChangeListeners.has(token)) { +// reject(new Error('Document change listener token already exists')); +// return; +// } + +// const subscription = this.startListeningEvents( +// this._eventCollectionDocumentChange, +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// (results: any) => { +// if (results.token === token) { +// this.debugLog( +// `::DEBUG:: Received document change event for token: ${token}` +// ); +// lcb(results); +// } +// } +// ); + +// this._emitterSubscriptions.set(token, subscription); +// this._collectionDocumentChangeListeners.set(token, lcb); + +// this.CblReactNative.collection_AddDocumentChangeListener( +// token, +// args.documentId, +// args.collectionName, +// args.name, +// args.scopeName +// ).then( +// () => resolve(), +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// (error: any) => { +// this._emitterSubscriptions.delete(token); +// this._collectionDocumentChangeListeners.delete(token); +// subscription.remove(); +// reject(error); +// } +// ); +// }); +// } + +// collection_CreateCollection(args: CollectionArgs): Promise { +// return new Promise((resolve, reject) => { +// this.CblReactNative.collection_CreateCollection( +// args.collectionName, +// args.name, +// args.scopeName +// ).then( +// (result: Collection) => { +// resolve(result); +// }, +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// (error: any) => { +// reject(error); +// } +// ); +// }); +// } + +// collection_CreateIndex(args: CollectionCreateIndexArgs): Promise { +// return new Promise((resolve, reject) => { +// this.CblReactNative.collection_CreateIndex( +// args.indexName, +// args.index, +// args.collectionName, +// args.scopeName, +// args.name +// ).then( +// () => { +// resolve(); +// }, +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// (error: any) => { +// reject(error); +// } +// ); +// }); +// } + +// collection_DeleteCollection(args: CollectionArgs): Promise { +// return new Promise((resolve, reject) => { +// this.CblReactNative.collection_DeleteCollection( +// args.collectionName, +// args.name, +// args.scopeName +// ).then( +// () => { +// resolve(); +// }, +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// (error: any) => { +// reject(error); +// } +// ); +// }); +// } + +// collection_DeleteDocument(args: CollectionDeleteDocumentArgs): Promise { +// const concurrencyControl = +// args.concurrencyControl !== null +// ? (args.concurrencyControl as number) +// : -9999; +// this.debugLog( +// `::DEBUG:: collection_DeleteDocument: ${args.docId} ${args.name} ${args.scopeName} ${args.collectionName} ${concurrencyControl}` +// ); +// return new Promise((resolve, reject) => { +// this.CblReactNative.collection_DeleteDocument( +// args.docId, +// args.name, +// args.scopeName, +// args.collectionName, +// concurrencyControl +// ).then( +// () => { +// this.debugLog(`::DEBUG:: collection_DeleteDocument completed`); +// resolve(); +// }, +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// (error: any) => { +// this.debugLog(`::DEBUG:: collection_DeleteDocument Error: ${error}`); +// reject(error); +// } +// ); +// }); +// } + +// collection_DeleteIndex(args: CollectionDeleteIndexArgs): Promise { +// return new Promise((resolve, reject) => { +// this.CblReactNative.collection_DeleteIndex( +// args.indexName, +// args.collectionName, +// args.scopeName, +// args.name +// ).then( +// () => { +// resolve(); +// }, +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// (error: any) => { +// reject(error); +// } +// ); +// }); +// } + +// collection_GetBlobContent( +// args: CollectionDocumentGetBlobContentArgs +// ): Promise<{ data: ArrayBuffer }> { +// return new Promise((resolve, reject) => { +// this.CblReactNative.collection_GetBlobContent( +// args.key, +// args.documentId, +// args.name, +// args.scopeName, +// args.collectionName +// ).then( +// (resultsData: { data: Iterable }) => { +// resolve({ data: new Uint8Array(resultsData.data).buffer }); +// }, +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// (error: any) => { +// reject(error); +// } +// ); +// }); +// } + +// collection_GetCollection(args: CollectionArgs): Promise { +// return new Promise((resolve, reject) => { +// this.CblReactNative.collection_GetCollection( +// args.collectionName, +// args.name, +// args.scopeName +// ).then( +// (result: Collection) => { +// resolve(result); +// }, +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// (error: any) => { +// reject(error); +// } +// ); +// }); +// } + +// collection_GetCollections(args: ScopeArgs): Promise { +// return new Promise((resolve, reject) => { +// this.CblReactNative.collection_GetCollections( +// args.name, +// args.scopeName +// ).then( +// (result: CollectionsResult) => { +// resolve(result); +// }, +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// (error: any) => { +// reject(error); +// } +// ); +// }); +// } + +// collection_GetCount(args: CollectionArgs): Promise<{ count: number }> { +// this.debugLog( +// `::DEBUG:: collection_GetCount: ${args.collectionName} ${args.name} ${args.scopeName}` +// ); +// return new Promise((resolve, reject) => { +// this.CblReactNative.collection_GetCount( +// args.collectionName, +// args.name, +// args.scopeName +// ).then( +// (result: { count: number }) => { +// this.debugLog( +// `::DEBUG:: collection_GetCount completed with result: ${JSON.stringify(result)}` +// ); +// resolve(result); +// }, +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// (error: any) => { +// this.debugLog(`::DEBUG:: collection_GetCount Error: ${error}`); +// reject(error); +// } +// ); +// }); +// } + +// async collection_GetFullName( +// args: CollectionArgs +// ): Promise<{ fullName: string }> { +// this.debugLog( +// `::DEBUG:: collection_GetFullName: ${args.collectionName} ${args.name} ${args.scopeName}` +// ); + +// try { +// const result = await this.CblReactNative.collection_GetFullName( +// args.collectionName, +// args.name, +// args.scopeName +// ); + +// this.debugLog( +// `::DEBUG:: collection_GetFullName completed with result: ${JSON.stringify(result)}` +// ); + +// return result; +// } catch (error: unknown) { +// this.debugLog(`::DEBUG:: collection_GetFullName Error: ${error}`); +// throw error; // Re-throw to maintain error propagation +// } +// } + +// collection_GetDefault(args: DatabaseArgs): Promise { +// return new Promise((resolve, reject) => { +// this.CblReactNative.collection_GetDefault(args.name).then( +// (result: Collection) => { +// resolve(result); +// }, +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// (error: any) => { +// reject(error); +// } +// ); +// }); +// } + +// collection_GetDocument( +// args: CollectionGetDocumentArgs +// ): Promise { +// this.debugLog( +// `::DEBUG:: collection_GetDocument: ${args.docId} ${args.name} ${args.scopeName} ${args.collectionName}` +// ); + +// return new Promise((resolve, reject) => { +// this.CblReactNative.collection_GetDocument( +// args.docId, +// args.name, +// args.scopeName, +// args.collectionName +// ).then( +// (dr: DocumentResult) => { +// this.debugLog( +// `::DEBUG:: collection_GetDocument completed with result: ${JSON.stringify(dr)}` +// ); +// resolve(dr); +// }, +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// (error: any) => { +// this.debugLog(`::DEBUG:: collection_GetDocument Error: ${error}`); +// reject(error); +// } +// ); +// }); +// } + +// collection_GetDocumentExpiration( +// args: CollectionGetDocumentArgs +// ): Promise { +// return new Promise((resolve, reject) => { +// this.CblReactNative.collection_GetDocumentExpiration( +// args.docId, +// args.name, +// args.scopeName, +// args.collectionName +// ).then( +// (der: DocumentExpirationResult) => { +// resolve(der); +// }, +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// (error: any) => { +// reject(error); +// } +// ); +// }); +// } + +// collection_GetIndexes(args: CollectionArgs): Promise<{ indexes: string[] }> { +// return new Promise((resolve, reject) => { +// this.CblReactNative.collection_GetIndexes( +// args.collectionName, +// args.scopeName, +// args.name +// ).then( +// (items: { indexes: string[] }) => { +// resolve(items); +// }, +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// (error: any) => { +// reject(error); +// } +// ); +// }); +// } + +// collection_PurgeDocument(args: CollectionPurgeDocumentArgs): Promise { +// return new Promise((resolve, reject) => { +// this.CblReactNative.collection_PurgeDocument( +// args.docId, +// args.name, +// args.scopeName, +// args.collectionName +// ).then( +// () => { +// resolve(); +// }, +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// (error: any) => { +// reject(error); +// } +// ); +// }); +// } + +// collection_RemoveChangeListener( +// // eslint-disable-next-line +// args: CollectionChangeListenerArgs +// ): Promise { +// return new Promise((resolve, reject) => { +// const token = args.changeListenerToken; + +// // Remove the subscription +// if (this._emitterSubscriptions.has(token)) { +// this._emitterSubscriptions.get(token)?.remove(); +// this._emitterSubscriptions.delete(token); +// } + +// // Remove the listener from the collection listeners map +// if (this._collectionChangeListeners.has(token)) { +// this._collectionChangeListeners.delete(token); +// } else { +// reject(new Error(`No listener found with token: ${token}`)); +// return; +// } + +// // Remove the listener from the native side +// this.CblReactNative.collection_RemoveChangeListener(token).then( +// () => { +// this.debugLog( +// `::DEBUG:: collection_RemoveChangeListener completed for token: ${token}` +// ); +// resolve(); +// }, +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// (error: any) => { +// this.debugLog( +// `::DEBUG:: collection_RemoveChangeListener Error: ${error}` +// ); +// reject(error); +// } +// ); +// }); +// } + +// collection_RemoveDocumentChangeListener( +// // eslint-disable-next-line +// args: CollectionChangeListenerArgs +// ): Promise { +// return new Promise((resolve, reject) => { +// const token = args.changeListenerToken; + +// // Remove the subscription +// if (this._emitterSubscriptions.has(token)) { +// this._emitterSubscriptions.get(token)?.remove(); +// this._emitterSubscriptions.delete(token); +// } + +// // Remove the listener from the document listeners map +// if (this._collectionDocumentChangeListeners.has(token)) { +// this._collectionDocumentChangeListeners.delete(token); +// } else { +// reject(new Error(`No document listener found with token: ${token}`)); +// return; +// } + +// // Remove the listener from the native side +// this.CblReactNative.collection_RemoveChangeListener(token).then( +// () => { +// this.debugLog( +// `::DEBUG:: collection_RemoveDocumentChangeListener completed for token: ${token}` +// ); +// resolve(); +// }, +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// (error: any) => { +// this.debugLog( +// `::DEBUG:: collection_RemoveDocumentChangeListener Error: ${error}` +// ); +// reject(error); +// } +// ); +// }); +// } + +// /** +// * Generic method to remove any listener by its UUID token. +// * Calls the native listenerToken_Remove bridge method. +// */ +// listenerToken_Remove(args: { changeListenerToken: string }): Promise { +// return this.CblReactNative.listenerToken_Remove( +// args.changeListenerToken +// ).then( +// () => { +// this.debugLog( +// `::DEBUG:: Successfully removed listener with token ${args.changeListenerToken}` +// ); +// }, +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// (error: any) => { +// this.debugLog( +// `::ERROR:: Failed to remove listener with token ${args.changeListenerToken}: ${error}` +// ); +// throw error; +// } +// ); +// } + +// collection_Save( +// args: CollectionSaveStringArgs +// ): Promise { +// //deal with react native passing nulls +// const concurrencyControl = +// args.concurrencyControl !== null +// ? (args.concurrencyControl as number) +// : -9999; +// this.debugLog( +// `::DEBUG:: collection_Save: ${args.document} ${args.blobs} ${args.id} ${args.name} ${args.scopeName} ${args.collectionName} ${concurrencyControl}` +// ); + +// return new Promise((resolve, reject) => { +// this.CblReactNative.collection_Save( +// args.document, +// args.blobs, +// args.id, +// args.name, +// args.scopeName, +// args.collectionName, +// concurrencyControl +// ).then( +// (resultsData: CollectionDocumentSaveResult) => { +// if (this.debugConsole) { +// console.log( +// `::DEBUG:: collection_Save completed with result: ${JSON.stringify(resultsData)}` +// ); +// } +// resolve(resultsData); +// }, +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// (error: any) => { +// console.log(`::DEBUG:: collection_Save Error: ${error}`); +// reject(error); +// } +// ); +// }); +// } + +// collection_SetDocumentExpiration( +// args: CollectionDocumentExpirationArgs +// ): Promise { +// return new Promise((resolve, reject) => { +// this.CblReactNative.collection_SetDocumentExpiration( +// args.expiration.toISOString(), +// args.docId, +// args.name, +// args.scopeName, +// args.collectionName +// ).then( +// () => { +// resolve(); +// }, +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// (error: any) => { +// reject(error); +// } +// ); +// }); +// } + +// database_ChangeEncryptionKey(args: DatabaseEncryptionKeyArgs): Promise { +// return new Promise((resolve, reject) => { +// this.CblReactNative.database_ChangeEncryptionKey( +// args.newKey, +// args.name +// ).then( +// () => resolve(), +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// (error: any) => { +// reject(error); +// } +// ); +// }); +// } + +// database_Close(args: DatabaseArgs): Promise { +// this.debugLog(`::DEBUG:: database_Close: ${args.name}`); +// return new Promise((resolve, reject) => { +// this.CblReactNative.database_Close(args.name).then( +// () => { +// this.debugLog(`::DEBUG:: database_Close completed`); +// resolve(); +// }, +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// (error: any) => { +// this.debugLog(`::DEBUG:: database_Close Error: ${error}`); +// reject(error); +// } +// ); +// }); +// } + +// database_Copy(args: DatabaseCopyArgs): Promise { +// return new Promise((resolve, reject) => { +// this.CblReactNative.database_Copy( +// args.path, +// args.newName, +// args.config.directory, +// args.config.encryptionKey +// ).then( +// () => resolve(), +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// (error: any) => { +// reject(error); +// } +// ); +// }); +// } + +// /** +// * @deprecated This function will be removed in future versions. Use collection_CreateIndex instead. +// */ +// database_CreateIndex(args: DatabaseCreateIndexArgs): Promise { +// const colArgs: CollectionCreateIndexArgs = { +// name: args.name, +// collectionName: this._defaultCollectionName, +// scopeName: this._defaultScopeName, +// indexName: args.indexName, +// index: args.index, +// }; +// return this.collection_CreateIndex(colArgs); +// } + +// database_Delete(args: DatabaseArgs): Promise { +// if (this.debugConsole) { +// console.log(`::DEBUG:: database_Delete: ${args.name}`); +// } +// return new Promise((resolve, reject) => { +// this.CblReactNative.database_Delete(args.name).then( +// () => { +// this.debugLog(`::DEBUG:: database_Delete completed`); +// resolve(); +// }, +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// (error: any) => { +// console.log(`::DEBUG:: database_Delete Error: ${error}`); +// reject(error); +// } +// ); +// }); +// } + +// database_DeleteWithPath(args: DatabaseExistsArgs): Promise { +// this.debugLog( +// `::DEBUG:: database_DeleteWithPath: ${args.directory} ${args.databaseName}` +// ); +// return new Promise((resolve, reject) => { +// this.CblReactNative.database_DeleteWithPath( +// args.directory, +// args.databaseName +// ).then( +// () => { +// this.debugLog(`::DEBUG:: database_DeleteWithPath completed`); +// resolve(); +// }, +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// (error: any) => { +// this.debugLog(`::DEBUG:: database_DeleteWithPath Error: ${error}`); +// reject(error); +// } +// ); +// }); +// } + +// /** +// * @deprecated This will be removed in future versions. Use collection_DeleteDocument instead. +// */ +// database_DeleteDocument(args: DatabaseDeleteDocumentArgs): Promise { +// const colArgs: CollectionDeleteDocumentArgs = { +// name: args.name, +// collectionName: this._defaultCollectionName, +// scopeName: this._defaultScopeName, +// docId: args.docId, +// concurrencyControl: args.concurrencyControl, +// }; +// this.debugLog( +// `::DEBUG:: database_DeleteDocument: ${args.docId} ${args.name} ${this._defaultScopeName} ${this._defaultCollectionName} ${args.concurrencyControl}` +// ); +// return this.collection_DeleteDocument(colArgs); +// } + +// /** +// * @deprecated This function will be removed in future versions. Use collection_DeleteIndex instead. +// */ +// database_DeleteIndex(args: DatabaseDeleteIndexArgs): Promise { +// const colArgs: CollectionDeleteIndexArgs = { +// name: args.name, +// collectionName: this._defaultCollectionName, +// scopeName: this._defaultScopeName, +// indexName: args.indexName, +// }; +// return this.collection_DeleteIndex(colArgs); +// } + +// database_Exists(args: DatabaseExistsArgs): Promise<{ exists: boolean }> { +// return new Promise((resolve, reject) => { +// this.CblReactNative.database_Exists( +// args.databaseName, +// args.directory +// ).then( +// (result: boolean) => resolve({ exists: result }), +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// (error: any) => { +// reject(error); +// } +// ); +// }); +// } + +// /** +// * @deprecated This will be removed in future versions. Use collection_GetCount instead. +// */ +// database_GetCount(args: DatabaseArgs): Promise<{ count: number }> { +// const colArgs: CollectionArgs = { +// name: args.name, +// collectionName: this._defaultCollectionName, +// scopeName: this._defaultScopeName, +// }; +// return this.collection_GetCount(colArgs); +// } + +// /** +// * @deprecated This will be removed in future versions. Use collection_GetDocument instead. +// */ +// database_GetDocument(args: DatabaseGetDocumentArgs): Promise { +// const colArgs: CollectionGetDocumentArgs = { +// name: args.name, +// collectionName: this._defaultCollectionName, +// scopeName: this._defaultScopeName, +// docId: args.docId, +// }; +// return this.collection_GetDocument(colArgs); +// } + +// /** +// * @deprecated This function will be removed in future versions. Use collection_GetIndexes instead. +// */ +// database_GetIndexes(args: DatabaseArgs): Promise<{ indexes: string[] }> { +// const colArgs: CollectionArgs = { +// name: args.name, +// collectionName: this._defaultCollectionName, +// scopeName: this._defaultScopeName, +// }; +// return this.collection_GetIndexes(colArgs); +// } + +// database_GetPath(args: DatabaseArgs): Promise<{ path: string }> { +// return new Promise((resolve, reject) => { +// this.CblReactNative.database_GetPath(args.name).then( +// (result: string) => resolve({ path: result }), +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// (error: any) => { +// reject(error); +// } +// ); +// }); +// } + +// database_Open( +// args: DatabaseOpenArgs +// ): Promise<{ databaseUniqueName: string }> { +// this.debugLog( +// `::DEBUG:: database_Open: ${args.name} ${args.config.directory} ${args.config.encryptionKey}` +// ); +// return new Promise((resolve, reject) => { +// this.CblReactNative.database_Open( +// args.name, +// args.config.directory, +// args.config.encryptionKey +// ).then( +// (databaseUniqueName) => { +// this.debugLog(`::DEBUG:: database_Open completed`); +// resolve(databaseUniqueName); +// }, +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// (error: any) => { +// this.debugLog(`::DEBUG:: database_Open Error: ${error}`); +// reject(error); +// } +// ); +// }); +// } + +// database_PerformMaintenance( +// args: DatabasePerformMaintenanceArgs +// ): Promise { +// const numValue = args.maintenanceType.valueOf(); +// return new Promise((resolve, reject) => { +// this.CblReactNative.database_PerformMaintenance(numValue, args.name).then( +// () => resolve(), +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// (error: any) => { +// reject(error); +// } +// ); +// }); +// } + +// /** +// * @deprecated This will be removed in future versions. Use collection_PurgeDocument instead. +// */ +// database_PurgeDocument(args: DatabasePurgeDocumentArgs): Promise { +// const colArgs: CollectionPurgeDocumentArgs = { +// name: args.name, +// collectionName: this._defaultCollectionName, +// scopeName: this._defaultScopeName, +// docId: args.docId, +// }; +// return this.collection_PurgeDocument(colArgs); +// } + +// /** +// * @deprecated This function will be removed in future versions. Use collection_Save instead. +// */ +// database_Save(args: DatabaseSaveArgs): Promise<{ _id: string }> { +// const colArgs: CollectionSaveStringArgs = { +// name: args.name, +// collectionName: this._defaultCollectionName, +// scopeName: this._defaultScopeName, +// id: args.id, +// document: JSON.stringify(args.document), +// blobs: JSON.stringify(args.blobs), +// concurrencyControl: args.concurrencyControl, +// }; +// return this.collection_Save(colArgs); +// } + +// database_SetFileLoggingConfig( +// args: DatabaseSetFileLoggingConfigArgs +// ): Promise { +// return new Promise((resolve, reject) => { +// this.CblReactNative.database_SetFileLoggingConfig( +// args.name, +// args.config.directory, +// args.config.level, +// args.config.maxSize, +// args.config.maxRotateCount, +// args.config.usePlaintext +// ).then( +// () => resolve(), +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// (error: any) => { +// reject(error); +// } +// ); +// }); +// } + +// database_SetLogLevel(args: DatabaseSetLogLevelArgs): Promise { +// return new Promise((resolve, reject) => { +// this.CblReactNative.database_SetLogLevel(args.domain, args.logLevel).then( +// () => resolve(), +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// (error: any) => { +// reject(error); +// } +// ); +// }); +// } + +// /** +// * @deprecated This will be removed in future versions. Use collection_GetBlobContent instead. +// */ +// document_GetBlobContent( +// args: DocumentGetBlobContentArgs +// ): Promise<{ data: ArrayBuffer }> { +// const colArgs: CollectionDocumentGetBlobContentArgs = { +// name: args.name, +// collectionName: this._defaultCollectionName, +// scopeName: this._defaultScopeName, +// documentId: args.documentId, +// key: args.key, +// }; +// return this.collection_GetBlobContent(colArgs); +// } + +// file_GetDefaultPath(): Promise<{ path: string }> { +// return new Promise((resolve, reject) => { +// this.CblReactNative.file_GetDefaultPath().then( +// (result: string) => { +// resolve({ path: result }); +// }, +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// (error: any) => { +// reject(error); +// } +// ); +// }); +// } + +// // eslint-disable-next-line +// file_GetFileNamesInDirectory(args: { +// path: string; +// }): Promise<{ files: string[] }> { +// return Promise.resolve({ files: [] }); +// } + +// query_AddChangeListener( +// args: QueryChangeListenerArgs, +// lcb: ListenerCallback +// ): Promise { +// return new Promise((resolve, reject) => { +// const token = args.changeListenerToken; + +// if (this._queryChangeListeners.has(token)) { +// reject(new Error('Query change listener token already exists')); +// return; +// } + +// const subscription = this.startListeningEvents( +// this._eventQueryChange, +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// (results: any) => { +// if (results.token === token) { +// this.debugLog( +// `::DEBUG:: Received query change event for token: ${token}` +// ); +// lcb(results); +// } +// } +// ); + +// this._emitterSubscriptions.set(token, subscription); +// this._queryChangeListeners.set(token, lcb); + +// this.CblReactNative.query_AddChangeListener( +// token, +// args.query, +// args.parameters, +// args.name +// ).then( +// () => resolve(), +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// (error: any) => { +// this._emitterSubscriptions.delete(token); +// this._queryChangeListeners.delete(token); +// subscription.remove(); +// reject(error); +// } +// ); +// }); +// } + +// query_Execute(args: QueryExecuteArgs): Promise { +// return new Promise((resolve, reject) => { +// this.CblReactNative.query_Execute( +// args.query, +// args.parameters, +// args.name +// ).then( +// (result: Result) => { +// resolve(result); +// }, +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// (error: any) => { +// reject(error); +// } +// ); +// }); +// } + +// query_Explain(args: QueryExecuteArgs): Promise<{ data: string }> { +// return new Promise((resolve, reject) => { +// this.CblReactNative.query_Explain( +// args.query, +// args.parameters, +// args.name +// ).then( +// (result: { data: string }) => { +// resolve(result); +// }, +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// (error: any) => { +// reject(error); +// } +// ); +// }); +// } + +// query_RemoveChangeListener( +// args: QueryRemoveChangeListenerArgs +// ): Promise { +// return new Promise((resolve, reject) => { +// const token = args.changeListenerToken; + +// if (this._emitterSubscriptions.has(token)) { +// this._emitterSubscriptions.get(token)?.remove(); +// this._emitterSubscriptions.delete(token); +// } + +// if (this._queryChangeListeners.has(token)) { +// this._queryChangeListeners.delete(token); +// } else { +// reject(new Error(`No query listener found with token: ${token}`)); +// return; +// } + +// this.CblReactNative.query_RemoveChangeListener(token).then( +// () => { +// this.debugLog( +// `::DEBUG:: query_RemoveChangeListener completed for token: ${token}` +// ); +// resolve(); +// }, +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// (error: any) => { +// this.debugLog(`::DEBUG:: query_RemoveChangeListener Error: ${error}`); +// reject(error); +// } +// ); +// }); +// } + +// replicator_AddChangeListener( +// args: ReplicationChangeListenerArgs, +// lcb: ListenerCallback +// ): Promise { +// //need to track the listener callback for later use due to how React Native events work. Events are global so we need to first find which callback to call, we could have multiple replicators registered +// //https://reactnative.dev/docs/native-modules-ios#sending-events-to-javascript +// if ( +// this._replicatorChangeListeners.has(args.changeListenerToken) || +// this._emitterSubscriptions.has(args.changeListenerToken) +// ) { +// throw new Error( +// 'ERROR: changeListenerToken already exists and is registered to listen to callbacks, cannot add a new one' +// ); +// } +// //if the event listener is not setup, then set up the listener. +// //Event listener only needs to be setup once for any replicators in memory +// const subscription = this._eventEmitter.addListener( +// this._eventReplicatorStatusChange, +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// (results: any) => { +// this.debugLog( +// `::DEBUG:: Received event ${this._eventReplicatorStatusChange}` +// ); +// const token = results.token as string; +// const data = results?.status; +// const error = results?.error; +// if (token === undefined || token === null || token.length === 0) { +// this.debugLog( +// '::ERROR:: No token to resolve back to proper callback for Replicator Status Change' +// ); +// throw new Error( +// 'ERROR: No token to resolve back to proper callback' +// ); +// } +// const callback = this._replicatorChangeListeners.get(token); +// if (callback !== undefined) { +// callback(data, error); +// } else { +// this.debugLog( +// `Error: Could not found callback method for token: ${token}.` +// ); +// throw new Error( +// `Error: Could not found callback method for token: ${token}.` +// ); +// } +// } +// ); +// return new Promise((resolve, reject) => { +// this.CblReactNative.replicator_AddChangeListener( +// args.changeListenerToken, +// args.replicatorId +// ).then( +// () => { +// //add token to change listener map +// this._emitterSubscriptions.set( +// args.changeListenerToken, +// subscription +// ); +// this._replicatorChangeListeners.set(args.changeListenerToken, lcb); +// this.debugLog( +// `::DEBUG:: replicator_AddChangeListener listener count: ${this._eventEmitter.listenerCount(this._eventReplicatorStatusChange)}` +// ); +// resolve(); +// }, +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// (error: any) => { +// this._replicatorChangeListeners.delete(args.changeListenerToken); +// subscription.remove(); +// reject(error); +// } +// ); +// }); +// } + +// replicator_AddDocumentChangeListener( +// args: ReplicationChangeListenerArgs, +// lcb: ListenerCallback +// ): Promise { +// //need to track the listener callback for later use due to how React Native events work +// if ( +// this._replicatorDocumentChangeListeners.has(args.changeListenerToken) || +// this._emitterSubscriptions.has(args.changeListenerToken + '_doc') +// ) { +// throw new Error( +// 'ERROR: changeListenerToken already exists and is registered to listen to document callbacks, cannot add a new one' +// ); +// } + +// // Set up document change listener if not already done +// if (!this._isReplicatorDocumentChangeEventSetup) { +// const docSubscription = this._eventEmitter.addListener( +// this._eventReplicatorDocumentChange, +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// (results: any) => { +// this.debugLog( +// `::DEBUG:: Received event ${this._eventReplicatorDocumentChange}` +// ); +// const token = results.token as string; +// const data = results?.documents; +// const error = results?.error; + +// if (token === undefined || token === null || token.length === 0) { +// this.debugLog( +// '::ERROR:: No token to resolve back to proper callback for Replicator Document Change' +// ); +// throw new Error( +// 'ERROR: No token to resolve back to proper callback' +// ); +// } + +// const callback = this._replicatorDocumentChangeListeners.get(token); +// if (callback !== undefined) { +// callback(data, error); +// } else { +// this.debugLog( +// `Error: Could not find callback method for document change token: ${token}.` +// ); +// throw new Error( +// `Error: Could not find callback method for document change token: ${token}.` +// ); +// } +// } +// ); + +// this._emitterSubscriptions.set( +// this._eventReplicatorDocumentChange, +// docSubscription +// ); +// this._isReplicatorDocumentChangeEventSetup = true; +// } + +// return new Promise((resolve, reject) => { +// this.CblReactNative.replicator_AddDocumentChangeListener( +// args.changeListenerToken, +// args.replicatorId +// ).then( +// () => { +// this._replicatorDocumentChangeListeners.set( +// args.changeListenerToken, +// lcb +// ); +// this.debugLog( +// `::DEBUG:: replicator_AddDocumentChangeListener added successfully with token: ${args.changeListenerToken}` +// ); +// resolve(); +// }, +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// (error: any) => { +// this._replicatorDocumentChangeListeners.delete( +// args.changeListenerToken +// ); +// reject(error); +// } +// ); +// }); +// } + +// replicator_Cleanup(args: ReplicatorArgs): Promise { +// return new Promise((resolve, reject) => { +// this.CblReactNative.replicator_Cleanup(args.replicatorId).then( +// () => { +// resolve(); +// }, +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// (error: any) => { +// reject(error); +// } +// ); +// }); +// } + +// replicator_Create(args: ReplicatorCreateArgs): Promise { +// return new Promise((resolve, reject) => { +// this.CblReactNative.replicator_Create(args.config).then( +// (results: ReplicatorArgs) => { +// resolve(results); +// }, +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// (error: any) => { +// reject(error); +// } +// ); +// }); +// } + +// replicator_GetPendingDocumentIds( +// args: ReplicatorCollectionArgs +// ): Promise<{ pendingDocumentIds: string[] }> { +// return new Promise((resolve, reject) => { +// this.CblReactNative.replicator_GetPendingDocumentIds( +// args.replicatorId, +// args.name, +// args.scopeName, +// args.collectionName +// ).then( +// (results: { pendingDocumentIds: string[] }) => { +// resolve(results); +// }, +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// (error: any) => { +// reject(error); +// } +// ); +// }); +// } + +// replicator_GetStatus(args: ReplicatorArgs): Promise { +// return new Promise((resolve, reject) => { +// this.CblReactNative.replicator_GetStatus(args.replicatorId).then( +// (results: ReplicatorStatus) => { +// resolve(results); +// }, +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// (error: any) => { +// reject(error); +// } +// ); +// }); +// } + +// replicator_IsDocumentPending( +// args: ReplicatorDocumentPendingArgs +// ): Promise<{ isPending: boolean }> { +// return new Promise((resolve, reject) => { +// this.CblReactNative.replicator_IsDocumentPending( +// args.documentId, +// args.replicatorId, +// args.name, +// args.scopeName, +// args.collectionName +// ).then( +// (results: { isPending: boolean }) => { +// resolve(results); +// }, +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// (error: any) => { +// reject(error); +// } +// ); +// }); +// } + +// replicator_RemoveChangeListener( +// args: ReplicationChangeListenerArgs +// ): Promise { +// if (this._replicatorDocumentChangeListeners.has(args.changeListenerToken)) { +// this._replicatorDocumentChangeListeners.delete(args.changeListenerToken); +// // Remove any subscription with the doc suffix +// if (this._emitterSubscriptions.has(args.changeListenerToken + '_doc')) { +// this._emitterSubscriptions +// .get(args.changeListenerToken + '_doc') +// ?.remove(); +// this._emitterSubscriptions.delete(args.changeListenerToken + '_doc'); +// } +// } + +// //remove the event subscription or you will have a leak +// if (this._emitterSubscriptions.has(args.changeListenerToken)) { +// this._emitterSubscriptions.get(args.changeListenerToken)?.remove(); +// this._emitterSubscriptions.delete(args.changeListenerToken); +// } +// return new Promise((resolve, reject) => { +// this.CblReactNative.replicator_RemoveChangeListener( +// args.changeListenerToken, +// args.replicatorId +// ).then( +// () => { +// //remove the listener callback from the map +// if (this._replicatorChangeListeners.has(args.changeListenerToken)) { +// this._replicatorChangeListeners.delete(args.changeListenerToken); +// } +// resolve(); +// }, +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// (error: any) => { +// reject(error); +// } +// ); +// }); +// } + +// replicator_ResetCheckpoint(args: ReplicatorArgs): Promise { +// return new Promise((resolve, reject) => { +// this.CblReactNative.replicator_ResetCheckpoint(args.replicatorId).then( +// () => { +// resolve(); +// }, +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// (error: any) => { +// reject(error); +// } +// ); +// }); +// } + +// replicator_Start(args: ReplicatorArgs): Promise { +// return new Promise((resolve, reject) => { +// this.CblReactNative.replicator_Start(args.replicatorId).then( +// () => { +// resolve(); +// }, +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// (error: any) => { +// reject(error); +// } +// ); +// }); +// } + +// replicator_Stop(args: ReplicatorArgs): Promise { +// return new Promise((resolve, reject) => { +// this.CblReactNative.replicator_Stop(args.replicatorId).then( +// () => { +// resolve(); +// }, +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// (error: any) => { +// reject(error); +// } +// ); +// }); +// } + +// scope_GetDefault(args: DatabaseArgs): Promise { +// return new Promise((resolve, reject) => { +// this.CblReactNative.scope_GetDefault(args.name).then( +// (result: Scope) => { +// resolve(result); +// }, +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// (error: any) => { +// reject(error); +// } +// ); +// }); +// } + +// scope_GetScope(args: ScopeArgs): Promise { +// return new Promise((resolve, reject) => { +// this.CblReactNative.scope_GetScope(args.scopeName, args.name).then( +// (result: Scope) => { +// resolve(result); +// }, +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// (error: any) => { +// reject(error); +// } +// ); +// }); +// } + +// scope_GetScopes(args: DatabaseArgs): Promise { +// return new Promise((resolve, reject) => { +// this.CblReactNative.scope_GetScopes(args.name).then( +// (result: ScopesResult) => { +// resolve(result); +// }, +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// (error: any) => { +// reject(error); +// } +// ); +// }); +// } + +// URLEndpointListener_createListener( +// // eslint-disable-next-line @typescript-eslint/no-unused-vars +// args: URLEndpointListenerCreateArgs +// ): Promise<{ listenerId: string }> { +// return Promise.reject(new Error('URLEndpointListener not implemented yet')); +// } + +// URLEndpointListener_startListener( +// // eslint-disable-next-line @typescript-eslint/no-unused-vars +// args: URLEndpointListenerArgs +// ): Promise { +// return Promise.reject(new Error('URLEndpointListener not implemented yet')); +// } + +// URLEndpointListener_stopListener( +// // eslint-disable-next-line @typescript-eslint/no-unused-vars +// args: URLEndpointListenerArgs +// ): Promise { +// return Promise.reject(new Error('URLEndpointListener not implemented yet')); +// } + +// URLEndpointListener_getStatus( +// // eslint-disable-next-line @typescript-eslint/no-unused-vars +// args: URLEndpointListenerArgs +// ): Promise { +// return Promise.reject(new Error('URLEndpointListener not implemented yet')); +// } + +// URLEndpointListener_deleteIdentity( +// // eslint-disable-next-line @typescript-eslint/no-unused-vars +// args: URLEndpointListenerTLSIdentityArgs +// ): Promise { +// return Promise.reject(new Error('URLEndpointListener not implemented yet')); +// } + +// getUUID(): string { +// return uuid.v4().toString(); +// } + +// // ============================================================================= +// // LOG SINKS API +// // ============================================================================= + +// /** +// * Sets or disables the console log sink +// * @param args Arguments containing level and domains, or null to disable +// */ +// async logsinks_SetConsole(args: LogSinksSetConsoleArgs): Promise { +// return this.CblReactNative.logsinks_SetConsole(args.level, args.domains); +// } + +// /** +// * Sets or disables the file log sink +// * @param args Arguments containing level and config, or null to disable +// */ +// async logsinks_SetFile(args: LogSinksSetFileArgs): Promise { +// return this.CblReactNative.logsinks_SetFile(args.level, args.config); +// } + +// /** +// * Sets or disables the custom log sink +// * @param args Arguments containing level, domains, and token, or null to disable +// */ +// async logsinks_SetCustom(args: LogSinksSetCustomArgs): Promise { +// return this.CblReactNative.logsinks_SetCustom( +// args.level, +// args.domains, +// args.token +// ); +// } +// }