@taladb/web 0.5.0 → 0.6.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.5.0",
3
+ "version": "0.6.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)",
8
- "version": "0.5.0",
8
+ "version": "0.6.1",
9
9
  "license": "MIT",
10
10
  "files": [
11
11
  "taladb_web_bg.wasm",
@@ -119,6 +119,20 @@ export class WorkerDB {
119
119
  private constructor();
120
120
  free(): void;
121
121
  [Symbol.dispose](): void;
122
+ /**
123
+ * Remove tombstones older than `before_ms` from the given collection.
124
+ *
125
+ * Call periodically (e.g. on app startup) after your sync retention window
126
+ * has elapsed so deleted document IDs no longer accumulate indefinitely.
127
+ * Returns the number of tombstones removed.
128
+ *
129
+ * ```js
130
+ * // Prune tombstones older than 30 days
131
+ * const cutoff = Date.now() - 30 * 24 * 60 * 60 * 1000;
132
+ * const pruned = db.compactTombstones('users', cutoff);
133
+ * ```
134
+ */
135
+ compactTombstones(collection: string, before_ms: number): number;
122
136
  /**
123
137
  * Count matching documents.
124
138
  */
@@ -148,6 +162,18 @@ export class WorkerDB {
148
162
  * Drop a vector index (and its HNSW graph if present).
149
163
  */
150
164
  dropVectorIndex(collection: string, field: string): void;
165
+ /**
166
+ * Export a changeset for the given collections since `since_ms`.
167
+ *
168
+ * Returns a JSON string representing `Vec<Change>` that can be sent
169
+ * to a remote peer via fetch, WebSocket, or SSE.
170
+ *
171
+ * ```js
172
+ * const json = db.exportChangeset(JSON.stringify(['users', 'posts']), 0);
173
+ * await fetch('/sync', { method: 'POST', body: json });
174
+ * ```
175
+ */
176
+ exportChangeset(collections_json: string, since_ms: number): string;
151
177
  /**
152
178
  * Serialize the entire in-memory database to bytes for persistence.
153
179
  *
@@ -167,6 +193,19 @@ export class WorkerDB {
167
193
  * Find one document. Returns a JSON object or `"null"`.
168
194
  */
169
195
  findOne(collection: string, filter_json: string): string;
196
+ /**
197
+ * Import a remote changeset and merge it into the local database using
198
+ * Last-Write-Wins conflict resolution.
199
+ *
200
+ * Returns the number of documents actually changed.
201
+ *
202
+ * ```js
203
+ * const resp = await fetch('/sync?since=' + lastSync);
204
+ * const applied = db.importChangeset(await resp.text());
205
+ * if (applied > 0) { /* refresh UI */ }
206
+ * ```
207
+ */
208
+ importChangeset(changeset_json: string): number;
170
209
  /**
171
210
  * Insert a document. Returns the new ULID as a string.
172
211
  */
@@ -175,6 +214,11 @@ export class WorkerDB {
175
214
  * Insert many documents. Returns a JSON array of ULID strings.
176
215
  */
177
216
  insertMany(collection: string, docs_json: string): string;
217
+ /**
218
+ * Returns a JSON array of all collection names in the database.
219
+ * Used by the Worker to build the collections list for exportChangeset.
220
+ */
221
+ listCollections(): string;
178
222
  /**
179
223
  * Returns a JSON string `{ btree: string[], fts: string[], vector: string[] }`
180
224
  * listing all indexes on the given collection.
@@ -184,6 +228,27 @@ export class WorkerDB {
184
228
  * Open an in-memory database (for tests and OPFS-unavailable fallback).
185
229
  */
186
230
  static openInMemory(): WorkerDB;
231
+ /**
232
+ * Open a database backed by OPFS with HTTP push sync config.
233
+ *
234
+ * `config_json` — JSON-serialised `TalaDbConfig`, or `null` to open without sync.
235
+ *
236
+ * ```js
237
+ * const handle = await file_handle.createSyncAccessHandle();
238
+ * const db = WorkerDB.openWithConfigAndOpfs(handle, JSON.stringify(config));
239
+ * ```
240
+ */
241
+ static openWithConfigAndOpfs(sync_handle: FileSystemSyncAccessHandle, config_json?: string | null): WorkerDB;
242
+ /**
243
+ * Open a database from an optional snapshot with HTTP push sync config.
244
+ *
245
+ * `config_json` — JSON-serialised `TalaDbConfig`, or `null` to open without sync.
246
+ *
247
+ * ```js
248
+ * const db = WorkerDB.openWithConfigAndSnapshot(snapshot, JSON.stringify(config));
249
+ * ```
250
+ */
251
+ static openWithConfigAndSnapshot(data?: Uint8Array | null, config_json?: string | null): WorkerDB;
187
252
  /**
188
253
  * Open a database backed by an OPFS `FileSystemSyncAccessHandle`.
189
254
  *
@@ -277,6 +342,7 @@ export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembl
277
342
  export interface InitOutput {
278
343
  readonly memory: WebAssembly.Memory;
279
344
  readonly __wbg_workerdb_free: (a: number, b: number) => void;
345
+ readonly workerdb_compactTombstones: (a: number, b: number, c: number, d: number) => [number, number, number];
280
346
  readonly workerdb_count: (a: number, b: number, c: number, d: number, e: number) => [number, number, number];
281
347
  readonly workerdb_createFtsIndex: (a: number, b: number, c: number, d: number, e: number) => [number, number];
282
348
  readonly workerdb_createIndex: (a: number, b: number, c: number, d: number, e: number) => [number, number];
@@ -286,14 +352,19 @@ export interface InitOutput {
286
352
  readonly workerdb_dropFtsIndex: (a: number, b: number, c: number, d: number, e: number) => [number, number];
287
353
  readonly workerdb_dropIndex: (a: number, b: number, c: number, d: number, e: number) => [number, number];
288
354
  readonly workerdb_dropVectorIndex: (a: number, b: number, c: number, d: number, e: number) => [number, number];
355
+ readonly workerdb_exportChangeset: (a: number, b: number, c: number, d: number) => [number, number, number, number];
289
356
  readonly workerdb_exportSnapshot: (a: number) => [number, number, number, number];
290
357
  readonly workerdb_find: (a: number, b: number, c: number, d: number, e: number) => [number, number, number, number];
291
358
  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];
292
359
  readonly workerdb_findOne: (a: number, b: number, c: number, d: number, e: number) => [number, number, number, number];
360
+ readonly workerdb_importChangeset: (a: number, b: number, c: number) => [number, number, number];
293
361
  readonly workerdb_insert: (a: number, b: number, c: number, d: number, e: number) => [number, number, number, number];
294
362
  readonly workerdb_insertMany: (a: number, b: number, c: number, d: number, e: number) => [number, number, number, number];
363
+ readonly workerdb_listCollections: (a: number) => [number, number, number, number];
295
364
  readonly workerdb_listIndexes: (a: number, b: number, c: number) => [number, number, number, number];
296
365
  readonly workerdb_openInMemory: () => [number, number, number];
366
+ readonly workerdb_openWithConfigAndOpfs: (a: any, b: number, c: number) => [number, number, number];
367
+ readonly workerdb_openWithConfigAndSnapshot: (a: number, b: number, c: number, d: number) => [number, number, number];
297
368
  readonly workerdb_openWithOpfs: (a: any) => [number, number, number];
298
369
  readonly workerdb_openWithSnapshot: (a: number, b: number) => [number, number, number];
299
370
  readonly workerdb_updateMany: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => [number, number, number];
@@ -321,18 +392,20 @@ export interface InitOutput {
321
392
  readonly taladbwasm_openInMemory: () => [number, number, number];
322
393
  readonly taladbwasm_openWithSnapshot: (a: number, b: number) => [number, number, number];
323
394
  readonly init: () => void;
395
+ readonly idb_load_snapshot: (a: number, b: number) => any;
396
+ readonly idb_save_snapshot: (a: number, b: number, c: number, d: number) => any;
324
397
  readonly is_opfs_available: () => any;
325
398
  readonly opfs_delete_snapshot: (a: number, b: number) => any;
326
399
  readonly opfs_flush_snapshot: (a: number, b: number, c: number, d: number) => any;
327
400
  readonly opfs_load_snapshot: (a: number, b: number) => any;
328
401
  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;
331
- readonly wasm_bindgen__closure__destroy__h1411c0b1433bd547: (a: number, b: number) => void;
332
- readonly wasm_bindgen__closure__destroy__hc294c7484c23aa40: (a: number, b: number) => void;
333
- readonly wasm_bindgen__convert__closures_____invoke__h4bd31e7387ab665d: (a: number, b: number, c: any) => [number, number];
334
- readonly wasm_bindgen__convert__closures_____invoke__h675f15260128efbc: (a: number, b: number, c: any, d: any) => void;
335
- readonly wasm_bindgen__convert__closures_____invoke__h6096c0f3a625f394: (a: number, b: number, c: any) => void;
402
+ readonly wasm_bindgen__closure__destroy__h72641f1e1c5255af: (a: number, b: number) => void;
403
+ readonly wasm_bindgen__closure__destroy__h478053cb26acc43f: (a: number, b: number) => void;
404
+ readonly wasm_bindgen__closure__destroy__h1febc128ce23ccd1: (a: number, b: number) => void;
405
+ readonly wasm_bindgen__convert__closures_____invoke__h3022e61d849d0b12: (a: number, b: number, c: any) => [number, number];
406
+ readonly wasm_bindgen__convert__closures_____invoke__h470c93d8a24e0015: (a: number, b: number, c: any, d: any) => void;
407
+ readonly wasm_bindgen__convert__closures_____invoke__h296c8d2fd599fbcf: (a: number, b: number, c: any) => void;
408
+ readonly wasm_bindgen__convert__closures_____invoke__h325a87e086f6f78c: (a: number, b: number) => void;
336
409
  readonly __wbindgen_malloc: (a: number, b: number) => number;
337
410
  readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
338
411
  readonly __wbindgen_exn_store: (a: number) => void;
package/pkg/taladb_web.js CHANGED
@@ -344,6 +344,31 @@ export class WorkerDB {
344
344
  const ptr = this.__destroy_into_raw();
345
345
  wasm.__wbg_workerdb_free(ptr, 0);
346
346
  }
347
+ /**
348
+ * Remove tombstones older than `before_ms` from the given collection.
349
+ *
350
+ * Call periodically (e.g. on app startup) after your sync retention window
351
+ * has elapsed so deleted document IDs no longer accumulate indefinitely.
352
+ * Returns the number of tombstones removed.
353
+ *
354
+ * ```js
355
+ * // Prune tombstones older than 30 days
356
+ * const cutoff = Date.now() - 30 * 24 * 60 * 60 * 1000;
357
+ * const pruned = db.compactTombstones('users', cutoff);
358
+ * ```
359
+ * @param {string} collection
360
+ * @param {number} before_ms
361
+ * @returns {number}
362
+ */
363
+ compactTombstones(collection, before_ms) {
364
+ const ptr0 = passStringToWasm0(collection, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
365
+ const len0 = WASM_VECTOR_LEN;
366
+ const ret = wasm.workerdb_compactTombstones(this.__wbg_ptr, ptr0, len0, before_ms);
367
+ if (ret[2]) {
368
+ throw takeFromExternrefTable0(ret[1]);
369
+ }
370
+ return ret[0] >>> 0;
371
+ }
347
372
  /**
348
373
  * Count matching documents.
349
374
  * @param {string} collection
@@ -495,6 +520,40 @@ export class WorkerDB {
495
520
  throw takeFromExternrefTable0(ret[0]);
496
521
  }
497
522
  }
523
+ /**
524
+ * Export a changeset for the given collections since `since_ms`.
525
+ *
526
+ * Returns a JSON string representing `Vec<Change>` that can be sent
527
+ * to a remote peer via fetch, WebSocket, or SSE.
528
+ *
529
+ * ```js
530
+ * const json = db.exportChangeset(JSON.stringify(['users', 'posts']), 0);
531
+ * await fetch('/sync', { method: 'POST', body: json });
532
+ * ```
533
+ * @param {string} collections_json
534
+ * @param {number} since_ms
535
+ * @returns {string}
536
+ */
537
+ exportChangeset(collections_json, since_ms) {
538
+ let deferred3_0;
539
+ let deferred3_1;
540
+ try {
541
+ const ptr0 = passStringToWasm0(collections_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
542
+ const len0 = WASM_VECTOR_LEN;
543
+ const ret = wasm.workerdb_exportChangeset(this.__wbg_ptr, ptr0, len0, since_ms);
544
+ var ptr2 = ret[0];
545
+ var len2 = ret[1];
546
+ if (ret[3]) {
547
+ ptr2 = 0; len2 = 0;
548
+ throw takeFromExternrefTable0(ret[2]);
549
+ }
550
+ deferred3_0 = ptr2;
551
+ deferred3_1 = len2;
552
+ return getStringFromWasm0(ptr2, len2);
553
+ } finally {
554
+ wasm.__wbindgen_free(deferred3_0, deferred3_1, 1);
555
+ }
556
+ }
498
557
  /**
499
558
  * Serialize the entire in-memory database to bytes for persistence.
500
559
  *
@@ -602,6 +661,29 @@ export class WorkerDB {
602
661
  wasm.__wbindgen_free(deferred4_0, deferred4_1, 1);
603
662
  }
604
663
  }
664
+ /**
665
+ * Import a remote changeset and merge it into the local database using
666
+ * Last-Write-Wins conflict resolution.
667
+ *
668
+ * Returns the number of documents actually changed.
669
+ *
670
+ * ```js
671
+ * const resp = await fetch('/sync?since=' + lastSync);
672
+ * const applied = db.importChangeset(await resp.text());
673
+ * if (applied > 0) { /* refresh UI */ }
674
+ * ```
675
+ * @param {string} changeset_json
676
+ * @returns {number}
677
+ */
678
+ importChangeset(changeset_json) {
679
+ const ptr0 = passStringToWasm0(changeset_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
680
+ const len0 = WASM_VECTOR_LEN;
681
+ const ret = wasm.workerdb_importChangeset(this.__wbg_ptr, ptr0, len0);
682
+ if (ret[2]) {
683
+ throw takeFromExternrefTable0(ret[1]);
684
+ }
685
+ return ret[0] >>> 0;
686
+ }
605
687
  /**
606
688
  * Insert a document. Returns the new ULID as a string.
607
689
  * @param {string} collection
@@ -658,6 +740,29 @@ export class WorkerDB {
658
740
  wasm.__wbindgen_free(deferred4_0, deferred4_1, 1);
659
741
  }
660
742
  }
743
+ /**
744
+ * Returns a JSON array of all collection names in the database.
745
+ * Used by the Worker to build the collections list for exportChangeset.
746
+ * @returns {string}
747
+ */
748
+ listCollections() {
749
+ let deferred2_0;
750
+ let deferred2_1;
751
+ try {
752
+ const ret = wasm.workerdb_listCollections(this.__wbg_ptr);
753
+ var ptr1 = ret[0];
754
+ var len1 = ret[1];
755
+ if (ret[3]) {
756
+ ptr1 = 0; len1 = 0;
757
+ throw takeFromExternrefTable0(ret[2]);
758
+ }
759
+ deferred2_0 = ptr1;
760
+ deferred2_1 = len1;
761
+ return getStringFromWasm0(ptr1, len1);
762
+ } finally {
763
+ wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
764
+ }
765
+ }
661
766
  /**
662
767
  * Returns a JSON string `{ btree: string[], fts: string[], vector: string[] }`
663
768
  * listing all indexes on the given collection.
@@ -695,6 +800,51 @@ export class WorkerDB {
695
800
  }
696
801
  return WorkerDB.__wrap(ret[0]);
697
802
  }
803
+ /**
804
+ * Open a database backed by OPFS with HTTP push sync config.
805
+ *
806
+ * `config_json` — JSON-serialised `TalaDbConfig`, or `null` to open without sync.
807
+ *
808
+ * ```js
809
+ * const handle = await file_handle.createSyncAccessHandle();
810
+ * const db = WorkerDB.openWithConfigAndOpfs(handle, JSON.stringify(config));
811
+ * ```
812
+ * @param {FileSystemSyncAccessHandle} sync_handle
813
+ * @param {string | null} [config_json]
814
+ * @returns {WorkerDB}
815
+ */
816
+ static openWithConfigAndOpfs(sync_handle, config_json) {
817
+ var ptr0 = isLikeNone(config_json) ? 0 : passStringToWasm0(config_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
818
+ var len0 = WASM_VECTOR_LEN;
819
+ const ret = wasm.workerdb_openWithConfigAndOpfs(sync_handle, ptr0, len0);
820
+ if (ret[2]) {
821
+ throw takeFromExternrefTable0(ret[1]);
822
+ }
823
+ return WorkerDB.__wrap(ret[0]);
824
+ }
825
+ /**
826
+ * Open a database from an optional snapshot with HTTP push sync config.
827
+ *
828
+ * `config_json` — JSON-serialised `TalaDbConfig`, or `null` to open without sync.
829
+ *
830
+ * ```js
831
+ * const db = WorkerDB.openWithConfigAndSnapshot(snapshot, JSON.stringify(config));
832
+ * ```
833
+ * @param {Uint8Array | null} [data]
834
+ * @param {string | null} [config_json]
835
+ * @returns {WorkerDB}
836
+ */
837
+ static openWithConfigAndSnapshot(data, config_json) {
838
+ var ptr0 = isLikeNone(data) ? 0 : passArray8ToWasm0(data, wasm.__wbindgen_malloc);
839
+ var len0 = WASM_VECTOR_LEN;
840
+ var ptr1 = isLikeNone(config_json) ? 0 : passStringToWasm0(config_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
841
+ var len1 = WASM_VECTOR_LEN;
842
+ const ret = wasm.workerdb_openWithConfigAndSnapshot(ptr0, len0, ptr1, len1);
843
+ if (ret[2]) {
844
+ throw takeFromExternrefTable0(ret[1]);
845
+ }
846
+ return WorkerDB.__wrap(ret[0]);
847
+ }
698
848
  /**
699
849
  * Open a database backed by an OPFS `FileSystemSyncAccessHandle`.
700
850
  *
@@ -988,10 +1138,23 @@ function __wbg_get_imports() {
988
1138
  __wbg__wbg_cb_unref_6b5b6b8576d35cb1: function(arg0) {
989
1139
  arg0._wbg_cb_unref();
990
1140
  },
1141
+ __wbg_abort_5ef96933660780b7: function(arg0) {
1142
+ arg0.abort();
1143
+ },
1144
+ __wbg_abort_6479c2d794ebf2ee: function(arg0, arg1) {
1145
+ arg0.abort(arg1);
1146
+ },
1147
+ __wbg_append_608dfb635ee8998f: function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) {
1148
+ arg0.append(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4));
1149
+ }, arguments); },
991
1150
  __wbg_apply_ac9afb97ca32f169: function() { return handleError(function (arg0, arg1, arg2) {
992
1151
  const ret = arg0.apply(arg1, arg2);
993
1152
  return ret;
994
1153
  }, arguments); },
1154
+ __wbg_apply_d7728efbea08f95e: function() { return handleError(function (arg0, arg1, arg2) {
1155
+ const ret = Reflect.apply(arg0, arg1, arg2);
1156
+ return ret;
1157
+ }, arguments); },
995
1158
  __wbg_arrayBuffer_7ff5e58aa85a64f7: function(arg0) {
996
1159
  const ret = arg0.arrayBuffer();
997
1160
  return ret;
@@ -1008,6 +1171,10 @@ function __wbg_get_imports() {
1008
1171
  const ret = arg0.call(arg1);
1009
1172
  return ret;
1010
1173
  }, arguments); },
1174
+ __wbg_clearTimeout_6b8d9a38b9263d65: function(arg0) {
1175
+ const ret = clearTimeout(arg0);
1176
+ return ret;
1177
+ },
1011
1178
  __wbg_close_8b24f0978d9a4917: function(arg0) {
1012
1179
  const ret = arg0.close();
1013
1180
  return ret;
@@ -1035,6 +1202,14 @@ function __wbg_get_imports() {
1035
1202
  wasm.__wbindgen_free(deferred0_0, deferred0_1, 1);
1036
1203
  }
1037
1204
  },
1205
+ __wbg_fetch_5550a88cf343aaa9: function(arg0, arg1) {
1206
+ const ret = arg0.fetch(arg1);
1207
+ return ret;
1208
+ },
1209
+ __wbg_fetch_9dad4fe911207b37: function(arg0) {
1210
+ const ret = fetch(arg0);
1211
+ return ret;
1212
+ },
1038
1213
  __wbg_flush_1eca046e0ff7c399: function() { return handleError(function (arg0) {
1039
1214
  arg0.flush();
1040
1215
  }, arguments); },
@@ -1077,6 +1252,14 @@ function __wbg_get_imports() {
1077
1252
  const ret = arg0[arg1 >>> 0];
1078
1253
  return ret;
1079
1254
  },
1255
+ __wbg_has_926ef2ff40b308cf: function() { return handleError(function (arg0, arg1) {
1256
+ const ret = Reflect.has(arg0, arg1);
1257
+ return ret;
1258
+ }, arguments); },
1259
+ __wbg_headers_eb2234545f9ff993: function(arg0) {
1260
+ const ret = arg0.headers;
1261
+ return ret;
1262
+ },
1080
1263
  __wbg_instanceof_ArrayBuffer_101e2bf31071a9f6: function(arg0) {
1081
1264
  let result;
1082
1265
  try {
@@ -1157,6 +1340,16 @@ function __wbg_get_imports() {
1157
1340
  const ret = result;
1158
1341
  return ret;
1159
1342
  },
1343
+ __wbg_instanceof_Response_9b4d9fd451e051b1: function(arg0) {
1344
+ let result;
1345
+ try {
1346
+ result = arg0 instanceof Response;
1347
+ } catch (_) {
1348
+ result = false;
1349
+ }
1350
+ const ret = result;
1351
+ return ret;
1352
+ },
1160
1353
  __wbg_instanceof_Uint8Array_740438561a5b956d: function(arg0) {
1161
1354
  let result;
1162
1355
  try {
@@ -1201,6 +1394,10 @@ function __wbg_get_imports() {
1201
1394
  const ret = arg0.navigator;
1202
1395
  return ret;
1203
1396
  },
1397
+ __wbg_new_0837727332ac86ba: function() { return handleError(function () {
1398
+ const ret = new Headers();
1399
+ return ret;
1400
+ }, arguments); },
1204
1401
  __wbg_new_227d7c05414eb861: function() {
1205
1402
  const ret = new Error();
1206
1403
  return ret;
@@ -1221,6 +1418,10 @@ function __wbg_get_imports() {
1221
1418
  const ret = new Object();
1222
1419
  return ret;
1223
1420
  },
1421
+ __wbg_new_c518c60af666645b: function() { return handleError(function () {
1422
+ const ret = new AbortController();
1423
+ return ret;
1424
+ }, arguments); },
1224
1425
  __wbg_new_d098e265629cd10f: function(arg0, arg1) {
1225
1426
  try {
1226
1427
  var state0 = {a: arg0, b: arg1};
@@ -1228,7 +1429,7 @@ function __wbg_get_imports() {
1228
1429
  const a = state0.a;
1229
1430
  state0.a = 0;
1230
1431
  try {
1231
- return wasm_bindgen__convert__closures_____invoke__h675f15260128efbc(a, state0.b, arg0, arg1);
1432
+ return wasm_bindgen__convert__closures_____invoke__h470c93d8a24e0015(a, state0.b, arg0, arg1);
1232
1433
  } finally {
1233
1434
  state0.a = a;
1234
1435
  }
@@ -1250,7 +1451,7 @@ function __wbg_get_imports() {
1250
1451
  const a = state0.a;
1251
1452
  state0.a = 0;
1252
1453
  try {
1253
- return wasm_bindgen__convert__closures_____invoke__h675f15260128efbc(a, state0.b, arg0, arg1);
1454
+ return wasm_bindgen__convert__closures_____invoke__h470c93d8a24e0015(a, state0.b, arg0, arg1);
1254
1455
  } finally {
1255
1456
  state0.a = a;
1256
1457
  }
@@ -1265,6 +1466,10 @@ function __wbg_get_imports() {
1265
1466
  const ret = new Uint8Array(arg0 >>> 0);
1266
1467
  return ret;
1267
1468
  },
1469
+ __wbg_new_with_str_and_init_b4b54d1a819bc724: function() { return handleError(function (arg0, arg1, arg2) {
1470
+ const ret = new Request(getStringFromWasm0(arg0, arg1), arg2);
1471
+ return ret;
1472
+ }, arguments); },
1268
1473
  __wbg_next_11b99ee6237339e3: function() { return handleError(function (arg0) {
1269
1474
  const ret = arg0.next();
1270
1475
  return ret;
@@ -1277,6 +1482,10 @@ function __wbg_get_imports() {
1277
1482
  const ret = Date.now();
1278
1483
  return ret;
1279
1484
  },
1485
+ __wbg_of_d6376e3774c51f89: function(arg0, arg1) {
1486
+ const ret = Array.of(arg0, arg1);
1487
+ return ret;
1488
+ },
1280
1489
  __wbg_prototypesetcall_d62e5099504357e6: function(arg0, arg1, arg2) {
1281
1490
  Uint8Array.prototype.set.call(getArrayU8FromWasm0(arg0, arg1), arg2);
1282
1491
  },
@@ -1299,6 +1508,10 @@ function __wbg_get_imports() {
1299
1508
  const ret = Promise.resolve(arg0);
1300
1509
  return ret;
1301
1510
  },
1511
+ __wbg_setTimeout_f757f00851f76c42: function(arg0, arg1) {
1512
+ const ret = setTimeout(arg0, arg1);
1513
+ return ret;
1514
+ },
1302
1515
  __wbg_set_282384002438957f: function(arg0, arg1, arg2) {
1303
1516
  arg0[arg1 >>> 0] = arg2;
1304
1517
  },
@@ -1313,9 +1526,34 @@ function __wbg_get_imports() {
1313
1526
  const ret = arg0.set(arg1, arg2);
1314
1527
  return ret;
1315
1528
  },
1529
+ __wbg_set_body_a3d856b097dfda04: function(arg0, arg1) {
1530
+ arg0.body = arg1;
1531
+ },
1532
+ __wbg_set_cache_ec7e430c6056ebda: function(arg0, arg1) {
1533
+ arg0.cache = __wbindgen_enum_RequestCache[arg1];
1534
+ },
1316
1535
  __wbg_set_create_ef897736206a6f05: function(arg0, arg1) {
1317
1536
  arg0.create = arg1 !== 0;
1318
1537
  },
1538
+ __wbg_set_credentials_ed63183445882c65: function(arg0, arg1) {
1539
+ arg0.credentials = __wbindgen_enum_RequestCredentials[arg1];
1540
+ },
1541
+ __wbg_set_headers_3c8fecc693b75327: function(arg0, arg1) {
1542
+ arg0.headers = arg1;
1543
+ },
1544
+ __wbg_set_method_8c015e8bcafd7be1: function(arg0, arg1, arg2) {
1545
+ arg0.method = getStringFromWasm0(arg1, arg2);
1546
+ },
1547
+ __wbg_set_mode_5a87f2c809cf37c2: function(arg0, arg1) {
1548
+ arg0.mode = __wbindgen_enum_RequestMode[arg1];
1549
+ },
1550
+ __wbg_set_signal_0cebecb698f25d21: function(arg0, arg1) {
1551
+ arg0.signal = arg1;
1552
+ },
1553
+ __wbg_signal_166e1da31adcac18: function(arg0) {
1554
+ const ret = arg0.signal;
1555
+ return ret;
1556
+ },
1319
1557
  __wbg_stack_3b0d974bbf31e44f: function(arg0, arg1) {
1320
1558
  const ret = arg1.stack;
1321
1559
  const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
@@ -1339,10 +1577,18 @@ function __wbg_get_imports() {
1339
1577
  const ret = typeof window === 'undefined' ? null : window;
1340
1578
  return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
1341
1579
  },
1580
+ __wbg_status_318629ab93a22955: function(arg0) {
1581
+ const ret = arg0.status;
1582
+ return ret;
1583
+ },
1342
1584
  __wbg_storage_63fa6fa9455d2153: function(arg0) {
1343
1585
  const ret = arg0.storage;
1344
1586
  return ret;
1345
1587
  },
1588
+ __wbg_stringify_5ae93966a84901ac: function() { return handleError(function (arg0) {
1589
+ const ret = JSON.stringify(arg0);
1590
+ return ret;
1591
+ }, arguments); },
1346
1592
  __wbg_then_098abe61755d12f6: function(arg0, arg1) {
1347
1593
  const ret = arg0.then(arg1);
1348
1594
  return ret;
@@ -1354,41 +1600,53 @@ function __wbg_get_imports() {
1354
1600
  __wbg_truncate_af8ac1613ab66393: function() { return handleError(function (arg0, arg1) {
1355
1601
  arg0.truncate(arg1);
1356
1602
  }, arguments); },
1603
+ __wbg_url_7fefc1820fba4e0c: function(arg0, arg1) {
1604
+ const ret = arg1.url;
1605
+ const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
1606
+ const len1 = WASM_VECTOR_LEN;
1607
+ getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
1608
+ getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
1609
+ },
1357
1610
  __wbg_value_21fc78aab0322612: function(arg0) {
1358
1611
  const ret = arg0.value;
1359
1612
  return ret;
1360
1613
  },
1361
1614
  __wbindgen_cast_0000000000000001: function(arg0, arg1) {
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`.
1363
- const ret = makeMutClosure(arg0, arg1, wasm.wasm_bindgen__closure__destroy__h1411c0b1433bd547, wasm_bindgen__convert__closures_____invoke__h4bd31e7387ab665d);
1615
+ // Cast intrinsic for `Closure(Closure { dtor_idx: 102, function: Function { arguments: [Externref], shim_idx: 103, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
1616
+ const ret = makeMutClosure(arg0, arg1, wasm.wasm_bindgen__closure__destroy__h72641f1e1c5255af, wasm_bindgen__convert__closures_____invoke__h296c8d2fd599fbcf);
1364
1617
  return ret;
1365
1618
  },
1366
1619
  __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__hc294c7484c23aa40, wasm_bindgen__convert__closures_____invoke__h6096c0f3a625f394);
