@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
package/dist/node.js CHANGED
@@ -1,12 +1,15 @@
1
- import { $ as StoreExistsError, A as prefixRange, B as DatabaseClosedError, C as compoundKey, Ct as isWorkerProtocolReadyEnvelope, D as indexKey, Dt as isWorkerProtocolResponseBatchMessage, E as encodeIndexScalar, F as utf8Encode, G as InvalidRangeError, H as InjectedFailureError, Ht as compareStringsByCodeUnit, I as ChangeFeedCompactedError, J as ReadonlyTransactionError, K as KeyTooLargeError, L as ConstraintError, M as splitCompoundKey, Mt as prepareWorkerCommandPayload, N as u64Key, O as jsonDecode, Ot as isWorkerProtocolResponseMessage, P as utf8Decode, Q as StorageError, R as CorruptionError, S as toPublicIndexDefinitions, T as encodeCompoundKeyParts, U as InternalError, Ut as isRecord$1, V as IndexNotFoundError, Vt as workerProtocolError, W as InvalidOpenOptionsError, X as ReservedStoreNameError, Y as RecoveryRequiredError, Z as SerializationError, _ as indexRangeToPhysicalRange, a as createIndexKeyExtractor, at as UnsupportedPlatformError, b as normalizeIndexDefinitions, c as decodeIndexedDocument, ct as WriteTransactionAlreadyOpenError, d as encodeIndexMetadataValue, et as StoreNameTooLongError, f as extractLogicalIndexKey, ft as WORKER_PROTOCOL_REQUEST, g as indexKeyExactRange, gt as captureWorkerPayload, h as indexDefinitionIdentity, i as compareNormalizedIndexDefinitions, it as UniqueIndexConstraintError, j as prefixSuccessor, k as jsonEncode, l as encodeIndexEntryKey, lt as normalizeError, m as findIndexDefinition, n as assertPublicStoreName, nt as TransactionClosedError, o as decodeIndexEntryKey, ot as ValueTooLargeError, p as extractLogicalIndexKeyFromDocument, pt as WORKER_PROTOCOL_REQUEST_BATCH, q as MoyoDbError, r as cloneNormalizedIndexDefinitions, rt as TransactionConflictError, s as decodeIndexMetadataValue, st as VersionError, t as INDEX_METADATA_STORE, tt as StoreNotFoundError, u as encodeIndexMetadataKey, v as indexesForStore, vt as decodeWorkerResponsePayload, w as compoundKeyRange, wt as isWorkerProtocolReadyMessage, x as serializeNormalizedIndexDefinitions, y as isInternalStoreName, yt as deserializeWorkerError, z as DatabaseBusyError } from "./chunks/indexing-BzmFhWXs.js";
1
+ import { $ as StoreExistsError, A as prefixRange, B as DatabaseClosedError, C as compoundKey, Ct as isWorkerProtocolReadyEnvelope, D as indexKey, Dt as isWorkerProtocolResponseBatchMessage, E as encodeIndexScalar, F as utf8Encode, G as InvalidRangeError, Gt as isRecord$1, H as InjectedFailureError, Ht as workerProtocolError, I as ChangeFeedCompactedError, J as ReadonlyTransactionError, K as KeyTooLargeError, L as ConstraintError, M as splitCompoundKey, N as u64Key, Nt as prepareWorkerCommandPayload, O as jsonDecode, Ot as isWorkerProtocolResponseMessage, P as utf8Decode, Q as StorageError, R as CorruptionError, S as toPublicIndexDefinitions, T as encodeCompoundKeyParts, U as InternalError, Ut as clampTimerDelayMs, V as IndexNotFoundError, W as InvalidOpenOptionsError, Wt as compareStringsByCodeUnit, X as ReservedStoreNameError, Y as RecoveryRequiredError, Z as SerializationError, _ as indexRangeToPhysicalRange, a as createIndexKeyExtractor, at as UnsupportedPlatformError, b as normalizeIndexDefinitions, c as decodeIndexedDocument, ct as WriteTransactionAlreadyOpenError, d as encodeIndexMetadataValue, et as StoreNameTooLongError, f as extractLogicalIndexKey, ft as WORKER_PROTOCOL_REQUEST, g as indexKeyExactRange, gt as captureWorkerPayload, h as indexDefinitionIdentity, i as compareNormalizedIndexDefinitions, it as UniqueIndexConstraintError, j as prefixSuccessor, k as jsonEncode, l as encodeIndexEntryKey, lt as normalizeError, m as findIndexDefinition, n as assertPublicStoreName, nt as TransactionClosedError, o as decodeIndexEntryKey, ot as ValueTooLargeError, p as extractLogicalIndexKeyFromDocument, pt as WORKER_PROTOCOL_REQUEST_BATCH, q as MoyoDbError, r as cloneNormalizedIndexDefinitions, rt as TransactionConflictError, s as decodeIndexMetadataValue, st as VersionError, t as INDEX_METADATA_STORE, tt as StoreNotFoundError, u as encodeIndexMetadataKey, v as indexesForStore, vt as decodeWorkerResponsePayload, w as compoundKeyRange, wt as isWorkerProtocolReadyMessage, x as serializeNormalizedIndexDefinitions, y as isInternalStoreName, yt as deserializeWorkerError, z as DatabaseBusyError } from "./chunks/indexing-CKtbpaZn.js";
2
2
  import { createHash, randomUUID } from "node:crypto";
