@taladb/react-native 0.8.4 → 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.
@@ -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
  }
@@ -245,12 +246,15 @@ std::vector<PropNameID> TalaDBHostObject::getPropertyNames(Runtime &rt) {
245
246
  "deleteOne", "deleteMany",
246
247
  "count",
247
248
  "aggregate",
249
+ "exportChanges", "importChanges", "listCollectionNames",
248
250
  "createIndex", "dropIndex",
251
+ "createCompoundIndex", "dropCompoundIndex",
249
252
  "createFtsIndex", "dropFtsIndex",
250
253
  "createVectorIndex", "dropVectorIndex", "upgradeVectorIndex",
251
254
  "findNearest", "findNearestAsync",
252
255
  "findAsync",
253
256
  "compact",
257
+ "syncStatus", "flushSync",
254
258
  "close",
255
259
  };
256
260
  std::vector<PropNameID> result;
@@ -444,6 +448,51 @@ Value TalaDBHostObject::get(Runtime &rt, const PropNameID &propName) {
444
448
  });
445
449
  }
446
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
+
447
496
  // ------------------------------------------------------------------
448
497
  // createIndex / dropIndex / createFtsIndex / dropFtsIndex
449
498
  // ------------------------------------------------------------------
@@ -463,6 +512,24 @@ Value TalaDBHostObject::get(Runtime &rt, const PropNameID &propName) {
463
512
  });
464
513
  }
465
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
+
466
533
  // ------------------------------------------------------------------
467
534
  // createVectorIndex(collection, field, dimensions, opts?): void
468
535
  // opts: { metric?: 'cosine'|'dot'|'euclidean', hnsw?: HnswOptions }
@@ -623,6 +690,26 @@ Value TalaDBHostObject::get(Runtime &rt, const PropNameID &propName) {
623
690
  });
624
691
  }
625
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
+
626
713
  // ------------------------------------------------------------------
627
714
  // close(): void (synchronous — the destructor does the real work)
628
715
  // ------------------------------------------------------------------
@@ -631,6 +718,7 @@ Value TalaDBHostObject::get(Runtime &rt, const PropNameID &propName) {
631
718
  rt, PropNameID::forAscii(rt, "close"), 0,
632
719
  [this](Runtime &rt, const Value &, const Value *, size_t) -> Value {
633
720
  if (db_) {
721
+ taladb_sync_flush(db_, 5000);
634
722
  taladb_close(db_);
635
723
  db_ = nullptr;
636
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
@@ -154,6 +154,32 @@ char *taladb_aggregate(TalaDbHandle *handle,
154
154
  const char *collection,
155
155
  const char *pipeline_json);
156
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
+
157
183
  /* -------------------------------------------------------------------------
158
184
  * Index management
159
185
  * ---------------------------------------------------------------------- */
@@ -163,6 +189,10 @@ void taladb_drop_index (TalaDbHandle *handle, const char *collection, const
163
189
  void taladb_create_fts_index(TalaDbHandle *handle, const char *collection, const char *field);
164
190
  void taladb_drop_fts_index (TalaDbHandle *handle, const char *collection, const char *field);
165
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
+
166
196
  /* -------------------------------------------------------------------------
167
197
  * Vector index management
168
198
  *
@@ -199,6 +229,9 @@ char *taladb_find_nearest(TalaDbHandle *handle,
199
229
  size_t top_k,
200
230
  const char *filter_json);
201
231
 
232
+ char *taladb_sync_status(TalaDbHandle *handle);
233
+ int32_t taladb_sync_flush(TalaDbHandle *handle, uint64_t timeout_ms);
234
+
202
235
  /* -------------------------------------------------------------------------
203
236
  * Async job API — run heavy queries on a background thread.
204
237
  *
@@ -208,11 +241,7 @@ char *taladb_find_nearest(TalaDbHandle *handle,
208
241
  * while (taladb_job_poll(j) == 0) { } // yield to JS event loop
209
242
  * char *json = taladb_job_take_result(j); // frees the job
210
243
  *
211
- * Lifetime contract
212
- * -----------------
213
- * The handle passed to `*_start` MUST remain valid until the job has been
214
- * taken (via take_result) or cancelled (via cancel). The C++ HostObject
215
- * 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.
216
245
  * ---------------------------------------------------------------------- */
217
246
 
218
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 ---
@@ -6,38 +6,38 @@
6
6
  <array>
7
7
  <dict>
8
8
  <key>BinaryPath</key>
9
- <string>libtaladb_ffi.a</string>
9
+ <string>libtaladb_ffi_sim.a</string>
10
10
  <key>HeadersPath</key>
11
11
  <string>Headers</string>
12
12
  <key>LibraryIdentifier</key>
13
- <string>ios-arm64</string>
13
+ <string>ios-arm64_x86_64-simulator</string>
14
14
  <key>LibraryPath</key>
15
- <string>libtaladb_ffi.a</string>
15
+ <string>libtaladb_ffi_sim.a</string>
16
16
  <key>SupportedArchitectures</key>
17
17
  <array>
18
18
  <string>arm64</string>
19
+ <string>x86_64</string>
19
20
  </array>
20
21
  <key>SupportedPlatform</key>
21
22
  <string>ios</string>
23
+ <key>SupportedPlatformVariant</key>
24
+ <string>simulator</string>
22
25
  </dict>
23
26
  <dict>
24
27
  <key>BinaryPath</key>
25
- <string>libtaladb_ffi_sim.a</string>
28
+ <string>libtaladb_ffi.a</string>
26
29
  <key>HeadersPath</key>
27
30
  <string>Headers</string>
28
31
  <key>LibraryIdentifier</key>
29
- <string>ios-arm64_x86_64-simulator</string>
32
+ <string>ios-arm64</string>
30
33
  <key>LibraryPath</key>
31
- <string>libtaladb_ffi_sim.a</string>
34
+ <string>libtaladb_ffi.a</string>
32
35
  <key>SupportedArchitectures</key>
33
36
  <array>
34
37
  <string>arm64</string>
35
- <string>x86_64</string>
36
38
  </array>
37
39
  <key>SupportedPlatform</key>
38
40
  <string>ios</string>
39
- <key>SupportedPlatformVariant</key>
40
- <string>simulator</string>
41
41
  </dict>
42
42
  </array>
43
43
  <key>CFBundlePackageType</key>
@@ -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
  }
@@ -245,12 +246,15 @@ std::vector<PropNameID> TalaDBHostObject::getPropertyNames(Runtime &rt) {
245
246
  "deleteOne", "deleteMany",
246
247
  "count",
247
248
  "aggregate",
249
+ "exportChanges", "importChanges", "listCollectionNames",
248
250
  "createIndex", "dropIndex",
251
+ "createCompoundIndex", "dropCompoundIndex",
249
252
  "createFtsIndex", "dropFtsIndex",
250
253
  "createVectorIndex", "dropVectorIndex", "upgradeVectorIndex",
251
254
  "findNearest", "findNearestAsync",
252
255
  "findAsync",
253
256
  "compact",
257
+ "syncStatus", "flushSync",
254
258
  "close",
255
259
  };
256
260
  std::vector<PropNameID> result;
@@ -444,6 +448,51 @@ Value TalaDBHostObject::get(Runtime &rt, const PropNameID &propName) {
444
448
  });
445
449
  }
446
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
+
447
496
  // ------------------------------------------------------------------
448
497
  // createIndex / dropIndex / createFtsIndex / dropFtsIndex
449
498
  // ------------------------------------------------------------------
@@ -463,6 +512,24 @@ Value TalaDBHostObject::get(Runtime &rt, const PropNameID &propName) {
463
512
  });
