@taladb/react-native 0.7.3 → 0.7.4

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,5 +1,6 @@
1
1
  #include "TalaDBHostObject.h"
2
2
 
3
+ #include <memory>
3
4
  #include <stdexcept>
4
5
  #include <vector>
5
6
 
@@ -7,6 +8,28 @@ using namespace facebook::jsi;
7
8
 
8
9
  namespace taladb {
9
10
 
11
+ namespace {
12
+
13
+ // Read the error string set by the most recent FFI call. Safe to call even
14
+ // when no error has been set — returns an empty string in that case.
15
+ std::string lastError() {
16
+ const char *msg = taladb_last_error();
17
+ return msg ? std::string(msg) : std::string();
18
+ }
19
+
20
+ // Build a JSError with the FFI last-error message appended.
21
+ JSError ffiError(Runtime &rt, const char *prefix) {
22
+ std::string msg = prefix;
23
+ std::string last = lastError();
24
+ if (!last.empty()) {
25
+ msg += ": ";
26
+ msg += last;
27
+ }
28
+ return JSError(rt, msg);
29
+ }
30
+
31
+ } // namespace
32
+
10
33
  // ---------------------------------------------------------------------------
11
34
  // Constructor / Destructor
12
35
  // ---------------------------------------------------------------------------
@@ -57,6 +80,159 @@ std::string TalaDBHostObject::valueToFilterJson(Runtime &rt, const Value &val) {
57
80
  return stringify(rt, val);
58
81
  }
59
82
 
83
+ // ---------------------------------------------------------------------------
84
+ // Float32Array fast path
85
+ // ---------------------------------------------------------------------------
86
+
87
+ void TalaDBHostObject::extractF32Query(Runtime &rt, const Value &val,
88
+ std::vector<float> &out) {
89
+ if (!val.isObject()) {
90
+ throw JSError(rt, "query must be a Float32Array or number[]");
91
+ }
92
+ auto obj = val.getObject(rt);
93
+
94
+ // --- Fast path: Float32Array — zero-copy read from the underlying ArrayBuffer ---
95
+ if (obj.isArrayBuffer(rt)) {
96
+ // Raw ArrayBuffer of bytes (length must be a multiple of 4)
97
+ auto ab = obj.getArrayBuffer(rt);
98
+ size_t size = ab.size(rt);
99
+ if (size % sizeof(float) != 0) {
100
+ throw JSError(rt, "ArrayBuffer length is not a multiple of 4 bytes");
101
+ }
102
+ size_t n = size / sizeof(float);
103
+ out.resize(n);
104
+ std::memcpy(out.data(), ab.data(rt), size);
105
+ return;
106
+ }
107
+
108
+ // Float32Array exposes `.buffer`, `.byteOffset`, `.byteLength`. We read
109
+ // those directly and memcpy from the underlying ArrayBuffer — no JSON,
110
+ // 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 buf = obj.getPropertyAsObject(rt, "buffer");
115
+ auto ab = buf.getArrayBuffer(rt);
116
+ auto offsetV = obj.getProperty(rt, "byteOffset");
117
+ 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;
120
+ if (length % sizeof(float) != 0) {
121
+ throw JSError(rt, "typed-array byteLength is not a multiple of 4");
122
+ }
123
+ size_t n = length / sizeof(float);
124
+ out.resize(n);
125
+ std::memcpy(out.data(), ab.data(rt) + offset, length);
126
+ return;
127
+ }
128
+ }
129
+
130
+ // --- Fallback: number[] — per-element conversion (slow, but correct) ---
131
+ if (obj.isArray(rt)) {
132
+ auto arr = obj.getArray(rt);
133
+ size_t len = arr.size(rt);
134
+ out.resize(len);
135
+ for (size_t i = 0; i < len; i++) {
136
+ auto v = arr.getValueAtIndex(rt, i);
137
+ if (!v.isNumber()) {
138
+ throw JSError(rt, "query array must contain only numbers");
139
+ }
140
+ out[i] = (float)v.getNumber();
141
+ }
142
+ return;
143
+ }
144
+
145
+ throw JSError(rt, "query must be a Float32Array, typed array, or number[]");
146
+ }
147
+
148
+ // ---------------------------------------------------------------------------
149
+ // Async job → Promise bridge
150
+ //
151
+ // JSI on RN does not give us a thread-safe way to resolve a Promise from a
152
+ // 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.
156
+ // ---------------------------------------------------------------------------
157
+
158
+ Value TalaDBHostObject::awaitJobAsPromise(Runtime &rt, TalaDbJob *job, bool parseAsJson) {
159
+ if (!job) {
160
+ throw ffiError(rt, "failed to start background job");
161
+ }
162
+
163
+ auto promiseCtor = rt.global().getPropertyAsFunction(rt, "Promise");
164
+
165
+ // The job pointer is owned by the executor closure; we null it out on
166
+ // consumption to prevent double-free.
167
+ auto jobBox = std::make_shared<TalaDbJob *>(job);
168
+
169
+ auto executor = Function::createFromHostFunction(
170
+ rt, PropNameID::forAscii(rt, "taladbJobExecutor"), 2,
171
+ [jobBox, parseAsJson](Runtime &rt, const Value &, const Value *args, size_t count) -> Value {
172
+ if (count < 2) {
173
+ return Value::undefined();
174
+ }
175
+ auto resolve = std::make_shared<Function>(args[0].getObject(rt).getFunction(rt));
176
+ auto reject = std::make_shared<Function>(args[1].getObject(rt).getFunction(rt));
177
+
178
+ // `setImmediate` is provided by React Native. Recurse until the
179
+ // 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));
182
+
183
+ // Capture a self-reference so the polling lambda can schedule itself.
184
+ auto poller = std::make_shared<std::function<void(Runtime &)>>();
185
+ *poller = [jobBox, resolve, reject, setImmediateShared, poller, parseAsJson](Runtime &rt) {
186
+ TalaDbJob *j = *jobBox;
187
+ if (!j) {
188
+ return; // already consumed — defensive
189
+ }
190
+ int32_t state = taladb_job_poll(j);
191
+ if (state == 0) {
192
+ // Still running — reschedule.
193
+ auto tick = Function::createFromHostFunction(
194
+ rt, PropNameID::forAscii(rt, "taladbJobTick"), 0,
195
+ [poller](Runtime &rt, const Value &, const Value *, size_t) -> Value {
196
+ (*poller)(rt);
197
+ return Value::undefined();
198
+ });
199
+ setImmediateShared->call(rt, tick);
200
+ return;
201
+ }
202
+
203
+ // Done — take the result and clear the job slot.
204
+ char *raw = taladb_job_take_result(j);
205
+ *jobBox = nullptr;
206
+
207
+ if (!raw) {
208
+ std::string last = lastError();
209
+ auto err = String::createFromUtf8(
210
+ rt, last.empty() ? "taladb job failed" : last);
211
+ reject->call(rt, err);
212
+ return;
213
+ }
214
+
215
+ std::string json(raw);
216
+ taladb_free_string(raw);
217
+
218
+ if (parseAsJson) {
219
+ try {
220
+ resolve->call(rt, parse(rt, json));
221
+ } catch (const JSError &e) {
222
+ reject->call(rt, String::createFromUtf8(rt, e.getMessage()));
223
+ }
224
+ } else {
225
+ resolve->call(rt, String::createFromUtf8(rt, json));
226
+ }
227
+ };
228
+
229
+ (*poller)(rt);
230
+ return Value::undefined();
231
+ });
232
+
233
+ return promiseCtor.callAsConstructor(rt, executor);
234
+ }
235
+
60
236
  // ---------------------------------------------------------------------------