1620
+ // Cast intrinsic for `Closure(Closure { dtor_idx: 181, function: Function { arguments: [], shim_idx: 182, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
1621
+ const ret = makeMutClosure(arg0, arg1, wasm.wasm_bindgen__closure__destroy__h478053cb26acc43f, wasm_bindgen__convert__closures_____invoke__h325a87e086f6f78c);
1622
+ return ret;
1623
+ },
1624
+ __wbindgen_cast_0000000000000003: function(arg0, arg1) {
1625
+ // Cast intrinsic for `Closure(Closure { dtor_idx: 589, function: Function { arguments: [Externref], shim_idx: 590, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
1626
+ const ret = makeMutClosure(arg0, arg1, wasm.wasm_bindgen__closure__destroy__h1febc128ce23ccd1, wasm_bindgen__convert__closures_____invoke__h3022e61d849d0b12);
1369
1627
  return ret;
1370
1628
  },
1371
- __wbindgen_cast_0000000000000003: function(arg0) {
1629
+ __wbindgen_cast_0000000000000004: function(arg0) {
1372
1630
  // Cast intrinsic for `F64 -> Externref`.
1373
1631
  const ret = arg0;
1374
1632
  return ret;
1375
1633
  },
1376
- __wbindgen_cast_0000000000000004: function(arg0) {
1634
+ __wbindgen_cast_0000000000000005: function(arg0) {
1377
1635
  // Cast intrinsic for `I64 -> Externref`.
1378
1636
  const ret = arg0;
1379
1637
  return ret;
1380
1638
  },
1381
- __wbindgen_cast_0000000000000005: function(arg0, arg1) {
1639
+ __wbindgen_cast_0000000000000006: function(arg0, arg1) {
1382
1640
  // Cast intrinsic for `Ref(String) -> Externref`.
1383
1641
  const ret = getStringFromWasm0(arg0, arg1);
1384
1642
  return ret;
1385
1643
  },
1386
- __wbindgen_cast_0000000000000006: function(arg0) {
1644
+ __wbindgen_cast_0000000000000007: function(arg0) {
1387
1645
  // Cast intrinsic for `U64 -> Externref`.
1388
1646
  const ret = BigInt.asUintN(64, arg0);
1389
1647
  return ret;
1390
1648
  },
1391
- __wbindgen_cast_0000000000000007: function(arg0, arg1) {
1649
+ __wbindgen_cast_0000000000000008: function(arg0, arg1) {
1392
1650
  var v0 = getArrayU8FromWasm0(arg0, arg1).slice();
1393
1651
  wasm.__wbindgen_free(arg0, arg1 * 1, 1);
1394
1652
  // Cast intrinsic for `Vector(U8) -> Externref`.
@@ -1411,21 +1669,33 @@ function __wbg_get_imports() {
1411
1669
  };
1412
1670
  }
1413
1671
 
1414
- function wasm_bindgen__convert__closures_____invoke__h6096c0f3a625f394(arg0, arg1, arg2) {
1415
- wasm.wasm_bindgen__convert__closures_____invoke__h6096c0f3a625f394(arg0, arg1, arg2);
1672
+ function wasm_bindgen__convert__closures_____invoke__h325a87e086f6f78c(arg0, arg1) {
1673
+ wasm.wasm_bindgen__convert__closures_____invoke__h325a87e086f6f78c(arg0, arg1);
1674
+ }
1675
+
1676
+ function wasm_bindgen__convert__closures_____invoke__h296c8d2fd599fbcf(arg0, arg1, arg2) {
1677
+ wasm.wasm_bindgen__convert__closures_____invoke__h296c8d2fd599fbcf(arg0, arg1, arg2);
1416
1678
  }
1417
1679
 
1418
- function wasm_bindgen__convert__closures_____invoke__h4bd31e7387ab665d(arg0, arg1, arg2) {
1419
- const ret = wasm.wasm_bindgen__convert__closures_____invoke__h4bd31e7387ab665d(arg0, arg1, arg2);
1680
+ function wasm_bindgen__convert__closures_____invoke__h3022e61d849d0b12(arg0, arg1, arg2) {
1681
+ const ret = wasm.wasm_bindgen__convert__closures_____invoke__h3022e61d849d0b12(arg0, arg1, arg2);
1420
1682
  if (ret[1]) {
1421
1683
  throw takeFromExternrefTable0(ret[0]);
1422
1684
  }
1423
1685
  }
1424
1686
 
1425
- function wasm_bindgen__convert__closures_____invoke__h675f15260128efbc(arg0, arg1, arg2, arg3) {
1426
- wasm.wasm_bindgen__convert__closures_____invoke__h675f15260128efbc(arg0, arg1, arg2, arg3);
1687
+ function wasm_bindgen__convert__closures_____invoke__h470c93d8a24e0015(arg0, arg1, arg2, arg3) {
1688
+ wasm.wasm_bindgen__convert__closures_____invoke__h470c93d8a24e0015(arg0, arg1, arg2, arg3);
1427
1689
  }
1428
1690
 
1691
+
1692
+ const __wbindgen_enum_RequestCache = ["default", "no-store", "reload", "no-cache", "force-cache", "only-if-cached"];
1693
+
1694
+
1695
+ const __wbindgen_enum_RequestCredentials = ["omit", "same-origin", "include"];
1696
+
1697
+
1698
+ const __wbindgen_enum_RequestMode = ["same-origin", "no-cors", "cors", "navigate"];
1429
1699
  const CollectionWasmFinalization = (typeof FinalizationRegistry === 'undefined')
1430
1700
  ? { register: () => {}, unregister: () => {} }
1431
1701
  : new FinalizationRegistry(ptr => wasm.__wbg_collectionwasm_free(ptr >>> 0, 1));
Binary file
@@ -2,6 +2,7 @@
2
2
  /* eslint-disable */
3
3
  export const memory: WebAssembly.Memory;
4
4
  export const __wbg_workerdb_free: (a: number, b: number) => void;
5
+ export const workerdb_compactTombstones: (a: number, b: number, c: number, d: number) => [number, number, number];
5
6
  export const workerdb_count: (a: number, b: number, c: number, d: number, e: number) => [number, number, number];
6
7
  export const workerdb_createFtsIndex: (a: number, b: number, c: number, d: number, e: number) => [number, number];
7
8
  export const workerdb_createIndex: (a: number, b: number, c: number, d: number, e: number) => [number, number];
@@ -11,14 +12,19 @@ export const workerdb_deleteOne: (a: number, b: number, c: number, d: number, e:
11
12
  export const workerdb_dropFtsIndex: (a: number, b: number, c: number, d: number, e: number) => [number, number];
12
13
  export const workerdb_dropIndex: (a: number, b: number, c: number, d: number, e: number) => [number, number];
13
14
  export const workerdb_dropVectorIndex: (a: number, b: number, c: number, d: number, e: number) => [number, number];
15
+ export const workerdb_exportChangeset: (a: number, b: number, c: number, d: number) => [number, number, number, number];
14
16
  export const workerdb_exportSnapshot: (a: number) => [number, number, number, number];
15
17
  export const workerdb_find: (a: number, b: number, c: number, d: number, e: number) => [number, number, number, number];
16
18
  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];
17
19
  export const workerdb_findOne: (a: number, b: number, c: number, d: number, e: number) => [number, number, number, number];
20
+ export const workerdb_importChangeset: (a: number, b: number, c: number) => [number, number, number];
18
21
  export const workerdb_insert: (a: number, b: number, c: number, d: number, e: number) => [number, number, number, number];
19
22
  export const workerdb_insertMany: (a: number, b: number, c: number, d: number, e: number) => [number, number, number, number];
23
+ export const workerdb_listCollections: (a: number) => [number, number, number, number];
20
24
  export const workerdb_listIndexes: (a: number, b: number, c: number) => [number, number, number, number];
21
25
  export const workerdb_openInMemory: () => [number, number, number];
26
+ export const workerdb_openWithConfigAndOpfs: (a: any, b: number, c: number) => [number, number, number];
27
+ export const workerdb_openWithConfigAndSnapshot: (a: number, b: number, c: number, d: number) => [number, number, number];
22
28
  export const workerdb_openWithOpfs: (a: any) => [number, number, number];
23
29
  export const workerdb_openWithSnapshot: (a: number, b: number) => [number, number, number];
24
30
  export const workerdb_updateMany: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => [number, number, number];
@@ -46,18 +52,20 @@ export const taladbwasm_exportSnapshot: (a: number) => [number, number, number,
46
52
  export const taladbwasm_openInMemory: () => [number, number, number];
47
53
  export const taladbwasm_openWithSnapshot: (a: number, b: number) => [number, number, number];
48
54
  export const init: () => void;
55
+ export const idb_load_snapshot: (a: number, b: number) => any;
56
+ export const idb_save_snapshot: (a: number, b: number, c: number, d: number) => any;
49
57
  export const is_opfs_available: () => any;
50
58
  export const opfs_delete_snapshot: (a: number, b: number) => any;
51
59
  export const opfs_flush_snapshot: (a: number, b: number, c: number, d: number) => any;
52
60
  export const opfs_load_snapshot: (a: number, b: number) => any;
53
61
  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;
56
- export const wasm_bindgen__closure__destroy__h1411c0b1433bd547: (a: number, b: number) => void;
57
- export const wasm_bindgen__closure__destroy__hc294c7484c23aa40: (a: number, b: number) => void;
58
- export const wasm_bindgen__convert__closures_____invoke__h4bd31e7387ab665d: (a: number, b: number, c: any) => [number, number];
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__h6096c0f3a625f394: (a: number, b: number, c: any) => void;
62
+ export const wasm_bindgen__closure__destroy__h72641f1e1c5255af: (a: number, b: number) => void;
63
+ export const wasm_bindgen__closure__destroy__h478053cb26acc43f: (a: number, b: number) => void;
64
+ export const wasm_bindgen__closure__destroy__h1febc128ce23ccd1: (a: number, b: number) => void;
65
+ export const wasm_bindgen__convert__closures_____invoke__h3022e61d849d0b12: (a: number, b: number, c: any) => [number, number];
66
+ export const wasm_bindgen__convert__closures_____invoke__h470c93d8a24e0015: (a: number, b: number, c: any, d: any) => void;
67
+ export const wasm_bindgen__convert__closures_____invoke__h296c8d2fd599fbcf: (a: number, b: number, c: any) => void;
68
+ export const wasm_bindgen__convert__closures_____invoke__h325a87e086f6f78c: (a: number, b: number) => void;
61
69
  export const __wbindgen_malloc: (a: number, b: number) => number;
62
70
  export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
63
71
  export const __wbindgen_exn_store: (a: number) => void;
@@ -48,6 +48,10 @@
48
48
  * dropVectorIndex { collection, field }
49
49
  * upgradeVectorIndex { collection, field }
50
50
  * findNearest { collection, field, queryJson, topK, filterJson? }
51
+ * listCollections {} → JSON string[]
52
+ * compactTombstones { collection, beforeMs } → number pruned
53
+ * exportChangeset { collectionsJson, sinceMs? } → JSON changeset string
54
+ * importChangeset { changesetJson } → number of applied changes
51
55
  * close {}
52
56
  *
53
57
  * Multi-tab live queries (BroadcastChannel)
@@ -57,6 +61,16 @@
57
61
  * `"taladb:<dbName>"`. Other tabs listening on the same channel re-trigger
58
62
  * their active `subscribe()` pollers immediately, bypassing the 300 ms tick.
59
63
  *
64
+ * Secondary-tab write propagation
65
+ * --------------------------------
66
+ * Fallback (non-OPFS) tabs can also make writes that need to reach the primary
67
+ * tab's OPFS-backed database. After every mutating op a fallback tab exports a
68
+ * mini-changeset (all collections, since the previous sync point) and posts it
69
+ * as `{ type: 'taladb:secondary-write', changeset }` on the BroadcastChannel.
70
+ * The primary (OPFS) tab receives it, calls importChangeset(), and runs the
71
+ * normal onWriteCommitted() path — so all other tabs are notified and the OPFS
72
+ * file stays authoritative. LWW merge handles any concurrent edits.
73
+ *
60
74
  * IndexedDB fallback (no OPFS)
61
75
  * ----------------------------
62
76
  * When OPFS is unavailable (cross-origin iframes, Firefox without storage
@@ -102,6 +116,9 @@ const initPromises = new Map();
102
116
  /** The dbName that was successfully initialised (or is being initialised). */
103
117
  let activeDbName = null;
104
118
 
119
+ /** The configJson passed during init (stored so snapshot reloads can use it). */
120
+ let activeConfigJson = null;
121
+
105
122
  const isDev = typeof location !== 'undefined' && (location.hostname === 'localhost' || location.hostname === '127.0.0.1');
106
123
  const log = isDev ? console.log.bind(console, '[TalaDB Worker]') : () => {};
107
124
  const warn = isDev ? console.warn.bind(console, '[TalaDB Worker]') : () => {};
@@ -127,6 +144,14 @@ let broadcastChannel = null;
127
144
  */
128
145
  let idbFallback = false;
129
146
 
147
+ /**
148
+ * Timestamp (ms) of the last changeset we exported and broadcast to the
149
+ * primary tab. Used as `sinceMs` for the next export so we only send the
150
+ * delta, not the entire database on every write.
151
+ * @type {number}
152
+ */
153
+ let lastSecondaryPushMs = 0;
154
+
130
155
  // ---------------------------------------------------------------------------
131
156
  // IndexedDB helpers (used only when OPFS is unavailable)
132
157
  // ---------------------------------------------------------------------------
@@ -190,22 +215,99 @@ async function idbSaveSnapshot(dbName, bytes) {
190
215
  } catch { /* best-effort persistence — ignore failures */ }
191
216
  }
192
217
 
218
+ // ---------------------------------------------------------------------------
219
+ // Debounced IDB snapshot
220
+ // ---------------------------------------------------------------------------
221
+
193
222
  /**
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.
223
+ * Debounce + max-interval parameters for IDB snapshot persistence.
224
+ *
225
+ * On every write we notify sibling tabs immediately via BroadcastChannel,
226
+ * but we defer the actual IDB persistence so that bulk inserts (insertMany,
227
+ * rapid sequential inserts) only produce a single IDB write rather than one
228
+ * per document. A max-interval cap ensures data is never more than 5 s stale
229
+ * in IDB even under continuous write load.
196
230
  */
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.
231
+ const SNAPSHOT_DEBOUNCE_MS = 500;
232
+ const SNAPSHOT_MAX_INTERVAL_MS = 5000;
233
+
234
+ /** setTimeout handle for the pending debounced flush. */
235
+ let snapshotTimer = null;
236
+
237
+ /** Timestamp of the last completed IDB flush (ms since epoch). */
238
+ let lastSnapshotMs = 0;
239
+
240
+ /** Perform the IDB snapshot write and reset state. */
241
+ async function flushSnapshot() {
242
+ clearTimeout(snapshotTimer);
243
+ snapshotTimer = null;
244
+ lastSnapshotMs = Date.now();
201
245
  if (db && activeDbName) {
202
246
  try {
203
247
  const bytes = db.exportSnapshot();
204
- idbSaveSnapshot(activeDbName, bytes).catch(() => {});
205
- } catch { /* ignore snapshot export errors */ }
248
+ await idbSaveSnapshot(activeDbName, bytes);
249
+ } catch { /* best-effort ignore failures */ }
206
250
  }
207
251
  }
208
252
 
253
+ /**
254
+ * Schedule (or immediately trigger) an IDB snapshot write.
255
+ *
256
+ * - If the last flush was more than SNAPSHOT_MAX_INTERVAL_MS ago, flush now.
257
+ * - Otherwise debounce: reset the timer to fire SNAPSHOT_DEBOUNCE_MS from now.
258
+ */
259
+ function scheduleSnapshot() {
260
+ const now = Date.now();
261
+ if (now - lastSnapshotMs > SNAPSHOT_MAX_INTERVAL_MS) {
262
+ // Overdue — flush synchronously in the microtask queue.
263
+ flushSnapshot().catch(() => {});
264
+ return;
265
+ }
266
+ clearTimeout(snapshotTimer);
267
+ snapshotTimer = setTimeout(() => { flushSnapshot().catch(() => {}); }, SNAPSHOT_DEBOUNCE_MS);
268
+ }
269
+
270
+ /**
271
+ * When running as a fallback (non-OPFS) tab, export the changes made since
272
+ * the last push and broadcast them to the primary tab for merging into OPFS.
273
+ *
274
+ * Uses exportChangeset with all known collections to keep the delta small.
275
+ * The primary tab's BroadcastChannel handler calls importChangeset() and runs
276
+ * its own onWriteCommitted(), which notifies all tabs (including this one) via
277
+ * taladb:changed so reads stay consistent.
278
+ */
279
+ function pushChangesetToPrimary() {
280
+ if (!idbFallback || !db || !broadcastChannel || !activeDbName) return;
281
+ try {
282
+ // Collect all collection names from the current in-memory state.
283
+ // exportChangeset accepts a JSON array of collection names.
284
+ const collections = db.listCollections();
285
+ const changeset = db.exportChangeset(collections, lastSecondaryPushMs);
286
+ // Only broadcast if there is actually something to send.
287
+ const parsed = JSON.parse(changeset);
288
+ if (parsed.length === 0) return;
289
+ lastSecondaryPushMs = Date.now();
290
+ broadcastChannel.postMessage({ type: 'taladb:secondary-write', changeset });
291
+ log(`Broadcast ${parsed.length} change(s) to primary tab`);
292
+ } catch (err) {
293
+ warn('Failed to export changeset for primary tab:', err);
294
+ }
295
+ }
296
+
297
+ /**
298
+ * Notify sibling tabs of a write and schedule IDB persistence.
299
+ * Must be called after every mutating op.
300
+ */
301
+ function onWriteCommitted() {
302
+ broadcastChannel?.postMessage('taladb:changed');
303
+ // Fallback tab: push local changes to the primary (OPFS) tab so they are
304
+ // merged into the authoritative database file.
305
+ pushChangesetToPrimary();
306
+ // Debounced IDB flush — keeps other tabs' fallback instances in sync via
307
+ // BroadcastChannel + snapshotDirty reload without writing to IDB on every op.
308
+ scheduleSnapshot();
309
+ }
310
+
209
311
  // ---------------------------------------------------------------------------
210
312
  // Dedicated Worker message handler
211
313
  // ---------------------------------------------------------------------------
@@ -231,12 +333,16 @@ async function dispatch(op, args) {
231
333
  snapshotDirty = false;
232
334
  try {
233
335
  const fresh = await idbLoadSnapshot(activeDbName);
234
- if (fresh) db = WorkerDB.openWithSnapshot(fresh);
336
+ if (fresh) {
337
+ db = activeConfigJson
338
+ ? WorkerDB.openWithConfigAndSnapshot(fresh, activeConfigJson)
339
+ : WorkerDB.openWithSnapshot(fresh);
340
+ }
235
341
  } catch { /* ignore reload errors — stale read is acceptable */ }
236
342
  }
237
343
 
238
344
  if (op === 'init') {
239
- const { dbName } = args;
345
+ const { dbName, configJson } = args;
240
346
 
241
347
  if (activeDbName !== null && activeDbName !== dbName) {
242
348
  throw new Error(
@@ -247,7 +353,8 @@ async function dispatch(op, args) {
247
353
 
248
354
  if (!initPromises.has(dbName)) {
249
355
  activeDbName = dbName;
250
- initPromises.set(dbName, doInit(dbName));
356
+ activeConfigJson = configJson ?? null;
357
+ initPromises.set(dbName, doInit(dbName, configJson ?? null));
251
358
  }
252
359
  await initPromises.get(dbName);
253
360
  return null;
@@ -349,7 +456,31 @@ async function dispatch(op, args) {
349
456
  args.filterJson ?? 'null',
350
457
  );
351
458
 
459
+ case 'listCollections':
460
+ return db.listCollections();
461
+
462
+ case 'compactTombstones':
463
+ // Prune tombstones older than beforeMs from a collection.
464
+ // Returns the count of tombstones removed.
465
+ return db.compactTombstones(args.collection, args.beforeMs ?? 0);
466
+
467
+ case 'exportChangeset':
468
+ // Export a LWW changeset for the given collections since sinceMs.
469
+ // Returns a JSON string the caller can POST to a sync server.
470
+ return db.exportChangeset(args.collectionsJson, args.sinceMs ?? 0);
471
+
472
+ case 'importChangeset': {
473
+ // Apply a remote changeset (JSON string from sync server) using LWW.
474
+ // Triggers onWriteCommitted so multi-tab peers get notified.
475
+ const applied = db.importChangeset(args.changesetJson);
476
+ if (applied > 0) onWriteCommitted();
477
+ return applied;
478
+ }
479
+
352
480
  case 'close':
481
+ // Flush any pending debounced snapshot before releasing the lock so
482
+ // no writes are lost when the tab closes or navigates away.
483
+ await flushSnapshot();
353
484
  // Release the Web Lock and close the sync handle gracefully.
354
485
  if (releaseLock) { releaseLock(); releaseLock = null; }
355
486
  broadcastChannel?.close();
@@ -367,7 +498,7 @@ async function dispatch(op, args) {
367
498
  // Initialisation — load WASM, acquire lock, open OPFS file
368
499
  // ---------------------------------------------------------------------------
369
500
 
370
- async function doInit(dbName) {
501
+ async function doInit(dbName, configJson) {
371
502
  const wasm = await import('../pkg/taladb_web.js');
372
503
  await wasm.default();
373
504
 
@@ -394,16 +525,39 @@ async function doInit(dbName) {
394
525
  const resolve = pendingSnapshotResolve;
395
526
  pendingSnapshotResolve = null;
396
527
  resolve();
528
+ } else if (e.data?.type === 'taladb:secondary-write' && !idbFallback && db) {
529
+ // Primary (OPFS) tab: a secondary tab made a write — merge it in via LWW.
530
+ try {
531
+ const applied = db.importChangeset(e.data.changeset);
532
+ if (applied > 0) {
533
+ log(`Merged ${applied} change(s) from secondary tab`);
534
+ onWriteCommitted();
535
+ }
536
+ } catch (err) {
537
+ warn('Failed to import secondary-tab changeset:', err);
538
+ }
397
539
  }
398
540
  };
399
541
  log('BroadcastChannel opened:', `taladb:${dbName}`);
400
542
  }
401
543
 
544
+ // Helpers to open DB with or without sync config.
545
+ // Uses the config-aware constructors when configJson is provided so that
546
+ // HTTP push sync is wired up from the first write.
547
+ function openWithSnapshot(snapshot) {
548
+ if (configJson) return WorkerDB.openWithConfigAndSnapshot(snapshot, configJson);
549
+ return WorkerDB.openWithSnapshot(snapshot);
550
+ }
551
+ function openWithOpfs(syncHandle) {
552
+ if (configJson) return WorkerDB.openWithConfigAndOpfs(syncHandle, configJson);
553
+ return WorkerDB.openWithOpfs(syncHandle);
554
+ }
555
+
402
556
  const opfsAvailable = await checkOpfs();
403
557
  if (!opfsAvailable) {
404
558
  warn('OPFS unavailable — falling back to IndexedDB-backed in-memory');
405
559
  const snapshot = await idbLoadSnapshot(dbName);
406
- db = WorkerDB.openWithSnapshot(snapshot);
560
+ db = openWithSnapshot(snapshot);
407
561
  idbFallback = true;
408
562
  if (snapshot) {
409
563
  log(`Restored from IndexedDB snapshot (${snapshot.byteLength} bytes)`);
@@ -421,7 +575,7 @@ async function doInit(dbName) {
421
575
  // Web Locks not available — open directly (single-tab safe only).
422
576
  warn('Web Locks unavailable — multi-tab write safety disabled');
423
577
  const syncHandle = await fileHandle.createSyncAccessHandle();
424
- db = WorkerDB.openWithOpfs(syncHandle);
578
+ db = openWithOpfs(syncHandle);
425
579
  log(`Opened "${fileName}" via OPFS`);
426
580
  return;
427
581
  }
@@ -451,7 +605,7 @@ async function doInit(dbName) {
451
605
  if (gotIt !== false) snapshot = await idbLoadSnapshot(dbName);
452
606
  }
453
607
 
454
- db = WorkerDB.openWithSnapshot(snapshot ?? null);
608
+ db = openWithSnapshot(snapshot ?? null);
455
609
  idbFallback = true;
456
610
  if (snapshot) {
457
611
  log(`Restored from IDB snapshot (${snapshot.byteLength} bytes)`);
@@ -465,7 +619,7 @@ async function doInit(dbName) {
465
619
  // Acquired the lock — use OPFS.
466
620
  try {
467
621
  const syncHandle = await fileHandle.createSyncAccessHandle();
468
- db = WorkerDB.openWithOpfs(syncHandle);
622
+ db = openWithOpfs(syncHandle);
469
623
  log(`Opened "${fileName}" via OPFS (Web Locks)`);
470
624
  resolve(); // signal doInit complete — caller can proceed
471
625