@taladb/web 0.8.4 → 0.9.1

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.8.4",
3
+ "version": "0.9.1",
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) and Cloudflare Workers",
8
- "version": "0.8.4",
8
+ "version": "0.9.1",
9
9
  "license": "MIT",
10
10
  "files": [
11
11
  "taladb_web_bg.wasm",
@@ -14,6 +14,10 @@ export class CollectionWasm {
14
14
  * Count documents matching the filter.
15
15
  */
16
16
  count(filter: any): number;
17
+ /**
18
+ * Create a compound index. `fields_json` is a JSON array of field names.
19
+ */
20
+ createCompoundIndex(fields_json: string): void;
17
21
  /**
18
22
  * Create a secondary index on a field.
19
23
  */
@@ -36,6 +40,10 @@ export class CollectionWasm {
36
40
  * Delete the first matching document. Returns true if deleted.
37
41
  */
38
42
  deleteOne(filter: any): boolean;
43
+ /**
44
+ * Drop a compound index by its ordered field list (`fields_json`).
45
+ */
46
+ dropCompoundIndex(fields_json: string): void;
39
47
  /**
40
48
  * Drop a secondary index.
41
49
  */
@@ -110,6 +118,11 @@ export class TalaDBWasm {
110
118
  * database via Last-Write-Wins. Returns the number of documents changed.
111
119
  */
112
120
  importChanges(changeset_json: string): number;
121
+ /**
122
+ * User collection names (reserved `_`-prefixed collections excluded).
123
+ * Backs the sync orchestration's "sync all collections" default.
124
+ */
125
+ listCollectionNames(): string[];
113
126
  /**
114
127
  * Open an in-memory database (suitable for tests and environments without OPFS).
115
128
  */
@@ -169,6 +182,10 @@ export class WorkerDB {
169
182
  * Count matching documents.
170
183
  */
171
184
  count(collection: string, filter_json: string): number;
185
+ /**
186
+ * Create a compound index. `fields_json` is a JSON array of field names.
187
+ */
188
+ createCompoundIndex(collection: string, fields_json: string): void;
172
189
  createFtsIndex(collection: string, field: string): void;
173
190
  createIndex(collection: string, field: string): void;
174
191
  /**
@@ -188,6 +205,10 @@ export class WorkerDB {
188
205
  * Delete the first matching document. Returns `true` / `false`.
189
206
  */
190
207
  deleteOne(collection: string, filter_json: string): boolean;
208
+ /**
209
+ * Drop a compound index by its ordered field list (`fields_json`).
210
+ */
211
+ dropCompoundIndex(collection: string, fields_json: string): void;
191
212
  dropFtsIndex(collection: string, field: string): void;
192
213
  dropIndex(collection: string, field: string): void;
193
214
  /**
@@ -272,7 +293,7 @@ export class WorkerDB {
272
293
  * const db = WorkerDB.openWithConfigAndOpfs(handle, JSON.stringify(config));
273
294
  * ```
274
295
  */
275
- static openWithConfigAndOpfs(sync_handle: FileSystemSyncAccessHandle, config_json?: string | null): WorkerDB;
296
+ static openWithConfigAndOpfs(sync_handle: FileSystemSyncAccessHandle, config_json?: string | null, passphrase?: string | null, salt?: Uint8Array | null): WorkerDB;
276
297
  /**
277
298
  * Open a database from an optional snapshot with HTTP push sync config.
278
299
  *
@@ -307,6 +328,8 @@ export class WorkerDB {
307
328
  * ```
308
329
  */
309
330
  static openWithSnapshot(data?: Uint8Array | null): WorkerDB;
331
+ syncPending(): bigint;
332
+ syncStatus(): string;
310
333
  /**
311
334
  * Update all matching documents. Returns the count updated.
312
335
  */
@@ -382,11 +405,13 @@ export interface InitOutput {
382
405
  readonly workerdb_compact: (a: number) => [number, number];
383
406
  readonly workerdb_compactTombstones: (a: number, b: number, c: number, d: number) => [number, number, number];
384
407
  readonly workerdb_count: (a: number, b: number, c: number, d: number, e: number) => [number, number, number];
408
+ readonly workerdb_createCompoundIndex: (a: number, b: number, c: number, d: number, e: number) => [number, number];
385
409
  readonly workerdb_createFtsIndex: (a: number, b: number, c: number, d: number, e: number) => [number, number];
386
410
  readonly workerdb_createIndex: (a: number, b: number, c: number, d: number, e: number) => [number, number];
387
411
  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];
388
412
  readonly workerdb_deleteMany: (a: number, b: number, c: number, d: number, e: number) => [number, number, number];
389
413
  readonly workerdb_deleteOne: (a: number, b: number, c: number, d: number, e: number) => [number, number, number];
414
+ readonly workerdb_dropCompoundIndex: (a: number, b: number, c: number, d: number, e: number) => [number, number];
390
415
  readonly workerdb_dropFtsIndex: (a: number, b: number, c: number, d: number, e: number) => [number, number];
391
416
  readonly workerdb_dropIndex: (a: number, b: number, c: number, d: number, e: number) => [number, number];
392
417
  readonly workerdb_dropVectorIndex: (a: number, b: number, c: number, d: number, e: number) => [number, number];
@@ -401,10 +426,12 @@ export interface InitOutput {
401
426
  readonly workerdb_listCollections: (a: number) => [number, number, number, number];
402
427
  readonly workerdb_listIndexes: (a: number, b: number, c: number) => [number, number, number, number];
403
428
  readonly workerdb_openInMemory: () => [number, number, number];
404
- readonly workerdb_openWithConfigAndOpfs: (a: any, b: number, c: number) => [number, number, number];
429
+ readonly workerdb_openWithConfigAndOpfs: (a: any, b: number, c: number, d: number, e: number, f: number, g: number) => [number, number, number];
405
430
  readonly workerdb_openWithConfigAndSnapshot: (a: number, b: number, c: number, d: number) => [number, number, number];
406
431
  readonly workerdb_openWithOpfs: (a: any) => [number, number, number];
407
432
  readonly workerdb_openWithSnapshot: (a: number, b: number) => [number, number, number];
433
+ readonly workerdb_syncPending: (a: number) => bigint;
434
+ readonly workerdb_syncStatus: (a: number) => [number, number];
408
435
  readonly workerdb_updateMany: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => [number, number, number];
409
436
  readonly workerdb_updateOne: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => [number, number, number];
410
437
  readonly workerdb_upgradeVectorIndex: (a: number, b: number, c: number, d: number, e: number) => [number, number];
@@ -412,10 +439,12 @@ export interface InitOutput {
412
439
  readonly __wbg_taladbwasm_free: (a: number, b: number) => void;
413
440
  readonly collectionwasm_aggregate: (a: number, b: any) => [number, number, number];
414
441
  readonly collectionwasm_count: (a: number, b: any) => [number, number, number];
442
+ readonly collectionwasm_createCompoundIndex: (a: number, b: number, c: number) => [number, number];
415
443
  readonly collectionwasm_createIndex: (a: number, b: number, c: number) => [number, number];
416
444
  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];
417
445
  readonly collectionwasm_deleteMany: (a: number, b: any) => [number, number, number];
418
446
  readonly collectionwasm_deleteOne: (a: number, b: any) => [number, number, number];
447
+ readonly collectionwasm_dropCompoundIndex: (a: number, b: number, c: number) => [number, number];
419
448
  readonly collectionwasm_dropIndex: (a: number, b: number, c: number) => [number, number];
420
449
  readonly collectionwasm_dropVectorIndex: (a: number, b: number, c: number) => [number, number];
421
450
  readonly collectionwasm_find: (a: number, b: any) => [number, number, number];
@@ -430,16 +459,17 @@ export interface InitOutput {
430
459
  readonly taladbwasm_exportChanges: (a: number, b: number, c: number, d: number) => [number, number, number, number];
431
460
  readonly taladbwasm_exportSnapshot: (a: number) => [number, number, number, number];
432
461
  readonly taladbwasm_importChanges: (a: number, b: number, c: number) => [number, number, number];
462
+ readonly taladbwasm_listCollectionNames: (a: number) => [number, number, number, number];
433
463
  readonly taladbwasm_openInMemory: () => [number, number, number];
434
464
  readonly taladbwasm_openWithSnapshot: (a: number, b: number) => [number, number, number];
435
465
  readonly init: () => void;
436
- readonly idb_load_snapshot: (a: number, b: number) => any;
437
- readonly idb_save_snapshot: (a: number, b: number, c: number, d: number) => any;
438
466
  readonly opfs_open_backend: (a: number, b: number) => any;
439
467
  readonly is_opfs_available: () => any;
440
468
  readonly opfs_delete_snapshot: (a: number, b: number) => any;
441
469
  readonly opfs_flush_snapshot: (a: number, b: number, c: number, d: number) => any;
442
470
  readonly opfs_load_snapshot: (a: number, b: number) => any;
471
+ readonly idb_load_snapshot: (a: number, b: number) => any;
472
+ readonly idb_save_snapshot: (a: number, b: number, c: number, d: number) => any;
443
473
  readonly wasm_bindgen_60ce93de3f474933___closure__destroy___dyn_core_7d5f0a2ba6a62c33___ops__function__FnMut_____Output_______: (a: number, b: number) => void;
444
474
  readonly wasm_bindgen_60ce93de3f474933___closure__destroy___dyn_core_7d5f0a2ba6a62c33___ops__function__FnMut__wasm_bindgen_60ce93de3f474933___JsValue____Output___core_7d5f0a2ba6a62c33___result__Result_____wasm_bindgen_60ce93de3f474933___JsError___: (a: number, b: number) => void;
445
475
  readonly wasm_bindgen_60ce93de3f474933___closure__destroy___dyn_core_7d5f0a2ba6a62c33___ops__function__FnMut__wasm_bindgen_60ce93de3f474933___JsValue____Output_______: (a: number, b: number) => void;
@@ -454,6 +484,7 @@ export interface InitOutput {
454
484
  readonly __wbindgen_externrefs: WebAssembly.Table;
455
485
  readonly __wbindgen_free: (a: number, b: number, c: number) => void;
456
486
  readonly __externref_table_dealloc: (a: number) => void;
487
+ readonly __externref_drop_slice: (a: number, b: number) => void;
457
488
  readonly __wbindgen_start: () => void;
458
489
  }
459
490
 
package/pkg/taladb_web.js CHANGED
@@ -43,6 +43,18 @@ export class CollectionWasm {
43
43
  }
44
44
  return ret[0] >>> 0;
45
45
  }
46
+ /**
47
+ * Create a compound index. `fields_json` is a JSON array of field names.
48
+ * @param {string} fields_json
49
+ */
50
+ createCompoundIndex(fields_json) {
51
+ const ptr0 = passStringToWasm0(fields_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
52
+ const len0 = WASM_VECTOR_LEN;
53
+ const ret = wasm.collectionwasm_createCompoundIndex(this.__wbg_ptr, ptr0, len0);
54
+ if (ret[1]) {
55
+ throw takeFromExternrefTable0(ret[0]);
56
+ }
57
+ }
46
58
  /**
47
59
  * Create a secondary index on a field.
48
60
  * @param {string} field
@@ -106,6 +118,18 @@ export class CollectionWasm {
106
118
  }
107
119
  return ret[0] !== 0;
108
120
  }
121
+ /**
122
+ * Drop a compound index by its ordered field list (`fields_json`).
123
+ * @param {string} fields_json
124
+ */
125
+ dropCompoundIndex(fields_json) {
126
+ const ptr0 = passStringToWasm0(fields_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
127
+ const len0 = WASM_VECTOR_LEN;
128
+ const ret = wasm.collectionwasm_dropCompoundIndex(this.__wbg_ptr, ptr0, len0);
129
+ if (ret[1]) {
130
+ throw takeFromExternrefTable0(ret[0]);
131
+ }
132
+ }
109
133
  /**
110
134
  * Drop a secondary index.
111
135
  * @param {string} field
@@ -346,6 +370,20 @@ export class TalaDBWasm {
346
370
  }
347
371
  return ret[0] >>> 0;
348
372
  }
373
+ /**
374
+ * User collection names (reserved `_`-prefixed collections excluded).
375
+ * Backs the sync orchestration's "sync all collections" default.
376
+ * @returns {string[]}
377
+ */
378
+ listCollectionNames() {
379
+ const ret = wasm.taladbwasm_listCollectionNames(this.__wbg_ptr);
380
+ if (ret[3]) {
381
+ throw takeFromExternrefTable0(ret[2]);
382
+ }
383
+ var v1 = getArrayJsValueFromWasm0(ret[0], ret[1]).slice();
384
+ wasm.__wbindgen_free(ret[0], ret[1] * 4, 4);
385
+ return v1;
386
+ }
349
387
  /**
350
388
  * Open an in-memory database (suitable for tests and environments without OPFS).
351
389
  * @returns {TalaDBWasm}
@@ -490,6 +528,21 @@ export class WorkerDB {
490
528
  }
491
529
  return ret[0] >>> 0;
492
530
  }
531
+ /**
532
+ * Create a compound index. `fields_json` is a JSON array of field names.
533
+ * @param {string} collection
534
+ * @param {string} fields_json
535
+ */
536
+ createCompoundIndex(collection, fields_json) {
537
+ const ptr0 = passStringToWasm0(collection, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
538
+ const len0 = WASM_VECTOR_LEN;
539
+ const ptr1 = passStringToWasm0(fields_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
540
+ const len1 = WASM_VECTOR_LEN;
541
+ const ret = wasm.workerdb_createCompoundIndex(this.__wbg_ptr, ptr0, len0, ptr1, len1);
542
+ if (ret[1]) {
543
+ throw takeFromExternrefTable0(ret[0]);
544
+ }
545
+ }
493
546
  /**
494
547
  * @param {string} collection
495
548
  * @param {string} field
@@ -581,6 +634,21 @@ export class WorkerDB {
581
634
  }
582
635
  return ret[0] !== 0;
583
636
  }
637
+ /**
638
+ * Drop a compound index by its ordered field list (`fields_json`).
639
+ * @param {string} collection
640
+ * @param {string} fields_json
641
+ */
642
+ dropCompoundIndex(collection, fields_json) {
643
+ const ptr0 = passStringToWasm0(collection, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
644
+ const len0 = WASM_VECTOR_LEN;
645
+ const ptr1 = passStringToWasm0(fields_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
646
+ const len1 = WASM_VECTOR_LEN;
647
+ const ret = wasm.workerdb_dropCompoundIndex(this.__wbg_ptr, ptr0, len0, ptr1, len1);
648
+ if (ret[1]) {
649
+ throw takeFromExternrefTable0(ret[0]);
650
+ }
651
+ }
584
652
  /**
585
653
  * @param {string} collection
586
654
  * @param {string} field
@@ -917,12 +985,18 @@ export class WorkerDB {
917
985
  * ```
918
986
  * @param {FileSystemSyncAccessHandle} sync_handle
919
987
  * @param {string | null} [config_json]
988
+ * @param {string | null} [passphrase]
989
+ * @param {Uint8Array | null} [salt]
920
990
  * @returns {WorkerDB}
921
991
  */
922
- static openWithConfigAndOpfs(sync_handle, config_json) {
992
+ static openWithConfigAndOpfs(sync_handle, config_json, passphrase, salt) {
923
993
  var ptr0 = isLikeNone(config_json) ? 0 : passStringToWasm0(config_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
924
994
  var len0 = WASM_VECTOR_LEN;
925
- const ret = wasm.workerdb_openWithConfigAndOpfs(sync_handle, ptr0, len0);
995
+ var ptr1 = isLikeNone(passphrase) ? 0 : passStringToWasm0(passphrase, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
996
+ var len1 = WASM_VECTOR_LEN;
997
+ var ptr2 = isLikeNone(salt) ? 0 : passArray8ToWasm0(salt, wasm.__wbindgen_malloc);
998
+ var len2 = WASM_VECTOR_LEN;
999
+ const ret = wasm.workerdb_openWithConfigAndOpfs(sync_handle, ptr0, len0, ptr1, len1, ptr2, len2);
926
1000
  if (ret[2]) {
927
1001
  throw takeFromExternrefTable0(ret[1]);
928
1002
  }
@@ -993,6 +1067,28 @@ export class WorkerDB {
993
1067
  }
994
1068
  return WorkerDB.__wrap(ret[0]);
995
1069
  }
1070
+ /**
1071
+ * @returns {bigint}
1072
+ */
1073
+ syncPending() {
1074
+ const ret = wasm.workerdb_syncPending(this.__wbg_ptr);
1075
+ return BigInt.asUintN(64, ret);
1076
+ }
1077
+ /**
1078
+ * @returns {string}
1079
+ */
1080
+ syncStatus() {
1081
+ let deferred1_0;
1082
+ let deferred1_1;
1083
+ try {
1084
+ const ret = wasm.workerdb_syncStatus(this.__wbg_ptr);
1085
+ deferred1_0 = ret[0];
1086
+ deferred1_1 = ret[1];
1087
+ return getStringFromWasm0(ret[0], ret[1]);
1088
+ } finally {
1089
+ wasm.__wbindgen_free(deferred1_0, deferred1_1, 1);
1090
+ }
1091
+ }
996
1092
  /**
997
1093
  * Update all matching documents. Returns the count updated.
998
1094
  * @param {string} collection
@@ -1291,6 +1387,10 @@ function __wbg_get_imports() {
1291
1387
  const ret = arg0.createWritable();
1292
1388
  return ret;
1293
1389
  },
1390
+ __wbg_crypto_38df2bab126b63dc: function(arg0) {
1391
+ const ret = arg0.crypto;
1392
+ return ret;
1393
+ },
1294
1394
  __wbg_done_08ce71ee07e3bd17: function(arg0) {
1295
1395
  const ret = arg0.done;
1296
1396
  return ret;
@@ -1340,6 +1440,9 @@ function __wbg_get_imports() {
1340
1440
  __wbg_getRandomValues_3f44b700395062e5: function() { return handleError(function (arg0, arg1) {
1341
1441
  globalThis.crypto.getRandomValues(getArrayU8FromWasm0(arg0, arg1));
1342
1442
  }, arguments); },
1443
+ __wbg_getRandomValues_c44a50d8cfdaebeb: function() { return handleError(function (arg0, arg1) {
1444
+ arg0.getRandomValues(arg1);
1445
+ }, arguments); },
1343
1446
  __wbg_getSize_0a16c5e2524d34aa: function() { return handleError(function (arg0) {
1344
1447
  const ret = arg0.getSize();
1345
1448
  return ret;
@@ -1498,6 +1601,10 @@ function __wbg_get_imports() {
1498
1601
  const ret = arg0.length;
1499
1602
  return ret;
1500
1603
  },
1604
+ __wbg_msCrypto_bd5a034af96bcba6: function(arg0) {
1605
+ const ret = arg0.msCrypto;
1606
+ return ret;
1607
+ },
1501
1608
  __wbg_navigator_9cebf56f28aa719b: function(arg0) {
1502
1609
  const ret = arg0.navigator;
1503
1610
  return ret;
@@ -1586,6 +1693,10 @@ function __wbg_get_imports() {
1586
1693
  const ret = arg0.next;
1587
1694
  return ret;
1588
1695
  },
1696
+ __wbg_node_84ea875411254db1: function(arg0) {
1697
+ const ret = arg0.node;
1698
+ return ret;
1699
+ },
1589
1700
  __wbg_now_16f0c993d5dd6c27: function() {
1590
1701
  const ret = Date.now();
1591
1702
  return ret;
@@ -1594,6 +1705,10 @@ function __wbg_get_imports() {
1594
1705
  const ret = Array.of(arg0, arg1);
1595
1706
  return ret;
1596
1707
  },
1708
+ __wbg_process_44c7a14e11e9f69e: function(arg0) {
1709
+ const ret = arg0.process;
1710
+ return ret;
1711
+ },
1597
1712
  __wbg_prototypesetcall_d62e5099504357e6: function(arg0, arg1, arg2) {
1598
1713
  Uint8Array.prototype.set.call(getArrayU8FromWasm0(arg0, arg1), arg2);
1599
1714
  },
@@ -1608,10 +1723,17 @@ function __wbg_get_imports() {
1608
1723
  __wbg_queueMicrotask_a082d78ce798393e: function(arg0) {
1609
1724
  queueMicrotask(arg0);
1610
1725
  },
1726
+ __wbg_randomFillSync_6c25eac9869eb53c: function() { return handleError(function (arg0, arg1) {
1727
+ arg0.randomFillSync(arg1);
1728
+ }, arguments); },
1611
1729
  __wbg_removeEntry_b2a4d6d0ee2040c5: function(arg0, arg1, arg2) {
1612
1730
  const ret = arg0.removeEntry(getStringFromWasm0(arg1, arg2));
1613
1731
  return ret;
1614
1732
  },
1733
+ __wbg_require_b4edbdcf3e2a1ef0: function() { return handleError(function () {
1734
+ const ret = module.require;
1735
+ return ret;
1736
+ }, arguments); },
1615
1737
  __wbg_resolve_ae8d83246e5bcc12: function(arg0) {
1616
1738
  const ret = Promise.resolve(arg0);
1617
1739
  return ret;
@@ -1697,6 +1819,10 @@ function __wbg_get_imports() {
1697
1819
  const ret = JSON.stringify(arg0);
1698
1820
  return ret;
1699
1821
  }, arguments); },
1822
+ __wbg_subarray_a068d24e39478a8a: function(arg0, arg1, arg2) {
1823
+ const ret = arg0.subarray(arg1 >>> 0, arg2 >>> 0);
1824
+ return ret;
1825
+ },
1700
1826
  __wbg_then_098abe61755d12f6: function(arg0, arg1) {
1701
1827
  const ret = arg0.then(arg1);
1702
1828
  return ret;
@@ -1719,18 +1845,22 @@ function __wbg_get_imports() {
1719
1845
  const ret = arg0.value;
1720
1846
  return ret;
1721
1847
  },
1848
+ __wbg_versions_276b2795b1c6a219: function(arg0) {
1849
+ const ret = arg0.versions;
1850
+ return ret;
1851
+ },
1722
1852
  __wbindgen_cast_0000000000000001: function(arg0, arg1) {
1723
- // Cast intrinsic for `Closure(Closure { dtor_idx: 208, function: Function { arguments: [], shim_idx: 209, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
1853
+ // Cast intrinsic for `Closure(Closure { dtor_idx: 215, function: Function { arguments: [], shim_idx: 216, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
1724
1854
  const ret = makeMutClosure(arg0, arg1, wasm.wasm_bindgen_60ce93de3f474933___closure__destroy___dyn_core_7d5f0a2ba6a62c33___ops__function__FnMut_____Output_______, wasm_bindgen_60ce93de3f474933___convert__closures_____invoke_______true_);
1725
1855
  return ret;
1726
1856
  },
1727
1857
  __wbindgen_cast_0000000000000002: function(arg0, arg1) {
1728
- // Cast intrinsic for `Closure(Closure { dtor_idx: 647, function: Function { arguments: [Externref], shim_idx: 648, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
1858
+ // Cast intrinsic for `Closure(Closure { dtor_idx: 692, function: Function { arguments: [Externref], shim_idx: 693, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
1729
1859
  const ret = makeMutClosure(arg0, arg1, wasm.wasm_bindgen_60ce93de3f474933___closure__destroy___dyn_core_7d5f0a2ba6a62c33___ops__function__FnMut__wasm_bindgen_60ce93de3f474933___JsValue____Output___core_7d5f0a2ba6a62c33___result__Result_____wasm_bindgen_60ce93de3f474933___JsError___, wasm_bindgen_60ce93de3f474933___convert__closures_____invoke___wasm_bindgen_60ce93de3f474933___JsValue__core_7d5f0a2ba6a62c33___result__Result_____wasm_bindgen_60ce93de3f474933___JsError___true_);
1730
1860
  return ret;
1731
1861
  },
1732
1862
  __wbindgen_cast_0000000000000003: function(arg0, arg1) {
1733
- // Cast intrinsic for `Closure(Closure { dtor_idx: 83, function: Function { arguments: [Externref], shim_idx: 84, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
1863
+ // Cast intrinsic for `Closure(Closure { dtor_idx: 71, function: Function { arguments: [Externref], shim_idx: 72, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
1734
1864
  const ret = makeMutClosure(arg0, arg1, wasm.wasm_bindgen_60ce93de3f474933___closure__destroy___dyn_core_7d5f0a2ba6a62c33___ops__function__FnMut__wasm_bindgen_60ce93de3f474933___JsValue____Output_______, wasm_bindgen_60ce93de3f474933___convert__closures_____invoke___wasm_bindgen_60ce93de3f474933___JsValue______true_);
1735
1865
  return ret;
1736
1866
  },
@@ -1745,16 +1875,21 @@ function __wbg_get_imports() {
1745
1875
  return ret;
1746
1876
  },
1747
1877
  __wbindgen_cast_0000000000000006: function(arg0, arg1) {
1878
+ // Cast intrinsic for `Ref(Slice(U8)) -> NamedExternref("Uint8Array")`.
1879
+ const ret = getArrayU8FromWasm0(arg0, arg1);
1880
+ return ret;
1881
+ },
1882
+ __wbindgen_cast_0000000000000007: function(arg0, arg1) {
1748
1883
  // Cast intrinsic for `Ref(String) -> Externref`.
1749
1884
  const ret = getStringFromWasm0(arg0, arg1);
1750
1885
  return ret;
1751
1886
  },
1752
- __wbindgen_cast_0000000000000007: function(arg0) {
1887
+ __wbindgen_cast_0000000000000008: function(arg0) {
1753
1888
  // Cast intrinsic for `U64 -> Externref`.
1754
1889
  const ret = BigInt.asUintN(64, arg0);
1755
1890
  return ret;
1756
1891
  },
1757
- __wbindgen_cast_0000000000000008: function(arg0, arg1) {
1892
+ __wbindgen_cast_0000000000000009: function(arg0, arg1) {
1758
1893
  var v0 = getArrayU8FromWasm0(arg0, arg1).slice();
1759
1894
  wasm.__wbindgen_free(arg0, arg1 * 1, 1);
1760
1895
  // Cast intrinsic for `Vector(U8) -> Externref`.
@@ -1889,6 +2024,17 @@ function debugString(val) {
1889
2024
  return className;
1890
2025
  }
1891
2026
 
2027
+ function getArrayJsValueFromWasm0(ptr, len) {
2028
+ ptr = ptr >>> 0;
2029
+ const mem = getDataViewMemory0();
2030
+ const result = [];
2031
+ for (let i = ptr; i < ptr + 4 * len; i += 4) {
2032
+ result.push(wasm.__wbindgen_externrefs.get(mem.getUint32(i, true)));
2033
+ }
2034
+ wasm.__externref_drop_slice(ptr, len);
2035
+ return result;
2036
+ }
2037
+
1892
2038
  function getArrayU8FromWasm0(ptr, len) {
1893
2039
  ptr = ptr >>> 0;
1894
2040
  return getUint8ArrayMemory0().subarray(ptr / 1, ptr / 1 + len);
Binary file
@@ -6,11 +6,13 @@ export const workerdb_aggregate: (a: number, b: number, c: number, d: number, e:
6
6
  export const workerdb_compact: (a: number) => [number, number];
7
7
  export const workerdb_compactTombstones: (a: number, b: number, c: number, d: number) => [number, number, number];
8
8
  export const workerdb_count: (a: number, b: number, c: number, d: number, e: number) => [number, number, number];
9
+ export const workerdb_createCompoundIndex: (a: number, b: number, c: number, d: number, e: number) => [number, number];
9
10
  export const workerdb_createFtsIndex: (a: number, b: number, c: number, d: number, e: number) => [number, number];
10
11
  export const workerdb_createIndex: (a: number, b: number, c: number, d: number, e: number) => [number, number];
11
12
  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];
12
13
  export const workerdb_deleteMany: (a: number, b: number, c: number, d: number, e: number) => [number, number, number];
13
14
  export const workerdb_deleteOne: (a: number, b: number, c: number, d: number, e: number) => [number, number, number];
15
+ export const workerdb_dropCompoundIndex: (a: number, b: number, c: number, d: number, e: number) => [number, number];
14
16
  export const workerdb_dropFtsIndex: (a: number, b: number, c: number, d: number, e: number) => [number, number];
15
17
  export const workerdb_dropIndex: (a: number, b: number, c: number, d: number, e: number) => [number, number];
16
18
  export const workerdb_dropVectorIndex: (a: number, b: number, c: number, d: number, e: number) => [number, number];
@@ -25,10 +27,12 @@ export const workerdb_insertMany: (a: number, b: number, c: number, d: number, e
25
27
  export const workerdb_listCollections: (a: number) => [number, number, number, number];
26
28
  export const workerdb_listIndexes: (a: number, b: number, c: number) => [number, number, number, number];
27
29
  export const workerdb_openInMemory: () => [number, number, number];
28
- export const workerdb_openWithConfigAndOpfs: (a: any, b: number, c: number) => [number, number, number];
30
+ export const workerdb_openWithConfigAndOpfs: (a: any, b: number, c: number, d: number, e: number, f: number, g: number) => [number, number, number];
29
31
  export const workerdb_openWithConfigAndSnapshot: (a: number, b: number, c: number, d: number) => [number, number, number];
30
32
  export const workerdb_openWithOpfs: (a: any) => [number, number, number];
31
33
  export const workerdb_openWithSnapshot: (a: number, b: number) => [number, number, number];
34
+ export const workerdb_syncPending: (a: number) => bigint;
35
+ export const workerdb_syncStatus: (a: number) => [number, number];
32
36
  export const workerdb_updateMany: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => [number, number, number];
33
37
  export const workerdb_updateOne: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => [number, number, number];
34
38
  export const workerdb_upgradeVectorIndex: (a: number, b: number, c: number, d: number, e: number) => [number, number];
@@ -36,10 +40,12 @@ export const __wbg_collectionwasm_free: (a: number, b: number) => void;
36
40
  export const __wbg_taladbwasm_free: (a: number, b: number) => void;
37
41
  export const collectionwasm_aggregate: (a: number, b: any) => [number, number, number];
38
42
  export const collectionwasm_count: (a: number, b: any) => [number, number, number];
43
+ export const collectionwasm_createCompoundIndex: (a: number, b: number, c: number) => [number, number];
39
44
  export const collectionwasm_createIndex: (a: number, b: number, c: number) => [number, number];
40
45
  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];
41
46
  export const collectionwasm_deleteMany: (a: number, b: any) => [number, number, number];
42
47
  export const collectionwasm_deleteOne: (a: number, b: any) => [number, number, number];
48
+ export const collectionwasm_dropCompoundIndex: (a: number, b: number, c: number) => [number, number];
43
49
  export const collectionwasm_dropIndex: (a: number, b: number, c: number) => [number, number];
44
50
  export const collectionwasm_dropVectorIndex: (a: number, b: number, c: number) => [number, number];
45
51
  export const collectionwasm_find: (a: number, b: any) => [number, number, number];
@@ -54,16 +60,17 @@ export const taladbwasm_collection: (a: number, b: number, c: number) => [number
54
60
  export const taladbwasm_exportChanges: (a: number, b: number, c: number, d: number) => [number, number, number, number];
55
61
  export const taladbwasm_exportSnapshot: (a: number) => [number, number, number, number];
56
62
  export const taladbwasm_importChanges: (a: number, b: number, c: number) => [number, number, number];
63
+ export const taladbwasm_listCollectionNames: (a: number) => [number, number, number, number];
57
64
  export const taladbwasm_openInMemory: () => [number, number, number];
58
65
  export const taladbwasm_openWithSnapshot: (a: number, b: number) => [number, number, number];
59
66
  export const init: () => void;
60
- export const idb_load_snapshot: (a: number, b: number) => any;
61
- export const idb_save_snapshot: (a: number, b: number, c: number, d: number) => any;
62
67
  export const opfs_open_backend: (a: number, b: number) => any;
63
68
  export const is_opfs_available: () => any;
64
69
  export const opfs_delete_snapshot: (a: number, b: number) => any;
65
70
  export const opfs_flush_snapshot: (a: number, b: number, c: number, d: number) => any;
66
71
  export const opfs_load_snapshot: (a: number, b: number) => any;
72
+ export const idb_load_snapshot: (a: number, b: number) => any;
73
+ export const idb_save_snapshot: (a: number, b: number, c: number, d: number) => any;
67
74
  export const wasm_bindgen_60ce93de3f474933___closure__destroy___dyn_core_7d5f0a2ba6a62c33___ops__function__FnMut_____Output_______: (a: number, b: number) => void;
68
75
  export const wasm_bindgen_60ce93de3f474933___closure__destroy___dyn_core_7d5f0a2ba6a62c33___ops__function__FnMut__wasm_bindgen_60ce93de3f474933___JsValue____Output___core_7d5f0a2ba6a62c33___result__Result_____wasm_bindgen_60ce93de3f474933___JsError___: (a: number, b: number) => void;
69
76
  export const wasm_bindgen_60ce93de3f474933___closure__destroy___dyn_core_7d5f0a2ba6a62c33___ops__function__FnMut__wasm_bindgen_60ce93de3f474933___JsValue____Output_______: (a: number, b: number) => void;
@@ -78,4 +85,5 @@ export const __externref_table_alloc: () => number;
78
85
  export const __wbindgen_externrefs: WebAssembly.Table;
79
86
  export const __wbindgen_free: (a: number, b: number, c: number) => void;
80
87
  export const __externref_table_dealloc: (a: number) => void;
88
+ export const __externref_drop_slice: (a: number, b: number) => void;
81
89
  export const __wbindgen_start: () => void;
@@ -162,14 +162,26 @@ let broadcastChannel = null;
162
162
  */
163
163
  let idbFallback = false;
164
164
 
165
+ /** Whether this worker is allowed to publish the authoritative IDB snapshot. */
166
+ let snapshotWriter = false;
167
+
168
+ /**
169
+ * True when the database is opened with a passphrase (encrypted at rest in the
170
+ * OPFS file). Encrypted databases NEVER write a snapshot to IndexedDB — an
171
+ * exported snapshot is decrypted plaintext, so persisting it would defeat
172
+ * encryption. Encrypted mode therefore requires exclusive OPFS and is
173
+ * single-tab: the multi-tab IDB-snapshot fallback is refused, not silently
174
+ * downgraded to plaintext.
175
+ * @type {boolean}
176
+ */
177
+ let encrypted = false;
178
+
165
179
  /**
166
180
  * Timestamp (ms) of the last changeset we exported and broadcast to the
167
181
  * primary tab. Used as `sinceMs` for the next export so we only send the
168
182
  * delta, not the entire database on every write.
169
183
  * @type {number}
170
184
  */
171
- let lastSecondaryPushMs = 0;
172
-
173
185
  // ---------------------------------------------------------------------------
174
186
  // IndexedDB helpers (used only when OPFS is unavailable)
175
187
  // ---------------------------------------------------------------------------
@@ -260,10 +272,11 @@ async function flushSnapshot() {
260
272
  clearTimeout(snapshotTimer);
261
273
  snapshotTimer = null;
262
274
  lastSnapshotMs = Date.now();
263
- if (db && activeDbName) {
275
+ if (db && activeDbName && snapshotWriter) {
264
276
  try {
265
277
  const bytes = db.exportSnapshot();
266
278
  await idbSaveSnapshot(activeDbName, bytes);
279
+ broadcastChannel?.postMessage('taladb:snapshot-ready');
267
280
  } catch { /* best-effort — ignore failures */ }
268
281
  }
269
282
  }
@@ -300,11 +313,13 @@ function pushChangesetToPrimary() {
300
313
  // Collect all collection names from the current in-memory state.
301
314
  // exportChangeset accepts a JSON array of collection names.
302
315
  const collections = db.listCollections();
303
- const changeset = db.exportChangeset(collections, lastSecondaryPushMs);
316
+ // Wall-clock timestamps are not safe incremental cursors: a write can be
317
+ // stamped before an export but commit after its snapshot. Replay is
318
+ // idempotent under LWW and cannot skip such a write.
319
+ const changeset = db.exportChangeset(collections, 0);
304
320
  // Only broadcast if there is actually something to send.
305
321
  const parsed = JSON.parse(changeset);
306
322
  if (parsed.length === 0) return;
307
- lastSecondaryPushMs = Date.now();
308
323
  broadcastChannel.postMessage({ type: 'taladb:secondary-write', token: SESSION_TOKEN, changeset });
309
324
  log(`Broadcast ${parsed.length} change(s) to primary tab`);
310
325
  } catch (err) {
@@ -323,7 +338,7 @@ function onWriteCommitted() {
323
338
  pushChangesetToPrimary();
324
339
  // Debounced IDB flush — keeps other tabs' fallback instances in sync via
325
340
  // BroadcastChannel + snapshotDirty reload without writing to IDB on every op.
326
- scheduleSnapshot();
341
+ if (snapshotWriter) scheduleSnapshot();
327
342
  }
328
343
 
329
344
  // ---------------------------------------------------------------------------
@@ -366,7 +381,7 @@ async function dispatch(op, args) {
366
381
  }
367
382
 
368
383
  if (op === 'init') {
369
- const { dbName, configJson } = args;
384
+ const { dbName, configJson, passphrase } = args;
370
385
 
371
386
  if (activeDbName !== null && activeDbName !== dbName) {
372
387
  throw new Error(
@@ -378,7 +393,7 @@ async function dispatch(op, args) {
378
393
  if (!initPromises.has(dbName)) {
379
394
  activeDbName = dbName;
380
395
  activeConfigJson = configJson ?? null;
381
- initPromises.set(dbName, doInit(dbName, configJson ?? null));
396
+ initPromises.set(dbName, doInit(dbName, configJson ?? null, passphrase ?? null));
382
397
  }
383
398
  await initPromises.get(dbName);
384
399
  return null;
@@ -443,6 +458,14 @@ async function dispatch(op, args) {
443
458
  db.dropIndex(args.collection, args.field);
444
459
  return null;
445
460
 
461
+ case 'createCompoundIndex':
462
+ db.createCompoundIndex(args.collection, args.fieldsJson);
463
+ return null;
464
+
465
+ case 'dropCompoundIndex':
466
+ db.dropCompoundIndex(args.collection, args.fieldsJson);
467
+ return null;
468
+
446
469
  case 'createFtsIndex':
447
470
  db.createFtsIndex(args.collection, args.field);
448
471
  return null;
@@ -491,6 +514,17 @@ async function dispatch(op, args) {
491
514
  db.compact();
492
515
  return null;
493
516
 
517
+ case 'syncStatus':
518
+ return db.syncStatus();
519
+
520
+ case 'flushSync': {
521
+ const deadline = Date.now() + (args.timeoutMs ?? 5000);
522
+ while (db.syncPending() > 0 && Date.now() < deadline) {
523
+ await new Promise(resolve => setTimeout(resolve, 20));
524
+ }
525
+ return db.syncPending() === 0;
526
+ }
527
+
494
528
  case 'compactTombstones':
495
529
  // Prune tombstones older than beforeMs from a collection.
496
530
  // Returns the count of tombstones removed.
@@ -510,6 +544,13 @@ async function dispatch(op, args) {
510
544
  }
511
545
 
512
546
  case 'close':
547
+ // Give accepted HTTP push events a bounded opportunity to finish.
548
+ {
549
+ const deadline = Date.now() + 5000;
550
+ while (db.syncPending() > 0 && Date.now() < deadline) {
551
+ await new Promise(resolve => setTimeout(resolve, 20));
552
+ }
553
+ }
513
554
  // Flush any pending debounced snapshot before releasing the lock so
514
555
  // no writes are lost when the tab closes or navigates away.
515
556
  await flushSnapshot();
@@ -518,6 +559,7 @@ async function dispatch(op, args) {
518
559
  broadcastChannel?.close();
519
560
  broadcastChannel = null;
520
561
  idbFallback = false;
562
+ snapshotWriter = false;
521
563
  db = null;
522
564
  return null;
523
565
 
@@ -526,26 +568,58 @@ async function dispatch(op, args) {
526
568
  }
527
569
  }
528
570
 
571
+ /**
572
+ * Load the 16-byte key-derivation salt from an OPFS sidecar file, or create it
573
+ * on first open. The salt is not secret (it defends against precomputed-hash
574
+ * attacks) but must be stable across opens, so it lives beside the DB file.
575
+ * @returns {Promise<Uint8Array>} the 16-byte salt
576
+ */
577
+ async function loadOrCreateSalt(root, saltFileName) {
578
+ const fh = await root.getFileHandle(saltFileName, { create: true });
579
+ const h = await fh.createSyncAccessHandle();
580
+ try {
581
+ const size = h.getSize();
582
+ if (size === 16) {
583
+ const salt = new Uint8Array(16);
584
+ h.read(salt, { at: 0 });
585
+ return salt;
586
+ }
587
+ if (size === 0) {
588
+ const salt = new Uint8Array(16);
589
+ self.crypto.getRandomValues(salt);
590
+ h.write(salt, { at: 0 });
591
+ h.flush();
592
+ return salt;
593
+ }
594
+ throw new Error(`invalid TalaDB salt file (${size} bytes, expected 16)`);
595
+ } finally {
596
+ h.close();
597
+ }
598
+ }
599
+
529
600
  // ---------------------------------------------------------------------------
530
601
  // Initialisation — load WASM, acquire lock, open OPFS file
531
602
  // ---------------------------------------------------------------------------
532
603
 
533
- async function doInit(dbName, configJson) {
604
+ async function doInit(dbName, configJson, passphrase = null) {
534
605
  const wasm = await import(/* @vite-ignore */ '../pkg/taladb_web.js');
535
606
  await wasm.default();
536
607
 
537
608
  // Hoist to module scope so snapshot reloads in dispatch() can use it.
538
609
  WorkerDB = wasm.WorkerDB;
539
610
 
611
+ encrypted = typeof passphrase === 'string' && passphrase.length > 0;
612
+
540
613
  // Open the BroadcastChannel now that we know the db name.
541
614
  if (typeof BroadcastChannel !== 'undefined') {
542
615
  broadcastChannel = new BroadcastChannel(`taladb:${dbName}`);
543
616
  broadcastChannel.onmessage = async (e) => {
544
617
  if (e.data === 'taladb:changed' && idbFallback) {
545
- // Fallback tab: primary tab wrote — reload snapshot before next read.
546
- snapshotDirty = true;
547
- } else if (e.data === 'taladb:request-snapshot' && !idbFallback && db && activeDbName) {
618
+ // Wait for snapshot-ready: changed is emitted before the primary's
619
+ // asynchronous IDB transaction has committed.
620
+ } else if (e.data === 'taladb:request-snapshot' && !idbFallback && !encrypted && db && activeDbName) {
548
621
  // Primary (OPFS) tab: a new tab asked for a snapshot — export and save it.
622
+ // Never for encrypted DBs: an exported snapshot is decrypted plaintext.
549
623
  try {
550
624
  const bytes = db.exportSnapshot();
551
625
  await idbSaveSnapshot(activeDbName, bytes);
@@ -557,8 +631,15 @@ async function doInit(dbName, configJson) {
557
631
  const resolve = pendingSnapshotResolve;
558
632
  pendingSnapshotResolve = null;
559
633
  resolve();
560
- } else if (e.data?.type === 'taladb:secondary-write' && typeof e.data.token === 'string' && e.data.token.length > 0 && !idbFallback && db) {
561
- // Primary (OPFS) tab: a secondary tab made a write — merge it in via LWW.
634
+ } else if (e.data === 'taladb:snapshot-ready' && idbFallback) {
635
+ snapshotDirty = true;
636
+ } else if (e.data?.type === 'taladb:secondary-write' && typeof e.data.token === 'string' && e.data.token.length > 0 && db && !encrypted) {
637
+ // `!encrypted`: encrypted databases are single-tab, so there are no
638
+ // legitimate secondary-tab writes — accepting them would let any
639
+ // same-origin script inject documents into an encrypted DB without
640
+ // knowing the passphrase.
641
+ // Merge peer writes in every mode. This also makes multiple tabs
642
+ // converge when OPFS is entirely unavailable and every tab uses IDB.
562
643
  // The token requirement rejects unauthenticated injection attempts that
563
644
  // omit the SESSION_TOKEN field (not a guarantee against same-origin attackers
564
645
  // who observe the token, but defence-in-depth against naive injection).
@@ -583,17 +664,27 @@ async function doInit(dbName, configJson) {
583
664
  if (configJson) return WorkerDB.openWithConfigAndSnapshot(snapshot, configJson);
584
665
  return WorkerDB.openWithSnapshot(snapshot);
585
666
  }
667
+ // Salt for key derivation, loaded/created in an OPFS sidecar (encrypted mode
668
+ // only). Passed to the WASM open so the derived key is stable across opens.
669
+ let salt = null;
586
670
  function openWithOpfs(syncHandle) {
587
- if (configJson) return WorkerDB.openWithConfigAndOpfs(syncHandle, configJson);
588
- return WorkerDB.openWithOpfs(syncHandle);
671
+ // openWithConfigAndOpfs(handle, configJson, passphrase?, salt?)
672
+ return WorkerDB.openWithConfigAndOpfs(syncHandle, configJson ?? null, passphrase, salt);
589
673
  }
590
674
 
591
675
  const opfsAvailable = await checkOpfs();
592
676
  if (!opfsAvailable) {
677
+ if (encrypted) {
678
+ throw new Error(
679
+ 'TalaDB encryption requires OPFS, which is unavailable in this browser context. ' +
680
+ 'Refusing to open — the in-memory/IndexedDB fallback cannot encrypt at rest.'
681
+ );
682
+ }
593
683
  warn('OPFS unavailable — falling back to IndexedDB-backed in-memory');
594
684
  const snapshot = await idbLoadSnapshot(dbName);
595
685
  db = openWithSnapshot(snapshot);
596
686
  idbFallback = true;
687
+ snapshotWriter = true;
597
688
  if (snapshot) {
598
689
  log(`Restored from IndexedDB snapshot (${snapshot.byteLength} bytes)`);
599
690
  } else {
@@ -606,11 +697,26 @@ async function doInit(dbName, configJson) {
606
697
  const fileName = `taladb_${dbName.replaceAll(/[/\\:]/g, '_')}.redb`;
607
698
  const fileHandle = await root.getFileHandle(fileName, { create: true });
608
699
 
700
+ // Encrypted mode: the 16-byte key-derivation salt lives in an OPFS sidecar
701
+ // file next to the DB. It is loaded/created only AFTER this tab has won the
702
+ // exclusive lock (or determined no locking applies), so two tabs racing to
703
+ // open never collide on the salt file's exclusive access handle.
704
+
609
705
  if (!('locks' in navigator)) {
610
706
  // Web Locks not available — open directly (single-tab safe only).
611
707
  warn('Web Locks unavailable — multi-tab write safety disabled');
708
+ if (encrypted) salt = await loadOrCreateSalt(root, `${fileName}.salt`);
612
709
  const syncHandle = await fileHandle.createSyncAccessHandle();
613
- db = openWithOpfs(syncHandle);
710
+ try {
711
+ db = openWithOpfs(syncHandle);
712
+ } catch (e) {
713
+ // A failed open (e.g. wrong passphrase) must release the exclusive OPFS
714
+ // access handle, or every retry fails with "Access Handles cannot be
715
+ // created" until the page reloads.
716
+ try { syncHandle.close(); } catch { /* best-effort */ }
717
+ throw e;
718
+ }
719
+ snapshotWriter = !encrypted; // encrypted DBs never write a plaintext IDB snapshot
614
720
  log(`Opened "${fileName}" via OPFS`);
615
721
  return;
616
722
  }
@@ -625,7 +731,18 @@ async function doInit(dbName, configJson) {
625
731
  await new Promise((resolve, reject) => {
626
732
  navigator.locks.request(lockName, { ifAvailable: true }, async (lock) => {
627
733
  if (lock === null) {
628
- // Lock is held by another tab — use IDB snapshot so this tab loads immediately.
734
+ // Lock is held by another tab.
735
+ if (encrypted) {
736
+ // The IDB-snapshot fallback stores decrypted plaintext, so it's
737
+ // refused for encrypted databases — they're single-tab. Reject
738
+ // rather than downgrade.
739
+ reject(new Error(
740
+ 'This encrypted TalaDB database is already open in another tab. ' +
741
+ 'Encrypted browser databases are single-tab (the multi-tab fallback would store plaintext).'
742
+ ));
743
+ return;
744
+ }
745
+ // Unencrypted: use IDB snapshot so this tab loads immediately.
629
746
  warn('OPFS lock held by another tab — falling back to IndexedDB snapshot (live-sync via BroadcastChannel)');
630
747
  let snapshot = await idbLoadSnapshot(dbName);
631
748
 
@@ -642,6 +759,7 @@ async function doInit(dbName, configJson) {
642
759
 
643
760
  db = openWithSnapshot(snapshot ?? null);
644
761
  idbFallback = true;
762
+ snapshotWriter = false;
645
763
  if (snapshot) {
646
764
  log(`Restored from IDB snapshot (${snapshot.byteLength} bytes)`);
647
765
  } else {
@@ -652,9 +770,12 @@ async function doInit(dbName, configJson) {
652
770
  }
653
771
 
654
772
  // Acquired the lock — use OPFS.
773
+ let syncHandle = null;
655
774
  try {
656
- const syncHandle = await fileHandle.createSyncAccessHandle();
775
+ if (encrypted) salt = await loadOrCreateSalt(root, `${fileName}.salt`);
776
+ syncHandle = await fileHandle.createSyncAccessHandle();
657
777
  db = openWithOpfs(syncHandle);
778
+ snapshotWriter = !encrypted; // encrypted DBs never write a plaintext IDB snapshot
658
779
  log(`Opened "${fileName}" via OPFS (Web Locks)`);
659
780
  resolve(); // signal doInit complete — caller can proceed
660
781
 
@@ -665,6 +786,13 @@ async function doInit(dbName, configJson) {
665
786
  syncHandle.close();
666
787
  db = null;
667
788
  } catch (e) {
789
+ // A failed open (e.g. wrong passphrase) must release the exclusive
790
+ // OPFS access handle, or every retry fails with "Access Handles
791
+ // cannot be created" until the page reloads. Returning from this
792
+ // callback also releases the Web Lock.
793
+ if (syncHandle) {
794
+ try { syncHandle.close(); } catch { /* best-effort */ }
795
+ }
668
796
  reject(e);
669
797
  }
670
798
  });