464
513
  }
465
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
+
466
533
  // ------------------------------------------------------------------
467
534
  // createVectorIndex(collection, field, dimensions, opts?): void
468
535
  // opts: { metric?: 'cosine'|'dot'|'euclidean', hnsw?: HnswOptions }
@@ -623,6 +690,26 @@ Value TalaDBHostObject::get(Runtime &rt, const PropNameID &propName) {
623
690
  });
624
691
  }
625
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
+
626
713
  // ------------------------------------------------------------------
627
714
  // close(): void (synchronous — the destructor does the real work)
628
715
  // ------------------------------------------------------------------
@@ -631,6 +718,7 @@ Value TalaDBHostObject::get(Runtime &rt, const PropNameID &propName) {
631
718
  rt, PropNameID::forAscii(rt, "close"), 0,
632
719
  [this](Runtime &rt, const Value &, const Value *, size_t) -> Value {
633
720
  if (db_) {
721
+ taladb_sync_flush(db_, 5000);
634
722
  taladb_close(db_);
635
723
  db_ = nullptr;
636
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
  }
@@ -154,6 +154,32 @@ char *taladb_aggregate(TalaDbHandle *handle,
154
154
  const char *collection,
155
155
  const char *pipeline_json);
156
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
+
157
183
  /* -------------------------------------------------------------------------
158
184
  * Index management
159
185
  * ---------------------------------------------------------------------- */
@@ -163,6 +189,10 @@ void taladb_drop_index (TalaDbHandle *handle, const char *collection, const
163
189
  void taladb_create_fts_index(TalaDbHandle *handle, const char *collection, const char *field);
164
190
  void taladb_drop_fts_index (TalaDbHandle *handle, const char *collection, const char *field);
165
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
+
166
196
  /* -------------------------------------------------------------------------
167
197
  * Vector index management
168
198
  *
@@ -199,6 +229,9 @@ char *taladb_find_nearest(TalaDbHandle *handle,
199
229
  size_t top_k,
200
230
  const char *filter_json);
201
231
 
232
+ char *taladb_sync_status(TalaDbHandle *handle);
233
+ int32_t taladb_sync_flush(TalaDbHandle *handle, uint64_t timeout_ms);
234
+
202
235
  /* -------------------------------------------------------------------------
203
236
  * Async job API — run heavy queries on a background thread.
204
237
  *
@@ -208,11 +241,7 @@ char *taladb_find_nearest(TalaDbHandle *handle,
208
241
  * while (taladb_job_poll(j) == 0) { } // yield to JS event loop
209
242
  * char *json = taladb_job_take_result(j); // frees the job
210
243
  *
211
- * Lifetime contract
212
- * -----------------
213
- * The handle passed to `*_start` MUST remain valid until the job has been
214
- * taken (via take_result) or cancelled (via cancel). The C++ HostObject
215
- * 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.
216
245
  * ---------------------------------------------------------------------- */
217
246
 
218
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
  }
@@ -245,12 +246,15 @@ std::vector<PropNameID> TalaDBHostObject::getPropertyNames(Runtime &rt) {
245
246
  "deleteOne", "deleteMany",
246
247
  "count",
247
248
  "aggregate",
249
+ "exportChanges", "importChanges", "listCollectionNames",
248
250
  "createIndex", "dropIndex",
251
+ "createCompoundIndex", "dropCompoundIndex",
249
252
  "createFtsIndex", "dropFtsIndex",
250
253
  "createVectorIndex", "dropVectorIndex", "upgradeVectorIndex",
251
254
  "findNearest", "findNearestAsync",
252
255
  "findAsync",
253
256
  "compact",
257
+ "syncStatus", "flushSync",
254
258
  "close",
255
259
  };
256
260
  std::vector<PropNameID> result;
@@ -444,6 +448,51 @@ Value TalaDBHostObject::get(Runtime &rt, const PropNameID &propName) {
444
448
  });
445
449
  }
