@moyodb/sdk 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/dist/LICENSE +202 -0
  2. package/dist/assets/browser-worker-DP6Mjz7c.js +4 -0
  3. package/dist/assets/browser-worker-DP6Mjz7c.js.map +1 -0
  4. package/dist/assets/shared-worker-DKzhauVf.js +2 -0
  5. package/dist/assets/shared-worker-DKzhauVf.js.map +1 -0
  6. package/dist/browser-worker.d.ts +2 -0
  7. package/dist/browser-worker.d.ts.map +1 -0
  8. package/dist/change-events.d.ts +1 -1
  9. package/dist/chunks/indexing-BzmFhWXs.js +1576 -0
  10. package/dist/chunks/indexing-BzmFhWXs.js.map +1 -0
  11. package/dist/codec.d.ts +1 -1
  12. package/dist/codec.d.ts.map +1 -1
  13. package/dist/compression.d.ts +1 -1
  14. package/dist/compression.d.ts.map +1 -1
  15. package/dist/demo.d.ts +2 -1
  16. package/dist/demo.d.ts.map +1 -1
  17. package/dist/engine/README.md +63 -16
  18. package/dist/engine/moyodb_engine.d.ts +29 -19
  19. package/dist/engine/moyodb_engine.js +78 -28
  20. package/dist/engine/moyodb_engine_bg.wasm +0 -0
  21. package/dist/engine/moyodb_engine_bg.wasm.d.ts +4 -2
  22. package/dist/engine/package.json +1 -1
  23. package/dist/engine/snippets/moyodb-engine-8ebe6af0cc2e81b7/js/opfs_shim.js +151 -62
  24. package/dist/errors.d.ts +6 -3
  25. package/dist/errors.d.ts.map +1 -1
  26. package/dist/index.d.ts +11 -5
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/indexing.d.ts +9 -1
  29. package/dist/indexing.d.ts.map +1 -1
  30. package/dist/moyodb-sdk.js +2253 -589
  31. package/dist/moyodb-sdk.js.map +1 -1
  32. package/dist/node-engine.js +2213 -0
  33. package/dist/node-engine.js.map +1 -0
  34. package/dist/node-storage.d.mts +12 -0
  35. package/dist/node-storage.mjs +634 -0
  36. package/dist/node-worker-client.d.ts +19 -0
  37. package/dist/node-worker-client.d.ts.map +1 -0
  38. package/dist/node-worker.mjs +118 -0
  39. package/dist/node.d.ts +19 -0
  40. package/dist/node.d.ts.map +1 -0
  41. package/dist/node.js +3194 -0
  42. package/dist/node.js.map +1 -0
  43. package/dist/records.d.ts +75 -0
  44. package/dist/records.d.ts.map +1 -0
  45. package/dist/registry.d.ts +13 -4
  46. package/dist/registry.d.ts.map +1 -1
  47. package/dist/shared-worker-client.d.ts +24 -0
  48. package/dist/shared-worker-client.d.ts.map +1 -0
  49. package/dist/shared-worker-protocol.d.ts +16 -0
  50. package/dist/shared-worker-protocol.d.ts.map +1 -0
  51. package/dist/shared-worker.d.ts +33 -0
  52. package/dist/shared-worker.d.ts.map +1 -0
  53. package/dist/sql-parser.d.ts +9 -0
  54. package/dist/sql-parser.d.ts.map +1 -0
  55. package/dist/sql-types.d.ts +82 -0
  56. package/dist/sql-types.d.ts.map +1 -0
  57. package/dist/sql.d.ts +32 -0
  58. package/dist/sql.d.ts.map +1 -0
  59. package/dist/subscriptions.d.ts +2 -2
  60. package/dist/subscriptions.d.ts.map +1 -1
  61. package/dist/types.d.ts +9 -6
  62. package/dist/types.d.ts.map +1 -1
  63. package/dist/worker-api.d.ts +5 -6
  64. package/dist/worker-api.d.ts.map +1 -1
  65. package/dist/worker-client.d.ts +32 -5
  66. package/dist/worker-client.d.ts.map +1 -1
  67. package/dist/worker-protocol.d.ts +36 -3
  68. package/dist/worker-protocol.d.ts.map +1 -1
  69. package/dist/worker-server.d.ts +10 -2
  70. package/dist/worker-server.d.ts.map +1 -1
  71. package/dist/worker.d.ts +263 -1
  72. package/dist/worker.d.ts.map +1 -1
  73. package/package.json +14 -5
  74. package/README.md +0 -59
  75. package/dist/assets/worker-R2dy1Ju5.js +0 -4
  76. package/dist/assets/worker-R2dy1Ju5.js.map +0 -1
@@ -1,5 +1,5 @@
1
1
  /* @ts-self-types="./moyodb_engine.d.ts" */
