@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.
- package/dist/LICENSE +202 -0
- package/dist/assets/browser-worker-DP6Mjz7c.js +4 -0
- package/dist/assets/browser-worker-DP6Mjz7c.js.map +1 -0
- package/dist/assets/shared-worker-DKzhauVf.js +2 -0
- package/dist/assets/shared-worker-DKzhauVf.js.map +1 -0
- package/dist/browser-worker.d.ts +2 -0
- package/dist/browser-worker.d.ts.map +1 -0
- package/dist/change-events.d.ts +1 -1
- package/dist/chunks/indexing-BzmFhWXs.js +1576 -0
- package/dist/chunks/indexing-BzmFhWXs.js.map +1 -0
- package/dist/codec.d.ts +1 -1
- package/dist/codec.d.ts.map +1 -1
- package/dist/compression.d.ts +1 -1
- package/dist/compression.d.ts.map +1 -1
- package/dist/demo.d.ts +2 -1
- package/dist/demo.d.ts.map +1 -1
- package/dist/engine/README.md +63 -16
- package/dist/engine/moyodb_engine.d.ts +29 -19
- package/dist/engine/moyodb_engine.js +78 -28
- package/dist/engine/moyodb_engine_bg.wasm +0 -0
- package/dist/engine/moyodb_engine_bg.wasm.d.ts +4 -2
- package/dist/engine/package.json +1 -1
- package/dist/engine/snippets/moyodb-engine-8ebe6af0cc2e81b7/js/opfs_shim.js +151 -62
- package/dist/errors.d.ts +6 -3
- package/dist/errors.d.ts.map +1 -1
- package/dist/index.d.ts +11 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/indexing.d.ts +9 -1
- package/dist/indexing.d.ts.map +1 -1
- package/dist/moyodb-sdk.js +2253 -589
- package/dist/moyodb-sdk.js.map +1 -1
- package/dist/node-engine.js +2213 -0
- package/dist/node-engine.js.map +1 -0
- package/dist/node-storage.d.mts +12 -0
- package/dist/node-storage.mjs +634 -0
- package/dist/node-worker-client.d.ts +19 -0
- package/dist/node-worker-client.d.ts.map +1 -0
- package/dist/node-worker.mjs +118 -0
- package/dist/node.d.ts +19 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +3194 -0
- package/dist/node.js.map +1 -0
- package/dist/records.d.ts +75 -0
- package/dist/records.d.ts.map +1 -0
- package/dist/registry.d.ts +13 -4
- package/dist/registry.d.ts.map +1 -1
- package/dist/shared-worker-client.d.ts +24 -0
- package/dist/shared-worker-client.d.ts.map +1 -0
- package/dist/shared-worker-protocol.d.ts +16 -0
- package/dist/shared-worker-protocol.d.ts.map +1 -0
- package/dist/shared-worker.d.ts +33 -0
- package/dist/shared-worker.d.ts.map +1 -0
- package/dist/sql-parser.d.ts +9 -0
- package/dist/sql-parser.d.ts.map +1 -0
- package/dist/sql-types.d.ts +82 -0
- package/dist/sql-types.d.ts.map +1 -0
- package/dist/sql.d.ts +32 -0
- package/dist/sql.d.ts.map +1 -0
- package/dist/subscriptions.d.ts +2 -2
- package/dist/subscriptions.d.ts.map +1 -1
- package/dist/types.d.ts +9 -6
- package/dist/types.d.ts.map +1 -1
- package/dist/worker-api.d.ts +5 -6
- package/dist/worker-api.d.ts.map +1 -1
- package/dist/worker-client.d.ts +32 -5
- package/dist/worker-client.d.ts.map +1 -1
- package/dist/worker-protocol.d.ts +36 -3
- package/dist/worker-protocol.d.ts.map +1 -1
- package/dist/worker-server.d.ts +10 -2
- package/dist/worker-server.d.ts.map +1 -1
- package/dist/worker.d.ts +263 -1
- package/dist/worker.d.ts.map +1 -1
- package/package.json +14 -5
- package/README.md +0 -59
- package/dist/assets/worker-R2dy1Ju5.js +0 -4
- 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,
|
|
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
|
|
58
|
-
*
|
|
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
|
|
179
|
-
*
|
|
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
|
|
215
|
-
*
|
|
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
|
-
*
|
|
422
|
-
* `u32 count | count x u32 length (u32::MAX = missing) | bytes
|
|
423
|
-
*
|
|
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"
|
|
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
|
-
*
|
|
915
|
-
*
|
|
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`
|
|
928
|
-
* `expected_current`
|
|
929
|
-
*
|
|
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
|
|
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
|
-
|
|
1210
|
-
const ret =
|
|
1211
|
-
return
|
|
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:
|
|
1287
|
-
const ret = makeMutClosure(arg0, arg1,
|
|
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
|
|
1330
|
-
wasm.
|
|
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
|
|
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.
|
|
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
|
|
54
|
-
export const
|
|
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;
|
package/dist/engine/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "moyodb-engine",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"description": "
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
142
|
-
//
|
|
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
|
|
178
|
-
// `invalid
|
|
179
|
-
// `valid
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
|
223
|
+
return await hasLegacyDataFiles(dbRoot, fileKind + 1);
|
|
217
224
|
}
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
//
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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
|
-
|
|
351
|
-
|
|
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
|
-
}
|
|
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
|
|
383
|
+
} catch {}
|
|
358
384
|
}
|
|
359
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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)
|
|
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)
|
|
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
|
-
|
|
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
|
-
//
|
|
460
|
-
//
|
|
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
|
|
474
|
-
// generation
|
|
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
|
-
|
|
578
|
+
await staleDirectories.reduce(async (previous, name) => {
|
|
579
|
+
await previous;
|
|
491
580
|
try {
|
|
492
581
|
await dbRoot.removeEntry(name, { recursive: true });
|
|
493
|
-
} catch
|
|
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
|
|
69
|
-
*
|
|
70
|
-
*
|
|
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);
|