From 102276fb41217ddd8a39e7273812ceb0d4c7181c Mon Sep 17 00:00:00 2001 From: jayant dhingra Date: Mon, 6 Apr 2026 22:14:51 +0530 Subject: [PATCH 1/3] feat: add TurboModule native spec files and update package configuration Adds NativeCbl* TurboModule spec files for Collection, Database, Document, Engine, Logging, Query, Replicator, and Scope. Updates package.json and podspec for TurboModule compatibility, and includes the migration plan. Made-with: Cursor --- .npmignore | 2 +- TURBO_MIGRATION_PLAN.md | 1456 ++++++++++++++++++++++++++++++++++++ cbl-reactnative.podspec | 24 +- package.json | 23 +- src/NativeCblCollection.ts | 83 ++ src/NativeCblDatabase.ts | 37 + src/NativeCblDocument.ts | 62 ++ src/NativeCblEngine.ts | 10 + src/NativeCblLogging.ts | 34 + src/NativeCblQuery.ts | 32 + src/NativeCblReplicator.ts | 53 ++ src/NativeCblScope.ts | 13 + 12 files changed, 1803 insertions(+), 26 deletions(-) create mode 100644 TURBO_MIGRATION_PLAN.md create mode 100644 src/NativeCblCollection.ts create mode 100644 src/NativeCblDatabase.ts create mode 100644 src/NativeCblDocument.ts create mode 100644 src/NativeCblEngine.ts create mode 100644 src/NativeCblLogging.ts create mode 100644 src/NativeCblQuery.ts create mode 100644 src/NativeCblReplicator.ts create mode 100644 src/NativeCblScope.ts 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/TURBO_MIGRATION_PLAN.md b/TURBO_MIGRATION_PLAN.md new file mode 100644 index 0000000..dfbddf7 --- /dev/null +++ b/TURBO_MIGRATION_PLAN.md @@ -0,0 +1,1456 @@ +# Turbo Module Migration Plan — cbl-reactnative + +## 1. Executive Summary + +This plan migrates `cbl-reactnative` from the legacy React Native bridge to **8 domain-specific Turbo Modules**. Instead of one monolithic native module with 54+ methods, the library is decomposed into 8 focused modules — each with its own TypeScript codegen spec, codegen-generated native bindings, and clear domain boundary. + +**What this delivers:** +- **Complete removal of the legacy bridge** — no backward compatibility layer, no interop mode, no old-arch fallback. The entire legacy bridge (`NativeModules`, `RCT_EXTERN_METHOD`, `ReactContextBaseJavaModule`, `@ReactMethod`, `RCTEventEmitter`, `ReactPackage`) is deleted. +- **8 independently validated TypeScript specs** — one per domain, each passing `tsc --noEmit` with zero errors +- **Codegen-generated native bindings** — static type safety across the JS ↔ native boundary +- **JSI-based calls** — no JSON serialization overhead, no async bridge queue +- **Lazy module loading** — each domain module loads on first access, not at startup +- **Lifecycle-scoped coroutines** — replaces deprecated `GlobalScope` on Android +- **New arch only** — minimum supported React Native version becomes 0.76+ + +> **This is a one-way migration.** After completion, the library will only work with React Native's New Architecture. The legacy bridge code is fully removed, not conditionally guarded. + +**The 8 Domains:** + +| # | Module Name | Spec File | Methods | Events | +|---|---|---|---|---| +| 1 | `CblDatabase` | `src/NativeCblDatabase.ts` | 9 | — | +| 2 | `CblCollection` | `src/NativeCblCollection.ts` | 13 | `collectionChange`, `collectionDocumentChange` | +| 3 | `CblDocument` | `src/NativeCblDocument.ts` | 7 | — | +| 4 | `CblQuery` | `src/NativeCblQuery.ts` | 4 | `queryChange` | +| 5 | `CblReplicator` | `src/NativeCblReplicator.ts` | 11 | `replicatorStatusChange`, `replicatorDocumentChange` | +| 6 | `CblScope` | `src/NativeCblScope.ts` | 3 | — | +| 7 | `CblLogging` | `src/NativeCblLogging.ts` | 5 | `customLogMessage` | +| 8 | `CblEngine` | `src/NativeCblEngine.ts` | 2 | — | + +**Totals:** 54 async domain methods + 8 synchronous event-emitter infrastructure methods (on 4 modules) = **62 total method signatures**. + +--- + +## 2. Architecture + +### 2.1 Current Architecture (Single Monolithic Module) + +```mermaid +graph TD + A["JS Layer
NativeModules.CblReactnative
NativeEventEmitter(nativeModule)"] --> B["Obj-C Bridge
ios/CblReactnative.mm
RCT_EXTERN_MODULE / RCT_EXTERN_METHOD"] + B --> C["Swift — 1 class
CblReactnative : RCTEventEmitter
54 methods"] + C --> D["Couchbase Lite Swift SDK 3.3.0"] + + A --> E["Kotlin — 1 class
CblReactnativeModule : ReactContextBaseJavaModule
54 @ReactMethod"] + E --> F["Couchbase Lite Android SDK 3.3.0"] +``` + +### 2.2 Target Architecture (8 Domain Modules) + +```mermaid +graph TD + subgraph "TypeScript Codegen Specs" + S1["NativeCblDatabase.ts"] + S2["NativeCblCollection.ts"] + S3["NativeCblDocument.ts"] + S4["NativeCblQuery.ts"] + S5["NativeCblReplicator.ts"] + S6["NativeCblScope.ts"] + S7["NativeCblLogging.ts"] + S8["NativeCblEngine.ts"] + end + + subgraph "JS Engine Layer" + E["CblReactNativeEngine.tsx
imports all 8 modules"] + end + + E --> S1 & S2 & S3 & S4 & S5 & S6 & S7 & S8 + + S1 & S2 & S3 & S4 & S5 & S6 & S7 & S8 --> JSI["JSI / Codegen"] + + JSI --> iOSAdapter["8 Obj-C++ Adapters
conforming to codegen protocols
(RCTCblDatabase.mm, etc.)"] + iOSAdapter --> iOSSwift["8 Swift Impl Classes
@objcMembers business logic
(CblDatabaseModule.swift, etc.)"] + iOSSwift --> CBLiOS["Couchbase Lite Swift SDK 3.3.0"] + + JSI --> Android["8 Kotlin classes
extending codegen abstract classes"] + Android --> CBLAndroid["Couchbase Lite Android SDK 3.3.0"] +``` + +--- + +## 3. Domain Decomposition + +### 3.1 Rationale + +The current codebase already has domain-separated manager singletons on the native side (`DatabaseManager`, `CollectionManager`, `ReplicatorManager`, `LoggingManager`, `LogSinksManager`, `FileSystemHelper`). The 8-module split aligns the TypeScript specs with these existing native managers, making the native implementation a thin delegation layer. + +### 3.2 Domain Boundaries + +| Domain | Responsibility | Native Manager(s) Used | +|---|---|---| +| **Database** | Database lifecycle: open, close, delete, copy, exists, path, maintenance, encryption | `DatabaseManager` | +| **Collection** | Collection CRUD, count, fullName, default collection, index CRUD, collection/document change listeners | `CollectionManager`, `DatabaseManager` | +| **Document** | Document CRUD within collections, blob content, document expiration | `CollectionManager` | +| **Query** | SQL++ query execution, explain, query change listeners | `DatabaseManager`, `QueryHelper` | +| **Replicator** | Replicator lifecycle, status, pending docs, checkpoint, status/document change listeners | `ReplicatorManager`, `ReplicatorHelper`, `CollectionManager` | +| **Scope** | Scope discovery and access | `DatabaseManager` | +| **Logging** | Legacy log level/file config + new LogSinks API (console, file, custom with events) | `LoggingManager`, `LogSinksManager` | +| **Engine** | Cross-cutting: file system default path, generic listener token removal | `FileSystemHelper` | + +### 3.3 Event Distribution + +Four of the 8 modules emit native events. Each event-emitting module includes `addListener`/`removeListeners` in its spec (required by React Native's event emitter protocol). On the JS side, `NativeEventEmitter()` is instantiated without arguments (RN 0.76+ pattern) — all events flow through the global device event emitter regardless of which native module emits them. + +| Module | Event Names | +|---|---| +| `CblCollection` | `collectionChange`, `collectionDocumentChange` | +| `CblQuery` | `queryChange` | +| `CblReplicator` | `replicatorStatusChange`, `replicatorDocumentChange` | +| `CblLogging` | `customLogMessage` | + +--- + +## 4. Codebase Audit Findings + +> Everything in this section must be addressed. The legacy bridge is being **fully removed** — not conditionally guarded or kept behind a flag. Items are grouped by category; the cleanup work is scheduled as the final migration phase (Phase 8). + +### 4.1 Stale/Dead C Library References + +| Location | Issue | Action | +|---|---|---| +| `package.json` line 50 | `"cpp"` listed in the `files` array — no `cpp/` directory exists in the repo | Remove from `files` | +| `.npmignore` line 6 | `cpp/**/*.dSYM` — references removed C library debug symbols | Remove line | +| `.npmignore` line 7 | `cpp/**/dSYMs` — references removed C library debug symbol folders | Remove line | +| `.npmignore` line 8 | `cpp/**/*.yml` — references removed C library CI config | Remove line | +| `cbl-reactnative.podspec` line 4 | `folly_compiler_flags` variable defined but only used inside the legacy new-arch guard | Delete variable | +| `cbl-reactnative.podspec` lines 29–41 | `if ENV['RCT_NEW_ARCH_ENABLED'] == '1'` block with stale Folly, RCT-Folly, RCTRequired, RCTTypeSafety, and ReactCommon dependencies | Delete entire conditional block; replace with `install_modules_dependencies(s)` | + +### 4.2 Missing `codegenConfig` in `package.json` + +`package.json` has **no** `codegenConfig` block. This is required for the React Native codegen to discover the TypeScript specs and generate native bindings. See Section 6.1 for the exact block to add, including `ios.modulesProvider` mappings. + +### 4.3 Legacy Bridge APIs — Complete Inventory (All Must Be Removed) + +| File | Legacy API | Occurrences | Line References | +|---|---|---|---| +| `src/CblReactNativeEngine.tsx` | `NativeModules` import | 1 | line 4 | +| `src/CblReactNativeEngine.tsx` | `NativeEventEmitter` import (used with module arg) | 1 | line 3 | +| `src/CblReactNativeEngine.tsx` | `NativeModules.CblReactnative` access with Proxy `LINKING_ERROR` fallback | 1 | lines 113–122 | +| `src/CblReactNativeEngine.tsx` | `new NativeEventEmitter(this.CblReactNative)` — passing module to constructor | 1 | line 133 | +| `ios/CblReactnative.mm` | `RCT_EXTERN_MODULE(CblReactnative, RCTEventEmitter)` | 1 | line 4 | +| `ios/CblReactnative.mm` | `RCT_EXTERN_METHOD` declarations | **47** | lines 6–388 (entire file) | +| `ios/CblReactnative.mm` | `#import ` | 1 | line 1 | +| `ios/CblReactnative.mm` | `#import ` | 1 | line 2 | +| `ios/CblReactnative.swift` | `class CblReactnative: RCTEventEmitter` | 1 | line 41 | +| `ios/CblReactnative.swift` | `override func supportedEvents()` | 1 | lines 101–108 | +| `ios/CblReactnative.swift` | `sendEvent(withName:body:)` call sites | **6** | lines 159, 215, 1277, 1408, 1442, 1867 | +| `ios/CblReactnative.swift` | `override func startObserving()` / `stopObserving()` | 2 | lines 83–89 | +| `ios/CblReactnative.swift` | `@objc override static func moduleName()` | 1 | lines 110–112 | +| `ios/CblReactnative.swift` | `@objc(...)` selector annotations on every method | ~52 | throughout file | +| `ios/CblReactnative-Bridging-Header.h` | `#import ` | 1 | line 1 | +| `ios/CblReactnative-Bridging-Header.h` | `#import ` | 1 | line 2 | +| `ios/CblReactnative-Bridging-Header.h` | `#import ` | 1 | line 3 | +| `android/.../CblReactnativeModule.kt` | `ReactContextBaseJavaModule` import and extends | 2 | lines 14, 59 | +| `android/.../CblReactnativeModule.kt` | `@ReactMethod` annotations | **48** | throughout file | +| `android/.../CblReactnativeModule.kt` | `@OptIn(DelicateCoroutinesApi::class)` | 1 | line 56 | +| `android/.../CblReactnativeModule.kt` | `GlobalScope.launch(Dispatchers.IO)` | **48+** | throughout file | +| `android/.../CblReactnativeModule.kt` | `sendEvent` helper using `DeviceEventManagerModule.RCTDeviceEventEmitter.emit()` | 1 | lines 94–101 | +| `android/.../CblReactnativeModule.kt` | `override fun getName(): String` | 1 | lines 77–79 | +| `android/.../CblReactnativePackage.kt` | `ReactPackage` import and extends | 2 | lines 3, 9 | +| `android/.../CblReactnativePackage.kt` | `createNativeModules` / `createViewManagers` | 2 | methods | + +### 4.4 Dead Obj-C Bridge File — `ios/CblReactnative.mm` (Full Method Inventory) + +This file will be **deleted entirely**. It declares the following methods via `RCT_EXTERN_METHOD` (47 declarations, 54 unique methods when counting the duplicate): + +**Collection (22 declarations):** +1. `collection_AddChangeListener` +2. `collection_AddDocumentChangeListener` +3. `collection_RemoveChangeListener` +4. `collection_CreateCollection` +5. `collection_CreateIndex` +6. `collection_DeleteCollection` +7. `collection_DeleteDocument` +8. `collection_DeleteIndex` +9. `collection_GetBlobContent` +10. `collection_GetDocument` (**declared twice** — lines 83–89 and 124–130, duplicate bug) +11. `collection_GetCollection` +12. `collection_GetCollections` +13. `collection_GetCount` +14. `collection_GetFullName` +15. `collection_GetDefault` +16. `collection_GetDocumentExpiration` +17. `collection_GetIndexes` +18. `collection_PurgeDocument` +19. `collection_Save` +20. `collection_SetDocumentExpiration` + +**Database (11):** +21. `database_ChangeEncryptionKey` +22. `database_Close` +23. `database_Copy` +24. `database_Delete` +25. `database_DeleteWithPath` +26. `database_Exists` +27. `database_GetPath` +28. `database_Open` +29. `database_PerformMaintenance` +30. `database_SetFileLoggingConfig` +31. `database_SetLogLevel` + +**File System (1):** +32. `file_GetDefaultPath` + +**Listener (1):** +33. `listenerToken_Remove` + +**LogSinks (3):** +34. `logsinks_SetConsole` +35. `logsinks_SetFile` +36. `logsinks_SetCustom` + +**Query (4):** +37. `query_AddChangeListener` +38. `query_RemoveChangeListener` +39. `query_Execute` +40. `query_Explain` + +**Replicator (9):** +41. `replicator_AddChangeListener` +42. `replicator_AddDocumentChangeListener` +43. `replicator_Cleanup` +44. `replicator_Create` +45. `replicator_GetPendingDocumentIds` +46. `replicator_GetStatus` +47. `replicator_IsDocumentPending` +48. `replicator_RemoveChangeListener` +49. `replicator_ResetCheckpoint` +50. `replicator_Start` +51. `replicator_Stop` + +**Scope (3):** +52. `scope_GetDefault` +53. `scope_GetScope` +54. `scope_GetScopes` + +### 4.5 Platform Method Inconsistencies + +Comparing every `RCT_EXTERN_METHOD` in `ios/CblReactnative.mm` against every `@ReactMethod` in `CblReactnativeModule.kt`: + +| Method | iOS (.mm) | Android (.kt) | Issue | +|---|---|---|---| +| `collection_GetDocument` | Declared **twice** in `.mm` (lines 83–89 and 124–130) | Declared once | iOS has a duplicate extern declaration — bug | +| `database_SetFileLoggingConfig` | Parameter `shouldUsePlainText` is `BOOL` | Parameter `shouldUsePlainText` is `Boolean` | Types match across platform boundaries — OK | +| `database_PerformMaintenance` | iOS: `maintenanceType: NSNumber, databaseName: NSString` | Android: `maintenanceType: Double, databaseName: String` | Parameter order matches, types are platform-appropriate — OK | +| `addListener` / `removeListeners` | Not declared in `.mm` (inherited from `RCTEventEmitter`) | Explicitly declared as `@ReactMethod` in `.kt` (lines 83–92) | Android has explicit event emitter methods; iOS inherits them. Both must appear in codegen specs for event-emitting modules. | + +**All other methods match in name, parameter count, and parameter order across both platforms.** + +### 4.6 Event Names Cross-Platform Consistency + +| Event Name | iOS (`CblReactnative.swift`) | Android (`CblReactnativeModule.kt`) | JS (`CblReactNativeEngine.tsx`) | +|---|---|---|---| +| `collectionChange` | line 94 `kCollectionChange` | line 699 literal | line 82 `_eventCollectionChange` | +| `collectionDocumentChange` | line 95 `kCollectionDocumentChange` | line 810 literal | line 83 `_eventCollectionDocumentChange` | +| `queryChange` | line 96 `kQueryChange` | line 1239 literal | line 84 `_eventQueryChange` | +| `replicatorStatusChange` | line 97 `kReplicatorStatusChange` | line 1288 literal | line 79 `_eventReplicatorStatusChange` | +| `replicatorDocumentChange` | line 98 `kReplicatorDocumentChange` | line 1325 literal | line 80 `_eventReplicatorDocumentChange` | +| `customLogMessage` | line 99 `kCustomLogMessage` | line 1720 literal | line 138 literal in constructor | + +All 6 names are identical across all platforms. ✅ + +### 4.7 Deprecated Android Coroutine Pattern + +`CblReactnativeModule.kt` uses `@OptIn(DelicateCoroutinesApi::class)` (line 56) and **every single `@ReactMethod`** wraps its body in `GlobalScope.launch(Dispatchers.IO)`. This is deprecated because: + +- `GlobalScope` has no lifecycle — coroutines launched on it leak if the module is destroyed +- The `@OptIn(DelicateCoroutinesApi::class)` annotation is a static acknowledgment that the pattern is intentionally fragile + +**Count:** 48+ occurrences of `GlobalScope.launch(Dispatchers.IO)` across the entire file. + +**Replacement per module:** `CoroutineScope(SupervisorJob() + Dispatchers.IO)` cancelled in `invalidate()`. + +### 4.8 `JavaScriptFilterEvaluator.kt` — Special Attention + +`android/src/main/java/com/cblreactnative/cbl-js-kotlin/JavaScriptFilterEvaluator.kt` embeds a **J2V8 JavaScript engine** inside native code to evaluate replication push/pull filter functions. + +Key details: +- Uses `ThreadLocal` for thread safety (line 14) +- Called from `ReplicatorHelper`/`ReplicatorManager` during replicator configuration setup, within `Dispatchers.IO` coroutine blocks +- The V8 runtime is independent of the Hermes/JSC runtime — this is **not** using the app's JS engine +- The `j2v8:6.2.1@aar` dependency (`android/build.gradle` line 111) is required solely for this evaluator +- **Risk:** Thread-local V8 instances may behave differently if Turbo Module methods are invoked from different threads than the legacy bridge. Since the evaluator runs inside `Dispatchers.IO`, the threading model should be unchanged. +- **No API changes required** during migration. **Must be tested** under new arch. + +### 4.9 `EngineLocator` Registration + +`EngineLocator.registerEngine(EngineLocator.key, this)` is called in the `CblReactNativeEngine` constructor (`src/CblReactNativeEngine.tsx` line 127). + +- The engine class name is `CblReactNativeEngine` +- The registration key is the static string `'default'` +- This call remains unchanged — the class name does not change +- The `EngineLocator` itself (`src/cblite-js/cblite/src/engine-locator.ts`) has no native bridge dependencies and requires no modification + +### 4.10 `uuid-fix.sh` Workaround + +`uuid-fix.sh` copies `src/cblite-js/cblite/src/util/uuid-rn.ts` to `uuid.ts` and renames `uuid-ionic.ts` to `uuid-ionic.txt` at build time. This is a workaround for the multi-platform shared `cblite-js` codebase (shared between React Native and Ionic). + +The Turbo Module migration does not change how TypeScript modules are resolved, so **this workaround remains necessary** unless the shared codebase adopts `tsconfig` path aliases or package.json conditional `exports`. + +### 4.11 `create-react-native-library.type` + +`package.json` line 197: `"type": "module-legacy"`. Must change to `"module-new"` to signal to `react-native-builder-bob` and the RN codegen that this library supports the new architecture natively. + +### 4.12 Expo Config Plugin — Legacy Assumptions + +`expo-example/cbl-reactnative-plugin.js` (line 9) appends `apply from: "../../android/build.gradle"` to the Expo app's `build.gradle`. Details: + +- The `android/build.gradle` already conditionally applies `com.facebook.react` plugin (lines 30–32): `if (isNewArchitectureEnabled()) { apply plugin: "com.facebook.react" }` +- After migration, this plugin must still apply, and the Gradle plugin `com.facebook.react` must be active +- The plugin must be audited to ensure it doesn't duplicate the `apply from:` line or conflict with Expo's own new-arch Gradle wiring +- The `modifyXcodeProject` function (lines 17–22) is currently a no-op — no changes needed +- The `includeNativeModulePod` function (lines 25–35) is defined but **not called** — dead code that should be removed + +### 4.13 `newArchEnabled` Flags + +| Location | Current Value | Required Value | +|---|---|---| +| `expo-example/android/gradle.properties` line 38 | `newArchEnabled=false` | `newArchEnabled=true` | +| `expo-example/app.json` | **Missing** | Add `"newArchEnabled": true` inside the `"expo"` object | + +### 4.14 Partially Migrated Pieces + +**No existing Turbo Module artifacts were found.** A search for `TurboModule`, `TurboModuleRegistry`, `NativeCblReactnative`, `requireNativeModule`, and `codegenConfig` returned zero results in source files. The migration starts from scratch. + +### 4.15 Other Code Quality Concerns + +| Location | Issue | Action | +|---|---|---| +| `android/build.gradle` line 107 | `implementation "com.facebook.react:react-native:+"` uses dynamic version | Verify this is managed by the `com.facebook.react` Gradle plugin after migration | +| `ios/CblReactnative.mm` lines 124–130 | Duplicate `collection_GetDocument` `RCT_EXTERN_METHOD` | File is deleted entirely — no separate fix needed | +| `expo-example/cbl-reactnative-plugin.js` lines 25–35 | Dead `includeNativeModulePod` function (defined but never called) | Remove dead code | + +--- + +## 5. The 8 TypeScript Module Specs + +> **Codegen best practices applied:** +> - All domain operations are `async` (`Promise`) +> - `addListener`/`removeListeners` remain synchronous (`void`) as required by RN event emitter protocol +> - Nullable parameters use `Type | null` (codegen-supported nullable syntax) +> - `Object` for generic dictionary/map returns; `string`/`boolean`/`number` for primitives +> - `string[]` for typed arrays +> - `import type` for tree-shaking; value import for `TurboModuleRegistry` +> - Every spec file is self-contained and independently passes `tsc --noEmit` + +### 5.1 Database — `src/NativeCblDatabase.ts` + +```typescript +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'); +``` + +**Method count: 9 async** + +--- + +### 5.2 Collection — `src/NativeCblCollection.ts` + +```typescript +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 CRUD + 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; + + // Index operations + 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; + + // Change listeners + 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'); +``` + +**Method count: 13 async + 2 event emitter = 15 total** + +--- + +### 5.3 Document — `src/NativeCblDocument.ts` + +```typescript +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, + docId: string, + name: string, + scopeName: string, + collectionName: string + ): Promise; +} + +export default TurboModuleRegistry.getEnforcing('CblDocument'); +``` + +**Method count: 7 async** + +--- + +### 5.4 Query — `src/NativeCblQuery.ts` + +```typescript +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'); +``` + +**Method count: 4 async + 2 event emitter = 6 total** + +--- + +### 5.5 Replicator — `src/NativeCblReplicator.ts` + +```typescript +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'); +``` + +**Method count: 11 async + 2 event emitter = 13 total** + +--- + +### 5.6 Scope — `src/NativeCblScope.ts` + +```typescript +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'); +``` + +**Method count: 3 async** + +--- + +### 5.7 Logging — `src/NativeCblLogging.ts` + +```typescript +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'); +``` + +**Method count: 5 async + 2 event emitter = 7 total** + +--- + +### 5.8 Engine — `src/NativeCblEngine.ts` + +```typescript +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'); +``` + +**Method count: 2 async** + +--- + +## 6. Codegen Configuration + +### 6.1 `package.json` Changes + +Add the following top-level block to `package.json`: + +```json +"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" + } + } +} +``` + +The `ios.modulesProvider` maps each JS module name (from `TurboModuleRegistry.getEnforcing('CblDatabase')`) to its Obj-C++ adapter class name (`RCTCblDatabase`). This replaces the old `RCT_EXPORT_MODULE` macro — module registration is now declarative via `package.json`. + +The codegen scans `src/` for all files matching `Native*.ts` and generates per-module bindings. + +Also change the `create-react-native-library` block: + +```json +"create-react-native-library": { + "type": "module-new", + "languages": "kotlin-swift", + "version": "0.38.1" +} +``` + +### 6.2 Cleanup in `package.json` + +Remove `"cpp"` from the `files` array. + +### 6.3 Cleanup in `.npmignore` + +Remove the three `cpp/**` entries (lines 6–8). + +### 6.4 Podspec Update — `cbl-reactnative.podspec` + +Remove the `folly_compiler_flags` variable and the entire legacy conditional. Replace lines 4 and 21–42 with: + +```ruby +install_modules_dependencies(s) +``` + +This single call handles all codegen, Folly, and React Native dependencies for RN 0.71+. + +--- + +## 7. Async / Promise Architecture (How Codegen Handles Async) + +In the **legacy bridge**, developers manually declared `RCTPromiseResolveBlock` / `RCTPromiseRejectBlock` (iOS) and `com.facebook.react.bridge.Promise` (Android) parameters in every async method. The Turbo Module codegen **eliminates this manual wiring entirely**. + +### 7.1 TypeScript Spec → Codegen → Native + +When you declare a method as `Promise` in the TypeScript spec: + +```typescript +database_Open(name: string, directory: string | null, encryptionKey: string | null): Promise; +``` + +The codegen automatically generates the correct native signature on each platform: + +**iOS (Obj-C++ protocol method):** + +```objc +- (void)database_Open:(NSString *)name + directory:(NSString * _Nullable)directory + encryptionKey:(NSString * _Nullable)encryptionKey + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject; +``` + +**Android (abstract method in generated spec):** + +```kotlin +abstract fun database_Open(name: String, directory: String?, encryptionKey: String?, promise: Promise) +``` + +> **You never write `RCTPromiseResolveBlock` or `@ReactMethod` yourself.** The codegen produces these from your TypeScript `Promise` declaration. Your native code simply calls `resolve(result)` or `reject(error)` on the provided objects. + +### 7.2 Why Every Database Operation Must Be Async + +All Couchbase Lite operations involve disk I/O. In the codegen architecture: + +1. The **JS thread** calls the Turbo Module method via JSI +2. The codegen plumbing creates a `Promise` and returns it to JS immediately +3. **Native code dispatches to a background thread** to do the actual work: + - iOS: `DispatchQueue` (existing `backgroundQueue` pattern) + - Android: `CoroutineScope(SupervisorJob() + Dispatchers.IO)` (replaces deprecated `GlobalScope`) +4. When the operation completes, native calls `resolve(result)` or `reject(error)` +5. The JS `Promise` settles, and `await` in the calling code resumes + +This is identical in behavior to the old bridge async pattern, but without the JSON serialization overhead — parameters pass through JSI as C++ values. + +### 7.3 Synchronous vs Async in the Spec + +| Spec Return Type | Codegen Output | Use Case | +|---|---|---| +| `Promise` | `resolve`/`reject` blocks (iOS) / `Promise` param (Android) | All I/O, network, database operations | +| `void` | No promise — synchronous void call | Event emitter infra (`addListener`, `removeListeners`) | +| `string`, `number`, `boolean` | Synchronous return on JSI thread | Only for trivial, non-blocking lookups (not used in this library) | + +**In this library, all 54 domain operations return `Promise`.** Only the 8 event-emitter infrastructure methods (`addListener`/`removeListeners`) are synchronous `void`. + +### 7.4 Official Codegen Type Mapping (from RN 0.84 Appendix) + +| TypeScript | Android (Kotlin/Java) | iOS (Obj-C) | +|---|---|---| +| `string` | `String` | `NSString` | +| `boolean` | `Boolean` | `NSNumber` (BOOL) | +| `number` | `double` | `NSNumber` | +| `Object` | `ReadableMap` | `NSDictionary` (untyped) | +| `string[]` | `ReadableArray` | `NSArray` | +| `string \| null` | `String?` | `NSString * _Nullable` | +| `Promise` | `Promise` param | `RCTPromiseResolveBlock` + `RCTPromiseRejectBlock` | + +--- + +## 8. JavaScript Layer Adaptation + +### 8.1 `src/CblReactNativeEngine.tsx` Changes + +The engine class currently uses a single `NativeModules.CblReactnative` reference. After migration, it imports all 8 modules: + +```typescript +// BEFORE +import { NativeEventEmitter, NativeModules, Platform } from 'react-native'; + +// AFTER +import { EmitterSubscription, NativeEventEmitter, Platform } from 'react-native'; +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'; +``` + +**Key changes:** + +1. **Remove** the `NativeModules.CblReactnative` accessor and the `LINKING_ERROR` Proxy fallback entirely. `TurboModuleRegistry.getEnforcing()` in each spec throws a clear error if the module is not linked. + +2. **Replace** `new NativeEventEmitter(this.CblReactNative)` with `new NativeEventEmitter()` (no argument). In RN 0.76+ new arch, all device events flow through a global emitter regardless of which native module emits them. A single `NativeEventEmitter()` instance handles all 6 event types. + +3. **Route each method call** to the appropriate module: + + ```typescript + // Database methods → NativeCblDatabase + database_Open(args) { return NativeCblDatabase.database_Open(args.name, args.config.directory, args.config.encryptionKey); } + + // Collection methods → NativeCblCollection + collection_CreateCollection(args) { return NativeCblCollection.collection_CreateCollection(args.collectionName, args.name, args.scopeName); } + + // Document methods → NativeCblDocument + collection_GetDocument(args) { return NativeCblDocument.collection_GetDocument(args.docId, args.name, args.scopeName, args.collectionName); } + + // Query methods → NativeCblQuery + query_Execute(args) { return NativeCblQuery.query_Execute(args.query, args.parameters, args.name); } + + // Replicator methods → NativeCblReplicator + replicator_Create(args) { return NativeCblReplicator.replicator_Create(args.config); } + + // Scope methods → NativeCblScope + scope_GetDefault(args) { return NativeCblScope.scope_GetDefault(args.name); } + + // Logging methods → NativeCblLogging + logsinks_SetConsole(args) { return NativeCblLogging.logsinks_SetConsole(args.level, args.domains); } + + // Engine methods → NativeCblEngine + file_GetDefaultPath() { return NativeCblEngine.file_GetDefaultPath(); } + listenerToken_Remove(args) { return NativeCblEngine.listenerToken_Remove(args.changeListenerToken); } + ``` + +4. **`EngineLocator.registerEngine`** call remains unchanged (line 127). + +5. **Event name constants** remain unchanged — they already match native event names. + +--- + +## 9. Native Implementation Guide + +Each of the 8 specs generates a native protocol (iOS) or abstract class (Android) via codegen. The native implementation classes delegate to existing manager singletons. + +### 9.1 iOS — Adapter Pattern (Required) + +Swift **cannot** directly conform to codegen-generated protocols because codegen produces Objective-C++ headers containing C++ code that Swift cannot import. The official React Native pattern (RN 0.84 docs) is the **Adapter pattern**: + +1. **Swift implementation class** — `@objcMembers public class` inheriting from `NSObject`, containing all business logic +2. **Obj-C++ adapter** (`.h` + `.mm`) — conforms to the codegen protocol, creates and holds a reference to the Swift class, forwards every method call to it + +For each of the 8 modules, you need 3 files: + +| Spec | Obj-C++ Adapter (.h + .mm) | Swift Implementation | Delegates To | +|---|---|---|---| +| `NativeCblDatabase.ts` | `RCTCblDatabase.h` / `.mm` | `CblDatabaseModule.swift` | `DatabaseManager.shared` | +| `NativeCblCollection.ts` | `RCTCblCollection.h` / `.mm` | `CblCollectionModule.swift` | `CollectionManager.shared`, `DatabaseManager.shared` | +| `NativeCblDocument.ts` | `RCTCblDocument.h` / `.mm` | `CblDocumentModule.swift` | `CollectionManager.shared` | +| `NativeCblQuery.ts` | `RCTCblQuery.h` / `.mm` | `CblQueryModule.swift` | `DatabaseManager.shared`, `QueryHelper` | +| `NativeCblReplicator.ts` | `RCTCblReplicator.h` / `.mm` | `CblReplicatorModule.swift` | `ReplicatorManager.shared`, `CollectionManager.shared` | +| `NativeCblScope.ts` | `RCTCblScope.h` / `.mm` | `CblScopeModule.swift` | `DatabaseManager.shared` | +| `NativeCblLogging.ts` | `RCTCblLogging.h` / `.mm` | `CblLoggingModule.swift` | `LoggingManager.shared`, `LogSinksManager.shared` | +| `NativeCblEngine.ts` | `RCTCblEngine.h` / `.mm` | `CblEngineModule.swift` | `FileSystemHelper` | + +**Example — Database module Obj-C++ adapter:** + +```objc +// RCTCblDatabase.h +#import +#import + +@interface RCTCblDatabase : NSObject +@end +``` + +```objc +// RCTCblDatabase.mm +#import "RCTCblDatabase.h" +#import "CblReactnative-Swift.h" // Auto-generated header exposing Swift to ObjC + +@implementation RCTCblDatabase { + CblDatabaseModule *_impl; +} + +- (id)init { + if (self = [super init]) { + _impl = [CblDatabaseModule new]; + } + return self; +} + +- (std::shared_ptr)getTurboModule: + (const facebook::react::ObjCTurboModule::InitParams &)params { + return std::make_shared(params); +} + ++ (NSString *)moduleName { return @"CblDatabase"; } + +- (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]; +} +// ... forward all other methods to _impl ... +@end +``` + +**Example — Database module Swift implementation:** + +```swift +// CblDatabaseModule.swift +import Foundation + +@objcMembers public class CblDatabaseModule: NSObject { + private let backgroundQueue = DispatchQueue(label: "CblDatabaseModule", qos: .userInitiated) + + public func database_Open( + name: String, + directory: String?, + encryptionKey: String?, + resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) { + backgroundQueue.async { + do { + let result = try DatabaseManager.shared.openDatabase(name, directory: directory, encryptionKey: encryptionKey) + resolve(result) + } catch { + reject("DATABASE_ERROR", error.localizedDescription, error) + } + } + } + // ... all other database methods follow the same async dispatch pattern ... +} +``` + +> **Key point:** The `resolve`/`reject` blocks are provided automatically by codegen from the `Promise` declaration in the TypeScript spec. You never import or declare `RCTPromiseResolveBlock` yourself — the codegen header supplies it. + +The old `ios/CblReactnative.swift` and `ios/CblReactnative.mm` are **deleted**. + +### 9.2 Android — Module Class Structure + +| Spec | Native Class | Codegen Base Class | Delegates To | +|---|---|---|---| +| `NativeCblDatabase.ts` | `CblDatabaseModule.kt` | `NativeCblDatabaseSpec` | `DatabaseManager` | +| `NativeCblCollection.ts` | `CblCollectionModule.kt` | `NativeCblCollectionSpec` | `CollectionManager`, `DatabaseManager` | +| `NativeCblDocument.ts` | `CblDocumentModule.kt` | `NativeCblDocumentSpec` | `CollectionManager` | +| `NativeCblQuery.ts` | `CblQueryModule.kt` | `NativeCblQuerySpec` | `DatabaseManager` | +| `NativeCblReplicator.ts` | `CblReplicatorModule.kt` | `NativeCblReplicatorSpec` | `ReplicatorManager`, `CollectionManager` | +| `NativeCblScope.ts` | `CblScopeModule.kt` | `NativeCblScopeSpec` | `DatabaseManager` | +| `NativeCblLogging.ts` | `CblLoggingModule.kt` | `NativeCblLoggingSpec` | `LoggingManager`, `LogSinksManager` | +| `NativeCblEngine.ts` | `CblEngineModule.kt` | `NativeCblEngineSpec` | `FileSystemHelper` | + +Each Kotlin module class: +- Extends the codegen-generated abstract class (e.g., `NativeCblDatabaseSpec(reactContext)`) +- The codegen-generated abstract class provides a `Promise` parameter for each `Promise` method — you call `promise.resolve(result)` or `promise.reject(error)` instead of manually declaring `@ReactMethod` with Promise +- Uses `CoroutineScope(SupervisorJob() + Dispatchers.IO)` instead of deprecated `GlobalScope` +- Cancels the scope in `invalidate()` +- Has **no** `@ReactMethod` annotations — codegen provides all method signatures + +**Example — Database module Kotlin implementation:** + +```kotlin +class CblDatabaseModule(reactContext: ReactApplicationContext) : + NativeCblDatabaseSpec(reactContext) { + + private val moduleScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + + override fun getName() = NAME + + override fun invalidate() { + moduleScope.cancel() + super.invalidate() + } + + override fun database_Open(name: String, directory: String?, encryptionKey: String?, promise: Promise) { + moduleScope.launch { + try { + val result = DatabaseManager.openDatabase(name, directory, encryptionKey, context) + promise.resolve(result) + } catch (e: Exception) { + promise.reject("DATABASE_ERROR", e.message, e) + } + } + } + // ... all other database methods follow the same pattern ... + + companion object { + const val NAME = "CblDatabase" + } +} +``` + +> **Key point:** The `promise: Promise` parameter is auto-generated by codegen from the `Promise` in the TypeScript spec. You never write `@ReactMethod` annotations. The codegen abstract class defines the method signatures for you. + +The old `CblReactnativeModule.kt` is **deleted**. `CblReactnativePackage.kt` is updated to register all 8 modules. + +### 9.3 Android Package Registration + +The latest React Native (0.77+/0.84) uses `BaseReactPackage` (not the deprecated `TurboReactPackage`): + +```kotlin +import com.facebook.react.BaseReactPackage +import com.facebook.react.bridge.NativeModule +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.module.model.ReactModuleInfo +import com.facebook.react.module.model.ReactModuleInfoProvider + +class CblReactnativePackage : BaseReactPackage() { + override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? { + return when (name) { + CblDatabaseModule.NAME -> CblDatabaseModule(reactContext) + CblCollectionModule.NAME -> CblCollectionModule(reactContext) + CblDocumentModule.NAME -> CblDocumentModule(reactContext) + CblQueryModule.NAME -> CblQueryModule(reactContext) + CblReplicatorModule.NAME -> CblReplicatorModule(reactContext) + CblScopeModule.NAME -> CblScopeModule(reactContext) + CblLoggingModule.NAME -> CblLoggingModule(reactContext) + CblEngineModule.NAME -> CblEngineModule(reactContext) + else -> null + } + } + + override fun getReactModuleInfoProvider() = ReactModuleInfoProvider { + mapOf( + CblDatabaseModule.NAME to ReactModuleInfo( + name = CblDatabaseModule.NAME, className = CblDatabaseModule.NAME, + canOverrideExistingModule = false, needsEagerInit = false, + isCxxModule = false, isTurboModule = true + ), + CblCollectionModule.NAME to ReactModuleInfo( + name = CblCollectionModule.NAME, className = CblCollectionModule.NAME, + canOverrideExistingModule = false, needsEagerInit = false, + isCxxModule = false, isTurboModule = true + ), + CblDocumentModule.NAME to ReactModuleInfo( + name = CblDocumentModule.NAME, className = CblDocumentModule.NAME, + canOverrideExistingModule = false, needsEagerInit = false, + isCxxModule = false, isTurboModule = true + ), + CblQueryModule.NAME to ReactModuleInfo( + name = CblQueryModule.NAME, className = CblQueryModule.NAME, + canOverrideExistingModule = false, needsEagerInit = false, + isCxxModule = false, isTurboModule = true + ), + CblReplicatorModule.NAME to ReactModuleInfo( + name = CblReplicatorModule.NAME, className = CblReplicatorModule.NAME, + canOverrideExistingModule = false, needsEagerInit = false, + isCxxModule = false, isTurboModule = true + ), + CblScopeModule.NAME to ReactModuleInfo( + name = CblScopeModule.NAME, className = CblScopeModule.NAME, + canOverrideExistingModule = false, needsEagerInit = false, + isCxxModule = false, isTurboModule = true + ), + CblLoggingModule.NAME to ReactModuleInfo( + name = CblLoggingModule.NAME, className = CblLoggingModule.NAME, + canOverrideExistingModule = false, needsEagerInit = false, + isCxxModule = false, isTurboModule = true + ), + CblEngineModule.NAME to ReactModuleInfo( + name = CblEngineModule.NAME, className = CblEngineModule.NAME, + canOverrideExistingModule = false, needsEagerInit = false, + isCxxModule = false, isTurboModule = true + ), + ) + } +} +``` + +### 9.4 Shared Listener State + +The current codebase stores all listener tokens in a single `allChangeListenerTokenByUuid` dictionary on the monolithic module. After splitting into 8 modules: + +- **Collection** module owns collection/document change listener tokens +- **Query** module owns query change listener tokens +- **Replicator** module owns replicator status/document change listener tokens +- **Engine** module's `listenerToken_Remove` needs access to all listener stores + +**Solution:** Extract listener storage into a shared singleton (`ListenerTokenStore`) accessible by all modules. Alternatively, `listenerToken_Remove` on the Engine module can delegate removal to each domain module until one succeeds. + +--- + +## 10. Validation & Exit Criteria + +### 10.1 TypeScript Validation + +Each of the 8 spec files must independently pass: + +```bash +npx tsc --noEmit src/NativeCblDatabase.ts +npx tsc --noEmit src/NativeCblCollection.ts +npx tsc --noEmit src/NativeCblDocument.ts +npx tsc --noEmit src/NativeCblQuery.ts +npx tsc --noEmit src/NativeCblReplicator.ts +npx tsc --noEmit src/NativeCblScope.ts +npx tsc --noEmit src/NativeCblLogging.ts +npx tsc --noEmit src/NativeCblEngine.ts +``` + +And the full project must pass: + +```bash +npx tsc --noEmit +npx eslint "**/*.{js,ts,tsx}" +``` + +### 10.2 Codegen Validation + +```bash +yarn react-native codegen +``` + +Must produce generated files for all 8 specs: +- iOS: 8 `Native*Spec.h` protocol headers + 8 `Native*SpecJSI` C++ implementations (generated during `pod install`) +- Android: 8 `Native*Spec.java` abstract classes in `android/build/generated/source/codegen/` + +### 10.3 Build Validation + +```bash +# iOS +cd expo-example/ios && RCT_NEW_ARCH_ENABLED=1 pod install && xcodebuild -workspace ExpoExample.xcworkspace -scheme expo-example -configuration Debug -sdk iphonesimulator + +# Android +cd expo-example/android && ./gradlew assembleDebug -PnewArchEnabled=true +``` + +### 10.4 Exit Criteria Checklist + +- [ ] All 8 TypeScript spec files exist in `src/` +- [ ] `npx tsc --noEmit` passes with zero errors on all 8 specs +- [ ] `npx eslint` passes with zero errors on all 8 specs +- [ ] `codegenConfig` is present in `package.json` +- [ ] `create-react-native-library.type` is `module-new` +- [ ] Codegen generates native bindings for all 8 modules +- [ ] All 54 async domain methods are covered across the 8 specs +- [ ] All 4 event-emitting modules include `addListener`/`removeListeners` +- [ ] No spec uses deprecated patterns (`NativeModules`, `UnsafeObject`, synchronous domain methods) +- [ ] Parameter types match native implementations on both platforms + +--- + +## 11. Migration Phases + +### Phase 1 — Write the 8 TypeScript Specs (THIS TICKET) + +Create all 8 spec files as defined in Section 5. Add `codegenConfig` to `package.json` (Section 6.1). Change `"type": "module-legacy"` to `"module-new"`. Validate per Section 10.1. + +### Phase 2 — Update JavaScript Engine Layer + +Update `src/CblReactNativeEngine.tsx` per Section 8.1: +- Import all 8 modules +- Remove `NativeModules` and Proxy fallback +- Replace `NativeEventEmitter(module)` with `NativeEventEmitter()` +- Route each method to the correct domain module + +### Phase 3 — Implement Native Modules (iOS — Adapter Pattern) + +- Create 8 Obj-C++ adapter classes (`.h` + `.mm`) conforming to codegen protocols per Section 9.1 +- Create 8 Swift `@objcMembers` implementation classes delegating to existing managers +- Each adapter implements `getTurboModule:` returning the codegen JSI instance +- Codegen auto-generates `resolve`/`reject` blocks for all `Promise` methods +- Delete `ios/CblReactnative.mm` and old `ios/CblReactnative.swift` +- Update bridging header and podspec + +### Phase 4 — Implement Native Modules (Android) + +- Create 8 Kotlin module classes extending codegen-generated specs per Section 9.2 +- Codegen auto-generates `promise: Promise` param for all `Promise` methods — call `promise.resolve()`/`promise.reject()` in each method +- Replace `GlobalScope` with `CoroutineScope(SupervisorJob() + Dispatchers.IO)` per module +- Delete old `CblReactnativeModule.kt` +- Update `CblReactnativePackage.kt` to `BaseReactPackage` per Section 9.3 +- Extract shared listener storage per Section 9.4 + +### Phase 5 — Expo Example App Updates + +- `expo-example/android/gradle.properties`: `newArchEnabled=true` +- `expo-example/app.json`: add `"newArchEnabled": true` +- Audit `expo-example/cbl-reactnative-plugin.js` for new-arch compatibility +- Run `npx expo prebuild --clean` + +### Phase 6 — Build & Integration Testing + +- Run codegen, verify generated files for all 8 modules +- Build both platforms with new arch enabled +- Run all integration tests under `expo-example/cblite-js-tests/` +- Verify all 6 event types emit correctly +- Verify replication filters (`JavaScriptFilterEvaluator.kt`) work under new arch + +### Phase 7 — Full Legacy Removal & Cleanup + +**All legacy bridge code is removed. No backward compatibility is maintained.** This is the final sweep addressing every issue catalogued in Section 4. + +**Stale/dead code removal (Section 4.1):** +1. Remove `"cpp"` from `package.json` `files` array (line 50) +2. Remove the three `cpp/**` entries from `.npmignore` (lines 6–8) +3. Remove `folly_compiler_flags` variable from `cbl-reactnative.podspec` (line 4) +4. Remove the entire `if ENV['RCT_NEW_ARCH_ENABLED'] == '1'` conditional block from `cbl-reactnative.podspec` (lines 29–41); replace with `install_modules_dependencies(s)` + +**Legacy bridge API removal verification (Section 4.3):** + +Run the following grep to confirm zero legacy patterns remain in non-test source files: +```bash +rg -l "NativeModules|RCT_EXTERN_MODULE|RCT_EXTERN_METHOD|ReactContextBaseJavaModule|@ReactMethod|RCTEventEmitter|RCTBridgeModule|ReactPackage|TurboReactPackage|GlobalScope" --glob '!**/__tests__/**' --glob '!**/node_modules/**' src/ ios/ android/ +``` +This must return **zero results**. + +**Expo config plugin cleanup (Section 4.12):** +- Remove dead `includeNativeModulePod` function from `expo-example/cbl-reactnative-plugin.js` +- Verify `modifyAndroidBuildGradle` still works with new arch Gradle setup + +**Other cleanup (Section 4.15):** +- Verify `android/build.gradle` dependency `com.facebook.react:react-native:+` is properly managed by the `com.facebook.react` Gradle plugin +- Run `npx eslint "**/*.{js,ts,tsx}"` — zero errors +- Run `npx tsc --noEmit` — zero errors +- Confirm `lefthook.yml` pre-commit hooks pass on all new/modified files + +**Documentation updates:** +- Update `README.md`: remove any legacy bridge setup instructions, add new-arch-only setup instructions, document minimum RN version 0.76+ +- Update `CHANGELOG.md`: add entry for Turbo Module migration and legacy bridge removal + +--- + +## 12. File Change Matrix + +### TypeScript Specs (8 files created) + +| File Path | Action | Summary | +|---|---|---| +| `src/NativeCblDatabase.ts` | **Create** | Database domain spec — 9 async methods | +| `src/NativeCblCollection.ts` | **Create** | Collection domain spec — 13 async + 2 event emitter methods | +| `src/NativeCblDocument.ts` | **Create** | Document domain spec — 7 async methods | +| `src/NativeCblQuery.ts` | **Create** | Query domain spec — 4 async + 2 event emitter methods | +| `src/NativeCblReplicator.ts` | **Create** | Replicator domain spec — 11 async + 2 event emitter methods | +| `src/NativeCblScope.ts` | **Create** | Scope domain spec — 3 async methods | +| `src/NativeCblLogging.ts` | **Create** | Logging domain spec — 5 async + 2 event emitter methods | +| `src/NativeCblEngine.ts` | **Create** | Engine domain spec — 2 async methods | + +### Configuration & JS Layer + +| File Path | Action | Summary | +|---|---|---| +| `package.json` | Modify | Remove `cpp` from `files`, add `codegenConfig` (incl. `ios.modulesProvider`), change type to `module-new` | +| `.npmignore` | Modify | Remove `cpp/**` entries | +| `cbl-reactnative.podspec` | Modify | Remove Folly flags; use `install_modules_dependencies(s)` unconditionally | +| `src/CblReactNativeEngine.tsx` | Modify | Import 8 modules; remove `NativeModules`; route methods per domain | + +### iOS Native (8 Obj-C++ adapters + 8 Swift impls + 2 deleted) + +| File Path | Action | Summary | +|---|---|---| +| `ios/CblReactnative.mm` | **Delete** | Old Obj-C extern bridge (RCT_EXTERN_MODULE / RCT_EXTERN_METHOD) | +| `ios/CblReactnative.swift` | **Delete** | Old monolithic Swift class (RCTEventEmitter subclass) | +| `ios/RCTCblDatabase.h` + `.mm` | **Create** | Obj-C++ adapter conforming to `NativeCblDatabaseSpec`, forwards to Swift | +| `ios/RCTCblCollection.h` + `.mm` | **Create** | Obj-C++ adapter conforming to `NativeCblCollectionSpec` | +| `ios/RCTCblDocument.h` + `.mm` | **Create** | Obj-C++ adapter conforming to `NativeCblDocumentSpec` | +| `ios/RCTCblQuery.h` + `.mm` | **Create** | Obj-C++ adapter conforming to `NativeCblQuerySpec` | +| `ios/RCTCblReplicator.h` + `.mm` | **Create** | Obj-C++ adapter conforming to `NativeCblReplicatorSpec` | +| `ios/RCTCblScope.h` + `.mm` | **Create** | Obj-C++ adapter conforming to `NativeCblScopeSpec` | +| `ios/RCTCblLogging.h` + `.mm` | **Create** | Obj-C++ adapter conforming to `NativeCblLoggingSpec` | +| `ios/RCTCblEngine.h` + `.mm` | **Create** | Obj-C++ adapter conforming to `NativeCblEngineSpec` | +| `ios/CblDatabaseModule.swift` | **Create** | Swift impl — `@objcMembers`, delegates to `DatabaseManager` | +| `ios/CblCollectionModule.swift` | **Create** | Swift impl — delegates to `CollectionManager` | +| `ios/CblDocumentModule.swift` | **Create** | Swift impl — delegates to `CollectionManager` | +| `ios/CblQueryModule.swift` | **Create** | Swift impl — delegates to `DatabaseManager` / `QueryHelper` | +| `ios/CblReplicatorModule.swift` | **Create** | Swift impl — delegates to `ReplicatorManager` | +| `ios/CblScopeModule.swift` | **Create** | Swift impl — delegates to `DatabaseManager` | +| `ios/CblLoggingModule.swift` | **Create** | Swift impl — delegates to `LoggingManager` / `LogSinksManager` | +| `ios/CblEngineModule.swift` | **Create** | Swift impl — delegates to `FileSystemHelper` | +| `ios/CblReactnative-Bridging-Header.h` | Modify | Remove legacy imports; add codegen header import | + +### Android Native (8 Kotlin modules + 1 deleted + 1 modified) + +| File Path | Action | Summary | +|---|---|---| +| `android/.../CblReactnativeModule.kt` | **Delete** | Old monolithic Kotlin module | +| `android/.../CblDatabaseModule.kt` | **Create** | Extends `NativeCblDatabaseSpec`; `CoroutineScope` + `promise.resolve()` | +| `android/.../CblCollectionModule.kt` | **Create** | Extends `NativeCblCollectionSpec` | +| `android/.../CblDocumentModule.kt` | **Create** | Extends `NativeCblDocumentSpec` | +| `android/.../CblQueryModule.kt` | **Create** | Extends `NativeCblQuerySpec` | +| `android/.../CblReplicatorModule.kt` | **Create** | Extends `NativeCblReplicatorSpec` | +| `android/.../CblScopeModule.kt` | **Create** | Extends `NativeCblScopeSpec` | +| `android/.../CblLoggingModule.kt` | **Create** | Extends `NativeCblLoggingSpec` | +| `android/.../CblEngineModule.kt` | **Create** | Extends `NativeCblEngineSpec` | +| `android/.../CblReactnativePackage.kt` | Modify | `BaseReactPackage` registering all 8 modules | +| `android/build.gradle` | Verify | Confirm `com.facebook.react` plugin applied | + +### Expo Example App + +| File Path | Action | Summary | +|---|---|---| +| `expo-example/android/gradle.properties` | Modify | `newArchEnabled=true` | +| `expo-example/app.json` | Modify | Add `"newArchEnabled": true` | + +--- + +## 13. Risks and Mitigations + +| Risk | Severity | Mitigation | +|---|---|---| +| **No backward compat with legacy bridge** — apps on RN < 0.76 or old arch cannot use this library after migration | **High** | This is intentional. The legacy bridge is fully removed. Minimum supported RN version becomes 0.76+. Existing users on older RN versions must either upgrade RN or pin to the last pre-migration library release. Document this as a **breaking change** in `CHANGELOG.md`. | +| 8 native modules = more module registration complexity | Medium | `BaseReactPackage.getModule()` switch is well-established official pattern. Each module is independently testable. | +| iOS Adapter pattern = 3 files per module (24 iOS files total) | Medium | Boilerplate is mechanical; the Obj-C++ adapters are thin forwarding layers. Existing manager classes are reused as-is. | +| Shared listener state across modules (`listenerToken_Remove`) | Medium | Extract into a `ListenerTokenStore` singleton shared across all native modules. | +| `GlobalScope` replacement may cancel in-flight operations | Low | Use `SupervisorJob()` so individual coroutine failures don't cascade. Cancel in `invalidate()`. | +| `JavaScriptFilterEvaluator.kt` J2V8 threading under JSI | Low | Evaluator runs on `Dispatchers.IO`, decoupled from JSI thread. Must be tested. | +| Expo config plugin conflicts with new-arch Gradle setup | Medium | Test `npx expo prebuild --clean` early. The plugin appends `apply from:` which should be idempotent. | +| Couchbase Lite SDK pinned to 3.3.0 | None | SDK version is orthogonal to bridge architecture. | + +--- + +## 14. Definition of Done + +**Spec Writing (Phase 1 — this ticket):** + +- [ ] All 8 TypeScript spec files created in `src/` +- [ ] Every spec file passes `npx tsc --noEmit` with zero errors +- [ ] Every spec file passes `npx eslint` with zero errors +- [ ] All 54 async domain methods are represented across the 8 specs +- [ ] All 4 event-emitting modules include `addListener`/`removeListeners` +- [ ] `codegenConfig` (with `ios.modulesProvider`) added to `package.json` +- [ ] `create-react-native-library.type` changed to `module-new` + +**Full Migration (all phases):** + +- [ ] No `NativeModules`, `NativeEventEmitter(module)`, `RCT_EXTERN_MODULE`, `RCT_EXTERN_METHOD`, `ReactContextBaseJavaModule`, `@ReactMethod`, `ReactPackage`, `RCTEventEmitter`, `TurboReactPackage` remain in any non-test file +- [ ] 8 Obj-C++ adapter classes + 8 Swift impl classes created on iOS (Adapter pattern) +- [ ] 8 Kotlin module classes created on Android, each extending codegen-generated spec +- [ ] `ios/CblReactnative.mm` and `ios/CblReactnative.swift` deleted +- [ ] `android/.../CblReactnativeModule.kt` deleted +- [ ] Android uses `BaseReactPackage` (not deprecated `TurboReactPackage`) +- [ ] `GlobalScope` replaced with lifecycle-scoped `CoroutineScope` in every Android module +- [ ] All async methods use codegen-generated `Promise`/`resolve`/`reject` (no manual `RCTPromiseResolveBlock` declarations) +- [ ] `ios.modulesProvider` in `codegenConfig` maps all 8 JS module names to Obj-C++ adapter class names +- [ ] `newArchEnabled=true` in `expo-example/android/gradle.properties` and `expo-example/app.json` +- [ ] Both platforms build successfully with new arch enabled +- [ ] All existing integration tests pass +- [ ] All 6 event types emit correctly on both platforms +- [ ] `tsc --noEmit` passes with zero errors +- [ ] `eslint` passes with zero errors + +**Legacy Cleanup (Phase 7):** + +- [ ] `cpp` removed from `package.json` `files` array +- [ ] `cpp/**` entries removed from `.npmignore` +- [ ] `folly_compiler_flags` and legacy `RCT_NEW_ARCH_ENABLED` conditional removed from `cbl-reactnative.podspec` +- [ ] Dead `includeNativeModulePod` function removed from `expo-example/cbl-reactnative-plugin.js` +- [ ] `rg` search for legacy patterns returns zero results in source files +- [ ] `README.md` updated — legacy setup instructions removed, new-arch-only docs added, minimum RN 0.76+ documented +- [ ] `CHANGELOG.md` updated — breaking change documented 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/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/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'); From 96cd80f1bb0ab93c5aa7a266c461d0e3e5913fd8 Mon Sep 17 00:00:00 2001 From: jayant dhingra Date: Tue, 7 Apr 2026 22:49:33 +0530 Subject: [PATCH 2/3] feat(ios): split CBL native bridge into Turbo modules - Replace monolithic CblReactnative with RCTCblModules and Swift modules - Add per-domain modules (database, collection, document, query, etc.) - Preserve legacy implementation under legacy_* files; wrap JS engine - Update bridging header for new module surface Made-with: Cursor --- ios/CblCollectionModule.swift | 424 +++++ ios/CblDatabaseModule.swift | 234 +++ ios/CblDocumentModule.swift | 317 ++++ ios/CblEngineModule.swift | 35 + ios/CblLoggingModule.swift | 140 ++ ios/CblNativeQueue.swift | 8 + ios/CblQueryModule.swift | 157 ++ ios/CblReactnative-Bridging-Header.h | 2 - ios/CblReplicatorModule.swift | 303 +++ ios/CblScopeModule.swift | 95 + ios/ListenerTokenStore.swift | 47 + ios/RCTCblModules.h | 17 + ios/RCTCblModules.mm | 645 +++++++ ios/legacy_CblReactnative-Bridging-Header.h | 28 + ...eactnative.mm => legacy_CblReactnative.mm} | 18 + ...tive.swift => legacy_CblReactnative.swift} | 21 + src/CblReactNativeEngine.tsx | 1529 ++++++++------- src/legacy_CblReactNativeEngine.tsx | 1648 +++++++++++++++++ 18 files changed, 4896 insertions(+), 772 deletions(-) create mode 100644 ios/CblCollectionModule.swift create mode 100644 ios/CblDatabaseModule.swift create mode 100644 ios/CblDocumentModule.swift create mode 100644 ios/CblEngineModule.swift create mode 100644 ios/CblLoggingModule.swift create mode 100644 ios/CblNativeQueue.swift create mode 100644 ios/CblQueryModule.swift create mode 100644 ios/CblReplicatorModule.swift create mode 100644 ios/CblScopeModule.swift create mode 100644 ios/ListenerTokenStore.swift create mode 100644 ios/RCTCblModules.h create mode 100644 ios/RCTCblModules.mm create mode 100644 ios/legacy_CblReactnative-Bridging-Header.h rename ios/{CblReactnative.mm => legacy_CblReactnative.mm} (95%) rename ios/{CblReactnative.swift => legacy_CblReactnative.swift} (99%) create mode 100644 src/legacy_CblReactNativeEngine.tsx 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/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/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 +// ); +// } +// } From 85f48b2d7951f5715ba6021322596bcd1c3cc1c4 Mon Sep 17 00:00:00 2001 From: jayant dhingra Date: Tue, 7 Apr 2026 22:54:53 +0530 Subject: [PATCH 3/3] chore(git): stop tracking TURBO_MIGRATION_PLAN.md Remove the migration plan from version control and list it in .gitignore so it can stay local. Made-with: Cursor --- .gitignore | 5 +- TURBO_MIGRATION_PLAN.md | 1456 --------------------------------------- 2 files changed, 4 insertions(+), 1457 deletions(-) delete mode 100644 TURBO_MIGRATION_PLAN.md 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/TURBO_MIGRATION_PLAN.md b/TURBO_MIGRATION_PLAN.md deleted file mode 100644 index dfbddf7..0000000 --- a/TURBO_MIGRATION_PLAN.md +++ /dev/null @@ -1,1456 +0,0 @@ -# Turbo Module Migration Plan — cbl-reactnative - -## 1. Executive Summary - -This plan migrates `cbl-reactnative` from the legacy React Native bridge to **8 domain-specific Turbo Modules**. Instead of one monolithic native module with 54+ methods, the library is decomposed into 8 focused modules — each with its own TypeScript codegen spec, codegen-generated native bindings, and clear domain boundary. - -**What this delivers:** -- **Complete removal of the legacy bridge** — no backward compatibility layer, no interop mode, no old-arch fallback. The entire legacy bridge (`NativeModules`, `RCT_EXTERN_METHOD`, `ReactContextBaseJavaModule`, `@ReactMethod`, `RCTEventEmitter`, `ReactPackage`) is deleted. -- **8 independently validated TypeScript specs** — one per domain, each passing `tsc --noEmit` with zero errors -- **Codegen-generated native bindings** — static type safety across the JS ↔ native boundary -- **JSI-based calls** — no JSON serialization overhead, no async bridge queue -- **Lazy module loading** — each domain module loads on first access, not at startup -- **Lifecycle-scoped coroutines** — replaces deprecated `GlobalScope` on Android -- **New arch only** — minimum supported React Native version becomes 0.76+ - -> **This is a one-way migration.** After completion, the library will only work with React Native's New Architecture. The legacy bridge code is fully removed, not conditionally guarded. - -**The 8 Domains:** - -| # | Module Name | Spec File | Methods | Events | -|---|---|---|---|---| -| 1 | `CblDatabase` | `src/NativeCblDatabase.ts` | 9 | — | -| 2 | `CblCollection` | `src/NativeCblCollection.ts` | 13 | `collectionChange`, `collectionDocumentChange` | -| 3 | `CblDocument` | `src/NativeCblDocument.ts` | 7 | — | -| 4 | `CblQuery` | `src/NativeCblQuery.ts` | 4 | `queryChange` | -| 5 | `CblReplicator` | `src/NativeCblReplicator.ts` | 11 | `replicatorStatusChange`, `replicatorDocumentChange` | -| 6 | `CblScope` | `src/NativeCblScope.ts` | 3 | — | -| 7 | `CblLogging` | `src/NativeCblLogging.ts` | 5 | `customLogMessage` | -| 8 | `CblEngine` | `src/NativeCblEngine.ts` | 2 | — | - -**Totals:** 54 async domain methods + 8 synchronous event-emitter infrastructure methods (on 4 modules) = **62 total method signatures**. - ---- - -## 2. Architecture - -### 2.1 Current Architecture (Single Monolithic Module) - -```mermaid -graph TD - A["JS Layer
NativeModules.CblReactnative
NativeEventEmitter(nativeModule)"] --> B["Obj-C Bridge
ios/CblReactnative.mm
RCT_EXTERN_MODULE / RCT_EXTERN_METHOD"] - B --> C["Swift — 1 class
CblReactnative : RCTEventEmitter
54 methods"] - C --> D["Couchbase Lite Swift SDK 3.3.0"] - - A --> E["Kotlin — 1 class
CblReactnativeModule : ReactContextBaseJavaModule
54 @ReactMethod"] - E --> F["Couchbase Lite Android SDK 3.3.0"] -``` - -### 2.2 Target Architecture (8 Domain Modules) - -```mermaid -graph TD - subgraph "TypeScript Codegen Specs" - S1["NativeCblDatabase.ts"] - S2["NativeCblCollection.ts"] - S3["NativeCblDocument.ts"] - S4["NativeCblQuery.ts"] - S5["NativeCblReplicator.ts"] - S6["NativeCblScope.ts"] - S7["NativeCblLogging.ts"] - S8["NativeCblEngine.ts"] - end - - subgraph "JS Engine Layer" - E["CblReactNativeEngine.tsx
imports all 8 modules"] - end - - E --> S1 & S2 & S3 & S4 & S5 & S6 & S7 & S8 - - S1 & S2 & S3 & S4 & S5 & S6 & S7 & S8 --> JSI["JSI / Codegen"] - - JSI --> iOSAdapter["8 Obj-C++ Adapters
conforming to codegen protocols
(RCTCblDatabase.mm, etc.)"] - iOSAdapter --> iOSSwift["8 Swift Impl Classes
@objcMembers business logic
(CblDatabaseModule.swift, etc.)"] - iOSSwift --> CBLiOS["Couchbase Lite Swift SDK 3.3.0"] - - JSI --> Android["8 Kotlin classes
extending codegen abstract classes"] - Android --> CBLAndroid["Couchbase Lite Android SDK 3.3.0"] -``` - ---- - -## 3. Domain Decomposition - -### 3.1 Rationale - -The current codebase already has domain-separated manager singletons on the native side (`DatabaseManager`, `CollectionManager`, `ReplicatorManager`, `LoggingManager`, `LogSinksManager`, `FileSystemHelper`). The 8-module split aligns the TypeScript specs with these existing native managers, making the native implementation a thin delegation layer. - -### 3.2 Domain Boundaries - -| Domain | Responsibility | Native Manager(s) Used | -|---|---|---| -| **Database** | Database lifecycle: open, close, delete, copy, exists, path, maintenance, encryption | `DatabaseManager` | -| **Collection** | Collection CRUD, count, fullName, default collection, index CRUD, collection/document change listeners | `CollectionManager`, `DatabaseManager` | -| **Document** | Document CRUD within collections, blob content, document expiration | `CollectionManager` | -| **Query** | SQL++ query execution, explain, query change listeners | `DatabaseManager`, `QueryHelper` | -| **Replicator** | Replicator lifecycle, status, pending docs, checkpoint, status/document change listeners | `ReplicatorManager`, `ReplicatorHelper`, `CollectionManager` | -| **Scope** | Scope discovery and access | `DatabaseManager` | -| **Logging** | Legacy log level/file config + new LogSinks API (console, file, custom with events) | `LoggingManager`, `LogSinksManager` | -| **Engine** | Cross-cutting: file system default path, generic listener token removal | `FileSystemHelper` | - -### 3.3 Event Distribution - -Four of the 8 modules emit native events. Each event-emitting module includes `addListener`/`removeListeners` in its spec (required by React Native's event emitter protocol). On the JS side, `NativeEventEmitter()` is instantiated without arguments (RN 0.76+ pattern) — all events flow through the global device event emitter regardless of which native module emits them. - -| Module | Event Names | -|---|---| -| `CblCollection` | `collectionChange`, `collectionDocumentChange` | -| `CblQuery` | `queryChange` | -| `CblReplicator` | `replicatorStatusChange`, `replicatorDocumentChange` | -| `CblLogging` | `customLogMessage` | - ---- - -## 4. Codebase Audit Findings - -> Everything in this section must be addressed. The legacy bridge is being **fully removed** — not conditionally guarded or kept behind a flag. Items are grouped by category; the cleanup work is scheduled as the final migration phase (Phase 8). - -### 4.1 Stale/Dead C Library References - -| Location | Issue | Action | -|---|---|---| -| `package.json` line 50 | `"cpp"` listed in the `files` array — no `cpp/` directory exists in the repo | Remove from `files` | -| `.npmignore` line 6 | `cpp/**/*.dSYM` — references removed C library debug symbols | Remove line | -| `.npmignore` line 7 | `cpp/**/dSYMs` — references removed C library debug symbol folders | Remove line | -| `.npmignore` line 8 | `cpp/**/*.yml` — references removed C library CI config | Remove line | -| `cbl-reactnative.podspec` line 4 | `folly_compiler_flags` variable defined but only used inside the legacy new-arch guard | Delete variable | -| `cbl-reactnative.podspec` lines 29–41 | `if ENV['RCT_NEW_ARCH_ENABLED'] == '1'` block with stale Folly, RCT-Folly, RCTRequired, RCTTypeSafety, and ReactCommon dependencies | Delete entire conditional block; replace with `install_modules_dependencies(s)` | - -### 4.2 Missing `codegenConfig` in `package.json` - -`package.json` has **no** `codegenConfig` block. This is required for the React Native codegen to discover the TypeScript specs and generate native bindings. See Section 6.1 for the exact block to add, including `ios.modulesProvider` mappings. - -### 4.3 Legacy Bridge APIs — Complete Inventory (All Must Be Removed) - -| File | Legacy API | Occurrences | Line References | -|---|---|---|---| -| `src/CblReactNativeEngine.tsx` | `NativeModules` import | 1 | line 4 | -| `src/CblReactNativeEngine.tsx` | `NativeEventEmitter` import (used with module arg) | 1 | line 3 | -| `src/CblReactNativeEngine.tsx` | `NativeModules.CblReactnative` access with Proxy `LINKING_ERROR` fallback | 1 | lines 113–122 | -| `src/CblReactNativeEngine.tsx` | `new NativeEventEmitter(this.CblReactNative)` — passing module to constructor | 1 | line 133 | -| `ios/CblReactnative.mm` | `RCT_EXTERN_MODULE(CblReactnative, RCTEventEmitter)` | 1 | line 4 | -| `ios/CblReactnative.mm` | `RCT_EXTERN_METHOD` declarations | **47** | lines 6–388 (entire file) | -| `ios/CblReactnative.mm` | `#import ` | 1 | line 1 | -| `ios/CblReactnative.mm` | `#import ` | 1 | line 2 | -| `ios/CblReactnative.swift` | `class CblReactnative: RCTEventEmitter` | 1 | line 41 | -| `ios/CblReactnative.swift` | `override func supportedEvents()` | 1 | lines 101–108 | -| `ios/CblReactnative.swift` | `sendEvent(withName:body:)` call sites | **6** | lines 159, 215, 1277, 1408, 1442, 1867 | -| `ios/CblReactnative.swift` | `override func startObserving()` / `stopObserving()` | 2 | lines 83–89 | -| `ios/CblReactnative.swift` | `@objc override static func moduleName()` | 1 | lines 110–112 | -| `ios/CblReactnative.swift` | `@objc(...)` selector annotations on every method | ~52 | throughout file | -| `ios/CblReactnative-Bridging-Header.h` | `#import ` | 1 | line 1 | -| `ios/CblReactnative-Bridging-Header.h` | `#import ` | 1 | line 2 | -| `ios/CblReactnative-Bridging-Header.h` | `#import ` | 1 | line 3 | -| `android/.../CblReactnativeModule.kt` | `ReactContextBaseJavaModule` import and extends | 2 | lines 14, 59 | -| `android/.../CblReactnativeModule.kt` | `@ReactMethod` annotations | **48** | throughout file | -| `android/.../CblReactnativeModule.kt` | `@OptIn(DelicateCoroutinesApi::class)` | 1 | line 56 | -| `android/.../CblReactnativeModule.kt` | `GlobalScope.launch(Dispatchers.IO)` | **48+** | throughout file | -| `android/.../CblReactnativeModule.kt` | `sendEvent` helper using `DeviceEventManagerModule.RCTDeviceEventEmitter.emit()` | 1 | lines 94–101 | -| `android/.../CblReactnativeModule.kt` | `override fun getName(): String` | 1 | lines 77–79 | -| `android/.../CblReactnativePackage.kt` | `ReactPackage` import and extends | 2 | lines 3, 9 | -| `android/.../CblReactnativePackage.kt` | `createNativeModules` / `createViewManagers` | 2 | methods | - -### 4.4 Dead Obj-C Bridge File — `ios/CblReactnative.mm` (Full Method Inventory) - -This file will be **deleted entirely**. It declares the following methods via `RCT_EXTERN_METHOD` (47 declarations, 54 unique methods when counting the duplicate): - -**Collection (22 declarations):** -1. `collection_AddChangeListener` -2. `collection_AddDocumentChangeListener` -3. `collection_RemoveChangeListener` -4. `collection_CreateCollection` -5. `collection_CreateIndex` -6. `collection_DeleteCollection` -7. `collection_DeleteDocument` -8. `collection_DeleteIndex` -9. `collection_GetBlobContent` -10. `collection_GetDocument` (**declared twice** — lines 83–89 and 124–130, duplicate bug) -11. `collection_GetCollection` -12. `collection_GetCollections` -13. `collection_GetCount` -14. `collection_GetFullName` -15. `collection_GetDefault` -16. `collection_GetDocumentExpiration` -17. `collection_GetIndexes` -18. `collection_PurgeDocument` -19. `collection_Save` -20. `collection_SetDocumentExpiration` - -**Database (11):** -21. `database_ChangeEncryptionKey` -22. `database_Close` -23. `database_Copy` -24. `database_Delete` -25. `database_DeleteWithPath` -26. `database_Exists` -27. `database_GetPath` -28. `database_Open` -29. `database_PerformMaintenance` -30. `database_SetFileLoggingConfig` -31. `database_SetLogLevel` - -**File System (1):** -32. `file_GetDefaultPath` - -**Listener (1):** -33. `listenerToken_Remove` - -**LogSinks (3):** -34. `logsinks_SetConsole` -35. `logsinks_SetFile` -36. `logsinks_SetCustom` - -**Query (4):** -37. `query_AddChangeListener` -38. `query_RemoveChangeListener` -39. `query_Execute` -40. `query_Explain` - -**Replicator (9):** -41. `replicator_AddChangeListener` -42. `replicator_AddDocumentChangeListener` -43. `replicator_Cleanup` -44. `replicator_Create` -45. `replicator_GetPendingDocumentIds` -46. `replicator_GetStatus` -47. `replicator_IsDocumentPending` -48. `replicator_RemoveChangeListener` -49. `replicator_ResetCheckpoint` -50. `replicator_Start` -51. `replicator_Stop` - -**Scope (3):** -52. `scope_GetDefault` -53. `scope_GetScope` -54. `scope_GetScopes` - -### 4.5 Platform Method Inconsistencies - -Comparing every `RCT_EXTERN_METHOD` in `ios/CblReactnative.mm` against every `@ReactMethod` in `CblReactnativeModule.kt`: - -| Method | iOS (.mm) | Android (.kt) | Issue | -|---|---|---|---| -| `collection_GetDocument` | Declared **twice** in `.mm` (lines 83–89 and 124–130) | Declared once | iOS has a duplicate extern declaration — bug | -| `database_SetFileLoggingConfig` | Parameter `shouldUsePlainText` is `BOOL` | Parameter `shouldUsePlainText` is `Boolean` | Types match across platform boundaries — OK | -| `database_PerformMaintenance` | iOS: `maintenanceType: NSNumber, databaseName: NSString` | Android: `maintenanceType: Double, databaseName: String` | Parameter order matches, types are platform-appropriate — OK | -| `addListener` / `removeListeners` | Not declared in `.mm` (inherited from `RCTEventEmitter`) | Explicitly declared as `@ReactMethod` in `.kt` (lines 83–92) | Android has explicit event emitter methods; iOS inherits them. Both must appear in codegen specs for event-emitting modules. | - -**All other methods match in name, parameter count, and parameter order across both platforms.** - -### 4.6 Event Names Cross-Platform Consistency - -| Event Name | iOS (`CblReactnative.swift`) | Android (`CblReactnativeModule.kt`) | JS (`CblReactNativeEngine.tsx`) | -|---|---|---|---| -| `collectionChange` | line 94 `kCollectionChange` | line 699 literal | line 82 `_eventCollectionChange` | -| `collectionDocumentChange` | line 95 `kCollectionDocumentChange` | line 810 literal | line 83 `_eventCollectionDocumentChange` | -| `queryChange` | line 96 `kQueryChange` | line 1239 literal | line 84 `_eventQueryChange` | -| `replicatorStatusChange` | line 97 `kReplicatorStatusChange` | line 1288 literal | line 79 `_eventReplicatorStatusChange` | -| `replicatorDocumentChange` | line 98 `kReplicatorDocumentChange` | line 1325 literal | line 80 `_eventReplicatorDocumentChange` | -| `customLogMessage` | line 99 `kCustomLogMessage` | line 1720 literal | line 138 literal in constructor | - -All 6 names are identical across all platforms. ✅ - -### 4.7 Deprecated Android Coroutine Pattern - -`CblReactnativeModule.kt` uses `@OptIn(DelicateCoroutinesApi::class)` (line 56) and **every single `@ReactMethod`** wraps its body in `GlobalScope.launch(Dispatchers.IO)`. This is deprecated because: - -- `GlobalScope` has no lifecycle — coroutines launched on it leak if the module is destroyed -- The `@OptIn(DelicateCoroutinesApi::class)` annotation is a static acknowledgment that the pattern is intentionally fragile - -**Count:** 48+ occurrences of `GlobalScope.launch(Dispatchers.IO)` across the entire file. - -**Replacement per module:** `CoroutineScope(SupervisorJob() + Dispatchers.IO)` cancelled in `invalidate()`. - -### 4.8 `JavaScriptFilterEvaluator.kt` — Special Attention - -`android/src/main/java/com/cblreactnative/cbl-js-kotlin/JavaScriptFilterEvaluator.kt` embeds a **J2V8 JavaScript engine** inside native code to evaluate replication push/pull filter functions. - -Key details: -- Uses `ThreadLocal` for thread safety (line 14) -- Called from `ReplicatorHelper`/`ReplicatorManager` during replicator configuration setup, within `Dispatchers.IO` coroutine blocks -- The V8 runtime is independent of the Hermes/JSC runtime — this is **not** using the app's JS engine -- The `j2v8:6.2.1@aar` dependency (`android/build.gradle` line 111) is required solely for this evaluator -- **Risk:** Thread-local V8 instances may behave differently if Turbo Module methods are invoked from different threads than the legacy bridge. Since the evaluator runs inside `Dispatchers.IO`, the threading model should be unchanged. -- **No API changes required** during migration. **Must be tested** under new arch. - -### 4.9 `EngineLocator` Registration - -`EngineLocator.registerEngine(EngineLocator.key, this)` is called in the `CblReactNativeEngine` constructor (`src/CblReactNativeEngine.tsx` line 127). - -- The engine class name is `CblReactNativeEngine` -- The registration key is the static string `'default'` -- This call remains unchanged — the class name does not change -- The `EngineLocator` itself (`src/cblite-js/cblite/src/engine-locator.ts`) has no native bridge dependencies and requires no modification - -### 4.10 `uuid-fix.sh` Workaround - -`uuid-fix.sh` copies `src/cblite-js/cblite/src/util/uuid-rn.ts` to `uuid.ts` and renames `uuid-ionic.ts` to `uuid-ionic.txt` at build time. This is a workaround for the multi-platform shared `cblite-js` codebase (shared between React Native and Ionic). - -The Turbo Module migration does not change how TypeScript modules are resolved, so **this workaround remains necessary** unless the shared codebase adopts `tsconfig` path aliases or package.json conditional `exports`. - -### 4.11 `create-react-native-library.type` - -`package.json` line 197: `"type": "module-legacy"`. Must change to `"module-new"` to signal to `react-native-builder-bob` and the RN codegen that this library supports the new architecture natively. - -### 4.12 Expo Config Plugin — Legacy Assumptions - -`expo-example/cbl-reactnative-plugin.js` (line 9) appends `apply from: "../../android/build.gradle"` to the Expo app's `build.gradle`. Details: - -- The `android/build.gradle` already conditionally applies `com.facebook.react` plugin (lines 30–32): `if (isNewArchitectureEnabled()) { apply plugin: "com.facebook.react" }` -- After migration, this plugin must still apply, and the Gradle plugin `com.facebook.react` must be active -- The plugin must be audited to ensure it doesn't duplicate the `apply from:` line or conflict with Expo's own new-arch Gradle wiring -- The `modifyXcodeProject` function (lines 17–22) is currently a no-op — no changes needed -- The `includeNativeModulePod` function (lines 25–35) is defined but **not called** — dead code that should be removed - -### 4.13 `newArchEnabled` Flags - -| Location | Current Value | Required Value | -|---|---|---| -| `expo-example/android/gradle.properties` line 38 | `newArchEnabled=false` | `newArchEnabled=true` | -| `expo-example/app.json` | **Missing** | Add `"newArchEnabled": true` inside the `"expo"` object | - -### 4.14 Partially Migrated Pieces - -**No existing Turbo Module artifacts were found.** A search for `TurboModule`, `TurboModuleRegistry`, `NativeCblReactnative`, `requireNativeModule`, and `codegenConfig` returned zero results in source files. The migration starts from scratch. - -### 4.15 Other Code Quality Concerns - -| Location | Issue | Action | -|---|---|---| -| `android/build.gradle` line 107 | `implementation "com.facebook.react:react-native:+"` uses dynamic version | Verify this is managed by the `com.facebook.react` Gradle plugin after migration | -| `ios/CblReactnative.mm` lines 124–130 | Duplicate `collection_GetDocument` `RCT_EXTERN_METHOD` | File is deleted entirely — no separate fix needed | -| `expo-example/cbl-reactnative-plugin.js` lines 25–35 | Dead `includeNativeModulePod` function (defined but never called) | Remove dead code | - ---- - -## 5. The 8 TypeScript Module Specs - -> **Codegen best practices applied:** -> - All domain operations are `async` (`Promise`) -> - `addListener`/`removeListeners` remain synchronous (`void`) as required by RN event emitter protocol -> - Nullable parameters use `Type | null` (codegen-supported nullable syntax) -> - `Object` for generic dictionary/map returns; `string`/`boolean`/`number` for primitives -> - `string[]` for typed arrays -> - `import type` for tree-shaking; value import for `TurboModuleRegistry` -> - Every spec file is self-contained and independently passes `tsc --noEmit` - -### 5.1 Database — `src/NativeCblDatabase.ts` - -```typescript -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'); -``` - -**Method count: 9 async** - ---- - -### 5.2 Collection — `src/NativeCblCollection.ts` - -```typescript -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 CRUD - 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; - - // Index operations - 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; - - // Change listeners - 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'); -``` - -**Method count: 13 async + 2 event emitter = 15 total** - ---- - -### 5.3 Document — `src/NativeCblDocument.ts` - -```typescript -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, - docId: string, - name: string, - scopeName: string, - collectionName: string - ): Promise; -} - -export default TurboModuleRegistry.getEnforcing('CblDocument'); -``` - -**Method count: 7 async** - ---- - -### 5.4 Query — `src/NativeCblQuery.ts` - -```typescript -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'); -``` - -**Method count: 4 async + 2 event emitter = 6 total** - ---- - -### 5.5 Replicator — `src/NativeCblReplicator.ts` - -```typescript -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'); -``` - -**Method count: 11 async + 2 event emitter = 13 total** - ---- - -### 5.6 Scope — `src/NativeCblScope.ts` - -```typescript -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'); -``` - -**Method count: 3 async** - ---- - -### 5.7 Logging — `src/NativeCblLogging.ts` - -```typescript -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'); -``` - -**Method count: 5 async + 2 event emitter = 7 total** - ---- - -### 5.8 Engine — `src/NativeCblEngine.ts` - -```typescript -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'); -``` - -**Method count: 2 async** - ---- - -## 6. Codegen Configuration - -### 6.1 `package.json` Changes - -Add the following top-level block to `package.json`: - -```json -"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" - } - } -} -``` - -The `ios.modulesProvider` maps each JS module name (from `TurboModuleRegistry.getEnforcing('CblDatabase')`) to its Obj-C++ adapter class name (`RCTCblDatabase`). This replaces the old `RCT_EXPORT_MODULE` macro — module registration is now declarative via `package.json`. - -The codegen scans `src/` for all files matching `Native*.ts` and generates per-module bindings. - -Also change the `create-react-native-library` block: - -```json -"create-react-native-library": { - "type": "module-new", - "languages": "kotlin-swift", - "version": "0.38.1" -} -``` - -### 6.2 Cleanup in `package.json` - -Remove `"cpp"` from the `files` array. - -### 6.3 Cleanup in `.npmignore` - -Remove the three `cpp/**` entries (lines 6–8). - -### 6.4 Podspec Update — `cbl-reactnative.podspec` - -Remove the `folly_compiler_flags` variable and the entire legacy conditional. Replace lines 4 and 21–42 with: - -```ruby -install_modules_dependencies(s) -``` - -This single call handles all codegen, Folly, and React Native dependencies for RN 0.71+. - ---- - -## 7. Async / Promise Architecture (How Codegen Handles Async) - -In the **legacy bridge**, developers manually declared `RCTPromiseResolveBlock` / `RCTPromiseRejectBlock` (iOS) and `com.facebook.react.bridge.Promise` (Android) parameters in every async method. The Turbo Module codegen **eliminates this manual wiring entirely**. - -### 7.1 TypeScript Spec → Codegen → Native - -When you declare a method as `Promise` in the TypeScript spec: - -```typescript -database_Open(name: string, directory: string | null, encryptionKey: string | null): Promise; -``` - -The codegen automatically generates the correct native signature on each platform: - -**iOS (Obj-C++ protocol method):** - -```objc -- (void)database_Open:(NSString *)name - directory:(NSString * _Nullable)directory - encryptionKey:(NSString * _Nullable)encryptionKey - resolve:(RCTPromiseResolveBlock)resolve - reject:(RCTPromiseRejectBlock)reject; -``` - -**Android (abstract method in generated spec):** - -```kotlin -abstract fun database_Open(name: String, directory: String?, encryptionKey: String?, promise: Promise) -``` - -> **You never write `RCTPromiseResolveBlock` or `@ReactMethod` yourself.** The codegen produces these from your TypeScript `Promise` declaration. Your native code simply calls `resolve(result)` or `reject(error)` on the provided objects. - -### 7.2 Why Every Database Operation Must Be Async - -All Couchbase Lite operations involve disk I/O. In the codegen architecture: - -1. The **JS thread** calls the Turbo Module method via JSI -2. The codegen plumbing creates a `Promise` and returns it to JS immediately -3. **Native code dispatches to a background thread** to do the actual work: - - iOS: `DispatchQueue` (existing `backgroundQueue` pattern) - - Android: `CoroutineScope(SupervisorJob() + Dispatchers.IO)` (replaces deprecated `GlobalScope`) -4. When the operation completes, native calls `resolve(result)` or `reject(error)` -5. The JS `Promise` settles, and `await` in the calling code resumes - -This is identical in behavior to the old bridge async pattern, but without the JSON serialization overhead — parameters pass through JSI as C++ values. - -### 7.3 Synchronous vs Async in the Spec - -| Spec Return Type | Codegen Output | Use Case | -|---|---|---| -| `Promise` | `resolve`/`reject` blocks (iOS) / `Promise` param (Android) | All I/O, network, database operations | -| `void` | No promise — synchronous void call | Event emitter infra (`addListener`, `removeListeners`) | -| `string`, `number`, `boolean` | Synchronous return on JSI thread | Only for trivial, non-blocking lookups (not used in this library) | - -**In this library, all 54 domain operations return `Promise`.** Only the 8 event-emitter infrastructure methods (`addListener`/`removeListeners`) are synchronous `void`. - -### 7.4 Official Codegen Type Mapping (from RN 0.84 Appendix) - -| TypeScript | Android (Kotlin/Java) | iOS (Obj-C) | -|---|---|---| -| `string` | `String` | `NSString` | -| `boolean` | `Boolean` | `NSNumber` (BOOL) | -| `number` | `double` | `NSNumber` | -| `Object` | `ReadableMap` | `NSDictionary` (untyped) | -| `string[]` | `ReadableArray` | `NSArray` | -| `string \| null` | `String?` | `NSString * _Nullable` | -| `Promise` | `Promise` param | `RCTPromiseResolveBlock` + `RCTPromiseRejectBlock` | - ---- - -## 8. JavaScript Layer Adaptation - -### 8.1 `src/CblReactNativeEngine.tsx` Changes - -The engine class currently uses a single `NativeModules.CblReactnative` reference. After migration, it imports all 8 modules: - -```typescript -// BEFORE -import { NativeEventEmitter, NativeModules, Platform } from 'react-native'; - -// AFTER -import { EmitterSubscription, NativeEventEmitter, Platform } from 'react-native'; -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'; -``` - -**Key changes:** - -1. **Remove** the `NativeModules.CblReactnative` accessor and the `LINKING_ERROR` Proxy fallback entirely. `TurboModuleRegistry.getEnforcing()` in each spec throws a clear error if the module is not linked. - -2. **Replace** `new NativeEventEmitter(this.CblReactNative)` with `new NativeEventEmitter()` (no argument). In RN 0.76+ new arch, all device events flow through a global emitter regardless of which native module emits them. A single `NativeEventEmitter()` instance handles all 6 event types. - -3. **Route each method call** to the appropriate module: - - ```typescript - // Database methods → NativeCblDatabase - database_Open(args) { return NativeCblDatabase.database_Open(args.name, args.config.directory, args.config.encryptionKey); } - - // Collection methods → NativeCblCollection - collection_CreateCollection(args) { return NativeCblCollection.collection_CreateCollection(args.collectionName, args.name, args.scopeName); } - - // Document methods → NativeCblDocument - collection_GetDocument(args) { return NativeCblDocument.collection_GetDocument(args.docId, args.name, args.scopeName, args.collectionName); } - - // Query methods → NativeCblQuery - query_Execute(args) { return NativeCblQuery.query_Execute(args.query, args.parameters, args.name); } - - // Replicator methods → NativeCblReplicator - replicator_Create(args) { return NativeCblReplicator.replicator_Create(args.config); } - - // Scope methods → NativeCblScope - scope_GetDefault(args) { return NativeCblScope.scope_GetDefault(args.name); } - - // Logging methods → NativeCblLogging - logsinks_SetConsole(args) { return NativeCblLogging.logsinks_SetConsole(args.level, args.domains); } - - // Engine methods → NativeCblEngine - file_GetDefaultPath() { return NativeCblEngine.file_GetDefaultPath(); } - listenerToken_Remove(args) { return NativeCblEngine.listenerToken_Remove(args.changeListenerToken); } - ``` - -4. **`EngineLocator.registerEngine`** call remains unchanged (line 127). - -5. **Event name constants** remain unchanged — they already match native event names. - ---- - -## 9. Native Implementation Guide - -Each of the 8 specs generates a native protocol (iOS) or abstract class (Android) via codegen. The native implementation classes delegate to existing manager singletons. - -### 9.1 iOS — Adapter Pattern (Required) - -Swift **cannot** directly conform to codegen-generated protocols because codegen produces Objective-C++ headers containing C++ code that Swift cannot import. The official React Native pattern (RN 0.84 docs) is the **Adapter pattern**: - -1. **Swift implementation class** — `@objcMembers public class` inheriting from `NSObject`, containing all business logic -2. **Obj-C++ adapter** (`.h` + `.mm`) — conforms to the codegen protocol, creates and holds a reference to the Swift class, forwards every method call to it - -For each of the 8 modules, you need 3 files: - -| Spec | Obj-C++ Adapter (.h + .mm) | Swift Implementation | Delegates To | -|---|---|---|---| -| `NativeCblDatabase.ts` | `RCTCblDatabase.h` / `.mm` | `CblDatabaseModule.swift` | `DatabaseManager.shared` | -| `NativeCblCollection.ts` | `RCTCblCollection.h` / `.mm` | `CblCollectionModule.swift` | `CollectionManager.shared`, `DatabaseManager.shared` | -| `NativeCblDocument.ts` | `RCTCblDocument.h` / `.mm` | `CblDocumentModule.swift` | `CollectionManager.shared` | -| `NativeCblQuery.ts` | `RCTCblQuery.h` / `.mm` | `CblQueryModule.swift` | `DatabaseManager.shared`, `QueryHelper` | -| `NativeCblReplicator.ts` | `RCTCblReplicator.h` / `.mm` | `CblReplicatorModule.swift` | `ReplicatorManager.shared`, `CollectionManager.shared` | -| `NativeCblScope.ts` | `RCTCblScope.h` / `.mm` | `CblScopeModule.swift` | `DatabaseManager.shared` | -| `NativeCblLogging.ts` | `RCTCblLogging.h` / `.mm` | `CblLoggingModule.swift` | `LoggingManager.shared`, `LogSinksManager.shared` | -| `NativeCblEngine.ts` | `RCTCblEngine.h` / `.mm` | `CblEngineModule.swift` | `FileSystemHelper` | - -**Example — Database module Obj-C++ adapter:** - -```objc -// RCTCblDatabase.h -#import -#import - -@interface RCTCblDatabase : NSObject -@end -``` - -```objc -// RCTCblDatabase.mm -#import "RCTCblDatabase.h" -#import "CblReactnative-Swift.h" // Auto-generated header exposing Swift to ObjC - -@implementation RCTCblDatabase { - CblDatabaseModule *_impl; -} - -- (id)init { - if (self = [super init]) { - _impl = [CblDatabaseModule new]; - } - return self; -} - -- (std::shared_ptr)getTurboModule: - (const facebook::react::ObjCTurboModule::InitParams &)params { - return std::make_shared(params); -} - -+ (NSString *)moduleName { return @"CblDatabase"; } - -- (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]; -} -// ... forward all other methods to _impl ... -@end -``` - -**Example — Database module Swift implementation:** - -```swift -// CblDatabaseModule.swift -import Foundation - -@objcMembers public class CblDatabaseModule: NSObject { - private let backgroundQueue = DispatchQueue(label: "CblDatabaseModule", qos: .userInitiated) - - public func database_Open( - name: String, - directory: String?, - encryptionKey: String?, - resolve: @escaping RCTPromiseResolveBlock, - reject: @escaping RCTPromiseRejectBlock - ) { - backgroundQueue.async { - do { - let result = try DatabaseManager.shared.openDatabase(name, directory: directory, encryptionKey: encryptionKey) - resolve(result) - } catch { - reject("DATABASE_ERROR", error.localizedDescription, error) - } - } - } - // ... all other database methods follow the same async dispatch pattern ... -} -``` - -> **Key point:** The `resolve`/`reject` blocks are provided automatically by codegen from the `Promise` declaration in the TypeScript spec. You never import or declare `RCTPromiseResolveBlock` yourself — the codegen header supplies it. - -The old `ios/CblReactnative.swift` and `ios/CblReactnative.mm` are **deleted**. - -### 9.2 Android — Module Class Structure - -| Spec | Native Class | Codegen Base Class | Delegates To | -|---|---|---|---| -| `NativeCblDatabase.ts` | `CblDatabaseModule.kt` | `NativeCblDatabaseSpec` | `DatabaseManager` | -| `NativeCblCollection.ts` | `CblCollectionModule.kt` | `NativeCblCollectionSpec` | `CollectionManager`, `DatabaseManager` | -| `NativeCblDocument.ts` | `CblDocumentModule.kt` | `NativeCblDocumentSpec` | `CollectionManager` | -| `NativeCblQuery.ts` | `CblQueryModule.kt` | `NativeCblQuerySpec` | `DatabaseManager` | -| `NativeCblReplicator.ts` | `CblReplicatorModule.kt` | `NativeCblReplicatorSpec` | `ReplicatorManager`, `CollectionManager` | -| `NativeCblScope.ts` | `CblScopeModule.kt` | `NativeCblScopeSpec` | `DatabaseManager` | -| `NativeCblLogging.ts` | `CblLoggingModule.kt` | `NativeCblLoggingSpec` | `LoggingManager`, `LogSinksManager` | -| `NativeCblEngine.ts` | `CblEngineModule.kt` | `NativeCblEngineSpec` | `FileSystemHelper` | - -Each Kotlin module class: -- Extends the codegen-generated abstract class (e.g., `NativeCblDatabaseSpec(reactContext)`) -- The codegen-generated abstract class provides a `Promise` parameter for each `Promise` method — you call `promise.resolve(result)` or `promise.reject(error)` instead of manually declaring `@ReactMethod` with Promise -- Uses `CoroutineScope(SupervisorJob() + Dispatchers.IO)` instead of deprecated `GlobalScope` -- Cancels the scope in `invalidate()` -- Has **no** `@ReactMethod` annotations — codegen provides all method signatures - -**Example — Database module Kotlin implementation:** - -```kotlin -class CblDatabaseModule(reactContext: ReactApplicationContext) : - NativeCblDatabaseSpec(reactContext) { - - private val moduleScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) - - override fun getName() = NAME - - override fun invalidate() { - moduleScope.cancel() - super.invalidate() - } - - override fun database_Open(name: String, directory: String?, encryptionKey: String?, promise: Promise) { - moduleScope.launch { - try { - val result = DatabaseManager.openDatabase(name, directory, encryptionKey, context) - promise.resolve(result) - } catch (e: Exception) { - promise.reject("DATABASE_ERROR", e.message, e) - } - } - } - // ... all other database methods follow the same pattern ... - - companion object { - const val NAME = "CblDatabase" - } -} -``` - -> **Key point:** The `promise: Promise` parameter is auto-generated by codegen from the `Promise` in the TypeScript spec. You never write `@ReactMethod` annotations. The codegen abstract class defines the method signatures for you. - -The old `CblReactnativeModule.kt` is **deleted**. `CblReactnativePackage.kt` is updated to register all 8 modules. - -### 9.3 Android Package Registration - -The latest React Native (0.77+/0.84) uses `BaseReactPackage` (not the deprecated `TurboReactPackage`): - -```kotlin -import com.facebook.react.BaseReactPackage -import com.facebook.react.bridge.NativeModule -import com.facebook.react.bridge.ReactApplicationContext -import com.facebook.react.module.model.ReactModuleInfo -import com.facebook.react.module.model.ReactModuleInfoProvider - -class CblReactnativePackage : BaseReactPackage() { - override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? { - return when (name) { - CblDatabaseModule.NAME -> CblDatabaseModule(reactContext) - CblCollectionModule.NAME -> CblCollectionModule(reactContext) - CblDocumentModule.NAME -> CblDocumentModule(reactContext) - CblQueryModule.NAME -> CblQueryModule(reactContext) - CblReplicatorModule.NAME -> CblReplicatorModule(reactContext) - CblScopeModule.NAME -> CblScopeModule(reactContext) - CblLoggingModule.NAME -> CblLoggingModule(reactContext) - CblEngineModule.NAME -> CblEngineModule(reactContext) - else -> null - } - } - - override fun getReactModuleInfoProvider() = ReactModuleInfoProvider { - mapOf( - CblDatabaseModule.NAME to ReactModuleInfo( - name = CblDatabaseModule.NAME, className = CblDatabaseModule.NAME, - canOverrideExistingModule = false, needsEagerInit = false, - isCxxModule = false, isTurboModule = true - ), - CblCollectionModule.NAME to ReactModuleInfo( - name = CblCollectionModule.NAME, className = CblCollectionModule.NAME, - canOverrideExistingModule = false, needsEagerInit = false, - isCxxModule = false, isTurboModule = true - ), - CblDocumentModule.NAME to ReactModuleInfo( - name = CblDocumentModule.NAME, className = CblDocumentModule.NAME, - canOverrideExistingModule = false, needsEagerInit = false, - isCxxModule = false, isTurboModule = true - ), - CblQueryModule.NAME to ReactModuleInfo( - name = CblQueryModule.NAME, className = CblQueryModule.NAME, - canOverrideExistingModule = false, needsEagerInit = false, - isCxxModule = false, isTurboModule = true - ), - CblReplicatorModule.NAME to ReactModuleInfo( - name = CblReplicatorModule.NAME, className = CblReplicatorModule.NAME, - canOverrideExistingModule = false, needsEagerInit = false, - isCxxModule = false, isTurboModule = true - ), - CblScopeModule.NAME to ReactModuleInfo( - name = CblScopeModule.NAME, className = CblScopeModule.NAME, - canOverrideExistingModule = false, needsEagerInit = false, - isCxxModule = false, isTurboModule = true - ), - CblLoggingModule.NAME to ReactModuleInfo( - name = CblLoggingModule.NAME, className = CblLoggingModule.NAME, - canOverrideExistingModule = false, needsEagerInit = false, - isCxxModule = false, isTurboModule = true - ), - CblEngineModule.NAME to ReactModuleInfo( - name = CblEngineModule.NAME, className = CblEngineModule.NAME, - canOverrideExistingModule = false, needsEagerInit = false, - isCxxModule = false, isTurboModule = true - ), - ) - } -} -``` - -### 9.4 Shared Listener State - -The current codebase stores all listener tokens in a single `allChangeListenerTokenByUuid` dictionary on the monolithic module. After splitting into 8 modules: - -- **Collection** module owns collection/document change listener tokens -- **Query** module owns query change listener tokens -- **Replicator** module owns replicator status/document change listener tokens -- **Engine** module's `listenerToken_Remove` needs access to all listener stores - -**Solution:** Extract listener storage into a shared singleton (`ListenerTokenStore`) accessible by all modules. Alternatively, `listenerToken_Remove` on the Engine module can delegate removal to each domain module until one succeeds. - ---- - -## 10. Validation & Exit Criteria - -### 10.1 TypeScript Validation - -Each of the 8 spec files must independently pass: - -```bash -npx tsc --noEmit src/NativeCblDatabase.ts -npx tsc --noEmit src/NativeCblCollection.ts -npx tsc --noEmit src/NativeCblDocument.ts -npx tsc --noEmit src/NativeCblQuery.ts -npx tsc --noEmit src/NativeCblReplicator.ts -npx tsc --noEmit src/NativeCblScope.ts -npx tsc --noEmit src/NativeCblLogging.ts -npx tsc --noEmit src/NativeCblEngine.ts -``` - -And the full project must pass: - -```bash -npx tsc --noEmit -npx eslint "**/*.{js,ts,tsx}" -``` - -### 10.2 Codegen Validation - -```bash -yarn react-native codegen -``` - -Must produce generated files for all 8 specs: -- iOS: 8 `Native*Spec.h` protocol headers + 8 `Native*SpecJSI` C++ implementations (generated during `pod install`) -- Android: 8 `Native*Spec.java` abstract classes in `android/build/generated/source/codegen/` - -### 10.3 Build Validation - -```bash -# iOS -cd expo-example/ios && RCT_NEW_ARCH_ENABLED=1 pod install && xcodebuild -workspace ExpoExample.xcworkspace -scheme expo-example -configuration Debug -sdk iphonesimulator - -# Android -cd expo-example/android && ./gradlew assembleDebug -PnewArchEnabled=true -``` - -### 10.4 Exit Criteria Checklist - -- [ ] All 8 TypeScript spec files exist in `src/` -- [ ] `npx tsc --noEmit` passes with zero errors on all 8 specs -- [ ] `npx eslint` passes with zero errors on all 8 specs -- [ ] `codegenConfig` is present in `package.json` -- [ ] `create-react-native-library.type` is `module-new` -- [ ] Codegen generates native bindings for all 8 modules -- [ ] All 54 async domain methods are covered across the 8 specs -- [ ] All 4 event-emitting modules include `addListener`/`removeListeners` -- [ ] No spec uses deprecated patterns (`NativeModules`, `UnsafeObject`, synchronous domain methods) -- [ ] Parameter types match native implementations on both platforms - ---- - -## 11. Migration Phases - -### Phase 1 — Write the 8 TypeScript Specs (THIS TICKET) - -Create all 8 spec files as defined in Section 5. Add `codegenConfig` to `package.json` (Section 6.1). Change `"type": "module-legacy"` to `"module-new"`. Validate per Section 10.1. - -### Phase 2 — Update JavaScript Engine Layer - -Update `src/CblReactNativeEngine.tsx` per Section 8.1: -- Import all 8 modules -- Remove `NativeModules` and Proxy fallback -- Replace `NativeEventEmitter(module)` with `NativeEventEmitter()` -- Route each method to the correct domain module - -### Phase 3 — Implement Native Modules (iOS — Adapter Pattern) - -- Create 8 Obj-C++ adapter classes (`.h` + `.mm`) conforming to codegen protocols per Section 9.1 -- Create 8 Swift `@objcMembers` implementation classes delegating to existing managers -- Each adapter implements `getTurboModule:` returning the codegen JSI instance -- Codegen auto-generates `resolve`/`reject` blocks for all `Promise` methods -- Delete `ios/CblReactnative.mm` and old `ios/CblReactnative.swift` -- Update bridging header and podspec - -### Phase 4 — Implement Native Modules (Android) - -- Create 8 Kotlin module classes extending codegen-generated specs per Section 9.2 -- Codegen auto-generates `promise: Promise` param for all `Promise` methods — call `promise.resolve()`/`promise.reject()` in each method -- Replace `GlobalScope` with `CoroutineScope(SupervisorJob() + Dispatchers.IO)` per module -- Delete old `CblReactnativeModule.kt` -- Update `CblReactnativePackage.kt` to `BaseReactPackage` per Section 9.3 -- Extract shared listener storage per Section 9.4 - -### Phase 5 — Expo Example App Updates - -- `expo-example/android/gradle.properties`: `newArchEnabled=true` -- `expo-example/app.json`: add `"newArchEnabled": true` -- Audit `expo-example/cbl-reactnative-plugin.js` for new-arch compatibility -- Run `npx expo prebuild --clean` - -### Phase 6 — Build & Integration Testing - -- Run codegen, verify generated files for all 8 modules -- Build both platforms with new arch enabled -- Run all integration tests under `expo-example/cblite-js-tests/` -- Verify all 6 event types emit correctly -- Verify replication filters (`JavaScriptFilterEvaluator.kt`) work under new arch - -### Phase 7 — Full Legacy Removal & Cleanup - -**All legacy bridge code is removed. No backward compatibility is maintained.** This is the final sweep addressing every issue catalogued in Section 4. - -**Stale/dead code removal (Section 4.1):** -1. Remove `"cpp"` from `package.json` `files` array (line 50) -2. Remove the three `cpp/**` entries from `.npmignore` (lines 6–8) -3. Remove `folly_compiler_flags` variable from `cbl-reactnative.podspec` (line 4) -4. Remove the entire `if ENV['RCT_NEW_ARCH_ENABLED'] == '1'` conditional block from `cbl-reactnative.podspec` (lines 29–41); replace with `install_modules_dependencies(s)` - -**Legacy bridge API removal verification (Section 4.3):** - -Run the following grep to confirm zero legacy patterns remain in non-test source files: -```bash -rg -l "NativeModules|RCT_EXTERN_MODULE|RCT_EXTERN_METHOD|ReactContextBaseJavaModule|@ReactMethod|RCTEventEmitter|RCTBridgeModule|ReactPackage|TurboReactPackage|GlobalScope" --glob '!**/__tests__/**' --glob '!**/node_modules/**' src/ ios/ android/ -``` -This must return **zero results**. - -**Expo config plugin cleanup (Section 4.12):** -- Remove dead `includeNativeModulePod` function from `expo-example/cbl-reactnative-plugin.js` -- Verify `modifyAndroidBuildGradle` still works with new arch Gradle setup - -**Other cleanup (Section 4.15):** -- Verify `android/build.gradle` dependency `com.facebook.react:react-native:+` is properly managed by the `com.facebook.react` Gradle plugin -- Run `npx eslint "**/*.{js,ts,tsx}"` — zero errors -- Run `npx tsc --noEmit` — zero errors -- Confirm `lefthook.yml` pre-commit hooks pass on all new/modified files - -**Documentation updates:** -- Update `README.md`: remove any legacy bridge setup instructions, add new-arch-only setup instructions, document minimum RN version 0.76+ -- Update `CHANGELOG.md`: add entry for Turbo Module migration and legacy bridge removal - ---- - -## 12. File Change Matrix - -### TypeScript Specs (8 files created) - -| File Path | Action | Summary | -|---|---|---| -| `src/NativeCblDatabase.ts` | **Create** | Database domain spec — 9 async methods | -| `src/NativeCblCollection.ts` | **Create** | Collection domain spec — 13 async + 2 event emitter methods | -| `src/NativeCblDocument.ts` | **Create** | Document domain spec — 7 async methods | -| `src/NativeCblQuery.ts` | **Create** | Query domain spec — 4 async + 2 event emitter methods | -| `src/NativeCblReplicator.ts` | **Create** | Replicator domain spec — 11 async + 2 event emitter methods | -| `src/NativeCblScope.ts` | **Create** | Scope domain spec — 3 async methods | -| `src/NativeCblLogging.ts` | **Create** | Logging domain spec — 5 async + 2 event emitter methods | -| `src/NativeCblEngine.ts` | **Create** | Engine domain spec — 2 async methods | - -### Configuration & JS Layer - -| File Path | Action | Summary | -|---|---|---| -| `package.json` | Modify | Remove `cpp` from `files`, add `codegenConfig` (incl. `ios.modulesProvider`), change type to `module-new` | -| `.npmignore` | Modify | Remove `cpp/**` entries | -| `cbl-reactnative.podspec` | Modify | Remove Folly flags; use `install_modules_dependencies(s)` unconditionally | -| `src/CblReactNativeEngine.tsx` | Modify | Import 8 modules; remove `NativeModules`; route methods per domain | - -### iOS Native (8 Obj-C++ adapters + 8 Swift impls + 2 deleted) - -| File Path | Action | Summary | -|---|---|---| -| `ios/CblReactnative.mm` | **Delete** | Old Obj-C extern bridge (RCT_EXTERN_MODULE / RCT_EXTERN_METHOD) | -| `ios/CblReactnative.swift` | **Delete** | Old monolithic Swift class (RCTEventEmitter subclass) | -| `ios/RCTCblDatabase.h` + `.mm` | **Create** | Obj-C++ adapter conforming to `NativeCblDatabaseSpec`, forwards to Swift | -| `ios/RCTCblCollection.h` + `.mm` | **Create** | Obj-C++ adapter conforming to `NativeCblCollectionSpec` | -| `ios/RCTCblDocument.h` + `.mm` | **Create** | Obj-C++ adapter conforming to `NativeCblDocumentSpec` | -| `ios/RCTCblQuery.h` + `.mm` | **Create** | Obj-C++ adapter conforming to `NativeCblQuerySpec` | -| `ios/RCTCblReplicator.h` + `.mm` | **Create** | Obj-C++ adapter conforming to `NativeCblReplicatorSpec` | -| `ios/RCTCblScope.h` + `.mm` | **Create** | Obj-C++ adapter conforming to `NativeCblScopeSpec` | -| `ios/RCTCblLogging.h` + `.mm` | **Create** | Obj-C++ adapter conforming to `NativeCblLoggingSpec` | -| `ios/RCTCblEngine.h` + `.mm` | **Create** | Obj-C++ adapter conforming to `NativeCblEngineSpec` | -| `ios/CblDatabaseModule.swift` | **Create** | Swift impl — `@objcMembers`, delegates to `DatabaseManager` | -| `ios/CblCollectionModule.swift` | **Create** | Swift impl — delegates to `CollectionManager` | -| `ios/CblDocumentModule.swift` | **Create** | Swift impl — delegates to `CollectionManager` | -| `ios/CblQueryModule.swift` | **Create** | Swift impl — delegates to `DatabaseManager` / `QueryHelper` | -| `ios/CblReplicatorModule.swift` | **Create** | Swift impl — delegates to `ReplicatorManager` | -| `ios/CblScopeModule.swift` | **Create** | Swift impl — delegates to `DatabaseManager` | -| `ios/CblLoggingModule.swift` | **Create** | Swift impl — delegates to `LoggingManager` / `LogSinksManager` | -| `ios/CblEngineModule.swift` | **Create** | Swift impl — delegates to `FileSystemHelper` | -| `ios/CblReactnative-Bridging-Header.h` | Modify | Remove legacy imports; add codegen header import | - -### Android Native (8 Kotlin modules + 1 deleted + 1 modified) - -| File Path | Action | Summary | -|---|---|---| -| `android/.../CblReactnativeModule.kt` | **Delete** | Old monolithic Kotlin module | -| `android/.../CblDatabaseModule.kt` | **Create** | Extends `NativeCblDatabaseSpec`; `CoroutineScope` + `promise.resolve()` | -| `android/.../CblCollectionModule.kt` | **Create** | Extends `NativeCblCollectionSpec` | -| `android/.../CblDocumentModule.kt` | **Create** | Extends `NativeCblDocumentSpec` | -| `android/.../CblQueryModule.kt` | **Create** | Extends `NativeCblQuerySpec` | -| `android/.../CblReplicatorModule.kt` | **Create** | Extends `NativeCblReplicatorSpec` | -| `android/.../CblScopeModule.kt` | **Create** | Extends `NativeCblScopeSpec` | -| `android/.../CblLoggingModule.kt` | **Create** | Extends `NativeCblLoggingSpec` | -| `android/.../CblEngineModule.kt` | **Create** | Extends `NativeCblEngineSpec` | -| `android/.../CblReactnativePackage.kt` | Modify | `BaseReactPackage` registering all 8 modules | -| `android/build.gradle` | Verify | Confirm `com.facebook.react` plugin applied | - -### Expo Example App - -| File Path | Action | Summary | -|---|---|---| -| `expo-example/android/gradle.properties` | Modify | `newArchEnabled=true` | -| `expo-example/app.json` | Modify | Add `"newArchEnabled": true` | - ---- - -## 13. Risks and Mitigations - -| Risk | Severity | Mitigation | -|---|---|---| -| **No backward compat with legacy bridge** — apps on RN < 0.76 or old arch cannot use this library after migration | **High** | This is intentional. The legacy bridge is fully removed. Minimum supported RN version becomes 0.76+. Existing users on older RN versions must either upgrade RN or pin to the last pre-migration library release. Document this as a **breaking change** in `CHANGELOG.md`. | -| 8 native modules = more module registration complexity | Medium | `BaseReactPackage.getModule()` switch is well-established official pattern. Each module is independently testable. | -| iOS Adapter pattern = 3 files per module (24 iOS files total) | Medium | Boilerplate is mechanical; the Obj-C++ adapters are thin forwarding layers. Existing manager classes are reused as-is. | -| Shared listener state across modules (`listenerToken_Remove`) | Medium | Extract into a `ListenerTokenStore` singleton shared across all native modules. | -| `GlobalScope` replacement may cancel in-flight operations | Low | Use `SupervisorJob()` so individual coroutine failures don't cascade. Cancel in `invalidate()`. | -| `JavaScriptFilterEvaluator.kt` J2V8 threading under JSI | Low | Evaluator runs on `Dispatchers.IO`, decoupled from JSI thread. Must be tested. | -| Expo config plugin conflicts with new-arch Gradle setup | Medium | Test `npx expo prebuild --clean` early. The plugin appends `apply from:` which should be idempotent. | -| Couchbase Lite SDK pinned to 3.3.0 | None | SDK version is orthogonal to bridge architecture. | - ---- - -## 14. Definition of Done - -**Spec Writing (Phase 1 — this ticket):** - -- [ ] All 8 TypeScript spec files created in `src/` -- [ ] Every spec file passes `npx tsc --noEmit` with zero errors -- [ ] Every spec file passes `npx eslint` with zero errors -- [ ] All 54 async domain methods are represented across the 8 specs -- [ ] All 4 event-emitting modules include `addListener`/`removeListeners` -- [ ] `codegenConfig` (with `ios.modulesProvider`) added to `package.json` -- [ ] `create-react-native-library.type` changed to `module-new` - -**Full Migration (all phases):** - -- [ ] No `NativeModules`, `NativeEventEmitter(module)`, `RCT_EXTERN_MODULE`, `RCT_EXTERN_METHOD`, `ReactContextBaseJavaModule`, `@ReactMethod`, `ReactPackage`, `RCTEventEmitter`, `TurboReactPackage` remain in any non-test file -- [ ] 8 Obj-C++ adapter classes + 8 Swift impl classes created on iOS (Adapter pattern) -- [ ] 8 Kotlin module classes created on Android, each extending codegen-generated spec -- [ ] `ios/CblReactnative.mm` and `ios/CblReactnative.swift` deleted -- [ ] `android/.../CblReactnativeModule.kt` deleted -- [ ] Android uses `BaseReactPackage` (not deprecated `TurboReactPackage`) -- [ ] `GlobalScope` replaced with lifecycle-scoped `CoroutineScope` in every Android module -- [ ] All async methods use codegen-generated `Promise`/`resolve`/`reject` (no manual `RCTPromiseResolveBlock` declarations) -- [ ] `ios.modulesProvider` in `codegenConfig` maps all 8 JS module names to Obj-C++ adapter class names -- [ ] `newArchEnabled=true` in `expo-example/android/gradle.properties` and `expo-example/app.json` -- [ ] Both platforms build successfully with new arch enabled -- [ ] All existing integration tests pass -- [ ] All 6 event types emit correctly on both platforms -- [ ] `tsc --noEmit` passes with zero errors -- [ ] `eslint` passes with zero errors - -**Legacy Cleanup (Phase 7):** - -- [ ] `cpp` removed from `package.json` `files` array -- [ ] `cpp/**` entries removed from `.npmignore` -- [ ] `folly_compiler_flags` and legacy `RCT_NEW_ARCH_ENABLED` conditional removed from `cbl-reactnative.podspec` -- [ ] Dead `includeNativeModulePod` function removed from `expo-example/cbl-reactnative-plugin.js` -- [ ] `rg` search for legacy patterns returns zero results in source files -- [ ] `README.md` updated — legacy setup instructions removed, new-arch-only docs added, minimum RN 0.76+ documented -- [ ] `CHANGELOG.md` updated — breaking change documented