@taladb/web 0.2.11 → 0.4.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taladb/web",
3
- "version": "0.2.11",
3
+ "version": "0.4.0",
4
4
  "description": "TalaDB WASM bindings — document queries and vector search in the browser",
5
5
  "main": "pkg/taladb_web.js",
6
6
  "types": "pkg/taladb_web.d.ts",
package/pkg/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "thinkgrid-labs"
6
6
  ],
7
7
  "description": "TalaDB browser WASM bindings (wasm-bindgen + OPFS)",
8
- "version": "0.2.11",
8
+ "version": "0.4.0",
9
9
  "license": "MIT",
10
10
  "files": [
11
11
  "taladb_web_bg.wasm",
@@ -16,10 +16,13 @@ export class CollectionWasm {
16
16
  /**
17
17
  * Create a vector index on `field`.
18
18
  *
19
- * `dimensions` — expected vector length.
20
- * `metric` — optional string: `"cosine"` (default), `"dot"`, or `"euclidean"`.
19
+ * `dimensions` — expected vector length.
20
+ * `metric` — optional: `"cosine"` (default), `"dot"`, or `"euclidean"`.
21
+ * `index_type` — optional: `"flat"` (default) or `"hnsw"`.
22
+ * `hnsw_m` — HNSW connectivity (default 16).
23
+ * `hnsw_ef_construction` — build quality (default 200).
21
24
  */
22
- createVectorIndex(field: string, dimensions: number, metric?: string | null): void;
25
+ createVectorIndex(field: string, dimensions: number, metric?: string | null, index_type?: string | null, hnsw_m?: number | null, hnsw_ef_construction?: number | null): void;
23
26
  /**
24
27
  * Delete all matching documents. Returns the count deleted.
25
28
  */
@@ -33,7 +36,7 @@ export class CollectionWasm {
33
36
  */
34
37
  dropIndex(field: string): void;
35
38
  /**
36
- * Drop a vector index.
39
+ * Drop a vector index (and its HNSW graph if present).
37
40
  */
38
41
  dropVectorIndex(field: string): void;
39
42
  /**
@@ -69,6 +72,10 @@ export class CollectionWasm {
69
72
  * Update the first matching document. Returns true if a document was updated.
70
73
  */
71
74
  updateOne(filter: any, update: any): boolean;
75
+ /**
76
+ * Rebuild the HNSW graph from the current flat vector table.
77
+ */
78
+ upgradeVectorIndex(field: string): void;
72
79
  }
73
80
 
74
81
  export class TalaDBWasm {
@@ -119,9 +126,14 @@ export class WorkerDB {
119
126
  createFtsIndex(collection: string, field: string): void;
120
127
  createIndex(collection: string, field: string): void;
121
128
  /**
122
- * Create a vector index. `metric_str`: `"cosine"` | `"dot"` | `"euclidean"` (default cosine).
129
+ * Create a vector index.
130
+ *
131
+ * - `metric_str`: `"cosine"` (default) | `"dot"` | `"euclidean"`
132
+ * - `index_type`: `"flat"` (default) | `"hnsw"`
133
+ * - `hnsw_m`: HNSW connectivity (default 16, only used when `index_type = "hnsw"`)
134
+ * - `hnsw_ef_construction`: build-time quality (default 200, only used when `index_type = "hnsw"`)
123
135
  */
124
- createVectorIndex(collection: string, field: string, dimensions: number, metric_str?: string | null): void;
136
+ createVectorIndex(collection: string, field: string, dimensions: number, metric_str?: string | null, index_type?: string | null, hnsw_m?: number | null, hnsw_ef_construction?: number | null): void;
125
137
  /**
126
138
  * Delete all matching documents. Returns the count deleted.
127
139
  */
@@ -133,9 +145,16 @@ export class WorkerDB {
133
145
  dropFtsIndex(collection: string, field: string): void;
134
146
  dropIndex(collection: string, field: string): void;
135
147
  /**
136
- * Drop a vector index.
148
+ * Drop a vector index (and its HNSW graph if present).
137
149
  */
138
150
  dropVectorIndex(collection: string, field: string): void;
151
+ /**
152
+ * Serialize the entire in-memory database to bytes for persistence.
153
+ *
154
+ * Pass the returned bytes to `idbSaveSnapshot` to persist across page reloads.
155
+ * On next open, pass the same bytes to `openWithSnapshot` to restore all data.
156
+ */
157
+ exportSnapshot(): Uint8Array;
139
158
  /**
140
159
  * Find documents. Returns a JSON array of document objects.
141
160
  */
@@ -156,6 +175,11 @@ export class WorkerDB {
156
175
  * Insert many documents. Returns a JSON array of ULID strings.
157
176
  */
158
177
  insertMany(collection: string, docs_json: string): string;
178
+ /**
179
+ * Returns a JSON string `{ btree: string[], fts: string[], vector: string[] }`
180
+ * listing all indexes on the given collection.
181
+ */
182
+ listIndexes(collection: string): string;
159
183
  /**
160
184
  * Open an in-memory database (for tests and OPFS-unavailable fallback).
161
185
  */
@@ -170,6 +194,18 @@ export class WorkerDB {
170
194
  * ```
171
195
  */
172
196
  static openWithOpfs(sync_handle: FileSystemSyncAccessHandle): WorkerDB;
197
+ /**
198
+ * Open a database, restoring from a previously exported snapshot if provided.
199
+ *
200
+ * Pass the bytes returned by `WorkerDB.exportSnapshot()` (or `null`/`undefined`
201
+ * for a fresh empty database). Used by the IndexedDB fallback path.
202
+ *
203
+ * ```js
204
+ * const bytes = await idbLoadSnapshot(dbName); // null on first open
205
+ * const workerDb = WorkerDB.openWithSnapshot(bytes);
206
+ * ```
207
+ */
208
+ static openWithSnapshot(data?: Uint8Array | null): WorkerDB;
173
209
  /**
174
210
  * Update all matching documents. Returns the count updated.
175
211
  */
@@ -178,17 +214,24 @@ export class WorkerDB {
178
214
  * Update the first matching document. Returns `true` / `false`.
179
215
  */
180
216
  updateOne(collection: string, filter_json: string, update_json: string): boolean;
217
+ /**
218
+ * Rebuild the HNSW graph for a vector index from the current flat vector
219
+ * table. Use after bulk inserts or when ANN recall has degraded.
220
+ *
221
+ * No-op when the `vector-hnsw` feature is disabled or the index is flat-only.
222
+ */
223
+ upgradeVectorIndex(collection: string, field: string): void;
181
224
  }
182
225
 
183
226
  /**
184
227
  * Load a previous database snapshot from IndexedDB.
185
- * Returns None if no snapshot exists yet.
228
+ * Returns `None` if no snapshot exists yet (first open) or IDB is unavailable.
186
229
  */
187
230
  export function idb_load_snapshot(db_name: string): Promise<Uint8Array | undefined>;
188
231
 
189
232
  /**
190
- * Persist a database snapshot to IndexedDB (via localStorage for simplicity in v1).
191
- * In v2 this should use a proper IDBObjectStore for binary data.
233
+ * Persist a database snapshot to IndexedDB.
234
+ * Returns `true` on success, `false` on any failure.
192
235
  */
193
236
  export function idb_save_snapshot(db_name: string, data: Uint8Array): Promise<boolean>;
194
237
 
@@ -237,26 +280,30 @@ export interface InitOutput {
237
280
  readonly workerdb_count: (a: number, b: number, c: number, d: number, e: number) => [number, number, number];
238
281
  readonly workerdb_createFtsIndex: (a: number, b: number, c: number, d: number, e: number) => [number, number];
239
282
  readonly workerdb_createIndex: (a: number, b: number, c: number, d: number, e: number) => [number, number];
240
- readonly workerdb_createVectorIndex: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => [number, number];
283
+ readonly workerdb_createVectorIndex: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number, k: number, l: number) => [number, number];
241
284
  readonly workerdb_deleteMany: (a: number, b: number, c: number, d: number, e: number) => [number, number, number];
242
285
  readonly workerdb_deleteOne: (a: number, b: number, c: number, d: number, e: number) => [number, number, number];
243
286
  readonly workerdb_dropFtsIndex: (a: number, b: number, c: number, d: number, e: number) => [number, number];
244
287
  readonly workerdb_dropIndex: (a: number, b: number, c: number, d: number, e: number) => [number, number];
245
288
  readonly workerdb_dropVectorIndex: (a: number, b: number, c: number, d: number, e: number) => [number, number];
289
+ readonly workerdb_exportSnapshot: (a: number) => [number, number, number, number];
246
290
  readonly workerdb_find: (a: number, b: number, c: number, d: number, e: number) => [number, number, number, number];
247
291
  readonly workerdb_findNearest: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number) => [number, number, number, number];
248
292
  readonly workerdb_findOne: (a: number, b: number, c: number, d: number, e: number) => [number, number, number, number];
249
293
  readonly workerdb_insert: (a: number, b: number, c: number, d: number, e: number) => [number, number, number, number];
250
294
  readonly workerdb_insertMany: (a: number, b: number, c: number, d: number, e: number) => [number, number, number, number];
295
+ readonly workerdb_listIndexes: (a: number, b: number, c: number) => [number, number, number, number];
251
296
  readonly workerdb_openInMemory: () => [number, number, number];
252
297
  readonly workerdb_openWithOpfs: (a: any) => [number, number, number];
298
+ readonly workerdb_openWithSnapshot: (a: number, b: number) => [number, number, number];
253
299
  readonly workerdb_updateMany: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => [number, number, number];
254
300
  readonly workerdb_updateOne: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => [number, number, number];
301
+ readonly workerdb_upgradeVectorIndex: (a: number, b: number, c: number, d: number, e: number) => [number, number];
255
302
  readonly __wbg_collectionwasm_free: (a: number, b: number) => void;
256
303
  readonly __wbg_taladbwasm_free: (a: number, b: number) => void;
257
304
  readonly collectionwasm_count: (a: number, b: any) => [number, number, number];
258
305
  readonly collectionwasm_createIndex: (a: number, b: number, c: number) => [number, number];
259
- readonly collectionwasm_createVectorIndex: (a: number, b: number, c: number, d: number, e: number, f: number) => [number, number];
306
+ readonly collectionwasm_createVectorIndex: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number) => [number, number];
260
307
  readonly collectionwasm_deleteMany: (a: number, b: any) => [number, number, number];
261
308
  readonly collectionwasm_deleteOne: (a: number, b: any) => [number, number, number];
262
309
  readonly collectionwasm_dropIndex: (a: number, b: number, c: number) => [number, number];
@@ -268,21 +315,24 @@ export interface InitOutput {
268
315
  readonly collectionwasm_insertMany: (a: number, b: any) => [number, number, number];
269
316
  readonly collectionwasm_updateMany: (a: number, b: any, c: any) => [number, number, number];
270
317
  readonly collectionwasm_updateOne: (a: number, b: any, c: any) => [number, number, number];
318
+ readonly collectionwasm_upgradeVectorIndex: (a: number, b: number, c: number) => [number, number];
271
319
  readonly taladbwasm_collection: (a: number, b: number, c: number) => number;
272
320
  readonly taladbwasm_exportSnapshot: (a: number) => [number, number, number, number];
273
321
  readonly taladbwasm_openInMemory: () => [number, number, number];
274
322
  readonly taladbwasm_openWithSnapshot: (a: number, b: number) => [number, number, number];
275
323
  readonly init: () => void;
276
- readonly idb_load_snapshot: (a: number, b: number) => any;
277
- readonly idb_save_snapshot: (a: number, b: number, c: number, d: number) => any;
278
324
  readonly is_opfs_available: () => any;
279
325
  readonly opfs_delete_snapshot: (a: number, b: number) => any;
280
326
  readonly opfs_flush_snapshot: (a: number, b: number, c: number, d: number) => any;
281
327
  readonly opfs_load_snapshot: (a: number, b: number) => any;
282
328
  readonly opfs_open_backend: (a: number, b: number) => any;
329
+ readonly idb_load_snapshot: (a: number, b: number) => any;
330
+ readonly idb_save_snapshot: (a: number, b: number, c: number, d: number) => any;
283
331
  readonly wasm_bindgen__closure__destroy__h1411c0b1433bd547: (a: number, b: number) => void;
332
+ readonly wasm_bindgen__closure__destroy__h7d7554216ff39279: (a: number, b: number) => void;
284
333
  readonly wasm_bindgen__convert__closures_____invoke__h4bd31e7387ab665d: (a: number, b: number, c: any) => [number, number];
285
334
  readonly wasm_bindgen__convert__closures_____invoke__h675f15260128efbc: (a: number, b: number, c: any, d: any) => void;
335
+ readonly wasm_bindgen__convert__closures_____invoke__h98155e79a0a742d1: (a: number, b: number, c: any) => void;
286
336
  readonly __wbindgen_malloc: (a: number, b: number) => number;
287
337
  readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
288
338
  readonly __wbindgen_exn_store: (a: number) => void;
package/pkg/taladb_web.js CHANGED
@@ -45,18 +45,26 @@ export class CollectionWasm {
45
45
  /**
46
46
  * Create a vector index on `field`.
47
47
  *
48
- * `dimensions` — expected vector length.
49
- * `metric` — optional string: `"cosine"` (default), `"dot"`, or `"euclidean"`.
48
+ * `dimensions` — expected vector length.
49
+ * `metric` — optional: `"cosine"` (default), `"dot"`, or `"euclidean"`.
50
+ * `index_type` — optional: `"flat"` (default) or `"hnsw"`.
51
+ * `hnsw_m` — HNSW connectivity (default 16).
52
+ * `hnsw_ef_construction` — build quality (default 200).
50
53
  * @param {string} field
51
54
  * @param {number} dimensions
52
55
  * @param {string | null} [metric]
56
+ * @param {string | null} [index_type]
57
+ * @param {number | null} [hnsw_m]
58
+ * @param {number | null} [hnsw_ef_construction]
53
59
  */
54
- createVectorIndex(field, dimensions, metric) {
60
+ createVectorIndex(field, dimensions, metric, index_type, hnsw_m, hnsw_ef_construction) {
55
61
  const ptr0 = passStringToWasm0(field, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
56
62
  const len0 = WASM_VECTOR_LEN;
57
63
  var ptr1 = isLikeNone(metric) ? 0 : passStringToWasm0(metric, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
58
64
  var len1 = WASM_VECTOR_LEN;
59
- const ret = wasm.collectionwasm_createVectorIndex(this.__wbg_ptr, ptr0, len0, dimensions, ptr1, len1);
65
+ var ptr2 = isLikeNone(index_type) ? 0 : passStringToWasm0(index_type, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
66
+ var len2 = WASM_VECTOR_LEN;
67
+ const ret = wasm.collectionwasm_createVectorIndex(this.__wbg_ptr, ptr0, len0, dimensions, ptr1, len1, ptr2, len2, isLikeNone(hnsw_m) ? 0x100000001 : (hnsw_m) >>> 0, isLikeNone(hnsw_ef_construction) ? 0x100000001 : (hnsw_ef_construction) >>> 0);
60
68
  if (ret[1]) {
61
69
  throw takeFromExternrefTable0(ret[0]);
62
70
  }
@@ -98,7 +106,7 @@ export class CollectionWasm {
98
106
  }
99
107
  }
100
108
  /**
101
- * Drop a vector index.
109
+ * Drop a vector index (and its HNSW graph if present).
102
110
  * @param {string} field
103
111
  */
104
112
  dropVectorIndex(field) {
@@ -218,6 +226,18 @@ export class CollectionWasm {
218
226
  }
219
227
  return ret[0] !== 0;
220
228
  }
229
+ /**
230
+ * Rebuild the HNSW graph from the current flat vector table.
231
+ * @param {string} field
232
+ */
233
+ upgradeVectorIndex(field) {
234
+ const ptr0 = passStringToWasm0(field, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
235
+ const len0 = WASM_VECTOR_LEN;
236
+ const ret = wasm.collectionwasm_upgradeVectorIndex(this.__wbg_ptr, ptr0, len0);
237
+ if (ret[1]) {
238
+ throw takeFromExternrefTable0(ret[0]);
239
+ }
240
+ }
221
241
  }
222
242
  if (Symbol.dispose) CollectionWasm.prototype[Symbol.dispose] = CollectionWasm.prototype.free;
223
243
 
@@ -370,20 +390,30 @@ export class WorkerDB {
370
390
  }
371
391
  }
372
392
  /**
373
- * Create a vector index. `metric_str`: `"cosine"` | `"dot"` | `"euclidean"` (default cosine).
393
+ * Create a vector index.
394
+ *
395
+ * - `metric_str`: `"cosine"` (default) | `"dot"` | `"euclidean"`
396
+ * - `index_type`: `"flat"` (default) | `"hnsw"`
397
+ * - `hnsw_m`: HNSW connectivity (default 16, only used when `index_type = "hnsw"`)
398
+ * - `hnsw_ef_construction`: build-time quality (default 200, only used when `index_type = "hnsw"`)
374
399
  * @param {string} collection
375
400
  * @param {string} field
376
401
  * @param {number} dimensions
377
402
  * @param {string | null} [metric_str]
403
+ * @param {string | null} [index_type]
404
+ * @param {number | null} [hnsw_m]
405
+ * @param {number | null} [hnsw_ef_construction]
378
406
  */
379
- createVectorIndex(collection, field, dimensions, metric_str) {
407
+ createVectorIndex(collection, field, dimensions, metric_str, index_type, hnsw_m, hnsw_ef_construction) {
380
408
  const ptr0 = passStringToWasm0(collection, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
381
409
  const len0 = WASM_VECTOR_LEN;
382
410
  const ptr1 = passStringToWasm0(field, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
383
411
  const len1 = WASM_VECTOR_LEN;
384
412
  var ptr2 = isLikeNone(metric_str) ? 0 : passStringToWasm0(metric_str, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
385
413
  var len2 = WASM_VECTOR_LEN;
386
- const ret = wasm.workerdb_createVectorIndex(this.__wbg_ptr, ptr0, len0, ptr1, len1, dimensions, ptr2, len2);
414
+ var ptr3 = isLikeNone(index_type) ? 0 : passStringToWasm0(index_type, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
415
+ var len3 = WASM_VECTOR_LEN;
416
+ const ret = wasm.workerdb_createVectorIndex(this.__wbg_ptr, ptr0, len0, ptr1, len1, dimensions, ptr2, len2, ptr3, len3, isLikeNone(hnsw_m) ? 0x100000001 : (hnsw_m) >>> 0, isLikeNone(hnsw_ef_construction) ? 0x100000001 : (hnsw_ef_construction) >>> 0);
387
417
  if (ret[1]) {
388
418
  throw takeFromExternrefTable0(ret[0]);
389
419
  }
@@ -451,7 +481,7 @@ export class WorkerDB {
451
481
  }
452
482
  }
453
483
  /**
454
- * Drop a vector index.
484
+ * Drop a vector index (and its HNSW graph if present).
455
485
  * @param {string} collection
456
486
  * @param {string} field
457
487
  */
@@ -465,6 +495,22 @@ export class WorkerDB {
465
495
  throw takeFromExternrefTable0(ret[0]);
466
496
  }
467
497
  }
498
+ /**
499
+ * Serialize the entire in-memory database to bytes for persistence.
500
+ *
501
+ * Pass the returned bytes to `idbSaveSnapshot` to persist across page reloads.
502
+ * On next open, pass the same bytes to `openWithSnapshot` to restore all data.
503
+ * @returns {Uint8Array}
504
+ */
505
+ exportSnapshot() {
506
+ const ret = wasm.workerdb_exportSnapshot(this.__wbg_ptr);
507
+ if (ret[3]) {
508
+ throw takeFromExternrefTable0(ret[2]);
509
+ }
510
+ var v1 = getArrayU8FromWasm0(ret[0], ret[1]).slice();
511
+ wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
512
+ return v1;
513
+ }
468
514
  /**
469
515
  * Find documents. Returns a JSON array of document objects.
470
516
  * @param {string} collection
@@ -612,6 +658,32 @@ export class WorkerDB {
612
658
  wasm.__wbindgen_free(deferred4_0, deferred4_1, 1);
613
659
  }
614
660
  }
661
+ /**
662
+ * Returns a JSON string `{ btree: string[], fts: string[], vector: string[] }`
663
+ * listing all indexes on the given collection.
664
+ * @param {string} collection
665
+ * @returns {string}
666
+ */
667
+ listIndexes(collection) {
668
+ let deferred3_0;
669
+ let deferred3_1;
670
+ try {
671
+ const ptr0 = passStringToWasm0(collection, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
672
+ const len0 = WASM_VECTOR_LEN;
673
+ const ret = wasm.workerdb_listIndexes(this.__wbg_ptr, ptr0, len0);
674
+ var ptr2 = ret[0];
675
+ var len2 = ret[1];
676
+ if (ret[3]) {
677
+ ptr2 = 0; len2 = 0;
678
+ throw takeFromExternrefTable0(ret[2]);
679
+ }
680
+ deferred3_0 = ptr2;
681
+ deferred3_1 = len2;
682
+ return getStringFromWasm0(ptr2, len2);
683
+ } finally {
684
+ wasm.__wbindgen_free(deferred3_0, deferred3_1, 1);
685
+ }
686
+ }
615
687
  /**
616
688
  * Open an in-memory database (for tests and OPFS-unavailable fallback).
617
689
  * @returns {WorkerDB}
@@ -641,6 +713,28 @@ export class WorkerDB {
641
713
  }
642
714
  return WorkerDB.__wrap(ret[0]);
643
715
  }
716
+ /**
717
+ * Open a database, restoring from a previously exported snapshot if provided.
718
+ *
719
+ * Pass the bytes returned by `WorkerDB.exportSnapshot()` (or `null`/`undefined`
720
+ * for a fresh empty database). Used by the IndexedDB fallback path.
721
+ *
722
+ * ```js
723
+ * const bytes = await idbLoadSnapshot(dbName); // null on first open
724
+ * const workerDb = WorkerDB.openWithSnapshot(bytes);
725
+ * ```
726
+ * @param {Uint8Array | null} [data]
727
+ * @returns {WorkerDB}
728
+ */
729
+ static openWithSnapshot(data) {
730
+ var ptr0 = isLikeNone(data) ? 0 : passArray8ToWasm0(data, wasm.__wbindgen_malloc);
731
+ var len0 = WASM_VECTOR_LEN;
732
+ const ret = wasm.workerdb_openWithSnapshot(ptr0, len0);
733
+ if (ret[2]) {
734
+ throw takeFromExternrefTable0(ret[1]);
735
+ }
736
+ return WorkerDB.__wrap(ret[0]);
737
+ }
644
738
  /**
645
739
  * Update all matching documents. Returns the count updated.
646
740
  * @param {string} collection
@@ -681,12 +775,30 @@ export class WorkerDB {
681
775
  }
682
776
  return ret[0] !== 0;
683
777
  }
778
+ /**
779
+ * Rebuild the HNSW graph for a vector index from the current flat vector
780
+ * table. Use after bulk inserts or when ANN recall has degraded.
781
+ *
782
+ * No-op when the `vector-hnsw` feature is disabled or the index is flat-only.
783
+ * @param {string} collection
784
+ * @param {string} field
785
+ */
786
+ upgradeVectorIndex(collection, field) {
787
+ const ptr0 = passStringToWasm0(collection, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
788
+ const len0 = WASM_VECTOR_LEN;
789
+ const ptr1 = passStringToWasm0(field, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
790
+ const len1 = WASM_VECTOR_LEN;
791
+ const ret = wasm.workerdb_upgradeVectorIndex(this.__wbg_ptr, ptr0, len0, ptr1, len1);
792
+ if (ret[1]) {
793
+ throw takeFromExternrefTable0(ret[0]);
794
+ }
795
+ }
684
796
  }
685
797
  if (Symbol.dispose) WorkerDB.prototype[Symbol.dispose] = WorkerDB.prototype.free;
686
798
 
687
799
  /**
688
800
  * Load a previous database snapshot from IndexedDB.
689
- * Returns None if no snapshot exists yet.
801
+ * Returns `None` if no snapshot exists yet (first open) or IDB is unavailable.
690
802
  * @param {string} db_name
691
803
  * @returns {Promise<Uint8Array | undefined>}
692
804
  */
@@ -698,8 +810,8 @@ export function idb_load_snapshot(db_name) {
698
810
  }
699
811
 
700
812
  /**
701
- * Persist a database snapshot to IndexedDB (via localStorage for simplicity in v1).
702
- * In v2 this should use a proper IDBObjectStore for binary data.
813
+ * Persist a database snapshot to IndexedDB.
814
+ * Returns `true` on success, `false` on any failure.
703
815
  * @param {string} db_name
704
816
  * @param {Uint8Array} data
705
817
  * @returns {Promise<boolean>}
@@ -876,14 +988,14 @@ function __wbg_get_imports() {
876
988
  __wbg__wbg_cb_unref_6b5b6b8576d35cb1: function(arg0) {
877
989
  arg0._wbg_cb_unref();
878
990
  },
991
+ __wbg_apply_ac9afb97ca32f169: function() { return handleError(function (arg0, arg1, arg2) {
992
+ const ret = arg0.apply(arg1, arg2);
993
+ return ret;
994
+ }, arguments); },
879
995
  __wbg_arrayBuffer_7ff5e58aa85a64f7: function(arg0) {
880
996
  const ret = arg0.arrayBuffer();
881
997
  return ret;
882
998
  },
883
- __wbg_buffer_60b8043cd926067d: function(arg0) {
884
- const ret = arg0.buffer;
885
- return ret;
886
- },
887
999
  __wbg_call_2d781c1f4d5c0ef8: function() { return handleError(function (arg0, arg1, arg2) {
888
1000
  const ret = arg0.call(arg1, arg2);
889
1001
  return ret;
@@ -942,13 +1054,6 @@ function __wbg_get_imports() {
942
1054
  const ret = arg0.getFile();
943
1055
  return ret;
944
1056
  },
945
- __wbg_getItem_a7cc1d4f154f2e6f: function() { return handleError(function (arg0, arg1, arg2, arg3) {
946
- const ret = arg1.getItem(getStringFromWasm0(arg2, arg3));
947
- var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
948
- var len1 = WASM_VECTOR_LEN;
949
- getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
950
- getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
951
- }, arguments); },
952
1057
  __wbg_getRandomValues_3f44b700395062e5: function() { return handleError(function (arg0, arg1) {
953
1058
  globalThis.crypto.getRandomValues(getArrayU8FromWasm0(arg0, arg1));
954
1059
  }, arguments); },
@@ -968,10 +1073,6 @@ function __wbg_get_imports() {
968
1073
  const ret = arg0[arg1 >>> 0];
969
1074
  return ret;
970
1075
  },
971
- __wbg_get_index_87179971b8d350e4: function(arg0, arg1) {
972
- const ret = arg0[arg1 >>> 0];
973
- return ret;
974
- },
975
1076
  __wbg_get_unchecked_329cfe50afab7352: function(arg0, arg1) {
976
1077
  const ret = arg0[arg1 >>> 0];
977
1078
  return ret;
@@ -1096,10 +1197,6 @@ function __wbg_get_imports() {
1096
1197
  const ret = arg0.length;
1097
1198
  return ret;
1098
1199
  },
1099
- __wbg_localStorage_51c38b3b222e1ed2: function() { return handleError(function (arg0) {
1100
- const ret = arg0.localStorage;
1101
- return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
1102
- }, arguments); },
1103
1200
  __wbg_navigator_9cebf56f28aa719b: function(arg0) {
1104
1201
  const ret = arg0.navigator;
1105
1202
  return ret;
@@ -1124,6 +1221,24 @@ function __wbg_get_imports() {
1124
1221
  const ret = new Object();
1125
1222
  return ret;
1126
1223
  },
1224
+ __wbg_new_d098e265629cd10f: function(arg0, arg1) {
1225
+ try {
1226
+ var state0 = {a: arg0, b: arg1};
1227
+ var cb0 = (arg0, arg1) => {
1228
+ const a = state0.a;
1229
+ state0.a = 0;
1230
+ try {
1231
+ return wasm_bindgen__convert__closures_____invoke__h675f15260128efbc(a, state0.b, arg0, arg1);
1232
+ } finally {
1233
+ state0.a = a;
1234
+ }
1235
+ };
1236
+ const ret = new Promise(cb0);
1237
+ return ret;
1238
+ } finally {
1239
+ state0.a = state0.b = 0;
1240
+ }
1241
+ },
1127
1242
  __wbg_new_from_slice_22da9388ac046e50: function(arg0, arg1) {
1128
1243
  const ret = new Uint8Array(getArrayU8FromWasm0(arg0, arg1));
1129
1244
  return ret;
@@ -1165,6 +1280,10 @@ function __wbg_get_imports() {
1165
1280
  __wbg_prototypesetcall_d62e5099504357e6: function(arg0, arg1, arg2) {
1166
1281
  Uint8Array.prototype.set.call(getArrayU8FromWasm0(arg0, arg1), arg2);
1167
1282
  },
1283
+ __wbg_push_e87b0e732085a946: function(arg0, arg1) {
1284
+ const ret = arg0.push(arg1);
1285
+ return ret;
1286
+ },
1168
1287
  __wbg_queueMicrotask_0c399741342fb10f: function(arg0) {
1169
1288
  const ret = arg0.queueMicrotask;
1170
1289
  return ret;
@@ -1180,9 +1299,6 @@ function __wbg_get_imports() {
1180
1299
  const ret = Promise.resolve(arg0);
1181
1300
  return ret;
1182
1301
  },
1183
- __wbg_setItem_5f84aeef0d7f3c17: function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) {
1184
- arg0.setItem(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4));
1185
- }, arguments); },
1186
1302
  __wbg_set_282384002438957f: function(arg0, arg1, arg2) {
1187
1303
  arg0[arg1 >>> 0] = arg2;
1188
1304
  },
@@ -1243,31 +1359,36 @@ function __wbg_get_imports() {
1243
1359
  return ret;
1244
1360
  },
1245
1361
  __wbindgen_cast_0000000000000001: function(arg0, arg1) {
1246
- // Cast intrinsic for `Closure(Closure { dtor_idx: 217, function: Function { arguments: [Externref], shim_idx: 218, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
1362
+ // Cast intrinsic for `Closure(Closure { dtor_idx: 422, function: Function { arguments: [Externref], shim_idx: 423, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
1247
1363
  const ret = makeMutClosure(arg0, arg1, wasm.wasm_bindgen__closure__destroy__h1411c0b1433bd547, wasm_bindgen__convert__closures_____invoke__h4bd31e7387ab665d);
1248
1364
  return ret;
1249
1365
  },
1250
- __wbindgen_cast_0000000000000002: function(arg0) {
1366
+ __wbindgen_cast_0000000000000002: function(arg0, arg1) {
1367
+ // Cast intrinsic for `Closure(Closure { dtor_idx: 63, function: Function { arguments: [Externref], shim_idx: 64, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
1368
+ const ret = makeMutClosure(arg0, arg1, wasm.wasm_bindgen__closure__destroy__h7d7554216ff39279, wasm_bindgen__convert__closures_____invoke__h98155e79a0a742d1);
1369
+ return ret;
1370
+ },
1371
+ __wbindgen_cast_0000000000000003: function(arg0) {
1251
1372
  // Cast intrinsic for `F64 -> Externref`.
1252
1373
  const ret = arg0;
1253
1374
  return ret;
1254
1375
  },
1255
- __wbindgen_cast_0000000000000003: function(arg0) {
1376
+ __wbindgen_cast_0000000000000004: function(arg0) {
1256
1377
  // Cast intrinsic for `I64 -> Externref`.
1257
1378
  const ret = arg0;
1258
1379
  return ret;
1259
1380
  },
1260
- __wbindgen_cast_0000000000000004: function(arg0, arg1) {
1381
+ __wbindgen_cast_0000000000000005: function(arg0, arg1) {
1261
1382
  // Cast intrinsic for `Ref(String) -> Externref`.
1262
1383
  const ret = getStringFromWasm0(arg0, arg1);
1263
1384
  return ret;
1264
1385
  },
1265
- __wbindgen_cast_0000000000000005: function(arg0) {
1386
+ __wbindgen_cast_0000000000000006: function(arg0) {
1266
1387
  // Cast intrinsic for `U64 -> Externref`.
1267
1388
  const ret = BigInt.asUintN(64, arg0);
1268
1389
  return ret;
1269
1390
  },
1270
- __wbindgen_cast_0000000000000006: function(arg0, arg1) {
1391
+ __wbindgen_cast_0000000000000007: function(arg0, arg1) {
1271
1392
  var v0 = getArrayU8FromWasm0(arg0, arg1).slice();
1272
1393
  wasm.__wbindgen_free(arg0, arg1 * 1, 1);
1273
1394
  // Cast intrinsic for `Vector(U8) -> Externref`.
@@ -1290,6 +1411,10 @@ function __wbg_get_imports() {
1290
1411
  };
1291
1412
  }
1292
1413
 
1414
+ function wasm_bindgen__convert__closures_____invoke__h98155e79a0a742d1(arg0, arg1, arg2) {
1415
+ wasm.wasm_bindgen__convert__closures_____invoke__h98155e79a0a742d1(arg0, arg1, arg2);
1416
+ }
1417
+
1293
1418
  function wasm_bindgen__convert__closures_____invoke__h4bd31e7387ab665d(arg0, arg1, arg2) {
1294
1419
  const ret = wasm.wasm_bindgen__convert__closures_____invoke__h4bd31e7387ab665d(arg0, arg1, arg2);
1295
1420
  if (ret[1]) {
Binary file
@@ -5,26 +5,30 @@ export const __wbg_workerdb_free: (a: number, b: number) => void;
5
5
  export const workerdb_count: (a: number, b: number, c: number, d: number, e: number) => [number, number, number];
6
6
  export const workerdb_createFtsIndex: (a: number, b: number, c: number, d: number, e: number) => [number, number];
7
7
  export const workerdb_createIndex: (a: number, b: number, c: number, d: number, e: number) => [number, number];
8
- export const workerdb_createVectorIndex: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => [number, number];
8
+ export const workerdb_createVectorIndex: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number, k: number, l: number) => [number, number];
9
9
  export const workerdb_deleteMany: (a: number, b: number, c: number, d: number, e: number) => [number, number, number];
10
10
  export const workerdb_deleteOne: (a: number, b: number, c: number, d: number, e: number) => [number, number, number];
11
11
  export const workerdb_dropFtsIndex: (a: number, b: number, c: number, d: number, e: number) => [number, number];
12
12
  export const workerdb_dropIndex: (a: number, b: number, c: number, d: number, e: number) => [number, number];
13
13
  export const workerdb_dropVectorIndex: (a: number, b: number, c: number, d: number, e: number) => [number, number];
14
+ export const workerdb_exportSnapshot: (a: number) => [number, number, number, number];
14
15
  export const workerdb_find: (a: number, b: number, c: number, d: number, e: number) => [number, number, number, number];
15
16
  export const workerdb_findNearest: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number) => [number, number, number, number];
16
17
  export const workerdb_findOne: (a: number, b: number, c: number, d: number, e: number) => [number, number, number, number];
17
18
  export const workerdb_insert: (a: number, b: number, c: number, d: number, e: number) => [number, number, number, number];
18
19
  export const workerdb_insertMany: (a: number, b: number, c: number, d: number, e: number) => [number, number, number, number];
20
+ export const workerdb_listIndexes: (a: number, b: number, c: number) => [number, number, number, number];
19
21
  export const workerdb_openInMemory: () => [number, number, number];
20
22
  export const workerdb_openWithOpfs: (a: any) => [number, number, number];
23
+ export const workerdb_openWithSnapshot: (a: number, b: number) => [number, number, number];
21
24
  export const workerdb_updateMany: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => [number, number, number];
22
25
  export const workerdb_updateOne: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => [number, number, number];
26
+ export const workerdb_upgradeVectorIndex: (a: number, b: number, c: number, d: number, e: number) => [number, number];
23
27
  export const __wbg_collectionwasm_free: (a: number, b: number) => void;
24
28
  export const __wbg_taladbwasm_free: (a: number, b: number) => void;
25
29
  export const collectionwasm_count: (a: number, b: any) => [number, number, number];
26
30
  export const collectionwasm_createIndex: (a: number, b: number, c: number) => [number, number];
27
- export const collectionwasm_createVectorIndex: (a: number, b: number, c: number, d: number, e: number, f: number) => [number, number];
31
+ export const collectionwasm_createVectorIndex: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number) => [number, number];
28
32
  export const collectionwasm_deleteMany: (a: number, b: any) => [number, number, number];
29
33
  export const collectionwasm_deleteOne: (a: number, b: any) => [number, number, number];
30
34
  export const collectionwasm_dropIndex: (a: number, b: number, c: number) => [number, number];
@@ -36,21 +40,24 @@ export const collectionwasm_insert: (a: number, b: any) => [number, number, numb
36
40
  export const collectionwasm_insertMany: (a: number, b: any) => [number, number, number];
37
41
  export const collectionwasm_updateMany: (a: number, b: any, c: any) => [number, number, number];
38
42
  export const collectionwasm_updateOne: (a: number, b: any, c: any) => [number, number, number];
43
+ export const collectionwasm_upgradeVectorIndex: (a: number, b: number, c: number) => [number, number];
39
44
  export const taladbwasm_collection: (a: number, b: number, c: number) => number;
40
45
  export const taladbwasm_exportSnapshot: (a: number) => [number, number, number, number];
41
46
  export const taladbwasm_openInMemory: () => [number, number, number];
42
47
  export const taladbwasm_openWithSnapshot: (a: number, b: number) => [number, number, number];
43
48
  export const init: () => void;
44
- export const idb_load_snapshot: (a: number, b: number) => any;
45
- export const idb_save_snapshot: (a: number, b: number, c: number, d: number) => any;
46
49
  export const is_opfs_available: () => any;
47
50
  export const opfs_delete_snapshot: (a: number, b: number) => any;
48
51
  export const opfs_flush_snapshot: (a: number, b: number, c: number, d: number) => any;
49
52
  export const opfs_load_snapshot: (a: number, b: number) => any;
50
53
  export const opfs_open_backend: (a: number, b: number) => any;
54
+ export const idb_load_snapshot: (a: number, b: number) => any;
55
+ export const idb_save_snapshot: (a: number, b: number, c: number, d: number) => any;
51
56
  export const wasm_bindgen__closure__destroy__h1411c0b1433bd547: (a: number, b: number) => void;
57
+ export const wasm_bindgen__closure__destroy__h7d7554216ff39279: (a: number, b: number) => void;
52
58
  export const wasm_bindgen__convert__closures_____invoke__h4bd31e7387ab665d: (a: number, b: number, c: any) => [number, number];
53
59
  export const wasm_bindgen__convert__closures_____invoke__h675f15260128efbc: (a: number, b: number, c: any, d: any) => void;
60
+ export const wasm_bindgen__convert__closures_____invoke__h98155e79a0a742d1: (a: number, b: number, c: any) => void;
54
61
  export const __wbindgen_malloc: (a: number, b: number) => number;
55
62
  export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
56
63
  export const __wbindgen_exn_store: (a: number) => void;
@@ -1,15 +1,25 @@
1
1
  /**
2
- * TalaDB SharedWorker
2
+ * TalaDB Worker
3
3
  *
4
- * Owns the OPFS file handle and the WASM + redb database instance.
5
- * The main thread connects via SharedWorker and communicates through
6
- * a typed message protocol.
4
+ * Runs as a Dedicated Worker. Each tab spawns its own worker instance.
5
+ * Multi-tab write safety is provided by the Web Locks API — only one worker
6
+ * holds the exclusive lock on the OPFS file at a time. Other workers queue up
7
+ * and acquire the lock automatically when the current holder closes.
7
8
  *
8
- * Why SharedWorker (not DedicatedWorker)?
9
+ * Why DedicatedWorker (not SharedWorker)?
9
10
  * ----------------------------------------
10
- * A SharedWorker persists as long as any tab/page from the same origin
11
- * has it open. This means multiple tabs share the same database instance
12
- * no write conflicts, no duplicate open files.
11
+ * createSyncAccessHandle() required for synchronous OPFS I/O is only
12
+ * available in DedicatedWorkerGlobalScope per the WHATWG File System spec.
13
+ * SharedWorker cannot call it; Chrome throws "is not a function".
14
+ *
15
+ * Why Web Locks?
16
+ * --------------
17
+ * Without coordination, two tabs opening the same OPFS file would race.
18
+ * navigator.locks.request() gives us an exclusive named lock. The first tab
19
+ * acquires it immediately; subsequent tabs block until the holder's worker is
20
+ * terminated (tab closed / navigated) or db.close() is called explicitly.
21
+ * If Web Locks is unavailable the worker opens the file directly and logs a
22
+ * warning (safe for single-tab use).
13
23
  *
14
24
  * Message protocol
15
25
  * ----------------
@@ -17,9 +27,6 @@
17
27
  * Response → { id: number, result: unknown }
18
28
  * | { id: number, error: string }
19
29
  *
20
- * The `id` field lets the main thread match async responses to pending
21
- * Promise resolvers even when operations complete out of order.
22
- *
23
30
  * Supported ops
24
31
  * -------------
25
32
  * init { dbName }
@@ -36,10 +43,26 @@
36
43
  * dropIndex { collection, field }
37
44
  * createFtsIndex { collection, field }
38
45
  * dropFtsIndex { collection, field }
39
- * createVectorIndex { collection, field, dimensions, metric? }
46
+ * listIndexes { collection } → JSON { btree, fts, vector }
47
+ * createVectorIndex { collection, field, dimensions, metric?, indexType?, hnswM?, hnswEfConstruction? }
40
48
  * dropVectorIndex { collection, field }
49
+ * upgradeVectorIndex { collection, field }
41
50
  * findNearest { collection, field, queryJson, topK, filterJson? }
42
51
  * close {}
52
+ *
53
+ * Multi-tab live queries (BroadcastChannel)
54
+ * -----------------------------------------
55
+ * When a write op (insert/insertMany/updateOne/updateMany/deleteOne/deleteMany) commits, the worker
56
+ * posts a `"taladb:changed"` message on a BroadcastChannel named
57
+ * `"taladb:<dbName>"`. Other tabs listening on the same channel re-trigger
58
+ * their active `subscribe()` pollers immediately, bypassing the 300 ms tick.
59
+ *
60
+ * IndexedDB fallback (no OPFS)
61
+ * ----------------------------
62
+ * When OPFS is unavailable (cross-origin iframes, Firefox without storage
63
+ * access) the worker opens an in-memory database seeded from the last snapshot
64
+ * stored in IndexedDB. After every write it flushes a new snapshot back to
65
+ * IndexedDB so data survives page reloads.
43
66
  */
44
67
 
45
68
  // ---------------------------------------------------------------------------
@@ -50,10 +73,28 @@
50
73
  let db = null;
51
74
 
52
75
  /**
53
- * Maps dbName Promise<void> so that:
54
- * - Concurrent init calls for the same dbName share one promise (deduplicated).
55
- * - Init calls for different dbNames are rejected after the first succeeds,
56
- * because a SharedWorker instance owns exactly one database file.
76
+ * WorkerDB constructor hoisted to module scope so snapshot reloads in
77
+ * IDB-fallback mode can call WorkerDB.openWithSnapshot without re-importing.
78
+ * @type {typeof import('../pkg/taladb_web').WorkerDB | null}
79
+ */
80
+ let WorkerDB = null;
81
+
82
+ /**
83
+ * Set to true by the BroadcastChannel listener (fallback mode) when the
84
+ * primary tab commits a write. Cleared at the start of the next dispatch.
85
+ * @type {boolean}
86
+ */
87
+ let snapshotDirty = false;
88
+
89
+ /**
90
+ * Resolve function set when a fallback tab is waiting for the primary tab to
91
+ * export a snapshot via BroadcastChannel. Cleared once resolved or timed out.
92
+ * @type {(() => void) | null}
93
+ */
94
+ let pendingSnapshotResolve = null;
95
+
96
+ /**
97
+ * Deduplicates concurrent init calls for the same dbName within one worker.
57
98
  * @type {Map<string, Promise<void>>}
58
99
  */
59
100
  const initPromises = new Map();
@@ -61,24 +102,122 @@ const initPromises = new Map();
61
102
  /** The dbName that was successfully initialised (or is being initialised). */
62
103
  let activeDbName = null;
63
104
 
105
+ const isDev = typeof location !== 'undefined' && (location.hostname === 'localhost' || location.hostname === '127.0.0.1');
106
+ const log = isDev ? console.log.bind(console, '[TalaDB Worker]') : () => {};
107
+ const warn = isDev ? console.warn.bind(console, '[TalaDB Worker]') : () => {};
108
+
109
+ /**
110
+ * Resolving this releases the Web Lock and closes the sync handle.
111
+ * Set inside doInit; called by the 'close' op or when the worker terminates.
112
+ * @type {(() => void) | null}
113
+ */
114
+ let releaseLock = null;
115
+
116
+ /**
117
+ * BroadcastChannel used to notify sibling tabs of writes.
118
+ * Created in doInit; null until the channel name is known.
119
+ * @type {BroadcastChannel | null}
120
+ */
121
+ let broadcastChannel = null;
122
+
123
+ /**
124
+ * True when running in IDB-fallback mode (OPFS unavailable).
125
+ * In this mode every write flushes a snapshot back to IndexedDB.
126
+ * @type {boolean}
127
+ */
128
+ let idbFallback = false;
129
+
64
130
  // ---------------------------------------------------------------------------
65
- // SharedWorker connect handler
131
+ // IndexedDB helpers (used only when OPFS is unavailable)
66
132
  // ---------------------------------------------------------------------------
67
133
 
68
- self.onconnect = (connectEvent) => {
69
- const port = connectEvent.ports[0];
134
+ const IDB_DB_NAME = 'taladb';
135
+ const IDB_STORE = 'snapshots';
136
+ const IDB_VERSION = 1;
137
+
138
+ /** Open (or upgrade) the "taladb" IDB database and return the IDBDatabase. */
139
+ function idbOpen() {
140
+ return new Promise((resolve, reject) => {
141
+ const req = self.indexedDB.open(IDB_DB_NAME, IDB_VERSION);
142
+ req.onupgradeneeded = () => {
143
+ // Create the object store the very first time.
144
+ if (!req.result.objectStoreNames.contains(IDB_STORE)) {
145
+ req.result.createObjectStore(IDB_STORE);
146
+ }
147
+ };
148
+ req.onsuccess = () => resolve(req.result);
149
+ req.onerror = () => reject(req.error);
150
+ });
151
+ }
152
+
153
+ /**
154
+ * Load a snapshot Uint8Array from IndexedDB for `dbName`.
155
+ * Returns null if no snapshot is stored yet.
156
+ * @param {string} dbName
157
+ * @returns {Promise<Uint8Array | null>}
158
+ */
159
+ async function idbLoadSnapshot(dbName) {
160
+ if (!self.indexedDB) return null;
161
+ try {
162
+ const idb = await idbOpen();
163
+ return new Promise((resolve) => {
164
+ const tx = idb.transaction(IDB_STORE, 'readonly');
165
+ const store = tx.objectStore(IDB_STORE);
166
+ const req = store.get(dbName);
167
+ req.onsuccess = () => resolve(req.result ?? null);
168
+ req.onerror = () => resolve(null);
169
+ });
170
+ } catch {
171
+ return null;
172
+ }
173
+ }
70
174
 
71
- port.onmessage = async (e) => {
72
- const { id, op, ...args } = e.data;
175
+ /**
176
+ * Persist a snapshot Uint8Array to IndexedDB for `dbName` (fire-and-forget).
177
+ * @param {string} dbName
178
+ * @param {Uint8Array} bytes
179
+ */
180
+ async function idbSaveSnapshot(dbName, bytes) {
181
+ if (!self.indexedDB) return;
182
+ try {
183
+ const idb = await idbOpen();
184
+ await new Promise((resolve, reject) => {
185
+ const tx = idb.transaction(IDB_STORE, 'readwrite');
186
+ tx.objectStore(IDB_STORE).put(bytes, dbName);
187
+ tx.oncomplete = resolve;
188
+ tx.onerror = reject;
189
+ });
190
+ } catch { /* best-effort persistence — ignore failures */ }
191
+ }
192
+
193
+ /**
194
+ * Notify sibling tabs of a write and, when in IDB-fallback mode, flush the
195
+ * updated snapshot to IndexedDB. Must be called after every mutating op.
196
+ */
197
+ function onWriteCommitted() {
198
+ broadcastChannel?.postMessage('taladb:changed');
199
+ // Always flush a snapshot to IDB after every write — this keeps other tabs'
200
+ // IDB-fallback instances in sync via BroadcastChannel + snapshotDirty reload.
201
+ if (db && activeDbName) {
73
202
  try {
74
- const result = await dispatch(op, args);
75
- port.postMessage({ id, result: result ?? null });
76
- } catch (err) {
77
- port.postMessage({ id, error: String(err?.message ?? err) });
78
- }
79
- };
203
+ const bytes = db.exportSnapshot();
204
+ idbSaveSnapshot(activeDbName, bytes).catch(() => {});
205
+ } catch { /* ignore snapshot export errors */ }
206
+ }
207
+ }
208
+
209
+ // ---------------------------------------------------------------------------
210
+ // Dedicated Worker message handler
211
+ // ---------------------------------------------------------------------------
80
212
 
81
- port.start();
213
+ self.onmessage = async (e) => {
214
+ const { id, op, ...args } = e.data;
215
+ try {
216
+ const result = await dispatch(op, args);
217
+ self.postMessage({ id, result: result ?? null });
218
+ } catch (err) {
219
+ self.postMessage({ id, error: String(err?.message ?? err) });
220
+ }
82
221
  };
83
222
 
84
223
  // ---------------------------------------------------------------------------
@@ -86,19 +225,26 @@ self.onconnect = (connectEvent) => {
86
225
  // ---------------------------------------------------------------------------
87
226
 
88
227
  async function dispatch(op, args) {
228
+ // In IDB-fallback mode, reload the snapshot before each op when the primary
229
+ // tab has signalled a write via BroadcastChannel. This keeps reads fresh.
230
+ if (idbFallback && snapshotDirty && op !== 'init' && db && activeDbName && WorkerDB) {
231
+ snapshotDirty = false;
232
+ try {
233
+ const fresh = await idbLoadSnapshot(activeDbName);
234
+ if (fresh) db = WorkerDB.openWithSnapshot(fresh);
235
+ } catch { /* ignore reload errors — stale read is acceptable */ }
236
+ }
237
+
89
238
  if (op === 'init') {
90
239
  const { dbName } = args;
91
240
 
92
- // Reject if a different database was already opened in this worker instance.
93
- // A SharedWorker owns exactly one OPFS file handle.
94
241
  if (activeDbName !== null && activeDbName !== dbName) {
95
242
  throw new Error(
96
243
  `TalaDB worker already initialised for "${activeDbName}". ` +
97
- `Cannot open "${dbName}" in the same SharedWorker instance.`
244
+ `Cannot open "${dbName}" in the same worker instance.`
98
245
  );
99
246
  }
100
247
 
101
- // Deduplicate concurrent init calls for the same dbName.
102
248
  if (!initPromises.has(dbName)) {
103
249
  activeDbName = dbName;
104
250
  initPromises.set(dbName, doInit(dbName));
@@ -110,11 +256,17 @@ async function dispatch(op, args) {
110
256
  if (!db) throw new Error('TalaDB worker not initialised — call init first');
111
257
 
112
258
  switch (op) {
113
- case 'insert':
114
- return db.insert(args.collection, args.docJson);
259
+ case 'insert': {
260
+ const result = db.insert(args.collection, args.docJson);
261
+ onWriteCommitted();
262
+ return result;
263
+ }
115
264
 
116
- case 'insertMany':
117
- return db.insertMany(args.collection, args.docsJson);
265
+ case 'insertMany': {
266
+ const result = db.insertMany(args.collection, args.docsJson);
267
+ onWriteCommitted();
268
+ return result;
269
+ }
118
270
 
119
271
  case 'find':
120
272
  return db.find(args.collection, args.filterJson ?? 'null');
@@ -122,17 +274,29 @@ async function dispatch(op, args) {
122
274
  case 'findOne':
123
275
  return db.findOne(args.collection, args.filterJson ?? 'null');
124
276
 
125
- case 'updateOne':
126
- return db.updateOne(args.collection, args.filterJson, args.updateJson);
277
+ case 'updateOne': {
278
+ const result = db.updateOne(args.collection, args.filterJson, args.updateJson);
279
+ onWriteCommitted();
280
+ return result;
281
+ }
127
282
 
128
- case 'updateMany':
129
- return db.updateMany(args.collection, args.filterJson, args.updateJson);
283
+ case 'updateMany': {
284
+ const result = db.updateMany(args.collection, args.filterJson, args.updateJson);
285
+ onWriteCommitted();
286
+ return result;
287
+ }
130
288
 
131
- case 'deleteOne':
132
- return db.deleteOne(args.collection, args.filterJson);
289
+ case 'deleteOne': {
290
+ const result = db.deleteOne(args.collection, args.filterJson);
291
+ onWriteCommitted();
292
+ return result;
293
+ }
133
294
 
134
- case 'deleteMany':
135
- return db.deleteMany(args.collection, args.filterJson);
295
+ case 'deleteMany': {
296
+ const result = db.deleteMany(args.collection, args.filterJson);
297
+ onWriteCommitted();
298
+ return result;
299
+ }
136
300
 
137
301
  case 'count':
138
302
  return db.count(args.collection, args.filterJson ?? 'null');
@@ -153,14 +317,29 @@ async function dispatch(op, args) {
153
317
  db.dropFtsIndex(args.collection, args.field);
154
318
  return null;
155
319
 
320
+ case 'listIndexes':
321
+ return db.listIndexes(args.collection);
322
+
156
323
  case 'createVectorIndex':
157
- db.createVectorIndex(args.collection, args.field, args.dimensions, args.metric ?? null);
324
+ db.createVectorIndex(
325
+ args.collection,
326
+ args.field,
327
+ args.dimensions,
328
+ args.metric ?? null,
329
+ args.indexType ?? null,
330
+ args.hnswM ?? null,
331
+ args.hnswEfConstruction ?? null,
332
+ );
158
333
  return null;
159
334
 
160
335
  case 'dropVectorIndex':
161
336
  db.dropVectorIndex(args.collection, args.field);
162
337
  return null;
163
338
 
339
+ case 'upgradeVectorIndex':
340
+ db.upgradeVectorIndex(args.collection, args.field);
341
+ return null;
342
+
164
343
  case 'findNearest':
165
344
  return db.findNearest(
166
345
  args.collection,
@@ -171,6 +350,11 @@ async function dispatch(op, args) {
171
350
  );
172
351
 
173
352
  case 'close':
353
+ // Release the Web Lock and close the sync handle gracefully.
354
+ if (releaseLock) { releaseLock(); releaseLock = null; }
355
+ broadcastChannel?.close();
356
+ broadcastChannel = null;
357
+ idbFallback = false;
174
358
  db = null;
175
359
  return null;
176
360
 
@@ -180,41 +364,141 @@ async function dispatch(op, args) {
180
364
  }
181
365
 
182
366
  // ---------------------------------------------------------------------------
183
- // Initialisation — load WASM, open OPFS file, create WorkerDB
367
+ // Initialisation — load WASM, acquire lock, open OPFS file
184
368
  // ---------------------------------------------------------------------------
185
369
 
186
370
  async function doInit(dbName) {
187
- // Dynamic import of the WASM module (wasm-pack --target web output)
188
- // The bundler (Vite/Webpack) will resolve this path correctly.
189
371
  const wasm = await import('../pkg/taladb_web.js');
190
- await wasm.default(); // run wasm-bindgen init (sets up memory, panic hook, etc.)
191
-
192
- const { WorkerDB } = wasm;
372
+ await wasm.default();
373
+
374
+ // Hoist to module scope so snapshot reloads in dispatch() can use it.
375
+ WorkerDB = wasm.WorkerDB;
376
+
377
+ // Open the BroadcastChannel now that we know the db name.
378
+ if (typeof BroadcastChannel !== 'undefined') {
379
+ broadcastChannel = new BroadcastChannel(`taladb:${dbName}`);
380
+ broadcastChannel.onmessage = async (e) => {
381
+ if (e.data === 'taladb:changed' && idbFallback) {
382
+ // Fallback tab: primary tab wrote — reload snapshot before next read.
383
+ snapshotDirty = true;
384
+ } else if (e.data === 'taladb:request-snapshot' && !idbFallback && db && activeDbName) {
385
+ // Primary (OPFS) tab: a new tab asked for a snapshot — export and save it.
386
+ try {
387
+ const bytes = db.exportSnapshot();
388
+ await idbSaveSnapshot(activeDbName, bytes);
389
+ broadcastChannel.postMessage('taladb:snapshot-ready');
390
+ log('Exported snapshot for waiting tab');
391
+ } catch { /* ignore */ }
392
+ } else if (e.data === 'taladb:snapshot-ready' && pendingSnapshotResolve) {
393
+ // Fallback tab: primary tab finished saving — wake up the waiting doInit.
394
+ const resolve = pendingSnapshotResolve;
395
+ pendingSnapshotResolve = null;
396
+ resolve();
397
+ }
398
+ };
399
+ log('BroadcastChannel opened:', `taladb:${dbName}`);
400
+ }
193
401
 
194
402
  const opfsAvailable = await checkOpfs();
195
403
  if (!opfsAvailable) {
196
- console.warn('[TalaDB Worker] OPFS unavailable — falling back to in-memory');
197
- db = WorkerDB.openInMemory();
404
+ warn('OPFS unavailable — falling back to IndexedDB-backed in-memory');
405
+ const snapshot = await idbLoadSnapshot(dbName);
406
+ db = WorkerDB.openWithSnapshot(snapshot);
407
+ idbFallback = true;
408
+ if (snapshot) {
409
+ log(`Restored from IndexedDB snapshot (${snapshot.byteLength} bytes)`);
410
+ } else {
411
+ log('New in-memory database — writes will be persisted to IndexedDB');
412
+ }
198
413
  return;
199
414
  }
200
415
 
201
- // Get the OPFS root directory
202
416
  const root = await navigator.storage.getDirectory();
203
-
204
- // Open (or create) the database file
205
417
  const fileName = `taladb_${dbName.replaceAll(/[/\\:]/g, '_')}.redb`;
206
418
  const fileHandle = await root.getFileHandle(fileName, { create: true });
207
419
 
208
- // createSyncAccessHandle — available in workers only
209
- const syncHandle = await fileHandle.createSyncAccessHandle();
420
+ if (!('locks' in navigator)) {
421
+ // Web Locks not available — open directly (single-tab safe only).
422
+ warn('Web Locks unavailable — multi-tab write safety disabled');
423
+ const syncHandle = await fileHandle.createSyncAccessHandle();
424
+ db = WorkerDB.openWithOpfs(syncHandle);
425
+ log(`Opened "${fileName}" via OPFS`);
426
+ return;
427
+ }
210
428
 
211
- db = WorkerDB.openWithOpfs(syncHandle);
212
- console.log(`[TalaDB Worker] Opened "${fileName}" via OPFS`);
429
+ // Try to acquire the exclusive OPFS lock immediately (ifAvailable).
430
+ // If another tab already holds it, fall back to IDB snapshot right away so
431
+ // this tab is immediately usable instead of blocking until the other tab closes.
432
+ // The primary (OPFS) tab flushes a snapshot to IDB after every write, and
433
+ // this tab's BroadcastChannel listener sets snapshotDirty so dispatch()
434
+ // reloads the snapshot before the next read — keeping data fresh across tabs.
435
+ const lockName = `taladb:${fileName}`;
436
+ await new Promise((resolve, reject) => {
437
+ navigator.locks.request(lockName, { ifAvailable: true }, async (lock) => {
438
+ if (lock === null) {
439
+ // Lock is held by another tab — use IDB snapshot so this tab loads immediately.
440
+ warn('OPFS lock held by another tab — falling back to IndexedDB snapshot (live-sync via BroadcastChannel)');
441
+ let snapshot = await idbLoadSnapshot(dbName);
442
+
443
+ if (!snapshot && broadcastChannel) {
444
+ // No IDB snapshot yet — ask the primary tab to export one now.
445
+ log('No IDB snapshot — requesting one from the primary tab...');
446
+ const gotIt = await new Promise(res => {
447
+ pendingSnapshotResolve = res;
448
+ setTimeout(() => { pendingSnapshotResolve = null; res(false); }, 2000);
449
+ broadcastChannel.postMessage('taladb:request-snapshot');
450
+ });
451
+ if (gotIt !== false) snapshot = await idbLoadSnapshot(dbName);
452
+ }
453
+
454
+ db = WorkerDB.openWithSnapshot(snapshot ?? null);
455
+ idbFallback = true;
456
+ if (snapshot) {
457
+ log(`Restored from IDB snapshot (${snapshot.byteLength} bytes)`);
458
+ } else {
459
+ log('No IDB snapshot yet — starting with empty in-memory database');
460
+ }
461
+ resolve();
462
+ return; // Do not hold the lock; returning releases it back to the queue.
463
+ }
464
+
465
+ // Acquired the lock — use OPFS.
466
+ try {
467
+ const syncHandle = await fileHandle.createSyncAccessHandle();
468
+ db = WorkerDB.openWithOpfs(syncHandle);
469
+ log(`Opened "${fileName}" via OPFS (Web Locks)`);
470
+ resolve(); // signal doInit complete — caller can proceed
471
+
472
+ // Hold the lock by keeping this async callback alive.
473
+ // Resolved by the 'close' op or when the worker is terminated.
474
+ await new Promise(res => { releaseLock = res; });
475
+
476
+ syncHandle.close();
477
+ db = null;
478
+ } catch (e) {
479
+ reject(e);
480
+ }
481
+ });
482
+ });
213
483
  }
214
484
 
485
+ // ---------------------------------------------------------------------------
486
+ // OPFS capability probe
487
+ // ---------------------------------------------------------------------------
488
+
215
489
  async function checkOpfs() {
216
490
  try {
217
- await navigator.storage.getDirectory();
491
+ const root = await navigator.storage.getDirectory();
492
+ // Probe createSyncAccessHandle — only available in Dedicated Workers.
493
+ // getDirectory() succeeding alone is not sufficient.
494
+ // Use a unique filename so concurrent workers don't collide on the same
495
+ // probe file (each createSyncAccessHandle is exclusive).
496
+ const probeName = `_taladb_probe_${Date.now()}_${Math.random().toString(36).slice(2)}`;
497
+ const probe = await root.getFileHandle(probeName, { create: true });
498
+ if (typeof probe.createSyncAccessHandle !== 'function') return false;
499
+ const handle = await probe.createSyncAccessHandle();
500
+ handle.close();
501
+ await root.removeEntry(probeName);
218
502
  return true;
219
503
  } catch {
220
504
  return false;