3
- import { mkdir, realpath } from "node:fs/promises";
3
+ import { realpath } from "node:fs/promises";
4
4
  import { dirname, join, resolve } from "node:path";
5
5
  import { Worker as Worker$1 } from "node:worker_threads";
6
6
  import fs from "node:fs";
7
7
  //#region src/worker-client.ts
8
8
  function clearPendingTimeout(pending) {
9
- if (pending.timeout) clearTimeout(pending.timeout);
9
+ if (pending.timeout) {
10
+ clearTimeout(pending.timeout);
11
+ pending.timeout = null;
12
+ }
10
13
  }
11
14
  var WorkerProtocolClient = class {
12
15
  worker;
@@ -26,16 +29,19 @@ var WorkerProtocolClient = class {
26
29
  flushScheduled = false;
27
30
  awaitingReady = 0;
28
31
  readyTimeout = null;
32
+ forwardScanPackets;
29
33
  constructor(worker, options = {}) {
30
34
  this.worker = worker;
31
- this.requestTimeoutMs = options.requestTimeoutMs ?? 0;
35
+ this.forwardScanPackets = options.forwardScanPackets === true;
36
+ this.requestTimeoutMs = clampTimerDelayMs(options.requestTimeoutMs ?? 0);
32
37
  this.ready = new Promise((resolve, reject) => {
33
38
  this.readyResolve = resolve;
34
39
  this.readyReject = reject;
35
40
  });
36
- if ((options.readyTimeoutMs ?? 0) > 0) this.readyTimeout = setTimeout(() => {
41
+ const readyTimeoutMs = clampTimerDelayMs(options.readyTimeoutMs ?? 0);
42
+ if (readyTimeoutMs > 0) this.readyTimeout = setTimeout(() => {
37
43
  this.fail(workerProtocolError("WorkerReadyTimeoutError", "worker did not become ready"));
38
- }, options.readyTimeoutMs);
44
+ }, readyTimeoutMs);
39
45
  this.ready.catch(() => void 0);
40
46
  this.worker.addEventListener("message", this.handleMessage);
41
47
  this.worker.addEventListener("messageerror", this.handleMessageError);
@@ -160,6 +166,12 @@ var WorkerProtocolClient = class {
160
166
  range
161
167
  ]);
162
168
  }
169
+ scanPage(request) {
170
+ return this.request("scanPage", [request]);
171
+ }
172
+ closeCursor(cursorId) {
173
+ return this.request("closeCursor", [cursorId]);
174
+ }
163
175
  getByIndex(txId, store, indexName, key) {
164
176
  return this.request("getByIndex", [
165
177
  txId,
@@ -176,14 +188,15 @@ var WorkerProtocolClient = class {
176
188
  range
177
189
  ]);
178
190
  }
179
- scanByIndexPage(txId, store, indexName, range, cursor, limit) {
191
+ scanByIndexPage(txId, store, indexName, range, cursor, limit, maxBytes) {
180
192
  return this.request("scanByIndexPage", [
181
193
  txId,
182
194
  store,
183
195
  indexName,
184
196
  range,
185
197
  cursor,
186
- limit
198
+ limit,
199
+ maxBytes
187
200
  ]);
188
201
  }
189
202
  getIndexes(txId) {
@@ -256,7 +269,10 @@ var WorkerProtocolClient = class {
256
269
  };
257
270
  return await new Promise((resolve, reject) => {
258
271
  const timeout = this.requestTimeoutMs > 0 ? setTimeout(() => {
272
+ const current = this.pending.get(id);
273
+ if (!current || current.timeout === null) return;
259
274
  this.pending.delete(id);
275
+ current.timeout = null;
260
276
  reject(workerProtocolError("WorkerRequestTimeoutError", `worker command ${command} timed out after ${this.requestTimeoutMs}ms`));
261
277
  }, this.requestTimeoutMs) : null;
262
278
  this.pending.set(id, {
@@ -335,6 +351,10 @@ var WorkerProtocolClient = class {
335
351
  this.fail(workerProtocolError("WorkerProtocolError", "incompatible worker protocol ready message"));
336
352
  return;
337
353
  }
354
+ if (isRecord$1(data) && (data.type === "moyodb:worker-protocol:response" || data.type === "moyodb:worker-protocol:response-batch") && data.version !== 1) {
355
+ this.fail(workerProtocolError("WorkerProtocolError", "incompatible worker protocol response"));
356
+ return;
357
+ }
338
358
  if (isWorkerProtocolResponseBatchMessage(data)) {
339
359
  for (const response of data.responses) this.handleResponse(response);
340
360
  return;
@@ -355,7 +375,7 @@ var WorkerProtocolClient = class {
355
375
  this.pending.delete(data.id);
356
376
  clearPendingTimeout(pending);
357
377
  try {
358
- if (data.ok) pending.resolve(decodeWorkerResponsePayload(pending.command, data.result));
378
+ if (data.ok) pending.resolve(this.forwardScanPackets && (pending.command === "scanPage" || pending.command === "scan" || pending.command === "scanByIndex" || pending.command === "scanByIndexPage") ? data.result : decodeWorkerResponsePayload(pending.command, data.result));
359
379
  else pending.reject(deserializeWorkerError(data.error));
360
380
  } catch (error) {
361
381
  pending.reject(error instanceof Error && error.name === "WorkerProtocolError" ? error : workerProtocolError("WorkerProtocolError", "worker response could not be decoded"));
@@ -1042,7 +1062,7 @@ function bindSqlParameters(parsed, parameters = []) {
1042
1062
  return Array.from(parameters, (value, index) => {
1043
1063
  if (value === null || typeof value === "boolean") return value;
1044
1064
  if (typeof value === "number") {
1045
- if (!Number.isFinite(value) || Number.isInteger(value) && !Number.isSafeInteger(value)) throw new RangeError(`SQL parameter ${index + 1} must be finite; use bigint for integers outside the safe range`);
1065
+ if (!Number.isFinite(value) || Number.isInteger(value) && !Number.isSafeInteger(value) && fitsSigned64(value)) throw new RangeError(`SQL parameter ${index + 1} must be finite; use bigint for integers outside the safe range`);
1046
1066
  return value;
1047
1067
  }
1048
1068
  if (typeof value === "bigint") {
@@ -1508,6 +1528,11 @@ function tokenize(sql) {
1508
1528
  });
1509
1529
  return tokens;
1510
1530
  }
1531
+ function fitsSigned64(value) {
1532
+ if (!Number.isInteger(value)) return false;
1533
+ const integer = BigInt(value);
1534
+ return integer >= SQL_INTEGER_MIN && integer <= SQL_INTEGER_MAX;
1535
+ }
1511
1536
  function parseNumber(text, position) {
1512
1537
  if (/^[+-]?\d+$/u.test(text)) {
1513
1538
  const integer = BigInt(text);
@@ -1515,7 +1540,7 @@ function parseNumber(text, position) {
1515
1540
  return integer >= BigInt(Number.MIN_SAFE_INTEGER) && integer <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(integer) : integer;
1516
1541
  }
1517
1542
  const value = Number(text);
1518
- if (!Number.isFinite(value) || Number.isInteger(value) && !Number.isSafeInteger(value)) throw new SqlSyntaxError("REAL literal must be finite; use integer notation for large integers", position);
1543
+ if (!Number.isFinite(value) || Number.isInteger(value) && !Number.isSafeInteger(value) && fitsSigned64(value)) throw new SqlSyntaxError("REAL literal must be finite; use integer notation for large integers", position);
1519
1544
  const [mantissa, exponentText = "0"] = text.toLowerCase().split("e");
1520
1545
  const digits = mantissa.replace(/[+.-]/gu, "");
1521
1546
  const nonzero = /[1-9]/u.test(digits);
@@ -1678,9 +1703,10 @@ async function executeStatement(tx, statement, parameters, indexes, explain) {
1678
1703
  rowsAffected: 0,
1679
1704
  plan: plan.public
1680
1705
  };
1681
- const rows = (limit === 0 ? [] : await readRows(tx, schema, plan)).filter(({ row }) => !statement.where || evaluateWhere(row, statement.where, parameters) === true);
1706
+ const maxMatches = statement.kind === "select" && plan.public.sort === "none" && limit !== null ? Math.min(Number.MAX_SAFE_INTEGER, offset + limit) : void 0;
1707
+ const rows = limit === 0 ? [] : await readRows(tx, schema, plan, statement.where, parameters, maxMatches);
1682
1708
  if (statement.kind === "select") {
1683
- if (orderBy.length) rows.sort((left, right) => compareRows(left, right, orderBy));
1709
+ if (plan.public.sort === "memory") rows.sort((left, right) => compareRows(left, right, orderBy));
1684
1710
  return {
1685
1711
  rows: rows.slice(offset, limit === null ? void 0 : offset + limit).map(({ row }) => projectRow(row, statement.columns ?? schema.columns.map((column) => column.name))),
1686
1712
  rowsAffected: 0,
@@ -1816,7 +1842,10 @@ function normalizeValue(column, value) {
1816
1842
  }
1817
1843
  case "REAL":
1818
1844
  if (typeof value === "number" && Number.isFinite(value)) return Object.is(value, -0) ? 0 : value;
1819
- if (typeof value === "bigint" && value >= BigInt(Number.MIN_SAFE_INTEGER) && value <= BigInt(Number.MAX_SAFE_INTEGER)) return Number(value);
1845
+ if (typeof value === "bigint") {
1846
+ const asNumber = Number(value);
1847
+ if (Number.isFinite(asNumber) && BigInt(asNumber) === value) return asNumber;
1848
+ }
1820
1849
  break;
1821
1850
  case "TEXT":
1822
1851
  if (typeof value === "string") {
@@ -1950,13 +1979,15 @@ function planAccess(schema, where, orderBy, parameters, indexes) {
1950
1979
  }
1951
1980
  if (!key && !Object.keys(range).length) continue;
1952
1981
  const access = candidate.index ? equalityOnly ? "index-lookup" : "index-range" : equalityOnly ? "primary-key-lookup" : "primary-key-range";
1982
+ const ordered = traversalOrder(schema, candidate.column, candidate.index, orderBy, key);
1983
+ if (ordered && orderBy[0]?.direction === "desc") range.reverse = true;
1953
1984
  return {
1954
1985
  public: {
1955
1986
  table: schema.table,
1956
1987
  access,
1957
1988
  column: candidate.column.name,
1958
1989
  ...candidate.index ? { index: candidate.index.name } : {},
1959
- sort: orderBy.length ? "memory" : "none",
1990
+ sort: ordered ? "none" : "memory",
1960
1991
  reason: `use ${candidate.index ? `index ${candidate.index.name}` : "primary key"} for ${equalityOnly ? "equality" : "range"}; apply remaining predicates after lookup`
1961
1992
  },
1962
1993
  key,
@@ -1964,6 +1995,20 @@ function planAccess(schema, where, orderBy, parameters, indexes) {
1964
1995
  empty: !key && emptyRange(range)
1965
1996
  };
1966
1997
  }
1998
+ if (orderBy.length) for (const candidate of candidates) {
1999
+ if (!traversalOrder(schema, candidate.column, candidate.index, orderBy)) continue;
2000
+ return {
2001
+ public: {
2002
+ table: schema.table,
2003
+ access: candidate.index ? "index-range" : "table-scan",
2004
+ column: candidate.column.name,
2005
+ ...candidate.index ? { index: candidate.index.name } : {},
2006
+ sort: "none",
2007
+ reason: `use ${candidate.index ? `index ${candidate.index.name}` : "primary key"} traversal order`
2008
+ },
2009
+ range: { reverse: orderBy[0].direction === "desc" }
2010
+ };
2011
+ }
1967
2012
  return { public: {
1968
2013
  table: schema.table,
1969
2014
  access: "table-scan",
@@ -1971,6 +2016,15 @@ function planAccess(schema, where, orderBy, parameters, indexes) {
1971
2016
  reason: "no supported primary key or index bound; filter table rows"
1972
2017
  } };
1973
2018
  }
2019
+ function traversalOrder(schema, column, index, orders, key) {
2020
+ if (!orders.length || key && !index) return true;
2021
+ if (orders[0].column !== column.name) return false;
2022
+ if (!index) return orders.length === 1;
2023
+ if (column.type === "INTEGER" && !key) return false;
2024
+ if (orders.length === 1) return orders[0].direction === "asc" || index.unique && !column.nullable;
2025
+ const primary = primaryColumn(schema);
2026
+ return orders.length === 2 && orders[1].column === primary?.name && orders[1].direction === orders[0].direction;
2027
+ }
1974
2028
  function planKey(column, value, secondary, equality) {
1975
2029
  if (value === null) return null;
1976
2030
  if (column.type === "INTEGER") {
@@ -2023,27 +2077,48 @@ function emptyRange(range) {
2023
2077
  const compared = compareBytes(lower, upper);
2024
2078
  return compared > 0 || compared === 0 && (Boolean(range.gt) || Boolean(range.lt));
2025
2079
  }
2026
- async function readRows(tx, schema, plan) {
2080
+ async function readRows(tx, schema, plan, where, parameters, maxMatches) {
2027
2081
  if (plan.empty) return [];
2028
- let items;
2082
+ const rows = [];
2083
+ const accept = (item) => {
2084
+ if (item.key.length === 1 && item.key[0] === TABLE_OWNER_KEY[0]) return false;
2085
+ const row = decodeRow(schema, item);
2086
+ if (!where || evaluateWhere(row.row, where, parameters) === true) rows.push(row);
2087
+ return maxMatches !== void 0 && rows.length >= maxMatches;
2088
+ };
2029
2089
  if (plan.public.index) {
2030
- items = [];
2031
2090
  const range = plan.key ? {
2091
+ ...plan.range,
2032
2092
  gte: plan.key,
2033
2093
  lte: plan.key
2034
2094
  } : plan.range;
2035
- for await (const [key, value] of tx.scanByIndex(schema.table, plan.public.index, range)) items.push({
2095
+ for await (const [key, value] of tx.scanByIndex(schema.table, plan.public.index, range, { maxRows: Math.min(256, maxMatches ?? 256) })) if (accept({
2036
2096
  key,
2037
2097
  value
2038
- });
2098
+ })) break;
2039
2099
  } else if (plan.key) {
2040
2100
  const value = await tx.get(schema.table, plan.key);
2041
- items = value ? [{
2101
+ if (value) accept({
2042
2102
  key: plan.key,
2043
2103
  value
2044
- }] : [];
2045
- } else items = await tx.scan(schema.table, plan.range);
2046
- return items.filter(({ key }) => key.length !== 1 || key[0] !== TABLE_OWNER_KEY[0]).map((item) => decodeRow(schema, item));
2104
+ });
2105
+ } else if (typeof tx.scanPage === "function" && typeof tx.closeScanCursor === "function") {
2106
+ let cursor;
2107
+ try {
2108
+ for (;;) {
2109
+ const page = await tx.scanPage(schema.table, plan.range, {
2110
+ cursor,
2111
+ maxRows: Math.min(256, maxMatches === void 0 ? 256 : maxMatches - rows.length)
2112
+ });
2113
+ cursor = page.cursor;
2114
+ for (const item of page.rows) if (accept(item)) return rows;
2115
+ if (page.done) break;
2116
+ }
2117
+ } finally {
2118
+ if (cursor !== void 0) await tx.closeScanCursor(cursor);
2119
+ }
2120
+ } else for (const item of await tx.scan(schema.table, plan.range)) if (accept(item)) break;
2121
+ return rows;
2047
2122
  }
2048
2123
  function compareRows(left, right, orders) {
2049
2124
  for (const order of orders) {
@@ -2123,6 +2198,8 @@ function compareBytes(left, right) {
2123
2198
  //#endregion
2124
2199
  //#region src/index.ts
2125
2200
  var INDEX_SCAN_PAGE_ROWS = 256;
2201
+ var SCAN_PAGE_ROWS = 256;
2202
+ var SCAN_PAGE_BYTES = 8389662;
2126
2203
  async function callProxy(fn) {
2127
2204
  try {
2128
2205
  return await fn();
@@ -2135,6 +2212,7 @@ var TransactionImpl = class {
2135
2212
  #entry;
2136
2213
  #txId;
2137
2214
  #closed = false;
2215
+ #scanCursors = /* @__PURE__ */ new Set();
2138
2216
  #onClose;
2139
2217
  constructor(entry, txId, mode, onClose) {
2140
2218
  this.#entry = entry;
@@ -2191,21 +2269,48 @@ var TransactionImpl = class {
2191
2269
  assertPublicStoreName(store);
2192
2270
  return callProxy(() => this.#entry.proxy.scan(this.#txId, store, range));
2193
2271
  }
2272
+ async scanPage(store, range = {}, options = {}) {
2273
+ this.#ensureOpen();
2274
+ assertPublicStoreName(store);
2275
+ const normalized = normalizeScanPageOptions(options);
2276
+ if (normalized.cursor !== void 0 && !this.#scanCursors.has(normalized.cursor)) throw new TypeError("scan cursor does not belong to this transaction");
2277
+ const page = await callProxy(() => this.#entry.proxy.scanPage({
2278
+ txId: this.#txId,
2279
+ store,
2280
+ range,
2281
+ cursorId: normalized.cursor,
2282
+ maxRows: normalized.maxRows,
2283
+ maxBytes: normalized.maxBytes
2284
+ }));
2285
+ this.#ensureOpen();
2286
+ if (normalized.cursor !== void 0) this.#scanCursors.delete(normalized.cursor);
2287
+ if (page.cursorId !== void 0 && !this.#closed) this.#scanCursors.add(page.cursorId);
2288
+ return publicScanPage(page);
2289
+ }
2290
+ scanIter(store, range = {}, options = {}) {
2291
+ return iterateScanPages(this, store, range, options);
2292
+ }
2293
+ async closeScanCursor(cursor) {
2294
+ normalizeScanCursor(cursor);
2295
+ if (!this.#scanCursors.delete(cursor) || this.#entry.invalidated) return;
2296
+ await callProxy(() => this.#entry.proxy.closeCursor(cursor));
2297
+ }
2194
2298
  async getByIndex(store, indexName, key) {
2195
2299
  this.#ensureOpen();
2196
2300
  assertPublicStoreName(store);
2197
2301
  return callProxy(() => this.#entry.proxy.getByIndex(this.#txId, store, indexName, key));
2198
2302
  }
2199
- async *scanByIndex(store, indexName, range = {}) {
2303
+ async *scanByIndex(store, indexName, range = {}, options = {}) {
2200
2304
  this.#ensureOpen();
2201
2305
  assertPublicStoreName(store);
2306
+ const { maxRows: pageRows, maxBytes } = normalizeScanPageOptions(options);
2202
2307
  let remaining = range.limit ?? Number.POSITIVE_INFINITY;
2203
2308
  let cursor = null;
2204
2309
  while (remaining > 0) {
2205
2310
  this.#ensureOpen();
2206
- const pageLimit = Math.min(remaining, INDEX_SCAN_PAGE_ROWS);
2311
+ const pageLimit = Math.min(remaining, pageRows, INDEX_SCAN_PAGE_ROWS);
2207
2312
  const resumeAfter = cursor;
2208
- const page = await callProxy(() => this.#entry.proxy.scanByIndexPage(this.#txId, store, indexName, range, resumeAfter, pageLimit));
2313
+ const page = await callProxy(() => this.#entry.proxy.scanByIndexPage(this.#txId, store, indexName, range, resumeAfter, pageLimit, maxBytes));
2209
2314
  for (const row of page.rows) yield [row.key, row.value];
2210
2315
  remaining -= page.rows.length;
2211
2316
  if (page.cursor === null) return;
@@ -2270,6 +2375,7 @@ var TransactionImpl = class {
2270
2375
  #markClosed() {
2271
2376
  if (this.#closed) return;
2272
2377
  this.#closed = true;
2378
+ this.#scanCursors.clear();
2273
2379
  this.#onClose();
2274
2380
  }
2275
2381
  };
@@ -2279,6 +2385,8 @@ var DBImpl = class {
2279
2385
  #closePromise = null;
2280
2386
  #transactions = /* @__PURE__ */ new Set();
2281
2387
  #pendingBegins = /* @__PURE__ */ new Set();
2388
+ #scanCursors = /* @__PURE__ */ new Set();
2389
+ #pendingScanPages = /* @__PURE__ */ new Set();
2282
2390
  #transactionGeneration = 0;
2283
2391
  #subscriptions;
2284
2392
  #unsubscribeTxInvalidated = null;
@@ -2402,6 +2510,42 @@ var DBImpl = class {
2402
2510
  assertPublicStoreName(store);
2403
2511
  return this.#autocommit("readonly", "scan", [store, range]);
2404
2512
  }
2513
+ async scanPage(store, range = {}, options = {}) {
2514
+ this.#ensureOpen();
2515
+ assertPublicStoreName(store);
2516
+ const normalized = normalizeScanPageOptions(options);
2517
+ if (normalized.cursor !== void 0 && !this.#scanCursors.has(normalized.cursor)) throw new TypeError("scan cursor does not belong to this database handle");
2518
+ const generation = this.#transactionGeneration;
2519
+ const pending = callProxy(() => this.#entry.proxy.scanPage({
2520
+ store,
2521
+ range,
2522
+ cursorId: normalized.cursor,
2523
+ maxRows: normalized.maxRows,
2524
+ maxBytes: normalized.maxBytes
2525
+ }));
2526
+ this.#pendingScanPages.add(pending);
2527
+ try {
2528
+ const page = await pending;
2529
+ if (normalized.cursor !== void 0) this.#scanCursors.delete(normalized.cursor);
2530
+ if (page.cursorId !== void 0) this.#scanCursors.add(page.cursorId);
2531
+ this.#ensureOpen();
2532
+ if (generation !== this.#transactionGeneration) {
2533
+ if (page.cursorId !== void 0) await this.closeScanCursor(page.cursorId);
2534
+ throw new TransactionClosedError();
2535
+ }
2536
+ return publicScanPage(page);
2537
+ } finally {
2538
+ this.#pendingScanPages.delete(pending);
2539
+ }
2540
+ }
2541
+ scanIter(store, range = {}, options = {}) {
2542
+ return iterateScanPages(this, store, range, options);
2543
+ }
2544
+ async closeScanCursor(cursor) {
2545
+ normalizeScanCursor(cursor);
2546
+ if (!this.#scanCursors.delete(cursor) || this.#entry.invalidated) return;
2547
+ await callProxy(() => this.#entry.proxy.closeCursor(cursor));
2548
+ }
2405
2549
  /** The transaction boundary stays in the worker, so one round trip is enough. */
2406
2550
  async #autocommit(mode, command, args) {
2407
2551
  this.#ensureOpen();
@@ -2481,7 +2625,10 @@ var DBImpl = class {
2481
2625
  }
2482
2626
  async #finishClose() {
2483
2627
  await Promise.allSettled(Array.from(this.#pendingBegins));
2628
+ await Promise.allSettled(Array.from(this.#pendingScanPages));
2484
2629
  let rollbackError = null;
2630
+ const cursorResults = await Promise.allSettled(Array.from(this.#scanCursors, (cursor) => this.closeScanCursor(cursor)));
2631
+ for (const result of cursorResults) if (result.status === "rejected" && rollbackError === null) rollbackError = normalizeError(result.reason);
2485
2632
  const txs = Array.from(this.#transactions);
2486
2633
  if (txs.length > 0) {
2487
2634
  const results = await Promise.allSettled(txs.map((tx) => tx.rollbackIfOpen()));
@@ -2502,6 +2649,7 @@ var DBImpl = class {
2502
2649
  }
2503
2650
  #forceCloseTransactions() {
2504
2651
  this.#transactionGeneration += 1;
2652
+ this.#scanCursors.clear();
2505
2653
  for (const tx of Array.from(this.#transactions)) tx.forceClose();
2506
2654
  }
2507
2655
  #markInvalidated() {
@@ -2573,11 +2721,20 @@ var MigrationTransactionImpl = class {
2573
2721
  async scan(store, range = {}) {
2574
2722
  return this.#inner.scan(store, range);
2575
2723
  }
2724
+ scanPage(store, range = {}, options = {}) {
2725
+ return this.#inner.scanPage(store, range, options);
2726
+ }
2727
+ scanIter(store, range = {}, options = {}) {
2728
+ return this.#inner.scanIter(store, range, options);
2729
+ }
2730
+ closeScanCursor(cursor) {
2731
+ return this.#inner.closeScanCursor(cursor);
2732
+ }
2576
2733
  async getByIndex(store, indexName, key) {
2577
2734
  return this.#inner.getByIndex(store, indexName, key);
2578
2735
  }
2579
- scanByIndex(store, indexName, range = {}) {
2580
- return this.#inner.scanByIndex(store, indexName, range);
2736
+ scanByIndex(store, indexName, range = {}, options = {}) {
2737
+ return this.#inner.scanByIndex(store, indexName, range, options);
2581
2738
  }
2582
2739
  async createStore(name, options = {}) {
2583
2740
  await this.#inner.createStore(name, options);
@@ -2648,6 +2805,15 @@ var MigrationDbImpl = class {
2648
2805
  async scan(store, range = {}) {
2649
2806
  return this.#transaction.scan(store, range);
2650
2807
  }
2808
+ scanPage(store, range = {}, options = {}) {
2809
+ return this.#transaction.scanPage(store, range, options);
2810
+ }
2811
+ scanIter(store, range = {}, options = {}) {
2812
+ return this.#transaction.scanIter(store, range, options);
2813
+ }
2814
+ closeScanCursor(cursor) {
2815
+ return this.#transaction.closeScanCursor(cursor);
2816
+ }
2651
2817
  exportSnapshot(_options = {}) {
2652
2818
  return Promise.reject(migrationUnsupportedMethod("db.exportSnapshot"));
2653
2819
  }
@@ -2693,8 +2859,61 @@ function migrationUnsupportedMethod(method) {
2693
2859
  error.name = "InvalidStateError";
2694
2860
  return error;
2695
2861
  }
2862
+ function publicScanPage(page) {
2863
+ return {
2864
+ rows: page.rows,
2865
+ cursor: page.cursorId,
2866
+ done: page.done,
2867
+ bytes: page.bytes
2868
+ };
2869
+ }
2870
+ function normalizeScanCursor(value) {
2871
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) throw new TypeError("scan cursor must be a positive safe integer");
2872
+ return value;
2873
+ }
2874
+ function normalizeScanPageOptions(options) {
2875
+ const input = normalizeOptionsObject(options, "scan page options must be an object");
2876
+ const maxRows = input.maxRows ?? SCAN_PAGE_ROWS;
2877
+ const maxBytes = input.maxBytes ?? SCAN_PAGE_BYTES;
2878
+ if (!Number.isSafeInteger(maxRows) || maxRows <= 0 || maxRows > 4294967295) throw new TypeError("scan page maxRows must be a positive 32-bit integer");
2879
+ if (!Number.isSafeInteger(maxBytes) || maxBytes < 4 || maxBytes > 4294967295) throw new TypeError("scan page maxBytes must be a 32-bit integer of at least 4");
2880
+ return {
2881
+ maxRows,
2882
+ maxBytes,
2883
+ cursor: input.cursor === void 0 ? void 0 : normalizeScanCursor(input.cursor)
2884
+ };
2885
+ }
2886
+ async function* iterateScanPages(source, store, range, options) {
2887
+ const bounds = { ...range };
2888
+ for (const field of [
2889
+ "gt",
2890
+ "gte",
2891
+ "lt",
2892
+ "lte"
2893
+ ]) {
2894
+ const bound = bounds[field];
2895
+ if (bound instanceof Uint8Array) bounds[field] = bound.slice();
2896
+ }
2897
+ const pageOptions = normalizeScanPageOptions(options);
2898
+ let cursor;
2899
+ try {
2900
+ for (;;) {
2901
+ const page = await source.scanPage(store, bounds, {
2902
+ ...pageOptions,
2903
+ cursor
2904
+ });
2905
+ cursor = page.cursor;
2906
+ for (const row of page.rows) yield row;
2907
+ if (page.done) return;
2908
+ }
2909
+ } finally {
2910
+ if (cursor !== void 0) await source.closeScanCursor(cursor);
2911
+ }
2912
+ }
2696
2913
  function normalizeDatabaseName(value, method) {
2697
2914
  if (typeof value !== "string" || value.length === 0) throw new TypeError(`${method}() database name must be a non-empty string`);
2915
+ if (/[\uD800-\uDFFF]/u.test(value)) throw new TypeError(`${method}() database name must contain valid Unicode`);
2916
+ if (new TextEncoder().encode(value).length > 127) throw new TypeError(`${method}() database name must contain at most 127 UTF-8 bytes`);
2698
2917
  return value;
2699
2918
  }
2700
2919
  var typeError = (message) => new TypeError(message);
@@ -2722,17 +2941,17 @@ function normalizePutOptions(options) {
2722
2941
  const normalizedTtl = normalizeOptionalNonNegativeSafeInteger(ttl, "ttl must be a non-negative safe integer");
2723
2942
  return normalizedTtl === void 0 ? {} : { ttl: normalizedTtl };
2724
2943
  }
2725
- function normalizeCompressionOptions(options, message) {
2944
+ function normalizeCompressionOptions(options, message, allowSnappy) {
2726
2945
  const { compression } = normalizeOptionsObject(options, message);
2727
2946
  if (compression === void 0) return {};
2728
- if (compression !== false && compression !== "gzip" && compression !== "deflate") throw new TypeError("compression must be \"gzip\", \"deflate\", or false");
2947
+ if (compression !== false && compression !== "gzip" && compression !== "deflate" && !(allowSnappy && compression === "snappy")) throw new TypeError(allowSnappy ? "compression must be \"gzip\", \"deflate\", \"snappy\", or false" : "compression must be \"gzip\", \"deflate\", or false");
2729
2948
  return { compression };
2730
2949
  }
2731
2950
  function normalizeCreateStoreOptions(options) {
2732
- return normalizeCompressionOptions(options, "createStore options must be an object");
2951
+ return normalizeCompressionOptions(options, "createStore options must be an object", true);
2733
2952
  }
2734
2953
  function normalizeExportSnapshotOptions(options) {
2735
- return normalizeCompressionOptions(options, "exportSnapshot options must be an object");
2954
+ return normalizeCompressionOptions(options, "exportSnapshot options must be an object", false);
2736
2955
  }
2737
2956
  function normalizeTxId(value) {
2738
2957
  return normalizeNonNegativeSafeInteger(value, "txid must be a non-negative safe integer");
@@ -2883,6 +3102,9 @@ var NodeWorkerTransport = class {
2883
3102
  var LOCK_DIRECTORY = ".moyodb.lock";
2884
3103
  var PENDING_PREFIX = ".moyodb.pending-";
2885
3104
  var TOKEN_PATTERN = /^[a-f\d]{8}-[a-f\d]{4}-[a-f\d]{4}-[a-f\d]{4}-[a-f\d]{12}$/i;
3105
+ var MAX_LEASE_RECORD_BYTES = 1024;
3106
+ var MAX_LEASE_PID = 4294967295;
3107
+ var MISSING_DATABASE_MESSAGE = "database missing and create_if_missing=false";
2886
3108
  function namedError(name, message, cause) {
2887
3109
  const error = new Error(message, cause === void 0 ? void 0 : { cause });
2888
3110
  error.name = name;
@@ -2928,9 +3150,9 @@ function readLease(directory, token) {
2928
3150
  const lockDirectory = join(directory, LOCK_DIRECTORY);
2929
3151
  entryStat(lockDirectory, "directory");
2930
3152
  const recordPath = join(lockDirectory, `${validateToken(token)}.json`);
2931
- entryStat(recordPath, "file");
3153
+ if (entryStat(recordPath, "file").size > MAX_LEASE_RECORD_BYTES) throw namedError("DatabaseBusyError", "storage lock owner cannot be verified");
2932
3154
  const owner = JSON.parse(fs.readFileSync(recordPath, "utf8"));
2933
- if (owner.token !== token || !Number.isSafeInteger(owner.pid) || owner.pid <= 0) throw namedError("DatabaseBusyError", "storage lock owner cannot be verified");
3155
+ if (owner.token !== token || !Number.isSafeInteger(owner.pid) || owner.pid <= 0 || owner.pid > MAX_LEASE_PID) throw namedError("DatabaseBusyError", "storage lock owner cannot be verified");
2934
3156
  return {
2935
3157
  lockDirectory,
2936
3158
  recordPath,
@@ -3011,6 +3233,21 @@ function releaseNodeStorageLease(directory, lockToken) {
3011
3233
  throw storageError(error);
3012
3234
  }
3013
3235
  }
3236
+ function ensureStorageRoot(directory, createIfMissing = true) {
3237
+ const path = validateDirectory(directory);
3238
+ if (!createIfMissing) {
3239
+ try {
3240
+ entryStat(path, "directory");
3241
+ } catch (error) {
3242
+ if (error?.code === "ENOENT") throw namedError("StorageError", MISSING_DATABASE_MESSAGE);
3243
+ if (error?.name === "TypeMismatchError" || error?.name === "SecurityError") throw error;
3244
+ throw storageError(error);
3245
+ }
3246
+ return path;
3247
+ }
3248
+ fs.mkdirSync(path, { recursive: true });
3249
+ return path;
3250
+ }
3014
3251
  //#endregion
3015
3252
  //#region src/node.ts
3016
3253
  var registry = /* @__PURE__ */ new Map();
@@ -3035,7 +3272,12 @@ function validateDirectoryOptions(options) {
3035
3272
  if (typeof directory !== "string" || directory.length === 0 || directory.includes("\0")) throw new InvalidOpenOptionsError("directory must be a non-empty filesystem path");
3036
3273
  }
3037
3274
  async function databaseLocation(name, options) {
3038
- await mkdir(options.directory, { recursive: true });
3275
+ try {
3276
+ ensureStorageRoot(options.directory, options.createIfMissing !== false);
3277
+ } catch (error) {
3278
+ if (error instanceof Error && error.name === "StorageError") throw new StorageError(error.message || "database missing and create_if_missing=false");
3279
+ throw error;
3280
+ }
3039
3281
  const root = await realpath(options.directory);
3040
3282
  const directory = join(root, "stackdb", Buffer.from(name, "utf8").toString("hex"));
3041
3283
  return {
@@ -3069,7 +3311,8 @@ function createEntry(dbName, directory, registryKey, options) {
3069
3311
  channelName,
3070
3312
  lockToken,
3071
3313
  ownerWaitMs: normalized.ownerWaitMs,
3072
- encodedDbName: Buffer.from(dbName, "utf8").toString("hex")
3314
+ encodedDbName: Buffer.from(dbName, "utf8").toString("hex"),
3315
+ createIfMissing: normalized.createIfMissing
3073
3316
  }, () => {
3074
3317
  releaseNodeStorageLease(directory, lockToken);
3075
3318
  });