446
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
+
447
496
  // ------------------------------------------------------------------
448
497
  // createIndex / dropIndex / createFtsIndex / dropFtsIndex
449
498
  // ------------------------------------------------------------------
@@ -463,6 +512,24 @@ Value TalaDBHostObject::get(Runtime &rt, const PropNameID &propName) {
463
512
  });
464
513
  }
465
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
+
466
533
  // ------------------------------------------------------------------
467
534
  // createVectorIndex(collection, field, dimensions, opts?): void
468
535
  // opts: { metric?: 'cosine'|'dot'|'euclidean', hnsw?: HnswOptions }
@@ -623,6 +690,26 @@ Value TalaDBHostObject::get(Runtime &rt, const PropNameID &propName) {
623
690
  });
624
691
  }
625
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
+
626
713
  // ------------------------------------------------------------------
627
714
  // close(): void (synchronous — the destructor does the real work)
628
715
  // ------------------------------------------------------------------
@@ -631,6 +718,7 @@ Value TalaDBHostObject::get(Runtime &rt, const PropNameID &propName) {
631
718
  rt, PropNameID::forAscii(rt, "close"), 0,
632
719
  [this](Runtime &rt, const Value &, const Value *, size_t) -> Value {
633
720
  if (db_) {
721
+ taladb_sync_flush(db_, 5000);
634
722
  taladb_close(db_);
635
723
  db_ = nullptr;
636
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
  }
@@ -154,6 +154,32 @@ char *taladb_aggregate(TalaDbHandle *handle,
154
154
  const char *collection,
155
155
  const char *pipeline_json);
156
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
+
157
183
  /* -------------------------------------------------------------------------
158
184
  * Index management
159
185
  * ---------------------------------------------------------------------- */
@@ -163,6 +189,10 @@ void taladb_drop_index (TalaDbHandle *handle, const char *collection, const
163
189
  void taladb_create_fts_index(TalaDbHandle *handle, const char *collection, const char *field);
164
190
  void taladb_drop_fts_index (TalaDbHandle *handle, const char *collection, const char *field);
165
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
+
166
196
  /* -------------------------------------------------------------------------
167
197
  * Vector index management
168
198
  *
@@ -199,6 +229,9 @@ char *taladb_find_nearest(TalaDbHandle *handle,
199
229
  size_t top_k,
200
230
  const char *filter_json);
201
231
 
232
+ char *taladb_sync_status(TalaDbHandle *handle);
233
+ int32_t taladb_sync_flush(TalaDbHandle *handle, uint64_t timeout_ms);
234
+
202
235
  /* -------------------------------------------------------------------------
203
236
  * Async job API — run heavy queries on a background thread.
204
237
  *
@@ -208,11 +241,7 @@ char *taladb_find_nearest(TalaDbHandle *handle,
208
241
  * while (taladb_job_poll(j) == 0) { } // yield to JS event loop
209
242
  * char *json = taladb_job_take_result(j); // frees the job
210
243
  *
211
- * Lifetime contract
212
- * -----------------
213
- * The handle passed to `*_start` MUST remain valid until the job has been
214
- * taken (via take_result) or cancelled (via cancel). The C++ HostObject
215
- * 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.
216
245
  * ---------------------------------------------------------------------- */
217
246
 
218
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.4",
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",
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
  }