@moyodb/sdk 1.1.0 → 1.3.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/README.md +142 -0
- package/dist/assets/browser-worker-DBt51gsS.js +4 -0
- package/dist/assets/browser-worker-DBt51gsS.js.map +1 -0
- package/dist/assets/shared-worker-NdniRuEL.js +2 -0
- package/dist/assets/shared-worker-NdniRuEL.js.map +1 -0
- package/dist/chunks/{indexing-BzmFhWXs.js → indexing-CKtbpaZn.js} +93 -12
- package/dist/chunks/indexing-CKtbpaZn.js.map +1 -0
- package/dist/compression.d.ts +16 -2
- package/dist/compression.d.ts.map +1 -1
- package/dist/engine/README.md +30 -3
- package/dist/engine/moyodb_engine.d.ts +34 -10
- package/dist/engine/moyodb_engine.js +238 -51
- package/dist/engine/moyodb_engine_bg.wasm +0 -0
- package/dist/engine/moyodb_engine_bg.wasm.d.ts +17 -10
- package/dist/engine/package.json +1 -1
- package/dist/index.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 +1122 -883
- package/dist/moyodb-sdk.js.map +1 -1
- package/dist/node-engine.js +703 -78
- 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 +280 -37
- 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 +17 -1
- 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 +11 -1
- package/dist/worker-protocol.d.ts.map +1 -1
- package/dist/worker-server.d.ts.map +1 -1
- package/dist/worker.d.ts +31 -2
- package/dist/worker.d.ts.map +1 -1
- package/package.json +4 -4
- 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-54a406e33b0fb849}/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-CKtbpaZn.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`);
|
|
@@ -539,7 +783,11 @@ function toWasmRange(range) {
|
|
|
539
783
|
var EMPTY_RANGE = {};
|
|
540
784
|
var EMPTY_VALUE = /* @__PURE__ */ new Uint8Array(0);
|
|
541
785
|
var MAX_ENGINE_KEY_BYTES = 1024;
|
|
786
|
+
var MAX_PACKED_BYTES = 4294967295;
|
|
787
|
+
var INDEX_PUT_BATCH_ROWS = 64;
|
|
788
|
+
var INDEX_PUT_BATCH_BYTES = 4194304;
|
|
542
789
|
var INDEX_SCAN_PAGE_ROWS = 256;
|
|
790
|
+
var INDEX_SCAN_PAGE_BYTES = 8389662;
|
|
543
791
|
var INDEX_SCAN_RAW_CHUNK_ROWS = 512;
|
|
544
792
|
var RECONCILE_DOCUMENT_CACHE_BYTES = 4194304;
|
|
545
793
|
var RECONCILE_DOCUMENT_CACHE_ROWS = 4096;
|
|
@@ -657,6 +905,32 @@ function bytesEqual(left, right) {
|
|
|
657
905
|
for (let index = 0; index < left.byteLength; index += 1) if (left[index] !== right[index]) return false;
|
|
658
906
|
return true;
|
|
659
907
|
}
|
|
908
|
+
function copyRange(range) {
|
|
909
|
+
const copy = toWasmRange(range);
|
|
910
|
+
for (const bound of [
|
|
911
|
+
"gt",
|
|
912
|
+
"gte",
|
|
913
|
+
"lt",
|
|
914
|
+
"lte"
|
|
915
|
+
]) {
|
|
916
|
+
const value = copy[bound];
|
|
917
|
+
if (value !== void 0) copy[bound] = value.slice();
|
|
918
|
+
}
|
|
919
|
+
return copy;
|
|
920
|
+
}
|
|
921
|
+
function rangesEqual(left, right) {
|
|
922
|
+
for (const bound of [
|
|
923
|
+
"gt",
|
|
924
|
+
"gte",
|
|
925
|
+
"lt",
|
|
926
|
+
"lte"
|
|
927
|
+
]) {
|
|
928
|
+
const a = left[bound];
|
|
929
|
+
const b = right[bound];
|
|
930
|
+
if (a === void 0 ? b !== void 0 : b === void 0 || !bytesEqual(a, b)) return false;
|
|
931
|
+
}
|
|
932
|
+
return left.reverse === right.reverse && left.limit === right.limit;
|
|
933
|
+
}
|
|
660
934
|
function normalizeWasmBytes(value) {
|
|
661
935
|
if (value instanceof Uint8Array) return value.byteOffset === 0 && value.byteLength === value.buffer.byteLength ? value : value.slice();
|
|
662
936
|
return new Uint8Array(value);
|
|
@@ -763,7 +1037,7 @@ var OwnershipLease = class {
|
|
|
763
1037
|
return;
|
|
764
1038
|
}
|
|
765
1039
|
const controller = new AbortController();
|
|
766
|
-
const timeout = setTimeout(() => controller.abort(), ownerWaitMs);
|
|
1040
|
+
const timeout = setTimeout(() => controller.abort(), clampTimerDelayMs(ownerWaitMs));
|
|
767
1041
|
let acquiredResolve = null;
|
|
768
1042
|
const acquiredPromise = new Promise((resolve) => {
|
|
769
1043
|
acquiredResolve = resolve;
|
|
@@ -806,6 +1080,8 @@ var DbWorker = class {
|
|
|
806
1080
|
persistenceBridge;
|
|
807
1081
|
txChanges = /* @__PURE__ */ new Map();
|
|
808
1082
|
txModes = /* @__PURE__ */ new Map();
|
|
1083
|
+
primaryCursors = /* @__PURE__ */ new Map();
|
|
1084
|
+
nextPrimaryCursorId = 1;
|
|
809
1085
|
/**
|
|
810
1086
|
* Shared committed schema at begin. Reconciliation replaces the array;
|
|
811
1087
|
* it never mutates the shared schema.
|
|
@@ -1004,7 +1280,7 @@ var DbWorker = class {
|
|
|
1004
1280
|
return result;
|
|
1005
1281
|
}
|
|
1006
1282
|
createStore(txId, name, options = {}) {
|
|
1007
|
-
const compression = options.compression ??
|
|
1283
|
+
const compression = isInternalStoreName(name) ? false : options.compression ?? "snappy";
|
|
1008
1284
|
this.txEnsuredRawStores.get(txId)?.delete(name);
|
|
1009
1285
|
this.withEngine((engine) => engine.create_store(toWasmU64(txId), name, { compression }));
|
|
1010
1286
|
this.recordStoreCompressionCreate(txId, name, compression);
|
|
@@ -1057,42 +1333,12 @@ var DbWorker = class {
|
|
|
1057
1333
|
this.recordPutChange(txId, store, key, baselineExists);
|
|
1058
1334
|
return;
|
|
1059
1335
|
}
|
|
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);
|
|
1336
|
+
await this.putIndexedMany(txId, store, [key, value], defs, options);
|
|
1092
1337
|
}
|
|
1093
1338
|
async putMany(txId, store, entries, options = {}) {
|
|
1094
1339
|
if (entries.length === 0) return;
|
|
1095
|
-
|
|
1340
|
+
const defs = this.indexesForStoreInTx(txId, store);
|
|
1341
|
+
if (defs.length === 0) {
|
|
1096
1342
|
const storedEntries = await this.encodeEntriesForWrite(txId, store, entries);
|
|
1097
1343
|
let baselines;
|
|
1098
1344
|
try {
|
|
@@ -1106,18 +1352,29 @@ var DbWorker = class {
|
|
|
1106
1352
|
this.recordPutChanges(txId, store, entries, baselines);
|
|
1107
1353
|
return;
|
|
1108
1354
|
}
|
|
1109
|
-
|
|
1355
|
+
const items = new Array(entries.length * 2);
|
|
1356
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
1357
|
+
items[index * 2] = entries[index][0];
|
|
1358
|
+
items[index * 2 + 1] = entries[index][1];
|
|
1359
|
+
}
|
|
1360
|
+
await this.putIndexedMany(txId, store, items, defs, options);
|
|
1110
1361
|
}
|
|
1111
1362
|
async putManyPacked(txId, store, packedEntries, options = {}) {
|
|
1112
|
-
|
|
1113
|
-
|
|
1363
|
+
const defs = this.indexesForStoreInTx(txId, store);
|
|
1364
|
+
if (defs.length !== 0) {
|
|
1365
|
+
const items = unpackPackedBinaryList(packedEntries);
|
|
1366
|
+
if (items.length % 2 !== 0) throw remoteError("WorkerProtocolError", "packed putMany payload has an odd item count");
|
|
1367
|
+
await this.putIndexedMany(txId, store, items, defs, options);
|
|
1114
1368
|
return;
|
|
1115
1369
|
}
|
|
1116
|
-
const
|
|
1370
|
+
const items = this.storeCompressionForTx(txId, store) === false ? null : unpackPackedBinaryList(packedEntries);
|
|
1371
|
+
if (items !== null && items.length % 2 !== 0) throw remoteError("WorkerProtocolError", "packed putMany payload has an odd item count");
|
|
1372
|
+
const keys = items === null ? unpackPackedBinaryPairKeys(packedEntries) : items.filter((_item, index) => index % 2 === 0);
|
|
1117
1373
|
if (keys.length === 0) return;
|
|
1374
|
+
const storedEntries = items === null ? packedEntries : await this.encodePackedEntriesForWrite(txId, store, items, keys);
|
|
1118
1375
|
let baselines;
|
|
1119
1376
|
try {
|
|
1120
|
-
baselines = this.withEngine((engine) => engine.put_many_packed(toWasmU64(txId), store,
|
|
1377
|
+
baselines = this.withEngine((engine) => engine.put_many_packed(toWasmU64(txId), store, storedEntries, toWasmPutOptions(options)));
|
|
1121
1378
|
} catch (error) {
|
|
1122
1379
|
const partial = partialBooleanOutcomes(error);
|
|
1123
1380
|
this.recordPutKeyChanges(txId, store, keys, partial);
|
|
@@ -1222,9 +1479,82 @@ var DbWorker = class {
|
|
|
1222
1479
|
async scan(txId, store, range) {
|
|
1223
1480
|
return this.readStoreScan(txId, store, range);
|
|
1224
1481
|
}
|
|
1482
|
+
async scanPage(request) {
|
|
1483
|
+
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");
|
|
1484
|
+
let cursorId = request.cursorId;
|
|
1485
|
+
let cursor = cursorId === void 0 ? void 0 : this.primaryCursors.get(cursorId);
|
|
1486
|
+
if (cursorId !== void 0) {
|
|
1487
|
+
if (!cursor) throw remoteError("TransactionClosedError", "scan cursor is closed");
|
|
1488
|
+
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");
|
|
1489
|
+
} else {
|
|
1490
|
+
const ownsTx = request.txId === void 0;
|
|
1491
|
+
const txId = request.txId ?? await this.begin("readonly");
|
|
1492
|
+
const range = copyRange(request.range ?? EMPTY_RANGE);
|
|
1493
|
+
let engineId;
|
|
1494
|
+
try {
|
|
1495
|
+
engineId = this.withEngine((engine) => engine.open_scan_cursor(toWasmU64(txId), request.store, toWasmRange(range), false));
|
|
1496
|
+
} catch (error) {
|
|
1497
|
+
if (ownsTx) try {
|
|
1498
|
+
await this.rollback(txId);
|
|
1499
|
+
} catch {}
|
|
1500
|
+
throw error;
|
|
1501
|
+
}
|
|
1502
|
+
cursorId = this.nextPrimaryCursorId++;
|
|
1503
|
+
cursor = {
|
|
1504
|
+
engineId,
|
|
1505
|
+
txId,
|
|
1506
|
+
ownsTx,
|
|
1507
|
+
store: request.store,
|
|
1508
|
+
range,
|
|
1509
|
+
callerTxId: request.txId
|
|
1510
|
+
};
|
|
1511
|
+
this.primaryCursors.set(cursorId, cursor);
|
|
1512
|
+
}
|
|
1513
|
+
try {
|
|
1514
|
+
const page = this.withEngine((engine) => engine.scan_cursor_next(cursor.engineId, request.maxRows, request.maxBytes));
|
|
1515
|
+
const packet = normalizeWasmBytes(page.packet);
|
|
1516
|
+
const rowCount = packet.byteLength < 4 ? -1 : new DataView(packet.buffer, packet.byteOffset, 4).getUint32(0, true);
|
|
1517
|
+
if (rowCount !== page.rowCount || rowCount > request.maxRows || packet.byteLength > request.maxBytes) throw remoteError("CorruptionError", "scan cursor returned an invalid page budget or count");
|
|
1518
|
+
const rows = this.storeCompressionForTx(cursor.txId, cursor.store) !== false ? unpackPackedScanRows(packet) : null;
|
|
1519
|
+
let bytes = packet.byteLength;
|
|
1520
|
+
if (rows !== null) {
|
|
1521
|
+
bytes = 4;
|
|
1522
|
+
for (const row of rows) {
|
|
1523
|
+
row.value = await this.decodeStoreValue(cursor.txId, cursor.store, row.value);
|
|
1524
|
+
bytes += 8 + row.key.byteLength + row.value.byteLength;
|
|
1525
|
+
if (bytes > request.maxBytes) throw remoteError("ValueTooLargeError", "decoded scan page exceeds maxBytes");
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
if (page.exhausted) await this.closeCursor(cursorId);
|
|
1529
|
+
return {
|
|
1530
|
+
rows: rows ?? packedScanRows(packet),
|
|
1531
|
+
done: page.exhausted,
|
|
1532
|
+
bytes,
|
|
1533
|
+
...page.exhausted ? {} : { cursorId }
|
|
1534
|
+
};
|
|
1535
|
+
} catch (error) {
|
|
1536
|
+
try {
|
|
1537
|
+
await this.closeCursor(cursorId);
|
|
1538
|
+
} catch {}
|
|
1539
|
+
throw error;
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
async closeCursor(cursorId) {
|
|
1543
|
+
const cursor = this.primaryCursors.get(cursorId);
|
|
1544
|
+
if (!cursor) return;
|
|
1545
|
+
this.primaryCursors.delete(cursorId);
|
|
1546
|
+
try {
|
|
1547
|
+
this.withEngine((engine) => engine.close_scan_cursor(cursor.engineId));
|
|
1548
|
+
} finally {
|
|
1549
|
+
if (cursor.ownsTx) await this.rollback(cursor.txId);
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1225
1552
|
async getByIndex(txId, store, indexName, key) {
|
|
1226
1553
|
const def = this.resolveIndexDefinition(txId, store, indexName);
|
|
1227
|
-
const page = await this.loadVisibleIndexPage(txId, def,
|
|
1554
|
+
const page = await this.loadVisibleIndexPage(txId, def, {
|
|
1555
|
+
gte: key,
|
|
1556
|
+
lte: key
|
|
1557
|
+
}, null, 1);
|
|
1228
1558
|
return page.rows.length === 0 ? null : page.rows[0].value;
|
|
1229
1559
|
}
|
|
1230
1560
|
async scanByIndex(txId, store, indexName, range = {}) {
|
|
@@ -1241,10 +1571,11 @@ var DbWorker = class {
|
|
|
1241
1571
|
}
|
|
1242
1572
|
return rows;
|
|
1243
1573
|
}
|
|
1244
|
-
async scanByIndexPage(txId, store, indexName, range, cursor, limit) {
|
|
1574
|
+
async scanByIndexPage(txId, store, indexName, range, cursor, limit, maxBytes = INDEX_SCAN_PAGE_BYTES) {
|
|
1245
1575
|
if (!Number.isSafeInteger(limit) || limit <= 0) throw remoteError("InvalidRangeError", "index scan page limit must be a positive integer");
|
|
1576
|
+
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
1577
|
const def = this.resolveIndexDefinition(txId, store, indexName);
|
|
1247
|
-
return this.loadVisibleIndexPage(txId, def, range, cursor, limit);
|
|
1578
|
+
return this.loadVisibleIndexPage(txId, def, range, cursor, limit, maxBytes);
|
|
1248
1579
|
}
|
|
1249
1580
|
getIndexes(txId) {
|
|
1250
1581
|
return Promise.resolve(toPublicIndexDefinitions(txId === void 0 ? this.loadCommittedIndexSchema() : this.indexSchemaForTx(txId)));
|
|
@@ -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);
|
|
@@ -1752,6 +2089,130 @@ var DbWorker = class {
|
|
|
1752
2089
|
for (const [key, value] of entries) storedEntries.push([key, await encodeStoreValueRecord(value, compression)]);
|
|
1753
2090
|
return storedEntries;
|
|
1754
2091
|
}
|
|
2092
|
+
async encodePackedEntriesForWrite(txId, store, items, keys) {
|
|
2093
|
+
const records = new Array(items.length / 2);
|
|
2094
|
+
for (let index = 0; index < records.length; index += 1) {
|
|
2095
|
+
const value = this.preparePutValue(txId, store, items[index * 2 + 1]);
|
|
2096
|
+
records[index] = value instanceof Promise ? await value : value;
|
|
2097
|
+
}
|
|
2098
|
+
return this.packPreparedPutEntries(keys, records);
|
|
2099
|
+
}
|
|
2100
|
+
preparePutValue(txId, store, value) {
|
|
2101
|
+
const compression = this.storeCompressionForTx(txId, store);
|
|
2102
|
+
return compression === "snappy" || compression === false ? prepareFastStoreValueRecord(value, compression) : encodeStoreValueRecord(value, compression);
|
|
2103
|
+
}
|
|
2104
|
+
packPreparedPutEntries(keys, records) {
|
|
2105
|
+
let total = 4 + keys.length * 8;
|
|
2106
|
+
for (let index = 0; index < keys.length; index += 1) {
|
|
2107
|
+
total += keys[index].byteLength + records[index].byteLength;
|
|
2108
|
+
if (total > MAX_PACKED_BYTES) throw remoteError("WorkerProtocolError", "compressed packed putMany exceeds the u32 byte limit");
|
|
2109
|
+
}
|
|
2110
|
+
const bytes = new Uint8Array(total);
|
|
2111
|
+
const view = new DataView(bytes.buffer);
|
|
2112
|
+
view.setUint32(0, keys.length * 2, true);
|
|
2113
|
+
let offset = 4 + keys.length * 8;
|
|
2114
|
+
for (let index = 0; index < records.length; index += 1) {
|
|
2115
|
+
const key = keys[index];
|
|
2116
|
+
const record = records[index];
|
|
2117
|
+
view.setUint32(4 + index * 8, key.byteLength, true);
|
|
2118
|
+
view.setUint32(8 + index * 8, record.byteLength, true);
|
|
2119
|
+
bytes.set(key, offset);
|
|
2120
|
+
offset += key.byteLength;
|
|
2121
|
+
if (record instanceof Uint8Array) {
|
|
2122
|
+
bytes.set(record, offset);
|
|
2123
|
+
offset += record.byteLength;
|
|
2124
|
+
} else offset = writePreparedStoreValueRecord(record, bytes, offset);
|
|
2125
|
+
}
|
|
2126
|
+
return bytes;
|
|
2127
|
+
}
|
|
2128
|
+
async putIndexedMany(txId, store, items, defs, options) {
|
|
2129
|
+
let keys = [];
|
|
2130
|
+
let values = [];
|
|
2131
|
+
let indexOps = [];
|
|
2132
|
+
let bytes = 4;
|
|
2133
|
+
const pendingPrimaries = /* @__PURE__ */ new Set();
|
|
2134
|
+
const pendingUniqueKeys = /* @__PURE__ */ new Set();
|
|
2135
|
+
const flush = () => {
|
|
2136
|
+
if (keys.length === 0) return;
|
|
2137
|
+
const batchKeys = keys;
|
|
2138
|
+
const batchValues = values;
|
|
2139
|
+
const batchOps = indexOps;
|
|
2140
|
+
keys = [];
|
|
2141
|
+
values = [];
|
|
2142
|
+
indexOps = [];
|
|
2143
|
+
bytes = 4;
|
|
2144
|
+
pendingPrimaries.clear();
|
|
2145
|
+
pendingUniqueKeys.clear();
|
|
2146
|
+
const packet = this.packPreparedPutEntries(batchKeys, batchValues);
|
|
2147
|
+
let baselines;
|
|
2148
|
+
try {
|
|
2149
|
+
baselines = this.withEngine((engine) => engine.put_many_indexed_packed(toWasmU64(txId), store, packet, batchOps, toWasmPutOptions(options)));
|
|
2150
|
+
} catch (error) {
|
|
2151
|
+
const partial = partialBooleanOutcomes(error);
|
|
2152
|
+
if (partial.length > 0) this.recordPutKeyChanges(txId, store, batchKeys, partial);
|
|
2153
|
+
throw error;
|
|
2154
|
+
}
|
|
2155
|
+
if (baselines.length !== batchKeys.length) throw remoteError("InternalError", `put_many_indexed_packed outcome count mismatch: ${baselines.length} != ${batchKeys.length}`);
|
|
2156
|
+
this.recordPutKeyChanges(txId, store, batchKeys, baselines);
|
|
2157
|
+
};
|
|
2158
|
+
for (let index = 0; index < items.length / 2; index += 1) {
|
|
2159
|
+
const key = items[index * 2];
|
|
2160
|
+
const value = items[index * 2 + 1];
|
|
2161
|
+
const primaryToken = bytesToHex(key);
|
|
2162
|
+
if (pendingPrimaries.has(primaryToken)) flush();
|
|
2163
|
+
try {
|
|
2164
|
+
const oldValue = await this.readStoreValue(txId, store, key);
|
|
2165
|
+
const oldKeyFor = oldValue === null ? null : createIndexKeyExtractor(oldValue);
|
|
2166
|
+
const newKeyFor = createIndexKeyExtractor(value);
|
|
2167
|
+
const plans = defs.map((def) => ({
|
|
2168
|
+
def,
|
|
2169
|
+
oldLogicalKey: oldKeyFor === null ? null : oldKeyFor(def),
|
|
2170
|
+
newLogicalKey: newKeyFor(def),
|
|
2171
|
+
newPhysicalKey: null
|
|
2172
|
+
}));
|
|
2173
|
+
if (plans.some((plan) => plan.def.unique && plan.newLogicalKey !== null && pendingUniqueKeys.has(`${plan.def.internalStore}:${bytesToHex(plan.newLogicalKey)}`))) {
|
|
2174
|
+
flush();
|
|
2175
|
+
index -= 1;
|
|
2176
|
+
continue;
|
|
2177
|
+
}
|
|
2178
|
+
this.ensureInternalStoresForDefinitions(txId, defs);
|
|
2179
|
+
for (const plan of plans) {
|
|
2180
|
+
if (plan.newLogicalKey === null) continue;
|
|
2181
|
+
plan.newPhysicalKey = this.buildIndexEntryKey(plan.def, plan.newLogicalKey, key);
|
|
2182
|
+
if (plan.oldLogicalKey === null || !bytesEqual(plan.oldLogicalKey, plan.newLogicalKey)) await this.assertUniqueIndexAvailability(txId, plan.def, plan.newLogicalKey, key);
|
|
2183
|
+
}
|
|
2184
|
+
const preparedValue = this.preparePutValue(txId, store, value);
|
|
2185
|
+
const storedValue = preparedValue instanceof Promise ? await preparedValue : preparedValue;
|
|
2186
|
+
const ops = [];
|
|
2187
|
+
for (const plan of plans) if (plan.oldLogicalKey !== null && (plan.newLogicalKey === null || !bytesEqual(plan.oldLogicalKey, plan.newLogicalKey))) ops.push({
|
|
2188
|
+
store: plan.def.internalStore,
|
|
2189
|
+
kind: "delete",
|
|
2190
|
+
key: encodeIndexEntryKey(plan.oldLogicalKey, key)
|
|
2191
|
+
});
|
|
2192
|
+
for (const plan of plans) {
|
|
2193
|
+
if (plan.newPhysicalKey !== null) ops.push({
|
|
2194
|
+
store: plan.def.internalStore,
|
|
2195
|
+
kind: "put",
|
|
2196
|
+
key: plan.newPhysicalKey
|
|
2197
|
+
});
|
|
2198
|
+
if (plan.def.unique) {
|
|
2199
|
+
for (const logicalKey of [plan.oldLogicalKey, plan.newLogicalKey]) if (logicalKey !== null) pendingUniqueKeys.add(`${plan.def.internalStore}:${bytesToHex(logicalKey)}`);
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
keys.push(key);
|
|
2203
|
+
values.push(storedValue);
|
|
2204
|
+
indexOps.push(ops);
|
|
2205
|
+
bytes += 8 + key.byteLength + storedValue.byteLength;
|
|
2206
|
+
for (const op of ops) bytes += op.key.byteLength;
|
|
2207
|
+
pendingPrimaries.add(primaryToken);
|
|
2208
|
+
} catch (error) {
|
|
2209
|
+
flush();
|
|
2210
|
+
throw error;
|
|
2211
|
+
}
|
|
2212
|
+
if (options.ttl !== void 0 || keys.length >= INDEX_PUT_BATCH_ROWS || bytes >= INDEX_PUT_BATCH_BYTES) flush();
|
|
2213
|
+
}
|
|
2214
|
+
flush();
|
|
2215
|
+
}
|
|
1755
2216
|
async encodeBatchOpsForWrite(txId, store, ops) {
|
|
1756
2217
|
if (isInternalStoreName(store)) return ops;
|
|
1757
2218
|
const compression = this.storeCompressionForTx(txId, store);
|
|
@@ -1908,12 +2369,100 @@ var DbWorker = class {
|
|
|
1908
2369
|
}
|
|
1909
2370
|
return rows;
|
|
1910
2371
|
}
|
|
2372
|
+
readIndexKeyScan(txId, store, range) {
|
|
2373
|
+
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);
|
|
2374
|
+
const maxRows = range.limit ?? INDEX_SCAN_RAW_CHUNK_ROWS;
|
|
2375
|
+
const maxBytes = 4 + maxRows * 1032;
|
|
2376
|
+
const cursorId = this.withEngine((engine) => engine.open_scan_cursor(toWasmU64(txId), store, toWasmRange(range), true));
|
|
2377
|
+
try {
|
|
2378
|
+
const page = this.withEngine((engine) => engine.scan_cursor_next(cursorId, maxRows, maxBytes));
|
|
2379
|
+
const packet = normalizeWasmBytes(page.packet);
|
|
2380
|
+
const rows = unpackPackedScanRows(packet);
|
|
2381
|
+
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");
|
|
2382
|
+
return rows;
|
|
2383
|
+
} finally {
|
|
2384
|
+
this.withEngine((engine) => engine.close_scan_cursor(cursorId));
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
1911
2387
|
putRawValue(txId, store, key, value) {
|
|
1912
2388
|
this.withEngine((engine) => engine.put(toWasmU64(txId), store, key, value));
|
|
1913
2389
|
}
|
|
1914
2390
|
putIndexEntry(txId, def, physicalKey, value) {
|
|
2391
|
+
if (typeof this.engine?.put_index_entry_checked === "function") {
|
|
2392
|
+
const primaryKey = decodeIndexEntryKey(physicalKey).primaryKey;
|
|
2393
|
+
const state = this.readNativeValueState(txId, def.store, primaryKey);
|
|
2394
|
+
if (state?.revisionKnown) {
|
|
2395
|
+
this.withEngine((engine) => engine.put_index_entry_checked(toWasmU64(txId), def.store, def.internalStore, primaryKey, physicalKey, state.revisionEpoch, state.revisionOrdinal));
|
|
2396
|
+
return;
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
1915
2399
|
this.putRawValue(txId, def.internalStore, physicalKey, value);
|
|
1916
2400
|
}
|
|
2401
|
+
readNativeValueState(txId, store, key) {
|
|
2402
|
+
if (typeof this.engine?.get_many_value_states !== "function") return null;
|
|
2403
|
+
try {
|
|
2404
|
+
const keys = new Uint8Array(8 + key.byteLength);
|
|
2405
|
+
const keyView = new DataView(keys.buffer);
|
|
2406
|
+
keyView.setUint32(0, 1, true);
|
|
2407
|
+
keyView.setUint32(4, key.byteLength, true);
|
|
2408
|
+
keys.set(key, 8);
|
|
2409
|
+
const packet = normalizeWasmBytes(this.withEngine((engine) => engine.get_many_value_states(toWasmU64(txId), store, keys)));
|
|
2410
|
+
return this.decodeNativeValueStates(packet, 1)?.[0] ?? null;
|
|
2411
|
+
} catch {
|
|
2412
|
+
return null;
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2415
|
+
packNativeKeys(keys) {
|
|
2416
|
+
let total = 4 + keys.length * 4;
|
|
2417
|
+
for (const key of keys) total += key.byteLength;
|
|
2418
|
+
const packet = new Uint8Array(total);
|
|
2419
|
+
const view = new DataView(packet.buffer);
|
|
2420
|
+
view.setUint32(0, keys.length, true);
|
|
2421
|
+
let offset = 4 + keys.length * 4;
|
|
2422
|
+
for (let index = 0; index < keys.length; index += 1) {
|
|
2423
|
+
const key = keys[index];
|
|
2424
|
+
view.setUint32(4 + index * 4, key.byteLength, true);
|
|
2425
|
+
packet.set(key, offset);
|
|
2426
|
+
offset += key.byteLength;
|
|
2427
|
+
}
|
|
2428
|
+
return packet;
|
|
2429
|
+
}
|
|
2430
|
+
decodeNativeValueStates(packet, count) {
|
|
2431
|
+
if (packet.byteLength !== 4 + count * 32) return null;
|
|
2432
|
+
const view = new DataView(packet.buffer, packet.byteOffset, packet.byteLength);
|
|
2433
|
+
if (view.getUint32(0, true) !== count) return null;
|
|
2434
|
+
const states = new Array(count);
|
|
2435
|
+
for (let index = 0; index < count; index += 1) {
|
|
2436
|
+
const offset = 4 + index * 32;
|
|
2437
|
+
const flags = view.getUint32(offset, true);
|
|
2438
|
+
const revisionEpoch = view.getBigUint64(offset + 12, true);
|
|
2439
|
+
const revisionOrdinal = view.getBigUint64(offset + 20, true);
|
|
2440
|
+
if ((flags & -8) !== 0 || (flags & 3) === 3 || (flags & 4) !== 0 && ((flags & 3) === 0 || revisionEpoch === 0n || revisionOrdinal === 0n)) return null;
|
|
2441
|
+
states[index] = {
|
|
2442
|
+
live: (flags & 1) !== 0,
|
|
2443
|
+
expired: (flags & 2) !== 0,
|
|
2444
|
+
revisionKnown: (flags & 4) !== 0,
|
|
2445
|
+
expiresAt: view.getBigUint64(offset + 4, true),
|
|
2446
|
+
revisionEpoch,
|
|
2447
|
+
revisionOrdinal,
|
|
2448
|
+
maxValueBytes: view.getUint32(offset + 28, true)
|
|
2449
|
+
};
|
|
2450
|
+
}
|
|
2451
|
+
return states;
|
|
2452
|
+
}
|
|
2453
|
+
inspectIndexedValue(txId, def, physicalKey, primaryKey) {
|
|
2454
|
+
const primary = this.readNativeValueState(txId, def.store, primaryKey);
|
|
2455
|
+
if (primary === null) return "unknown";
|
|
2456
|
+
if (!primary.live) {
|
|
2457
|
+
if (primary.expired && this.txModes.get(txId) === "readwrite") {
|
|
2458
|
+
if (this.withEngine((engine) => engine.has(toWasmU64(txId), def.store, primaryKey))) return "unknown";
|
|
2459
|
+
}
|
|
2460
|
+
return "missing";
|
|
2461
|
+
}
|
|
2462
|
+
if (!primary.revisionKnown) return "unknown";
|
|
2463
|
+
const index = this.readNativeValueState(txId, def.internalStore, physicalKey);
|
|
2464
|
+
return index?.live && index.revisionKnown && index.revisionEpoch === primary.revisionEpoch && index.revisionOrdinal === primary.revisionOrdinal ? "trusted" : "unknown";
|
|
2465
|
+
}
|
|
1917
2466
|
deleteIndexEntryIfPresent(txId, def, logicalKey, primaryKey) {
|
|
1918
2467
|
const physicalKey = this.buildIndexEntryKey(def, logicalKey, primaryKey);
|
|
1919
2468
|
this.ignoreNamedError("StoreNotFoundError", () => {
|
|
@@ -1939,11 +2488,17 @@ var DbWorker = class {
|
|
|
1939
2488
|
range.gt = cursor;
|
|
1940
2489
|
delete range.gte;
|
|
1941
2490
|
}
|
|
1942
|
-
const rows = this.
|
|
2491
|
+
const rows = this.readIndexKeyScan(txId, def.internalStore, range);
|
|
1943
2492
|
for (const row of rows) {
|
|
1944
2493
|
cursor = row.key;
|
|
1945
2494
|
const decoded = decodeIndexEntryKey(row.key);
|
|
1946
2495
|
if (bytesEqual(decoded.primaryKey, primaryKey)) continue;
|
|
2496
|
+
const state = this.inspectIndexedValue(txId, def, row.key, decoded.primaryKey);
|
|
2497
|
+
if (state === "missing") {
|
|
2498
|
+
this.cleanupStaleIndexRow(txId, def, row.key);
|
|
2499
|
+
continue;
|
|
2500
|
+
}
|
|
2501
|
+
if (state === "trusted") throw remoteError("UniqueIndexConstraintError", `unique index constraint violation on ${def.store}.${def.name}`);
|
|
1947
2502
|
const value = await this.readStoreValue(txId, def.store, decoded.primaryKey);
|
|
1948
2503
|
if (value === null) {
|
|
1949
2504
|
this.cleanupStaleIndexRow(txId, def, row.key);
|
|
@@ -1965,12 +2520,13 @@ var DbWorker = class {
|
|
|
1965
2520
|
* chunks prevent stale entries from forcing the whole range into memory.
|
|
1966
2521
|
* The output limit counts only live rows.
|
|
1967
2522
|
*/
|
|
1968
|
-
async loadVisibleIndexPage(txId, def, range, cursor, limit) {
|
|
2523
|
+
async loadVisibleIndexPage(txId, def, range, cursor, limit, maxBytes = INDEX_SCAN_PAGE_BYTES) {
|
|
1969
2524
|
const { limit: _ignored, ...withoutLimit } = range;
|
|
1970
2525
|
const physical = indexRangeToPhysicalRange(withoutLimit);
|
|
1971
2526
|
const reverse = physical.reverse === true;
|
|
1972
2527
|
let resumeAfter = cursor;
|
|
1973
2528
|
const visible = [];
|
|
2529
|
+
let outputBytes = 4;
|
|
1974
2530
|
let staleChunkFloor = 1;
|
|
1975
2531
|
for (;;) {
|
|
1976
2532
|
const chunkLimit = Math.min(INDEX_SCAN_RAW_CHUNK_ROWS, Math.max(limit - visible.length, staleChunkFloor));
|
|
@@ -1989,7 +2545,7 @@ var DbWorker = class {
|
|
|
1989
2545
|
}
|
|
1990
2546
|
let rawRows;
|
|
1991
2547
|
try {
|
|
1992
|
-
rawRows = this.
|
|
2548
|
+
rawRows = this.readIndexKeyScan(txId, def.internalStore, chunkRange);
|
|
1993
2549
|
} catch (error) {
|
|
1994
2550
|
if (isNamedError(error, "StoreNotFoundError")) return {
|
|
1995
2551
|
rows: [],
|
|
@@ -2004,14 +2560,28 @@ var DbWorker = class {
|
|
|
2004
2560
|
for (let index = 0; index < rawRows.length; index += 1) {
|
|
2005
2561
|
if (index === batchEnd) {
|
|
2006
2562
|
batchStart = index;
|
|
2007
|
-
|
|
2008
|
-
|
|
2563
|
+
batch = this.readIndexPrimaryValues(txId, def, rawRows, index, Math.min(rawRows.length, index + limit - visible.length), maxBytes - outputBytes);
|
|
2564
|
+
batchEnd = index + (batch?.entries.length ?? 1);
|
|
2009
2565
|
}
|
|
2010
2566
|
const row = rawRows[index];
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
if (!resolved)
|
|
2567
|
+
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);
|
|
2568
|
+
if (batch !== null) batch.values[index - batchStart] = null;
|
|
2569
|
+
if (!resolved) {
|
|
2570
|
+
resumeAfter = row.key;
|
|
2571
|
+
continue;
|
|
2572
|
+
}
|
|
2573
|
+
const nextBytes = outputBytes + 8 + resolved.key.byteLength + resolved.value.byteLength;
|
|
2574
|
+
if (nextBytes > maxBytes) {
|
|
2575
|
+
if (visible.length === 0) throw remoteError("ValueTooLargeError", "indexed scan row exceeds maxBytes");
|
|
2576
|
+
if (resumeAfter === null) throw remoteError("InternalError", "indexed scan lost its continuation key");
|
|
2577
|
+
return {
|
|
2578
|
+
rows: visible,
|
|
2579
|
+
cursor: resumeAfter.slice()
|
|
2580
|
+
};
|
|
2581
|
+
}
|
|
2014
2582
|
visible.push(resolved);
|
|
2583
|
+
outputBytes = nextBytes;
|
|
2584
|
+
resumeAfter = row.key;
|
|
2015
2585
|
if (visible.length >= limit) return {
|
|
2016
2586
|
rows: visible,
|
|
2017
2587
|
cursor: row.key.slice()
|
|
@@ -2024,37 +2594,91 @@ var DbWorker = class {
|
|
|
2024
2594
|
staleChunkFloor = visible.length === visibleBefore ? Math.min(INDEX_SCAN_RAW_CHUNK_ROWS, chunkLimit * 2) : 1;
|
|
2025
2595
|
}
|
|
2026
2596
|
}
|
|
2027
|
-
readIndexPrimaryValues(txId, def, rows, start, end) {
|
|
2028
|
-
if (this.txModes.get(txId) !== "readonly" || end - start <= 1) return null;
|
|
2597
|
+
readIndexPrimaryValues(txId, def, rows, start, end, remainingBytes) {
|
|
2598
|
+
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
2599
|
let entries;
|
|
2030
2600
|
let values;
|
|
2601
|
+
let trusted;
|
|
2602
|
+
let loaded;
|
|
2603
|
+
let selectedIndices;
|
|
2031
2604
|
try {
|
|
2032
2605
|
entries = rows.slice(start, end).map((row) => decodeIndexEntryKey(row.key));
|
|
2033
|
-
|
|
2606
|
+
const packedKeys = this.packNativeKeys(entries.map((entry) => entry.primaryKey));
|
|
2607
|
+
let primaryStates = null;
|
|
2608
|
+
let indexStates = null;
|
|
2609
|
+
let sizes;
|
|
2610
|
+
if (typeof this.engine?.get_many_value_states === "function") {
|
|
2611
|
+
primaryStates = this.decodeNativeValueStates(normalizeWasmBytes(this.withEngine((engine) => engine.get_many_value_states(toWasmU64(txId), def.store, packedKeys))), entries.length);
|
|
2612
|
+
if (primaryStates === null) return null;
|
|
2613
|
+
sizes = Uint32Array.from(primaryStates, (state) => state.live ? state.maxValueBytes : MAX_PACKED_BYTES);
|
|
2614
|
+
if (primaryStates.some((state) => state.live && state.revisionKnown)) {
|
|
2615
|
+
const indexKeys = this.packNativeKeys(rows.slice(start, end).map((row) => row.key));
|
|
2616
|
+
indexStates = this.decodeNativeValueStates(normalizeWasmBytes(this.withEngine((engine) => engine.get_many_value_states(toWasmU64(txId), def.internalStore, indexKeys))), entries.length);
|
|
2617
|
+
if (indexStates === null) return null;
|
|
2618
|
+
}
|
|
2619
|
+
} else sizes = this.withEngine((engine) => engine.get_many_value_sizes(toWasmU64(txId), def.store, packedKeys));
|
|
2620
|
+
if (sizes.length !== entries.length) return null;
|
|
2621
|
+
let selected = 0;
|
|
2622
|
+
for (const entry of entries) {
|
|
2623
|
+
const size = sizes[selected];
|
|
2624
|
+
if (!Number.isSafeInteger(size) || size < 0 || size > MAX_PACKED_BYTES) return null;
|
|
2625
|
+
const required = size === MAX_PACKED_BYTES ? 0 : 8 + entry.primaryKey.byteLength + size;
|
|
2626
|
+
if (required > remainingBytes) break;
|
|
2627
|
+
remainingBytes -= required;
|
|
2628
|
+
selected += 1;
|
|
2629
|
+
}
|
|
2630
|
+
if (selected <= 1) return null;
|
|
2631
|
+
entries.length = selected;
|
|
2632
|
+
trusted = new Array(selected).fill(false);
|
|
2633
|
+
const selectedKeys = [];
|
|
2634
|
+
selectedIndices = [];
|
|
2635
|
+
values = new Array(selected).fill(null);
|
|
2636
|
+
for (let index = 0; index < selected; index += 1) {
|
|
2637
|
+
const primary = primaryStates?.[index];
|
|
2638
|
+
if (primary === void 0 || primary.live) {
|
|
2639
|
+
selectedKeys.push(entries[index].primaryKey);
|
|
2640
|
+
selectedIndices.push(index);
|
|
2641
|
+
}
|
|
2642
|
+
const proof = indexStates?.[index];
|
|
2643
|
+
trusted[index] = Boolean(primary?.live && primary.revisionKnown && proof?.live && proof.revisionKnown && primary.revisionEpoch === proof.revisionEpoch && primary.revisionOrdinal === proof.revisionOrdinal);
|
|
2644
|
+
}
|
|
2645
|
+
loaded = selectedKeys.length === 0 ? [] : this.withEngine((engine) => engine.get_many(toWasmU64(txId), def.store, selectedKeys));
|
|
2034
2646
|
} catch {
|
|
2035
2647
|
return null;
|
|
2036
2648
|
}
|
|
2037
|
-
if (
|
|
2649
|
+
if (loaded.length !== selectedIndices.length) throw remoteError("InternalError", `get_many outcome count mismatch: ${loaded.length} != ${selectedIndices.length}`);
|
|
2650
|
+
for (let index = 0; index < loaded.length; index += 1) {
|
|
2651
|
+
values[selectedIndices[index]] = loaded[index];
|
|
2652
|
+
loaded[index] = null;
|
|
2653
|
+
}
|
|
2038
2654
|
return {
|
|
2039
2655
|
entries,
|
|
2040
|
-
values
|
|
2656
|
+
values,
|
|
2657
|
+
trusted
|
|
2041
2658
|
};
|
|
2042
2659
|
}
|
|
2043
2660
|
async resolveVisibleIndexedRow(txId, def, physicalKey) {
|
|
2044
2661
|
const decoded = decodeIndexEntryKey(physicalKey);
|
|
2662
|
+
const state = this.inspectIndexedValue(txId, def, physicalKey, decoded.primaryKey);
|
|
2663
|
+
if (state === "missing") {
|
|
2664
|
+
this.cleanupStaleIndexRow(txId, def, physicalKey);
|
|
2665
|
+
return null;
|
|
2666
|
+
}
|
|
2045
2667
|
const value = this.readRawValue(txId, def.store, decoded.primaryKey);
|
|
2046
|
-
return this.resolveVisibleIndexedValue(txId, def, physicalKey, decoded, value);
|
|
2668
|
+
return this.resolveVisibleIndexedValue(txId, def, physicalKey, decoded, value, state === "trusted");
|
|
2047
2669
|
}
|
|
2048
|
-
async resolveVisibleIndexedValue(txId, def, physicalKey, decoded, rawValue) {
|
|
2670
|
+
async resolveVisibleIndexedValue(txId, def, physicalKey, decoded, rawValue, trusted = false) {
|
|
2049
2671
|
if (rawValue === null) {
|
|
2050
2672
|
this.cleanupStaleIndexRow(txId, def, physicalKey);
|
|
2051
2673
|
return null;
|
|
2052
2674
|
}
|
|
2053
2675
|
const value = await this.decodeStoreValue(txId, def.store, normalizeWasmBytes(rawValue));
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2676
|
+
if (!trusted) {
|
|
2677
|
+
const currentLogicalKey = extractLogicalIndexKey(def, value);
|
|
2678
|
+
if (currentLogicalKey === null || !bytesEqual(currentLogicalKey, decoded.logicalKey)) {
|
|
2679
|
+
this.cleanupStaleIndexRow(txId, def, physicalKey);
|
|
2680
|
+
return null;
|
|
2681
|
+
}
|
|
2058
2682
|
}
|
|
2059
2683
|
return {
|
|
2060
2684
|
key: decoded.primaryKey,
|
|
@@ -2098,6 +2722,7 @@ var DbWorker = class {
|
|
|
2098
2722
|
}
|
|
2099
2723
|
}
|
|
2100
2724
|
clearTxCaches() {
|
|
2725
|
+
this.primaryCursors.clear();
|
|
2101
2726
|
this.txChanges.clear();
|
|
2102
2727
|
this.txModes.clear();
|
|
2103
2728
|
this.txIndexSchemas.clear();
|