@moyodb/sdk 1.1.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRIBUTING.md +46 -0
- package/README.md +144 -0
- package/dist/assets/browser-worker-Bj2ErRvC.js +4 -0
- package/dist/assets/browser-worker-Bj2ErRvC.js.map +1 -0
- package/dist/assets/shared-worker-CMeTcusa.js +2 -0
- package/dist/assets/shared-worker-CMeTcusa.js.map +1 -0
- package/dist/chunks/{indexing-BzmFhWXs.js → indexing-B0duqlUs.js} +131 -124
- package/dist/chunks/indexing-B0duqlUs.js.map +1 -0
- package/dist/compression.d.ts +16 -2
- package/dist/compression.d.ts.map +1 -1
- package/dist/demo.d.ts.map +1 -1
- package/dist/engine/README.md +32 -3
- package/dist/engine/moyodb_engine.d.ts +36 -10
- package/dist/engine/moyodb_engine.js +261 -51
- package/dist/engine/moyodb_engine_bg.wasm +0 -0
- package/dist/engine/moyodb_engine_bg.wasm.d.ts +18 -10
- package/dist/engine/package.json +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/indexing.d.ts.map +1 -1
- package/dist/internal.d.ts +6 -0
- package/dist/internal.d.ts.map +1 -1
- package/dist/moyodb-sdk.js +1069 -835
- package/dist/moyodb-sdk.js.map +1 -1
- package/dist/node-engine.js +731 -111
- package/dist/node-engine.js.map +1 -1
- package/dist/node-storage.d.mts +4 -0
- package/dist/node-storage.mjs +38 -3
- package/dist/node-worker-client.d.ts +1 -0
- package/dist/node-worker-client.d.ts.map +1 -1
- package/dist/node-worker.mjs +2 -1
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +302 -67
- package/dist/node.js.map +1 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/shared-worker-client.d.ts +4 -0
- package/dist/shared-worker-client.d.ts.map +1 -1
- package/dist/shared-worker.d.ts.map +1 -1
- package/dist/snappy.d.ts +3 -0
- package/dist/snappy.d.ts.map +1 -0
- package/dist/sql-parser.d.ts.map +1 -1
- package/dist/types.d.ts +33 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/worker-api.d.ts +19 -6
- package/dist/worker-api.d.ts.map +1 -1
- package/dist/worker-client.d.ts +7 -2
- package/dist/worker-client.d.ts.map +1 -1
- package/dist/worker-protocol.d.ts +18 -3
- package/dist/worker-protocol.d.ts.map +1 -1
- package/dist/worker-server.d.ts.map +1 -1
- package/dist/worker.d.ts +33 -4
- package/dist/worker.d.ts.map +1 -1
- package/package.json +6 -5
- package/dist/assets/browser-worker-DP6Mjz7c.js +0 -4
- package/dist/assets/browser-worker-DP6Mjz7c.js.map +0 -1
- package/dist/assets/shared-worker-DKzhauVf.js +0 -2
- package/dist/assets/shared-worker-DKzhauVf.js.map +0 -1
- package/dist/chunks/indexing-BzmFhWXs.js.map +0 -1
- package/dist/engine/snippets/moyodb-engine-cff0a0e2441a7a8f/js/opfs_shim.js +0 -513
- /package/dist/engine/snippets/{moyodb-engine-8ebe6af0cc2e81b7 → moyodb-engine-1d53039816cbc2f7}/js/opfs_shim.js +0 -0
package/dist/node-engine.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { At as packedBinaryListBytes, Bt as unpackPackedOptionalValues, Et as isWorkerProtocolRequestMessage, F as utf8Encode, Ft as serializeWorkerError, Gt as isRecord, Ht as workerProtocolError, It as unpackPackedBatchOpKeys, Lt as unpackPackedBatchOps, Mt as packedScanRows, Pt as prepareWorkerResponsePayload, Rt as unpackPackedBinaryList, S as toPublicIndexDefinitions, St as isWorkerProtocolEnvelope, Tt as isWorkerProtocolRequestBatchMessage, Ut as clampTimerDelayMs, Vt as unpackPackedScanRows, _ as indexRangeToPhysicalRange, _t as decodeWorkerCommandPayload, a as createIndexKeyExtractor, b as normalizeIndexDefinitions, bt as isAutocommitCommand, c as decodeIndexedDocument, d as encodeIndexMetadataValue, dt as WORKER_PROTOCOL_READY, f as extractLogicalIndexKey, g as indexKeyExactRange, gt as captureWorkerPayload, ht as WORKER_PROTOCOL_RESPONSE_BATCH, i as compareNormalizedIndexDefinitions, jt as packedOptionalValues, kt as packedBatchOpsBytes, l as encodeIndexEntryKey, m as findIndexDefinition, mt as WORKER_PROTOCOL_RESPONSE, o as decodeIndexEntryKey, p as extractLogicalIndexKeyFromDocument, r as cloneNormalizedIndexDefinitions, s as decodeIndexMetadataValue, t as INDEX_METADATA_STORE, u as encodeIndexMetadataKey, xt as isWorkerCommand, y as isInternalStoreName, zt as unpackPackedBinaryPairKeys } from "./chunks/indexing-B0duqlUs.js";
|
|
2
2
|
//#region src/worker-server.ts
|
|
3
3
|
/** The first argument identifies the transaction lane. */
|
|
4
4
|
var TRANSACTION_COMMANDS = /* @__PURE__ */ new Set([
|
|
@@ -52,10 +52,36 @@ var RequestScheduler = class {
|
|
|
52
52
|
#lanes = /* @__PURE__ */ new Map();
|
|
53
53
|
#inFlight = /* @__PURE__ */ new Set();
|
|
54
54
|
#barrier = Promise.resolve();
|
|
55
|
+
#cursorLanes = /* @__PURE__ */ new Map();
|
|
55
56
|
schedule(command, args, task) {
|
|
56
57
|
if (UNSCHEDULED_COMMANDS.has(command)) return task();
|
|
57
|
-
if (EXCLUSIVE_COMMANDS.has(command))
|
|
58
|
-
|
|
58
|
+
if (EXCLUSIVE_COMMANDS.has(command)) {
|
|
59
|
+
if (this.#cursorLanes.size === 0) return this.#runExclusive(task);
|
|
60
|
+
return this.#runExclusive(async () => {
|
|
61
|
+
const result = await task();
|
|
62
|
+
this.#cursorLanes.clear();
|
|
63
|
+
return result;
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
if (command !== "scanPage" && command !== "closeCursor" && !((command === "commit" || command === "rollback") && this.#cursorLanes.size > 0)) return this.#runShared(laneFor(command, args), task);
|
|
67
|
+
const request = command === "scanPage" && isRecord(args[0]) ? args[0] : null;
|
|
68
|
+
const cursorId = request?.cursorId ?? (command === "closeCursor" ? args[0] : void 0);
|
|
69
|
+
const lane = (typeof cursorId === "number" ? this.#cursorLanes.get(cursorId) : void 0) ?? (request && typeof request.txId === "number" ? request.txId : typeof cursorId === "number" ? `cursor:${cursorId}` : laneFor(command, args));
|
|
70
|
+
return this.#runShared(lane, async () => {
|
|
71
|
+
try {
|
|
72
|
+
const result = await task();
|
|
73
|
+
if (request && isRecord(result)) {
|
|
74
|
+
if (typeof cursorId === "number") this.#cursorLanes.delete(cursorId);
|
|
75
|
+
if (typeof result.cursorId === "number") this.#cursorLanes.set(result.cursorId, lane ?? `cursor:${result.cursorId}`);
|
|
76
|
+
}
|
|
77
|
+
return result;
|
|
78
|
+
} finally {
|
|
79
|
+
if (command === "closeCursor" && typeof cursorId === "number") this.#cursorLanes.delete(cursorId);
|
|
80
|
+
if (command === "commit" || command === "rollback") {
|
|
81
|
+
for (const [cursor, ownerLane] of this.#cursorLanes) if (ownerLane === args[0]) this.#cursorLanes.delete(cursor);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
});
|
|
59
85
|
}
|
|
60
86
|
#runShared(lane, task) {
|
|
61
87
|
const barrier = this.#barrier;
|
|
@@ -291,6 +317,146 @@ function postWorkerResponse(scope, response, transfer = []) {
|
|
|
291
317
|
scope.postMessage(response);
|
|
292
318
|
}
|
|
293
319
|
//#endregion
|
|
320
|
+
//#region src/snappy.ts
|
|
321
|
+
var BLOCK_SIZE = 65536;
|
|
322
|
+
var HASH_SIZE = 16384;
|
|
323
|
+
var MAX_LENGTH = 4294967295;
|
|
324
|
+
var MATCH_TABLE = new Uint32Array(HASH_SIZE);
|
|
325
|
+
function corrupt(message) {
|
|
326
|
+
const error = /* @__PURE__ */ new Error(`invalid Snappy block: ${message}`);
|
|
327
|
+
error.name = "CorruptionError";
|
|
328
|
+
return error;
|
|
329
|
+
}
|
|
330
|
+
function word(bytes, offset) {
|
|
331
|
+
return (bytes[offset] | bytes[offset + 1] << 8 | bytes[offset + 2] << 16 | bytes[offset + 3] << 24) >>> 0;
|
|
332
|
+
}
|
|
333
|
+
function hash(value, bits) {
|
|
334
|
+
return Math.imul(value, 2654435761) >>> 32 - bits;
|
|
335
|
+
}
|
|
336
|
+
function encodeSnappy(input) {
|
|
337
|
+
if (input.byteLength > MAX_LENGTH) throw new RangeError("Snappy input exceeds the 32-bit length limit");
|
|
338
|
+
const output = new Uint8Array(input.byteLength + Math.ceil(input.byteLength / 6) + 32);
|
|
339
|
+
let written = 0;
|
|
340
|
+
let remainingLength = input.byteLength;
|
|
341
|
+
while (remainingLength >= 128) {
|
|
342
|
+
output[written++] = remainingLength & 127 | 128;
|
|
343
|
+
remainingLength = Math.floor(remainingLength / 128);
|
|
344
|
+
}
|
|
345
|
+
output[written++] = remainingLength;
|
|
346
|
+
const literal = (start, end) => {
|
|
347
|
+
if (start === end) return;
|
|
348
|
+
let length = end - start - 1;
|
|
349
|
+
if (length < 60) output[written++] = length << 2;
|
|
350
|
+
else {
|
|
351
|
+
const lengthBytes = length <= 255 ? 1 : length <= 65535 ? 2 : length <= 16777215 ? 3 : 4;
|
|
352
|
+
output[written++] = 59 + lengthBytes << 2;
|
|
353
|
+
for (let index = 0; index < lengthBytes; index += 1) {
|
|
354
|
+
output[written++] = length & 255;
|
|
355
|
+
length >>>= 8;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
output.set(input.subarray(start, end), written);
|
|
359
|
+
written += end - start;
|
|
360
|
+
};
|
|
361
|
+
const copy = (offset, length) => {
|
|
362
|
+
while (length > 0) {
|
|
363
|
+
const count = Math.min(length, 64);
|
|
364
|
+
if (count >= 4 && count <= 11 && offset < 2048) {
|
|
365
|
+
output[written++] = 1 | count - 4 << 2 | offset >>> 8 << 5;
|
|
366
|
+
output[written++] = offset & 255;
|
|
367
|
+
} else {
|
|
368
|
+
output[written++] = 2 | count - 1 << 2;
|
|
369
|
+
output[written++] = offset & 255;
|
|
370
|
+
output[written++] = offset >>> 8;
|
|
371
|
+
}
|
|
372
|
+
length -= count;
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
let hashBits = 8;
|
|
376
|
+
while (1 << hashBits < Math.min(input.byteLength, HASH_SIZE)) hashBits += 1;
|
|
377
|
+
const tableSize = 1 << hashBits;
|
|
378
|
+
const table = MATCH_TABLE;
|
|
379
|
+
for (let blockStart = 0; blockStart < input.byteLength; blockStart += BLOCK_SIZE) {
|
|
380
|
+
const blockEnd = Math.min(blockStart + BLOCK_SIZE, input.byteLength);
|
|
381
|
+
table.fill(0, 0, tableSize);
|
|
382
|
+
let position = blockStart;
|
|
383
|
+
let literalStart = blockStart;
|
|
384
|
+
let misses = 0;
|
|
385
|
+
while (position + 4 <= blockEnd) {
|
|
386
|
+
const current = word(input, position);
|
|
387
|
+
const slot = hash(current, hashBits);
|
|
388
|
+
const previous = table[slot];
|
|
389
|
+
table[slot] = position - blockStart + 1;
|
|
390
|
+
const candidate = blockStart + previous - 1;
|
|
391
|
+
if (previous === 0 || current !== word(input, candidate)) {
|
|
392
|
+
misses += 1;
|
|
393
|
+
position += 1 + Math.min(misses >>> 5, 64);
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
literal(literalStart, position);
|
|
397
|
+
const matchStart = position;
|
|
398
|
+
position += 4;
|
|
399
|
+
while (position < blockEnd && input[position] === input[candidate + position - matchStart]) position += 1;
|
|
400
|
+
copy(matchStart - candidate, position - matchStart);
|
|
401
|
+
literalStart = position;
|
|
402
|
+
misses = 0;
|
|
403
|
+
if (position + 4 <= blockEnd) table[hash(word(input, position - 1), hashBits)] = position - blockStart;
|
|
404
|
+
}
|
|
405
|
+
literal(literalStart, blockEnd);
|
|
406
|
+
}
|
|
407
|
+
return output.subarray(0, written);
|
|
408
|
+
}
|
|
409
|
+
function decodeSnappy(input, maxOutputBytes, expectedOutputBytes) {
|
|
410
|
+
if (!Number.isSafeInteger(maxOutputBytes) || maxOutputBytes < 0 || maxOutputBytes > MAX_LENGTH) throw new RangeError("invalid Snappy output limit");
|
|
411
|
+
let read = 0;
|
|
412
|
+
let length = 0;
|
|
413
|
+
for (let index = 0;; index += 1) {
|
|
414
|
+
if (read >= input.byteLength || index === 5) throw corrupt("truncated or invalid length");
|
|
415
|
+
const byte = input[read++];
|
|
416
|
+
if (index === 4 && byte > 15) throw corrupt("length exceeds 32 bits");
|
|
417
|
+
length += (byte & 127) * 2 ** (index * 7);
|
|
418
|
+
if (byte < 128) break;
|
|
419
|
+
}
|
|
420
|
+
if (length > maxOutputBytes) throw corrupt(`output exceeds ${maxOutputBytes} byte limit`);
|
|
421
|
+
if (expectedOutputBytes !== void 0 && length !== expectedOutputBytes) throw corrupt(`length mismatch: expected ${expectedOutputBytes} bytes, got ${length}`);
|
|
422
|
+
const output = new Uint8Array(length);
|
|
423
|
+
let written = 0;
|
|
424
|
+
while (read < input.byteLength) {
|
|
425
|
+
const tag = input[read++];
|
|
426
|
+
const kind = tag & 3;
|
|
427
|
+
if (kind === 0) {
|
|
428
|
+
let count = tag >>> 2;
|
|
429
|
+
if (count < 60) count += 1;
|
|
430
|
+
else {
|
|
431
|
+
const lengthBytes = count - 59;
|
|
432
|
+
if (lengthBytes > input.byteLength - read) throw corrupt("truncated literal length");
|
|
433
|
+
count = 1;
|
|
434
|
+
for (let index = 0; index < lengthBytes; index += 1) count += input[read++] * 2 ** (index * 8);
|
|
435
|
+
}
|
|
436
|
+
if (count > input.byteLength - read || count > length - written) throw corrupt("literal exceeds input or output");
|
|
437
|
+
output.set(input.subarray(read, read + count), written);
|
|
438
|
+
read += count;
|
|
439
|
+
written += count;
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
const offsetBytes = kind === 1 ? 1 : kind === 2 ? 2 : 4;
|
|
443
|
+
if (offsetBytes > input.byteLength - read) throw corrupt("truncated copy offset");
|
|
444
|
+
const count = kind === 1 ? 4 + (tag >>> 2 & 7) : 1 + (tag >>> 2);
|
|
445
|
+
let offset = kind === 1 ? (tag & 224) << 3 : 0;
|
|
446
|
+
for (let index = 0; index < offsetBytes; index += 1) offset += input[read++] * 2 ** (index * 8);
|
|
447
|
+
if (offset === 0 || offset > written || count > length - written) throw corrupt("copy exceeds output or has an invalid offset");
|
|
448
|
+
let copied = 0;
|
|
449
|
+
while (copied < count) {
|
|
450
|
+
const chunk = Math.min(offset + copied, count - copied);
|
|
451
|
+
output.copyWithin(written + copied, written - offset, written - offset + chunk);
|
|
452
|
+
copied += chunk;
|
|
453
|
+
}
|
|
454
|
+
written += count;
|
|
455
|
+
}
|
|
456
|
+
if (written !== length) throw corrupt(`length mismatch: expected ${length} bytes, got ${written}`);
|
|
457
|
+
return output;
|
|
458
|
+
}
|
|
459
|
+
//#endregion
|
|
294
460
|
//#region src/compression.ts
|
|
295
461
|
var STORE_RECORD_MAGIC = utf8Encode("BDBZVAL1");
|
|
296
462
|
var SNAPSHOT_EXPORT_MAGIC = utf8Encode("BDBZSNP1");
|
|
@@ -299,18 +465,27 @@ var ENVELOPE_HEADER_SIZE = 18;
|
|
|
299
465
|
var COMPRESSION_TAG_NONE = 0;
|
|
300
466
|
var COMPRESSION_TAG_GZIP = 1;
|
|
301
467
|
var COMPRESSION_TAG_DEFLATE = 2;
|
|
468
|
+
var COMPRESSION_TAG_SNAPPY = 3;
|
|
302
469
|
var STORE_FLAG_COMPRESSION_SHIFT = 2;
|
|
303
470
|
var STORE_FLAG_COMPRESSION_MASK = 12;
|
|
471
|
+
var STORE_VALUE_COMPRESSION_PREFLIGHT_WINDOW_BYTES = 1024;
|
|
304
472
|
var MAX_DECOMPRESSED_STORE_VALUE_BYTES = 8388608;
|
|
305
473
|
var MAX_DECOMPRESSED_SNAPSHOT_BYTES = 268435456;
|
|
306
474
|
var CRC32_TABLE = (() => {
|
|
307
|
-
const table = /* @__PURE__ */ new Uint32Array(
|
|
475
|
+
const table = /* @__PURE__ */ new Uint32Array(2048);
|
|
308
476
|
for (let index = 0; index < 256; index += 1) {
|
|
309
477
|
let crc = index;
|
|
310
478
|
for (let bit = 0; bit < 8; bit += 1) if ((crc & 1) !== 0) crc = crc >>> 1 ^ 3988292384;
|
|
311
479
|
else crc >>>= 1;
|
|
312
480
|
table[index] = crc >>> 0;
|
|
313
481
|
}
|
|
482
|
+
for (let index = 0; index < 256; index += 1) {
|
|
483
|
+
let crc = table[index];
|
|
484
|
+
for (let slice = 1; slice < 8; slice += 1) {
|
|
485
|
+
crc = crc >>> 8 ^ table[crc & 255];
|
|
486
|
+
table[slice * 256 + index] = crc >>> 0;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
314
489
|
return table;
|
|
315
490
|
})();
|
|
316
491
|
function namedError(name, message) {
|
|
@@ -323,6 +498,7 @@ function compressionTag(kind) {
|
|
|
323
498
|
case false: return COMPRESSION_TAG_NONE;
|
|
324
499
|
case "gzip": return COMPRESSION_TAG_GZIP;
|
|
325
500
|
case "deflate": return COMPRESSION_TAG_DEFLATE;
|
|
501
|
+
case "snappy": return COMPRESSION_TAG_SNAPPY;
|
|
326
502
|
default: throw namedError("InternalError", `unsupported compression kind: ${String(kind)}`);
|
|
327
503
|
}
|
|
328
504
|
}
|
|
@@ -331,6 +507,7 @@ function compressionKindFromTag(tag) {
|
|
|
331
507
|
case COMPRESSION_TAG_NONE: return false;
|
|
332
508
|
case COMPRESSION_TAG_GZIP: return "gzip";
|
|
333
509
|
case COMPRESSION_TAG_DEFLATE: return "deflate";
|
|
510
|
+
case COMPRESSION_TAG_SNAPPY: return "snappy";
|
|
334
511
|
default: throw namedError("CorruptionError", `invalid compression tag: ${tag}`);
|
|
335
512
|
}
|
|
336
513
|
}
|
|
@@ -342,20 +519,79 @@ function ensureCompressionRuntime() {
|
|
|
342
519
|
}
|
|
343
520
|
function crc32(bytes) {
|
|
344
521
|
let crc = 4294967295;
|
|
345
|
-
|
|
522
|
+
let offset = 0;
|
|
523
|
+
const blockEnd = bytes.byteLength - 7;
|
|
524
|
+
for (; offset < blockEnd; offset += 8) {
|
|
525
|
+
const first = crc ^ bytes[offset] ^ bytes[offset + 1] << 8 ^ bytes[offset + 2] << 16 ^ bytes[offset + 3] << 24;
|
|
526
|
+
crc = CRC32_TABLE[1792 + (first & 255)] ^ CRC32_TABLE[1536 + (first >>> 8 & 255)] ^ CRC32_TABLE[1280 + (first >>> 16 & 255)] ^ CRC32_TABLE[1024 + (first >>> 24)] ^ CRC32_TABLE[768 + bytes[offset + 4]] ^ CRC32_TABLE[512 + bytes[offset + 5]] ^ CRC32_TABLE[256 + bytes[offset + 6]] ^ CRC32_TABLE[bytes[offset + 7]];
|
|
527
|
+
}
|
|
528
|
+
for (; offset < bytes.byteLength; offset += 1) crc = crc >>> 8 ^ CRC32_TABLE[(crc ^ bytes[offset]) & 255];
|
|
346
529
|
return (crc ^ 4294967295) >>> 0;
|
|
347
530
|
}
|
|
348
531
|
function buildEnvelope(magic, kindTag, rawLength, payload) {
|
|
349
532
|
const out = new Uint8Array(ENVELOPE_HEADER_SIZE + payload.byteLength);
|
|
350
|
-
out
|
|
351
|
-
out[8] = ENVELOPE_VERSION;
|
|
352
|
-
out[9] = kindTag;
|
|
353
|
-
const view = new DataView(out.buffer, out.byteOffset, ENVELOPE_HEADER_SIZE);
|
|
354
|
-
view.setUint32(10, rawLength, true);
|
|
355
|
-
view.setUint32(14, crc32(payload), true);
|
|
356
|
-
out.set(payload, ENVELOPE_HEADER_SIZE);
|
|
533
|
+
writeEnvelope(out, 0, magic, kindTag, rawLength, payload);
|
|
357
534
|
return out;
|
|
358
535
|
}
|
|
536
|
+
function writeEnvelope(out, offset, magic, kindTag, rawLength, payload) {
|
|
537
|
+
const checksum = crc32(payload);
|
|
538
|
+
out.set(payload, offset + ENVELOPE_HEADER_SIZE);
|
|
539
|
+
out.set(magic, offset);
|
|
540
|
+
out[offset + 8] = ENVELOPE_VERSION;
|
|
541
|
+
out[offset + 9] = kindTag;
|
|
542
|
+
const view = new DataView(out.buffer, out.byteOffset + offset, ENVELOPE_HEADER_SIZE);
|
|
543
|
+
view.setUint32(10, rawLength, true);
|
|
544
|
+
view.setUint32(14, checksum, true);
|
|
545
|
+
}
|
|
546
|
+
function compressionIsProfitable(rawLength, compressedLength) {
|
|
547
|
+
return (compressedLength + ENVELOPE_HEADER_SIZE) * 100 <= rawLength * 90;
|
|
548
|
+
}
|
|
549
|
+
function snappyPreflightIsProfitable(value) {
|
|
550
|
+
if (value.byteLength < 65536) return true;
|
|
551
|
+
let compressedLength = 0;
|
|
552
|
+
for (let index = 0; index < 3; index += 1) {
|
|
553
|
+
const offset = Math.floor((value.byteLength - STORE_VALUE_COMPRESSION_PREFLIGHT_WINDOW_BYTES) * index / 2);
|
|
554
|
+
compressedLength += encodeSnappy(value.subarray(offset, offset + STORE_VALUE_COMPRESSION_PREFLIGHT_WINDOW_BYTES)).byteLength;
|
|
555
|
+
}
|
|
556
|
+
return compressionIsProfitable(STORE_VALUE_COMPRESSION_PREFLIGHT_WINDOW_BYTES * 3, compressedLength);
|
|
557
|
+
}
|
|
558
|
+
function buildStoreRecord(input, compressed, compression) {
|
|
559
|
+
if (!compressionIsProfitable(input.byteLength, compressed.byteLength)) return buildEnvelope(STORE_RECORD_MAGIC, COMPRESSION_TAG_NONE, input.byteLength, input);
|
|
560
|
+
return buildEnvelope(STORE_RECORD_MAGIC, compressionTag(compression), input.byteLength, compressed);
|
|
561
|
+
}
|
|
562
|
+
function validateStoreValueLength(value) {
|
|
563
|
+
if (value.byteLength > MAX_DECOMPRESSED_STORE_VALUE_BYTES) throw namedError("ValueTooLargeError", `value has ${value.byteLength} bytes, exceeding the ${MAX_DECOMPRESSED_STORE_VALUE_BYTES} byte limit`);
|
|
564
|
+
}
|
|
565
|
+
function prepareFastStoreValueRecord(value, compression) {
|
|
566
|
+
if (compression === false) return {
|
|
567
|
+
payload: value,
|
|
568
|
+
rawLength: value.byteLength,
|
|
569
|
+
byteLength: value.byteLength,
|
|
570
|
+
kindTag: null
|
|
571
|
+
};
|
|
572
|
+
validateStoreValueLength(value);
|
|
573
|
+
let payload = value;
|
|
574
|
+
let kindTag = COMPRESSION_TAG_NONE;
|
|
575
|
+
if (value.byteLength >= 1024 && snappyPreflightIsProfitable(value)) {
|
|
576
|
+
const compressed = encodeSnappy(value);
|
|
577
|
+
if (compressionIsProfitable(value.byteLength, compressed.byteLength)) {
|
|
578
|
+
payload = compressed;
|
|
579
|
+
kindTag = COMPRESSION_TAG_SNAPPY;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
return {
|
|
583
|
+
payload,
|
|
584
|
+
rawLength: value.byteLength,
|
|
585
|
+
byteLength: ENVELOPE_HEADER_SIZE + payload.byteLength,
|
|
586
|
+
kindTag
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
function writePreparedStoreValueRecord(record, target, offset) {
|
|
590
|
+
if (!Number.isSafeInteger(offset) || offset < 0 || record.byteLength > target.byteLength - offset) throw namedError("InternalError", "prepared store value exceeds its destination");
|
|
591
|
+
if (record.kindTag === null) target.set(record.payload, offset);
|
|
592
|
+
else writeEnvelope(target, offset, STORE_RECORD_MAGIC, record.kindTag, record.rawLength, record.payload);
|
|
593
|
+
return offset + record.byteLength;
|
|
594
|
+
}
|
|
359
595
|
function copyBytes(bytes) {
|
|
360
596
|
const copy = new Uint8Array(bytes.byteLength);
|
|
361
597
|
copy.set(bytes);
|
|
@@ -402,6 +638,7 @@ async function compressBytes(data, kind) {
|
|
|
402
638
|
}
|
|
403
639
|
}
|
|
404
640
|
async function decompressBytes(data, kind, label, maxOutputBytes) {
|
|
641
|
+
if (kind === "snappy") return decodeSnappy(data, maxOutputBytes ?? MAX_DECOMPRESSED_STORE_VALUE_BYTES, maxOutputBytes);
|
|
405
642
|
ensureCompressionRuntime();
|
|
406
643
|
let stream;
|
|
407
644
|
try {
|
|
@@ -447,17 +684,22 @@ function compressionFromStoreFlags(flags) {
|
|
|
447
684
|
case 0: return false;
|
|
448
685
|
case 1: return "gzip";
|
|
449
686
|
case 2: return "deflate";
|
|
687
|
+
case 3: return "snappy";
|
|
450
688
|
default: throw namedError("CorruptionError", `invalid store compression flags: ${encoded}`);
|
|
451
689
|
}
|
|
452
690
|
}
|
|
453
691
|
async function encodeStoreValueRecord(value, compression) {
|
|
454
692
|
if (compression === false) return value;
|
|
455
|
-
if (
|
|
693
|
+
if (compression === "snappy") {
|
|
694
|
+
const record = prepareFastStoreValueRecord(value, compression);
|
|
695
|
+
const output = new Uint8Array(record.byteLength);
|
|
696
|
+
writePreparedStoreValueRecord(record, output, 0);
|
|
697
|
+
return output;
|
|
698
|
+
}
|
|
699
|
+
validateStoreValueLength(value);
|
|
456
700
|
if (value.byteLength < 1024) return buildEnvelope(STORE_RECORD_MAGIC, COMPRESSION_TAG_NONE, value.byteLength, value);
|
|
457
701
|
const input = copyBytes(value);
|
|
458
|
-
|
|
459
|
-
if (compressed.byteLength >= input.byteLength) return buildEnvelope(STORE_RECORD_MAGIC, COMPRESSION_TAG_NONE, input.byteLength, input);
|
|
460
|
-
return buildEnvelope(STORE_RECORD_MAGIC, compressionTag(compression), input.byteLength, compressed);
|
|
702
|
+
return buildStoreRecord(input, await compressBytes(input, compression), compression);
|
|
461
703
|
}
|
|
462
704
|
async function decodeStoreValueRecord(value, options) {
|
|
463
705
|
const header = readEnvelopeHeader(STORE_RECORD_MAGIC, value, options.strict);
|
|
@@ -492,6 +734,7 @@ async function decodeStoreValueRecord(value, options) {
|
|
|
492
734
|
}
|
|
493
735
|
async function wrapSnapshotWithCompression(snapshot, compression) {
|
|
494
736
|
if (compression === false) return snapshot;
|
|
737
|
+
if (!["gzip", "deflate"].includes(compression)) throw namedError("InternalError", `unsupported snapshot compression kind: ${String(compression)}`);
|
|
495
738
|
const input = copyBytes(snapshot);
|
|
496
739
|
const compressed = await compressBytes(input, compression);
|
|
497
740
|
return buildEnvelope(SNAPSHOT_EXPORT_MAGIC, compressionTag(compression), input.byteLength, compressed);
|
|
@@ -502,6 +745,7 @@ async function unwrapSnapshotCompression(snapshot) {
|
|
|
502
745
|
if (header.version !== ENVELOPE_VERSION) throw namedError("CorruptionError", `unsupported snapshot compression version: ${header.version}`);
|
|
503
746
|
const compression = compressionKindFromTag(header.kindTag);
|
|
504
747
|
if (compression === false) throw namedError("CorruptionError", "snapshot compression envelope cannot store kind \"none\"");
|
|
748
|
+
if (compression === "snappy") throw namedError("CorruptionError", `invalid snapshot compression tag: ${header.kindTag}`);
|
|
505
749
|
const payload = snapshot.subarray(ENVELOPE_HEADER_SIZE);
|
|
506
750
|
if (crc32(payload) !== header.payloadChecksum) throw namedError("CorruptionError", "snapshot compression checksum mismatch");
|
|
507
751
|
if (header.rawLength > MAX_DECOMPRESSED_SNAPSHOT_BYTES) throw namedError("CorruptionError", `compressed snapshot advertises ${header.rawLength} decompressed bytes, exceeding the ${MAX_DECOMPRESSED_SNAPSHOT_BYTES} byte limit`);
|
|
@@ -515,7 +759,9 @@ function toWasmU64(value) {
|
|
|
515
759
|
return BigInt(value);
|
|
516
760
|
}
|
|
517
761
|
function fromWasmU64(value) {
|
|
518
|
-
|
|
762
|
+
const number = typeof value === "bigint" ? Number(value) : value;
|
|
763
|
+
if (!Number.isSafeInteger(number) || number < 0) throw remoteError("SerializationError", "WASM integer exceeds the JavaScript safe integer range");
|
|
764
|
+
return number;
|
|
519
765
|
}
|
|
520
766
|
function fromOptionalWasmU64(value) {
|
|
521
767
|
return value === null || value === void 0 ? null : fromWasmU64(value);
|
|
@@ -539,7 +785,11 @@ function toWasmRange(range) {
|
|
|
539
785
|
var EMPTY_RANGE = {};
|
|
540
786
|
var EMPTY_VALUE = /* @__PURE__ */ new Uint8Array(0);
|
|
541
787
|
var MAX_ENGINE_KEY_BYTES = 1024;
|
|
788
|
+
var MAX_PACKED_BYTES = 4294967295;
|
|
789
|
+
var INDEX_PUT_BATCH_ROWS = 64;
|
|
790
|
+
var INDEX_PUT_BATCH_BYTES = 4194304;
|
|
542
791
|
var INDEX_SCAN_PAGE_ROWS = 256;
|
|
792
|
+
var INDEX_SCAN_PAGE_BYTES = 8389662;
|
|
543
793
|
var INDEX_SCAN_RAW_CHUNK_ROWS = 512;
|
|
544
794
|
var RECONCILE_DOCUMENT_CACHE_BYTES = 4194304;
|
|
545
795
|
var RECONCILE_DOCUMENT_CACHE_ROWS = 4096;
|
|
@@ -657,6 +907,32 @@ function bytesEqual(left, right) {
|
|
|
657
907
|
for (let index = 0; index < left.byteLength; index += 1) if (left[index] !== right[index]) return false;
|
|
658
908
|
return true;
|
|
659
909
|
}
|
|
910
|
+
function copyRange(range) {
|
|
911
|
+
const copy = toWasmRange(range);
|
|
912
|
+
for (const bound of [
|
|
913
|
+
"gt",
|
|
914
|
+
"gte",
|
|
915
|
+
"lt",
|
|
916
|
+
"lte"
|
|
917
|
+
]) {
|
|
918
|
+
const value = copy[bound];
|
|
919
|
+
if (value !== void 0) copy[bound] = value.slice();
|
|
920
|
+
}
|
|
921
|
+
return copy;
|
|
922
|
+
}
|
|
923
|
+
function rangesEqual(left, right) {
|
|
924
|
+
for (const bound of [
|
|
925
|
+
"gt",
|
|
926
|
+
"gte",
|
|
927
|
+
"lt",
|
|
928
|
+
"lte"
|
|
929
|
+
]) {
|
|
930
|
+
const a = left[bound];
|
|
931
|
+
const b = right[bound];
|
|
932
|
+
if (a === void 0 ? b !== void 0 : b === void 0 || !bytesEqual(a, b)) return false;
|
|
933
|
+
}
|
|
934
|
+
return left.reverse === right.reverse && left.limit === right.limit;
|
|
935
|
+
}
|
|
660
936
|
function normalizeWasmBytes(value) {
|
|
661
937
|
if (value instanceof Uint8Array) return value.byteOffset === 0 && value.byteLength === value.buffer.byteLength ? value : value.slice();
|
|
662
938
|
return new Uint8Array(value);
|
|
@@ -763,7 +1039,7 @@ var OwnershipLease = class {
|
|
|
763
1039
|
return;
|
|
764
1040
|
}
|
|
765
1041
|
const controller = new AbortController();
|
|
766
|
-
const timeout = setTimeout(() => controller.abort(), ownerWaitMs);
|
|
1042
|
+
const timeout = setTimeout(() => controller.abort(), clampTimerDelayMs(ownerWaitMs));
|
|
767
1043
|
let acquiredResolve = null;
|
|
768
1044
|
const acquiredPromise = new Promise((resolve) => {
|
|
769
1045
|
acquiredResolve = resolve;
|
|
@@ -806,6 +1082,8 @@ var DbWorker = class {
|
|
|
806
1082
|
persistenceBridge;
|
|
807
1083
|
txChanges = /* @__PURE__ */ new Map();
|
|
808
1084
|
txModes = /* @__PURE__ */ new Map();
|
|
1085
|
+
primaryCursors = /* @__PURE__ */ new Map();
|
|
1086
|
+
nextPrimaryCursorId = 1;
|
|
809
1087
|
/**
|
|
810
1088
|
* Shared committed schema at begin. Reconciliation replaces the array;
|
|
811
1089
|
* it never mutates the shared schema.
|
|
@@ -1004,7 +1282,7 @@ var DbWorker = class {
|
|
|
1004
1282
|
return result;
|
|
1005
1283
|
}
|
|
1006
1284
|
createStore(txId, name, options = {}) {
|
|
1007
|
-
const compression = options.compression ??
|
|
1285
|
+
const compression = isInternalStoreName(name) ? false : options.compression ?? "snappy";
|
|
1008
1286
|
this.txEnsuredRawStores.get(txId)?.delete(name);
|
|
1009
1287
|
this.withEngine((engine) => engine.create_store(toWasmU64(txId), name, { compression }));
|
|
1010
1288
|
this.recordStoreCompressionCreate(txId, name, compression);
|
|
@@ -1057,42 +1335,12 @@ var DbWorker = class {
|
|
|
1057
1335
|
this.recordPutChange(txId, store, key, baselineExists);
|
|
1058
1336
|
return;
|
|
1059
1337
|
}
|
|
1060
|
-
|
|
1061
|
-
const oldKeyFor = oldValue === null ? null : createIndexKeyExtractor(oldValue);
|
|
1062
|
-
const newKeyFor = createIndexKeyExtractor(value);
|
|
1063
|
-
const plans = defs.map((def) => {
|
|
1064
|
-
return {
|
|
1065
|
-
def,
|
|
1066
|
-
oldLogicalKey: oldKeyFor === null ? null : oldKeyFor(def),
|
|
1067
|
-
newLogicalKey: newKeyFor(def),
|
|
1068
|
-
newPhysicalKey: null
|
|
1069
|
-
};
|
|
1070
|
-
});
|
|
1071
|
-
this.ensureInternalStoresForDefinitions(txId, defs);
|
|
1072
|
-
for (const plan of plans) {
|
|
1073
|
-
if (plan.newLogicalKey === null) continue;
|
|
1074
|
-
plan.newPhysicalKey = this.buildIndexEntryKey(plan.def, plan.newLogicalKey, key);
|
|
1075
|
-
if (plan.oldLogicalKey !== null && bytesEqual(plan.oldLogicalKey, plan.newLogicalKey)) continue;
|
|
1076
|
-
await this.assertUniqueIndexAvailability(txId, plan.def, plan.newLogicalKey, key);
|
|
1077
|
-
}
|
|
1078
|
-
const storedValue = await this.encodeStoreValueForWrite(txId, store, value);
|
|
1079
|
-
const baselineExists = this.withEngine((engine) => engine.put(toWasmU64(txId), store, key, storedValue, toWasmPutOptions(options)));
|
|
1080
|
-
for (const plan of plans) {
|
|
1081
|
-
if (plan.oldLogicalKey === null) continue;
|
|
1082
|
-
if (plan.newLogicalKey !== null && bytesEqual(plan.oldLogicalKey, plan.newLogicalKey)) continue;
|
|
1083
|
-
this.deleteIndexEntryIfPresent(txId, plan.def, plan.oldLogicalKey, key);
|
|
1084
|
-
}
|
|
1085
|
-
for (const plan of plans) {
|
|
1086
|
-
if (plan.newLogicalKey === null) continue;
|
|
1087
|
-
if (plan.oldLogicalKey !== null && bytesEqual(plan.oldLogicalKey, plan.newLogicalKey)) continue;
|
|
1088
|
-
if (plan.newPhysicalKey === null) throw remoteError("InternalError", "missing index entry key");
|
|
1089
|
-
this.putIndexEntry(txId, plan.def, plan.newPhysicalKey, EMPTY_VALUE);
|
|
1090
|
-
}
|
|
1091
|
-
this.recordPutChange(txId, store, key, baselineExists);
|
|
1338
|
+
await this.putIndexedMany(txId, store, [key, value], defs, options);
|
|
1092
1339
|
}
|
|
1093
1340
|
async putMany(txId, store, entries, options = {}) {
|
|
1094
1341
|
if (entries.length === 0) return;
|
|
1095
|
-
|
|
1342
|
+
const defs = this.indexesForStoreInTx(txId, store);
|
|
1343
|
+
if (defs.length === 0) {
|
|
1096
1344
|
const storedEntries = await this.encodeEntriesForWrite(txId, store, entries);
|
|
1097
1345
|
let baselines;
|
|
1098
1346
|
try {
|
|
@@ -1102,28 +1350,39 @@ var DbWorker = class {
|
|
|
1102
1350
|
this.recordPutChanges(txId, store, entries, partial);
|
|
1103
1351
|
throw error;
|
|
1104
1352
|
}
|
|
1105
|
-
if (baselines.length !== entries.length) throw
|
|
1353
|
+
if (baselines.length !== entries.length) throw outcomeCountMismatch("put_many", baselines.length, entries.length);
|
|
1106
1354
|
this.recordPutChanges(txId, store, entries, baselines);
|
|
1107
1355
|
return;
|
|
1108
1356
|
}
|
|
1109
|
-
|
|
1357
|
+
const items = new Array(entries.length * 2);
|
|
1358
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
1359
|
+
items[index * 2] = entries[index][0];
|
|
1360
|
+
items[index * 2 + 1] = entries[index][1];
|
|
1361
|
+
}
|
|
1362
|
+
await this.putIndexedMany(txId, store, items, defs, options);
|
|
1110
1363
|
}
|
|
1111
1364
|
async putManyPacked(txId, store, packedEntries, options = {}) {
|
|
1112
|
-
|
|
1113
|
-
|
|
1365
|
+
const defs = this.indexesForStoreInTx(txId, store);
|
|
1366
|
+
if (defs.length !== 0) {
|
|
1367
|
+
const items = unpackPackedBinaryList(packedEntries);
|
|
1368
|
+
if (items.length % 2 !== 0) throw remoteError("WorkerProtocolError", "packed putMany payload has an odd item count");
|
|
1369
|
+
await this.putIndexedMany(txId, store, items, defs, options);
|
|
1114
1370
|
return;
|
|
1115
1371
|
}
|
|
1116
|
-
const
|
|
1372
|
+
const items = this.storeCompressionForTx(txId, store) === false ? null : unpackPackedBinaryList(packedEntries);
|
|
1373
|
+
if (items !== null && items.length % 2 !== 0) throw remoteError("WorkerProtocolError", "packed putMany payload has an odd item count");
|
|
1374
|
+
const keys = items === null ? unpackPackedBinaryPairKeys(packedEntries) : items.filter((_item, index) => index % 2 === 0);
|
|
1117
1375
|
if (keys.length === 0) return;
|
|
1376
|
+
const storedEntries = items === null ? packedEntries : await this.encodePackedEntriesForWrite(txId, store, items, keys);
|
|
1118
1377
|
let baselines;
|
|
1119
1378
|
try {
|
|
1120
|
-
baselines = this.withEngine((engine) => engine.put_many_packed(toWasmU64(txId), store,
|
|
1379
|
+
baselines = this.withEngine((engine) => engine.put_many_packed(toWasmU64(txId), store, storedEntries, toWasmPutOptions(options)));
|
|
1121
1380
|
} catch (error) {
|
|
1122
1381
|
const partial = partialBooleanOutcomes(error);
|
|
1123
1382
|
this.recordPutKeyChanges(txId, store, keys, partial);
|
|
1124
1383
|
throw error;
|
|
1125
1384
|
}
|
|
1126
|
-
if (baselines.length !== keys.length) throw
|
|
1385
|
+
if (baselines.length !== keys.length) throw outcomeCountMismatch("put_many_packed", baselines.length, keys.length);
|
|
1127
1386
|
this.recordPutKeyChanges(txId, store, keys, baselines);
|
|
1128
1387
|
}
|
|
1129
1388
|
async delete(txId, store, key) {
|
|
@@ -1153,7 +1412,7 @@ var DbWorker = class {
|
|
|
1153
1412
|
for (let i = 0; i < partial.length && i < keys.length; i += 1) this.recordDeleteChange(txId, store, keys[i], partial[i]);
|
|
1154
1413
|
throw error;
|
|
1155
1414
|
}
|
|
1156
|
-
if (deleted.length !== keys.length) throw
|
|
1415
|
+
if (deleted.length !== keys.length) throw outcomeCountMismatch("delete_many", deleted.length, keys.length);
|
|
1157
1416
|
for (let i = 0; i < keys.length; i += 1) this.recordDeleteChange(txId, store, keys[i], deleted[i]);
|
|
1158
1417
|
return;
|
|
1159
1418
|
}
|
|
@@ -1175,7 +1434,7 @@ var DbWorker = class {
|
|
|
1175
1434
|
for (let i = 0; i < partial.length && i < keys.length; i += 1) this.recordDeleteChange(txId, store, keys[i], partial[i]);
|
|
1176
1435
|
throw error;
|
|
1177
1436
|
}
|
|
1178
|
-
if (deleted.length !== keys.length) throw
|
|
1437
|
+
if (deleted.length !== keys.length) throw outcomeCountMismatch("delete_many_packed", deleted.length, keys.length);
|
|
1179
1438
|
for (let i = 0; i < keys.length; i += 1) this.recordDeleteChange(txId, store, keys[i], deleted[i] === 1);
|
|
1180
1439
|
}
|
|
1181
1440
|
async applyBatch(txId, store, ops) {
|
|
@@ -1189,7 +1448,7 @@ var DbWorker = class {
|
|
|
1189
1448
|
this.recordBatchOutcomes(txId, store, ops, partialBatchOutcomes(error));
|
|
1190
1449
|
throw error;
|
|
1191
1450
|
}
|
|
1192
|
-
if (outcomes.length !== ops.length) throw
|
|
1451
|
+
if (outcomes.length !== ops.length) throw outcomeCountMismatch("apply_batch", outcomes.length, ops.length);
|
|
1193
1452
|
this.recordBatchOutcomes(txId, store, ops, outcomes);
|
|
1194
1453
|
return;
|
|
1195
1454
|
}
|
|
@@ -1209,10 +1468,10 @@ var DbWorker = class {
|
|
|
1209
1468
|
try {
|
|
1210
1469
|
flags = this.withEngine((engine) => engine.apply_batch_packed(toWasmU64(txId), store, packedOps));
|
|
1211
1470
|
} catch (error) {
|
|
1212
|
-
this.
|
|
1471
|
+
this.recordBatchOutcomes(txId, store, ops, partialBatchOutcomes(error));
|
|
1213
1472
|
throw error;
|
|
1214
1473
|
}
|
|
1215
|
-
if (flags.length !== ops.length) throw
|
|
1474
|
+
if (flags.length !== ops.length) throw outcomeCountMismatch("apply_batch_packed", flags.length, ops.length);
|
|
1216
1475
|
for (let i = 0; i < ops.length; i += 1) {
|
|
1217
1476
|
const op = ops[i];
|
|
1218
1477
|
if (op.kind === "put") this.recordPutChange(txId, store, op.key, flags[i] === 1);
|
|
@@ -1222,9 +1481,82 @@ var DbWorker = class {
|
|
|
1222
1481
|
async scan(txId, store, range) {
|
|
1223
1482
|
return this.readStoreScan(txId, store, range);
|
|
1224
1483
|
}
|
|
1484
|
+
async scanPage(request) {
|
|
1485
|
+
if (!Number.isSafeInteger(request.maxRows) || request.maxRows <= 0 || request.maxRows > MAX_PACKED_BYTES || !Number.isSafeInteger(request.maxBytes) || request.maxBytes < 4 || request.maxBytes > MAX_PACKED_BYTES) throw remoteError("InvalidRangeError", "scan page budgets must be positive u32 integers; maxBytes must be at least 4");
|
|
1486
|
+
let cursorId = request.cursorId;
|
|
1487
|
+
let cursor = cursorId === void 0 ? void 0 : this.primaryCursors.get(cursorId);
|
|
1488
|
+
if (cursorId !== void 0) {
|
|
1489
|
+
if (!cursor) throw remoteError("TransactionClosedError", "scan cursor is closed");
|
|
1490
|
+
if (cursor.store !== request.store || cursor.callerTxId !== request.txId || !rangesEqual(cursor.range, request.range ?? EMPTY_RANGE)) throw remoteError("InvalidRangeError", "scan continuation does not match the cursor");
|
|
1491
|
+
} else {
|
|
1492
|
+
const ownsTx = request.txId === void 0;
|
|
1493
|
+
const txId = request.txId ?? await this.begin("readonly");
|
|
1494
|
+
const range = copyRange(request.range ?? EMPTY_RANGE);
|
|
1495
|
+
let engineId;
|
|
1496
|
+
try {
|
|
1497
|
+
engineId = this.withEngine((engine) => engine.open_scan_cursor(toWasmU64(txId), request.store, toWasmRange(range), false));
|
|
1498
|
+
} catch (error) {
|
|
1499
|
+
if (ownsTx) try {
|
|
1500
|
+
await this.rollback(txId);
|
|
1501
|
+
} catch {}
|
|
1502
|
+
throw error;
|
|
1503
|
+
}
|
|
1504
|
+
cursorId = this.nextPrimaryCursorId++;
|
|
1505
|
+
cursor = {
|
|
1506
|
+
engineId,
|
|
1507
|
+
txId,
|
|
1508
|
+
ownsTx,
|
|
1509
|
+
store: request.store,
|
|
1510
|
+
range,
|
|
1511
|
+
callerTxId: request.txId
|
|
1512
|
+
};
|
|
1513
|
+
this.primaryCursors.set(cursorId, cursor);
|
|
1514
|
+
}
|
|
1515
|
+
try {
|
|
1516
|
+
const page = this.withEngine((engine) => engine.scan_cursor_next(cursor.engineId, request.maxRows, request.maxBytes));
|
|
1517
|
+
const packet = normalizeWasmBytes(page.packet);
|
|
1518
|
+
const rowCount = packet.byteLength < 4 ? -1 : new DataView(packet.buffer, packet.byteOffset, 4).getUint32(0, true);
|
|
1519
|
+
if (rowCount !== page.rowCount || rowCount > request.maxRows || packet.byteLength > request.maxBytes) throw remoteError("CorruptionError", "scan cursor returned an invalid page budget or count");
|
|
1520
|
+
const rows = this.storeCompressionForTx(cursor.txId, cursor.store) !== false ? unpackPackedScanRows(packet) : null;
|
|
1521
|
+
let bytes = packet.byteLength;
|
|
1522
|
+
if (rows !== null) {
|
|
1523
|
+
bytes = 4;
|
|
1524
|
+
for (const row of rows) {
|
|
1525
|
+
row.value = await this.decodeStoreValue(cursor.txId, cursor.store, row.value);
|
|
1526
|
+
bytes += 8 + row.key.byteLength + row.value.byteLength;
|
|
1527
|
+
if (bytes > request.maxBytes) throw remoteError("ValueTooLargeError", "decoded scan page exceeds maxBytes");
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
if (page.exhausted) await this.closeCursor(cursorId);
|
|
1531
|
+
return {
|
|
1532
|
+
rows: rows ?? packedScanRows(packet),
|
|
1533
|
+
done: page.exhausted,
|
|
1534
|
+
bytes,
|
|
1535
|
+
...page.exhausted ? {} : { cursorId }
|
|
1536
|
+
};
|
|
1537
|
+
} catch (error) {
|
|
1538
|
+
try {
|
|
1539
|
+
await this.closeCursor(cursorId);
|
|
1540
|
+
} catch {}
|
|
1541
|
+
throw error;
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
async closeCursor(cursorId) {
|
|
1545
|
+
const cursor = this.primaryCursors.get(cursorId);
|
|
1546
|
+
if (!cursor) return;
|
|
1547
|
+
this.primaryCursors.delete(cursorId);
|
|
1548
|
+
try {
|
|
1549
|
+
this.withEngine((engine) => engine.close_scan_cursor(cursor.engineId));
|
|
1550
|
+
} finally {
|
|
1551
|
+
if (cursor.ownsTx) await this.rollback(cursor.txId);
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1225
1554
|
async getByIndex(txId, store, indexName, key) {
|
|
1226
1555
|
const def = this.resolveIndexDefinition(txId, store, indexName);
|
|
1227
|
-
const page = await this.loadVisibleIndexPage(txId, def,
|
|
1556
|
+
const page = await this.loadVisibleIndexPage(txId, def, {
|
|
1557
|
+
gte: key,
|
|
1558
|
+
lte: key
|
|
1559
|
+
}, null, 1);
|
|
1228
1560
|
return page.rows.length === 0 ? null : page.rows[0].value;
|
|
1229
1561
|
}
|
|
1230
1562
|
async scanByIndex(txId, store, indexName, range = {}) {
|
|
@@ -1241,10 +1573,11 @@ var DbWorker = class {
|
|
|
1241
1573
|
}
|
|
1242
1574
|
return rows;
|
|
1243
1575
|
}
|
|
1244
|
-
async scanByIndexPage(txId, store, indexName, range, cursor, limit) {
|
|
1576
|
+
async scanByIndexPage(txId, store, indexName, range, cursor, limit, maxBytes = INDEX_SCAN_PAGE_BYTES) {
|
|
1245
1577
|
if (!Number.isSafeInteger(limit) || limit <= 0) throw remoteError("InvalidRangeError", "index scan page limit must be a positive integer");
|
|
1578
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes < 4 || maxBytes > MAX_PACKED_BYTES) throw remoteError("InvalidRangeError", "index scan page maxBytes must be a u32 integer of at least 4");
|
|
1246
1579
|
const def = this.resolveIndexDefinition(txId, store, indexName);
|
|
1247
|
-
return this.loadVisibleIndexPage(txId, def, range, cursor, limit);
|
|
1580
|
+
return this.loadVisibleIndexPage(txId, def, range, cursor, limit, maxBytes);
|
|
1248
1581
|
}
|
|
1249
1582
|
getIndexes(txId) {
|
|
1250
1583
|
return Promise.resolve(toPublicIndexDefinitions(txId === void 0 ? this.loadCommittedIndexSchema() : this.indexSchemaForTx(txId)));
|
|
@@ -1274,7 +1607,7 @@ var DbWorker = class {
|
|
|
1274
1607
|
let rows;
|
|
1275
1608
|
if (sourceRows !== null) {
|
|
1276
1609
|
const visible = this.withEngine((engine) => engine.has_many(toWasmU64(txId), def.store, sourceRows.map((row) => row.key)));
|
|
1277
|
-
if (visible.length !== sourceRows.length) throw
|
|
1610
|
+
if (visible.length !== sourceRows.length) throw outcomeCountMismatch("has_many", visible.length, sourceRows.length);
|
|
1278
1611
|
rows = sourceRows.filter((_row, index) => visible[index]);
|
|
1279
1612
|
} else {
|
|
1280
1613
|
rows = await this.readStoreScan(txId, def.store, EMPTY_RANGE);
|
|
@@ -1366,12 +1699,9 @@ var DbWorker = class {
|
|
|
1366
1699
|
return await wrapSnapshotWithCompression(wrapSnapshotWithIndexDefinitions(normalizeWasmBytes(this.withEngine((engine) => engine.export_snapshot())), defs), options.compression ?? false);
|
|
1367
1700
|
}
|
|
1368
1701
|
async importSnapshot(data) {
|
|
1369
|
-
this.
|
|
1702
|
+
this.ensureNotInMaintenance();
|
|
1370
1703
|
const parsed = unwrapSnapshotWithIndexDefinitions(await unwrapSnapshotCompression(data));
|
|
1371
|
-
this.
|
|
1372
|
-
this.committedIndexes = [];
|
|
1373
|
-
this.committedStoreCompression = null;
|
|
1374
|
-
await this.applyCommittedIndexSchema(parsed.indexes);
|
|
1704
|
+
await this.runGenerationOperation("import", parsed);
|
|
1375
1705
|
}
|
|
1376
1706
|
async reset() {
|
|
1377
1707
|
const dbName = this.dbName;
|
|
@@ -1399,16 +1729,16 @@ var DbWorker = class {
|
|
|
1399
1729
|
}
|
|
1400
1730
|
}
|
|
1401
1731
|
async compact() {
|
|
1402
|
-
return this.
|
|
1732
|
+
return this.runGenerationOperation("compact");
|
|
1403
1733
|
}
|
|
1404
1734
|
async rebuild() {
|
|
1405
|
-
return this.
|
|
1735
|
+
return this.runGenerationOperation("rebuild");
|
|
1406
1736
|
}
|
|
1407
1737
|
stats() {
|
|
1408
1738
|
const stats = this.withEngine((engine) => engine.stats());
|
|
1409
1739
|
return Promise.resolve({
|
|
1410
1740
|
...stats,
|
|
1411
|
-
db_id:
|
|
1741
|
+
db_id: typeof stats.db_id === "bigint" ? Number(stats.db_id) : stats.db_id,
|
|
1412
1742
|
catalog_root_page_id: fromWasmU64(stats.catalog_root_page_id),
|
|
1413
1743
|
next_page_id: fromWasmU64(stats.next_page_id),
|
|
1414
1744
|
last_committed_txid: fromWasmU64(stats.last_committed_txid),
|
|
@@ -1469,7 +1799,7 @@ var DbWorker = class {
|
|
|
1469
1799
|
});
|
|
1470
1800
|
try {
|
|
1471
1801
|
await this.reconcileIndexes(txId, toPublicIndexDefinitions(normalized));
|
|
1472
|
-
this.withEngine((engine) => engine.commit_tx(toWasmU64(txId)), { allowDuringMaintenance: true });
|
|
1802
|
+
fromWasmU64(this.withEngine((engine) => engine.commit_tx(toWasmU64(txId)), { allowDuringMaintenance: true }));
|
|
1473
1803
|
this.committedIndexes = cloneNormalizedIndexDefinitions(normalized);
|
|
1474
1804
|
} catch (error) {
|
|
1475
1805
|
try {
|
|
@@ -1485,18 +1815,19 @@ var DbWorker = class {
|
|
|
1485
1815
|
* A failed swap may have reached storage; read the control file before cleanup.
|
|
1486
1816
|
* After publication, abandon the old engine to avoid a checkpoint into inactive files.
|
|
1487
1817
|
*/
|
|
1488
|
-
async
|
|
1818
|
+
async runGenerationOperation(operation, imported) {
|
|
1489
1819
|
const dbName = this.requireDbName();
|
|
1490
1820
|
if (this.maintenanceOperation) throw remoteError("DatabaseBusyError", `database ${dbName} is busy: ${this.maintenanceOperation} in progress`);
|
|
1491
1821
|
this.maintenanceOperation = operation;
|
|
1492
1822
|
const start = performance.now();
|
|
1493
|
-
|
|
1823
|
+
let wasm = null;
|
|
1494
1824
|
let rebuiltEngine = null;
|
|
1495
1825
|
let generationName = null;
|
|
1496
1826
|
let published = false;
|
|
1497
1827
|
try {
|
|
1828
|
+
wasm = await this.loadWasm();
|
|
1498
1829
|
const sizeBefore = await wasm.dbDirectorySize(dbName);
|
|
1499
|
-
const defs = cloneNormalizedIndexDefinitions(this.loadCommittedIndexSchema());
|
|
1830
|
+
const defs = cloneNormalizedIndexDefinitions(imported?.indexes ?? this.loadCommittedIndexSchema());
|
|
1500
1831
|
this.rollbackAllTransactions({ allowDuringMaintenance: true });
|
|
1501
1832
|
const expectedGeneration = await wasm.readActiveGeneration(dbName);
|
|
1502
1833
|
generationName = (await wasm.prepareRebuildTarget(dbName)).generationName;
|
|
@@ -1506,8 +1837,8 @@ var DbWorker = class {
|
|
|
1506
1837
|
cache_pages: this.currentCachePages()
|
|
1507
1838
|
});
|
|
1508
1839
|
const target = rebuiltEngine;
|
|
1509
|
-
this.withEngine((engine) => operation === "rebuild" ? engine.compact_into_skipping_stores(target, [...defs.map((def) => def.internalStore), INDEX_METADATA_STORE]) : engine.compact_into(target), { allowDuringMaintenance: true });
|
|
1510
|
-
if (operation === "rebuild" && defs.length > 0) {
|
|
1840
|
+
this.withEngine((engine) => imported ? fromWasmU64(engine.import_snapshot_into(target, imported.snapshot)) : operation === "rebuild" ? engine.compact_into_skipping_stores(target, [...defs.map((def) => def.internalStore), INDEX_METADATA_STORE]) : engine.compact_into(target), { allowDuringMaintenance: true });
|
|
1841
|
+
if ((operation === "rebuild" || imported) && defs.length > 0) {
|
|
1511
1842
|
const previousEngine = this.engine;
|
|
1512
1843
|
const previousMaintenance = this.maintenanceOperation;
|
|
1513
1844
|
this.engine = target;
|
|
@@ -1540,7 +1871,7 @@ var DbWorker = class {
|
|
|
1540
1871
|
try {
|
|
1541
1872
|
await wasm.cleanupInactiveEntries(dbName);
|
|
1542
1873
|
} catch {}
|
|
1543
|
-
const sizeAfter = await wasm.dbDirectorySize(dbName);
|
|
1874
|
+
const sizeAfter = imported ? sizeBefore : await wasm.dbDirectorySize(dbName);
|
|
1544
1875
|
return {
|
|
1545
1876
|
sizeBefore,
|
|
1546
1877
|
sizeAfter,
|
|
@@ -1554,7 +1885,7 @@ var DbWorker = class {
|
|
|
1554
1885
|
try {
|
|
1555
1886
|
rebuiltEngine?.abandon();
|
|
1556
1887
|
} catch {}
|
|
1557
|
-
if (generationName !== null) {
|
|
1888
|
+
if (generationName !== null && wasm !== null) {
|
|
1558
1889
|
const active = await readActiveGenerationOrNull(wasm, dbName);
|
|
1559
1890
|
if (active !== void 0 && active !== generationName) try {
|
|
1560
1891
|
await wasm.cleanupInactiveEntries(dbName);
|
|
@@ -1587,6 +1918,12 @@ var DbWorker = class {
|
|
|
1587
1918
|
};
|
|
1588
1919
|
}
|
|
1589
1920
|
cleanupTxState(txId) {
|
|
1921
|
+
for (const [cursorId, cursor] of this.primaryCursors) if (cursor.txId === txId) {
|
|
1922
|
+
this.primaryCursors.delete(cursorId);
|
|
1923
|
+
try {
|
|
1924
|
+
this.engine?.close_scan_cursor(cursor.engineId);
|
|
1925
|
+
} catch {}
|
|
1926
|
+
}
|
|
1590
1927
|
this.txChanges.delete(txId);
|
|
1591
1928
|
this.txModes.delete(txId);
|
|
1592
1929
|
this.txIndexSchemas.delete(txId);
|
|
@@ -1658,14 +1995,6 @@ var DbWorker = class {
|
|
|
1658
1995
|
else if (op.kind === "delete" && outcome.kind === "delete") this.recordDeleteChange(txId, store, op.key, outcome.deleted);
|
|
1659
1996
|
}
|
|
1660
1997
|
}
|
|
1661
|
-
recordBatchKeyOutcomes(txId, store, ops, outcomes) {
|
|
1662
|
-
for (let i = 0; i < outcomes.length && i < ops.length; i += 1) {
|
|
1663
|
-
const op = ops[i];
|
|
1664
|
-
const outcome = outcomes[i];
|
|
1665
|
-
if (op.kind === "put" && outcome.kind === "put") this.recordPutChange(txId, store, op.key, outcome.baselineExists);
|
|
1666
|
-
else if (op.kind === "delete" && outcome.kind === "delete") this.recordDeleteChange(txId, store, op.key, outcome.deleted);
|
|
1667
|
-
}
|
|
1668
|
-
}
|
|
1669
1998
|
finalizeTrackedChanges(tracked) {
|
|
1670
1999
|
const stores = [];
|
|
1671
2000
|
for (const [store, trackedStore] of tracked) {
|
|
@@ -1752,6 +2081,130 @@ var DbWorker = class {
|
|
|
1752
2081
|
for (const [key, value] of entries) storedEntries.push([key, await encodeStoreValueRecord(value, compression)]);
|
|
1753
2082
|
return storedEntries;
|
|
1754
2083
|
}
|
|
2084
|
+
async encodePackedEntriesForWrite(txId, store, items, keys) {
|
|
2085
|
+
const records = new Array(items.length / 2);
|
|
2086
|
+
for (let index = 0; index < records.length; index += 1) {
|
|
2087
|
+
const value = this.preparePutValue(txId, store, items[index * 2 + 1]);
|
|
2088
|
+
records[index] = value instanceof Promise ? await value : value;
|
|
2089
|
+
}
|
|
2090
|
+
return this.packPreparedPutEntries(keys, records);
|
|
2091
|
+
}
|
|
2092
|
+
preparePutValue(txId, store, value) {
|
|
2093
|
+
const compression = this.storeCompressionForTx(txId, store);
|
|
2094
|
+
return compression === "snappy" || compression === false ? prepareFastStoreValueRecord(value, compression) : encodeStoreValueRecord(value, compression);
|
|
2095
|
+
}
|
|
2096
|
+
packPreparedPutEntries(keys, records) {
|
|
2097
|
+
let total = 4 + keys.length * 8;
|
|
2098
|
+
for (let index = 0; index < keys.length; index += 1) {
|
|
2099
|
+
total += keys[index].byteLength + records[index].byteLength;
|
|
2100
|
+
if (total > MAX_PACKED_BYTES) throw remoteError("WorkerProtocolError", "compressed packed putMany exceeds the u32 byte limit");
|
|
2101
|
+
}
|
|
2102
|
+
const bytes = new Uint8Array(total);
|
|
2103
|
+
const view = new DataView(bytes.buffer);
|
|
2104
|
+
view.setUint32(0, keys.length * 2, true);
|
|
2105
|
+
let offset = 4 + keys.length * 8;
|
|
2106
|
+
for (let index = 0; index < records.length; index += 1) {
|
|
2107
|
+
const key = keys[index];
|
|
2108
|
+
const record = records[index];
|
|
2109
|
+
view.setUint32(4 + index * 8, key.byteLength, true);
|
|
2110
|
+
view.setUint32(8 + index * 8, record.byteLength, true);
|
|
2111
|
+
bytes.set(key, offset);
|
|
2112
|
+
offset += key.byteLength;
|
|
2113
|
+
if (record instanceof Uint8Array) {
|
|
2114
|
+
bytes.set(record, offset);
|
|
2115
|
+
offset += record.byteLength;
|
|
2116
|
+
} else offset = writePreparedStoreValueRecord(record, bytes, offset);
|
|
2117
|
+
}
|
|
2118
|
+
return bytes;
|
|
2119
|
+
}
|
|
2120
|
+
async putIndexedMany(txId, store, items, defs, options) {
|
|
2121
|
+
let keys = [];
|
|
2122
|
+
let values = [];
|
|
2123
|
+
let indexOps = [];
|
|
2124
|
+
let bytes = 4;
|
|
2125
|
+
const pendingPrimaries = /* @__PURE__ */ new Set();
|
|
2126
|
+
const pendingUniqueKeys = /* @__PURE__ */ new Set();
|
|
2127
|
+
const flush = () => {
|
|
2128
|
+
if (keys.length === 0) return;
|
|
2129
|
+
const batchKeys = keys;
|
|
2130
|
+
const batchValues = values;
|
|
2131
|
+
const batchOps = indexOps;
|
|
2132
|
+
keys = [];
|
|
2133
|
+
values = [];
|
|
2134
|
+
indexOps = [];
|
|
2135
|
+
bytes = 4;
|
|
2136
|
+
pendingPrimaries.clear();
|
|
2137
|
+
pendingUniqueKeys.clear();
|
|
2138
|
+
const packet = this.packPreparedPutEntries(batchKeys, batchValues);
|
|
2139
|
+
let baselines;
|
|
2140
|
+
try {
|
|
2141
|
+
baselines = this.withEngine((engine) => engine.put_many_indexed_packed(toWasmU64(txId), store, packet, batchOps, toWasmPutOptions(options)));
|
|
2142
|
+
} catch (error) {
|
|
2143
|
+
const partial = partialBooleanOutcomes(error);
|
|
2144
|
+
if (partial.length > 0) this.recordPutKeyChanges(txId, store, batchKeys, partial);
|
|
2145
|
+
throw error;
|
|
2146
|
+
}
|
|
2147
|
+
if (baselines.length !== batchKeys.length) throw remoteError("InternalError", `put_many_indexed_packed outcome count mismatch: ${baselines.length} != ${batchKeys.length}`);
|
|
2148
|
+
this.recordPutKeyChanges(txId, store, batchKeys, baselines);
|
|
2149
|
+
};
|
|
2150
|
+
for (let index = 0; index < items.length / 2; index += 1) {
|
|
2151
|
+
const key = items[index * 2];
|
|
2152
|
+
const value = items[index * 2 + 1];
|
|
2153
|
+
const primaryToken = bytesToHex(key);
|
|
2154
|
+
if (pendingPrimaries.has(primaryToken)) flush();
|
|
2155
|
+
try {
|
|
2156
|
+
const oldValue = await this.readStoreValue(txId, store, key);
|
|
2157
|
+
const oldKeyFor = oldValue === null ? null : createIndexKeyExtractor(oldValue);
|
|
2158
|
+
const newKeyFor = createIndexKeyExtractor(value);
|
|
2159
|
+
const plans = defs.map((def) => ({
|
|
2160
|
+
def,
|
|
2161
|
+
oldLogicalKey: oldKeyFor === null ? null : oldKeyFor(def),
|
|
2162
|
+
newLogicalKey: newKeyFor(def),
|
|
2163
|
+
newPhysicalKey: null
|
|
2164
|
+
}));
|
|
2165
|
+
if (plans.some((plan) => plan.def.unique && plan.newLogicalKey !== null && pendingUniqueKeys.has(`${plan.def.internalStore}:${bytesToHex(plan.newLogicalKey)}`))) {
|
|
2166
|
+
flush();
|
|
2167
|
+
index -= 1;
|
|
2168
|
+
continue;
|
|
2169
|
+
}
|
|
2170
|
+
this.ensureInternalStoresForDefinitions(txId, defs);
|
|
2171
|
+
for (const plan of plans) {
|
|
2172
|
+
if (plan.newLogicalKey === null) continue;
|
|
2173
|
+
plan.newPhysicalKey = this.buildIndexEntryKey(plan.def, plan.newLogicalKey, key);
|
|
2174
|
+
if (plan.oldLogicalKey === null || !bytesEqual(plan.oldLogicalKey, plan.newLogicalKey)) await this.assertUniqueIndexAvailability(txId, plan.def, plan.newLogicalKey, key);
|
|
2175
|
+
}
|
|
2176
|
+
const preparedValue = this.preparePutValue(txId, store, value);
|
|
2177
|
+
const storedValue = preparedValue instanceof Promise ? await preparedValue : preparedValue;
|
|
2178
|
+
const ops = [];
|
|
2179
|
+
for (const plan of plans) if (plan.oldLogicalKey !== null && (plan.newLogicalKey === null || !bytesEqual(plan.oldLogicalKey, plan.newLogicalKey))) ops.push({
|
|
2180
|
+
store: plan.def.internalStore,
|
|
2181
|
+
kind: "delete",
|
|
2182
|
+
key: encodeIndexEntryKey(plan.oldLogicalKey, key)
|
|
2183
|
+
});
|
|
2184
|
+
for (const plan of plans) {
|
|
2185
|
+
if (plan.newPhysicalKey !== null) ops.push({
|
|
2186
|
+
store: plan.def.internalStore,
|
|
2187
|
+
kind: "put",
|
|
2188
|
+
key: plan.newPhysicalKey
|
|
2189
|
+
});
|
|
2190
|
+
if (plan.def.unique) {
|
|
2191
|
+
for (const logicalKey of [plan.oldLogicalKey, plan.newLogicalKey]) if (logicalKey !== null) pendingUniqueKeys.add(`${plan.def.internalStore}:${bytesToHex(logicalKey)}`);
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
keys.push(key);
|
|
2195
|
+
values.push(storedValue);
|
|
2196
|
+
indexOps.push(ops);
|
|
2197
|
+
bytes += 8 + key.byteLength + storedValue.byteLength;
|
|
2198
|
+
for (const op of ops) bytes += op.key.byteLength;
|
|
2199
|
+
pendingPrimaries.add(primaryToken);
|
|
2200
|
+
} catch (error) {
|
|
2201
|
+
flush();
|
|
2202
|
+
throw error;
|
|
2203
|
+
}
|
|
2204
|
+
if (options.ttl !== void 0 || keys.length >= INDEX_PUT_BATCH_ROWS || bytes >= INDEX_PUT_BATCH_BYTES) flush();
|
|
2205
|
+
}
|
|
2206
|
+
flush();
|
|
2207
|
+
}
|
|
1755
2208
|
async encodeBatchOpsForWrite(txId, store, ops) {
|
|
1756
2209
|
if (isInternalStoreName(store)) return ops;
|
|
1757
2210
|
const compression = this.storeCompressionForTx(txId, store);
|
|
@@ -1908,12 +2361,100 @@ var DbWorker = class {
|
|
|
1908
2361
|
}
|
|
1909
2362
|
return rows;
|
|
1910
2363
|
}
|
|
2364
|
+
readIndexKeyScan(txId, store, range) {
|
|
2365
|
+
if (typeof this.engine?.open_scan_cursor !== "function" || typeof this.engine?.scan_cursor_next !== "function" || typeof this.engine?.close_scan_cursor !== "function") return this.readRawScan(txId, store, range);
|
|
2366
|
+
const maxRows = range.limit ?? INDEX_SCAN_RAW_CHUNK_ROWS;
|
|
2367
|
+
const maxBytes = 4 + maxRows * 1032;
|
|
2368
|
+
const cursorId = this.withEngine((engine) => engine.open_scan_cursor(toWasmU64(txId), store, toWasmRange(range), true));
|
|
2369
|
+
try {
|
|
2370
|
+
const page = this.withEngine((engine) => engine.scan_cursor_next(cursorId, maxRows, maxBytes));
|
|
2371
|
+
const packet = normalizeWasmBytes(page.packet);
|
|
2372
|
+
const rows = unpackPackedScanRows(packet);
|
|
2373
|
+
if (rows.length !== page.rowCount || rows.length > maxRows || packet.byteLength > maxBytes || rows.some((row) => row.value.byteLength !== 0)) throw remoteError("CorruptionError", "index key cursor returned an invalid page");
|
|
2374
|
+
return rows;
|
|
2375
|
+
} finally {
|
|
2376
|
+
this.withEngine((engine) => engine.close_scan_cursor(cursorId));
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
1911
2379
|
putRawValue(txId, store, key, value) {
|
|
1912
2380
|
this.withEngine((engine) => engine.put(toWasmU64(txId), store, key, value));
|
|
1913
2381
|
}
|
|
1914
2382
|
putIndexEntry(txId, def, physicalKey, value) {
|
|
2383
|
+
if (typeof this.engine?.put_index_entry_checked === "function") {
|
|
2384
|
+
const primaryKey = decodeIndexEntryKey(physicalKey).primaryKey;
|
|
2385
|
+
const state = this.readNativeValueState(txId, def.store, primaryKey);
|
|
2386
|
+
if (state?.revisionKnown) {
|
|
2387
|
+
this.withEngine((engine) => engine.put_index_entry_checked(toWasmU64(txId), def.store, def.internalStore, primaryKey, physicalKey, state.revisionEpoch, state.revisionOrdinal));
|
|
2388
|
+
return;
|
|
2389
|
+
}
|
|
2390
|
+
}
|
|
1915
2391
|
this.putRawValue(txId, def.internalStore, physicalKey, value);
|
|
1916
2392
|
}
|
|
2393
|
+
readNativeValueState(txId, store, key) {
|
|
2394
|
+
if (typeof this.engine?.get_many_value_states !== "function") return null;
|
|
2395
|
+
try {
|
|
2396
|
+
const keys = new Uint8Array(8 + key.byteLength);
|
|
2397
|
+
const keyView = new DataView(keys.buffer);
|
|
2398
|
+
keyView.setUint32(0, 1, true);
|
|
2399
|
+
keyView.setUint32(4, key.byteLength, true);
|
|
2400
|
+
keys.set(key, 8);
|
|
2401
|
+
const packet = normalizeWasmBytes(this.withEngine((engine) => engine.get_many_value_states(toWasmU64(txId), store, keys)));
|
|
2402
|
+
return this.decodeNativeValueStates(packet, 1)?.[0] ?? null;
|
|
2403
|
+
} catch {
|
|
2404
|
+
return null;
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
packNativeKeys(keys) {
|
|
2408
|
+
let total = 4 + keys.length * 4;
|
|
2409
|
+
for (const key of keys) total += key.byteLength;
|
|
2410
|
+
const packet = new Uint8Array(total);
|
|
2411
|
+
const view = new DataView(packet.buffer);
|
|
2412
|
+
view.setUint32(0, keys.length, true);
|
|
2413
|
+
let offset = 4 + keys.length * 4;
|
|
2414
|
+
for (let index = 0; index < keys.length; index += 1) {
|
|
2415
|
+
const key = keys[index];
|
|
2416
|
+
view.setUint32(4 + index * 4, key.byteLength, true);
|
|
2417
|
+
packet.set(key, offset);
|
|
2418
|
+
offset += key.byteLength;
|
|
2419
|
+
}
|
|
2420
|
+
return packet;
|
|
2421
|
+
}
|
|
2422
|
+
decodeNativeValueStates(packet, count) {
|
|
2423
|
+
if (packet.byteLength !== 4 + count * 32) return null;
|
|
2424
|
+
const view = new DataView(packet.buffer, packet.byteOffset, packet.byteLength);
|
|
2425
|
+
if (view.getUint32(0, true) !== count) return null;
|
|
2426
|
+
const states = new Array(count);
|
|
2427
|
+
for (let index = 0; index < count; index += 1) {
|
|
2428
|
+
const offset = 4 + index * 32;
|
|
2429
|
+
const flags = view.getUint32(offset, true);
|
|
2430
|
+
const revisionEpoch = view.getBigUint64(offset + 12, true);
|
|
2431
|
+
const revisionOrdinal = view.getBigUint64(offset + 20, true);
|
|
2432
|
+
if ((flags & -8) !== 0 || (flags & 3) === 3 || (flags & 4) !== 0 && ((flags & 3) === 0 || revisionEpoch === 0n || revisionOrdinal === 0n)) return null;
|
|
2433
|
+
states[index] = {
|
|
2434
|
+
live: (flags & 1) !== 0,
|
|
2435
|
+
expired: (flags & 2) !== 0,
|
|
2436
|
+
revisionKnown: (flags & 4) !== 0,
|
|
2437
|
+
expiresAt: view.getBigUint64(offset + 4, true),
|
|
2438
|
+
revisionEpoch,
|
|
2439
|
+
revisionOrdinal,
|
|
2440
|
+
maxValueBytes: view.getUint32(offset + 28, true)
|
|
2441
|
+
};
|
|
2442
|
+
}
|
|
2443
|
+
return states;
|
|
2444
|
+
}
|
|
2445
|
+
inspectIndexedValue(txId, def, physicalKey, primaryKey) {
|
|
2446
|
+
const primary = this.readNativeValueState(txId, def.store, primaryKey);
|
|
2447
|
+
if (primary === null) return "unknown";
|
|
2448
|
+
if (!primary.live) {
|
|
2449
|
+
if (primary.expired && this.txModes.get(txId) === "readwrite") {
|
|
2450
|
+
if (this.withEngine((engine) => engine.has(toWasmU64(txId), def.store, primaryKey))) return "unknown";
|
|
2451
|
+
}
|
|
2452
|
+
return "missing";
|
|
2453
|
+
}
|
|
2454
|
+
if (!primary.revisionKnown) return "unknown";
|
|
2455
|
+
const index = this.readNativeValueState(txId, def.internalStore, physicalKey);
|
|
2456
|
+
return index?.live && index.revisionKnown && index.revisionEpoch === primary.revisionEpoch && index.revisionOrdinal === primary.revisionOrdinal ? "trusted" : "unknown";
|
|
2457
|
+
}
|
|
1917
2458
|
deleteIndexEntryIfPresent(txId, def, logicalKey, primaryKey) {
|
|
1918
2459
|
const physicalKey = this.buildIndexEntryKey(def, logicalKey, primaryKey);
|
|
1919
2460
|
this.ignoreNamedError("StoreNotFoundError", () => {
|
|
@@ -1939,11 +2480,17 @@ var DbWorker = class {
|
|
|
1939
2480
|
range.gt = cursor;
|
|
1940
2481
|
delete range.gte;
|
|
1941
2482
|
}
|
|
1942
|
-
const rows = this.
|
|
2483
|
+
const rows = this.readIndexKeyScan(txId, def.internalStore, range);
|
|
1943
2484
|
for (const row of rows) {
|
|
1944
2485
|
cursor = row.key;
|
|
1945
2486
|
const decoded = decodeIndexEntryKey(row.key);
|
|
1946
2487
|
if (bytesEqual(decoded.primaryKey, primaryKey)) continue;
|
|
2488
|
+
const state = this.inspectIndexedValue(txId, def, row.key, decoded.primaryKey);
|
|
2489
|
+
if (state === "missing") {
|
|
2490
|
+
this.cleanupStaleIndexRow(txId, def, row.key);
|
|
2491
|
+
continue;
|
|
2492
|
+
}
|
|
2493
|
+
if (state === "trusted") throw remoteError("UniqueIndexConstraintError", `unique index constraint violation on ${def.store}.${def.name}`);
|
|
1947
2494
|
const value = await this.readStoreValue(txId, def.store, decoded.primaryKey);
|
|
1948
2495
|
if (value === null) {
|
|
1949
2496
|
this.cleanupStaleIndexRow(txId, def, row.key);
|
|
@@ -1965,12 +2512,13 @@ var DbWorker = class {
|
|
|
1965
2512
|
* chunks prevent stale entries from forcing the whole range into memory.
|
|
1966
2513
|
* The output limit counts only live rows.
|
|
1967
2514
|
*/
|
|
1968
|
-
async loadVisibleIndexPage(txId, def, range, cursor, limit) {
|
|
2515
|
+
async loadVisibleIndexPage(txId, def, range, cursor, limit, maxBytes = INDEX_SCAN_PAGE_BYTES) {
|
|
1969
2516
|
const { limit: _ignored, ...withoutLimit } = range;
|
|
1970
2517
|
const physical = indexRangeToPhysicalRange(withoutLimit);
|
|
1971
2518
|
const reverse = physical.reverse === true;
|
|
1972
2519
|
let resumeAfter = cursor;
|
|
1973
2520
|
const visible = [];
|
|
2521
|
+
let outputBytes = 4;
|
|
1974
2522
|
let staleChunkFloor = 1;
|
|
1975
2523
|
for (;;) {
|
|
1976
2524
|
const chunkLimit = Math.min(INDEX_SCAN_RAW_CHUNK_ROWS, Math.max(limit - visible.length, staleChunkFloor));
|
|
@@ -1989,7 +2537,7 @@ var DbWorker = class {
|
|
|
1989
2537
|
}
|
|
1990
2538
|
let rawRows;
|
|
1991
2539
|
try {
|
|
1992
|
-
rawRows = this.
|
|
2540
|
+
rawRows = this.readIndexKeyScan(txId, def.internalStore, chunkRange);
|
|
1993
2541
|
} catch (error) {
|
|
1994
2542
|
if (isNamedError(error, "StoreNotFoundError")) return {
|
|
1995
2543
|
rows: [],
|
|
@@ -2004,14 +2552,28 @@ var DbWorker = class {
|
|
|
2004
2552
|
for (let index = 0; index < rawRows.length; index += 1) {
|
|
2005
2553
|
if (index === batchEnd) {
|
|
2006
2554
|
batchStart = index;
|
|
2007
|
-
|
|
2008
|
-
|
|
2555
|
+
batch = this.readIndexPrimaryValues(txId, def, rawRows, index, Math.min(rawRows.length, index + limit - visible.length), maxBytes - outputBytes);
|
|
2556
|
+
batchEnd = index + (batch?.entries.length ?? 1);
|
|
2009
2557
|
}
|
|
2010
2558
|
const row = rawRows[index];
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
if (!resolved)
|
|
2559
|
+
const resolved = batch ? await this.resolveVisibleIndexedValue(txId, def, row.key, batch.entries[index - batchStart], batch.values[index - batchStart], batch.trusted[index - batchStart]) : await this.resolveVisibleIndexedRow(txId, def, row.key);
|
|
2560
|
+
if (batch !== null) batch.values[index - batchStart] = null;
|
|
2561
|
+
if (!resolved) {
|
|
2562
|
+
resumeAfter = row.key;
|
|
2563
|
+
continue;
|
|
2564
|
+
}
|
|
2565
|
+
const nextBytes = outputBytes + 8 + resolved.key.byteLength + resolved.value.byteLength;
|
|
2566
|
+
if (nextBytes > maxBytes) {
|
|
2567
|
+
if (visible.length === 0) throw remoteError("ValueTooLargeError", "indexed scan row exceeds maxBytes");
|
|
2568
|
+
if (resumeAfter === null) throw remoteError("InternalError", "indexed scan lost its continuation key");
|
|
2569
|
+
return {
|
|
2570
|
+
rows: visible,
|
|
2571
|
+
cursor: resumeAfter.slice()
|
|
2572
|
+
};
|
|
2573
|
+
}
|
|
2014
2574
|
visible.push(resolved);
|
|
2575
|
+
outputBytes = nextBytes;
|
|
2576
|
+
resumeAfter = row.key;
|
|
2015
2577
|
if (visible.length >= limit) return {
|
|
2016
2578
|
rows: visible,
|
|
2017
2579
|
cursor: row.key.slice()
|
|
@@ -2024,37 +2586,91 @@ var DbWorker = class {
|
|
|
2024
2586
|
staleChunkFloor = visible.length === visibleBefore ? Math.min(INDEX_SCAN_RAW_CHUNK_ROWS, chunkLimit * 2) : 1;
|
|
2025
2587
|
}
|
|
2026
2588
|
}
|
|
2027
|
-
readIndexPrimaryValues(txId, def, rows, start, end) {
|
|
2028
|
-
if (this.txModes.get(txId) !== "readonly" || end - start <= 1) return null;
|
|
2589
|
+
readIndexPrimaryValues(txId, def, rows, start, end, remainingBytes) {
|
|
2590
|
+
if (this.txModes.get(txId) !== "readonly" || end - start <= 1 || typeof this.engine?.get_many_value_sizes !== "function" && typeof this.engine?.get_many_value_states !== "function") return null;
|
|
2029
2591
|
let entries;
|
|
2030
2592
|
let values;
|
|
2593
|
+
let trusted;
|
|
2594
|
+
let loaded;
|
|
2595
|
+
let selectedIndices;
|
|
2031
2596
|
try {
|
|
2032
2597
|
entries = rows.slice(start, end).map((row) => decodeIndexEntryKey(row.key));
|
|
2033
|
-
|
|
2598
|
+
const packedKeys = this.packNativeKeys(entries.map((entry) => entry.primaryKey));
|
|
2599
|
+
let primaryStates = null;
|
|
2600
|
+
let indexStates = null;
|
|
2601
|
+
let sizes;
|
|
2602
|
+
if (typeof this.engine?.get_many_value_states === "function") {
|
|
2603
|
+
primaryStates = this.decodeNativeValueStates(normalizeWasmBytes(this.withEngine((engine) => engine.get_many_value_states(toWasmU64(txId), def.store, packedKeys))), entries.length);
|
|
2604
|
+
if (primaryStates === null) return null;
|
|
2605
|
+
sizes = Uint32Array.from(primaryStates, (state) => state.live ? state.maxValueBytes : MAX_PACKED_BYTES);
|
|
2606
|
+
if (primaryStates.some((state) => state.live && state.revisionKnown)) {
|
|
2607
|
+
const indexKeys = this.packNativeKeys(rows.slice(start, end).map((row) => row.key));
|
|
2608
|
+
indexStates = this.decodeNativeValueStates(normalizeWasmBytes(this.withEngine((engine) => engine.get_many_value_states(toWasmU64(txId), def.internalStore, indexKeys))), entries.length);
|
|
2609
|
+
if (indexStates === null) return null;
|
|
2610
|
+
}
|
|
2611
|
+
} else sizes = this.withEngine((engine) => engine.get_many_value_sizes(toWasmU64(txId), def.store, packedKeys));
|
|
2612
|
+
if (sizes.length !== entries.length) return null;
|
|
2613
|
+
let selected = 0;
|
|
2614
|
+
for (const entry of entries) {
|
|
2615
|
+
const size = sizes[selected];
|
|
2616
|
+
if (!Number.isSafeInteger(size) || size < 0 || size > MAX_PACKED_BYTES) return null;
|
|
2617
|
+
const required = size === MAX_PACKED_BYTES ? 0 : 8 + entry.primaryKey.byteLength + size;
|
|
2618
|
+
if (required > remainingBytes) break;
|
|
2619
|
+
remainingBytes -= required;
|
|
2620
|
+
selected += 1;
|
|
2621
|
+
}
|
|
2622
|
+
if (selected <= 1) return null;
|
|
2623
|
+
entries.length = selected;
|
|
2624
|
+
trusted = new Array(selected).fill(false);
|
|
2625
|
+
const selectedKeys = [];
|
|
2626
|
+
selectedIndices = [];
|
|
2627
|
+
values = new Array(selected).fill(null);
|
|
2628
|
+
for (let index = 0; index < selected; index += 1) {
|
|
2629
|
+
const primary = primaryStates?.[index];
|
|
2630
|
+
if (primary === void 0 || primary.live) {
|
|
2631
|
+
selectedKeys.push(entries[index].primaryKey);
|
|
2632
|
+
selectedIndices.push(index);
|
|
2633
|
+
}
|
|
2634
|
+
const proof = indexStates?.[index];
|
|
2635
|
+
trusted[index] = Boolean(primary?.live && primary.revisionKnown && proof?.live && proof.revisionKnown && primary.revisionEpoch === proof.revisionEpoch && primary.revisionOrdinal === proof.revisionOrdinal);
|
|
2636
|
+
}
|
|
2637
|
+
loaded = selectedKeys.length === 0 ? [] : this.withEngine((engine) => engine.get_many(toWasmU64(txId), def.store, selectedKeys));
|
|
2034
2638
|
} catch {
|
|
2035
2639
|
return null;
|
|
2036
2640
|
}
|
|
2037
|
-
if (
|
|
2641
|
+
if (loaded.length !== selectedIndices.length) throw outcomeCountMismatch("get_many", loaded.length, selectedIndices.length);
|
|
2642
|
+
for (let index = 0; index < loaded.length; index += 1) {
|
|
2643
|
+
values[selectedIndices[index]] = loaded[index];
|
|
2644
|
+
loaded[index] = null;
|
|
2645
|
+
}
|
|
2038
2646
|
return {
|
|
2039
2647
|
entries,
|
|
2040
|
-
values
|
|
2648
|
+
values,
|
|
2649
|
+
trusted
|
|
2041
2650
|
};
|
|
2042
2651
|
}
|
|
2043
2652
|
async resolveVisibleIndexedRow(txId, def, physicalKey) {
|
|
2044
2653
|
const decoded = decodeIndexEntryKey(physicalKey);
|
|
2654
|
+
const state = this.inspectIndexedValue(txId, def, physicalKey, decoded.primaryKey);
|
|
2655
|
+
if (state === "missing") {
|
|
2656
|
+
this.cleanupStaleIndexRow(txId, def, physicalKey);
|
|
2657
|
+
return null;
|
|
2658
|
+
}
|
|
2045
2659
|
const value = this.readRawValue(txId, def.store, decoded.primaryKey);
|
|
2046
|
-
return this.resolveVisibleIndexedValue(txId, def, physicalKey, decoded, value);
|
|
2660
|
+
return this.resolveVisibleIndexedValue(txId, def, physicalKey, decoded, value, state === "trusted");
|
|
2047
2661
|
}
|
|
2048
|
-
async resolveVisibleIndexedValue(txId, def, physicalKey, decoded, rawValue) {
|
|
2662
|
+
async resolveVisibleIndexedValue(txId, def, physicalKey, decoded, rawValue, trusted = false) {
|
|
2049
2663
|
if (rawValue === null) {
|
|
2050
2664
|
this.cleanupStaleIndexRow(txId, def, physicalKey);
|
|
2051
2665
|
return null;
|
|
2052
2666
|
}
|
|
2053
2667
|
const value = await this.decodeStoreValue(txId, def.store, normalizeWasmBytes(rawValue));
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2668
|
+
if (!trusted) {
|
|
2669
|
+
const currentLogicalKey = extractLogicalIndexKey(def, value);
|
|
2670
|
+
if (currentLogicalKey === null || !bytesEqual(currentLogicalKey, decoded.logicalKey)) {
|
|
2671
|
+
this.cleanupStaleIndexRow(txId, def, physicalKey);
|
|
2672
|
+
return null;
|
|
2673
|
+
}
|
|
2058
2674
|
}
|
|
2059
2675
|
return {
|
|
2060
2676
|
key: decoded.primaryKey,
|
|
@@ -2098,6 +2714,7 @@ var DbWorker = class {
|
|
|
2098
2714
|
}
|
|
2099
2715
|
}
|
|
2100
2716
|
clearTxCaches() {
|
|
2717
|
+
this.primaryCursors.clear();
|
|
2101
2718
|
this.txChanges.clear();
|
|
2102
2719
|
this.txModes.clear();
|
|
2103
2720
|
this.txIndexSchemas.clear();
|
|
@@ -2183,6 +2800,9 @@ function normalizeEngineHealth(health) {
|
|
|
2183
2800
|
};
|
|
2184
2801
|
return { state: health.state };
|
|
2185
2802
|
}
|
|
2803
|
+
function outcomeCountMismatch(operation, actual, expected) {
|
|
2804
|
+
return remoteError("InternalError", `${operation} outcome count mismatch: ${actual} != ${expected}`);
|
|
2805
|
+
}
|
|
2186
2806
|
function partialBooleanOutcomes(error) {
|
|
2187
2807
|
if (!isRecord(error) || !Array.isArray(error.partial)) return [];
|
|
2188
2808
|
return error.partial.filter((value) => typeof value === "boolean");
|