@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.
Files changed (59) hide show
  1. package/CONTRIBUTING.md +46 -0
  2. package/README.md +144 -0
  3. package/dist/assets/browser-worker-Bj2ErRvC.js +4 -0
  4. package/dist/assets/browser-worker-Bj2ErRvC.js.map +1 -0
  5. package/dist/assets/shared-worker-CMeTcusa.js +2 -0
  6. package/dist/assets/shared-worker-CMeTcusa.js.map +1 -0
  7. package/dist/chunks/{indexing-BzmFhWXs.js → indexing-B0duqlUs.js} +131 -124
  8. package/dist/chunks/indexing-B0duqlUs.js.map +1 -0
  9. package/dist/compression.d.ts +16 -2
  10. package/dist/compression.d.ts.map +1 -1
  11. package/dist/demo.d.ts.map +1 -1
  12. package/dist/engine/README.md +32 -3
  13. package/dist/engine/moyodb_engine.d.ts +36 -10
  14. package/dist/engine/moyodb_engine.js +261 -51
  15. package/dist/engine/moyodb_engine_bg.wasm +0 -0
  16. package/dist/engine/moyodb_engine_bg.wasm.d.ts +18 -10
  17. package/dist/engine/package.json +1 -1
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/indexing.d.ts.map +1 -1
  20. package/dist/internal.d.ts +6 -0
  21. package/dist/internal.d.ts.map +1 -1
  22. package/dist/moyodb-sdk.js +1069 -835
  23. package/dist/moyodb-sdk.js.map +1 -1
  24. package/dist/node-engine.js +731 -111
  25. package/dist/node-engine.js.map +1 -1
  26. package/dist/node-storage.d.mts +4 -0
  27. package/dist/node-storage.mjs +38 -3
  28. package/dist/node-worker-client.d.ts +1 -0
  29. package/dist/node-worker-client.d.ts.map +1 -1
  30. package/dist/node-worker.mjs +2 -1
  31. package/dist/node.d.ts.map +1 -1
  32. package/dist/node.js +302 -67
  33. package/dist/node.js.map +1 -1
  34. package/dist/registry.d.ts.map +1 -1
  35. package/dist/shared-worker-client.d.ts +4 -0
  36. package/dist/shared-worker-client.d.ts.map +1 -1
  37. package/dist/shared-worker.d.ts.map +1 -1
  38. package/dist/snappy.d.ts +3 -0
  39. package/dist/snappy.d.ts.map +1 -0
  40. package/dist/sql-parser.d.ts.map +1 -1
  41. package/dist/types.d.ts +33 -3
  42. package/dist/types.d.ts.map +1 -1
  43. package/dist/worker-api.d.ts +19 -6
  44. package/dist/worker-api.d.ts.map +1 -1
  45. package/dist/worker-client.d.ts +7 -2
  46. package/dist/worker-client.d.ts.map +1 -1
  47. package/dist/worker-protocol.d.ts +18 -3
  48. package/dist/worker-protocol.d.ts.map +1 -1
  49. package/dist/worker-server.d.ts.map +1 -1
  50. package/dist/worker.d.ts +33 -4
  51. package/dist/worker.d.ts.map +1 -1
  52. package/package.json +6 -5
  53. package/dist/assets/browser-worker-DP6Mjz7c.js +0 -4
  54. package/dist/assets/browser-worker-DP6Mjz7c.js.map +0 -1
  55. package/dist/assets/shared-worker-DKzhauVf.js +0 -2
  56. package/dist/assets/shared-worker-DKzhauVf.js.map +0 -1
  57. package/dist/chunks/indexing-BzmFhWXs.js.map +0 -1
  58. package/dist/engine/snippets/moyodb-engine-cff0a0e2441a7a8f/js/opfs_shim.js +0 -513
  59. /package/dist/engine/snippets/{moyodb-engine-8ebe6af0cc2e81b7 → moyodb-engine-1d53039816cbc2f7}/js/opfs_shim.js +0 -0
