@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.
- package/README.md +142 -0
- package/dist/assets/browser-worker-DBt51gsS.js +4 -0
- package/dist/assets/browser-worker-DBt51gsS.js.map +1 -0
- package/dist/assets/shared-worker-NdniRuEL.js +2 -0
- package/dist/assets/shared-worker-NdniRuEL.js.map +1 -0
- package/dist/chunks/{indexing-BzmFhWXs.js → indexing-CKtbpaZn.js} +93 -12
- package/dist/chunks/indexing-CKtbpaZn.js.map +1 -0
- package/dist/compression.d.ts +16 -2
- package/dist/compression.d.ts.map +1 -1
- package/dist/engine/README.md +30 -3
- package/dist/engine/moyodb_engine.d.ts +34 -10
- package/dist/engine/moyodb_engine.js +238 -51
- package/dist/engine/moyodb_engine_bg.wasm +0 -0
- package/dist/engine/moyodb_engine_bg.wasm.d.ts +17 -10
- package/dist/engine/package.json +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/internal.d.ts +6 -0
- package/dist/internal.d.ts.map +1 -1
- package/dist/moyodb-sdk.js +1122 -883
- package/dist/moyodb-sdk.js.map +1 -1
- package/dist/node-engine.js +703 -78
- package/dist/node-engine.js.map +1 -1
- package/dist/node-storage.d.mts +4 -0
- package/dist/node-storage.mjs +38 -3
- package/dist/node-worker-client.d.ts +1 -0
- package/dist/node-worker-client.d.ts.map +1 -1
- package/dist/node-worker.mjs +2 -1
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +280 -37
- package/dist/node.js.map +1 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/shared-worker-client.d.ts +4 -0
- package/dist/shared-worker-client.d.ts.map +1 -1
- package/dist/shared-worker.d.ts.map +1 -1
- package/dist/snappy.d.ts +3 -0
- package/dist/snappy.d.ts.map +1 -0
- package/dist/sql-parser.d.ts.map +1 -1
- package/dist/types.d.ts +33 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/worker-api.d.ts +17 -1
- package/dist/worker-api.d.ts.map +1 -1
- package/dist/worker-client.d.ts +7 -2
- package/dist/worker-client.d.ts.map +1 -1
- package/dist/worker-protocol.d.ts +11 -1
- package/dist/worker-protocol.d.ts.map +1 -1
- package/dist/worker-server.d.ts.map +1 -1
- package/dist/worker.d.ts +31 -2
- package/dist/worker.d.ts.map +1 -1
- package/package.json +4 -4
- package/dist/assets/browser-worker-DP6Mjz7c.js +0 -4
- package/dist/assets/browser-worker-DP6Mjz7c.js.map +0 -1
- package/dist/assets/shared-worker-DKzhauVf.js +0 -2
- package/dist/assets/shared-worker-DKzhauVf.js.map +0 -1
- package/dist/chunks/indexing-BzmFhWXs.js.map +0 -1
- package/dist/engine/snippets/moyodb-engine-cff0a0e2441a7a8f/js/opfs_shim.js +0 -513
- /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
|
|
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 {
|
|
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)
|
|
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.
|
|
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
|
-
|
|
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
|
-
},
|
|
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
|
|
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 (
|
|
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"
|
|
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:
|
|
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
|
-
|
|
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))
|
|
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
|
-
|
|
2101
|
+
if (value) accept({
|
|
2042
2102
|
key: plan.key,
|
|
2043
2103
|
value
|
|
2044
|
-
}
|
|
2045
|
-
} else
|
|
2046
|
-
|
|
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
|
-
|
|
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
|
});
|