@moyodb/sdk 1.3.0 → 1.5.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 (43) hide show
  1. package/CONTRIBUTING.md +46 -0
  2. package/README.md +4 -2
  3. package/dist/assets/browser-worker-p7uX85l-.js +4 -0
  4. package/dist/assets/browser-worker-p7uX85l-.js.map +1 -0
  5. package/dist/assets/shared-worker-BALTqEUe.js +2 -0
  6. package/dist/assets/shared-worker-BALTqEUe.js.map +1 -0
  7. package/dist/chunks/{indexing-CKtbpaZn.js → indexing-DdpU69Fd.js} +344 -185
  8. package/dist/chunks/indexing-DdpU69Fd.js.map +1 -0
  9. package/dist/codec.d.ts.map +1 -1
  10. package/dist/compression.d.ts.map +1 -1
  11. package/dist/demo.d.ts.map +1 -1
  12. package/dist/engine/README.md +4 -2
  13. package/dist/engine/moyodb_engine.d.ts +4 -2
  14. package/dist/engine/moyodb_engine.js +45 -22
  15. package/dist/engine/moyodb_engine_bg.wasm +0 -0
  16. package/dist/engine/moyodb_engine_bg.wasm.d.ts +3 -2
  17. package/dist/engine/package.json +1 -1
  18. package/dist/engine/snippets/{moyodb-engine-54a406e33b0fb849 → moyodb-engine-1f8134232168280b}/js/opfs_shim.js +8 -2
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/indexing.d.ts +7 -0
  21. package/dist/indexing.d.ts.map +1 -1
  22. package/dist/moyodb-sdk.js +1265 -872
  23. package/dist/moyodb-sdk.js.map +1 -1
  24. package/dist/node-engine.js +411 -81
  25. package/dist/node-engine.js.map +1 -1
  26. package/dist/node.js +354 -74
  27. package/dist/node.js.map +1 -1
  28. package/dist/registry.d.ts.map +1 -1
  29. package/dist/subscriptions.d.ts.map +1 -1
  30. package/dist/worker-api.d.ts +2 -5
  31. package/dist/worker-api.d.ts.map +1 -1
  32. package/dist/worker-client.d.ts.map +1 -1
  33. package/dist/worker-protocol.d.ts +7 -2
  34. package/dist/worker-protocol.d.ts.map +1 -1
  35. package/dist/worker-server.d.ts.map +1 -1
  36. package/dist/worker.d.ts +12 -5
  37. package/dist/worker.d.ts.map +1 -1
  38. package/package.json +5 -3
  39. package/dist/assets/browser-worker-DBt51gsS.js +0 -4
  40. package/dist/assets/browser-worker-DBt51gsS.js.map +0 -1
  41. package/dist/assets/shared-worker-NdniRuEL.js +0 -2
  42. package/dist/assets/shared-worker-NdniRuEL.js.map +0 -1
  43. package/dist/chunks/indexing-CKtbpaZn.js.map +0 -1