@@ -1,4 +1,4 @@
1
- import { A as prefixRange, At as packedBinaryListBytes, Bt as unpackPackedOptionalValues, Et as isWorkerProtocolRequestMessage, F as utf8Encode, Ft as unpackPackedBatchOpKeys, It as unpackPackedBatchOps, Lt as unpackPackedBinaryList, Nt as prepareWorkerResponsePayload, Pt as serializeWorkerError, Rt as unpackPackedBinaryPairKeys, S as toPublicIndexDefinitions, St as isWorkerProtocolEnvelope, Tt as isWorkerProtocolRequestBatchMessage, Ut as isRecord, Vt as workerProtocolError, _ 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 unpackPackedBinaryPairs } from "./chunks/indexing-BzmFhWXs.js";
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)) return this.#runExclusive(task);
58
- return this.#runShared(laneFor(command, args), task);
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(256);
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
- for (const byte of bytes) crc = crc >>> 8 ^ CRC32_TABLE[(crc ^ byte) & 255];
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.set(magic, 0);
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 (value.byteLength > MAX_DECOMPRESSED_STORE_VALUE_BYTES) throw namedError("ValueTooLargeError", `value has ${value.byteLength} bytes, exceeding the ${MAX_DECOMPRESSED_STORE_VALUE_BYTES} byte limit`);
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
- const compressed = await compressBytes(input, compression);
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
- return typeof value === "bigint" ? Number(value) : value;
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 ?? false;
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
- const oldValue = await this.readStoreValue(txId, store, key);
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
- if (this.indexesForStoreInTx(txId, store).length === 0) {
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 remoteError("InternalError", `put_many outcome count mismatch: ${baselines.length} != ${entries.length}`);
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
- for (const [key, value] of entries) await this.put(txId, store, key, value, options);
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
- if (this.indexesForStoreInTx(txId, store).length !== 0 || this.storeCompressionForTx(txId, store) !== false) {
1113
- await this.putMany(txId, store, unpackPackedBinaryPairs(packedEntries), options);
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 keys = unpackPackedBinaryPairKeys(packedEntries);
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, packedEntries, toWasmPutOptions(options)));
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 remoteError("InternalError", `put_many_packed outcome count mismatch: ${baselines.length} != ${keys.length}`);
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 remoteError("InternalError", `delete_many outcome count mismatch: ${deleted.length} != ${keys.length}`);
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 remoteError("InternalError", `delete_many_packed outcome count mismatch: ${deleted.length} != ${keys.length}`);
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 remoteError("InternalError", `apply_batch outcome count mismatch: ${outcomes.length} != ${ops.length}`);
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.recordBatchKeyOutcomes(txId, store, ops, partialBatchOutcomes(error));
1471
+ this.recordBatchOutcomes(txId, store, ops, partialBatchOutcomes(error));
1213
1472
  throw error;
1214
1473
  }
1215
- if (flags.length !== ops.length) throw remoteError("InternalError", `apply_batch_packed outcome count mismatch: ${flags.length} != ${ops.length}`);
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, prefixRange(key), null, 1);
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 remoteError("InternalError", `has_many outcome count mismatch: ${visible.length} != ${sourceRows.length}`);
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.rollbackAllTransactions();
1702
+ this.ensureNotInMaintenance();
1370
1703
  const parsed = unwrapSnapshotWithIndexDefinitions(await unwrapSnapshotCompression(data));
1371
- this.withEngine((engine) => engine.import_snapshot(parsed.snapshot));
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.runCompactionOperation("compact");
1732
+ return this.runGenerationOperation("compact");
1403
1733
  }
1404
1734
  async rebuild() {
1405
- return this.runCompactionOperation("rebuild");
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: fromWasmU64(stats.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 runCompactionOperation(operation) {
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
- const wasm = await this.loadWasm();
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.readRawScan(txId, def.internalStore, range);
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.readRawScan(txId, def.internalStore, chunkRange);
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
- batchEnd = Math.min(rawRows.length, index + limit - visible.length);
2008
- batch = this.readIndexPrimaryValues(txId, def, rawRows, batchStart, batchEnd);
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
- resumeAfter = row.key;
2012
- const resolved = batch ? await this.resolveVisibleIndexedValue(txId, def, row.key, batch.entries[index - batchStart], batch.values[index - batchStart]) : await this.resolveVisibleIndexedRow(txId, def, row.key);
2013
- if (!resolved) continue;
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
- values = this.withEngine((engine) => engine.get_many(toWasmU64(txId), def.store, entries.map((entry) => entry.primaryKey)));
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 (values.length !== entries.length) throw remoteError("InternalError", `get_many outcome count mismatch: ${values.length} != ${entries.length}`);
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
- const currentLogicalKey = extractLogicalIndexKey(def, value);
2055
- if (currentLogicalKey === null || !bytesEqual(currentLogicalKey, decoded.logicalKey)) {
2056
- this.cleanupStaleIndexRow(txId, def, physicalKey);
2057
- return null;
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");