@taladb/react-native 0.11.2 → 0.11.3

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,6 +1,9 @@
1
1
  #include "TalaDBHostObject.h"
2
2
 
3
3
  #include <memory>
4
+ #include <cmath>
5
+ #include <cstring>
6
+ #include <limits>
4
7
  #include <stdexcept>
5
8
  #include <vector>
6
9
 
@@ -101,30 +104,45 @@ void TalaDBHostObject::extractF32Query(Runtime &rt, const Value &val,
101
104
  }
102
105
  size_t n = size / sizeof(float);
103
106
  out.resize(n);
104
- std::memcpy(out.data(), ab.data(rt), size);
107
+ if (size != 0) std::memcpy(out.data(), ab.data(rt), size);
105
108
  return;
106
109
  }
107
110
 
108
111
  // Float32Array exposes `.buffer`, `.byteOffset`, `.byteLength`. We read
109
112
  // those directly and memcpy from the underlying ArrayBuffer — no JSON,
110
113
  // no per-element Value conversion.
111
- if (obj.hasProperty(rt, "BYTES_PER_ELEMENT")) {
112
- auto bpe = obj.getProperty(rt, "BYTES_PER_ELEMENT");
113
- if (bpe.isNumber() && (int)bpe.getNumber() == 4) {
114
+ auto arrayBuffer = rt.global().getPropertyAsObject(rt, "ArrayBuffer");
115
+ auto isView = arrayBuffer.getPropertyAsFunction(rt, "isView").call(rt, obj);
116
+ if (isView.isBool() && isView.getBool()) {
117
+ auto float32 = rt.global().getPropertyAsFunction(rt, "Float32Array");
118
+ if (obj.instanceOf(rt, float32)) {
114
119
  auto buf = obj.getPropertyAsObject(rt, "buffer");
115
120
  auto ab = buf.getArrayBuffer(rt);
116
121
  auto offsetV = obj.getProperty(rt, "byteOffset");
117
122
  auto lengthV = obj.getProperty(rt, "byteLength");
118
- size_t offset = offsetV.isNumber() ? (size_t)offsetV.getNumber() : 0;
119
- size_t length = lengthV.isNumber() ? (size_t)lengthV.getNumber() : 0;
123
+ auto checkedSize = [&rt](const Value &v) -> size_t {
124
+ if (!v.isNumber()) throw JSError(rt, "invalid typed-array byte range");
125
+ double n = v.getNumber();
126
+ if (!std::isfinite(n) || n < 0 || std::floor(n) != n ||
127
+ n >= static_cast<double>(std::numeric_limits<size_t>::max())) {
128
+ throw JSError(rt, "invalid typed-array byte range");
129
+ }
130
+ return static_cast<size_t>(n);
131
+ };
132
+ size_t offset = checkedSize(offsetV);
133
+ size_t length = checkedSize(lengthV);
134
+ if (offset > ab.size(rt) || length > ab.size(rt) - offset) {
135
+ throw JSError(rt, "typed-array byte range exceeds its buffer");
136
+ }
120
137
  if (length % sizeof(float) != 0) {
121
138
  throw JSError(rt, "typed-array byteLength is not a multiple of 4");
122
139
  }
123
140
  size_t n = length / sizeof(float);
124
141
  out.resize(n);
125
- std::memcpy(out.data(), ab.data(rt) + offset, length);
142
+ if (length != 0) std::memcpy(out.data(), ab.data(rt) + offset, length);
126
143
  return;
127
144
  }
145
+ throw JSError(rt, "query typed array must be Float32Array");
128
146
  }
129
147
 
130
148
  // --- Fallback: number[] — per-element conversion (slow, but correct) ---
@@ -150,9 +168,8 @@ void TalaDBHostObject::extractF32Query(Runtime &rt, const Value &val,
150
168
  //
151
169
  // JSI on RN does not give us a thread-safe way to resolve a Promise from a
152
170
  // background OS thread. We poll the job handle from the JS thread using
153
- // `setImmediate`, which costs ~0.1 ms per tick — negligible next to a
154
- // hundred-millisecond vector search — and the DB work itself runs on the
155
- // background worker the FFI spawned.
171
+ // a bounded timer. The DB work itself runs on the background worker the FFI
172
+ // spawned; idle polling yields to rendering instead of spinning on setImmediate.
156
173
  // ---------------------------------------------------------------------------
157
174
 
158
175
  Value TalaDBHostObject::awaitJobAsPromise(Runtime &rt, TalaDbJob *job, bool parseAsJson) {
@@ -164,7 +181,10 @@ Value TalaDBHostObject::awaitJobAsPromise(Runtime &rt, TalaDbJob *job, bool pars
164
181
 
165
182
  // The job pointer is owned by the executor closure; we null it out on
166
183
  // consumption to prevent double-free.
167
- auto jobBox = std::make_shared<TalaDbJob *>(job);
184
+ auto jobBox = std::shared_ptr<TalaDbJob *>(new TalaDbJob *(job), [](TalaDbJob **slot) {
185
+ if (*slot) taladb_job_cancel(*slot);
186
+ delete slot;
187
+ });
168
188
 
169
189
  auto executor = Function::createFromHostFunction(
170
190
  rt, PropNameID::forAscii(rt, "taladbJobExecutor"), 2,
@@ -175,14 +195,16 @@ Value TalaDBHostObject::awaitJobAsPromise(Runtime &rt, TalaDbJob *job, bool pars
175
195
  auto resolve = std::make_shared<Function>(args[0].getObject(rt).getFunction(rt));
176
196
  auto reject = std::make_shared<Function>(args[1].getObject(rt).getFunction(rt));
177
197
 
178
- // `setImmediate` is provided by React Native. Recurse until the
198
+ // `setTimeout` is provided by React Native. Reschedule until the
179
199
  // worker thread finishes, then take the result on the JS thread.
180
- auto setImmediate = rt.global().getPropertyAsFunction(rt, "setImmediate");
181
- auto setImmediateShared = std::make_shared<Function>(std::move(setImmediate));
200
+ auto timer = rt.global().getPropertyAsFunction(rt, "setTimeout");
201
+ auto timerShared = std::make_shared<Function>(std::move(timer));
182
202
 
183
- // Capture a self-reference so the polling lambda can schedule itself.
203
+ // Only the pending JS tick owns the poller; the callable itself
204
+ // keeps a weak reference so completion releases all JSI captures.
184
205
  auto poller = std::make_shared<std::function<void(Runtime &)>>();
185
- *poller = [jobBox, resolve, reject, setImmediateShared, poller, parseAsJson](Runtime &rt) {
206
+ std::weak_ptr<std::function<void(Runtime &)>> weakPoller = poller;
207
+ *poller = [jobBox, resolve, reject, timerShared, weakPoller, parseAsJson](Runtime &rt) {
186
208
  TalaDbJob *j = *jobBox;
187
209
  if (!j) {
188
210
  return; // already consumed — defensive
@@ -190,13 +212,15 @@ Value TalaDBHostObject::awaitJobAsPromise(Runtime &rt, TalaDbJob *job, bool pars
190
212
  int32_t state = taladb_job_poll(j);
191
213
  if (state == 0) {
192
214
  // Still running — reschedule.
215
+ auto poller = weakPoller.lock();
216
+ if (!poller) return;
193
217
  auto tick = Function::createFromHostFunction(
194
218
  rt, PropNameID::forAscii(rt, "taladbJobTick"), 0,
195
219
  [poller](Runtime &rt, const Value &, const Value *, size_t) -> Value {
196
220
  (*poller)(rt);
197
221
  return Value::undefined();
198
222
  });
199
- setImmediateShared->call(rt, tick);
223
+ timerShared->call(rt, tick, 4.0);
200
224
  return;
201
225
  }
202
226
 
@@ -254,7 +278,7 @@ std::vector<PropNameID> TalaDBHostObject::getPropertyNames(Runtime &rt) {
254
278
  "createVectorIndex", "dropVectorIndex", "upgradeVectorIndex",
255
279
  "findNearest", "findNearestAsync",
256
280
  "searchText", "hybridSearch",
257
- "findAsync",
281
+ "findAsync", "callAsync",
258
282
  "compact",
259
283
  "close",
260
284
  };
@@ -275,6 +299,15 @@ void TalaDBHostObject::set(Runtime &, const PropNameID &, const Value &) {}
275
299
  Value TalaDBHostObject::get(Runtime &rt, const PropNameID &propName) {
276
300
  auto name = propName.utf8(rt);
277
301
 
302
+ if (name == "callAsync") {
303
+ return Function::createFromHostFunction(rt, PropNameID::forAscii(rt, "callAsync"), 2,
304
+ [this](Runtime &rt, const Value &, const Value *args, size_t count) -> Value {
305
+ if (count < 2) throw JSError(rt, "callAsync requires an operation and argument array");
306
+ auto op = args[0].getString(rt).utf8(rt);
307
+ auto json = stringify(rt, args[1]);
308
+ return awaitJobAsPromise(rt, taladb_call_start(db_, op.c_str(), json.c_str()), true);
309
+ });
310
+ }
278
311
  // ------------------------------------------------------------------
279
312
  // insert(collection: string, doc: object): string
280
313
  // ------------------------------------------------------------------
@@ -642,7 +675,7 @@ Value TalaDBHostObject::get(Runtime &rt, const PropNameID &propName) {
642
675
 
643
676
  // ------------------------------------------------------------------
644
677
  // findNearestAsync(...) → Promise<{ document, score }[]>
645
- // Runs on a background thread. JS thread polls via setImmediate.
678
+ // Runs on a background thread. JS thread polls via a bounded timer.
646
679
  // ------------------------------------------------------------------
647
680
  if (name == "findNearestAsync") {
648
681
  return Function::createFromHostFunction(
@@ -12,9 +12,9 @@ namespace taladb {
12
12
  * Installed into the JS runtime as a global:
13
13
  * global.__TalaDB__ = <TalaDBHostObject instance>
14
14
  *
15
- * Every property access returns a JSI Function. All CRUD methods are
16
- * synchronous (the Rust core does no async I/O); `initialize` and `close`
17
- * are async only to conform to the TurboModule spec (they resolve immediately).
15
+ * Every property access returns a JSI Function. `callAsync` and vector search
16
+ * run Rust work on a bounded background executor and resolve a Promise from a
17
+ * short JS timer; legacy synchronous methods remain for binary compatibility.
18
18
  *
19
19
  * JSON is used at the C boundary:
20
20
  * JS object → JSON.stringify → C string → Rust → C string → JSON.parse → JS object
@@ -62,7 +62,7 @@ private:
62
62
  const facebook::jsi::Value &val,
63
63
  std::vector<float> &out);
64
64
 
65
- // Poll a TalaDbJob from the JS thread via setImmediate until done,
65
+ // Poll a TalaDbJob from the JS thread via a short setTimeout until done,
66
66
  // then resolve / reject a Promise with the result / error.
67
67
  static facebook::jsi::Value awaitJobAsPromise(facebook::jsi::Runtime &rt,
68
68
  TalaDbJob *job,
package/cpp/taladb.h CHANGED
@@ -289,6 +289,8 @@ int32_t taladb_job_poll(TalaDbJob *job);
289
289
  char *taladb_job_take_result(TalaDbJob *job);
290
290
 
291
291
  /** Detach the job and free the handle without waiting. */
292
+ TalaDbJob *taladb_call_start(TalaDbHandle *handle, const char *op, const char *args_json);
293
+
292
294
  void taladb_job_cancel(TalaDbJob *job);
293
295
 
294
296
  /**
@@ -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>
@@ -1,6 +1,9 @@
1
1
  #include "TalaDBHostObject.h"
2
2
 
3
3
  #include <memory>
4
+ #include <cmath>
5
+ #include <cstring>
6
+ #include <limits>
4
7
  #include <stdexcept>
5
8
  #include <vector>
6
9
 
@@ -101,30 +104,45 @@ void TalaDBHostObject::extractF32Query(Runtime &rt, const Value &val,
101
104
  }
102
105
  size_t n = size / sizeof(float);
103
106
  out.resize(n);
104
- std::memcpy(out.data(), ab.data(rt), size);
107
+ if (size != 0) std::memcpy(out.data(), ab.data(rt), size);
105
108
  return;
106
109
  }
107
110
 
108
111
  // Float32Array exposes `.buffer`, `.byteOffset`, `.byteLength`. We read
109
112
  // those directly and memcpy from the underlying ArrayBuffer — no JSON,
110
113
  // no per-element Value conversion.
111
- if (obj.hasProperty(rt, "BYTES_PER_ELEMENT")) {
112
- auto bpe = obj.getProperty(rt, "BYTES_PER_ELEMENT");
113
- if (bpe.isNumber() && (int)bpe.getNumber() == 4) {
114
+ auto arrayBuffer = rt.global().getPropertyAsObject(rt, "ArrayBuffer");
115
+ auto isView = arrayBuffer.getPropertyAsFunction(rt, "isView").call(rt, obj);
116
+ if (isView.isBool() && isView.getBool()) {
117
+ auto float32 = rt.global().getPropertyAsFunction(rt, "Float32Array");
118
+ if (obj.instanceOf(rt, float32)) {
114
119
  auto buf = obj.getPropertyAsObject(rt, "buffer");
115
120
  auto ab = buf.getArrayBuffer(rt);
116
121
  auto offsetV = obj.getProperty(rt, "byteOffset");
117
122
  auto lengthV = obj.getProperty(rt, "byteLength");
118
- size_t offset = offsetV.isNumber() ? (size_t)offsetV.getNumber() : 0;
119
- size_t length = lengthV.isNumber() ? (size_t)lengthV.getNumber() : 0;
123
+ auto checkedSize = [&rt](const Value &v) -> size_t {
124
+ if (!v.isNumber()) throw JSError(rt, "invalid typed-array byte range");
125
+ double n = v.getNumber();
126
+ if (!std::isfinite(n) || n < 0 || std::floor(n) != n ||
127
+ n >= static_cast<double>(std::numeric_limits<size_t>::max())) {
128
+ throw JSError(rt, "invalid typed-array byte range");
129
+ }
130
+ return static_cast<size_t>(n);
131
+ };
132
+ size_t offset = checkedSize(offsetV);
133
+ size_t length = checkedSize(lengthV);
134
+ if (offset > ab.size(rt) || length > ab.size(rt) - offset) {
135
+ throw JSError(rt, "typed-array byte range exceeds its buffer");
136
+ }
120
137
  if (length % sizeof(float) != 0) {
121
138
  throw JSError(rt, "typed-array byteLength is not a multiple of 4");
122
139
  }
123
140
  size_t n = length / sizeof(float);
124
141
  out.resize(n);
125
- std::memcpy(out.data(), ab.data(rt) + offset, length);
142
+ if (length != 0) std::memcpy(out.data(), ab.data(rt) + offset, length);
126
143
  return;
127
144
  }
145
+ throw JSError(rt, "query typed array must be Float32Array");
128
146
  }
129
147
 
130
148
  // --- Fallback: number[] — per-element conversion (slow, but correct) ---
@@ -150,9 +168,8 @@ void TalaDBHostObject::extractF32Query(Runtime &rt, const Value &val,
150
168
  //
151
169
  // JSI on RN does not give us a thread-safe way to resolve a Promise from a
152
170
  // background OS thread. We poll the job handle from the JS thread using
153
- // `setImmediate`, which costs ~0.1 ms per tick — negligible next to a
154
- // hundred-millisecond vector search — and the DB work itself runs on the
155
- // background worker the FFI spawned.
171
+ // a bounded timer. The DB work itself runs on the background worker the FFI
172
+ // spawned; idle polling yields to rendering instead of spinning on setImmediate.
156
173
  // ---------------------------------------------------------------------------
157
174
 
158
175
  Value TalaDBHostObject::awaitJobAsPromise(Runtime &rt, TalaDbJob *job, bool parseAsJson) {
@@ -164,7 +181,10 @@ Value TalaDBHostObject::awaitJobAsPromise(Runtime &rt, TalaDbJob *job, bool pars
164
181
 
165
182
  // The job pointer is owned by the executor closure; we null it out on
166
183
  // consumption to prevent double-free.
167
- auto jobBox = std::make_shared<TalaDbJob *>(job);
184
+ auto jobBox = std::shared_ptr<TalaDbJob *>(new TalaDbJob *(job), [](TalaDbJob **slot) {
185
+ if (*slot) taladb_job_cancel(*slot);
186
+ delete slot;
187
+ });
168
188
 
169
189
  auto executor = Function::createFromHostFunction(
170
190
  rt, PropNameID::forAscii(rt, "taladbJobExecutor"), 2,
@@ -175,14 +195,16 @@ Value TalaDBHostObject::awaitJobAsPromise(Runtime &rt, TalaDbJob *job, bool pars
175
195
  auto resolve = std::make_shared<Function>(args[0].getObject(rt).getFunction(rt));
176
196
  auto reject = std::make_shared<Function>(args[1].getObject(rt).getFunction(rt));
177
197
 
178
- // `setImmediate` is provided by React Native. Recurse until the
198
+ // `setTimeout` is provided by React Native. Reschedule until the
179
199
  // worker thread finishes, then take the result on the JS thread.
180
- auto setImmediate = rt.global().getPropertyAsFunction(rt, "setImmediate");
181
- auto setImmediateShared = std::make_shared<Function>(std::move(setImmediate));
200
+ auto timer = rt.global().getPropertyAsFunction(rt, "setTimeout");
201
+ auto timerShared = std::make_shared<Function>(std::move(timer));
182
202
 
183
- // Capture a self-reference so the polling lambda can schedule itself.
203
+ // Only the pending JS tick owns the poller; the callable itself
204
+ // keeps a weak reference so completion releases all JSI captures.
184
205
  auto poller = std::make_shared<std::function<void(Runtime &)>>();
185
- *poller = [jobBox, resolve, reject, setImmediateShared, poller, parseAsJson](Runtime &rt) {
206
+ std::weak_ptr<std::function<void(Runtime &)>> weakPoller = poller;
207
+ *poller = [jobBox, resolve, reject, timerShared, weakPoller, parseAsJson](Runtime &rt) {
186
208
  TalaDbJob *j = *jobBox;
187
209
  if (!j) {
188
210
  return; // already consumed — defensive
@@ -190,13 +212,15 @@ Value TalaDBHostObject::awaitJobAsPromise(Runtime &rt, TalaDbJob *job, bool pars
190
212
  int32_t state = taladb_job_poll(j);
191
213
  if (state == 0) {
192
214
  // Still running — reschedule.
215
+ auto poller = weakPoller.lock();
216
+ if (!poller) return;
193
217
  auto tick = Function::createFromHostFunction(
194
218
  rt, PropNameID::forAscii(rt, "taladbJobTick"), 0,
195
219
  [poller](Runtime &rt, const Value &, const Value *, size_t) -> Value {
196
220
  (*poller)(rt);
197
221
  return Value::undefined();
198
222
  });
199
- setImmediateShared->call(rt, tick);
223
+ timerShared->call(rt, tick, 4.0);
200
224
  return;
201
225
  }
202
226
 
@@ -254,7 +278,7 @@ std::vector<PropNameID> TalaDBHostObject::getPropertyNames(Runtime &rt) {
254
278
  "createVectorIndex", "dropVectorIndex", "upgradeVectorIndex",
255
279
  "findNearest", "findNearestAsync",
256
280
  "searchText", "hybridSearch",
257
- "findAsync",
281
+ "findAsync", "callAsync",
258
282
  "compact",
259
283
  "close",
260
284
  };
@@ -275,6 +299,15 @@ void TalaDBHostObject::set(Runtime &, const PropNameID &, const Value &) {}
275
299
  Value TalaDBHostObject::get(Runtime &rt, const PropNameID &propName) {
276
300
  auto name = propName.utf8(rt);
277
301
 
302
+ if (name == "callAsync") {
303
+ return Function::createFromHostFunction(rt, PropNameID::forAscii(rt, "callAsync"), 2,
304
+ [this](Runtime &rt, const Value &, const Value *args, size_t count) -> Value {
305
+ if (count < 2) throw JSError(rt, "callAsync requires an operation and argument array");
306
+ auto op = args[0].getString(rt).utf8(rt);
307
+ auto json = stringify(rt, args[1]);
308
+ return awaitJobAsPromise(rt, taladb_call_start(db_, op.c_str(), json.c_str()), true);
309
+ });
310
+ }
278
311
  // ------------------------------------------------------------------
279
312
  // insert(collection: string, doc: object): string
280
313
  // ------------------------------------------------------------------
@@ -642,7 +675,7 @@ Value TalaDBHostObject::get(Runtime &rt, const PropNameID &propName) {
642
675
 
643
676
  // ------------------------------------------------------------------
644
677
  // findNearestAsync(...) → Promise<{ document, score }[]>
645
- // Runs on a background thread. JS thread polls via setImmediate.
678
+ // Runs on a background thread. JS thread polls via a bounded timer.
646
679
  // ------------------------------------------------------------------
647
680
  if (name == "findNearestAsync") {
648
681
  return Function::createFromHostFunction(
@@ -12,9 +12,9 @@ namespace taladb {
12
12
  * Installed into the JS runtime as a global:
13
13
  * global.__TalaDB__ = <TalaDBHostObject instance>
14
14
  *
15
- * Every property access returns a JSI Function. All CRUD methods are
16
- * synchronous (the Rust core does no async I/O); `initialize` and `close`
17
- * are async only to conform to the TurboModule spec (they resolve immediately).
15
+ * Every property access returns a JSI Function. `callAsync` and vector search
16
+ * run Rust work on a bounded background executor and resolve a Promise from a
17
+ * short JS timer; legacy synchronous methods remain for binary compatibility.
18
18
  *
19
19
  * JSON is used at the C boundary:
20
20
  * JS object → JSON.stringify → C string → Rust → C string → JSON.parse → JS object
@@ -62,7 +62,7 @@ private:
62
62
  const facebook::jsi::Value &val,
63
63
  std::vector<float> &out);
64
64
 
65
- // Poll a TalaDbJob from the JS thread via setImmediate until done,
65
+ // Poll a TalaDbJob from the JS thread via a short setTimeout until done,
66
66
  // then resolve / reject a Promise with the result / error.
67
67
  static facebook::jsi::Value awaitJobAsPromise(facebook::jsi::Runtime &rt,
68
68
  TalaDbJob *job,
@@ -289,6 +289,8 @@ int32_t taladb_job_poll(TalaDbJob *job);
289
289
  char *taladb_job_take_result(TalaDbJob *job);
290
290
 
291
291
  /** Detach the job and free the handle without waiting. */
292
+ TalaDbJob *taladb_call_start(TalaDbHandle *handle, const char *op, const char *args_json);
293
+
292
294
  void taladb_job_cancel(TalaDbJob *job);
293
295
 
294
296
  /**
@@ -1,6 +1,9 @@
1
1
  #include "TalaDBHostObject.h"
2
2
 
3
3
  #include <memory>
4
+ #include <cmath>
5
+ #include <cstring>
6
+ #include <limits>
4
7
  #include <stdexcept>
5
8
  #include <vector>
6
9
 
@@ -101,30 +104,45 @@ void TalaDBHostObject::extractF32Query(Runtime &rt, const Value &val,
101
104
  }
102
105
  size_t n = size / sizeof(float);
103
106
  out.resize(n);
104
- std::memcpy(out.data(), ab.data(rt), size);
107
+ if (size != 0) std::memcpy(out.data(), ab.data(rt), size);
105
108
  return;
106
109
  }
107
110
 
108
111
  // Float32Array exposes `.buffer`, `.byteOffset`, `.byteLength`. We read
109
112
  // those directly and memcpy from the underlying ArrayBuffer — no JSON,
110
113
  // no per-element Value conversion.
111
- if (obj.hasProperty(rt, "BYTES_PER_ELEMENT")) {
112
- auto bpe = obj.getProperty(rt, "BYTES_PER_ELEMENT");
113
- if (bpe.isNumber() && (int)bpe.getNumber() == 4) {
114
+ auto arrayBuffer = rt.global().getPropertyAsObject(rt, "ArrayBuffer");
115
+ auto isView = arrayBuffer.getPropertyAsFunction(rt, "isView").call(rt, obj);
116
+ if (isView.isBool() && isView.getBool()) {
117
+ auto float32 = rt.global().getPropertyAsFunction(rt, "Float32Array");
118
+ if (obj.instanceOf(rt, float32)) {
114
119
  auto buf = obj.getPropertyAsObject(rt, "buffer");
115
120
  auto ab = buf.getArrayBuffer(rt);
116
121
  auto offsetV = obj.getProperty(rt, "byteOffset");
117
122
  auto lengthV = obj.getProperty(rt, "byteLength");
118
- size_t offset = offsetV.isNumber() ? (size_t)offsetV.getNumber() : 0;
119
- size_t length = lengthV.isNumber() ? (size_t)lengthV.getNumber() : 0;
123
+ auto checkedSize = [&rt](const Value &v) -> size_t {
124
+ if (!v.isNumber()) throw JSError(rt, "invalid typed-array byte range");
125
+ double n = v.getNumber();
126
+ if (!std::isfinite(n) || n < 0 || std::floor(n) != n ||
127
+ n >= static_cast<double>(std::numeric_limits<size_t>::max())) {
128
+ throw JSError(rt, "invalid typed-array byte range");
129
+ }
130
+ return static_cast<size_t>(n);
131
+ };
132
+ size_t offset = checkedSize(offsetV);
133
+ size_t length = checkedSize(lengthV);
134
+ if (offset > ab.size(rt) || length > ab.size(rt) - offset) {
135
+ throw JSError(rt, "typed-array byte range exceeds its buffer");
136
+ }
120
137
  if (length % sizeof(float) != 0) {
121
138
  throw JSError(rt, "typed-array byteLength is not a multiple of 4");
122
139
  }
123
140
  size_t n = length / sizeof(float);
124
141
  out.resize(n);
125
- std::memcpy(out.data(), ab.data(rt) + offset, length);
142
+ if (length != 0) std::memcpy(out.data(), ab.data(rt) + offset, length);
126
143
  return;
127
144
  }
145
+ throw JSError(rt, "query typed array must be Float32Array");
128
146
  }
129
147
 
130
148
  // --- Fallback: number[] — per-element conversion (slow, but correct) ---
@@ -150,9 +168,8 @@ void TalaDBHostObject::extractF32Query(Runtime &rt, const Value &val,
150
168
  //
151
169
  // JSI on RN does not give us a thread-safe way to resolve a Promise from a
152
170
  // background OS thread. We poll the job handle from the JS thread using
153
- // `setImmediate`, which costs ~0.1 ms per tick — negligible next to a
154
- // hundred-millisecond vector search — and the DB work itself runs on the
155
- // background worker the FFI spawned.
171
+ // a bounded timer. The DB work itself runs on the background worker the FFI
172
+ // spawned; idle polling yields to rendering instead of spinning on setImmediate.
156
173
  // ---------------------------------------------------------------------------
157
174
 
158
175
  Value TalaDBHostObject::awaitJobAsPromise(Runtime &rt, TalaDbJob *job, bool parseAsJson) {
@@ -164,7 +181,10 @@ Value TalaDBHostObject::awaitJobAsPromise(Runtime &rt, TalaDbJob *job, bool pars
164
181
 
165
182
  // The job pointer is owned by the executor closure; we null it out on
166
183
  // consumption to prevent double-free.
167
- auto jobBox = std::make_shared<TalaDbJob *>(job);
184
+ auto jobBox = std::shared_ptr<TalaDbJob *>(new TalaDbJob *(job), [](TalaDbJob **slot) {
185
+ if (*slot) taladb_job_cancel(*slot);
186
+ delete slot;
187
+ });
168
188
 
169
189
  auto executor = Function::createFromHostFunction(
170
190
  rt, PropNameID::forAscii(rt, "taladbJobExecutor"), 2,
@@ -175,14 +195,16 @@ Value TalaDBHostObject::awaitJobAsPromise(Runtime &rt, TalaDbJob *job, bool pars
175
195
  auto resolve = std::make_shared<Function>(args[0].getObject(rt).getFunction(rt));
176
196
  auto reject = std::make_shared<Function>(args[1].getObject(rt).getFunction(rt));
177
197
 
178
- // `setImmediate` is provided by React Native. Recurse until the
198
+ // `setTimeout` is provided by React Native. Reschedule until the
179
199
  // worker thread finishes, then take the result on the JS thread.
180
- auto setImmediate = rt.global().getPropertyAsFunction(rt, "setImmediate");
181
- auto setImmediateShared = std::make_shared<Function>(std::move(setImmediate));
200
+ auto timer = rt.global().getPropertyAsFunction(rt, "setTimeout");
201
+ auto timerShared = std::make_shared<Function>(std::move(timer));
182
202
 
183
- // Capture a self-reference so the polling lambda can schedule itself.
203
+ // Only the pending JS tick owns the poller; the callable itself
204
+ // keeps a weak reference so completion releases all JSI captures.
184
205
  auto poller = std::make_shared<std::function<void(Runtime &)>>();
185
- *poller = [jobBox, resolve, reject, setImmediateShared, poller, parseAsJson](Runtime &rt) {
206
+ std::weak_ptr<std::function<void(Runtime &)>> weakPoller = poller;
207
+ *poller = [jobBox, resolve, reject, timerShared, weakPoller, parseAsJson](Runtime &rt) {
186
208
  TalaDbJob *j = *jobBox;
187
209
  if (!j) {
188
210
  return; // already consumed — defensive
@@ -190,13 +212,15 @@ Value TalaDBHostObject::awaitJobAsPromise(Runtime &rt, TalaDbJob *job, bool pars
190
212
  int32_t state = taladb_job_poll(j);
191
213
  if (state == 0) {
192
214
  // Still running — reschedule.
215
+ auto poller = weakPoller.lock();
216
+ if (!poller) return;
193
217
  auto tick = Function::createFromHostFunction(
194
218
  rt, PropNameID::forAscii(rt, "taladbJobTick"), 0,
195
219
  [poller](Runtime &rt, const Value &, const Value *, size_t) -> Value {
196
220
  (*poller)(rt);
197
221
  return Value::undefined();
198
222
  });
199
- setImmediateShared->call(rt, tick);
223
+ timerShared->call(rt, tick, 4.0);
200
224
  return;
201
225
  }
202
226
 
@@ -254,7 +278,7 @@ std::vector<PropNameID> TalaDBHostObject::getPropertyNames(Runtime &rt) {
254
278
  "createVectorIndex", "dropVectorIndex", "upgradeVectorIndex",
255
279
  "findNearest", "findNearestAsync",
256
280
  "searchText", "hybridSearch",
257
- "findAsync",
281
+ "findAsync", "callAsync",
258
282
  "compact",
259
283
  "close",
260
284
  };
@@ -275,6 +299,15 @@ void TalaDBHostObject::set(Runtime &, const PropNameID &, const Value &) {}
275
299
  Value TalaDBHostObject::get(Runtime &rt, const PropNameID &propName) {
276
300
  auto name = propName.utf8(rt);
277
301
 
302
+ if (name == "callAsync") {
303
+ return Function::createFromHostFunction(rt, PropNameID::forAscii(rt, "callAsync"), 2,
304
+ [this](Runtime &rt, const Value &, const Value *args, size_t count) -> Value {
305
+ if (count < 2) throw JSError(rt, "callAsync requires an operation and argument array");
306
+ auto op = args[0].getString(rt).utf8(rt);
307
+ auto json = stringify(rt, args[1]);
308
+ return awaitJobAsPromise(rt, taladb_call_start(db_, op.c_str(), json.c_str()), true);
309
+ });
310
+ }
278
311
  // ------------------------------------------------------------------
279
312
  // insert(collection: string, doc: object): string
280
313
  // ------------------------------------------------------------------
@@ -642,7 +675,7 @@ Value TalaDBHostObject::get(Runtime &rt, const PropNameID &propName) {
642
675
 
643
676
  // ------------------------------------------------------------------
644
677
  // findNearestAsync(...) → Promise<{ document, score }[]>
645
- // Runs on a background thread. JS thread polls via setImmediate.
678
+ // Runs on a background thread. JS thread polls via a bounded timer.
646
679
  // ------------------------------------------------------------------
647
680
  if (name == "findNearestAsync") {
648
681
  return Function::createFromHostFunction(
@@ -12,9 +12,9 @@ namespace taladb {
12
12
  * Installed into the JS runtime as a global:
13
13
  * global.__TalaDB__ = <TalaDBHostObject instance>
14
14
  *
15
- * Every property access returns a JSI Function. All CRUD methods are
16
- * synchronous (the Rust core does no async I/O); `initialize` and `close`
17
- * are async only to conform to the TurboModule spec (they resolve immediately).
15
+ * Every property access returns a JSI Function. `callAsync` and vector search
16
+ * run Rust work on a bounded background executor and resolve a Promise from a
17
+ * short JS timer; legacy synchronous methods remain for binary compatibility.
18
18
  *
19
19
  * JSON is used at the C boundary:
20
20
  * JS object → JSON.stringify → C string → Rust → C string → JSON.parse → JS object
@@ -62,7 +62,7 @@ private:
62
62
  const facebook::jsi::Value &val,
63
63
  std::vector<float> &out);
64
64
 
65
- // Poll a TalaDbJob from the JS thread via setImmediate until done,
65
+ // Poll a TalaDbJob from the JS thread via a short setTimeout until done,
66
66
  // then resolve / reject a Promise with the result / error.
67
67
  static facebook::jsi::Value awaitJobAsPromise(facebook::jsi::Runtime &rt,
68
68
  TalaDbJob *job,
@@ -289,6 +289,8 @@ int32_t taladb_job_poll(TalaDbJob *job);
289
289
  char *taladb_job_take_result(TalaDbJob *job);
290
290
 
291
291
  /** Detach the job and free the handle without waiting. */
292
+ TalaDbJob *taladb_call_start(TalaDbHandle *handle, const char *op, const char *args_json);
293
+
292
294
  void taladb_job_cancel(TalaDbJob *job);
293
295
 
294
296
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taladb/react-native",
3
- "version": "0.11.2",
3
+ "version": "0.11.3",
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",
@@ -49,7 +49,7 @@
49
49
  "react-native": "^0.83.0"
50
50
  },
51
51
  "dependencies": {
52
- "taladb": "0.11.2"
52
+ "taladb": "0.11.3"
53
53
  },
54
54
  "scripts": {
55
55
  "typecheck": "tsc --noEmit",