@taladb/react-native 0.8.3 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (24) hide show
  1. package/android/src/main/AndroidManifest.xml +1 -0
  2. package/android/src/main/java/com/taladb/TalaDBModule.kt +24 -9
  3. package/android/src/main/jniLibs/arm64-v8a/libredb-8bfc5664b8a7ca7f.so +0 -0
  4. package/android/src/main/jniLibs/arm64-v8a/libtaladb_ffi.so +0 -0
  5. package/android/src/main/jniLibs/armeabi-v7a/{libredb-a2370ade61d4905d.so → libredb-0d8cc50fcdf20858.so} +0 -0
  6. package/android/src/main/jniLibs/armeabi-v7a/libtaladb_ffi.so +0 -0
  7. package/android/src/main/jniLibs/x86_64/libredb-a437336529f1b346.so +0 -0
  8. package/android/src/main/jniLibs/x86_64/libtaladb_ffi.so +0 -0
  9. package/cpp/TalaDBHostObject.cpp +107 -0
  10. package/cpp/TalaDBJni.cpp +16 -5
  11. package/cpp/taladb.h +47 -5
  12. package/ios/TalaDB.mm +56 -28
  13. package/ios/TalaDBFfi.xcframework/ios-arm64/Headers/TalaDBHostObject.cpp +107 -0
  14. package/ios/TalaDBFfi.xcframework/ios-arm64/Headers/TalaDBJni.cpp +16 -5
  15. package/ios/TalaDBFfi.xcframework/ios-arm64/Headers/taladb.h +47 -5
  16. package/ios/TalaDBFfi.xcframework/ios-arm64/libtaladb_ffi.a +0 -0
  17. package/ios/TalaDBFfi.xcframework/ios-arm64_x86_64-simulator/Headers/TalaDBHostObject.cpp +107 -0
  18. package/ios/TalaDBFfi.xcframework/ios-arm64_x86_64-simulator/Headers/TalaDBJni.cpp +16 -5
  19. package/ios/TalaDBFfi.xcframework/ios-arm64_x86_64-simulator/Headers/taladb.h +47 -5
  20. package/ios/TalaDBFfi.xcframework/ios-arm64_x86_64-simulator/libtaladb_ffi_sim.a +0 -0
  21. package/package.json +2 -2
  22. package/src/index.tsx +45 -13
  23. package/android/src/main/jniLibs/arm64-v8a/libredb-ad8ceaabb8e8f32b.so +0 -0
  24. package/android/src/main/jniLibs/x86_64/libredb-aea6e7f25462c4b5.so +0 -0
@@ -1,2 +1,3 @@
1
1
  <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ <uses-permission android:name="android.permission.INTERNET" />
2
3
  </manifest>
@@ -51,7 +51,8 @@ class TalaDBModule(private val reactContext: ReactApplicationContext) :
51
51
  * JSI runtime identified by [jsContextNativePtr].
52
52
  * Called once from [initialize].
53
53
  */
54
- private external fun nativeInstall(jsContextNativePtr: Long, dbPath: String, configJson: String?)
54
+ private external fun nativeInstall(jsContextNativePtr: Long, dbPath: String, configJson: String?): String?
55
+ private external fun nativeClose(jsContextNativePtr: Long)
55
56
 
56
57
  // -----------------------------------------------------------------------
57
58
  // TurboModule: initialize(dbName, configJson?) → Promise<void>
@@ -59,16 +60,33 @@ class TalaDBModule(private val reactContext: ReactApplicationContext) :
59
60
 
