@taladb/web 0.3.0 → 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.3.0",
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.3.0",
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;
@@ -43,10 +43,26 @@
43
43
  * dropIndex { collection, field }
44
44
  * createFtsIndex { collection, field }
45
45
  * dropFtsIndex { collection, field }
46
- * createVectorIndex { collection, field, dimensions, metric? }
46
+ * listIndexes { collection } → JSON { btree, fts, vector }
47
+ * createVectorIndex { collection, field, dimensions, metric?, indexType?, hnswM?, hnswEfConstruction? }
47
48
  * dropVectorIndex { collection, field }
49
+ * upgradeVectorIndex { collection, field }
48
50
  * findNearest { collection, field, queryJson, topK, filterJson? }
49
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.
50
66
  */
51
67
 
52
68
  // ---------------------------------------------------------------------------
@@ -56,6 +72,27 @@
56
72
  /** @type {import('../pkg/taladb_web').WorkerDB | null} */
57
73
  let db = null;
58
74
 
75
+ /**
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
+
59
96
  /**
60
97
  * Deduplicates concurrent init calls for the same dbName within one worker.
61
98
  * @type {Map<string, Promise<void>>}
@@ -76,6 +113,99 @@ const warn = isDev ? console.warn.bind(console, '[TalaDB Worker]') : () => {};
76
113
  */
77
114
  let releaseLock = null;
78
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
+
130
+ // ---------------------------------------------------------------------------
131
+ // IndexedDB helpers (used only when OPFS is unavailable)
132
+ // ---------------------------------------------------------------------------
133
+
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
+ }
174
+
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) {
202
+ try {
203
+ const bytes = db.exportSnapshot();
204
+ idbSaveSnapshot(activeDbName, bytes).catch(() => {});
205
+ } catch { /* ignore snapshot export errors */ }
206
+ }
207
+ }
208
+
79
209
  // ---------------------------------------------------------------------------
80
210
  // Dedicated Worker message handler
81
211
  // ---------------------------------------------------------------------------
@@ -95,6 +225,16 @@ self.onmessage = async (e) => {
95
225
  // ---------------------------------------------------------------------------
96
226
 
97
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
+
98
238
  if (op === 'init') {
99
239
  const { dbName } = args;
100
240
 
@@ -116,11 +256,17 @@ async function dispatch(op, args) {
116
256
  if (!db) throw new Error('TalaDB worker not initialised — call init first');
117
257
 
118
258
  switch (op) {
119
- case 'insert':
120
- 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
+ }
121
264
 
122
- case 'insertMany':
123
- 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
+ }
124
270
 
125
271
  case 'find':
126
272
  return db.find(args.collection, args.filterJson ?? 'null');
@@ -128,17 +274,29 @@ async function dispatch(op, args) {
128
274
  case 'findOne':
129
275
  return db.findOne(args.collection, args.filterJson ?? 'null');
130
276
 
131
- case 'updateOne':
132
- 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
+ }
133
282
 
134
- case 'updateMany':
135
- 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
+ }
136
288
 
137
- case 'deleteOne':
138
- 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
+ }
139
294
 
140
- case 'deleteMany':
141
- 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
+ }
142
300
 
143
301
  case 'count':
144
302
  return db.count(args.collection, args.filterJson ?? 'null');
@@ -159,14 +317,29 @@ async function dispatch(op, args) {
159
317
  db.dropFtsIndex(args.collection, args.field);
160
318
  return null;
161
319
 
320
+ case 'listIndexes':
321
+ return db.listIndexes(args.collection);
322
+
162
323
  case 'createVectorIndex':
163
- 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
+ );
164
333
  return null;
165
334
 
166
335
  case 'dropVectorIndex':
167
336
  db.dropVectorIndex(args.collection, args.field);
168
337
  return null;
169
338
 
339
+ case 'upgradeVectorIndex':
340
+ db.upgradeVectorIndex(args.collection, args.field);
341
+ return null;
342
+
170
343
  case 'findNearest':
171
344
  return db.findNearest(
172
345
  args.collection,
@@ -179,6 +352,9 @@ async function dispatch(op, args) {
179
352
  case 'close':
180
353
  // Release the Web Lock and close the sync handle gracefully.
181
354
  if (releaseLock) { releaseLock(); releaseLock = null; }
355
+ broadcastChannel?.close();
356
+ broadcastChannel = null;
357
+ idbFallback = false;
182
358
  db = null;
183
359
  return null;
184
360
 
@@ -195,12 +371,45 @@ async function doInit(dbName) {
195
371
  const wasm = await import('../pkg/taladb_web.js');
196
372
  await wasm.default();
197
373
 
198
- const { WorkerDB } = wasm;
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
+ }
199
401
 
200
402
  const opfsAvailable = await checkOpfs();
201
403
  if (!opfsAvailable) {
202
- warn('OPFS unavailable — falling back to in-memory');
203
- 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
+ }
204
413
  return;
205
414
  }
206
415
 
@@ -217,12 +426,43 @@ async function doInit(dbName) {
217
426
  return;
218
427
  }
219
428
 
220
- // Acquire an exclusive lock on the database file.
221
- // If another tab's worker already holds it, this call blocks until that
222
- // worker calls close() or the tab is terminated (lock auto-released).
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.
223
435
  const lockName = `taladb:${fileName}`;
224
436
  await new Promise((resolve, reject) => {
225
- navigator.locks.request(lockName, async () => {
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.
226
466
  try {
227
467
  const syncHandle = await fileHandle.createSyncAccessHandle();
228
468
  db = WorkerDB.openWithOpfs(syncHandle);