61
237
  // Property names advertised to JS
62
238
  // ---------------------------------------------------------------------------
@@ -70,6 +246,9 @@ std::vector<PropNameID> TalaDBHostObject::getPropertyNames(Runtime &rt) {
70
246
  "count",
71
247
  "createIndex", "dropIndex",
72
248
  "createFtsIndex", "dropFtsIndex",
249
+ "createVectorIndex", "dropVectorIndex", "upgradeVectorIndex",
250
+ "findNearest", "findNearestAsync",
251
+ "findAsync",
73
252
  "compact",
74
253
  "close",
75
254
  };
@@ -265,6 +444,153 @@ Value TalaDBHostObject::get(Runtime &rt, const PropNameID &propName) {
265
444
  });
266
445
  }
267
446
 
447
+ // ------------------------------------------------------------------
448
+ // createVectorIndex(collection, field, dimensions, opts?): void
449
+ // opts: { metric?: 'cosine'|'dot'|'euclidean', hnsw?: HnswOptions }
450
+ // ------------------------------------------------------------------
451
+ if (name == "createVectorIndex") {
452
+ return Function::createFromHostFunction(
453
+ rt, PropNameID::forAscii(rt, "createVectorIndex"), 4,
454
+ [this](Runtime &rt, const Value &, const Value *args, size_t count) -> Value {
455
+ if (count < 3) throw JSError(rt, "createVectorIndex requires (collection, field, dimensions, opts?)");
456
+ auto col = args[0].getString(rt).utf8(rt);
457
+ auto field = args[1].getString(rt).utf8(rt);
458
+ if (!args[2].isNumber()) throw JSError(rt, "dimensions must be a number");
459
+ size_t dims = (size_t)args[2].getNumber();
460
+
461
+ std::string metricStr;
462
+ std::string hnswJson;
463
+ const char *metricPtr = nullptr;
464
+ const char *hnswPtr = nullptr;
465
+ if (count > 3 && args[3].isObject()) {
466
+ auto opts = args[3].getObject(rt);
467
+ if (opts.hasProperty(rt, "metric")) {
468
+ auto m = opts.getProperty(rt, "metric");
469
+ if (m.isString()) {
470
+ metricStr = m.getString(rt).utf8(rt);
471
+ metricPtr = metricStr.c_str();
472
+ }
473
+ }
474
+ if (opts.hasProperty(rt, "hnsw")) {
475
+ auto h = opts.getProperty(rt, "hnsw");
476
+ if (h.isObject()) {
477
+ hnswJson = stringify(rt, h);
478
+ hnswPtr = hnswJson.c_str();
479
+ }
480
+ }
481
+ }
482
+
483
+ int32_t res = taladb_create_vector_index(
484
+ db_, col.c_str(), field.c_str(), dims, metricPtr, hnswPtr);
485
+ if (res < 0) throw ffiError(rt, "taladb_create_vector_index failed");
486
+ return Value::undefined();
487
+ });
488
+ }
489
+
490
+ // ------------------------------------------------------------------
491
+ // dropVectorIndex(collection, field): void
492
+ // ------------------------------------------------------------------
493
+ if (name == "dropVectorIndex") {
494
+ return Function::createFromHostFunction(
495
+ rt, PropNameID::forAscii(rt, "dropVectorIndex"), 2,
496
+ [this](Runtime &rt, const Value &, const Value *args, size_t count) -> Value {
497
+ if (count < 2) throw JSError(rt, "dropVectorIndex requires 2 arguments");
498
+ auto col = args[0].getString(rt).utf8(rt);
499
+ auto field = args[1].getString(rt).utf8(rt);
500
+ int32_t res = taladb_drop_vector_index(db_, col.c_str(), field.c_str());
501
+ if (res < 0) throw ffiError(rt, "taladb_drop_vector_index failed");
502
+ return Value::undefined();
503
+ });
504
+ }
505
+
506
+ // ------------------------------------------------------------------
507
+ // upgradeVectorIndex(collection, field): void
508
+ // ------------------------------------------------------------------
509
+ if (name == "upgradeVectorIndex") {
510
+ return Function::createFromHostFunction(
511
+ rt, PropNameID::forAscii(rt, "upgradeVectorIndex"), 2,
512
+ [this](Runtime &rt, const Value &, const Value *args, size_t count) -> Value {
513
+ if (count < 2) throw JSError(rt, "upgradeVectorIndex requires 2 arguments");
514
+ auto col = args[0].getString(rt).utf8(rt);
515
+ auto field = args[1].getString(rt).utf8(rt);
516
+ int32_t res = taladb_upgrade_vector_index(db_, col.c_str(), field.c_str());
517
+ if (res < 0) throw ffiError(rt, "taladb_upgrade_vector_index failed");
518
+ return Value::undefined();
519
+ });
520
+ }
521
+
522
+ // ------------------------------------------------------------------
523
+ // findNearest(collection, field, query, topK, filter?): { document, score }[]
524
+ // query — Float32Array (preferred, zero-copy) or number[]
525
+ // ------------------------------------------------------------------
526
+ if (name == "findNearest") {
527
+ return Function::createFromHostFunction(
528
+ rt, PropNameID::forAscii(rt, "findNearest"), 5,
529
+ [this](Runtime &rt, const Value &, const Value *args, size_t count) -> Value {
530
+ if (count < 4) throw JSError(rt, "findNearest requires (collection, field, query, topK, filter?)");
531
+ auto col = args[0].getString(rt).utf8(rt);
532
+ auto field = args[1].getString(rt).utf8(rt);
533
+ std::vector<float> qbuf;
534
+ extractF32Query(rt, args[2], qbuf);
535
+ if (!args[3].isNumber()) throw JSError(rt, "topK must be a number");
536
+ size_t topK = (size_t)args[3].getNumber();
537
+ std::string filterJson = count > 4
538
+ ? valueToFilterJson(rt, args[4])
539
+ : std::string();
540
+ const char *filterPtr = count > 4 ? filterJson.c_str() : nullptr;
541
+
542
+ char *result = taladb_find_nearest(
543
+ db_, col.c_str(), field.c_str(),
544
+ qbuf.data(), qbuf.size(), topK, filterPtr);
545
+ if (!result) throw ffiError(rt, "taladb_find_nearest failed");
546
+ std::string json(result);
547
+ taladb_free_string(result);
548
+ return parse(rt, json);
549
+ });
550
+ }
551
+
552
+ // ------------------------------------------------------------------
553
+ // findNearestAsync(...) → Promise<{ document, score }[]>
554
+ // Runs on a background thread. JS thread polls via setImmediate.
555
+ // ------------------------------------------------------------------
556
+ if (name == "findNearestAsync") {
557
+ return Function::createFromHostFunction(
558
+ rt, PropNameID::forAscii(rt, "findNearestAsync"), 5,
559
+ [this](Runtime &rt, const Value &, const Value *args, size_t count) -> Value {
560
+ if (count < 4) throw JSError(rt, "findNearestAsync requires (collection, field, query, topK, filter?)");
561
+ auto col = args[0].getString(rt).utf8(rt);
562
+ auto field = args[1].getString(rt).utf8(rt);
563
+ std::vector<float> qbuf;
564
+ extractF32Query(rt, args[2], qbuf);
565
+ if (!args[3].isNumber()) throw JSError(rt, "topK must be a number");
566
+ size_t topK = (size_t)args[3].getNumber();
567
+ std::string filterJson = count > 4
568
+ ? valueToFilterJson(rt, args[4])
569
+ : std::string();
570
+ const char *filterPtr = count > 4 ? filterJson.c_str() : nullptr;
571
+
572
+ TalaDbJob *job = taladb_find_nearest_start(
573
+ db_, col.c_str(), field.c_str(),
574
+ qbuf.data(), qbuf.size(), topK, filterPtr);
575
+ return awaitJobAsPromise(rt, job, /*parseAsJson=*/true);
576
+ });
577
+ }
578
+
579
+ // ------------------------------------------------------------------
580
+ // findAsync(collection, filter?) → Promise<object[]>
581
+ // ------------------------------------------------------------------
582
+ if (name == "findAsync") {
583
+ return Function::createFromHostFunction(
584
+ rt, PropNameID::forAscii(rt, "findAsync"), 2,
585
+ [this](Runtime &rt, const Value &, const Value *args, size_t count) -> Value {
586
+ if (count < 1) throw JSError(rt, "findAsync requires at least 1 argument");
587
+ auto col = args[0].getString(rt).utf8(rt);
588
+ auto filterJson = count > 1 ? valueToFilterJson(rt, args[1]) : std::string("{}");
589
+ TalaDbJob *job = taladb_find_start(db_, col.c_str(), filterJson.c_str());
590
+ return awaitJobAsPromise(rt, job, /*parseAsJson=*/true);
591
+ });
592
+ }
593
+
268
594
  // ------------------------------------------------------------------