2
- import { opfsCleanupInactiveEntries, opfsCloseSession, opfsDbDirectorySize, opfsFlush, opfsLen, opfsOpenActiveDb, opfsOpenGenerationDb, opfsPrepareRebuildTarget, opfsReadActiveGeneration, opfsReadAt, opfsRemoveDb, opfsSwapActiveGeneration, opfsTruncate, opfsWriteAt } from './snippets/moyodb-engine-8ebe6af0cc2e81b7/js/opfs_shim.js';
2
+ import { opfsAppendOffset, opfsCleanupInactiveEntries, opfsCloseSession, opfsDbDirectorySize, opfsFlush, opfsLen, opfsOpenActiveDb, opfsOpenGenerationDb, opfsPrepareRebuildTarget, opfsReadActiveGeneration, opfsReadAtInto, opfsRemoveDb, opfsSwapActiveGeneration, opfsTruncate, opfsWriteAt } from './snippets/moyodb-engine-8ebe6af0cc2e81b7/js/opfs_shim.js';
3
3
 
4
4
 
5
5
  export class WasmEngine {
@@ -54,8 +54,8 @@ export class WasmEngine {
54
54
  }
55
55
  }
56
56
  /**
57
- * Returns one byte per op: for puts whether the key existed before,
58
- * for deletes whether a key was deleted. The op kinds are the caller's.
57
+ * Returns one byte per operation in input order. For puts, 1 means
58
+ * the key existed before the write. For deletes, 1 means it was deleted.
59
59
  * @param {bigint} tx_id
60
60
  * @param {string} store
61
61
  * @param {Uint8Array} ops
@@ -175,8 +175,8 @@ export class WasmEngine {
175
175
  }
176
176
  }
177
177
  /**
178
- * Checkpoints (when healthy) and closes. The handle is released even
179
- * when closing fails, so a retry never reuses a half-closed engine.
178
+ * Checkpoints a healthy engine and closes it. Releases the binding
179
+ * handle even on error so a retry cannot reuse a partially closed engine.
180
180
  */
181
181
  close() {
182
182
  try {
@@ -211,8 +211,8 @@ export class WasmEngine {
211
211
  }
212
212
  }
213
213
  /**
214
- * Streams this database into `target`, a freshly opened empty
215
- * generation. Returns the txid the target was published at.
214
+ * Streams this database into a new, empty target generation.
215
+ * Returns the target txid. The caller must activate the generation.
216
216
  * @param {WasmEngine} target
217
217
  * @returns {bigint}
218
218
  */
@@ -232,6 +232,29 @@ export class WasmEngine {
232
232
  wasm.__wbindgen_add_to_stack_pointer(16);
233
233
  }
234
234
  }
235
+ /**
236
+ * Omits the named internal stores during a rebuild. Compaction
237
+ * also omits the change log and retains the other stores.
238
+ * @param {WasmEngine} target
239
+ * @param {any} skip_stores
240
+ * @returns {bigint}
241
+ */
242
+ compact_into_skipping_stores(target, skip_stores) {
243
+ try {
244
+ const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
245
+ _assertClass(target, WasmEngine);
246
+ wasm.wasmengine_compact_into_skipping_stores(retptr, this.__wbg_ptr, target.__wbg_ptr, addHeapObject(skip_stores));
247
+ var r0 = getDataViewMemory0().getBigInt64(retptr + 8 * 0, true);
248
+ var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
249
+ var r3 = getDataViewMemory0().getInt32(retptr + 4 * 3, true);
250
+ if (r3) {
251
+ throw takeObject(r2);
252
+ }
253
+ return BigInt.asUintN(64, r0);
254
+ } finally {
255
+ wasm.__wbindgen_add_to_stack_pointer(16);
256
+ }
257
+ }
235
258
  /**
236
259
  * @param {bigint} tx_id
237
260
  * @param {string} name
@@ -418,9 +441,9 @@ export class WasmEngine {
418
441
  }
419
442
  }
420
443
  /**
421
- * Keys arrive packed; values leave packed as
422
- * `u32 count | count x u32 length (u32::MAX = missing) | bytes`,
423
- * one buffer instead of one JS array per value.
444
+ * Returns packed values as
445
+ * `u32 count | count x u32 length (u32::MAX = missing) | bytes`.
446
+ * All integers are little-endian.
424
447
  * @param {bigint} tx_id
425
448
  * @param {string} store
426
449
  * @param {Uint8Array} keys
@@ -491,6 +514,30 @@ export class WasmEngine {
491
514
  wasm.__wbindgen_add_to_stack_pointer(16);
492
515
  }
493
516
  }
517
+ /**
518
+ * Checks existence using one TTL timestamp without loading value bodies.
519
+ * @param {bigint} tx_id
520
+ * @param {string} store
521
+ * @param {any} keys
522
+ * @returns {any}
523
+ */
524
+ has_many(tx_id, store, keys) {
525
+ try {
526
+ const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
527
+ const ptr0 = passStringToWasm0(store, wasm.__wbindgen_export, wasm.__wbindgen_export2);
528
+ const len0 = WASM_VECTOR_LEN;
529
+ wasm.wasmengine_has_many(retptr, this.__wbg_ptr, tx_id, ptr0, len0, addHeapObject(keys));
530
+ var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
531
+ var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
532
+ var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
533
+ if (r2) {
534
+ throw takeObject(r1);
535
+ }
536
+ return takeObject(r0);
537
+ } finally {
538
+ wasm.__wbindgen_add_to_stack_pointer(16);
539
+ }
540
+ }
494
541
  /**
495
542
  * @returns {any}
496
543
  */
