@moyodb/sdk 1.1.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/CONTRIBUTING.md +46 -0
  2. package/README.md +144 -0
  3. package/dist/assets/browser-worker-Bj2ErRvC.js +4 -0
  4. package/dist/assets/browser-worker-Bj2ErRvC.js.map +1 -0
  5. package/dist/assets/shared-worker-CMeTcusa.js +2 -0
  6. package/dist/assets/shared-worker-CMeTcusa.js.map +1 -0
  7. package/dist/chunks/{indexing-BzmFhWXs.js → indexing-B0duqlUs.js} +131 -124
  8. package/dist/chunks/indexing-B0duqlUs.js.map +1 -0
  9. package/dist/compression.d.ts +16 -2
  10. package/dist/compression.d.ts.map +1 -1
  11. package/dist/demo.d.ts.map +1 -1
  12. package/dist/engine/README.md +32 -3
  13. package/dist/engine/moyodb_engine.d.ts +36 -10
  14. package/dist/engine/moyodb_engine.js +261 -51
  15. package/dist/engine/moyodb_engine_bg.wasm +0 -0
  16. package/dist/engine/moyodb_engine_bg.wasm.d.ts +18 -10
  17. package/dist/engine/package.json +1 -1
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/indexing.d.ts.map +1 -1
  20. package/dist/internal.d.ts +6 -0
  21. package/dist/internal.d.ts.map +1 -1
  22. package/dist/moyodb-sdk.js +1069 -835
  23. package/dist/moyodb-sdk.js.map +1 -1
  24. package/dist/node-engine.js +731 -111
  25. package/dist/node-engine.js.map +1 -1
  26. package/dist/node-storage.d.mts +4 -0
  27. package/dist/node-storage.mjs +38 -3
  28. package/dist/node-worker-client.d.ts +1 -0
  29. package/dist/node-worker-client.d.ts.map +1 -1
  30. package/dist/node-worker.mjs +2 -1
  31. package/dist/node.d.ts.map +1 -1
  32. package/dist/node.js +302 -67
  33. package/dist/node.js.map +1 -1
  34. package/dist/registry.d.ts.map +1 -1
  35. package/dist/shared-worker-client.d.ts +4 -0
  36. package/dist/shared-worker-client.d.ts.map +1 -1
  37. package/dist/shared-worker.d.ts.map +1 -1
  38. package/dist/snappy.d.ts +3 -0
  39. package/dist/snappy.d.ts.map +1 -0
  40. package/dist/sql-parser.d.ts.map +1 -1
  41. package/dist/types.d.ts +33 -3
  42. package/dist/types.d.ts.map +1 -1
  43. package/dist/worker-api.d.ts +19 -6
  44. package/dist/worker-api.d.ts.map +1 -1
  45. package/dist/worker-client.d.ts +7 -2
  46. package/dist/worker-client.d.ts.map +1 -1
  47. package/dist/worker-protocol.d.ts +18 -3
  48. package/dist/worker-protocol.d.ts.map +1 -1
  49. package/dist/worker-server.d.ts.map +1 -1
  50. package/dist/worker.d.ts +33 -4
  51. package/dist/worker.d.ts.map +1 -1
  52. package/package.json +6 -5
  53. package/dist/assets/browser-worker-DP6Mjz7c.js +0 -4
  54. package/dist/assets/browser-worker-DP6Mjz7c.js.map +0 -1
  55. package/dist/assets/shared-worker-DKzhauVf.js +0 -2
  56. package/dist/assets/shared-worker-DKzhauVf.js.map +0 -1
  57. package/dist/chunks/indexing-BzmFhWXs.js.map +0 -1
  58. package/dist/engine/snippets/moyodb-engine-cff0a0e2441a7a8f/js/opfs_shim.js +0 -513
  59. /package/dist/engine/snippets/{moyodb-engine-8ebe6af0cc2e81b7 → moyodb-engine-1d53039816cbc2f7}/js/opfs_shim.js +0 -0
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-B0duqlUs.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;
@@ -2320,20 +2428,17 @@ var DBImpl = class {
2320
2428
  }