269
595
  // compact(): void
270
596
  // ------------------------------------------------------------------
@@ -50,6 +50,23 @@ private:
50
50
  // Returns "{}" when the value is null/undefined.
51
51
  static std::string valueToFilterJson(facebook::jsi::Runtime &rt,
52
52
  const facebook::jsi::Value &val);
53
+
54
+ // ------------------------------------------------------------------
55
+ // Float32Array helpers — zero-copy extraction from JSI
56
+ // ------------------------------------------------------------------
57
+ //
58
+ // Accepts a JS `Float32Array` *or* a plain `number[]`. Populates `out`
59
+ // with a copy of the values (kept alive by the caller for the FFI call).
60
+ // Throws JSError when the value is neither.
61
+ static void extractF32Query(facebook::jsi::Runtime &rt,
62
+ const facebook::jsi::Value &val,
63
+ std::vector<float> &out);
64
+
65
+ // Poll a TalaDbJob from the JS thread via setImmediate until done,
66
+ // then resolve / reject a Promise with the result / error.
67
+ static facebook::jsi::Value awaitJobAsPromise(facebook::jsi::Runtime &rt,
68
+ TalaDbJob *job,
69
+ bool parseAsJson);
53
70
  };
54
71
 
55
72
  } // namespace taladb
@@ -150,6 +150,94 @@ void taladb_drop_index (TalaDbHandle *handle, const char *collection, const
150
150
  void taladb_create_fts_index(TalaDbHandle *handle, const char *collection, const char *field);
151
151
  void taladb_drop_fts_index (TalaDbHandle *handle, const char *collection, const char *field);
152
152
 
153
+ /* -------------------------------------------------------------------------
154
+ * Vector index management
155
+ *
156
+ * metric — "cosine" (default), "dot", or "euclidean". NULL ⇒ cosine.
157
+ * hnsw_json — JSON-encoded HnswOptions, or NULL for flat index.
158
+ * ---------------------------------------------------------------------- */
159
+
160
+ int32_t taladb_create_vector_index(TalaDbHandle *handle,
161
+ const char *collection,
162
+ const char *field,
163
+ size_t dimensions,
164
+ const char *metric,
165
+ const char *hnsw_json);
166
+
167
+ int32_t taladb_drop_vector_index (TalaDbHandle *handle, const char *collection, const char *field);
168
+ int32_t taladb_upgrade_vector_index(TalaDbHandle *handle, const char *collection, const char *field);
169
+
170
+ /* -------------------------------------------------------------------------
171
+ * findNearest — Float32 zero-copy fast path
172
+ *
173
+ * query_ptr / query_len address `query_len` consecutive f32 values. Caller
174
+ * retains ownership; the buffer may be freed immediately after the call.
175
+ * filter_json may be NULL / "{}" / "null" to search without a pre-filter.
176
+ *
177
+ * Returns a JSON array string `[{document, score}, ...]`, or NULL on error.
178
+ * Caller must free with taladb_free_string.
179
+ * ---------------------------------------------------------------------- */
180
+
181
+ char *taladb_find_nearest(TalaDbHandle *handle,
182
+ const char *collection,
183
+ const char *field,
184
+ const float *query_ptr,
185
+ size_t query_len,
186
+ size_t top_k,
187
+ const char *filter_json);
188
+
189
+ /* -------------------------------------------------------------------------
190
+ * Async job API — run heavy queries on a background thread.
191
+ *
192
+ * Flow
193
+ * ----
194
+ * TalaDbJob *j = taladb_find_nearest_start(...); // spawns worker thread
195
+ * while (taladb_job_poll(j) == 0) { /* yield to JS event loop */ }
196
+ * char *json = taladb_job_take_result(j); // frees the job
197
+ *
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.
203
+ * ---------------------------------------------------------------------- */
204
+
205
+ typedef struct TalaDbJob TalaDbJob;
206
+
207
+ /** Kick a background `find_nearest`. Returns NULL on immediate arg error. */
208
+ TalaDbJob *taladb_find_nearest_start(TalaDbHandle *handle,
209
+ const char *collection,
210
+ const char *field,
211
+ const float *query_ptr,
212
+ size_t query_len,
213
+ size_t top_k,
214
+ const char *filter_json);
215
+
216
+ /** Kick a background `find`. Returns NULL on immediate arg error. */
217
+ TalaDbJob *taladb_find_start(TalaDbHandle *handle,
218
+ const char *collection,
219
+ const char *filter_json);
220
+
221
+ /** Non-blocking. Returns 1 if complete, 0 if still running, -1 on NULL job. */
222
+ int32_t taladb_job_poll(TalaDbJob *job);
223
+
224
+ /**
225
+ * Join the worker, take its result, and free the job.
226
+ * Returns a JSON string on success, or NULL on error (see taladb_last_error).
227
+ * Caller must free with taladb_free_string. Always frees the job.
228
+ */
229
+ char *taladb_job_take_result(TalaDbJob *job);
230
+
231
+ /** Detach the job and free the handle without waiting. */
232
+ void taladb_job_cancel(TalaDbJob *job);
233
+
234
+ /**
235
+ * Last error message for the current thread, or NULL.
236
+ * The returned pointer is valid until the next taladb_* call on this thread.
237
+ * Do NOT free.
238
+ */
239
+ const char *taladb_last_error(void);
240
+
153
241
  #ifdef __cplusplus
154
242
  } /* extern "C" */
155
243
  #endif