@@ -845,8 +892,7 @@ export class WasmEngine {
845
892
  if (Symbol.dispose) WasmEngine.prototype[Symbol.dispose] = WasmEngine.prototype.free;
846
893
 
847
894
  /**
848
- * `"debug"` or `"release"`; benchmark reports record which build they
849
- * measured instead of trusting the build script that was supposed to run.
895
+ * Returns `"debug"` or `"release"` for the loaded WASM build.
850
896
  * @returns {string}
851
897
  */
852
898
  export function buildProfile() {
@@ -911,8 +957,9 @@ export function prepareRebuildTarget(name) {
911
957
  }
912
958
 
913
959
  /**
914
- * Active generation from the control file, or `null` for the legacy
915
- * layout. A corrupt control file is an error, never `null`.
960
+ * Returns the active generation, or `null` for the legacy layout.
961
+ * A corrupt control file permits legacy fallback only while legacy data
962
+ * files remain. Otherwise it is an error.
916
963
  * @param {string} name
917
964
  * @returns {Promise<any>}
918
965
  */
@@ -924,10 +971,9 @@ export function readActiveGeneration(name) {
924
971
  }
925
972
 
926
973
  /**
927
- * Publishes `generation_name` as the active generation. With
928
- * `expected_current` set, the swap fails unless the control file still
929
- * names that generation (or is absent, for `null`), and the written
930
- * control file is read back and verified before this resolves.
974
+ * Publishes `generation_name` only if the active generation matches
975
+ * `expected_current`. `None` requires no active generation.
976
+ * Verifies the written control file before returning.
931
977
  * @param {string} name
932
978
  * @param {string} generation_name
933
979
  * @param {string | null} [expected_current]
@@ -1145,7 +1191,7 @@ function __wbg_get_imports() {
1145
1191
  const a = state0.a;
1146
1192
  state0.a = 0;
1147
1193
  try {
1148
- return __wasm_bindgen_func_elem_680(a, state0.b, arg0, arg1);
1194
+ return __wasm_bindgen_func_elem_736(a, state0.b, arg0, arg1);
1149
1195
  } finally {
1150
1196
  state0.a = a;
1151
1197
  }
@@ -1172,6 +1218,10 @@ function __wbg_get_imports() {
1172
1218
  const ret = Date.now();
1173
1219
  return ret;
1174
1220
  },
1221
+ __wbg_opfsAppendOffset_8c38ddb12af5281b: function() { return handleError(function (arg0, arg1) {
1222
+ const ret = opfsAppendOffset(arg0 >>> 0, arg1 >>> 0);
1223
+ return ret;
1224
+ }, arguments) ?? 0n; },
1175
1225
  __wbg_opfsCleanupInactiveEntries_e761ad3f373325fd: function() { return handleError(function (arg0, arg1) {
1176
1226
  const ret = opfsCleanupInactiveEntries(getStringFromWasm0(arg0, arg1));
1177
1227
  return addHeapObject(ret);
@@ -1206,9 +1256,9 @@ function __wbg_get_imports() {
1206
1256
  const ret = opfsReadActiveGeneration(getStringFromWasm0(arg0, arg1));
1207
1257
  return addHeapObject(ret);
1208
1258
  }, arguments); },
1209
- __wbg_opfsReadAt_e446074c3bbf69d1: function() { return handleError(function (arg0, arg1, arg2, arg3) {
1210
- const ret = opfsReadAt(arg0 >>> 0, arg1 >>> 0, BigInt.asUintN(64, arg2), arg3 >>> 0);
1211
- return addHeapObject(ret);
1259
+ __wbg_opfsReadAtInto_11f57c26cf06d807: function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) {
1260
+ const ret = opfsReadAtInto(arg0 >>> 0, arg1 >>> 0, BigInt.asUintN(64, arg2), getArrayU8FromWasm0(arg3, arg4));
1261
+ return ret;
1212
1262
  }, arguments); },
1213
1263
  __wbg_opfsRemoveDb_7e05aef0688d4c5a: function() { return handleError(function (arg0, arg1) {
1214
1264
  const ret = opfsRemoveDb(getStringFromWasm0(arg0, arg1));
@@ -1283,8 +1333,8 @@ function __wbg_get_imports() {
1283
1333
  return addHeapObject(ret);
1284
1334
  },
1285
1335
  __wbindgen_generic_0000000000000001: function(arg0, arg1) {
1286
- // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 106, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
1287
- const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_665);
1336
+ // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 109, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
1337
+ const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_721);
1288
1338
  return addHeapObject(ret);
1289
1339
  },
1290
1340
  __wbindgen_generic_0000000000000002: function(arg0) {
@@ -1326,14 +1376,14 @@ function __wbg_get_imports() {
1326
1376
  };
1327
1377
  }
1328
1378
 
1329
- function __wasm_bindgen_func_elem_680(arg0, arg1, arg2, arg3) {
1330
- wasm.__wasm_bindgen_func_elem_680(arg0, arg1, addHeapObject(arg2), addHeapObject(arg3));
1379
+ function __wasm_bindgen_func_elem_736(arg0, arg1, arg2, arg3) {
1380
+ wasm.__wasm_bindgen_func_elem_736(arg0, arg1, addHeapObject(arg2), addHeapObject(arg3));
1331
1381
  }
1332
1382
 
1333
- function __wasm_bindgen_func_elem_665(arg0, arg1, arg2) {
1383
+ function __wasm_bindgen_func_elem_721(arg0, arg1, arg2) {
1334
1384
  try {
1335
1385
  const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
1336
- wasm.__wasm_bindgen_func_elem_665(retptr, arg0, arg1, addHeapObject(arg2));
1386
+ wasm.__wasm_bindgen_func_elem_721(retptr, arg0, arg1, addHeapObject(arg2));
1337
1387
  var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
1338
1388
  var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
1339
1389
  if (r1) {
Binary file
@@ -20,6 +20,7 @@ export const wasmengine_clear_store: (a: number, b: number, c: bigint, d: number
20
20
  export const wasmengine_close: (a: number, b: number) => void;
21
21
  export const wasmengine_commit_tx: (a: number, b: number, c: bigint) => void;
22
22
  export const wasmengine_compact_into: (a: number, b: number, c: number) => void;
23
+ export const wasmengine_compact_into_skipping_stores: (a: number, b: number, c: number, d: number) => void;
23
24
  export const wasmengine_create_store: (a: number, b: number, c: bigint, d: number, e: number, f: number) => void;
24
25
  export const wasmengine_delete: (a: number, b: number, c: bigint, d: number, e: number, f: number, g: number) => void;
25
26
  export const wasmengine_delete_many: (a: number, b: number, c: bigint, d: number, e: number, f: number) => void;
@@ -31,6 +32,7 @@ export const wasmengine_get_many: (a: number, b: number, c: bigint, d: number, e
31
32
  export const wasmengine_get_many_packed: (a: number, b: number, c: bigint, d: number, e: number, f: number, g: number) => void;
32
33
  export const wasmengine_get_schema_version: (a: number, b: number) => void;
33
34
  export const wasmengine_has: (a: number, b: number, c: bigint, d: number, e: number, f: number, g: number) => void;
35
+ export const wasmengine_has_many: (a: number, b: number, c: bigint, d: number, e: number, f: number) => void;
34
36
  export const wasmengine_health: (a: number, b: number) => void;
35
37
  export const wasmengine_import_snapshot: (a: number, b: number, c: number, d: number) => void;
36
38
  export const wasmengine_list_store_configs: (a: number, b: number) => void;
@@ -50,8 +52,8 @@ export const wasmengine_set_change_feed_policy: (a: number, b: number, c: bigint
50
52
  export const wasmengine_set_failpoint: (a: number, b: number, c: number) => void;
51
53
  export const wasmengine_set_schema_version: (a: number, b: number, c: bigint, d: bigint) => void;
52
54
  export const wasmengine_stats: (a: number, b: number) => void;
53
- export const __wasm_bindgen_func_elem_665: (a: number, b: number, c: number, d: number) => void;
54
- export const __wasm_bindgen_func_elem_680: (a: number, b: number, c: number, d: number) => void;
55
+ export const __wasm_bindgen_func_elem_721: (a: number, b: number, c: number, d: number) => void;
56
+ export const __wasm_bindgen_func_elem_736: (a: number, b: number, c: number, d: number) => void;
55
57
  export const __wbindgen_export: (a: number, b: number) => number;
56
58
  export const __wbindgen_export2: (a: number, b: number, c: number, d: number) => number;
57
59
  export const __wbindgen_export3: (a: number) => void;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "moyodb-engine",
3
3
  "type": "module",
4
- "description": "Experimental Rust/WASM transactional key-value storage engine for browser apps.",
4
+ "description": "Rust/WASM transactional storage engine for browser and native file runtimes.",
5
5
  "version": "1.0.0",
6
6
  "license": "SEE LICENSE IN ../../LICENSE",
7
7
  "repository": {
@@ -6,12 +6,16 @@ const CONTROL_VERSION = 1;
6
6
  const CONTROL_CHECKSUM_OFFSET = 24;
7
7
  const CONTROL_NAME_OFFSET = 32;
8
8
  const CONTROL_MAGIC = new Uint8Array([66, 68, 66, 82, 79, 79, 84, 49]);
9
- const OPFS_WRITE_CHUNK_SIZE = 256 * 1024;
9
+ // Chromium rejects a SyncAccessHandle.write larger than a signed 32-bit count.
10
+ const OPFS_MAX_WRITE_SIZE = 0x7fffffff;
10
11
  const TEXT_ENCODER = new TextEncoder();
11
12
  const TEXT_DECODER = new TextDecoder();
12
13
  let rootDir = null;
13
14
  let nextSessionId = 1;
14
15
  const sessions = new Map();
16
+ // Exclusive session handles keep cached sizes valid across Rust backend clones.
17
+ // Length checks for validation and statistics still query OPFS.
18
+ const appendOffsets = new WeakMap();
15
19
  function namedError(name, message) {
16
20
  const error = new Error(message);
17
21
  error.name = name;
@@ -67,7 +71,7 @@ function writeU64(view, offset, value) {
67
71
  view.setBigUint64(offset, BigInt(value), true);
68
72
  }
69
73
  function readU64(view, offset) {
70
- return Number(view.getBigUint64(offset, true));
74
+ return view.getBigUint64(offset, true);
71
75
  }
72
76
  function fnv1a32(bytes, zeroOffset = -1, zeroLength = 0) {
73
77
  let hash = 0x811c9dc5;
@@ -82,12 +86,14 @@ function fnv1a32(bytes, zeroOffset = -1, zeroLength = 0) {
82
86
  function isGenerationNameValid(name) {
83
87
  return typeof name === 'string' && /^gen-[a-z0-9]+-[a-z0-9]+$/i.test(name);
84
88
  }
85
- // Every byte must reach the file or the call fails: a short write that returns
86
- // normally would let callers publish state that is not on disk.
89
+ // Returns only after all bytes are written. The caller must flush for durability.
87
90
  export function writeAll(handle, bytes, at) {
88
91
  let writtenTotal = 0;
89
92
  while (writtenTotal < bytes.length) {
90
- const chunk = bytes.subarray(writtenTotal, writtenTotal + OPFS_WRITE_CHUNK_SIZE);
93
+ // Preserve the engine's batch; split only at the browser size limit or
94
+ // after a short write. Suffix views borrow the original buffer.
95
+ const end = Math.min(bytes.length, writtenTotal + OPFS_MAX_WRITE_SIZE);
96
+ const chunk = writtenTotal === 0 && end === bytes.length ? bytes : bytes.subarray(writtenTotal, end);
91
97
  const rawWritten = handle.write(chunk, { at: at + writtenTotal });
92
98
  const written = Number(rawWritten);
93
99
  if (!Number.isSafeInteger(written) || written < 0 || written > chunk.length) {
@@ -100,7 +106,7 @@ export function writeAll(handle, bytes, at) {
100
106
  }
101
107
  return writtenTotal;
102
108
  }
103
- // Fills `buffer` from `at`, stopping only at end of file. Bytes past EOF stay zero.
109
+ // Reads until the buffer is full or a read returns zero. The unread suffix is unchanged.
104
110
  export function readAll(handle, buffer, at) {
105
111
  let readTotal = 0;
106
112
  while (readTotal < buffer.length) {
@@ -138,8 +144,8 @@ export function encodeControlSlot(generationCounter, activeGeneration) {
138
144
  view.setUint32(CONTROL_CHECKSUM_OFFSET, checksum, true);
139
145
  return slot;
140
146
  }
141
- // Returns null for any slot whose bytes do not checksum. Fields are interpreted
142
- // only after the checksum passes, so a torn slot never masks its valid sibling.
147
+ // Reject incomplete slots and bad magic or checksums. Validate the remaining
148
+ // fields after the checksum passes, so torn fields cannot mask a valid slot.
143
149
  export function decodeControlSlot(slotIndex, bytes) {
144
150
  if (bytes.length < CONTROL_SLOT_SIZE) {
145
151
  return null;
@@ -174,9 +180,9 @@ export function decodeControlSlot(slotIndex, bytes) {
174
180
  activeGeneration
175
181
  };
176
182
  }
177
- // `absent` — the file is empty: no generation has ever been published.
178
- // `invalid` — the file has bytes but no slot checksums.
179
- // `valid` — `control` is the newest checksummed slot.
183
+ // `absent`: empty file.
184
+ // `invalid`: nonempty file with no valid slot.
185
+ // `valid`: `control` is the valid slot with the highest generation counter.
180
186
  export function readControlStateFromAccessHandle(accessHandle) {
181
187
  const size = Number(accessHandle.getSize());
182
188
  if (!Number.isSafeInteger(size) || size < 0) {
@@ -207,21 +213,18 @@ async function readControlFile(dbRoot) {
207
213
  accessHandle.close();
208
214
  }
209
215
  }
210
- async function hasLegacyDataFiles(dbRoot) {
211
- for (const fileName of FILE_NAMES) {
212
- if (await lookupFileHandle(dbRoot, fileName)) {
213
- return true;
214
- }
216
+ async function hasLegacyDataFiles(dbRoot, fileKind = 0) {
217
+ if (fileKind === FILE_NAMES.length) {
218
+ return false;
219
+ }
220
+ if (await lookupFileHandle(dbRoot, FILE_NAMES[fileKind])) {
221
+ return true;
215
222
  }
216
- return false;
223
+ return await hasLegacyDataFiles(dbRoot, fileKind + 1);
217
224
  }
218
- // Resolves which directory holds the live database, failing closed on damage.
219
- //
220
- // Legacy root files are removed before the first write into a published
221
- // generation (see opfsCleanupInactiveEntries and opfsOpenActiveDb). So an
222
- // unreadable control file next to legacy files can only be a torn first
223
- // publication, and legacy is still authoritative. Without legacy files it is
224
- // real corruption of the pointer to live data.
225
+ // Legacy files remain authoritative after a torn first publication. They must
226
+ // be removed before a published generation accepts writes, so later control
227
+ // damage cannot make stale legacy files appear current.
225
228
  async function resolveActiveGeneration(dbRoot) {
226
229
  const state = await readControlFile(dbRoot);
227
230
  if (state.status === 'valid') {
@@ -251,7 +254,11 @@ async function writeControlState(dbRoot, activeGeneration, expectedCurrentGenera
251
254
  );
252
255
  }
253
256
  const nextSlot = current.control ? (current.control.slotIndex === 0 ? 1 : 0) : 0;
254
- const nextGenerationCounter = (current.control?.generationCounter ?? 0) + 1;
257
+ const generationCounter = current.control?.generationCounter ?? 0n;
258
+ if (generationCounter === 0xffffffffffffffffn) {
259
+ throw namedError('StorageError', 'control generation counter exhausted');
260
+ }
261
+ const nextGenerationCounter = generationCounter + 1n;
255
262
  const encoded = encodeControlSlot(nextGenerationCounter, activeGeneration);
256
263
  writeAll(accessHandle, encoded, nextSlot * CONTROL_SLOT_SIZE);
257
264
  accessHandle.flush();
@@ -274,19 +281,29 @@ function createGenerationName() {
274
281
  const randomPart = Array.from(randomBytes, (byte) => byte.toString(16).padStart(2, '0')).join('');
275
282
  return `gen-${timePart}-${randomPart}`;
276
283
  }
284
+ async function createGenerationDirectory(dbRoot) {
285
+ const generationName = createGenerationName();
286
+ if (await lookupDirectoryHandle(dbRoot, generationName)) {
287
+ return await createGenerationDirectory(dbRoot);
288
+ }
289
+ await dbRoot.getDirectoryHandle(generationName, { create: true });
290
+ return generationName;
291
+ }
277
292
  async function getDbRoot(encodedDbName, createIfMissing) {
278
293
  const stackdb = await getOrCreateStackdbRoot();
279
- let dbRoot = await lookupDirectoryHandle(stackdb, encodedDbName);
294
+ if (createIfMissing) {
295
+ // Creation also opens existing directories, avoiding a separate lookup.
296
+ return await stackdb.getDirectoryHandle(encodedDbName, { create: true });
297
+ }
298
+ const dbRoot = await lookupDirectoryHandle(stackdb, encodedDbName);
280
299
  if (!dbRoot) {
281
- if (!createIfMissing) {
282
- throw new Error(`database ${encodedDbName} does not exist`);
283
- }
284
- dbRoot = await stackdb.getDirectoryHandle(encodedDbName, { create: true });
300
+ throw new Error(`database ${encodedDbName} does not exist`);
285
301
  }
286
302
  return dbRoot;
287
303
  }
288
304
  async function removeLegacyDataFiles(dbRoot) {
289
- for (const fileName of FILE_NAMES) {
305
+ await FILE_NAMES.reduce(async (previous, fileName) => {
306
+ await previous;
290
307
  try {
291
308
  await dbRoot.removeEntry(fileName);
292
309
  } catch (err) {
@@ -294,7 +311,7 @@ async function removeLegacyDataFiles(dbRoot) {
294
311
  throw err;
295
312
  }
296
313
  }
297
- }
314
+ }, Promise.resolve());
298
315
  }
299
316
  async function resolveActiveDataDir(dbRoot) {
300
317
  const activeGeneration = await resolveActiveGeneration(dbRoot);
@@ -309,8 +326,7 @@ async function resolveActiveDataDir(dbRoot) {
309
326
  if (!activeDir) {
310
327
  throw corruptionError(`active generation ${activeGeneration} is missing`);
311
328
  }
312
- // A crash after publication but before legacy cleanup leaves stale root
313
- // files. They must be gone before this generation accepts writes.
329
+ // Remove stale legacy files before writes can make this generation newer.
314
330
  await removeLegacyDataFiles(dbRoot);
315
331
  return {
316
332
  dirHandle: activeDir,
@@ -324,9 +340,10 @@ function closeSession(sessionId) {
324
340
  return;
325
341
  }
326
342
  for (const handle of session.handles.values()) {
343
+ appendOffsets.delete(handle);
327
344
  try {
328
345
  handle.close();
329
- } catch (_err) {}
346
+ } catch {}
330
347
  }
331
348
  sessions.delete(sessionId);
332
349
  }
@@ -339,24 +356,34 @@ function closeSessionsForDb(encodedDbName) {
339
356
  }
340
357
  }
341
358
  async function openSessionForDir(dbPath, dirHandle, generationName) {
342
- const handles = new Map();
343
- try {
344
- for (let fileKind = 0; fileKind < FILE_NAMES.length; fileKind += 1) {
345
- const fileName = FILE_NAMES[fileKind];
359
+ // Acquire independent files concurrently. Publish the session only after
360
+ // all handles are acquired; failures must release every acquired handle.
361
+ const opened = await Promise.allSettled(
362
+ FILE_NAMES.map(async (fileName) => {
346
363
  const fileHandle = await dirHandle.getFileHandle(fileName, { create: true });
347
364
  if (typeof fileHandle.createSyncAccessHandle !== 'function') {
348
365
  throw new Error('createSyncAccessHandle is unavailable');
349
366
  }
350
- const accessHandle = await fileHandle.createSyncAccessHandle();
351
- handles.set(fileKind, accessHandle);
367
+ return await fileHandle.createSyncAccessHandle();
368
+ })
369
+ );
370
+ const handles = new Map();
371
+ for (let fileKind = 0; fileKind < opened.length; fileKind += 1) {
372
+ const result = opened[fileKind];
373
+ if (result.status === 'fulfilled') {
374
+ handles.set(fileKind, result.value);
352
375
  }
353
- } catch (error) {
376
+ }
377
+ const failed = opened.find((result) => result.status === 'rejected');
378
+ if (failed) {
379
+ // Waiting for all results also captures late successes for cleanup.
354
380
  for (const handle of handles.values()) {
355
381
  try {
356
382
  handle.close();
357
- } catch (_err) {}
383
+ } catch {}
358
384
  }
359
- throw error;
385
+ // Error precedence follows FILE_NAMES order, regardless of completion order.
386
+ throw failed.reason;
360
387
  }
361
388
  const sessionId = nextSessionId;
362
389
  nextSessionId += 1;
@@ -381,11 +408,29 @@ function getAccessHandle(sessionId, fileKind) {
381
408
  }
382
409
  return handle;
383
410
  }
411
+ function rememberFileSize(handle, size) {
412
+ if (typeof size === 'number' && Number.isSafeInteger(size) && size >= 0) {
413
+ appendOffsets.set(handle, size);
414
+ } else {
415
+ appendOffsets.delete(handle);
416
+ }
417
+ }
418
+ function readFileSize(handle) {
419
+ try {
420
+ const size = handle.getSize();
421
+ rememberFileSize(handle, size);
422
+ return size;
423
+ } catch (error) {
424
+ appendOffsets.delete(handle);
425
+ throw error;
426
+ }
427
+ }
384
428
  function lookupOpenFileSize(path) {
385
429
  for (const session of sessions.values()) {
386
430
  for (let fileKind = 0; fileKind < FILE_NAMES.length; fileKind += 1) {
387
431
  if (`${session.path}/${FILE_NAMES[fileKind]}` === path) {
388
- return Number(session.handles.get(fileKind)?.getSize() ?? 0);
432
+ const handle = session.handles.get(fileKind);
433
+ return Number((handle ? readFileSize(handle) : 0) ?? 0);
389
434
  }
390
435
  }
391
436
  }
@@ -426,6 +471,14 @@ export async function opfsReadActiveGeneration(encodedDbName) {
426
471
  const dbRoot = await getDbRoot(encodedDbName, false);
427
472
  return await resolveActiveGeneration(dbRoot);
428
473
  }
474
+ // The WASM view is valid only during this synchronous call. Zero the unread
475
+ // suffix to preserve the backend read contract when the destination is reused.
476
+ export function opfsReadAtInto(sessionId, fileKind, offset, buffer) {
477
+ const handle = getAccessHandle(sessionId, fileKind);
478
+ const read = readAll(handle, buffer, Number(offset));
479
+ buffer.fill(0, read);
480
+ return read;
481
+ }
429
482
  export function opfsReadAt(sessionId, fileKind, offset, len) {
430
483
  const handle = getAccessHandle(sessionId, fileKind);
431
484
  const buffer = new Uint8Array(len);
@@ -433,32 +486,67 @@ export function opfsReadAt(sessionId, fileKind, offset, len) {
433
486
  return buffer;
434
487
  }
435
488
  export function opfsWriteAt(sessionId, fileKind, offset, bytes) {
436
- return writeAll(getAccessHandle(sessionId, fileKind), bytes, Number(offset));
489
+ const handle = getAccessHandle(sessionId, fileKind);
490
+ try {
491
+ const at = Number(offset);
492
+ const written = writeAll(handle, bytes, at);
493
+ // Empty writes cannot extend the file. A successful overwrite alone
494
+ // cannot establish the full file length.
495
+ if (written > 0) {
496
+ const previous = appendOffsets.get(handle);
497
+ const end = at + written;
498
+ if (previous !== undefined && Number.isSafeInteger(at) && at >= 0 && Number.isSafeInteger(end)) {
499
+ appendOffsets.set(handle, Math.max(previous, end));
500
+ } else {
501
+ appendOffsets.delete(handle);
502
+ }
503
+ }
504
+ return written;
505
+ } catch (error) {
506
+ // A failed write may leave a prefix. The next append must read the
507
+ // actual file length, even when the thrown value is not an Error.
508
+ appendOffsets.delete(handle);
509
+ throw error;
510
+ }
437
511
  }
438
512
  export function opfsFlush(sessionId, fileKind) {
439
- getAccessHandle(sessionId, fileKind).flush();
513
+ const handle = getAccessHandle(sessionId, fileKind);
514
+ try {
515
+ handle.flush();
516
+ } catch (error) {
517
+ appendOffsets.delete(handle);
518
+ throw error;
519
+ }
440
520
  }
441
521
  export function opfsLen(sessionId, fileKind) {
442
- return BigInt(getAccessHandle(sessionId, fileKind).getSize());
522
+ return BigInt(readFileSize(getAccessHandle(sessionId, fileKind)));
523
+ }
524
+ export function opfsAppendOffset(sessionId, fileKind) {
525
+ const handle = getAccessHandle(sessionId, fileKind);
526
+ const size = appendOffsets.get(handle);
527
+ return size === undefined ? opfsLen(sessionId, fileKind) : BigInt(size);
443
528
  }
444
529
  export function opfsTruncate(sessionId, fileKind, size) {
445
- getAccessHandle(sessionId, fileKind).truncate(Number(size));
530
+ const handle = getAccessHandle(sessionId, fileKind);
531
+ try {
532
+ const length = Number(size);
533
+ handle.truncate(length);
534
+ rememberFileSize(handle, length);
535
+ } catch (error) {
536
+ appendOffsets.delete(handle);
537
+ throw error;
538
+ }
446
539
  }
447
540
  export function opfsCloseSession(sessionId) {
448
541
  closeSession(sessionId);
449
542
  }
450
543
  export async function opfsPrepareRebuildTarget(encodedDbName) {
451
544
  const dbRoot = await getDbRoot(encodedDbName, true);
452
- let generationName = createGenerationName();
453
- while (await lookupDirectoryHandle(dbRoot, generationName)) {
454
- generationName = createGenerationName();
455
- }
456
- await dbRoot.getDirectoryHandle(generationName, { create: true });
545
+ const generationName = await createGenerationDirectory(dbRoot);
457
546
  return { generationName };
458
547
  }
459
- // Publishes `generationName` as the live database. Resolves only after the
460
- // control file has been flushed and read back selecting the new generation;
461
- // the caller may switch its live engine only after that.
548
+ // Resolves after the control file is flushed and verified to select the new
549
+ // generation. The caller may then switch its live engine.
462
550
  export async function opfsSwapActiveGeneration(encodedDbName, generationName, expectedCurrentGeneration) {
463
551
  if (!isGenerationNameValid(generationName)) {
464
552
  throw new Error(`invalid generation name ${generationName}`);
@@ -470,8 +558,8 @@ export async function opfsSwapActiveGeneration(encodedDbName, generationName, ex
470
558
  }
471
559
  await writeControlState(dbRoot, generationName, expectedCurrentGeneration ?? null);
472
560
  }
473
- // Legacy root files are removed strictly (see resolveActiveGeneration); stale
474
- // generation directories are best effort because nothing can select them.
561
+ // Legacy-file removal must succeed to prevent stale fallback after control
562
+ // damage. Inactive generation cleanup is best effort.
475
563
  export async function opfsCleanupInactiveEntries(encodedDbName) {
476
564
  const dbRoot = await lookupDirectoryHandle(await getOrCreateStackdbRoot(), encodedDbName);
477
565
  if (!dbRoot) {
@@ -487,11 +575,12 @@ export async function opfsCleanupInactiveEntries(encodedDbName) {
487
575
  staleDirectories.push(name);
488
576
  }
489
577
  }
490
- for (const name of staleDirectories) {
578
+ await staleDirectories.reduce(async (previous, name) => {
579
+ await previous;
491
580
  try {
492
581
  await dbRoot.removeEntry(name, { recursive: true });
493
- } catch (_err) {}
494
- }
582
+ } catch {}
583
+ }, Promise.resolve());
495
584
  }
496
585
  export async function opfsDbDirectorySize(encodedDbName) {
497
586
  const dbRoot = await lookupDirectoryHandle(await getOrCreateStackdbRoot(), encodedDbName);
package/dist/errors.d.ts CHANGED
@@ -28,6 +28,9 @@ export declare class UniqueIndexConstraintError extends ConstraintError {
28
28
  export declare class WriteTransactionAlreadyOpenError extends MoyoDbError {
29
29
  constructor(message?: string);
30
30
  }
31
+ export declare class TransactionConflictError extends MoyoDbError {
32
+ constructor(message?: string);
33
+ }
31
34
  export declare class ReadonlyTransactionError extends MoyoDbError {
32
35
  constructor(message?: string);
33
36
  }
@@ -65,9 +68,9 @@ export declare class ChangeFeedCompactedError extends MoyoDbError {
65
68
  constructor(message: string);
66
69
  }
67
70
  /**
68
- * The outcome of a commit could not be confirmed (storage failed at or after
69
- * the WAL append) or the engine hit an unrecoverable fault. The database
70
- * rejects further work until it is reopened, which replays durable state.
71
+ * The engine state could not be trusted, or automatic recovery failed.
72
+ * A failed commit may already be durable. The worker attempts recovery before
73
+ * a new transaction and after a failed commit. Recovery discards all open transactions.
71
74
  */
72
75
  export declare class RecoveryRequiredError extends MoyoDbError {
73
76
  constructor(message: string);