2321
2429
  }
2322
2430
  async listStores() {
2323
- this.#ensureOpen();
2324
- return callProxy(() => this.#entry.proxy.listStores());
2431
+ return this.#call(() => this.#entry.proxy.listStores());
2325
2432
  }
2326
2433
  async listIndexes() {
2327
2434
  this.#ensureOpen();
2328
2435
  return callProxy(() => this.#entry.proxy.getIndexes());
2329
2436
  }
2330
2437
  async getVersion() {
2331
- this.#ensureOpen();
2332
- return callProxy(() => this.#entry.proxy.getVersion());
2438
+ return this.#call(() => this.#entry.proxy.getVersion());
2333
2439
  }
2334
2440
  async changesSince(txid, options = {}) {
2335
- this.#ensureOpen();
2336
- return callProxy(() => this.#entry.proxy.changesSince(normalizeTxId(txid), normalizeChangeFeedOptions(options)));
2441
+ return this.#call(() => this.#entry.proxy.changesSince(normalizeTxId(txid), normalizeChangeFeedOptions(options)));
2337
2442
  }
2338
2443
  async #beginImpl(mode) {
2339
2444
  this.#ensureOpen();
@@ -2402,60 +2507,82 @@ var DBImpl = class {
2402
2507
  assertPublicStoreName(store);
2403
2508
  return this.#autocommit("readonly", "scan", [store, range]);
2404
2509
  }
2510
+ async scanPage(store, range = {}, options = {}) {
2511
+ this.#ensureOpen();
2512
+ assertPublicStoreName(store);
2513
+ const normalized = normalizeScanPageOptions(options);
2514
+ if (normalized.cursor !== void 0 && !this.#scanCursors.has(normalized.cursor)) throw new TypeError("scan cursor does not belong to this database handle");
2515
+ const generation = this.#transactionGeneration;
2516
+ const pending = callProxy(() => this.#entry.proxy.scanPage({
2517
+ store,
2518
+ range,
2519
+ cursorId: normalized.cursor,
2520
+ maxRows: normalized.maxRows,
2521
+ maxBytes: normalized.maxBytes
2522
+ }));
2523
+ this.#pendingScanPages.add(pending);
2524
+ try {
2525
+ const page = await pending;
2526
+ if (normalized.cursor !== void 0) this.#scanCursors.delete(normalized.cursor);
2527
+ if (page.cursorId !== void 0) this.#scanCursors.add(page.cursorId);
2528
+ this.#ensureOpen();
2529
+ if (generation !== this.#transactionGeneration) {
2530
+ if (page.cursorId !== void 0) await this.closeScanCursor(page.cursorId);
2531
+ throw new TransactionClosedError();
2532
+ }
2533
+ return publicScanPage(page);
2534
+ } finally {
2535
+ this.#pendingScanPages.delete(pending);
2536
+ }
2537
+ }
2538
+ scanIter(store, range = {}, options = {}) {
2539
+ return iterateScanPages(this, store, range, options);
2540
+ }
2541
+ async closeScanCursor(cursor) {
2542
+ normalizeScanCursor(cursor);
2543
+ if (!this.#scanCursors.delete(cursor) || this.#entry.invalidated) return;
2544
+ await callProxy(() => this.#entry.proxy.closeCursor(cursor));
2545
+ }
2405
2546
  /** The transaction boundary stays in the worker, so one round trip is enough. */
2406
2547
  async #autocommit(mode, command, args) {
2407
2548
  this.#ensureOpen();
2408
2549
  return callProxy(() => this.#entry.proxy.autocommit(mode, command, args));
2409
2550
  }
2410
2551
  async exportSnapshot(options = {}) {
2411
- this.#ensureOpen();
2412
- return callProxy(async () => {
2552
+ return this.#call(async () => {
2413
2553
  const bytes = await this.#entry.proxy.exportSnapshot(normalizeExportSnapshotOptions(options));
2414
2554
  return bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
2415
2555
  });
2416
2556
  }
2417
2557
  async importSnapshot(data) {
2418
- this.#ensureOpen();
2419
- invalidateTransactions(this.#entry);
2420
- return callProxy(() => {
2558
+ return this.#callAfterTxInvalidation(() => {
2421
2559
  const bytes = data instanceof Uint8Array ? data : new Uint8Array(data);
2422
2560
  return this.#entry.proxy.importSnapshot(bytes);
2423
2561
  });
2424
2562
  }
2425
2563
  async reset() {
2426
- this.#ensureOpen();
2427
- invalidateTransactions(this.#entry);
2428
- return callProxy(() => this.#entry.proxy.reset());
2564
+ return this.#callAfterTxInvalidation(() => this.#entry.proxy.reset());
2429
2565
  }
2430
2566
  async compact() {
2431
- this.#ensureOpen();
2432
- invalidateTransactions(this.#entry);
2433
- return callProxy(() => this.#entry.proxy.compact());
2567
+ return this.#callAfterTxInvalidation(() => this.#entry.proxy.compact());
2434
2568
  }
2435
2569
  async rebuild() {
2436
- this.#ensureOpen();
2437
- invalidateTransactions(this.#entry);
2438
- return callProxy(() => this.#entry.proxy.rebuild());
2570
+ return this.#callAfterTxInvalidation(() => this.#entry.proxy.rebuild());
2439
2571
  }
2440
2572
  async stats() {
2441
- this.#ensureOpen();
2442
- return callProxy(() => this.#entry.proxy.stats());
2573
+ return this.#call(() => this.#entry.proxy.stats());
2443
2574
  }
2444
2575
  async storageInfo() {
2445
- this.#ensureOpen();
2446
- return callProxy(() => this.#entry.proxy.storageInfo());
2576
+ return this.#call(() => this.#entry.proxy.storageInfo());
2447
2577
  }
2448
2578
  async requestPersistence() {
2449
- this.#ensureOpen();
2450
- return callProxy(() => this.#entry.proxy.requestPersistence());
2579
+ return this.#call(() => this.#entry.proxy.requestPersistence());
2451
2580
  }
2452
2581
  async destroy() {
2453
- this.#ensureOpen();
2454
- return callProxy(() => this.#entry.destroy?.() ?? destroyDbWorker(this.#entry));
2582
+ return this.#call(() => this.#entry.destroy?.() ?? destroyDbWorker(this.#entry));
2455
2583
  }
2456
2584
  async setFailpoint(failpoint) {
2457
- this.#ensureOpen();
2458
- return callProxy(() => this.#entry.proxy.setFailpoint(failpoint));
2585
+ return this.#call(() => this.#entry.proxy.setFailpoint(failpoint));
2459
2586
  }
2460
2587
  subscribe(arg1, arg2, arg3) {
2461
2588
  this.#ensureOpen();
@@ -2481,7 +2608,10 @@ var DBImpl = class {
2481
2608
  }
2482
2609
  async #finishClose() {
2483
2610
  await Promise.allSettled(Array.from(this.#pendingBegins));
2611
+ await Promise.allSettled(Array.from(this.#pendingScanPages));
2484
2612
  let rollbackError = null;
2613
+ const cursorResults = await Promise.allSettled(Array.from(this.#scanCursors, (cursor) => this.closeScanCursor(cursor)));
2614
+ for (const result of cursorResults) if (result.status === "rejected" && rollbackError === null) rollbackError = normalizeError(result.reason);
2485
2615
  const txs = Array.from(this.#transactions);
2486
2616
  if (txs.length > 0) {
2487
2617
  const results = await Promise.allSettled(txs.map((tx) => tx.rollbackIfOpen()));
@@ -2502,6 +2632,7 @@ var DBImpl = class {
2502
2632
  }
2503
2633
  #forceCloseTransactions() {
2504
2634
  this.#transactionGeneration += 1;
2635
+ this.#scanCursors.clear();
2505
2636
  for (const tx of Array.from(this.#transactions)) tx.forceClose();
2506
2637
  }
2507
2638
  #markInvalidated() {
@@ -2511,6 +2642,15 @@ var DBImpl = class {
2511
2642
  this.#forceCloseTransactions();
2512
2643
  this.#subscriptions.close();
2513
2644
  }
2645
+ #call(run) {
2646
+ this.#ensureOpen();
2647
+ return callProxy(run);
2648
+ }
2649
+ #callAfterTxInvalidation(run) {
2650
+ this.#ensureOpen();
2651
+ invalidateTransactions(this.#entry);
2652
+ return callProxy(run);
2653
+ }
2514
2654
  #ensureOpen() {
2515
2655
  if (this.#closed || this.#entry.invalidated) throw new DatabaseClosedError();
2516
2656
  }
@@ -2573,11 +2713,20 @@ var MigrationTransactionImpl = class {
2573
2713
  async scan(store, range = {}) {
2574
2714
  return this.#inner.scan(store, range);
2575
2715
  }
2716
+ scanPage(store, range = {}, options = {}) {
2717
+ return this.#inner.scanPage(store, range, options);
2718
+ }
2719
+ scanIter(store, range = {}, options = {}) {
2720
+ return this.#inner.scanIter(store, range, options);
2721
+ }
2722
+ closeScanCursor(cursor) {
2723
+ return this.#inner.closeScanCursor(cursor);
2724
+ }
2576
2725
  async getByIndex(store, indexName, key) {
2577
2726
  return this.#inner.getByIndex(store, indexName, key);
2578
2727
  }
2579
- scanByIndex(store, indexName, range = {}) {
2580
- return this.#inner.scanByIndex(store, indexName, range);
2728
+ scanByIndex(store, indexName, range = {}, options = {}) {
2729
+ return this.#inner.scanByIndex(store, indexName, range, options);
2581
2730
  }
2582
2731
  async createStore(name, options = {}) {
2583
2732
  await this.#inner.createStore(name, options);
@@ -2648,6 +2797,15 @@ var MigrationDbImpl = class {
2648
2797
  async scan(store, range = {}) {
2649
2798
  return this.#transaction.scan(store, range);
2650
2799
  }
2800
+ scanPage(store, range = {}, options = {}) {
2801
+ return this.#transaction.scanPage(store, range, options);
2802
+ }
2803
+ scanIter(store, range = {}, options = {}) {
2804
+ return this.#transaction.scanIter(store, range, options);
2805
+ }
2806
+ closeScanCursor(cursor) {
2807
+ return this.#transaction.closeScanCursor(cursor);
2808
+ }
2651
2809
  exportSnapshot(_options = {}) {
2652
2810
  return Promise.reject(migrationUnsupportedMethod("db.exportSnapshot"));
2653
2811
  }
@@ -2693,8 +2851,61 @@ function migrationUnsupportedMethod(method) {
2693
2851
  error.name = "InvalidStateError";
2694
2852
  return error;
2695
2853
  }
2854
+ function publicScanPage(page) {
2855
+ return {
2856
+ rows: page.rows,
2857
+ cursor: page.cursorId,
2858
+ done: page.done,
2859
+ bytes: page.bytes
2860
+ };
2861
+ }
2862
+ function normalizeScanCursor(value) {
2863
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) throw new TypeError("scan cursor must be a positive safe integer");
2864
+ return value;
2865
+ }
2866
+ function normalizeScanPageOptions(options) {
2867
+ const input = normalizeOptionsObject(options, "scan page options must be an object");
2868
+ const maxRows = input.maxRows ?? SCAN_PAGE_ROWS;
2869
+ const maxBytes = input.maxBytes ?? SCAN_PAGE_BYTES;
2870
+ if (!Number.isSafeInteger(maxRows) || maxRows <= 0 || maxRows > 4294967295) throw new TypeError("scan page maxRows must be a positive 32-bit integer");
2871
+ if (!Number.isSafeInteger(maxBytes) || maxBytes < 4 || maxBytes > 4294967295) throw new TypeError("scan page maxBytes must be a 32-bit integer of at least 4");
2872
+ return {
2873
+ maxRows,
2874
+ maxBytes,
2875
+ cursor: input.cursor === void 0 ? void 0 : normalizeScanCursor(input.cursor)
2876
+ };
2877
+ }
2878
+ async function* iterateScanPages(source, store, range, options) {
2879
+ const bounds = { ...range };
2880
+ for (const field of [
2881
+ "gt",
2882
+ "gte",
2883
+ "lt",
2884
+ "lte"
2885
+ ]) {
2886
+ const bound = bounds[field];
2887
+ if (bound instanceof Uint8Array) bounds[field] = bound.slice();
2888
+ }
2889
+ const pageOptions = normalizeScanPageOptions(options);
2890
+ let cursor;
2891
+ try {
2892
+ for (;;) {
2893
+ const page = await source.scanPage(store, bounds, {
2894
+ ...pageOptions,
2895
+ cursor
2896
+ });
2897
+ cursor = page.cursor;
2898
+ for (const row of page.rows) yield row;
2899
+ if (page.done) return;
2900
+ }
2901
+ } finally {
2902
+ if (cursor !== void 0) await source.closeScanCursor(cursor);
2903
+ }
2904
+ }
2696
2905
  function normalizeDatabaseName(value, method) {
2697
2906
  if (typeof value !== "string" || value.length === 0) throw new TypeError(`${method}() database name must be a non-empty string`);
2907
+ if (/[\uD800-\uDFFF]/u.test(value)) throw new TypeError(`${method}() database name must contain valid Unicode`);
2908
+ if (new TextEncoder().encode(value).length > 127) throw new TypeError(`${method}() database name must contain at most 127 UTF-8 bytes`);
2698
2909
  return value;
2699
2910
  }
2700
2911
  var typeError = (message) => new TypeError(message);
@@ -2722,17 +2933,17 @@ function normalizePutOptions(options) {
2722
2933
  const normalizedTtl = normalizeOptionalNonNegativeSafeInteger(ttl, "ttl must be a non-negative safe integer");
2723
2934
  return normalizedTtl === void 0 ? {} : { ttl: normalizedTtl };
2724
2935
  }
2725
- function normalizeCompressionOptions(options, message) {
2936
+ function normalizeCompressionOptions(options, message, allowSnappy) {
2726
2937
  const { compression } = normalizeOptionsObject(options, message);
2727
2938
  if (compression === void 0) return {};
2728
- if (compression !== false && compression !== "gzip" && compression !== "deflate") throw new TypeError("compression must be \"gzip\", \"deflate\", or false");
2939
+ 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
2940
  return { compression };
2730
2941
  }
2731
2942
  function normalizeCreateStoreOptions(options) {
2732
- return normalizeCompressionOptions(options, "createStore options must be an object");
2943
+ return normalizeCompressionOptions(options, "createStore options must be an object", true);
2733
2944
  }
2734
2945
  function normalizeExportSnapshotOptions(options) {
2735
- return normalizeCompressionOptions(options, "exportSnapshot options must be an object");
2946
+ return normalizeCompressionOptions(options, "exportSnapshot options must be an object", false);
2736
2947
  }
2737
2948
  function normalizeTxId(value) {
2738
2949
  return normalizeNonNegativeSafeInteger(value, "txid must be a non-negative safe integer");
@@ -2883,6 +3094,9 @@ var NodeWorkerTransport = class {
2883
3094
  var LOCK_DIRECTORY = ".moyodb.lock";
2884
3095
  var PENDING_PREFIX = ".moyodb.pending-";
2885
3096
  var TOKEN_PATTERN = /^[a-f\d]{8}-[a-f\d]{4}-[a-f\d]{4}-[a-f\d]{4}-[a-f\d]{12}$/i;
3097
+ var MAX_LEASE_RECORD_BYTES = 1024;
3098
+ var MAX_LEASE_PID = 4294967295;
3099
+ var MISSING_DATABASE_MESSAGE = "database missing and create_if_missing=false";
2886
3100
  function namedError(name, message, cause) {
2887
3101
  const error = new Error(message, cause === void 0 ? void 0 : { cause });
2888
3102
  error.name = name;
@@ -2928,9 +3142,9 @@ function readLease(directory, token) {
2928
3142
  const lockDirectory = join(directory, LOCK_DIRECTORY);
2929
3143
  entryStat(lockDirectory, "directory");
2930
3144
  const recordPath = join(lockDirectory, `${validateToken(token)}.json`);
2931
- entryStat(recordPath, "file");
3145
+ if (entryStat(recordPath, "file").size > MAX_LEASE_RECORD_BYTES) throw namedError("DatabaseBusyError", "storage lock owner cannot be verified");
2932
3146
  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");
3147
+ 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
3148
  return {
2935
3149
  lockDirectory,
2936
3150
  recordPath,
@@ -3011,6 +3225,21 @@ function releaseNodeStorageLease(directory, lockToken) {
3011
3225
  throw storageError(error);
3012
3226
  }
3013
3227
  }
3228
+ function ensureStorageRoot(directory, createIfMissing = true) {
3229
+ const path = validateDirectory(directory);
3230
+ if (!createIfMissing) {
3231
+ try {
3232
+ entryStat(path, "directory");
3233
+ } catch (error) {
3234
+ if (error?.code === "ENOENT") throw namedError("StorageError", MISSING_DATABASE_MESSAGE);
3235
+ if (error?.name === "TypeMismatchError" || error?.name === "SecurityError") throw error;
3236
+ throw storageError(error);
3237
+ }
3238
+ return path;
3239
+ }
3240
+ fs.mkdirSync(path, { recursive: true });
3241
+ return path;
3242
+ }
3014
3243
  //#endregion
3015
3244
  //#region src/node.ts
3016
3245
  var registry = /* @__PURE__ */ new Map();
@@ -3035,7 +3264,12 @@ function validateDirectoryOptions(options) {
3035
3264
  if (typeof directory !== "string" || directory.length === 0 || directory.includes("\0")) throw new InvalidOpenOptionsError("directory must be a non-empty filesystem path");
3036
3265
  }
3037
3266
  async function databaseLocation(name, options) {
3038
- await mkdir(options.directory, { recursive: true });
3267
+ try {
3268
+ ensureStorageRoot(options.directory, options.createIfMissing !== false);
3269
+ } catch (error) {
3270
+ if (error instanceof Error && error.name === "StorageError") throw new StorageError(error.message || "database missing and create_if_missing=false");
3271
+ throw error;
3272
+ }
3039
3273
  const root = await realpath(options.directory);
3040
3274
  const directory = join(root, "stackdb", Buffer.from(name, "utf8").toString("hex"));
3041
3275
  return {
@@ -3069,7 +3303,8 @@ function createEntry(dbName, directory, registryKey, options) {
3069
3303
  channelName,
3070
3304
  lockToken,
3071
3305
  ownerWaitMs: normalized.ownerWaitMs,
3072
- encodedDbName: Buffer.from(dbName, "utf8").toString("hex")
3306
+ encodedDbName: Buffer.from(dbName, "utf8").toString("hex"),
3307
+ createIfMissing: normalized.createIfMissing
3073
3308
  }, () => {
3074
3309
  releaseNodeStorageLease(directory, lockToken);
3075
3310
  });