60
61
  override fun initialize(dbName: String, configJson: String?, promise: Promise) {
61
62
  try {
62
- val dbPath = reactContext.filesDir.absolutePath + "/$dbName"
63
+ // Private app storage excluded from Android Auto Backup. The OS
64
+ // file-based-encryption layer protects it while the device is locked.
65
+ val dbFile = java.io.File(reactContext.noBackupFilesDir, dbName)
66
+ val legacyFile = java.io.File(reactContext.filesDir, dbName)
67
+ if (!dbFile.exists() && legacyFile.exists() && !legacyFile.renameTo(dbFile)) {
68
+ throw java.io.IOException("failed to migrate TalaDB into no-backup storage")
69
+ }
70
+ val legacySalt = java.io.File(legacyFile.absolutePath + ".taladb-salt")
71
+ val saltFile = java.io.File(dbFile.absolutePath + ".taladb-salt")
72
+ if (!saltFile.exists() && legacySalt.exists() && !legacySalt.renameTo(saltFile)) {
73
+ throw java.io.IOException("failed to migrate TalaDB encryption salt")
74
+ }
75
+ val dbPath = dbFile.absolutePath
63
76
 
64
77
  // javaScriptContextHolder.get() returns the raw jsi::Runtime* pointer.
65
78
  val jsContextPtr = reactContext.javaScriptContextHolder!!.get()
79
+ if (jsContextPtr == 0L) {
80
+ promise.reject("TALADB_NO_RUNTIME", "JSI runtime is not available")
81
+ return
82
+ }
66
83
 
67
84
  // Install on the JS thread
68
85
  reactContext.runOnJSQueueThread {
69
86
  try {
70
- nativeInstall(jsContextPtr, dbPath, configJson)
71
- promise.resolve(null)
87
+ val error = nativeInstall(jsContextPtr, dbPath, configJson)
88
+ if (error == null) promise.resolve(null)
89
+ else promise.reject("TALADB_INSTALL_ERROR", error)
72
90
  } catch (e: Exception) {
73
91
  promise.reject("TALADB_INSTALL_ERROR", e.message, e)
74
92
  }
@@ -89,11 +107,8 @@ class TalaDBModule(private val reactContext: ReactApplicationContext) :
89
107
  try {
90
108
  reactContext.runOnJSQueueThread {
91
109
  try {
92
- reactContext.javaScriptContextHolder?.let { holder ->
93
- // Setting the property to undefined lets the JSI
94
- // HostObject destructor run (Hermes GC permitting).
95
- // For an immediate close, call nativeClose() instead.
96
- }
110
+ val ptr = reactContext.javaScriptContextHolder?.get() ?: 0L
111
+ if (ptr != 0L) nativeClose(ptr)
97
112
  promise.resolve(null)
98
113
  } catch (e: Exception) {
99
114
  promise.reject("TALADB_CLOSE_ERROR", e.message, e)
@@ -38,6 +38,7 @@ TalaDBHostObject::TalaDBHostObject(TalaDbHandle *db) : db_(db) {}
38
38
 
39
39
  TalaDBHostObject::~TalaDBHostObject() {
40
40
  if (db_) {
41
+ taladb_sync_flush(db_, 5000);
41
42
  taladb_close(db_);
42
43
  db_ = nullptr;
43
44
  }
@@ -244,12 +245,16 @@ std::vector<PropNameID> TalaDBHostObject::getPropertyNames(Runtime &rt) {
244
245
  "updateOne", "updateMany",
245
246
  "deleteOne", "deleteMany",
246
247
  "count",
248
+ "aggregate",
249
+ "exportChanges", "importChanges", "listCollectionNames",
247
250
  "createIndex", "dropIndex",
251
+ "createCompoundIndex", "dropCompoundIndex",
248
252
  "createFtsIndex", "dropFtsIndex",
249
253
  "createVectorIndex", "dropVectorIndex", "upgradeVectorIndex",
250
254
  "findNearest", "findNearestAsync",
251
255
  "findAsync",
252
256
  "compact",
257
+ "syncStatus", "flushSync",
253
258
  "close",
254
259
  };
255
260
  std::vector<PropNameID> result;
@@ -425,6 +430,69 @@ Value TalaDBHostObject::get(Runtime &rt, const PropNameID &propName) {
425
430
  });
426
431
  }
427
432
 
433
+ // ------------------------------------------------------------------
434
+ // aggregate(collection: string, pipeline: object[]): object[]
435
+ // ------------------------------------------------------------------
436
+ if (name == "aggregate") {
437
+ return Function::createFromHostFunction(
438
+ rt, PropNameID::forAscii(rt, "aggregate"), 2,
439
+ [this](Runtime &rt, const Value &, const Value *args, size_t count) -> Value {
440
+ if (count < 2) throw JSError(rt, "aggregate requires 2 arguments");
441
+ auto col = args[0].getString(rt).utf8(rt);
442
+ auto pipelineJson = stringify(rt, args[1]);
443
+ char *result = taladb_aggregate(db_, col.c_str(), pipelineJson.c_str());
444
+ if (!result) throw JSError(rt, "taladb_aggregate failed");
445
+ std::string json(result);
446
+ taladb_free_string(result);
447
+ return parse(rt, json);
448
+ });
449
+ }
450
+
451
+ // ------------------------------------------------------------------
452
+ // Bidirectional sync — exportChanges / importChanges / listCollectionNames
453
+ // (back JS db.sync(); the runtime-agnostic loop lives in taladb/src/sync.ts)
454
+ // ------------------------------------------------------------------
455
+ if (name == "exportChanges") {
456
+ return Function::createFromHostFunction(
457
+ rt, PropNameID::forAscii(rt, "exportChanges"), 2,
458
+ [this](Runtime &rt, const Value &, const Value *args, size_t count) -> Value {
459
+ if (count < 2) throw JSError(rt, "exportChanges requires 2 arguments");
460
+ auto collectionsJson = stringify(rt, args[0]); // string[] → JSON array
461
+ double sinceMs = args[1].getNumber();
462
+ char *result = taladb_export_changes(db_, collectionsJson.c_str(), sinceMs);
463
+ if (!result) throw JSError(rt, "taladb_export_changes failed");
464
+ std::string json(result);
465
+ taladb_free_string(result);
466
+ // Return the changeset as an opaque string (not parsed) — the
467
+ // JS sync adapter passes it straight to the transport.
468
+ return String::createFromUtf8(rt, json);
469
+ });
470
+ }
471
+
472
+ if (name == "importChanges") {
473
+ return Function::createFromHostFunction(
474
+ rt, PropNameID::forAscii(rt, "importChanges"), 1,
475
+ [this](Runtime &rt, const Value &, const Value *args, size_t count) -> Value {
476
+ if (count < 1) throw JSError(rt, "importChanges requires 1 argument");
477
+ auto changeset = args[0].getString(rt).utf8(rt);
478
+ int32_t n = taladb_import_changes(db_, changeset.c_str());
479
+ if (n < 0) throw JSError(rt, "taladb_import_changes failed");
480
+ return Value(static_cast<double>(n));
481
+ });
482
+ }
483
+
484
+ if (name == "listCollectionNames") {
485
+ return Function::createFromHostFunction(
486
+ rt, PropNameID::forAscii(rt, "listCollectionNames"), 0,
487
+ [this](Runtime &rt, const Value &, const Value *, size_t) -> Value {
488
+ char *result = taladb_list_collection_names(db_);
489
+ if (!result) throw JSError(rt, "taladb_list_collection_names failed");
490
+ std::string json(result);
491
+ taladb_free_string(result);
492
+ return parse(rt, json); // JSON array → JS string[]
493
+ });
494
+ }
495
+
428
496
  // ------------------------------------------------------------------
429
497
  // createIndex / dropIndex / createFtsIndex / dropFtsIndex
430
498
  // ------------------------------------------------------------------
@@ -444,6 +512,24 @@ Value TalaDBHostObject::get(Runtime &rt, const PropNameID &propName) {
444
512
  });
445
513
  }
446
514
 
515
+ // ------------------------------------------------------------------
516
+ // createCompoundIndex / dropCompoundIndex(collection, fields: string[]): void
517
+ // ------------------------------------------------------------------
518
+ if (name == "createCompoundIndex" || name == "dropCompoundIndex") {
519
+ return Function::createFromHostFunction(
520
+ rt, PropNameID::forUtf8(rt, name), 2,
521
+ [this, name](Runtime &rt, const Value &, const Value *args, size_t count) -> Value {
522
+ if (count < 2) throw JSError(rt, (name + " requires 2 arguments").c_str());
523
+ auto col = args[0].getString(rt).utf8(rt);
524
+ auto fieldsJson = stringify(rt, args[1]); // string[] → JSON array
525
+ if (name == "createCompoundIndex")
526
+ taladb_create_compound_index(db_, col.c_str(), fieldsJson.c_str());
527
+ else
528
+ taladb_drop_compound_index(db_, col.c_str(), fieldsJson.c_str());
529
+ return Value::undefined();
530
+ });
531
+ }
532
+
447
533
  // ------------------------------------------------------------------
448
534
  // createVectorIndex(collection, field, dimensions, opts?): void
449
535
  // opts: { metric?: 'cosine'|'dot'|'euclidean', hnsw?: HnswOptions }
@@ -604,6 +690,26 @@ Value TalaDBHostObject::get(Runtime &rt, const PropNameID &propName) {
604
690
  });
605
691
  }
606
692
 
693
+ if (name == "syncStatus") {
694
+ return Function::createFromHostFunction(
695
+ rt, PropNameID::forAscii(rt, "syncStatus"), 0,
696
+ [this](Runtime &rt, const Value &, const Value *, size_t) -> Value {
697
+ char *raw = taladb_sync_status(db_);
698
+ if (!raw) throw ffiError(rt, "taladb_sync_status failed");
699
+ std::string json(raw); taladb_free_string(raw);
700
+ return parse(rt, json);
701
+ });
702
+ }
703
+
704
+ if (name == "flushSync") {
705
+ return Function::createFromHostFunction(
706
+ rt, PropNameID::forAscii(rt, "flushSync"), 1,
707
+ [this](Runtime &rt, const Value &, const Value *args, size_t count) -> Value {
708
+ uint64_t timeout = count && args[0].isNumber() ? (uint64_t)args[0].getNumber() : 5000;
709
+ return Value(taladb_sync_flush(db_, timeout) == 1);
710
+ });
711
+ }
712
+
607
713
  // ------------------------------------------------------------------
608
714
  // close(): void (synchronous — the destructor does the real work)
609
715
  // ------------------------------------------------------------------
@@ -612,6 +718,7 @@ Value TalaDBHostObject::get(Runtime &rt, const PropNameID &propName) {
612
718
  rt, PropNameID::forAscii(rt, "close"), 0,
613
719
  [this](Runtime &rt, const Value &, const Value *, size_t) -> Value {
614
720
  if (db_) {
721
+ taladb_sync_flush(db_, 5000);
615
722
  taladb_close(db_);
616
723
  db_ = nullptr;
617
724
  }
package/cpp/TalaDBJni.cpp CHANGED
@@ -21,7 +21,7 @@
21
21
 
22
22
  using namespace facebook::jsi;
23
23
 
24
- extern "C" JNIEXPORT void JNICALL
24
+ extern "C" JNIEXPORT jstring JNICALL
25
25
  Java_com_taladb_TalaDBModule_nativeInstall(
26
26
  JNIEnv *env,
27
27
  jobject /* thiz */,
@@ -29,6 +29,10 @@ Java_com_taladb_TalaDBModule_nativeInstall(
29
29
  jstring dbPathJ,
30
30
  jstring configJsonJ)
31
31
  {
32
+ // Release any previous HostObject before reopening the same redb file.
33
+ auto &rt = *reinterpret_cast<Runtime *>(jsContextNativePtr);
34
+ rt.global().setProperty(rt, "__TalaDB__", Value::undefined());
35
+
32
36
  // Resolve db path
33
37
  const char *dbPathC = env->GetStringUTFChars(dbPathJ, nullptr);
34
38
  std::string dbPath(dbPathC);
@@ -43,11 +47,18 @@ Java_com_taladb_TalaDBModule_nativeInstall(
43
47
  } else {
44
48
  db = taladb_open(dbPath.c_str());
45
49
  }
46
- if (!db) return; // failed to open — JS will see no __TalaDB__ global
47
-
48
- // Get the JSI runtime from the pointer passed by RN internals
49
- auto &rt = *reinterpret_cast<Runtime *>(jsContextNativePtr);
50
+ if (!db) {
51
+ const char *error = taladb_last_error();
52
+ return env->NewStringUTF(error ? error : "failed to open TalaDB");
53
+ }
50
54
 
51
55
  // Install the JSI HostObject as global.__TalaDB__
52
56
  taladb::TalaDBHostObject::install(rt, db);
57
+ return nullptr;
58
+ }
59
+
60
+ extern "C" JNIEXPORT void JNICALL
61
+ Java_com_taladb_TalaDBModule_nativeClose(JNIEnv *, jobject, jlong jsContextNativePtr) {
62
+ auto &rt = *reinterpret_cast<Runtime *>(jsContextNativePtr);
63
+ rt.global().setProperty(rt, "__TalaDB__", Value::undefined());
53
64
  }
package/cpp/taladb.h CHANGED
@@ -141,6 +141,45 @@ int32_t taladb_count(TalaDbHandle *handle,
141
141
  const char *collection,
142
142
  const char *filter_json);
143
143
 
144
+ /* -------------------------------------------------------------------------
145
+ * Aggregate
146
+ * ---------------------------------------------------------------------- */
147
+
148
+ /**
149
+ * Run an aggregation pipeline. pipeline_json is a JSON array of stages.
150
+ * Returns a JSON array of result documents, or NULL on error.
151
+ * Caller must free with taladb_free_string().
152
+ */
153
+ char *taladb_aggregate(TalaDbHandle *handle,
154
+ const char *collection,
155
+ const char *pipeline_json);
156
+
157
+ /* -------------------------------------------------------------------------
158
+ * Bidirectional sync — changeset export / import (backs JS db.sync())
159
+ * ---------------------------------------------------------------------- */
160
+
161
+ /**
162
+ * Export a changeset for the collections in collections_json (a JSON array of
163
+ * names) with changed_at > since_ms. Returns a JSON string, or NULL on error.
164
+ * Caller must free with taladb_free_string().
165
+ */
166
+ char *taladb_export_changes(TalaDbHandle *handle,
167
+ const char *collections_json,
168
+ double since_ms);
169
+
170
+ /**
171
+ * Merge a JSON changeset (from a remote peer) via Last-Write-Wins.
172
+ * Returns the number of documents changed, or -1 on error.
173
+ */
174
+ int32_t taladb_import_changes(TalaDbHandle *handle,
175
+ const char *changeset_json);
176
+
177
+ /**
178
+ * User collection names (reserved names excluded) as a JSON array string.
179
+ * Returns NULL on error. Caller must free with taladb_free_string().
180
+ */
181
+ char *taladb_list_collection_names(TalaDbHandle *handle);
182
+
144
183
  /* -------------------------------------------------------------------------
145
184
  * Index management
146
185
  * ---------------------------------------------------------------------- */
@@ -150,6 +189,10 @@ void taladb_drop_index (TalaDbHandle *handle, const char *collection, const
150
189
  void taladb_create_fts_index(TalaDbHandle *handle, const char *collection, const char *field);
151
190
  void taladb_drop_fts_index (TalaDbHandle *handle, const char *collection, const char *field);
152
191
 
192
+ /* Compound (multi-field) indexes. fields_json is a JSON array of field names. */
193
+ void taladb_create_compound_index(TalaDbHandle *handle, const char *collection, const char *fields_json);
194
+ void taladb_drop_compound_index (TalaDbHandle *handle, const char *collection, const char *fields_json);
195
+
153
196
  /* -------------------------------------------------------------------------
154
197
  * Vector index management
155
198
  *
@@ -186,6 +229,9 @@ char *taladb_find_nearest(TalaDbHandle *handle,
186
229
  size_t top_k,
187
230
  const char *filter_json);
188
231
 
232
+ char *taladb_sync_status(TalaDbHandle *handle);
233
+ int32_t taladb_sync_flush(TalaDbHandle *handle, uint64_t timeout_ms);
234
+
189
235
  /* -------------------------------------------------------------------------
190
236
  * Async job API — run heavy queries on a background thread.
191
237
  *
@@ -195,11 +241,7 @@ char *taladb_find_nearest(TalaDbHandle *handle,
195
241
  * while (taladb_job_poll(j) == 0) { } // yield to JS event loop
196
242
  * char *json = taladb_job_take_result(j); // frees the job
197
243
  *
198
- * Lifetime contract
199
- * -----------------
200
- * The handle passed to `*_start` MUST remain valid until the job has been
201
- * taken (via take_result) or cancelled (via cancel). The C++ HostObject
202
- * enforces this by not calling taladb_close while jobs are outstanding.
244
+ * Workers clone the Arc-backed database state and do not borrow the handle.
203
245
  * ---------------------------------------------------------------------- */
204
246
 
205
247
  typedef struct TalaDbJob TalaDbJob;
package/ios/TalaDB.mm CHANGED
@@ -32,12 +32,6 @@
32
32
 
33
33
  using namespace facebook::jsi;
34
34
 
35
- // ---------------------------------------------------------------------------
36
- // Global handle — one database per process
37
- // ---------------------------------------------------------------------------
38
-
39
- static TalaDbHandle *gHandle = nullptr;
40
-
41
35
  // ---------------------------------------------------------------------------
42
36
  // TalaDB — Obj-C TurboModule
43
37
  // ---------------------------------------------------------------------------
@@ -51,27 +45,26 @@ RCT_EXPORT_MODULE(TalaDB)
51
45
 
52
46
  // ---- Class method: open DB and install the JSI HostObject ----------------
53
47
 
54
- + (void)installInRuntime:(facebook::jsi::Runtime &)rt
48
+ + (NSString * _Nullable)installInRuntime:(facebook::jsi::Runtime &)rt
55
49
  dbPath:(NSString *)path
56
50
  configJson:(NSString * _Nullable)configJson {
57
- if (gHandle) {
58
- taladb_close(gHandle);
59
- gHandle = nullptr;
60
- }
61
-
51
+ // Release any previous HostObject before reopening the same redb file.
52
+ rt.global().setProperty(rt, "__TalaDB__", Value::undefined());
53
+ TalaDbHandle *handle = nullptr;
62
54
  if (configJson != nil) {
63
- gHandle = taladb_open_with_config(path.UTF8String, configJson.UTF8String);
55
+ handle = taladb_open_with_config(path.UTF8String, configJson.UTF8String);
64
56
  } else {
65
- gHandle = taladb_open(path.UTF8String);
57
+ handle = taladb_open(path.UTF8String);
66
58
  }
67
59
 
68
- if (!gHandle) {
69
- NSLog(@"[TalaDB] Failed to open database at %@", path);
70
- return;
60
+ if (!handle) {
61
+ const char *raw = taladb_last_error();
62
+ return raw ? [NSString stringWithUTF8String:raw] : @"failed to open TalaDB";
71
63
  }
72
64
 
73
- taladb::TalaDBHostObject::install(rt, gHandle);
65
+ taladb::TalaDBHostObject::install(rt, handle);
74
66
  NSLog(@"[TalaDB] Installed JSI HostObject — db: %@", path);
67
+ return nil;
75
68
  }
76
69
 
77
70
  // ---- initialize(dbName) → Promise<void> ---------------------------------
@@ -81,9 +74,29 @@ RCT_EXPORT_METHOD(initialize:(NSString *)dbName
81
74
  resolve:(RCTPromiseResolveBlock)resolve
82
75
  reject:(RCTPromiseRejectBlock)reject) {
83
76
  @try {
84
- NSString *docs = [NSSearchPathForDirectoriesInDomains(
77
+ NSString *support = [NSSearchPathForDirectoriesInDomains(
78
+ NSApplicationSupportDirectory, NSUserDomainMask, YES) firstObject];
79
+ [[NSFileManager defaultManager] createDirectoryAtPath:support
80
+ withIntermediateDirectories:YES
81
+ attributes:@{NSFileProtectionKey: NSFileProtectionCompleteUntilFirstUserAuthentication}
82
+ error:nil];
83
+ NSString *dbPath = [support stringByAppendingPathComponent:dbName];
84
+ NSString *documents = [NSSearchPathForDirectoriesInDomains(
85
85
  NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
86
- NSString *dbPath = [docs stringByAppendingPathComponent:dbName];
86
+ NSString *legacyPath = [documents stringByAppendingPathComponent:dbName];
87
+ if (![[NSFileManager defaultManager] fileExistsAtPath:dbPath] &&
88
+ [[NSFileManager defaultManager] fileExistsAtPath:legacyPath]) {
89
+ NSError *migrationError = nil;
90
+ if (![[NSFileManager defaultManager] moveItemAtPath:legacyPath toPath:dbPath error:&migrationError]) {
91
+ reject(@"TALADB_MIGRATION_ERROR", migrationError.localizedDescription, migrationError);
92
+ return;
93
+ }
94
+ NSString *legacySalt = [legacyPath stringByAppendingString:@".taladb-salt"];
95
+ NSString *newSalt = [dbPath stringByAppendingString:@".taladb-salt"];
96
+ if ([[NSFileManager defaultManager] fileExistsAtPath:legacySalt]) {
97
+ [[NSFileManager defaultManager] moveItemAtPath:legacySalt toPath:newSalt error:nil];
98
+ }
99
+ }
87
100
 
88
101
  RCTCxxBridge *bridge = (RCTCxxBridge *)[RCTBridge currentBridge];
89
102
  if (!bridge || !bridge.runtime) {
@@ -92,12 +105,22 @@ RCT_EXPORT_METHOD(initialize:(NSString *)dbName
92
105
  }
93
106
 
94
107
  // Install the HostObject on the JS thread
95
- bridge.jsCallInvoker->invokeAsync([bridge, dbPath, configJson]() {
108
+ bridge.jsCallInvoker->invokeAsync([bridge, dbPath, configJson, resolve, reject]() {
96
109
  auto &rt = *(Runtime *)bridge.runtime;
97
- [TalaDB installInRuntime:rt dbPath:dbPath configJson:configJson];
110
+ NSString *error = [TalaDB installInRuntime:rt dbPath:dbPath configJson:configJson];
111
+ if (error) reject(@"TALADB_INSTALL_ERROR", error, nil);
112
+ else {
113
+ NSDictionary *protection = @{NSFileProtectionKey: NSFileProtectionCompleteUntilFirstUserAuthentication};
114
+ [[NSFileManager defaultManager] setAttributes:protection ofItemAtPath:dbPath error:nil];
115
+ NSString *saltPath = [dbPath stringByAppendingString:@".taladb-salt"];
116
+ [[NSFileManager defaultManager] setAttributes:protection ofItemAtPath:saltPath error:nil];
117
+ NSURL *dbURL = [NSURL fileURLWithPath:dbPath];
118
+ [dbURL setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:nil];
119
+ NSURL *saltURL = [NSURL fileURLWithPath:saltPath];
120
+ [saltURL setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:nil];
121
+ resolve(nil);
122
+ }
98
123
  });
99
-
100
- resolve(nil);
101
124
  } @catch (NSException *ex) {
102
125
  reject(@"TALADB_INIT_ERROR", ex.reason, nil);
103
126
  }
@@ -107,11 +130,16 @@ RCT_EXPORT_METHOD(initialize:(NSString *)dbName
107
130
 
108
131
  RCT_EXPORT_METHOD(close:(RCTPromiseResolveBlock)resolve
109
132
  reject:(RCTPromiseRejectBlock)reject) {
110
- if (gHandle) {
111
- taladb_close(gHandle);
112
- gHandle = nullptr;
133
+ RCTCxxBridge *bridge = (RCTCxxBridge *)[RCTBridge currentBridge];
134
+ if (!bridge || !bridge.runtime) {
135
+ reject(@"TALADB_NO_BRIDGE", @"JSI bridge not available", nil);
136
+ return;
113
137
  }
114
- resolve(nil);
138
+ bridge.jsCallInvoker->invokeAsync([bridge, resolve]() {
139
+ auto &rt = *(Runtime *)bridge.runtime;
140
+ rt.global().setProperty(rt, "__TalaDB__", Value::undefined());
141
+ resolve(nil);
142
+ });
115
143
  }
116
144
 
117
145
  // ---- Synchronous stubs — all real work goes through the JSI HostObject ---
@@ -38,6 +38,7 @@ TalaDBHostObject::TalaDBHostObject(TalaDbHandle *db) : db_(db) {}
38
38
 
39
39
  TalaDBHostObject::~TalaDBHostObject() {
40
40
  if (db_) {
41
+ taladb_sync_flush(db_, 5000);
41
42
  taladb_close(db_);
42
43
  db_ = nullptr;
43
44
  }
@@ -244,12 +245,16 @@ std::vector<PropNameID> TalaDBHostObject::getPropertyNames(Runtime &rt) {
244
245
  "updateOne", "updateMany",
245
246
  "deleteOne", "deleteMany",
246
247
  "count",
248
+ "aggregate",
249
+ "exportChanges", "importChanges", "listCollectionNames",
247
250
  "createIndex", "dropIndex",
251
+ "createCompoundIndex", "dropCompoundIndex",
248
252
  "createFtsIndex", "dropFtsIndex",
249
253
  "createVectorIndex", "dropVectorIndex", "upgradeVectorIndex",
250
254
  "findNearest", "findNearestAsync",
251
255
  "findAsync",
252
256
  "compact",
257
+ "syncStatus", "flushSync",
253
258
  "close",
254
259
  };
255
260
  std::vector<PropNameID> result;
@@ -425,6 +430,69 @@ Value TalaDBHostObject::get(Runtime &rt, const PropNameID &propName) {
425
430
  });
426
431
  }
427
432
 
433
+ // ------------------------------------------------------------------
434
+ // aggregate(collection: string, pipeline: object[]): object[]
435
+ // ------------------------------------------------------------------
436
+ if (name == "aggregate") {
437
+ return Function::createFromHostFunction(
438
+ rt, PropNameID::forAscii(rt, "aggregate"), 2,
439
+ [this](Runtime &rt, const Value &, const Value *args, size_t count) -> Value {
440
+ if (count < 2) throw JSError(rt, "aggregate requires 2 arguments");
441
+ auto col = args[0].getString(rt).utf8(rt);
442
+ auto pipelineJson = stringify(rt, args[1]);
443
+ char *result = taladb_aggregate(db_, col.c_str(), pipelineJson.c_str());
444
+ if (!result) throw JSError(rt, "taladb_aggregate failed");
445
+ std::string json(result);
446
+ taladb_free_string(result);
447
+ return parse(rt, json);
448
+ });
449
+ }
450
+
451
+ // ------------------------------------------------------------------
452
+ // Bidirectional sync — exportChanges / importChanges / listCollectionNames
453
+ // (back JS db.sync(); the runtime-agnostic loop lives in taladb/src/sync.ts)
454
+ // ------------------------------------------------------------------
455
+ if (name == "exportChanges") {
456
+ return Function::createFromHostFunction(
457
+ rt, PropNameID::forAscii(rt, "exportChanges"), 2,
458
+ [this](Runtime &rt, const Value &, const Value *args, size_t count) -> Value {
459
+ if (count < 2) throw JSError(rt, "exportChanges requires 2 arguments");
460
+ auto collectionsJson = stringify(rt, args[0]); // string[] → JSON array
461
+ double sinceMs = args[1].getNumber();
462
+ char *result = taladb_export_changes(db_, collectionsJson.c_str(), sinceMs);
463
+ if (!result) throw JSError(rt, "taladb_export_changes failed");
464
+ std::string json(result);
465
+ taladb_free_string(result);
466
+ // Return the changeset as an opaque string (not parsed) — the
467
+ // JS sync adapter passes it straight to the transport.
468
+ return String::createFromUtf8(rt, json);
469
+ });
470
+ }
471
+
472
+ if (name == "importChanges") {
473
+ return Function::createFromHostFunction(
474
+ rt, PropNameID::forAscii(rt, "importChanges"), 1,
475
+ [this](Runtime &rt, const Value &, const Value *args, size_t count) -> Value {
476
+ if (count < 1) throw JSError(rt, "importChanges requires 1 argument");
477
+ auto changeset = args[0].getString(rt).utf8(rt);
478
+ int32_t n = taladb_import_changes(db_, changeset.c_str());
479
+ if (n < 0) throw JSError(rt, "taladb_import_changes failed");
480
+ return Value(static_cast<double>(n));
481
+ });
482
+ }
483
+
484
+ if (name == "listCollectionNames") {
485
+ return Function::createFromHostFunction(
486
+ rt, PropNameID::forAscii(rt, "listCollectionNames"), 0,
487
+ [this](Runtime &rt, const Value &, const Value *, size_t) -> Value {
488
+ char *result = taladb_list_collection_names(db_);
489
+ if (!result) throw JSError(rt, "taladb_list_collection_names failed");
490
+ std::string json(result);
491
+ taladb_free_string(result);
492
+ return parse(rt, json); // JSON array → JS string[]
493
+ });
494
+ }
495
+
428
496
  // ------------------------------------------------------------------
429
497
  // createIndex / dropIndex / createFtsIndex / dropFtsIndex
430
498
  // ------------------------------------------------------------------
@@ -444,6 +512,24 @@ Value TalaDBHostObject::get(Runtime &rt, const PropNameID &propName) {
444
512
  });
445
513
  }
446
514
 
515
+ // ------------------------------------------------------------------
516
+ // createCompoundIndex / dropCompoundIndex(collection, fields: string[]): void
517
+ // ------------------------------------------------------------------
518
+ if (name == "createCompoundIndex" || name == "dropCompoundIndex") {
519
+ return Function::createFromHostFunction(
520
+ rt, PropNameID::forUtf8(rt, name), 2,
521
+ [this, name](Runtime &rt, const Value &, const Value *args, size_t count) -> Value {
522
+ if (count < 2) throw JSError(rt, (name + " requires 2 arguments").c_str());
523
+ auto col = args[0].getString(rt).utf8(rt);
524
+ auto fieldsJson = stringify(rt, args[1]); // string[] → JSON array
525
+ if (name == "createCompoundIndex")
526
+ taladb_create_compound_index(db_, col.c_str(), fieldsJson.c_str());
527
+ else
528
+ taladb_drop_compound_index(db_, col.c_str(), fieldsJson.c_str());
529
+ return Value::undefined();
530
+ });
531
+ }
532
+
447
533
  // ------------------------------------------------------------------
448
534
  // createVectorIndex(collection, field, dimensions, opts?): void
449
535
  // opts: { metric?: 'cosine'|'dot'|'euclidean', hnsw?: HnswOptions }
@@ -604,6 +690,26 @@ Value TalaDBHostObject::get(Runtime &rt, const PropNameID &propName) {
604
690
  });
605
691
  }
606
692
 
693
+ if (name == "syncStatus") {
694
+ return Function::createFromHostFunction(
695
+ rt, PropNameID::forAscii(rt, "syncStatus"), 0,
696
+ [this](Runtime &rt, const Value &, const Value *, size_t) -> Value {
697
+ char *raw = taladb_sync_status(db_);
698
+ if (!raw) throw ffiError(rt, "taladb_sync_status failed");
699
+ std::string json(raw); taladb_free_string(raw);
700
+ return parse(rt, json);
701
+ });
702
+ }
703
+
704
+ if (name == "flushSync") {
705
+ return Function::createFromHostFunction(
706
+ rt, PropNameID::forAscii(rt, "flushSync"), 1,
707
+ [this](Runtime &rt, const Value &, const Value *args, size_t count) -> Value {
708
+ uint64_t timeout = count && args[0].isNumber() ? (uint64_t)args[0].getNumber() : 5000;
709
+ return Value(taladb_sync_flush(db_, timeout) == 1);
710
+ });
711
+ }
712
+
607
713
  // ------------------------------------------------------------------
608
714
  // close(): void (synchronous — the destructor does the real work)
609
715
  // ------------------------------------------------------------------
@@ -612,6 +718,7 @@ Value TalaDBHostObject::get(Runtime &rt, const PropNameID &propName) {
612
718
  rt, PropNameID::forAscii(rt, "close"), 0,
613
719
  [this](Runtime &rt, const Value &, const Value *, size_t) -> Value {
614
720
  if (db_) {
721
+ taladb_sync_flush(db_, 5000);
615
722
  taladb_close(db_);
616
723
  db_ = nullptr;
617
724
  }
@@ -21,7 +21,7 @@
21
21
 
22
22
  using namespace facebook::jsi;
23
23
 
24
- extern "C" JNIEXPORT void JNICALL
24
+ extern "C" JNIEXPORT jstring JNICALL
25
25
  Java_com_taladb_TalaDBModule_nativeInstall(
26
26
  JNIEnv *env,
27
27
  jobject /* thiz */,
@@ -29,6 +29,10 @@ Java_com_taladb_TalaDBModule_nativeInstall(
29
29
  jstring dbPathJ,
30
30
  jstring configJsonJ)
31
31
  {
32
+ // Release any previous HostObject before reopening the same redb file.
33
+ auto &rt = *reinterpret_cast<Runtime *>(jsContextNativePtr);
34
+ rt.global().setProperty(rt, "__TalaDB__", Value::undefined());
35
+
32
36
  // Resolve db path
33
37
  const char *dbPathC = env->GetStringUTFChars(dbPathJ, nullptr);
34
38
  std::string dbPath(dbPathC);
@@ -43,11 +47,18 @@ Java_com_taladb_TalaDBModule_nativeInstall(
43
47
  } else {
44
48
  db = taladb_open(dbPath.c_str());
45
49
  }
46
- if (!db) return; // failed to open — JS will see no __TalaDB__ global
47
-
48
- // Get the JSI runtime from the pointer passed by RN internals
49
- auto &rt = *reinterpret_cast<Runtime *>(jsContextNativePtr);
50
+ if (!db) {
51
+ const char *error = taladb_last_error();
52
+ return env->NewStringUTF(error ? error : "failed to open TalaDB");
53
+ }
50
54
 
51
55
  // Install the JSI HostObject as global.__TalaDB__
52
56
  taladb::TalaDBHostObject::install(rt, db);
57
+ return nullptr;
58
+ }
59
+
60
+ extern "C" JNIEXPORT void JNICALL
61
+ Java_com_taladb_TalaDBModule_nativeClose(JNIEnv *, jobject, jlong jsContextNativePtr) {
62
+ auto &rt = *reinterpret_cast<Runtime *>(jsContextNativePtr);
63
+ rt.global().setProperty(rt, "__TalaDB__", Value::undefined());
53
64
  }
@@ -141,6 +141,45 @@ int32_t taladb_count(TalaDbHandle *handle,
141
141
  const char *collection,
142
142
  const char *filter_json);
143
143
 
144
+ /* -------------------------------------------------------------------------
145
+ * Aggregate
146
+ * ---------------------------------------------------------------------- */
147
+
148
+ /**
149
+ * Run an aggregation pipeline. pipeline_json is a JSON array of stages.
150
+ * Returns a JSON array of result documents, or NULL on error.
151
+ * Caller must free with taladb_free_string().
152
+ */
153
+ char *taladb_aggregate(TalaDbHandle *handle,
154
+ const char *collection,
155
+ const char *pipeline_json);
156
+
157
+ /* -------------------------------------------------------------------------
158
+ * Bidirectional sync — changeset export / import (backs JS db.sync())
159
+ * ---------------------------------------------------------------------- */
160
+
161
+ /**
162
+ * Export a changeset for the collections in collections_json (a JSON array of
163
+ * names) with changed_at > since_ms. Returns a JSON string, or NULL on error.
164
+ * Caller must free with taladb_free_string().
165
+ */
166
+ char *taladb_export_changes(TalaDbHandle *handle,
167
+ const char *collections_json,
168
+ double since_ms);
169
+
170
+ /**
171
+ * Merge a JSON changeset (from a remote peer) via Last-Write-Wins.
172
+ * Returns the number of documents changed, or -1 on error.
173
+ */
174
+ int32_t taladb_import_changes(TalaDbHandle *handle,
175
+ const char *changeset_json);
176
+
177
+ /**
178
+ * User collection names (reserved names excluded) as a JSON array string.
179
+ * Returns NULL on error. Caller must free with taladb_free_string().
180
+ */
181
+ char *taladb_list_collection_names(TalaDbHandle *handle);
182
+
144
183
  /* -------------------------------------------------------------------------
145
184
  * Index management
146
185
  * ---------------------------------------------------------------------- */
@@ -150,6 +189,10 @@ void taladb_drop_index (TalaDbHandle *handle, const char *collection, const
150
189
  void taladb_create_fts_index(TalaDbHandle *handle, const char *collection, const char *field);
151
190
  void taladb_drop_fts_index (TalaDbHandle *handle, const char *collection, const char *field);
152
191
 
192
+ /* Compound (multi-field) indexes. fields_json is a JSON array of field names. */
193
+ void taladb_create_compound_index(TalaDbHandle *handle, const char *collection, const char *fields_json);
194
+ void taladb_drop_compound_index (TalaDbHandle *handle, const char *collection, const char *fields_json);
195
+
153
196
  /* -------------------------------------------------------------------------
154
197
  * Vector index management
155
198
  *
@@ -186,6 +229,9 @@ char *taladb_find_nearest(TalaDbHandle *handle,
186
229
  size_t top_k,
187
230
  const char *filter_json);
188
231
 
232
+ char *taladb_sync_status(TalaDbHandle *handle);
233
+ int32_t taladb_sync_flush(TalaDbHandle *handle, uint64_t timeout_ms);
234
+
189
235
  /* -------------------------------------------------------------------------
190
236
  * Async job API — run heavy queries on a background thread.
191
237
  *
@@ -195,11 +241,7 @@ char *taladb_find_nearest(TalaDbHandle *handle,
195
241
  * while (taladb_job_poll(j) == 0) { } // yield to JS event loop
196
242
  * char *json = taladb_job_take_result(j); // frees the job
197
243
  *
198
- * Lifetime contract
199
- * -----------------
200
- * The handle passed to `*_start` MUST remain valid until the job has been
201
- * taken (via take_result) or cancelled (via cancel). The C++ HostObject
202
- * enforces this by not calling taladb_close while jobs are outstanding.
244
+ * Workers clone the Arc-backed database state and do not borrow the handle.
203
245
  * ---------------------------------------------------------------------- */
204
246
 
205
247
  typedef struct TalaDbJob TalaDbJob;
@@ -38,6 +38,7 @@ TalaDBHostObject::TalaDBHostObject(TalaDbHandle *db) : db_(db) {}
38
38
 
39
39
  TalaDBHostObject::~TalaDBHostObject() {
40
40
  if (db_) {
41
+ taladb_sync_flush(db_, 5000);
41
42
  taladb_close(db_);
42
43
  db_ = nullptr;
43
44
  }
@@ -244,12 +245,16 @@ std::vector<PropNameID> TalaDBHostObject::getPropertyNames(Runtime &rt) {
244
245
  "updateOne", "updateMany",
245
246
  "deleteOne", "deleteMany",
246
247
  "count",
248
+ "aggregate",
249
+ "exportChanges", "importChanges", "listCollectionNames",
247
250
  "createIndex", "dropIndex",
251
+ "createCompoundIndex", "dropCompoundIndex",
248
252
  "createFtsIndex", "dropFtsIndex",
249
253
  "createVectorIndex", "dropVectorIndex", "upgradeVectorIndex",
250
254
  "findNearest", "findNearestAsync",
251
255
  "findAsync",
252
256
  "compact",
257
+ "syncStatus", "flushSync",
253
258
  "close",
254
259
  };
255
260
  std::vector<PropNameID> result;
@@ -425,6 +430,69 @@ Value TalaDBHostObject::get(Runtime &rt, const PropNameID &propName) {
425
430
  });
426
431
  }
427
432
 
433
+ // ------------------------------------------------------------------
434
+ // aggregate(collection: string, pipeline: object[]): object[]
435
+ // ------------------------------------------------------------------
436
+ if (name == "aggregate") {
437
+ return Function::createFromHostFunction(
438
+ rt, PropNameID::forAscii(rt, "aggregate"), 2,
439
+ [this](Runtime &rt, const Value &, const Value *args, size_t count) -> Value {
440
+ if (count < 2) throw JSError(rt, "aggregate requires 2 arguments");
441
+ auto col = args[0].getString(rt).utf8(rt);
442
+ auto pipelineJson = stringify(rt, args[1]);
443
+ char *result = taladb_aggregate(db_, col.c_str(), pipelineJson.c_str());
444
+ if (!result) throw JSError(rt, "taladb_aggregate failed");
445
+ std::string json(result);
446
+ taladb_free_string(result);
447
+ return parse(rt, json);
448
+ });
449
+ }
450
+
451
+ // ------------------------------------------------------------------
452
+ // Bidirectional sync — exportChanges / importChanges / listCollectionNames
453
+ // (back JS db.sync(); the runtime-agnostic loop lives in taladb/src/sync.ts)
454
+ // ------------------------------------------------------------------
455
+ if (name == "exportChanges") {
456
+ return Function::createFromHostFunction(
457
+ rt, PropNameID::forAscii(rt, "exportChanges"), 2,
458
+ [this](Runtime &rt, const Value &, const Value *args, size_t count) -> Value {
459
+ if (count < 2) throw JSError(rt, "exportChanges requires 2 arguments");
460
+ auto collectionsJson = stringify(rt, args[0]); // string[] → JSON array
461
+ double sinceMs = args[1].getNumber();
462
+ char *result = taladb_export_changes(db_, collectionsJson.c_str(), sinceMs);
463
+ if (!result) throw JSError(rt, "taladb_export_changes failed");
464
+ std::string json(result);
465
+ taladb_free_string(result);
466
+ // Return the changeset as an opaque string (not parsed) — the
467
+ // JS sync adapter passes it straight to the transport.
468
+ return String::createFromUtf8(rt, json);
469
+ });
470
+ }
471
+
472
+ if (name == "importChanges") {
473
+ return Function::createFromHostFunction(
474
+ rt, PropNameID::forAscii(rt, "importChanges"), 1,
475
+ [this](Runtime &rt, const Value &, const Value *args, size_t count) -> Value {
476
+ if (count < 1) throw JSError(rt, "importChanges requires 1 argument");
477
+ auto changeset = args[0].getString(rt).utf8(rt);
478
+ int32_t n = taladb_import_changes(db_, changeset.c_str());
479
+ if (n < 0) throw JSError(rt, "taladb_import_changes failed");
480
+ return Value(static_cast<double>(n));
481
+ });
482
+ }
483
+
484
+ if (name == "listCollectionNames") {
485
+ return Function::createFromHostFunction(
486
+ rt, PropNameID::forAscii(rt, "listCollectionNames"), 0,
487
+ [this](Runtime &rt, const Value &, const Value *, size_t) -> Value {
488
+ char *result = taladb_list_collection_names(db_);
489
+ if (!result) throw JSError(rt, "taladb_list_collection_names failed");
490
+ std::string json(result);
491
+ taladb_free_string(result);
492
+ return parse(rt, json); // JSON array → JS string[]
493
+ });
494
+ }
495
+
428
496
  // ------------------------------------------------------------------
429
497
  // createIndex / dropIndex / createFtsIndex / dropFtsIndex
430
498
  // ------------------------------------------------------------------
@@ -444,6 +512,24 @@ Value TalaDBHostObject::get(Runtime &rt, const PropNameID &propName) {
444
512
  });
445
513
  }
446
514
 
515
+ // ------------------------------------------------------------------
516
+ // createCompoundIndex / dropCompoundIndex(collection, fields: string[]): void
517
+ // ------------------------------------------------------------------
518
+ if (name == "createCompoundIndex" || name == "dropCompoundIndex") {
519
+ return Function::createFromHostFunction(
520
+ rt, PropNameID::forUtf8(rt, name), 2,
521
+ [this, name](Runtime &rt, const Value &, const Value *args, size_t count) -> Value {
522
+ if (count < 2) throw JSError(rt, (name + " requires 2 arguments").c_str());
523
+ auto col = args[0].getString(rt).utf8(rt);
524
+ auto fieldsJson = stringify(rt, args[1]); // string[] → JSON array
525
+ if (name == "createCompoundIndex")
526
+ taladb_create_compound_index(db_, col.c_str(), fieldsJson.c_str());
527
+ else
528
+ taladb_drop_compound_index(db_, col.c_str(), fieldsJson.c_str());
529
+ return Value::undefined();
530
+ });
531
+ }
532
+
447
533
  // ------------------------------------------------------------------
448
534
  // createVectorIndex(collection, field, dimensions, opts?): void
449
535
  // opts: { metric?: 'cosine'|'dot'|'euclidean', hnsw?: HnswOptions }
@@ -604,6 +690,26 @@ Value TalaDBHostObject::get(Runtime &rt, const PropNameID &propName) {
604
690
  });
605
691
  }
606
692
 
693
+ if (name == "syncStatus") {
694
+ return Function::createFromHostFunction(
695
+ rt, PropNameID::forAscii(rt, "syncStatus"), 0,
696
+ [this](Runtime &rt, const Value &, const Value *, size_t) -> Value {
697
+ char *raw = taladb_sync_status(db_);
698
+ if (!raw) throw ffiError(rt, "taladb_sync_status failed");
699
+ std::string json(raw); taladb_free_string(raw);
700
+ return parse(rt, json);
701
+ });
702
+ }
703
+
704
+ if (name == "flushSync") {
705
+ return Function::createFromHostFunction(
706
+ rt, PropNameID::forAscii(rt, "flushSync"), 1,
707
+ [this](Runtime &rt, const Value &, const Value *args, size_t count) -> Value {
708
+ uint64_t timeout = count && args[0].isNumber() ? (uint64_t)args[0].getNumber() : 5000;
709
+ return Value(taladb_sync_flush(db_, timeout) == 1);
710
+ });
711
+ }
712
+
607
713
  // ------------------------------------------------------------------
608
714
  // close(): void (synchronous — the destructor does the real work)
609
715
  // ------------------------------------------------------------------
@@ -612,6 +718,7 @@ Value TalaDBHostObject::get(Runtime &rt, const PropNameID &propName) {
612
718
  rt, PropNameID::forAscii(rt, "close"), 0,
613
719
  [this](Runtime &rt, const Value &, const Value *, size_t) -> Value {
614
720
  if (db_) {
721
+ taladb_sync_flush(db_, 5000);
615
722
  taladb_close(db_);
616
723
  db_ = nullptr;
617
724
  }
@@ -21,7 +21,7 @@
21
21
 
22
22
  using namespace facebook::jsi;
23
23
 
24
- extern "C" JNIEXPORT void JNICALL
24
+ extern "C" JNIEXPORT jstring JNICALL
25
25
  Java_com_taladb_TalaDBModule_nativeInstall(
26
26
  JNIEnv *env,
27
27
  jobject /* thiz */,
@@ -29,6 +29,10 @@ Java_com_taladb_TalaDBModule_nativeInstall(
29
29
  jstring dbPathJ,
30
30
  jstring configJsonJ)
31
31
  {
32
+ // Release any previous HostObject before reopening the same redb file.
33
+ auto &rt = *reinterpret_cast<Runtime *>(jsContextNativePtr);
34
+ rt.global().setProperty(rt, "__TalaDB__", Value::undefined());
35
+
32
36
  // Resolve db path
33
37
  const char *dbPathC = env->GetStringUTFChars(dbPathJ, nullptr);
34
38
  std::string dbPath(dbPathC);
@@ -43,11 +47,18 @@ Java_com_taladb_TalaDBModule_nativeInstall(
43
47
  } else {
44
48
  db = taladb_open(dbPath.c_str());
45
49
  }
46
- if (!db) return; // failed to open — JS will see no __TalaDB__ global
47
-
48
- // Get the JSI runtime from the pointer passed by RN internals
49
- auto &rt = *reinterpret_cast<Runtime *>(jsContextNativePtr);
50
+ if (!db) {
51
+ const char *error = taladb_last_error();
52
+ return env->NewStringUTF(error ? error : "failed to open TalaDB");
53
+ }
50
54
 
51
55
  // Install the JSI HostObject as global.__TalaDB__
52
56
  taladb::TalaDBHostObject::install(rt, db);
57
+ return nullptr;
58
+ }
59
+
60
+ extern "C" JNIEXPORT void JNICALL
61
+ Java_com_taladb_TalaDBModule_nativeClose(JNIEnv *, jobject, jlong jsContextNativePtr) {
62
+ auto &rt = *reinterpret_cast<Runtime *>(jsContextNativePtr);
63
+ rt.global().setProperty(rt, "__TalaDB__", Value::undefined());
53
64
  }
@@ -141,6 +141,45 @@ int32_t taladb_count(TalaDbHandle *handle,
141
141
  const char *collection,
142
142
  const char *filter_json);
143
143
 
144
+ /* -------------------------------------------------------------------------
145
+ * Aggregate
146
+ * ---------------------------------------------------------------------- */
147
+
148
+ /**
149
+ * Run an aggregation pipeline. pipeline_json is a JSON array of stages.
150
+ * Returns a JSON array of result documents, or NULL on error.
151
+ * Caller must free with taladb_free_string().
152
+ */
153
+ char *taladb_aggregate(TalaDbHandle *handle,
154
+ const char *collection,
155
+ const char *pipeline_json);
156
+
157
+ /* -------------------------------------------------------------------------
158
+ * Bidirectional sync — changeset export / import (backs JS db.sync())
159
+ * ---------------------------------------------------------------------- */
160
+
161
+ /**
162
+ * Export a changeset for the collections in collections_json (a JSON array of
163
+ * names) with changed_at > since_ms. Returns a JSON string, or NULL on error.
164
+ * Caller must free with taladb_free_string().
165
+ */
166
+ char *taladb_export_changes(TalaDbHandle *handle,
167
+ const char *collections_json,
168
+ double since_ms);
169
+
170
+ /**
171
+ * Merge a JSON changeset (from a remote peer) via Last-Write-Wins.
172
+ * Returns the number of documents changed, or -1 on error.
173
+ */
174
+ int32_t taladb_import_changes(TalaDbHandle *handle,
175
+ const char *changeset_json);
176
+
177
+ /**
178
+ * User collection names (reserved names excluded) as a JSON array string.
179
+ * Returns NULL on error. Caller must free with taladb_free_string().
180
+ */
181
+ char *taladb_list_collection_names(TalaDbHandle *handle);
182
+
144
183
  /* -------------------------------------------------------------------------
145
184
  * Index management
146
185
  * ---------------------------------------------------------------------- */
@@ -150,6 +189,10 @@ void taladb_drop_index (TalaDbHandle *handle, const char *collection, const
150
189
  void taladb_create_fts_index(TalaDbHandle *handle, const char *collection, const char *field);
151
190
  void taladb_drop_fts_index (TalaDbHandle *handle, const char *collection, const char *field);
152
191
 
192
+ /* Compound (multi-field) indexes. fields_json is a JSON array of field names. */
193
+ void taladb_create_compound_index(TalaDbHandle *handle, const char *collection, const char *fields_json);
194
+ void taladb_drop_compound_index (TalaDbHandle *handle, const char *collection, const char *fields_json);
195
+
153
196
  /* -------------------------------------------------------------------------
154
197
  * Vector index management
155
198
  *
@@ -186,6 +229,9 @@ char *taladb_find_nearest(TalaDbHandle *handle,
186
229
  size_t top_k,
187
230
  const char *filter_json);
188
231
 
232
+ char *taladb_sync_status(TalaDbHandle *handle);
233
+ int32_t taladb_sync_flush(TalaDbHandle *handle, uint64_t timeout_ms);
234
+
189
235
  /* -------------------------------------------------------------------------
190
236
  * Async job API — run heavy queries on a background thread.
191
237
  *
@@ -195,11 +241,7 @@ char *taladb_find_nearest(TalaDbHandle *handle,
195
241
  * while (taladb_job_poll(j) == 0) { } // yield to JS event loop
196
242
  * char *json = taladb_job_take_result(j); // frees the job
197
243
  *
198
- * Lifetime contract
199
- * -----------------
200
- * The handle passed to `*_start` MUST remain valid until the job has been
201
- * taken (via take_result) or cancelled (via cancel). The C++ HostObject
202
- * enforces this by not calling taladb_close while jobs are outstanding.
244
+ * Workers clone the Arc-backed database state and do not borrow the handle.
203
245
  * ---------------------------------------------------------------------- */
204
246
 
205
247
  typedef struct TalaDbJob TalaDbJob;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taladb/react-native",
3
- "version": "0.8.3",
3
+ "version": "0.9.0",
4
4
  "description": "TalaDB React Native module — document and vector database via JSI HostObject",
5
5
  "main": "src/index",
6
6
  "types": "src/index.tsx",
@@ -24,7 +24,7 @@
24
24
  "repository": {
25
25
  "type": "git",
26
26
  "url": "https://github.com/thinkgrid-labs/taladb.git",
27
- "directory": "packages/taladb-react-native"
27
+ "directory": "packages/bindings/react-native"
28
28
  },
29
29
  "homepage": "https://thinkgrid-labs.github.io/taladb/guide/react-native",
30
30
  "bugs": {
package/src/index.tsx CHANGED
@@ -35,6 +35,10 @@ export const TalaDBModule = {
35
35
  NativeTalaDB.initialize(dbName, configJson),
36
36
  /** Close the database gracefully. */
37
37
  close: () => NativeTalaDB.close(),
38
+ /** HTTP push events dropped by backpressure or failed after retries. */
39
+ syncStatus: () => native().syncStatus(),
40
+ /** Wait for HTTP push work accepted before this call. */
41
+ flushSync: (timeoutMs = 5000) => native().flushSync(timeoutMs),
38
42
  };
39
43
 
40
44
  // ---------------------------------------------------------------------------
@@ -67,9 +71,35 @@ export interface Collection<T extends Document = Document> {
67
71
 
68
72
  export interface DB {
69
73
  collection<T extends Document = Document>(name: string): Collection<T>;
74
+ syncStatus(): { dropped: number; failed: number };
75
+ flushSync(timeoutMs?: number): boolean;
70
76
  close(): Promise<void>;
71
77
  }
72
78
 
79
+ interface JsiTalaDB {
80
+ insert(collection: string, doc: Object): string;
81
+ insertMany(collection: string, docs: Object[]): string[];
82
+ find(collection: string, filter: Object | null): Object[];
83
+ findOne(collection: string, filter: Object | null): Object | null;
84
+ updateOne(collection: string, filter: Object, update: Object): boolean;
85
+ updateMany(collection: string, filter: Object, update: Object): number;
86
+ deleteOne(collection: string, filter: Object): boolean;
87
+ deleteMany(collection: string, filter: Object): number;
88
+ count(collection: string, filter: Object | null): number;
89
+ createIndex(collection: string, field: string): void;
90
+ dropIndex(collection: string, field: string): void;
91
+ createFtsIndex(collection: string, field: string): void;
92
+ dropFtsIndex(collection: string, field: string): void;
93
+ syncStatus(): { dropped: number; failed: number };
94
+ flushSync(timeoutMs?: number): boolean;
95
+ }
96
+
97
+ function native(): JsiTalaDB {
98
+ const host = (globalThis as { __TalaDB__?: JsiTalaDB }).__TalaDB__;
99
+ if (!host) throw new Error('TalaDB is not initialized; await TalaDBModule.initialize() first');
100
+ return host;
101
+ }
102
+
73
103
  // ---------------------------------------------------------------------------
74
104
  // openDB — synchronous DB handle (after initialize() has been called)
75
105
  // ---------------------------------------------------------------------------
@@ -82,25 +112,27 @@ export interface DB {
82
112
  */
83
113
  function collection<T extends Document>(colName: string): Collection<T> {
84
114
  return {
85
- insert: (doc) => NativeTalaDB.insert(colName, doc as Object),
86
- insertMany: (docs) => NativeTalaDB.insertMany(colName, docs as Object[]),
87
- find: (filter?) => NativeTalaDB.find(colName, filter ?? null) as T[],
88
- findOne: (filter) => NativeTalaDB.findOne(colName, filter) as T | null,
89
- updateOne: (filter, update) => NativeTalaDB.updateOne(colName, filter, update),
90
- updateMany: (filter, update) => NativeTalaDB.updateMany(colName, filter, update),
91
- deleteOne: (filter) => NativeTalaDB.deleteOne(colName, filter),
92
- deleteMany: (filter) => NativeTalaDB.deleteMany(colName, filter),
93
- count: (filter?) => NativeTalaDB.count(colName, filter ?? null),
94
- createIndex: (field) => NativeTalaDB.createIndex(colName, field),
95
- dropIndex: (field) => NativeTalaDB.dropIndex(colName, field),
96
- createFtsIndex: (field) => NativeTalaDB.createFtsIndex(colName, field),
97
- dropFtsIndex: (field) => NativeTalaDB.dropFtsIndex(colName, field),
115
+ insert: (doc) => native().insert(colName, doc as Object),
116
+ insertMany: (docs) => native().insertMany(colName, docs as Object[]),
117
+ find: (filter?) => native().find(colName, filter ?? null) as T[],
118
+ findOne: (filter) => native().findOne(colName, filter) as T | null,
119
+ updateOne: (filter, update) => native().updateOne(colName, filter, update),
120
+ updateMany: (filter, update) => native().updateMany(colName, filter, update),
121
+ deleteOne: (filter) => native().deleteOne(colName, filter),
122
+ deleteMany: (filter) => native().deleteMany(colName, filter),
123
+ count: (filter?) => native().count(colName, filter ?? null),
124
+ createIndex: (field) => native().createIndex(colName, field),
125
+ dropIndex: (field) => native().dropIndex(colName, field),
126
+ createFtsIndex: (field) => native().createFtsIndex(colName, field),
127
+ dropFtsIndex: (field) => native().dropFtsIndex(colName, field),
98
128
  };
99
129
  }
100
130
 
101
131
  export function openDB(_dbName: string): DB {
102
132
  return {
103
133
  collection,
134
+ syncStatus: () => native().syncStatus(),
135
+ flushSync: (timeoutMs = 5000) => native().flushSync(timeoutMs),
104
136
  close: () => NativeTalaDB.close(),
105
137
  };
106
138
  }