@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.
Files changed (56) hide show
  1. package/README.md +142 -0
  2. package/dist/assets/browser-worker-DBt51gsS.js +4 -0
  3. package/dist/assets/browser-worker-DBt51gsS.js.map +1 -0
  4. package/dist/assets/shared-worker-NdniRuEL.js +2 -0
  5. package/dist/assets/shared-worker-NdniRuEL.js.map +1 -0
  6. package/dist/chunks/{indexing-BzmFhWXs.js → indexing-CKtbpaZn.js} +93 -12
  7. package/dist/chunks/indexing-CKtbpaZn.js.map +1 -0
  8. package/dist/compression.d.ts +16 -2
  9. package/dist/compression.d.ts.map +1 -1
  10. package/dist/engine/README.md +30 -3
  11. package/dist/engine/moyodb_engine.d.ts +34 -10
  12. package/dist/engine/moyodb_engine.js +238 -51
  13. package/dist/engine/moyodb_engine_bg.wasm +0 -0
  14. package/dist/engine/moyodb_engine_bg.wasm.d.ts +17 -10
  15. package/dist/engine/package.json +1 -1
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/internal.d.ts +6 -0
  18. package/dist/internal.d.ts.map +1 -1
  19. package/dist/moyodb-sdk.js +1122 -883
  20. package/dist/moyodb-sdk.js.map +1 -1
  21. package/dist/node-engine.js +703 -78
  22. package/dist/node-engine.js.map +1 -1
  23. package/dist/node-storage.d.mts +4 -0
  24. package/dist/node-storage.mjs +38 -3
  25. package/dist/node-worker-client.d.ts +1 -0
  26. package/dist/node-worker-client.d.ts.map +1 -1
  27. package/dist/node-worker.mjs +2 -1
  28. package/dist/node.d.ts.map +1 -1
  29. package/dist/node.js +280 -37
  30. package/dist/node.js.map +1 -1
  31. package/dist/registry.d.ts.map +1 -1
  32. package/dist/shared-worker-client.d.ts +4 -0
  33. package/dist/shared-worker-client.d.ts.map +1 -1
  34. package/dist/shared-worker.d.ts.map +1 -1
  35. package/dist/snappy.d.ts +3 -0
  36. package/dist/snappy.d.ts.map +1 -0
  37. package/dist/sql-parser.d.ts.map +1 -1
  38. package/dist/types.d.ts +33 -3
  39. package/dist/types.d.ts.map +1 -1
  40. package/dist/worker-api.d.ts +17 -1
  41. package/dist/worker-api.d.ts.map +1 -1
  42. package/dist/worker-client.d.ts +7 -2
  43. package/dist/worker-client.d.ts.map +1 -1
  44. package/dist/worker-protocol.d.ts +11 -1
  45. package/dist/worker-protocol.d.ts.map +1 -1
  46. package/dist/worker-server.d.ts.map +1 -1
  47. package/dist/worker.d.ts +31 -2
  48. package/dist/worker.d.ts.map +1 -1
  49. package/package.json +4 -4
  50. package/dist/assets/browser-worker-DP6Mjz7c.js +0 -4
  51. package/dist/assets/browser-worker-DP6Mjz7c.js.map +0 -1
  52. package/dist/assets/shared-worker-DKzhauVf.js +0 -2
  53. package/dist/assets/shared-worker-DKzhauVf.js.map +0 -1
  54. package/dist/chunks/indexing-BzmFhWXs.js.map +0 -1
  55. package/dist/engine/snippets/moyodb-engine-cff0a0e2441a7a8f/js/opfs_shim.js +0 -513
  56. /package/dist/engine/snippets/{moyodb-engine-8ebe6af0cc2e81b7 → moyodb-engine-54a406e33b0fb849}/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-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)) 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`);
@@ -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 ?? false;
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
- 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);
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
- if (this.indexesForStoreInTx(txId, store).length === 0) {
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
- for (const [key, value] of entries) await this.put(txId, store, key, value, options);
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
- if (this.indexesForStoreInTx(txId, store).length !== 0 || this.storeCompressionForTx(txId, store) !== false) {
1113
- await this.putMany(txId, store, unpackPackedBinaryPairs(packedEntries), options);
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 keys = unpackPackedBinaryPairKeys(packedEntries);
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, packedEntries, toWasmPutOptions(options)));
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, prefixRange(key), null, 1);
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.readRawScan(txId, def.internalStore, range);
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.readRawScan(txId, def.internalStore, chunkRange);
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
- batchEnd = Math.min(rawRows.length, index + limit - visible.length);
2008
- batch = this.readIndexPrimaryValues(txId, def, rawRows, batchStart, batchEnd);
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
- 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;
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
- values = this.withEngine((engine) => engine.get_many(toWasmU64(txId), def.store, entries.map((entry) => entry.primaryKey)));
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 (values.length !== entries.length) throw remoteError("InternalError", `get_many outcome count mismatch: ${values.length} != ${entries.length}`);
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
- const currentLogicalKey = extractLogicalIndexKey(def, value);
2055
- if (currentLogicalKey === null || !bytesEqual(currentLogicalKey, decoded.logicalKey)) {
2056
- this.cleanupStaleIndexRow(txId, def, physicalKey);
2057
- return null;
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();