@@ -1,4 +1,4 @@
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";
1
+ import { Bt as unpackPackedBinaryList, Ct as isWorkerCommand, Dt as isWorkerProtocolRequestBatchMessage, Gt as clampTimerDelayMs, Ht as unpackPackedOptionalValues, It as prepareWorkerResponsePayload, L as utf8Encode, Lt as serializeWorkerError, Mt as packedBinaryListBytes, Nt as packedOptionalValues, Ot as isWorkerProtocolRequestMessage, Pt as packedScanRows, Rt as unpackPackedBatchOpKeys, S as normalizeIndexDefinitions, St as isAutocommitCommand, Ut as unpackPackedScanRows, Vt as unpackPackedBinaryPairKeys, Wt as workerProtocolError, _t as WORKER_PROTOCOL_RESPONSE_BATCH, a as cloneNormalizedIndexDefinitions, c as decodeIndexEntryKey, d as encodeIndexEntryKey, f as encodeIndexMetadataKey, g as findIndexDefinition, gt as WORKER_PROTOCOL_RESPONSE, h as extractLogicalIndexKeyFromDocument, i as assertValidScanRange, jt as packedBatchOpsBytes, l as decodeIndexMetadataValue, m as extractLogicalIndexKey, o as compareNormalizedIndexDefinitions, p as encodeIndexMetadataValue, pt as WORKER_PROTOCOL_READY, qt as isRecord, s as createIndexKeyExtractor, t as INDEX_METADATA_STORE, u as decodeIndexedDocument, v as indexKeyExactRange, vt as captureWorkerPayload, w as toPublicIndexDefinitions, wt as isWorkerProtocolEnvelope, x as isInternalStoreName, y as indexRangeToPhysicalRange, yt as decodeWorkerCommandPayload, zt as unpackPackedBatchOps } from "./chunks/indexing-DdpU69Fd.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([
@@ -128,10 +128,20 @@ var ResponseQueue = class {
128
128
  constructor(scope) {
129
129
  this.scope = scope;
130
130
  }
131
- enqueue = (response, transfer = []) => {
131
+ enqueue(response, transfer = [], flushNow = false) {
132
132
  if (this.closed) return;
133
- const captured = captureWorkerPayload(response, transfer);
134
- if (this.queued.length > 0 && this.queuedBytes + captured.byteLength > 1048576) this.flush();
133
+ const byteLength = payloadBytes(response);
134
+ if (this.queued.length > 0 && this.queuedBytes + byteLength > 1048576) this.flush();
135
+ if (flushNow || this.queued.length + 1 >= 128 || this.queuedBytes + byteLength >= 1048576) {
136
+ this.queued.push({
137
+ response,
138
+ transfer
139
+ });
140
+ this.queuedBytes += byteLength;
141
+ this.flush();
142
+ return;
143
+ }
144
+ const captured = snapshotQueuedPayload(response, transfer);
135
145
  this.queued.push({
136
146
  response: captured.value,
137
147
  transfer: captured.transfer
@@ -148,8 +158,7 @@ var ResponseQueue = class {
148
158
  this.flushChannel.port2.postMessage(null);
149
159
  });
150
160
  }
151
- if (this.queued.length >= 128 || this.queuedBytes >= 1048576) this.flush();
152
- };
161
+ }
153
162
  close() {
154
163
  this.closed = true;
155
164
  this.queued = [];
@@ -190,9 +199,8 @@ function exposeWorkerApi(api, scope = self) {
190
199
  if (isWorkerProtocolRequestBatchMessage(event.data)) {
191
200
  let remaining = event.data.requests.length;
192
201
  const respond = (response, transfer) => {
193
- responses.enqueue(response, transfer);
194
202
  remaining -= 1;
195
- if (remaining === 0) responses.flush();
203
+ responses.enqueue(response, transfer, remaining === 0);
196
204
  };
197
205
  for (const request of event.data.requests) dispatchWorkerRequest(scope, api, scheduler, request, respond);
198
206
  return;
@@ -316,6 +324,203 @@ function postWorkerResponse(scope, response, transfer = []) {
316
324
  } catch {}
317
325
  scope.postMessage(response);
318
326
  }
327
+ var STANDARD_VIEW_CONSTRUCTORS = /* @__PURE__ */ new Set([
328
+ DataView,
329
+ Uint8Array,
330
+ Uint8ClampedArray,
331
+ Uint16Array,
332
+ Uint32Array,
333
+ Int8Array,
334
+ Int16Array,
335
+ Int32Array,
336
+ Float32Array,
337
+ Float64Array,
338
+ BigInt64Array,
339
+ BigUint64Array
340
+ ]);
341
+ /**
342
+ * Freeze a queued payload so the later postMessage is its only structured clone.
343
+ * Listed buffers move now. Other ArrayBuffers are copied. SharedArrayBuffers stay shared.
344
+ */
345
+ function snapshotQueuedPayload(value, transfer) {
346
+ const moving = /* @__PURE__ */ new Set();
347
+ for (const item of transfer) {
348
+ if (!(item instanceof ArrayBuffer) || isResizableBuffer(item)) return captureWorkerPayload(value, transfer);
349
+ moving.add(item);
350
+ }
351
+ const geometries = /* @__PURE__ */ new Map();
352
+ if (!isPlainPayload(value, geometries, /* @__PURE__ */ new WeakSet())) return captureWorkerPayload(value, transfer);
353
+ const relocated = /* @__PURE__ */ new Map();
354
+ for (const buffer of moving) relocated.set(buffer, moveArrayBuffer(buffer));
355
+ const owned = [];
356
+ const seen = /* @__PURE__ */ new WeakMap();
357
+ const cloneValue = (item) => {
358
+ if (item === null || typeof item !== "object") return item;
359
+ const existing = seen.get(item);
360
+ if (existing !== void 0) return existing;
361
+ if (typeof SharedArrayBuffer !== "undefined" && item instanceof SharedArrayBuffer) {
362
+ seen.set(item, item);
363
+ return item;
364
+ }
365
+ if (item instanceof ArrayBuffer) {
366
+ const next = relocated.get(item) ?? item.slice(0);
367
+ seen.set(item, next);
368
+ owned.push(next);
369
+ return next;
370
+ }
371
+ if (ArrayBuffer.isView(item)) {
372
+ const geometry = geometries.get(item);
373
+ if (!geometry) throw workerProtocolError("WorkerProtocolError", "worker payload could not be queued");
374
+ const buffer = item.buffer;
375
+ const out = retargetView(item, typeof SharedArrayBuffer !== "undefined" && buffer instanceof SharedArrayBuffer ? buffer : cloneValue(buffer), geometry);
376
+ seen.set(item, out);
377
+ return out;
378
+ }
379
+ if (Array.isArray(item)) {
380
+ const out = new Array(item.length);
381
+ seen.set(item, out);
382
+ for (let index = 0; index < item.length; index += 1) if (index in item) out[index] = cloneValue(item[index]);
383
+ return out;
384
+ }
385
+ const out = Object.getPrototypeOf(item) === null ? Object.create(null) : {};
386
+ seen.set(item, out);
387
+ const record = item;
388
+ for (const key in record) if (Object.prototype.hasOwnProperty.call(record, key)) out[key] = cloneValue(record[key]);
389
+ return out;
390
+ };
391
+ const cloned = cloneValue(value);
392
+ return {
393
+ value: cloned,
394
+ transfer: owned,
395
+ byteLength: payloadBytes(cloned)
396
+ };
397
+ }
398
+ function payloadBytes(value) {
399
+ let byteLength = 0;
400
+ const seen = /* @__PURE__ */ new WeakSet();
401
+ const visit = (item) => {
402
+ if (!Number.isFinite(byteLength)) return;
403
+ if (typeof item === "string") {
404
+ byteLength += 8 + item.length * 2;
405
+ return;
406
+ }
407
+ if (item === null || typeof item !== "object") {
408
+ byteLength += 8;
409
+ return;
410
+ }
411
+ if (seen.has(item)) return;
412
+ seen.add(item);
413
+ byteLength += 8;
414
+ if (item instanceof ArrayBuffer) byteLength += item.byteLength;
415
+ else if (typeof SharedArrayBuffer !== "undefined" && item instanceof SharedArrayBuffer) byteLength += item.byteLength;
416
+ else if (ArrayBuffer.isView(item)) visit(item.buffer);
417
+ else if (Array.isArray(item)) {
418
+ if (Object.getPrototypeOf(item) !== Array.prototype || hasExtraKeys(item)) {
419
+ byteLength = Number.POSITIVE_INFINITY;
420
+ return;
421
+ }
422
+ for (let index = 0; index < item.length; index += 1) {
423
+ const descriptor = Object.getOwnPropertyDescriptor(item, String(index));
424
+ if (descriptor && !("value" in descriptor)) {
425
+ byteLength = Number.POSITIVE_INFINITY;
426
+ return;
427
+ }
428
+ visit(descriptor?.value);
429
+ }
430
+ } else if (item instanceof Map) for (const [key, child] of item) {
431
+ visit(key);
432
+ visit(child);
433
+ }
434
+ else if (item instanceof Set) for (const child of item) visit(child);
435
+ else {
436
+ const record = item;
437
+ for (const key in record) if (Object.prototype.hasOwnProperty.call(record, key)) {
438
+ const descriptor = Object.getOwnPropertyDescriptor(record, key);
439
+ if (key === "__proto__" || !descriptor || !("value" in descriptor)) {
440
+ byteLength = Number.POSITIVE_INFINITY;
441
+ return;
442
+ }
443
+ byteLength += 8 + key.length * 2;
444
+ visit(descriptor.value);
445
+ }
446
+ }
447
+ };
448
+ visit(value);
449
+ return byteLength;
450
+ }
451
+ function isPlainPayload(value, geometries, seen) {
452
+ if (typeof value === "function" || typeof value === "symbol") return false;
453
+ if (value === null || typeof value !== "object") return true;
454
+ if (seen.has(value)) return true;
455
+ seen.add(value);
456
+ if (typeof SharedArrayBuffer !== "undefined" && value instanceof SharedArrayBuffer) return !isResizableBuffer(value);
457
+ if (value instanceof ArrayBuffer) return !isResizableBuffer(value);
458
+ if (ArrayBuffer.isView(value)) {
459
+ if (!isStandardView(value)) return false;
460
+ const buffer = value.buffer;
461
+ if (typeof SharedArrayBuffer !== "undefined" && buffer instanceof SharedArrayBuffer) {
462
+ if (isResizableBuffer(buffer)) return false;
463
+ geometries.set(value, viewGeometry(value));
464
+ return true;
465
+ }
466
+ if (!(buffer instanceof ArrayBuffer) || isResizableBuffer(buffer)) return false;
467
+ geometries.set(value, viewGeometry(value));
468
+ return true;
469
+ }
470
+ if (value instanceof Map || value instanceof Set || value instanceof Date || value instanceof RegExp) return false;
471
+ if (Array.isArray(value)) {
472
+ if (hasExtraKeys(value) || hasEnumerableSymbols(value)) return false;
473
+ for (const child of value) if (!isPlainPayload(child, geometries, seen)) return false;
474
+ return true;
475
+ }
476
+ const proto = Object.getPrototypeOf(value);
477
+ if (proto !== Object.prototype && proto !== null) return false;
478
+ if (hasEnumerableSymbols(value)) return false;
479
+ const record = value;
480
+ for (const key in record) if (Object.prototype.hasOwnProperty.call(record, key) && !isPlainPayload(record[key], geometries, seen)) return false;
481
+ return true;
482
+ }
483
+ function hasExtraKeys(value) {
484
+ const record = value;
485
+ for (const key in record) {
486
+ if (!Object.prototype.hasOwnProperty.call(value, key)) continue;
487
+ const index = Number(key);
488
+ if (!Number.isInteger(index) || index < 0 || index >= value.length || String(index) !== key) return true;
489
+ }
490
+ return false;
491
+ }
492
+ function hasEnumerableSymbols(value) {
493
+ for (const symbol of Object.getOwnPropertySymbols(value)) if (Object.prototype.propertyIsEnumerable.call(value, symbol)) return true;
494
+ return false;
495
+ }
496
+ function isStandardView(view) {
497
+ return STANDARD_VIEW_CONSTRUCTORS.has(view.constructor);
498
+ }
499
+ function viewGeometry(view) {
500
+ if (view instanceof DataView) return {
501
+ offset: view.byteOffset,
502
+ length: view.byteLength,
503
+ dataView: true
504
+ };
505
+ return {
506
+ offset: view.byteOffset,
507
+ length: view.length,
508
+ dataView: false
509
+ };
510
+ }
511
+ function retargetView(view, buffer, geometry) {
512
+ if (geometry.dataView) return new DataView(buffer, geometry.offset, geometry.length);
513
+ const Typed = view.constructor;
514
+ return new Typed(buffer, geometry.offset, geometry.length);
515
+ }
516
+ function moveArrayBuffer(buffer) {
517
+ const movable = buffer;
518
+ if (typeof movable.transfer === "function") return movable.transfer();
519
+ return structuredClone(buffer, { transfer: [buffer] });
520
+ }
521
+ function isResizableBuffer(buffer) {
522
+ return buffer.resizable === true;
523
+ }
319
524
  //#endregion
320
525
  //#region src/snappy.ts
321
526
  var BLOCK_SIZE = 65536;
@@ -543,6 +748,25 @@ function writeEnvelope(out, offset, magic, kindTag, rawLength, payload) {
543
748
  view.setUint32(10, rawLength, true);
544
749
  view.setUint32(14, checksum, true);
545
750
  }
751
+ function writeEnvelopeHeader(out, magic, kindTag, rawLength, payload) {
752
+ const checksum = crc32(payload);
753
+ out.set(magic);
754
+ out[8] = ENVELOPE_VERSION;
755
+ out[9] = kindTag;
756
+ const view = new DataView(out.buffer, out.byteOffset, ENVELOPE_HEADER_SIZE);
757
+ view.setUint32(10, rawLength, true);
758
+ view.setUint32(14, checksum, true);
759
+ }
760
+ function envelopeFromChunks(magic, kindTag, rawLength, chunks, totalBytes) {
761
+ const out = new Uint8Array(ENVELOPE_HEADER_SIZE + totalBytes);
762
+ let offset = ENVELOPE_HEADER_SIZE;
763
+ for (const chunk of chunks) {
764
+ out.set(chunk, offset);
765
+ offset += chunk.byteLength;
766
+ }
767
+ writeEnvelopeHeader(out, magic, kindTag, rawLength, out.subarray(ENVELOPE_HEADER_SIZE));
768
+ return out;
769
+ }
546
770
  function compressionIsProfitable(rawLength, compressedLength) {
547
771
  return (compressedLength + ENVELOPE_HEADER_SIZE) * 100 <= rawLength * 90;
548
772
  }
@@ -555,10 +779,6 @@ function snappyPreflightIsProfitable(value) {
555
779
  }
556
780
  return compressionIsProfitable(STORE_VALUE_COMPRESSION_PREFLIGHT_WINDOW_BYTES * 3, compressedLength);
557
781
  }
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
782
  function validateStoreValueLength(value) {
563
783
  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
784
  }
@@ -623,19 +843,41 @@ function readEnvelopeHeader(magic, bytes, strict = true) {
623
843
  payloadChecksum: view.getUint32(14, true)
624
844
  };
625
845
  }
626
- async function compressBytes(data, kind) {
846
+ async function collectCompressed(input, kind, stopAtBytes) {
627
847
  ensureCompressionRuntime();
628
848
  let stream;
629
849
  try {
630
- stream = byteStream(data).pipeThrough(new CompressionStream(kind));
850
+ stream = byteStream(input).pipeThrough(new CompressionStream(kind));
631
851
  } catch {
632
852
  throw compressionRuntimeUnavailable();
633
853
  }
854
+ const reader = stream.getReader();
855
+ const chunks = [];
856
+ let totalBytes = 0;
857
+ let stopped = false;
634
858
  try {
635
- return new Uint8Array(await new Response(stream).arrayBuffer());
859
+ for (let next = await reader.read(); !next.done; next = await reader.read()) {
860
+ const part = next.value;
861
+ totalBytes += part.byteLength;
862
+ if (stopAtBytes !== void 0 && totalBytes >= stopAtBytes) {
863
+ stopped = true;
864
+ await reader.cancel("compressed output is not smaller than the raw payload");
865
+ break;
866
+ }
867
+ chunks.push(part);
868
+ }
636
869
  } catch (error) {
637
- throw namedError("InternalError", `failed to compress bytes with ${kind}: ${String(error)}`);
870
+ if (!stopped) throw namedError("InternalError", `failed to compress bytes with ${kind}: ${String(error)}`);
871
+ } finally {
872
+ try {
873
+ reader.releaseLock();
874
+ } catch {}
638
875
  }
876
+ if (stopped || stopAtBytes !== void 0 && totalBytes >= stopAtBytes) return null;
877
+ return {
878
+ chunks,
879
+ totalBytes
880
+ };
639
881
  }
640
882
  async function decompressBytes(data, kind, label, maxOutputBytes) {
641
883
  if (kind === "snappy") return decodeSnappy(data, maxOutputBytes ?? MAX_DECOMPRESSED_STORE_VALUE_BYTES, maxOutputBytes);
@@ -689,6 +931,7 @@ function compressionFromStoreFlags(flags) {
689
931
  }
690
932
  }
691
933
  async function encodeStoreValueRecord(value, compression) {
934
+ validateStoreValueLength(value);
692
935
  if (compression === false) return value;
693
936
  if (compression === "snappy") {
694
937
  const record = prepareFastStoreValueRecord(value, compression);
@@ -696,10 +939,16 @@ async function encodeStoreValueRecord(value, compression) {
696
939
  writePreparedStoreValueRecord(record, output, 0);
697
940
  return output;
698
941
  }
699
- validateStoreValueLength(value);
700
942
  if (value.byteLength < 1024) return buildEnvelope(STORE_RECORD_MAGIC, COMPRESSION_TAG_NONE, value.byteLength, value);
701
- const input = copyBytes(value);
702
- return buildStoreRecord(input, await compressBytes(input, compression), compression);
943
+ const stored = new Uint8Array(ENVELOPE_HEADER_SIZE + value.byteLength);
944
+ const payload = stored.subarray(ENVELOPE_HEADER_SIZE);
945
+ payload.set(value);
946
+ const compressed = await collectCompressed(payload, compression, payload.byteLength);
947
+ if (compressed === null || !compressionIsProfitable(payload.byteLength, compressed.totalBytes)) {
948
+ writeEnvelopeHeader(stored, STORE_RECORD_MAGIC, COMPRESSION_TAG_NONE, payload.byteLength, payload);
949
+ return stored;
950
+ }
951
+ return envelopeFromChunks(STORE_RECORD_MAGIC, compressionTag(compression), payload.byteLength, compressed.chunks, compressed.totalBytes);
703
952
  }
704
953
  async function decodeStoreValueRecord(value, options) {
705
954
  const header = readEnvelopeHeader(STORE_RECORD_MAGIC, value, options.strict);
@@ -735,9 +984,11 @@ async function decodeStoreValueRecord(value, options) {
735
984
  async function wrapSnapshotWithCompression(snapshot, compression) {
736
985
  if (compression === false) return snapshot;
737
986
  if (!["gzip", "deflate"].includes(compression)) throw namedError("InternalError", `unsupported snapshot compression kind: ${String(compression)}`);
987
+ if (snapshot.byteLength > MAX_DECOMPRESSED_SNAPSHOT_BYTES) throw namedError("SerializationError", `snapshot has ${snapshot.byteLength} bytes, exceeding the ${MAX_DECOMPRESSED_SNAPSHOT_BYTES} byte limit`);
738
988
  const input = copyBytes(snapshot);
739
- const compressed = await compressBytes(input, compression);
740
- return buildEnvelope(SNAPSHOT_EXPORT_MAGIC, compressionTag(compression), input.byteLength, compressed);
989
+ const compressed = await collectCompressed(input, compression);
990
+ if (compressed === null) throw namedError("InternalError", `failed to compress bytes with ${compression}`);
991
+ return envelopeFromChunks(SNAPSHOT_EXPORT_MAGIC, compressionTag(compression), input.byteLength, compressed.chunks, compressed.totalBytes);
741
992
  }
742
993
  async function unwrapSnapshotCompression(snapshot) {
743
994
  const header = readEnvelopeHeader(SNAPSHOT_EXPORT_MAGIC, snapshot);
@@ -759,7 +1010,9 @@ function toWasmU64(value) {
759
1010
  return BigInt(value);
760
1011
  }
761
1012
  function fromWasmU64(value) {
762
- return typeof value === "bigint" ? Number(value) : value;
1013
+ const number = typeof value === "bigint" ? Number(value) : value;
1014
+ if (!Number.isSafeInteger(number) || number < 0) throw remoteError("SerializationError", "WASM integer exceeds the JavaScript safe integer range");
1015
+ return number;
763
1016
  }
764
1017
  function fromOptionalWasmU64(value) {
765
1018
  return value === null || value === void 0 ? null : fromWasmU64(value);
@@ -771,6 +1024,7 @@ function toWasmPutOptions(options) {
771
1024
  };
772
1025
  }
773
1026
  function toWasmRange(range) {
1027
+ assertValidScanRange(range);
774
1028
  const normalized = {};
775
1029
  if (range.gt !== void 0) normalized.gt = range.gt;
776
1030
  if (range.gte !== void 0) normalized.gte = range.gte;
@@ -786,6 +1040,7 @@ var MAX_ENGINE_KEY_BYTES = 1024;
786
1040
  var MAX_PACKED_BYTES = 4294967295;
787
1041
  var INDEX_PUT_BATCH_ROWS = 64;
788
1042
  var INDEX_PUT_BATCH_BYTES = 4194304;
1043
+ var MAX_PLAIN_STORE_VALUE_BYTES = 8388608;
789
1044
  var INDEX_SCAN_PAGE_ROWS = 256;
790
1045
  var INDEX_SCAN_PAGE_BYTES = 8389662;
791
1046
  var INDEX_SCAN_RAW_CHUNK_ROWS = 512;
@@ -931,6 +1186,29 @@ function rangesEqual(left, right) {
931
1186
  }
932
1187
  return left.reverse === right.reverse && left.limit === right.limit;
933
1188
  }
1189
+ function sameBound(cached, next) {
1190
+ if (cached === void 0 || next === void 0) return cached === next;
1191
+ return bytesEqual(cached, next);
1192
+ }
1193
+ function copyBound(value) {
1194
+ return value === void 0 ? void 0 : value.slice();
1195
+ }
1196
+ function compoundEntryKeyLength(logicalKey, primaryKey) {
1197
+ return escapedPartLength(logicalKey) + escapedPartLength(primaryKey);
1198
+ }
1199
+ function escapedPartLength(part) {
1200
+ let length = 2;
1201
+ for (let index = 0; index < part.length; index += 1) length += part[index] === 0 ? 2 : 1;
1202
+ return length;
1203
+ }
1204
+ function limitPlainStoreValue(value) {
1205
+ if (value.byteLength > MAX_PLAIN_STORE_VALUE_BYTES) {
1206
+ const error = /* @__PURE__ */ new Error(`stored value has ${value.byteLength} bytes, exceeding the ${MAX_PLAIN_STORE_VALUE_BYTES} byte limit`);
1207
+ error.name = "ValueTooLargeError";
1208
+ throw error;
1209
+ }
1210
+ return value;
1211
+ }
934
1212
  function normalizeWasmBytes(value) {
935
1213
  if (value instanceof Uint8Array) return value.byteOffset === 0 && value.byteLength === value.buffer.byteLength ? value : value.slice();
936
1214
  return new Uint8Array(value);
@@ -1088,6 +1366,9 @@ var DbWorker = class {
1088
1366
  */
1089
1367
  txIndexSchemas = /* @__PURE__ */ new Map();
1090
1368
  indexesBySchema = /* @__PURE__ */ new WeakMap();
1369
+ /** Readonly scans reuse a resolved index; readwrite scans recheck so TTL staging still runs. */
1370
+ readonlyIndexCache = /* @__PURE__ */ new Map();
1371
+ indexBoundCache = null;
1091
1372
  txEnsuredRawStores = /* @__PURE__ */ new Map();
1092
1373
  txIndexSchemaChanged = /* @__PURE__ */ new Set();
1093
1374
  txStoreCompression = /* @__PURE__ */ new Map();
@@ -1326,9 +1607,14 @@ var DbWorker = class {
1326
1607
  return Promise.resolve(this.withEngine((engine) => engine.has(toWasmU64(txId), store, key)));
1327
1608
  }
1328
1609
  async put(txId, store, key, value, options = {}) {
1329
- const defs = this.indexesForStoreInTx(txId, store);
1610
+ await this.putEntry(txId, store, key, value, options);
1611
+ }
1612
+ async putEntry(txId, store, key, value, options, knownDefs, knownCompression) {
1613
+ const defs = knownDefs ?? this.indexesForStoreInTx(txId, store);
1614
+ const compression = knownCompression ?? this.storeCompressionForTx(txId, store);
1330
1615
  if (defs.length === 0) {
1331
- const storedValue = await this.encodeStoreValueForWrite(txId, store, value);
1616
+ const preparedValue = this.prepareStoredValue(store, value, compression);
1617
+ const storedValue = preparedValue instanceof Uint8Array ? preparedValue : await preparedValue;
1332
1618
  const baselineExists = this.withEngine((engine) => engine.put(toWasmU64(txId), store, key, storedValue, toWasmPutOptions(options)));
1333
1619
  this.recordPutChange(txId, store, key, baselineExists);
1334
1620
  return;
@@ -1348,7 +1634,7 @@ var DbWorker = class {
1348
1634
  this.recordPutChanges(txId, store, entries, partial);
1349
1635
  throw error;
1350
1636
  }
1351
- if (baselines.length !== entries.length) throw remoteError("InternalError", `put_many outcome count mismatch: ${baselines.length} != ${entries.length}`);
1637
+ if (baselines.length !== entries.length) throw outcomeCountMismatch("put_many", baselines.length, entries.length);
1352
1638
  this.recordPutChanges(txId, store, entries, baselines);
1353
1639
  return;
1354
1640
  }
@@ -1380,7 +1666,7 @@ var DbWorker = class {
1380
1666
  this.recordPutKeyChanges(txId, store, keys, partial);
1381
1667
  throw error;
1382
1668
  }
1383
- if (baselines.length !== keys.length) throw remoteError("InternalError", `put_many_packed outcome count mismatch: ${baselines.length} != ${keys.length}`);
1669
+ if (baselines.length !== keys.length) throw outcomeCountMismatch("put_many_packed", baselines.length, keys.length);
1384
1670
  this.recordPutKeyChanges(txId, store, keys, baselines);
1385
1671
  }
1386
1672
  async delete(txId, store, key) {
@@ -1410,7 +1696,7 @@ var DbWorker = class {
1410
1696
  for (let i = 0; i < partial.length && i < keys.length; i += 1) this.recordDeleteChange(txId, store, keys[i], partial[i]);
1411
1697
  throw error;
1412
1698
  }
1413
- if (deleted.length !== keys.length) throw remoteError("InternalError", `delete_many outcome count mismatch: ${deleted.length} != ${keys.length}`);
1699
+ if (deleted.length !== keys.length) throw outcomeCountMismatch("delete_many", deleted.length, keys.length);
1414
1700
  for (let i = 0; i < keys.length; i += 1) this.recordDeleteChange(txId, store, keys[i], deleted[i]);
1415
1701
  return;
1416
1702
  }
@@ -1432,7 +1718,7 @@ var DbWorker = class {
1432
1718
  for (let i = 0; i < partial.length && i < keys.length; i += 1) this.recordDeleteChange(txId, store, keys[i], partial[i]);
1433
1719
  throw error;
1434
1720
  }
1435
- if (deleted.length !== keys.length) throw remoteError("InternalError", `delete_many_packed outcome count mismatch: ${deleted.length} != ${keys.length}`);
1721
+ if (deleted.length !== keys.length) throw outcomeCountMismatch("delete_many_packed", deleted.length, keys.length);
1436
1722
  for (let i = 0; i < keys.length; i += 1) this.recordDeleteChange(txId, store, keys[i], deleted[i] === 1);
1437
1723
  }
1438
1724
  async applyBatch(txId, store, ops) {
@@ -1446,7 +1732,7 @@ var DbWorker = class {
1446
1732
  this.recordBatchOutcomes(txId, store, ops, partialBatchOutcomes(error));
1447
1733
  throw error;
1448
1734
  }
1449
- if (outcomes.length !== ops.length) throw remoteError("InternalError", `apply_batch outcome count mismatch: ${outcomes.length} != ${ops.length}`);
1735
+ if (outcomes.length !== ops.length) throw outcomeCountMismatch("apply_batch", outcomes.length, ops.length);
1450
1736
  this.recordBatchOutcomes(txId, store, ops, outcomes);
1451
1737
  return;
1452
1738
  }
@@ -1466,10 +1752,10 @@ var DbWorker = class {
1466
1752
  try {
1467
1753
  flags = this.withEngine((engine) => engine.apply_batch_packed(toWasmU64(txId), store, packedOps));
1468
1754
  } catch (error) {
1469
- this.recordBatchKeyOutcomes(txId, store, ops, partialBatchOutcomes(error));
1755
+ this.recordBatchOutcomes(txId, store, ops, partialBatchOutcomes(error));
1470
1756
  throw error;
1471
1757
  }
1472
- if (flags.length !== ops.length) throw remoteError("InternalError", `apply_batch_packed outcome count mismatch: ${flags.length} != ${ops.length}`);
1758
+ if (flags.length !== ops.length) throw outcomeCountMismatch("apply_batch_packed", flags.length, ops.length);
1473
1759
  for (let i = 0; i < ops.length; i += 1) {
1474
1760
  const op = ops[i];
1475
1761
  if (op.kind === "put") this.recordPutChange(txId, store, op.key, flags[i] === 1);
@@ -1520,7 +1806,7 @@ var DbWorker = class {
1520
1806
  if (rows !== null) {
1521
1807
  bytes = 4;
1522
1808
  for (const row of rows) {
1523
- row.value = await this.decodeStoreValue(cursor.txId, cursor.store, row.value);
1809
+ row.value = await decodeStoreValueRecord(row.value, { strict: true });
1524
1810
  bytes += 8 + row.key.byteLength + row.value.byteLength;
1525
1811
  if (bytes > request.maxBytes) throw remoteError("ValueTooLargeError", "decoded scan page exceeds maxBytes");
1526
1812
  }
@@ -1558,12 +1844,15 @@ var DbWorker = class {
1558
1844
  return page.rows.length === 0 ? null : page.rows[0].value;
1559
1845
  }
1560
1846
  async scanByIndex(txId, store, indexName, range = {}) {
1847
+ assertValidScanRange(range);
1561
1848
  const def = this.resolveIndexDefinition(txId, store, indexName);
1562
1849
  const rows = [];
1563
1850
  let remaining = range.limit ?? Number.POSITIVE_INFINITY;
1851
+ if (remaining <= 0) return rows;
1852
+ const physical = this.physicalIndexRange(txId, def, range);
1564
1853
  let cursor = null;
1565
1854
  while (remaining > 0) {
1566
- const page = await this.loadVisibleIndexPage(txId, def, range, cursor, Math.min(remaining, INDEX_SCAN_PAGE_ROWS));
1855
+ const page = await this.loadVisibleIndexPage(txId, def, range, cursor, Math.min(remaining, INDEX_SCAN_PAGE_ROWS), INDEX_SCAN_PAGE_BYTES, physical);
1567
1856
  rows.push(...page.rows);
1568
1857
  remaining -= page.rows.length;
1569
1858
  if (page.cursor === null) break;
@@ -1575,7 +1864,7 @@ var DbWorker = class {
1575
1864
  if (!Number.isSafeInteger(limit) || limit <= 0) throw remoteError("InvalidRangeError", "index scan page limit must be a positive integer");
1576
1865
  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");
1577
1866
  const def = this.resolveIndexDefinition(txId, store, indexName);
1578
- return this.loadVisibleIndexPage(txId, def, range, cursor, limit, maxBytes);
1867
+ return this.loadVisibleIndexPage(txId, def, range, cursor, limit, maxBytes, this.physicalIndexRange(txId, def, range));
1579
1868
  }
1580
1869
  getIndexes(txId) {
1581
1870
  return Promise.resolve(toPublicIndexDefinitions(txId === void 0 ? this.loadCommittedIndexSchema() : this.indexSchemaForTx(txId)));
@@ -1605,7 +1894,7 @@ var DbWorker = class {
1605
1894
  let rows;
1606
1895
  if (sourceRows !== null) {
1607
1896
  const visible = this.withEngine((engine) => engine.has_many(toWasmU64(txId), def.store, sourceRows.map((row) => row.key)));
1608
- if (visible.length !== sourceRows.length) throw remoteError("InternalError", `has_many outcome count mismatch: ${visible.length} != ${sourceRows.length}`);
1897
+ if (visible.length !== sourceRows.length) throw outcomeCountMismatch("has_many", visible.length, sourceRows.length);
1609
1898
  rows = sourceRows.filter((_row, index) => visible[index]);
1610
1899
  } else {
1611
1900
  rows = await this.readStoreScan(txId, def.store, EMPTY_RANGE);
@@ -1697,12 +1986,9 @@ var DbWorker = class {
1697
1986
  return await wrapSnapshotWithCompression(wrapSnapshotWithIndexDefinitions(normalizeWasmBytes(this.withEngine((engine) => engine.export_snapshot())), defs), options.compression ?? false);
1698
1987
  }
1699
1988
  async importSnapshot(data) {
1700
- this.rollbackAllTransactions();
1989
+ this.ensureNotInMaintenance();
1701
1990
  const parsed = unwrapSnapshotWithIndexDefinitions(await unwrapSnapshotCompression(data));
1702
- this.withEngine((engine) => engine.import_snapshot(parsed.snapshot));
1703
- this.committedIndexes = [];
1704
- this.committedStoreCompression = null;
1705
- await this.applyCommittedIndexSchema(parsed.indexes);
1991
+ await this.runGenerationOperation("import", parsed);
1706
1992
  }
1707
1993
  async reset() {
1708
1994
  const dbName = this.dbName;
@@ -1730,16 +2016,16 @@ var DbWorker = class {
1730
2016
  }
1731
2017
  }
1732
2018
  async compact() {
1733
- return this.runCompactionOperation("compact");
2019
+ return this.runGenerationOperation("compact");
1734
2020
  }
1735
2021
  async rebuild() {
1736
- return this.runCompactionOperation("rebuild");
2022
+ return this.runGenerationOperation("rebuild");
1737
2023
  }
1738
2024
  stats() {
1739
2025
  const stats = this.withEngine((engine) => engine.stats());
1740
2026
  return Promise.resolve({
1741
2027
  ...stats,
1742
- db_id: fromWasmU64(stats.db_id),
2028
+ db_id: typeof stats.db_id === "bigint" ? Number(stats.db_id) : stats.db_id,
1743
2029
  catalog_root_page_id: fromWasmU64(stats.catalog_root_page_id),
1744
2030
  next_page_id: fromWasmU64(stats.next_page_id),
1745
2031
  last_committed_txid: fromWasmU64(stats.last_committed_txid),
@@ -1800,7 +2086,7 @@ var DbWorker = class {
1800
2086
  });
1801
2087
  try {
1802
2088
  await this.reconcileIndexes(txId, toPublicIndexDefinitions(normalized));
1803
- this.withEngine((engine) => engine.commit_tx(toWasmU64(txId)), { allowDuringMaintenance: true });
2089
+ fromWasmU64(this.withEngine((engine) => engine.commit_tx(toWasmU64(txId)), { allowDuringMaintenance: true }));
1804
2090
  this.committedIndexes = cloneNormalizedIndexDefinitions(normalized);
1805
2091
  } catch (error) {
1806
2092
  try {
@@ -1816,18 +2102,19 @@ var DbWorker = class {
1816
2102
  * A failed swap may have reached storage; read the control file before cleanup.
1817
2103
  * After publication, abandon the old engine to avoid a checkpoint into inactive files.
1818
2104
  */
1819
- async runCompactionOperation(operation) {
2105
+ async runGenerationOperation(operation, imported) {
1820
2106
  const dbName = this.requireDbName();
1821
2107
  if (this.maintenanceOperation) throw remoteError("DatabaseBusyError", `database ${dbName} is busy: ${this.maintenanceOperation} in progress`);
1822
2108
  this.maintenanceOperation = operation;
1823
2109
  const start = performance.now();
1824
- const wasm = await this.loadWasm();
2110
+ let wasm = null;
1825
2111
  let rebuiltEngine = null;
1826
2112
  let generationName = null;
1827
2113
  let published = false;
1828
2114
  try {
2115
+ wasm = await this.loadWasm();
1829
2116
  const sizeBefore = await wasm.dbDirectorySize(dbName);
1830
- const defs = cloneNormalizedIndexDefinitions(this.loadCommittedIndexSchema());
2117
+ const defs = cloneNormalizedIndexDefinitions(imported?.indexes ?? this.loadCommittedIndexSchema());
1831
2118
  this.rollbackAllTransactions({ allowDuringMaintenance: true });
1832
2119
  const expectedGeneration = await wasm.readActiveGeneration(dbName);
1833
2120
  generationName = (await wasm.prepareRebuildTarget(dbName)).generationName;
@@ -1837,8 +2124,8 @@ var DbWorker = class {
1837
2124
  cache_pages: this.currentCachePages()
1838
2125
  });
1839
2126
  const target = rebuiltEngine;
1840
- 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 });
1841
- if (operation === "rebuild" && defs.length > 0) {
2127
+ 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 });
2128
+ if ((operation === "rebuild" || imported) && defs.length > 0) {
1842
2129
  const previousEngine = this.engine;
1843
2130
  const previousMaintenance = this.maintenanceOperation;
1844
2131
  this.engine = target;
@@ -1871,7 +2158,7 @@ var DbWorker = class {
1871
2158
  try {
1872
2159
  await wasm.cleanupInactiveEntries(dbName);
1873
2160
  } catch {}
1874
- const sizeAfter = await wasm.dbDirectorySize(dbName);
2161
+ const sizeAfter = imported ? sizeBefore : await wasm.dbDirectorySize(dbName);
1875
2162
  return {
1876
2163
  sizeBefore,
1877
2164
  sizeAfter,
@@ -1885,7 +2172,7 @@ var DbWorker = class {
1885
2172
  try {
1886
2173
  rebuiltEngine?.abandon();
1887
2174
  } catch {}
1888
- if (generationName !== null) {
2175
+ if (generationName !== null && wasm !== null) {
1889
2176
  const active = await readActiveGenerationOrNull(wasm, dbName);
1890
2177
  if (active !== void 0 && active !== generationName) try {
1891
2178
  await wasm.cleanupInactiveEntries(dbName);
@@ -1927,9 +2214,11 @@ var DbWorker = class {
1927
2214
  this.txChanges.delete(txId);
1928
2215
  this.txModes.delete(txId);
1929
2216
  this.txIndexSchemas.delete(txId);
2217
+ this.readonlyIndexCache.delete(txId);
1930
2218
  this.txEnsuredRawStores.delete(txId);
1931
2219
  this.txIndexSchemaChanged.delete(txId);
1932
2220
  this.txStoreCompression.delete(txId);
2221
+ if (this.indexBoundCache?.txId === txId) this.indexBoundCache = null;
1933
2222
  }
1934
2223
  getOrCreateTrackedTxn(txId) {
1935
2224
  return getOrInsert(this.txChanges, txId, () => /* @__PURE__ */ new Map());
@@ -1995,14 +2284,6 @@ var DbWorker = class {
1995
2284
  else if (op.kind === "delete" && outcome.kind === "delete") this.recordDeleteChange(txId, store, op.key, outcome.deleted);
1996
2285
  }
1997
2286
  }
1998
- recordBatchKeyOutcomes(txId, store, ops, outcomes) {
1999
- for (let i = 0; i < outcomes.length && i < ops.length; i += 1) {
2000
- const op = ops[i];
2001
- const outcome = outcomes[i];
2002
- if (op.kind === "put" && outcome.kind === "put") this.recordPutChange(txId, store, op.key, outcome.baselineExists);
2003
- else if (op.kind === "delete" && outcome.kind === "delete") this.recordDeleteChange(txId, store, op.key, outcome.deleted);
2004
- }
2005
- }
2006
2287
  finalizeTrackedChanges(tracked) {
2007
2288
  const stores = [];
2008
2289
  for (const [store, trackedStore] of tracked) {
@@ -2225,22 +2506,20 @@ var DbWorker = class {
2225
2506
  } : op);
2226
2507
  return storedOps;
2227
2508
  }
2228
- async encodeStoreValueForWrite(txId, store, value) {
2229
- if (isInternalStoreName(store)) return value;
2230
- return encodeStoreValueRecord(value, this.storeCompressionForTx(txId, store));
2231
- }
2232
- async decodeStoreValue(txId, store, value) {
2509
+ prepareStoredValue(store, value, compression) {
2233
2510
  if (isInternalStoreName(store)) return value;
2234
- if (this.storeCompressionForTx(txId, store) === false) return value;
2235
- return decodeStoreValueRecord(value, { strict: true });
2511
+ if (compression === false) return limitPlainStoreValue(value);
2512
+ return encodeStoreValueRecord(value, compression);
2236
2513
  }
2237
2514
  async decodeStoreValueForFeed(store, value) {
2238
2515
  if (isInternalStoreName(store)) return value;
2239
2516
  return decodeStoreValueRecord(value, { strict: (this.loadCommittedStoreCompression().get(store) ?? false) !== false });
2240
2517
  }
2241
- async readStoreValue(txId, store, key) {
2518
+ async readStoreValue(txId, store, key, knownCompression) {
2242
2519
  const value = this.readRawValue(txId, store, key);
2243
- return value === null ? null : this.decodeStoreValue(txId, store, value);
2520
+ if (value === null || isInternalStoreName(store)) return value;
2521
+ if ((knownCompression ?? this.storeCompressionForTx(txId, store)) === false) return value;
2522
+ return await decodeStoreValueRecord(value, { strict: true });
2244
2523
  }
2245
2524
  async readStoreScan(txId, store, range) {
2246
2525
  const rows = this.readRawScan(txId, store, range);
@@ -2250,7 +2529,7 @@ var DbWorker = class {
2250
2529
  for (let index = 0; index < rows.length; index += 1) rows[index].value = values[index];
2251
2530
  return rows;
2252
2531
  }
2253
- async decodeStoreValues(txId, store, values) {
2532
+ async decodeStoreValues(_txId, _store, values) {
2254
2533
  const state = {
2255
2534
  next: 0,
2256
2535
  failed: false,
@@ -2262,7 +2541,7 @@ var DbWorker = class {
2262
2541
  const value = values[index];
2263
2542
  if (value === null) continue;
2264
2543
  try {
2265
- values[index] = await this.decodeStoreValue(txId, store, value);
2544
+ values[index] = await decodeStoreValueRecord(value, { strict: true });
2266
2545
  } catch (error) {
2267
2546
  if (!state.failed) {
2268
2547
  state.failed = true;
@@ -2290,11 +2569,42 @@ var DbWorker = class {
2290
2569
  return byStore.get(store) ?? [];
2291
2570
  }
2292
2571
  resolveIndexDefinition(txId, store, indexName) {
2572
+ if (this.txModes.get(txId) === "readonly") {
2573
+ const byStore = this.readonlyIndexCache.get(txId)?.get(store);
2574
+ if (byStore) {
2575
+ const cached = byStore.get(indexName);
2576
+ if (cached) return cached;
2577
+ const def = findIndexDefinition(this.indexesForStoreInTx(txId, store), store, indexName);
2578
+ if (!def) throw remoteError("IndexNotFoundError", `index not found: ${store}.${indexName}`);
2579
+ byStore.set(indexName, def);
2580
+ return def;
2581
+ }
2582
+ }
2293
2583
  if (!this.rawStoreExistsInTx(txId, store)) throw remoteError("StoreNotFoundError", `store not found: ${store}`);
2294
2584
  const def = findIndexDefinition(this.indexesForStoreInTx(txId, store), store, indexName);
2295
2585
  if (!def) throw remoteError("IndexNotFoundError", `index not found: ${store}.${indexName}`);
2586
+ if (this.txModes.get(txId) === "readonly") getOrInsert(getOrInsert(this.readonlyIndexCache, txId, () => /* @__PURE__ */ new Map()), store, () => /* @__PURE__ */ new Map()).set(indexName, def);
2296
2587
  return def;
2297
2588
  }
2589
+ physicalIndexRange(txId, def, range) {
2590
+ assertValidScanRange(range);
2591
+ const reverse = range.reverse === true;
2592
+ const cached = this.indexBoundCache;
2593
+ if (cached !== null && cached.txId === txId && cached.def === def && cached.reverse === reverse && sameBound(cached.gt, range.gt) && sameBound(cached.gte, range.gte) && sameBound(cached.lt, range.lt) && sameBound(cached.lte, range.lte)) return cached.physical;
2594
+ const { limit: _ignored, ...withoutLimit } = range;
2595
+ const physical = indexRangeToPhysicalRange(withoutLimit);
2596
+ this.indexBoundCache = {
2597
+ txId,
2598
+ def,
2599
+ reverse,
2600
+ gt: copyBound(range.gt),
2601
+ gte: copyBound(range.gte),
2602
+ lt: copyBound(range.lt),
2603
+ lte: copyBound(range.lte),
2604
+ physical
2605
+ };
2606
+ return physical;
2607
+ }
2298
2608
  indexSchemaForTx(txId) {
2299
2609
  const override = this.txIndexSchemas.get(txId);
2300
2610
  if (override) return override;
@@ -2474,7 +2784,10 @@ var DbWorker = class {
2474
2784
  if (physicalKey.byteLength > MAX_ENGINE_KEY_BYTES) throw remoteError("KeyTooLargeError", `index key too large for ${def.store}.${def.name}`);
2475
2785
  return physicalKey;
2476
2786
  }
2477
- async assertUniqueIndexAvailability(txId, def, logicalKey, primaryKey) {
2787
+ assertIndexEntryKeySize(def, logicalKey, primaryKey) {
2788
+ if (compoundEntryKeyLength(logicalKey, primaryKey) > MAX_ENGINE_KEY_BYTES) throw remoteError("KeyTooLargeError", `index key too large for ${def.store}.${def.name}`);
2789
+ }
2790
+ async assertUniqueIndexAvailability(txId, def, logicalKey, primaryKey, knownCompression) {
2478
2791
  if (!def.unique) return;
2479
2792
  const physical = indexKeyExactRange(logicalKey);
2480
2793
  let cursor = null;
@@ -2499,7 +2812,7 @@ var DbWorker = class {
2499
2812
  continue;
2500
2813
  }
2501
2814
  if (state === "trusted") throw remoteError("UniqueIndexConstraintError", `unique index constraint violation on ${def.store}.${def.name}`);
2502
- const value = await this.readStoreValue(txId, def.store, decoded.primaryKey);
2815
+ const value = await this.readStoreValue(txId, def.store, decoded.primaryKey, knownCompression);
2503
2816
  if (value === null) {
2504
2817
  this.cleanupStaleIndexRow(txId, def, row.key);
2505
2818
  continue;
@@ -2520,10 +2833,11 @@ var DbWorker = class {
2520
2833
  * chunks prevent stale entries from forcing the whole range into memory.
2521
2834
  * The output limit counts only live rows.
2522
2835
  */
2523
- async loadVisibleIndexPage(txId, def, range, cursor, limit, maxBytes = INDEX_SCAN_PAGE_BYTES) {
2836
+ async loadVisibleIndexPage(txId, def, range, cursor, limit, maxBytes = INDEX_SCAN_PAGE_BYTES, physicalRange) {
2524
2837
  const { limit: _ignored, ...withoutLimit } = range;
2525
- const physical = indexRangeToPhysicalRange(withoutLimit);
2838
+ const physical = physicalRange ?? indexRangeToPhysicalRange(withoutLimit);
2526
2839
  const reverse = physical.reverse === true;
2840
+ const compression = this.storeCompressionForTx(txId, def.store);
2527
2841
  let resumeAfter = cursor;
2528
2842
  const visible = [];
2529
2843
  let outputBytes = 4;
@@ -2562,9 +2876,10 @@ var DbWorker = class {
2562
2876
  batchStart = index;
2563
2877
  batch = this.readIndexPrimaryValues(txId, def, rawRows, index, Math.min(rawRows.length, index + limit - visible.length), maxBytes - outputBytes);
2564
2878
  batchEnd = index + (batch?.entries.length ?? 1);
2879
+ if (batch && compression !== false) this.stabilizeWasmValues(batch.values);
2565
2880
  }
2566
2881
  const row = rawRows[index];
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);
2882
+ const resolved = batch ? await this.resolveVisibleIndexedValue(txId, def, row.key, batch.entries[index - batchStart], batch.values[index - batchStart], batch.trusted[index - batchStart], compression, true) : await this.visibleIndexedRow(txId, def, row.key, compression);
2568
2883
  if (batch !== null) batch.values[index - batchStart] = null;
2569
2884
  if (!resolved) {
2570
2885
  resumeAfter = row.key;
@@ -2584,7 +2899,7 @@ var DbWorker = class {
2584
2899
  resumeAfter = row.key;
2585
2900
  if (visible.length >= limit) return {
2586
2901
  rows: visible,
2587
- cursor: row.key.slice()
2902
+ cursor: row.key
2588
2903
  };
2589
2904
  }
2590
2905
  if (rawRows.length < chunkLimit) return {
@@ -2646,7 +2961,7 @@ var DbWorker = class {
2646
2961
  } catch {
2647
2962
  return null;
2648
2963
  }
2649
- if (loaded.length !== selectedIndices.length) throw remoteError("InternalError", `get_many outcome count mismatch: ${loaded.length} != ${selectedIndices.length}`);
2964
+ if (loaded.length !== selectedIndices.length) throw outcomeCountMismatch("get_many", loaded.length, selectedIndices.length);
2650
2965
  for (let index = 0; index < loaded.length; index += 1) {
2651
2966
  values[selectedIndices[index]] = loaded[index];
2652
2967
  loaded[index] = null;
@@ -2657,7 +2972,13 @@ var DbWorker = class {
2657
2972
  trusted
2658
2973
  };
2659
2974
  }
2660
- async resolveVisibleIndexedRow(txId, def, physicalKey) {
2975
+ stabilizeWasmValues(values) {
2976
+ for (let index = 0; index < values.length; index += 1) {
2977
+ const value = values[index];
2978
+ if (value !== null) values[index] = normalizeWasmBytes(value);
2979
+ }
2980
+ }
2981
+ visibleIndexedRow(txId, def, physicalKey, compression) {
2661
2982
  const decoded = decodeIndexEntryKey(physicalKey);
2662
2983
  const state = this.inspectIndexedValue(txId, def, physicalKey, decoded.primaryKey);
2663
2984
  if (state === "missing") {
@@ -2665,14 +2986,18 @@ var DbWorker = class {
2665
2986
  return null;
2666
2987
  }
2667
2988
  const value = this.readRawValue(txId, def.store, decoded.primaryKey);
2668
- return this.resolveVisibleIndexedValue(txId, def, physicalKey, decoded, value, state === "trusted");
2989
+ return this.resolveVisibleIndexedValue(txId, def, physicalKey, decoded, value, state === "trusted", compression, true);
2669
2990
  }
2670
- async resolveVisibleIndexedValue(txId, def, physicalKey, decoded, rawValue, trusted = false) {
2991
+ resolveVisibleIndexedValue(txId, def, physicalKey, decoded, rawValue, trusted = false, compression, stable) {
2671
2992
  if (rawValue === null) {
2672
2993
  this.cleanupStaleIndexRow(txId, def, physicalKey);
2673
2994
  return null;
2674
2995
  }
2675
- const value = await this.decodeStoreValue(txId, def.store, normalizeWasmBytes(rawValue));
2996
+ const bytes = stable ? rawValue : normalizeWasmBytes(rawValue);
2997
+ if (compression === false) return this.finishVisibleIndexedValue(txId, def, physicalKey, decoded, bytes, trusted);
2998
+ return decodeStoreValueRecord(bytes, { strict: true }).then((value) => this.finishVisibleIndexedValue(txId, def, physicalKey, decoded, value, trusted));
2999
+ }
3000
+ finishVisibleIndexedValue(txId, def, physicalKey, decoded, value, trusted) {
2676
3001
  if (!trusted) {
2677
3002
  const currentLogicalKey = extractLogicalIndexKey(def, value);
2678
3003
  if (currentLogicalKey === null || !bytesEqual(currentLogicalKey, decoded.logicalKey)) {
@@ -2726,6 +3051,8 @@ var DbWorker = class {
2726
3051
  this.txChanges.clear();
2727
3052
  this.txModes.clear();
2728
3053
  this.txIndexSchemas.clear();
3054
+ this.readonlyIndexCache.clear();
3055
+ this.indexBoundCache = null;
2729
3056
  this.txEnsuredRawStores.clear();
2730
3057
  this.txIndexSchemaChanged.clear();
2731
3058
  this.txStoreCompression.clear();
@@ -2808,6 +3135,9 @@ function normalizeEngineHealth(health) {
2808
3135
  };
2809
3136
  return { state: health.state };
2810
3137
  }
3138
+ function outcomeCountMismatch(operation, actual, expected) {
3139
+ return remoteError("InternalError", `${operation} outcome count mismatch: ${actual} != ${expected}`);
3140
+ }
2811
3141
  function partialBooleanOutcomes(error) {
2812
3142
  if (!isRecord(error) || !Array.isArray(error.partial)) return [];
2813
3143
  return error.partial.filter((value) => typeof value === "boolean");