@moyodb/sdk 1.0.1 → 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 +111 -28
- 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/browser-worker.d.ts +2 -0
- package/dist/browser-worker.d.ts.map +1 -0
- package/dist/change-events.d.ts +1 -1
- package/dist/chunks/indexing-CKtbpaZn.js +1657 -0
- package/dist/chunks/indexing-CKtbpaZn.js.map +1 -0
- package/dist/codec.d.ts +1 -1
- package/dist/compression.d.ts +16 -2
- package/dist/compression.d.ts.map +1 -1
- package/dist/demo.d.ts +2 -1
- package/dist/demo.d.ts.map +1 -1
- package/dist/engine/README.md +89 -16
- 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 +2 -2
- package/dist/errors.d.ts +3 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/index.d.ts +11 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/indexing.d.ts +1 -1
- package/dist/internal.d.ts +6 -0
- package/dist/internal.d.ts.map +1 -1
- package/dist/moyodb-sdk.js +2362 -640
- package/dist/moyodb-sdk.js.map +1 -1
- package/dist/node-engine.js +2838 -0
- package/dist/node-engine.js.map +1 -0
- package/dist/node-storage.d.mts +16 -0
- package/dist/node-storage.mjs +669 -0
- package/dist/node-worker-client.d.ts +20 -0
- package/dist/node-worker-client.d.ts.map +1 -0
- package/dist/node-worker.mjs +119 -0
- package/dist/node.d.ts +19 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +3437 -0
- package/dist/node.js.map +1 -0
- package/dist/records.d.ts +75 -0
- package/dist/records.d.ts.map +1 -0
- package/dist/registry.d.ts +12 -4
- package/dist/registry.d.ts.map +1 -1
- package/dist/shared-worker-client.d.ts +28 -0
- package/dist/shared-worker-client.d.ts.map +1 -0
- package/dist/shared-worker-protocol.d.ts +16 -0
- package/dist/shared-worker-protocol.d.ts.map +1 -0
- package/dist/shared-worker.d.ts +33 -0
- package/dist/shared-worker.d.ts.map +1 -0
- package/dist/snappy.d.ts +3 -0
- package/dist/snappy.d.ts.map +1 -0
- package/dist/sql-parser.d.ts +9 -0
- package/dist/sql-parser.d.ts.map +1 -0
- package/dist/sql-types.d.ts +82 -0
- package/dist/sql-types.d.ts.map +1 -0
- package/dist/sql.d.ts +32 -0
- package/dist/sql.d.ts.map +1 -0
- package/dist/subscriptions.d.ts +2 -2
- package/dist/subscriptions.d.ts.map +1 -1
- package/dist/types.d.ts +36 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/worker-api.d.ts +19 -3
- package/dist/worker-api.d.ts.map +1 -1
- package/dist/worker-client.d.ts +30 -6
- package/dist/worker-client.d.ts.map +1 -1
- package/dist/worker-protocol.d.ts +19 -4
- package/dist/worker-protocol.d.ts.map +1 -1
- package/dist/worker-server.d.ts +10 -2
- package/dist/worker-server.d.ts.map +1 -1
- package/dist/worker.d.ts +292 -1
- package/dist/worker.d.ts.map +1 -1
- package/package.json +13 -6
- package/dist/assets/worker-KDc8Khl_.js +0 -4
- package/dist/assets/worker-KDc8Khl_.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
|
@@ -0,0 +1,2838 @@
|
|
|
1
|
+
import { At as packedBinaryListBytes, Bt as unpackPackedOptionalValues, Et as isWorkerProtocolRequestMessage, F as utf8Encode, Ft as serializeWorkerError, Gt as isRecord, Ht as workerProtocolError, It as unpackPackedBatchOpKeys, Lt as unpackPackedBatchOps, Mt as packedScanRows, Pt as prepareWorkerResponsePayload, Rt as unpackPackedBinaryList, S as toPublicIndexDefinitions, St as isWorkerProtocolEnvelope, Tt as isWorkerProtocolRequestBatchMessage, Ut as clampTimerDelayMs, Vt as unpackPackedScanRows, _ as indexRangeToPhysicalRange, _t as decodeWorkerCommandPayload, a as createIndexKeyExtractor, b as normalizeIndexDefinitions, bt as isAutocommitCommand, c as decodeIndexedDocument, d as encodeIndexMetadataValue, dt as WORKER_PROTOCOL_READY, f as extractLogicalIndexKey, g as indexKeyExactRange, gt as captureWorkerPayload, ht as WORKER_PROTOCOL_RESPONSE_BATCH, i as compareNormalizedIndexDefinitions, jt as packedOptionalValues, kt as packedBatchOpsBytes, l as encodeIndexEntryKey, m as findIndexDefinition, mt as WORKER_PROTOCOL_RESPONSE, o as decodeIndexEntryKey, p as extractLogicalIndexKeyFromDocument, r as cloneNormalizedIndexDefinitions, s as decodeIndexMetadataValue, t as INDEX_METADATA_STORE, u as encodeIndexMetadataKey, xt as isWorkerCommand, y as isInternalStoreName, zt as unpackPackedBinaryPairKeys } from "./chunks/indexing-CKtbpaZn.js";
|
|
2
|
+
//#region src/worker-server.ts
|
|
3
|
+
/** The first argument identifies the transaction lane. */
|
|
4
|
+
var TRANSACTION_COMMANDS = /* @__PURE__ */ new Set([
|
|
5
|
+
"commit",
|
|
6
|
+
"rollback",
|
|
7
|
+
"createStore",
|
|
8
|
+
"dropStore",
|
|
9
|
+
"clearStore",
|
|
10
|
+
"get",
|
|
11
|
+
"getMany",
|
|
12
|
+
"has",
|
|
13
|
+
"put",
|
|
14
|
+
"putMany",
|
|
15
|
+
"delete",
|
|
16
|
+
"deleteMany",
|
|
17
|
+
"applyBatch",
|
|
18
|
+
"scan",
|
|
19
|
+
"getByIndex",
|
|
20
|
+
"scanByIndex",
|
|
21
|
+
"scanByIndexPage",
|
|
22
|
+
"getIndexes",
|
|
23
|
+
"reconcileIndexes",
|
|
24
|
+
"setSchemaVersion"
|
|
25
|
+
]);
|
|
26
|
+
/** These commands require exclusive access to the open engine. */
|
|
27
|
+
var EXCLUSIVE_COMMANDS = /* @__PURE__ */ new Set([
|
|
28
|
+
"open",
|
|
29
|
+
"close",
|
|
30
|
+
"destroy",
|
|
31
|
+
"importSnapshot",
|
|
32
|
+
"reset",
|
|
33
|
+
"compact",
|
|
34
|
+
"rebuild"
|
|
35
|
+
]);
|
|
36
|
+
/**
|
|
37
|
+
* These commands do not use the open engine, so browser storage calls
|
|
38
|
+
* can run without delaying its close.
|
|
39
|
+
*/
|
|
40
|
+
var UNSCHEDULED_COMMANDS = /* @__PURE__ */ new Set([
|
|
41
|
+
"deleteDB",
|
|
42
|
+
"storageInfo",
|
|
43
|
+
"requestPersistence"
|
|
44
|
+
]);
|
|
45
|
+
var noop = () => {};
|
|
46
|
+
/**
|
|
47
|
+
* Serializes each transaction lane, including awaited work. Readwrite
|
|
48
|
+
* autocommits share one writer lane. Exclusive commands wait for scheduled
|
|
49
|
+
* work and block later scheduled work until they finish.
|
|
50
|
+
*/
|
|
51
|
+
var RequestScheduler = class {
|
|
52
|
+
#lanes = /* @__PURE__ */ new Map();
|
|
53
|
+
#inFlight = /* @__PURE__ */ new Set();
|
|
54
|
+
#barrier = Promise.resolve();
|
|
55
|
+
#cursorLanes = /* @__PURE__ */ new Map();
|
|
56
|
+
schedule(command, args, task) {
|
|
57
|
+
if (UNSCHEDULED_COMMANDS.has(command)) return task();
|
|
58
|
+
if (EXCLUSIVE_COMMANDS.has(command)) {
|
|
59
|
+
if (this.#cursorLanes.size === 0) return this.#runExclusive(task);
|
|
60
|
+
return this.#runExclusive(async () => {
|
|
61
|
+
const result = await task();
|
|
62
|
+
this.#cursorLanes.clear();
|
|
63
|
+
return result;
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
if (command !== "scanPage" && command !== "closeCursor" && !((command === "commit" || command === "rollback") && this.#cursorLanes.size > 0)) return this.#runShared(laneFor(command, args), task);
|
|
67
|
+
const request = command === "scanPage" && isRecord(args[0]) ? args[0] : null;
|
|
68
|
+
const cursorId = request?.cursorId ?? (command === "closeCursor" ? args[0] : void 0);
|
|
69
|
+
const lane = (typeof cursorId === "number" ? this.#cursorLanes.get(cursorId) : void 0) ?? (request && typeof request.txId === "number" ? request.txId : typeof cursorId === "number" ? `cursor:${cursorId}` : laneFor(command, args));
|
|
70
|
+
return this.#runShared(lane, async () => {
|
|
71
|
+
try {
|
|
72
|
+
const result = await task();
|
|
73
|
+
if (request && isRecord(result)) {
|
|
74
|
+
if (typeof cursorId === "number") this.#cursorLanes.delete(cursorId);
|
|
75
|
+
if (typeof result.cursorId === "number") this.#cursorLanes.set(result.cursorId, lane ?? `cursor:${result.cursorId}`);
|
|
76
|
+
}
|
|
77
|
+
return result;
|
|
78
|
+
} finally {
|
|
79
|
+
if (command === "closeCursor" && typeof cursorId === "number") this.#cursorLanes.delete(cursorId);
|
|
80
|
+
if (command === "commit" || command === "rollback") {
|
|
81
|
+
for (const [cursor, ownerLane] of this.#cursorLanes) if (ownerLane === args[0]) this.#cursorLanes.delete(cursor);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
#runShared(lane, task) {
|
|
87
|
+
const barrier = this.#barrier;
|
|
88
|
+
const previous = lane === null ? void 0 : this.#lanes.get(lane);
|
|
89
|
+
const result = (async () => {
|
|
90
|
+
await barrier;
|
|
91
|
+
if (previous) await previous;
|
|
92
|
+
return task();
|
|
93
|
+
})();
|
|
94
|
+
const settled = result.then(noop, noop);
|
|
95
|
+
this.#inFlight.add(settled);
|
|
96
|
+
if (lane !== null) this.#lanes.set(lane, settled);
|
|
97
|
+
settled.then(() => {
|
|
98
|
+
this.#inFlight.delete(settled);
|
|
99
|
+
if (lane !== null && this.#lanes.get(lane) === settled) this.#lanes.delete(lane);
|
|
100
|
+
});
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
#runExclusive(task) {
|
|
104
|
+
const previousBarrier = this.#barrier;
|
|
105
|
+
const pending = Array.from(this.#inFlight);
|
|
106
|
+
const result = (async () => {
|
|
107
|
+
await previousBarrier;
|
|
108
|
+
await Promise.all(pending);
|
|
109
|
+
return task();
|
|
110
|
+
})();
|
|
111
|
+
this.#barrier = result.then(noop, noop);
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
var AUTOCOMMIT_WRITER_LANE = "autocommit:readwrite";
|
|
116
|
+
function laneFor(command, args) {
|
|
117
|
+
if (TRANSACTION_COMMANDS.has(command) && typeof args[0] === "number") return args[0];
|
|
118
|
+
if (command === "autocommit" && args[0] === "readwrite") return AUTOCOMMIT_WRITER_LANE;
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
var ResponseQueue = class {
|
|
122
|
+
scope;
|
|
123
|
+
queued = [];
|
|
124
|
+
queuedBytes = 0;
|
|
125
|
+
flushScheduled = false;
|
|
126
|
+
flushChannel = null;
|
|
127
|
+
closed = false;
|
|
128
|
+
constructor(scope) {
|
|
129
|
+
this.scope = scope;
|
|
130
|
+
}
|
|
131
|
+
enqueue = (response, transfer = []) => {
|
|
132
|
+
if (this.closed) return;
|
|
133
|
+
const captured = captureWorkerPayload(response, transfer);
|
|
134
|
+
if (this.queued.length > 0 && this.queuedBytes + captured.byteLength > 1048576) this.flush();
|
|
135
|
+
this.queued.push({
|
|
136
|
+
response: captured.value,
|
|
137
|
+
transfer: captured.transfer
|
|
138
|
+
});
|
|
139
|
+
this.queuedBytes += captured.byteLength;
|
|
140
|
+
if (!this.flushScheduled) {
|
|
141
|
+
this.flushScheduled = true;
|
|
142
|
+
queueMicrotask(() => {
|
|
143
|
+
if (this.closed || this.queued.length === 0) {
|
|
144
|
+
this.flushScheduled = false;
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
this.flushChannel ??= this.createFlushChannel();
|
|
148
|
+
this.flushChannel.port2.postMessage(null);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
if (this.queued.length >= 128 || this.queuedBytes >= 1048576) this.flush();
|
|
152
|
+
};
|
|
153
|
+
close() {
|
|
154
|
+
this.closed = true;
|
|
155
|
+
this.queued = [];
|
|
156
|
+
this.queuedBytes = 0;
|
|
157
|
+
this.flushChannel?.port1.close();
|
|
158
|
+
this.flushChannel?.port2.close();
|
|
159
|
+
this.flushChannel = null;
|
|
160
|
+
}
|
|
161
|
+
createFlushChannel() {
|
|
162
|
+
const channel = new MessageChannel();
|
|
163
|
+
channel.port1.addEventListener("message", () => {
|
|
164
|
+
this.flushScheduled = false;
|
|
165
|
+
this.flush();
|
|
166
|
+
});
|
|
167
|
+
channel.port1.start();
|
|
168
|
+
return channel;
|
|
169
|
+
}
|
|
170
|
+
flush() {
|
|
171
|
+
const queued = this.queued;
|
|
172
|
+
this.queued = [];
|
|
173
|
+
this.queuedBytes = 0;
|
|
174
|
+
if (this.closed || queued.length === 0) return;
|
|
175
|
+
const response = queued.length === 1 ? queued[0].response : {
|
|
176
|
+
type: WORKER_PROTOCOL_RESPONSE_BATCH,
|
|
177
|
+
version: 1,
|
|
178
|
+
responses: queued.map((entry) => entry.response)
|
|
179
|
+
};
|
|
180
|
+
const transfer = [];
|
|
181
|
+
for (const entry of queued) for (const buffer of entry.transfer) transfer.push(buffer);
|
|
182
|
+
postWorkerResponse(this.scope, response, transfer);
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
function exposeWorkerApi(api, scope = self) {
|
|
186
|
+
const scheduler = new RequestScheduler();
|
|
187
|
+
const responses = new ResponseQueue(scope);
|
|
188
|
+
const handleMessage = (event) => {
|
|
189
|
+
if (event.origin !== "" && event.origin !== scope.location.origin) return;
|
|
190
|
+
if (isWorkerProtocolRequestBatchMessage(event.data)) {
|
|
191
|
+
let remaining = event.data.requests.length;
|
|
192
|
+
const respond = (response, transfer) => {
|
|
193
|
+
responses.enqueue(response, transfer);
|
|
194
|
+
remaining -= 1;
|
|
195
|
+
if (remaining === 0) responses.flush();
|
|
196
|
+
};
|
|
197
|
+
for (const request of event.data.requests) dispatchWorkerRequest(scope, api, scheduler, request, respond);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
dispatchWorkerRequest(scope, api, scheduler, event.data);
|
|
201
|
+
};
|
|
202
|
+
scope.addEventListener("message", handleMessage);
|
|
203
|
+
scope.postMessage({
|
|
204
|
+
type: WORKER_PROTOCOL_READY,
|
|
205
|
+
version: 1,
|
|
206
|
+
batching: 1
|
|
207
|
+
});
|
|
208
|
+
return { close() {
|
|
209
|
+
scope.removeEventListener("message", handleMessage);
|
|
210
|
+
responses.close();
|
|
211
|
+
} };
|
|
212
|
+
}
|
|
213
|
+
async function dispatchWorkerRequest(scope, api, scheduler, data, respond = (response, transfer) => postWorkerResponse(scope, response, transfer)) {
|
|
214
|
+
if (!isWorkerProtocolEnvelope(data)) return;
|
|
215
|
+
const id = typeof data.id === "number" && Number.isSafeInteger(data.id) ? data.id : 0;
|
|
216
|
+
if (!isWorkerProtocolRequestMessage(data)) {
|
|
217
|
+
respond(errorResponse(id, workerProtocolError("WorkerProtocolError", "invalid worker protocol request")));
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (!isWorkerCommand(data.command)) {
|
|
221
|
+
respond(errorResponse(data.id, workerProtocolError("WorkerProtocolError", `unsupported worker command: ${String(data.command)}`)));
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const command = data.command;
|
|
225
|
+
const args = data.args;
|
|
226
|
+
try {
|
|
227
|
+
let responseCommand = command;
|
|
228
|
+
let invoke;
|
|
229
|
+
if (command === "autocommit") {
|
|
230
|
+
const [mode, inner, innerArgs] = args;
|
|
231
|
+
if (mode !== "readonly" && mode !== "readwrite" || !isAutocommitCommand(inner) || !Array.isArray(innerArgs)) throw workerProtocolError("WorkerProtocolError", "invalid autocommit request");
|
|
232
|
+
assertImplemented(api, inner);
|
|
233
|
+
responseCommand = inner;
|
|
234
|
+
invoke = () => runAutocommit(api, mode, inner, innerArgs);
|
|
235
|
+
} else {
|
|
236
|
+
assertImplemented(api, command);
|
|
237
|
+
invoke = () => invokeCommand(api, command, args);
|
|
238
|
+
}
|
|
239
|
+
const result = await scheduler.schedule(command, args, invoke);
|
|
240
|
+
const responsePayload = prepareWorkerResponsePayload(responseCommand, result);
|
|
241
|
+
respond(successResponse(data.id, responsePayload.result), responsePayload.transfer);
|
|
242
|
+
} catch (error) {
|
|
243
|
+
respond(errorResponse(data.id, error));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
function assertImplemented(api, command) {
|
|
247
|
+
if (typeof api[command] !== "function") throw workerProtocolError("WorkerProtocolError", `worker command is not implemented: ${command}`);
|
|
248
|
+
}
|
|
249
|
+
function invokeCommand(api, command, args) {
|
|
250
|
+
const packedResult = dispatchPackedCommand(api, command, args);
|
|
251
|
+
if (packedResult !== null) return packedResult;
|
|
252
|
+
return api[command].apply(api, decodeWorkerCommandPayload(command, args));
|
|
253
|
+
}
|
|
254
|
+
async function runAutocommit(api, mode, command, args) {
|
|
255
|
+
const txId = await api.begin(mode);
|
|
256
|
+
let result;
|
|
257
|
+
try {
|
|
258
|
+
result = await invokeCommand(api, command, [txId, ...args]);
|
|
259
|
+
} catch (error) {
|
|
260
|
+
try {
|
|
261
|
+
await api.rollback(txId);
|
|
262
|
+
} catch {}
|
|
263
|
+
throw error;
|
|
264
|
+
}
|
|
265
|
+
if (mode === "readwrite") await api.commit(txId);
|
|
266
|
+
else await api.rollback(txId);
|
|
267
|
+
return result;
|
|
268
|
+
}
|
|
269
|
+
function dispatchPackedCommand(api, command, args) {
|
|
270
|
+
const packedApi = api;
|
|
271
|
+
if (command === "getMany" || command === "deleteMany") {
|
|
272
|
+
const [txId, store, keys] = args;
|
|
273
|
+
if (typeof txId !== "number" || typeof store !== "string") return null;
|
|
274
|
+
const packedKeys = packedBinaryListBytes(keys);
|
|
275
|
+
if (packedKeys === null) return null;
|
|
276
|
+
if (command === "getMany" && typeof packedApi.getManyPacked === "function") return packedApi.getManyPacked(txId, store, packedKeys);
|
|
277
|
+
if (command === "deleteMany" && typeof packedApi.deleteManyPacked === "function") return packedApi.deleteManyPacked(txId, store, packedKeys);
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
if (command === "putMany") {
|
|
281
|
+
const [txId, store, entries, options] = args;
|
|
282
|
+
if (typeof txId !== "number" || typeof store !== "string" || typeof packedApi.putManyPacked !== "function") return null;
|
|
283
|
+
const packedEntries = packedBinaryListBytes(entries);
|
|
284
|
+
return packedEntries === null ? null : packedApi.putManyPacked(txId, store, packedEntries, options);
|
|
285
|
+
}
|
|
286
|
+
if (command === "applyBatch") {
|
|
287
|
+
const [txId, store, ops] = args;
|
|
288
|
+
if (typeof txId !== "number" || typeof store !== "string" || typeof packedApi.applyBatchPacked !== "function") return null;
|
|
289
|
+
const packedOps = packedBatchOpsBytes(ops);
|
|
290
|
+
return packedOps === null ? null : packedApi.applyBatchPacked(txId, store, packedOps);
|
|
291
|
+
}
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
function successResponse(id, result) {
|
|
295
|
+
return {
|
|
296
|
+
type: WORKER_PROTOCOL_RESPONSE,
|
|
297
|
+
version: 1,
|
|
298
|
+
id,
|
|
299
|
+
ok: true,
|
|
300
|
+
result
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
function errorResponse(id, error) {
|
|
304
|
+
return {
|
|
305
|
+
type: WORKER_PROTOCOL_RESPONSE,
|
|
306
|
+
version: 1,
|
|
307
|
+
id,
|
|
308
|
+
ok: false,
|
|
309
|
+
error: serializeWorkerError(error)
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
function postWorkerResponse(scope, response, transfer = []) {
|
|
313
|
+
if (transfer.length > 0) try {
|
|
314
|
+
scope.postMessage(response, transfer);
|
|
315
|
+
return;
|
|
316
|
+
} catch {}
|
|
317
|
+
scope.postMessage(response);
|
|
318
|
+
}
|
|
319
|
+
//#endregion
|
|
320
|
+
//#region src/snappy.ts
|
|
321
|
+
var BLOCK_SIZE = 65536;
|
|
322
|
+
var HASH_SIZE = 16384;
|
|
323
|
+
var MAX_LENGTH = 4294967295;
|
|
324
|
+
var MATCH_TABLE = new Uint32Array(HASH_SIZE);
|
|
325
|
+
function corrupt(message) {
|
|
326
|
+
const error = /* @__PURE__ */ new Error(`invalid Snappy block: ${message}`);
|
|
327
|
+
error.name = "CorruptionError";
|
|
328
|
+
return error;
|
|
329
|
+
}
|
|
330
|
+
function word(bytes, offset) {
|
|
331
|
+
return (bytes[offset] | bytes[offset + 1] << 8 | bytes[offset + 2] << 16 | bytes[offset + 3] << 24) >>> 0;
|
|
332
|
+
}
|
|
333
|
+
function hash(value, bits) {
|
|
334
|
+
return Math.imul(value, 2654435761) >>> 32 - bits;
|
|
335
|
+
}
|
|
336
|
+
function encodeSnappy(input) {
|
|
337
|
+
if (input.byteLength > MAX_LENGTH) throw new RangeError("Snappy input exceeds the 32-bit length limit");
|
|
338
|
+
const output = new Uint8Array(input.byteLength + Math.ceil(input.byteLength / 6) + 32);
|
|
339
|
+
let written = 0;
|
|
340
|
+
let remainingLength = input.byteLength;
|
|
341
|
+
while (remainingLength >= 128) {
|
|
342
|
+
output[written++] = remainingLength & 127 | 128;
|
|
343
|
+
remainingLength = Math.floor(remainingLength / 128);
|
|
344
|
+
}
|
|
345
|
+
output[written++] = remainingLength;
|
|
346
|
+
const literal = (start, end) => {
|
|
347
|
+
if (start === end) return;
|
|
348
|
+
let length = end - start - 1;
|
|
349
|
+
if (length < 60) output[written++] = length << 2;
|
|
350
|
+
else {
|
|
351
|
+
const lengthBytes = length <= 255 ? 1 : length <= 65535 ? 2 : length <= 16777215 ? 3 : 4;
|
|
352
|
+
output[written++] = 59 + lengthBytes << 2;
|
|
353
|
+
for (let index = 0; index < lengthBytes; index += 1) {
|
|
354
|
+
output[written++] = length & 255;
|
|
355
|
+
length >>>= 8;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
output.set(input.subarray(start, end), written);
|
|
359
|
+
written += end - start;
|
|
360
|
+
};
|
|
361
|
+
const copy = (offset, length) => {
|
|
362
|
+
while (length > 0) {
|
|
363
|
+
const count = Math.min(length, 64);
|
|
364
|
+
if (count >= 4 && count <= 11 && offset < 2048) {
|
|
365
|
+
output[written++] = 1 | count - 4 << 2 | offset >>> 8 << 5;
|
|
366
|
+
output[written++] = offset & 255;
|
|
367
|
+
} else {
|
|
368
|
+
output[written++] = 2 | count - 1 << 2;
|
|
369
|
+
output[written++] = offset & 255;
|
|
370
|
+
output[written++] = offset >>> 8;
|
|
371
|
+
}
|
|
372
|
+
length -= count;
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
let hashBits = 8;
|
|
376
|
+
while (1 << hashBits < Math.min(input.byteLength, HASH_SIZE)) hashBits += 1;
|
|
377
|
+
const tableSize = 1 << hashBits;
|
|
378
|
+
const table = MATCH_TABLE;
|
|
379
|
+
for (let blockStart = 0; blockStart < input.byteLength; blockStart += BLOCK_SIZE) {
|
|
380
|
+
const blockEnd = Math.min(blockStart + BLOCK_SIZE, input.byteLength);
|
|
381
|
+
table.fill(0, 0, tableSize);
|
|
382
|
+
let position = blockStart;
|
|
383
|
+
let literalStart = blockStart;
|
|
384
|
+
let misses = 0;
|
|
385
|
+
while (position + 4 <= blockEnd) {
|
|
386
|
+
const current = word(input, position);
|
|
387
|
+
const slot = hash(current, hashBits);
|
|
388
|
+
const previous = table[slot];
|
|
389
|
+
table[slot] = position - blockStart + 1;
|
|
390
|
+
const candidate = blockStart + previous - 1;
|
|
391
|
+
if (previous === 0 || current !== word(input, candidate)) {
|
|
392
|
+
misses += 1;
|
|
393
|
+
position += 1 + Math.min(misses >>> 5, 64);
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
literal(literalStart, position);
|
|
397
|
+
const matchStart = position;
|
|
398
|
+
position += 4;
|
|
399
|
+
while (position < blockEnd && input[position] === input[candidate + position - matchStart]) position += 1;
|
|
400
|
+
copy(matchStart - candidate, position - matchStart);
|
|
401
|
+
literalStart = position;
|
|
402
|
+
misses = 0;
|
|
403
|
+
if (position + 4 <= blockEnd) table[hash(word(input, position - 1), hashBits)] = position - blockStart;
|
|
404
|
+
}
|
|
405
|
+
literal(literalStart, blockEnd);
|
|
406
|
+
}
|
|
407
|
+
return output.subarray(0, written);
|
|
408
|
+
}
|
|
409
|
+
function decodeSnappy(input, maxOutputBytes, expectedOutputBytes) {
|
|
410
|
+
if (!Number.isSafeInteger(maxOutputBytes) || maxOutputBytes < 0 || maxOutputBytes > MAX_LENGTH) throw new RangeError("invalid Snappy output limit");
|
|
411
|
+
let read = 0;
|
|
412
|
+
let length = 0;
|
|
413
|
+
for (let index = 0;; index += 1) {
|
|
414
|
+
if (read >= input.byteLength || index === 5) throw corrupt("truncated or invalid length");
|
|
415
|
+
const byte = input[read++];
|
|
416
|
+
if (index === 4 && byte > 15) throw corrupt("length exceeds 32 bits");
|
|
417
|
+
length += (byte & 127) * 2 ** (index * 7);
|
|
418
|
+
if (byte < 128) break;
|
|
419
|
+
}
|
|
420
|
+
if (length > maxOutputBytes) throw corrupt(`output exceeds ${maxOutputBytes} byte limit`);
|
|
421
|
+
if (expectedOutputBytes !== void 0 && length !== expectedOutputBytes) throw corrupt(`length mismatch: expected ${expectedOutputBytes} bytes, got ${length}`);
|
|
422
|
+
const output = new Uint8Array(length);
|
|
423
|
+
let written = 0;
|
|
424
|
+
while (read < input.byteLength) {
|
|
425
|
+
const tag = input[read++];
|
|
426
|
+
const kind = tag & 3;
|
|
427
|
+
if (kind === 0) {
|
|
428
|
+
let count = tag >>> 2;
|
|
429
|
+
if (count < 60) count += 1;
|
|
430
|
+
else {
|
|
431
|
+
const lengthBytes = count - 59;
|
|
432
|
+
if (lengthBytes > input.byteLength - read) throw corrupt("truncated literal length");
|
|
433
|
+
count = 1;
|
|
434
|
+
for (let index = 0; index < lengthBytes; index += 1) count += input[read++] * 2 ** (index * 8);
|
|
435
|
+
}
|
|
436
|
+
if (count > input.byteLength - read || count > length - written) throw corrupt("literal exceeds input or output");
|
|
437
|
+
output.set(input.subarray(read, read + count), written);
|
|
438
|
+
read += count;
|
|
439
|
+
written += count;
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
const offsetBytes = kind === 1 ? 1 : kind === 2 ? 2 : 4;
|
|
443
|
+
if (offsetBytes > input.byteLength - read) throw corrupt("truncated copy offset");
|
|
444
|
+
const count = kind === 1 ? 4 + (tag >>> 2 & 7) : 1 + (tag >>> 2);
|
|
445
|
+
let offset = kind === 1 ? (tag & 224) << 3 : 0;
|
|
446
|
+
for (let index = 0; index < offsetBytes; index += 1) offset += input[read++] * 2 ** (index * 8);
|
|
447
|
+
if (offset === 0 || offset > written || count > length - written) throw corrupt("copy exceeds output or has an invalid offset");
|
|
448
|
+
let copied = 0;
|
|
449
|
+
while (copied < count) {
|
|
450
|
+
const chunk = Math.min(offset + copied, count - copied);
|
|
451
|
+
output.copyWithin(written + copied, written - offset, written - offset + chunk);
|
|
452
|
+
copied += chunk;
|
|
453
|
+
}
|
|
454
|
+
written += count;
|
|
455
|
+
}
|
|
456
|
+
if (written !== length) throw corrupt(`length mismatch: expected ${length} bytes, got ${written}`);
|
|
457
|
+
return output;
|
|
458
|
+
}
|
|
459
|
+
//#endregion
|
|
460
|
+
//#region src/compression.ts
|
|
461
|
+
var STORE_RECORD_MAGIC = utf8Encode("BDBZVAL1");
|
|
462
|
+
var SNAPSHOT_EXPORT_MAGIC = utf8Encode("BDBZSNP1");
|
|
463
|
+
var ENVELOPE_VERSION = 1;
|
|
464
|
+
var ENVELOPE_HEADER_SIZE = 18;
|
|
465
|
+
var COMPRESSION_TAG_NONE = 0;
|
|
466
|
+
var COMPRESSION_TAG_GZIP = 1;
|
|
467
|
+
var COMPRESSION_TAG_DEFLATE = 2;
|
|
468
|
+
var COMPRESSION_TAG_SNAPPY = 3;
|
|
469
|
+
var STORE_FLAG_COMPRESSION_SHIFT = 2;
|
|
470
|
+
var STORE_FLAG_COMPRESSION_MASK = 12;
|
|
471
|
+
var STORE_VALUE_COMPRESSION_PREFLIGHT_WINDOW_BYTES = 1024;
|
|
472
|
+
var MAX_DECOMPRESSED_STORE_VALUE_BYTES = 8388608;
|
|
473
|
+
var MAX_DECOMPRESSED_SNAPSHOT_BYTES = 268435456;
|
|
474
|
+
var CRC32_TABLE = (() => {
|
|
475
|
+
const table = /* @__PURE__ */ new Uint32Array(2048);
|
|
476
|
+
for (let index = 0; index < 256; index += 1) {
|
|
477
|
+
let crc = index;
|
|
478
|
+
for (let bit = 0; bit < 8; bit += 1) if ((crc & 1) !== 0) crc = crc >>> 1 ^ 3988292384;
|
|
479
|
+
else crc >>>= 1;
|
|
480
|
+
table[index] = crc >>> 0;
|
|
481
|
+
}
|
|
482
|
+
for (let index = 0; index < 256; index += 1) {
|
|
483
|
+
let crc = table[index];
|
|
484
|
+
for (let slice = 1; slice < 8; slice += 1) {
|
|
485
|
+
crc = crc >>> 8 ^ table[crc & 255];
|
|
486
|
+
table[slice * 256 + index] = crc >>> 0;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
return table;
|
|
490
|
+
})();
|
|
491
|
+
function namedError(name, message) {
|
|
492
|
+
const error = new Error(message);
|
|
493
|
+
error.name = name;
|
|
494
|
+
return error;
|
|
495
|
+
}
|
|
496
|
+
function compressionTag(kind) {
|
|
497
|
+
switch (kind) {
|
|
498
|
+
case false: return COMPRESSION_TAG_NONE;
|
|
499
|
+
case "gzip": return COMPRESSION_TAG_GZIP;
|
|
500
|
+
case "deflate": return COMPRESSION_TAG_DEFLATE;
|
|
501
|
+
case "snappy": return COMPRESSION_TAG_SNAPPY;
|
|
502
|
+
default: throw namedError("InternalError", `unsupported compression kind: ${String(kind)}`);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
function compressionKindFromTag(tag) {
|
|
506
|
+
switch (tag) {
|
|
507
|
+
case COMPRESSION_TAG_NONE: return false;
|
|
508
|
+
case COMPRESSION_TAG_GZIP: return "gzip";
|
|
509
|
+
case COMPRESSION_TAG_DEFLATE: return "deflate";
|
|
510
|
+
case COMPRESSION_TAG_SNAPPY: return "snappy";
|
|
511
|
+
default: throw namedError("CorruptionError", `invalid compression tag: ${tag}`);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
function compressionRuntimeUnavailable() {
|
|
515
|
+
return namedError("UnsupportedPlatformError", "CompressionStream and DecompressionStream are required for moyodb compression");
|
|
516
|
+
}
|
|
517
|
+
function ensureCompressionRuntime() {
|
|
518
|
+
if (typeof CompressionStream !== "function" || typeof DecompressionStream !== "function") throw compressionRuntimeUnavailable();
|
|
519
|
+
}
|
|
520
|
+
function crc32(bytes) {
|
|
521
|
+
let crc = 4294967295;
|
|
522
|
+
let offset = 0;
|
|
523
|
+
const blockEnd = bytes.byteLength - 7;
|
|
524
|
+
for (; offset < blockEnd; offset += 8) {
|
|
525
|
+
const first = crc ^ bytes[offset] ^ bytes[offset + 1] << 8 ^ bytes[offset + 2] << 16 ^ bytes[offset + 3] << 24;
|
|
526
|
+
crc = CRC32_TABLE[1792 + (first & 255)] ^ CRC32_TABLE[1536 + (first >>> 8 & 255)] ^ CRC32_TABLE[1280 + (first >>> 16 & 255)] ^ CRC32_TABLE[1024 + (first >>> 24)] ^ CRC32_TABLE[768 + bytes[offset + 4]] ^ CRC32_TABLE[512 + bytes[offset + 5]] ^ CRC32_TABLE[256 + bytes[offset + 6]] ^ CRC32_TABLE[bytes[offset + 7]];
|
|
527
|
+
}
|
|
528
|
+
for (; offset < bytes.byteLength; offset += 1) crc = crc >>> 8 ^ CRC32_TABLE[(crc ^ bytes[offset]) & 255];
|
|
529
|
+
return (crc ^ 4294967295) >>> 0;
|
|
530
|
+
}
|
|
531
|
+
function buildEnvelope(magic, kindTag, rawLength, payload) {
|
|
532
|
+
const out = new Uint8Array(ENVELOPE_HEADER_SIZE + payload.byteLength);
|
|
533
|
+
writeEnvelope(out, 0, magic, kindTag, rawLength, payload);
|
|
534
|
+
return out;
|
|
535
|
+
}
|
|
536
|
+
function writeEnvelope(out, offset, magic, kindTag, rawLength, payload) {
|
|
537
|
+
const checksum = crc32(payload);
|
|
538
|
+
out.set(payload, offset + ENVELOPE_HEADER_SIZE);
|
|
539
|
+
out.set(magic, offset);
|
|
540
|
+
out[offset + 8] = ENVELOPE_VERSION;
|
|
541
|
+
out[offset + 9] = kindTag;
|
|
542
|
+
const view = new DataView(out.buffer, out.byteOffset + offset, ENVELOPE_HEADER_SIZE);
|
|
543
|
+
view.setUint32(10, rawLength, true);
|
|
544
|
+
view.setUint32(14, checksum, true);
|
|
545
|
+
}
|
|
546
|
+
function compressionIsProfitable(rawLength, compressedLength) {
|
|
547
|
+
return (compressedLength + ENVELOPE_HEADER_SIZE) * 100 <= rawLength * 90;
|
|
548
|
+
}
|
|
549
|
+
function snappyPreflightIsProfitable(value) {
|
|
550
|
+
if (value.byteLength < 65536) return true;
|
|
551
|
+
let compressedLength = 0;
|
|
552
|
+
for (let index = 0; index < 3; index += 1) {
|
|
553
|
+
const offset = Math.floor((value.byteLength - STORE_VALUE_COMPRESSION_PREFLIGHT_WINDOW_BYTES) * index / 2);
|
|
554
|
+
compressedLength += encodeSnappy(value.subarray(offset, offset + STORE_VALUE_COMPRESSION_PREFLIGHT_WINDOW_BYTES)).byteLength;
|
|
555
|
+
}
|
|
556
|
+
return compressionIsProfitable(STORE_VALUE_COMPRESSION_PREFLIGHT_WINDOW_BYTES * 3, compressedLength);
|
|
557
|
+
}
|
|
558
|
+
function buildStoreRecord(input, compressed, compression) {
|
|
559
|
+
if (!compressionIsProfitable(input.byteLength, compressed.byteLength)) return buildEnvelope(STORE_RECORD_MAGIC, COMPRESSION_TAG_NONE, input.byteLength, input);
|
|
560
|
+
return buildEnvelope(STORE_RECORD_MAGIC, compressionTag(compression), input.byteLength, compressed);
|
|
561
|
+
}
|
|
562
|
+
function validateStoreValueLength(value) {
|
|
563
|
+
if (value.byteLength > MAX_DECOMPRESSED_STORE_VALUE_BYTES) throw namedError("ValueTooLargeError", `value has ${value.byteLength} bytes, exceeding the ${MAX_DECOMPRESSED_STORE_VALUE_BYTES} byte limit`);
|
|
564
|
+
}
|
|
565
|
+
function prepareFastStoreValueRecord(value, compression) {
|
|
566
|
+
if (compression === false) return {
|
|
567
|
+
payload: value,
|
|
568
|
+
rawLength: value.byteLength,
|
|
569
|
+
byteLength: value.byteLength,
|
|
570
|
+
kindTag: null
|
|
571
|
+
};
|
|
572
|
+
validateStoreValueLength(value);
|
|
573
|
+
let payload = value;
|
|
574
|
+
let kindTag = COMPRESSION_TAG_NONE;
|
|
575
|
+
if (value.byteLength >= 1024 && snappyPreflightIsProfitable(value)) {
|
|
576
|
+
const compressed = encodeSnappy(value);
|
|
577
|
+
if (compressionIsProfitable(value.byteLength, compressed.byteLength)) {
|
|
578
|
+
payload = compressed;
|
|
579
|
+
kindTag = COMPRESSION_TAG_SNAPPY;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
return {
|
|
583
|
+
payload,
|
|
584
|
+
rawLength: value.byteLength,
|
|
585
|
+
byteLength: ENVELOPE_HEADER_SIZE + payload.byteLength,
|
|
586
|
+
kindTag
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
function writePreparedStoreValueRecord(record, target, offset) {
|
|
590
|
+
if (!Number.isSafeInteger(offset) || offset < 0 || record.byteLength > target.byteLength - offset) throw namedError("InternalError", "prepared store value exceeds its destination");
|
|
591
|
+
if (record.kindTag === null) target.set(record.payload, offset);
|
|
592
|
+
else writeEnvelope(target, offset, STORE_RECORD_MAGIC, record.kindTag, record.rawLength, record.payload);
|
|
593
|
+
return offset + record.byteLength;
|
|
594
|
+
}
|
|
595
|
+
function copyBytes(bytes) {
|
|
596
|
+
const copy = new Uint8Array(bytes.byteLength);
|
|
597
|
+
copy.set(bytes);
|
|
598
|
+
return copy;
|
|
599
|
+
}
|
|
600
|
+
function byteStream(bytes) {
|
|
601
|
+
let offset = 0;
|
|
602
|
+
return new ReadableStream({ pull(controller) {
|
|
603
|
+
if (offset < bytes.byteLength) {
|
|
604
|
+
const end = Math.min(offset + 65536, bytes.byteLength);
|
|
605
|
+
controller.enqueue(bytes.subarray(offset, end));
|
|
606
|
+
offset = end;
|
|
607
|
+
}
|
|
608
|
+
if (offset === bytes.byteLength) controller.close();
|
|
609
|
+
} });
|
|
610
|
+
}
|
|
611
|
+
function readEnvelopeHeader(magic, bytes, strict = true) {
|
|
612
|
+
if (bytes.byteLength < magic.byteLength) return null;
|
|
613
|
+
for (let index = 0; index < magic.byteLength; index += 1) if (bytes[index] !== magic[index]) return null;
|
|
614
|
+
if (bytes.byteLength < ENVELOPE_HEADER_SIZE) {
|
|
615
|
+
if (strict) throw namedError("CorruptionError", `truncated compression envelope: expected at least ${ENVELOPE_HEADER_SIZE} bytes, got ${bytes.byteLength}`);
|
|
616
|
+
return null;
|
|
617
|
+
}
|
|
618
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, ENVELOPE_HEADER_SIZE);
|
|
619
|
+
return {
|
|
620
|
+
version: bytes[8],
|
|
621
|
+
kindTag: bytes[9],
|
|
622
|
+
rawLength: view.getUint32(10, true),
|
|
623
|
+
payloadChecksum: view.getUint32(14, true)
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
async function compressBytes(data, kind) {
|
|
627
|
+
ensureCompressionRuntime();
|
|
628
|
+
let stream;
|
|
629
|
+
try {
|
|
630
|
+
stream = byteStream(data).pipeThrough(new CompressionStream(kind));
|
|
631
|
+
} catch {
|
|
632
|
+
throw compressionRuntimeUnavailable();
|
|
633
|
+
}
|
|
634
|
+
try {
|
|
635
|
+
return new Uint8Array(await new Response(stream).arrayBuffer());
|
|
636
|
+
} catch (error) {
|
|
637
|
+
throw namedError("InternalError", `failed to compress bytes with ${kind}: ${String(error)}`);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
async function decompressBytes(data, kind, label, maxOutputBytes) {
|
|
641
|
+
if (kind === "snappy") return decodeSnappy(data, maxOutputBytes ?? MAX_DECOMPRESSED_STORE_VALUE_BYTES, maxOutputBytes);
|
|
642
|
+
ensureCompressionRuntime();
|
|
643
|
+
let stream;
|
|
644
|
+
try {
|
|
645
|
+
stream = byteStream(copyBytes(data)).pipeThrough(new DecompressionStream(kind));
|
|
646
|
+
} catch {
|
|
647
|
+
throw compressionRuntimeUnavailable();
|
|
648
|
+
}
|
|
649
|
+
try {
|
|
650
|
+
return await readBoundedStream(stream, label, maxOutputBytes);
|
|
651
|
+
} catch (error) {
|
|
652
|
+
throw namedError("CorruptionError", `failed to decompress ${label}: ${String(error)}`);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
async function readBoundedStream(stream, label, maxOutputBytes) {
|
|
656
|
+
const reader = stream.getReader();
|
|
657
|
+
const chunks = [];
|
|
658
|
+
let totalBytes = 0;
|
|
659
|
+
try {
|
|
660
|
+
for (let next = await reader.read(); !next.done; next = await reader.read()) {
|
|
661
|
+
const value = next.value;
|
|
662
|
+
totalBytes += value.byteLength;
|
|
663
|
+
if (maxOutputBytes !== void 0 && totalBytes > maxOutputBytes) {
|
|
664
|
+
await reader.cancel(`decompressed ${label} exceeded ${maxOutputBytes} bytes`);
|
|
665
|
+
throw namedError("CorruptionError", `decompressed ${label} exceeds ${maxOutputBytes} byte limit`);
|
|
666
|
+
}
|
|
667
|
+
chunks.push(value);
|
|
668
|
+
}
|
|
669
|
+
} finally {
|
|
670
|
+
reader.releaseLock();
|
|
671
|
+
}
|
|
672
|
+
if (chunks.length === 1) return chunks[0];
|
|
673
|
+
const out = new Uint8Array(totalBytes);
|
|
674
|
+
let offset = 0;
|
|
675
|
+
for (const chunk of chunks) {
|
|
676
|
+
out.set(chunk, offset);
|
|
677
|
+
offset += chunk.byteLength;
|
|
678
|
+
}
|
|
679
|
+
return out;
|
|
680
|
+
}
|
|
681
|
+
function compressionFromStoreFlags(flags) {
|
|
682
|
+
const encoded = (flags & STORE_FLAG_COMPRESSION_MASK) >>> STORE_FLAG_COMPRESSION_SHIFT;
|
|
683
|
+
switch (encoded) {
|
|
684
|
+
case 0: return false;
|
|
685
|
+
case 1: return "gzip";
|
|
686
|
+
case 2: return "deflate";
|
|
687
|
+
case 3: return "snappy";
|
|
688
|
+
default: throw namedError("CorruptionError", `invalid store compression flags: ${encoded}`);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
async function encodeStoreValueRecord(value, compression) {
|
|
692
|
+
if (compression === false) return value;
|
|
693
|
+
if (compression === "snappy") {
|
|
694
|
+
const record = prepareFastStoreValueRecord(value, compression);
|
|
695
|
+
const output = new Uint8Array(record.byteLength);
|
|
696
|
+
writePreparedStoreValueRecord(record, output, 0);
|
|
697
|
+
return output;
|
|
698
|
+
}
|
|
699
|
+
validateStoreValueLength(value);
|
|
700
|
+
if (value.byteLength < 1024) return buildEnvelope(STORE_RECORD_MAGIC, COMPRESSION_TAG_NONE, value.byteLength, value);
|
|
701
|
+
const input = copyBytes(value);
|
|
702
|
+
return buildStoreRecord(input, await compressBytes(input, compression), compression);
|
|
703
|
+
}
|
|
704
|
+
async function decodeStoreValueRecord(value, options) {
|
|
705
|
+
const header = readEnvelopeHeader(STORE_RECORD_MAGIC, value, options.strict);
|
|
706
|
+
if (!header) return value;
|
|
707
|
+
if (header.version !== ENVELOPE_VERSION) {
|
|
708
|
+
if (options.strict) throw namedError("CorruptionError", `unsupported value record version: ${header.version}`);
|
|
709
|
+
return value;
|
|
710
|
+
}
|
|
711
|
+
let compression;
|
|
712
|
+
try {
|
|
713
|
+
compression = compressionKindFromTag(header.kindTag);
|
|
714
|
+
} catch (error) {
|
|
715
|
+
if (options.strict) throw error;
|
|
716
|
+
return value;
|
|
717
|
+
}
|
|
718
|
+
const payload = value.subarray(ENVELOPE_HEADER_SIZE);
|
|
719
|
+
if (crc32(payload) !== header.payloadChecksum) {
|
|
720
|
+
if (options.strict) throw namedError("CorruptionError", "value record checksum mismatch");
|
|
721
|
+
return value;
|
|
722
|
+
}
|
|
723
|
+
if (compression === false) {
|
|
724
|
+
if (payload.byteLength !== header.rawLength) {
|
|
725
|
+
if (options.strict) throw namedError("CorruptionError", `value record length mismatch: expected ${header.rawLength} bytes, got ${payload.byteLength}`);
|
|
726
|
+
return value;
|
|
727
|
+
}
|
|
728
|
+
return payload.slice();
|
|
729
|
+
}
|
|
730
|
+
if (header.rawLength > MAX_DECOMPRESSED_STORE_VALUE_BYTES) throw namedError("CorruptionError", `compressed value advertises ${header.rawLength} decompressed bytes, exceeding the ${MAX_DECOMPRESSED_STORE_VALUE_BYTES} byte limit`);
|
|
731
|
+
const decompressed = await decompressBytes(payload, compression, `value record (${compression})`, header.rawLength);
|
|
732
|
+
if (decompressed.byteLength !== header.rawLength) throw namedError("CorruptionError", `decompressed value length mismatch: expected ${header.rawLength} bytes, got ${decompressed.byteLength}`);
|
|
733
|
+
return decompressed;
|
|
734
|
+
}
|
|
735
|
+
async function wrapSnapshotWithCompression(snapshot, compression) {
|
|
736
|
+
if (compression === false) return snapshot;
|
|
737
|
+
if (!["gzip", "deflate"].includes(compression)) throw namedError("InternalError", `unsupported snapshot compression kind: ${String(compression)}`);
|
|
738
|
+
const input = copyBytes(snapshot);
|
|
739
|
+
const compressed = await compressBytes(input, compression);
|
|
740
|
+
return buildEnvelope(SNAPSHOT_EXPORT_MAGIC, compressionTag(compression), input.byteLength, compressed);
|
|
741
|
+
}
|
|
742
|
+
async function unwrapSnapshotCompression(snapshot) {
|
|
743
|
+
const header = readEnvelopeHeader(SNAPSHOT_EXPORT_MAGIC, snapshot);
|
|
744
|
+
if (!header) return snapshot;
|
|
745
|
+
if (header.version !== ENVELOPE_VERSION) throw namedError("CorruptionError", `unsupported snapshot compression version: ${header.version}`);
|
|
746
|
+
const compression = compressionKindFromTag(header.kindTag);
|
|
747
|
+
if (compression === false) throw namedError("CorruptionError", "snapshot compression envelope cannot store kind \"none\"");
|
|
748
|
+
if (compression === "snappy") throw namedError("CorruptionError", `invalid snapshot compression tag: ${header.kindTag}`);
|
|
749
|
+
const payload = snapshot.subarray(ENVELOPE_HEADER_SIZE);
|
|
750
|
+
if (crc32(payload) !== header.payloadChecksum) throw namedError("CorruptionError", "snapshot compression checksum mismatch");
|
|
751
|
+
if (header.rawLength > MAX_DECOMPRESSED_SNAPSHOT_BYTES) throw namedError("CorruptionError", `compressed snapshot advertises ${header.rawLength} decompressed bytes, exceeding the ${MAX_DECOMPRESSED_SNAPSHOT_BYTES} byte limit`);
|
|
752
|
+
const decompressed = await decompressBytes(payload, compression, `snapshot export (${compression})`, header.rawLength);
|
|
753
|
+
if (decompressed.byteLength !== header.rawLength) throw namedError("CorruptionError", `decompressed snapshot length mismatch: expected ${header.rawLength} bytes, got ${decompressed.byteLength}`);
|
|
754
|
+
return decompressed;
|
|
755
|
+
}
|
|
756
|
+
//#endregion
|
|
757
|
+
//#region src/worker.ts
|
|
758
|
+
function toWasmU64(value) {
|
|
759
|
+
return BigInt(value);
|
|
760
|
+
}
|
|
761
|
+
function fromWasmU64(value) {
|
|
762
|
+
return typeof value === "bigint" ? Number(value) : value;
|
|
763
|
+
}
|
|
764
|
+
function fromOptionalWasmU64(value) {
|
|
765
|
+
return value === null || value === void 0 ? null : fromWasmU64(value);
|
|
766
|
+
}
|
|
767
|
+
function toWasmPutOptions(options) {
|
|
768
|
+
return options.ttl === void 0 ? options : {
|
|
769
|
+
...options,
|
|
770
|
+
ttl: toWasmU64(options.ttl)
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
function toWasmRange(range) {
|
|
774
|
+
const normalized = {};
|
|
775
|
+
if (range.gt !== void 0) normalized.gt = range.gt;
|
|
776
|
+
if (range.gte !== void 0) normalized.gte = range.gte;
|
|
777
|
+
if (range.lt !== void 0) normalized.lt = range.lt;
|
|
778
|
+
if (range.lte !== void 0) normalized.lte = range.lte;
|
|
779
|
+
if (range.reverse !== void 0) normalized.reverse = range.reverse;
|
|
780
|
+
if (range.limit !== void 0) normalized.limit = range.limit;
|
|
781
|
+
return normalized;
|
|
782
|
+
}
|
|
783
|
+
var EMPTY_RANGE = {};
|
|
784
|
+
var EMPTY_VALUE = /* @__PURE__ */ new Uint8Array(0);
|
|
785
|
+
var MAX_ENGINE_KEY_BYTES = 1024;
|
|
786
|
+
var MAX_PACKED_BYTES = 4294967295;
|
|
787
|
+
var INDEX_PUT_BATCH_ROWS = 64;
|
|
788
|
+
var INDEX_PUT_BATCH_BYTES = 4194304;
|
|
789
|
+
var INDEX_SCAN_PAGE_ROWS = 256;
|
|
790
|
+
var INDEX_SCAN_PAGE_BYTES = 8389662;
|
|
791
|
+
var INDEX_SCAN_RAW_CHUNK_ROWS = 512;
|
|
792
|
+
var RECONCILE_DOCUMENT_CACHE_BYTES = 4194304;
|
|
793
|
+
var RECONCILE_DOCUMENT_CACHE_ROWS = 4096;
|
|
794
|
+
var HEX_BYTE_STRINGS = Array.from({ length: 256 }, (_value, byte) => byte.toString(16).padStart(2, "0"));
|
|
795
|
+
var COMPRESSED_READ_CONCURRENCY = 8;
|
|
796
|
+
function getOrInsert(map, key, create) {
|
|
797
|
+
const existing = map.get(key);
|
|
798
|
+
if (existing !== void 0) return existing;
|
|
799
|
+
const created = create();
|
|
800
|
+
map.set(key, created);
|
|
801
|
+
return created;
|
|
802
|
+
}
|
|
803
|
+
var PERSISTENCE_BRIDGE_INIT = "moyodb:persistence-bridge:init";
|
|
804
|
+
var PERSISTENCE_BRIDGE_REQUEST = "moyodb:persistence-bridge:request";
|
|
805
|
+
var PERSISTENCE_BRIDGE_RESPONSE = "moyodb:persistence-bridge:response";
|
|
806
|
+
function isPersistenceBridgeResponse(value) {
|
|
807
|
+
return isRecord(value) && value.type === PERSISTENCE_BRIDGE_RESPONSE && typeof value.id === "number" && typeof value.granted === "boolean";
|
|
808
|
+
}
|
|
809
|
+
var MainThreadPersistenceBridge = class {
|
|
810
|
+
port = null;
|
|
811
|
+
nextRequestId = 1;
|
|
812
|
+
pending = /* @__PURE__ */ new Map();
|
|
813
|
+
readyResolve = null;
|
|
814
|
+
ready;
|
|
815
|
+
constructor() {
|
|
816
|
+
this.ready = new Promise((resolve) => {
|
|
817
|
+
this.readyResolve = resolve;
|
|
818
|
+
});
|
|
819
|
+
self.addEventListener("message", this.handleInitMessage);
|
|
820
|
+
}
|
|
821
|
+
async persisted() {
|
|
822
|
+
return this.request("persisted");
|
|
823
|
+
}
|
|
824
|
+
async persist() {
|
|
825
|
+
return this.request("persist");
|
|
826
|
+
}
|
|
827
|
+
close() {
|
|
828
|
+
self.removeEventListener("message", this.handleInitMessage);
|
|
829
|
+
if (this.readyResolve) {
|
|
830
|
+
this.readyResolve(null);
|
|
831
|
+
this.readyResolve = null;
|
|
832
|
+
}
|
|
833
|
+
if (this.port) {
|
|
834
|
+
this.port.onmessage = null;
|
|
835
|
+
this.port.close();
|
|
836
|
+
this.port = null;
|
|
837
|
+
}
|
|
838
|
+
for (const pending of this.pending.values()) pending.resolve(false);
|
|
839
|
+
this.pending.clear();
|
|
840
|
+
}
|
|
841
|
+
async request(op) {
|
|
842
|
+
const port = await this.ready;
|
|
843
|
+
if (!port || this.port !== port) return false;
|
|
844
|
+
return new Promise((resolve) => {
|
|
845
|
+
const id = this.nextRequestId;
|
|
846
|
+
this.nextRequestId += 1;
|
|
847
|
+
this.pending.set(id, { resolve });
|
|
848
|
+
const request = {
|
|
849
|
+
type: PERSISTENCE_BRIDGE_REQUEST,
|
|
850
|
+
id,
|
|
851
|
+
op
|
|
852
|
+
};
|
|
853
|
+
try {
|
|
854
|
+
port.postMessage(request);
|
|
855
|
+
} catch {
|
|
856
|
+
this.pending.delete(id);
|
|
857
|
+
resolve(false);
|
|
858
|
+
}
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
handleInitMessage = (event) => {
|
|
862
|
+
if (event.origin !== "" && event.origin !== self.location.origin) return;
|
|
863
|
+
if (event.data?.type !== PERSISTENCE_BRIDGE_INIT) return;
|
|
864
|
+
if (event.ports.length === 0) return;
|
|
865
|
+
const port = event.ports[0];
|
|
866
|
+
if (this.port) {
|
|
867
|
+
port.close();
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
this.port = port;
|
|
871
|
+
this.port.onmessage = this.handleResponse;
|
|
872
|
+
this.port.start();
|
|
873
|
+
self.removeEventListener("message", this.handleInitMessage);
|
|
874
|
+
this.readyResolve?.(port);
|
|
875
|
+
this.readyResolve = null;
|
|
876
|
+
};
|
|
877
|
+
handleResponse = (event) => {
|
|
878
|
+
const data = event.data;
|
|
879
|
+
if (!isPersistenceBridgeResponse(data)) return;
|
|
880
|
+
const pending = this.pending.get(data.id);
|
|
881
|
+
if (!pending) return;
|
|
882
|
+
this.pending.delete(data.id);
|
|
883
|
+
pending.resolve(Boolean(data.granted));
|
|
884
|
+
};
|
|
885
|
+
};
|
|
886
|
+
async function estimateOriginStorage() {
|
|
887
|
+
const storage = navigator.storage;
|
|
888
|
+
if (typeof storage.estimate !== "function") return {
|
|
889
|
+
usage: 0,
|
|
890
|
+
quota: 0
|
|
891
|
+
};
|
|
892
|
+
const estimate = await storage.estimate();
|
|
893
|
+
return {
|
|
894
|
+
usage: estimate.usage ?? 0,
|
|
895
|
+
quota: estimate.quota ?? 0
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
function bytesToHex(bytes) {
|
|
899
|
+
let out = "";
|
|
900
|
+
for (let index = 0; index < bytes.byteLength; index += 1) out += HEX_BYTE_STRINGS[bytes[index]];
|
|
901
|
+
return out;
|
|
902
|
+
}
|
|
903
|
+
function bytesEqual(left, right) {
|
|
904
|
+
if (left.byteLength !== right.byteLength) return false;
|
|
905
|
+
for (let index = 0; index < left.byteLength; index += 1) if (left[index] !== right[index]) return false;
|
|
906
|
+
return true;
|
|
907
|
+
}
|
|
908
|
+
function copyRange(range) {
|
|
909
|
+
const copy = toWasmRange(range);
|
|
910
|
+
for (const bound of [
|
|
911
|
+
"gt",
|
|
912
|
+
"gte",
|
|
913
|
+
"lt",
|
|
914
|
+
"lte"
|
|
915
|
+
]) {
|
|
916
|
+
const value = copy[bound];
|
|
917
|
+
if (value !== void 0) copy[bound] = value.slice();
|
|
918
|
+
}
|
|
919
|
+
return copy;
|
|
920
|
+
}
|
|
921
|
+
function rangesEqual(left, right) {
|
|
922
|
+
for (const bound of [
|
|
923
|
+
"gt",
|
|
924
|
+
"gte",
|
|
925
|
+
"lt",
|
|
926
|
+
"lte"
|
|
927
|
+
]) {
|
|
928
|
+
const a = left[bound];
|
|
929
|
+
const b = right[bound];
|
|
930
|
+
if (a === void 0 ? b !== void 0 : b === void 0 || !bytesEqual(a, b)) return false;
|
|
931
|
+
}
|
|
932
|
+
return left.reverse === right.reverse && left.limit === right.limit;
|
|
933
|
+
}
|
|
934
|
+
function normalizeWasmBytes(value) {
|
|
935
|
+
if (value instanceof Uint8Array) return value.byteOffset === 0 && value.byteLength === value.buffer.byteLength ? value : value.slice();
|
|
936
|
+
return new Uint8Array(value);
|
|
937
|
+
}
|
|
938
|
+
function errorName(error) {
|
|
939
|
+
if (!isRecord(error)) return null;
|
|
940
|
+
const candidate = error.name ?? error.code;
|
|
941
|
+
return typeof candidate === "string" ? candidate : null;
|
|
942
|
+
}
|
|
943
|
+
function isNamedError(error, name) {
|
|
944
|
+
return errorName(error) === name;
|
|
945
|
+
}
|
|
946
|
+
var INDEX_SNAPSHOT_TRAILER_MAGIC = utf8Encode("BDBIDX1");
|
|
947
|
+
var indexSnapshotDecoder = new TextDecoder("utf-8", { fatal: true });
|
|
948
|
+
function wrapSnapshotWithIndexDefinitions(snapshot, defs) {
|
|
949
|
+
if (defs.length === 0) return snapshot;
|
|
950
|
+
const manifest = utf8Encode(JSON.stringify(toPublicIndexDefinitions(defs)));
|
|
951
|
+
const lengthBuffer = /* @__PURE__ */ new ArrayBuffer(4);
|
|
952
|
+
new DataView(lengthBuffer).setUint32(0, manifest.byteLength, true);
|
|
953
|
+
const out = new Uint8Array(snapshot.byteLength + manifest.byteLength + 4 + INDEX_SNAPSHOT_TRAILER_MAGIC.byteLength);
|
|
954
|
+
let offset = 0;
|
|
955
|
+
out.set(snapshot, offset);
|
|
956
|
+
offset += snapshot.byteLength;
|
|
957
|
+
out.set(manifest, offset);
|
|
958
|
+
offset += manifest.byteLength;
|
|
959
|
+
out.set(new Uint8Array(lengthBuffer), offset);
|
|
960
|
+
offset += 4;
|
|
961
|
+
out.set(INDEX_SNAPSHOT_TRAILER_MAGIC, offset);
|
|
962
|
+
return out;
|
|
963
|
+
}
|
|
964
|
+
function unwrapSnapshotWithIndexDefinitions(data) {
|
|
965
|
+
const magicLength = INDEX_SNAPSHOT_TRAILER_MAGIC.byteLength;
|
|
966
|
+
if (data.byteLength < magicLength + 4) return {
|
|
967
|
+
snapshot: data,
|
|
968
|
+
indexes: []
|
|
969
|
+
};
|
|
970
|
+
const magicOffset = data.byteLength - magicLength;
|
|
971
|
+
if (!bytesEqual(data.subarray(magicOffset), INDEX_SNAPSHOT_TRAILER_MAGIC)) return {
|
|
972
|
+
snapshot: data,
|
|
973
|
+
indexes: []
|
|
974
|
+
};
|
|
975
|
+
const lengthOffset = magicOffset - 4;
|
|
976
|
+
if (lengthOffset < 0) return {
|
|
977
|
+
snapshot: data,
|
|
978
|
+
indexes: []
|
|
979
|
+
};
|
|
980
|
+
const manifestOffset = lengthOffset - new DataView(data.buffer, data.byteOffset + lengthOffset, 4).getUint32(0, true);
|
|
981
|
+
if (manifestOffset < 0) throw remoteError("CorruptionError", "invalid snapshot index trailer length");
|
|
982
|
+
let parsed;
|
|
983
|
+
try {
|
|
984
|
+
parsed = JSON.parse(indexSnapshotDecoder.decode(data.subarray(manifestOffset, lengthOffset)));
|
|
985
|
+
} catch (error) {
|
|
986
|
+
throw remoteError("CorruptionError", `invalid snapshot index trailer: ${String(error)}`);
|
|
987
|
+
}
|
|
988
|
+
return {
|
|
989
|
+
snapshot: data.subarray(0, manifestOffset),
|
|
990
|
+
indexes: normalizeIndexDefinitions(parsed)
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
function lifecycleEventChannel(dbName) {
|
|
994
|
+
return new BroadcastChannel(`db:${dbName}:events`);
|
|
995
|
+
}
|
|
996
|
+
function broadcastLifecycleEvent(dbName, type) {
|
|
997
|
+
const channel = lifecycleEventChannel(dbName);
|
|
998
|
+
try {
|
|
999
|
+
channel.postMessage({
|
|
1000
|
+
type,
|
|
1001
|
+
dbName,
|
|
1002
|
+
txid: null
|
|
1003
|
+
});
|
|
1004
|
+
} finally {
|
|
1005
|
+
channel.close();
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
var OwnershipLease = class {
|
|
1009
|
+
releaseResolver = null;
|
|
1010
|
+
requestPromise = null;
|
|
1011
|
+
held = false;
|
|
1012
|
+
async acquire(dbName, ownerWaitMs) {
|
|
1013
|
+
const holdPromise = new Promise((resolve) => {
|
|
1014
|
+
this.releaseResolver = resolve;
|
|
1015
|
+
});
|
|
1016
|
+
const lockName = `db:${dbName}:owner`;
|
|
1017
|
+
if (ownerWaitMs === 0) {
|
|
1018
|
+
let acquiredResolve = null;
|
|
1019
|
+
const acquiredPromise = new Promise((resolve) => {
|
|
1020
|
+
acquiredResolve = resolve;
|
|
1021
|
+
});
|
|
1022
|
+
const request = navigator.locks.request(lockName, {
|
|
1023
|
+
mode: "exclusive",
|
|
1024
|
+
ifAvailable: true
|
|
1025
|
+
}, async (lock) => {
|
|
1026
|
+
if (!lock) {
|
|
1027
|
+
acquiredResolve?.(false);
|
|
1028
|
+
return false;
|
|
1029
|
+
}
|
|
1030
|
+
this.held = true;
|
|
1031
|
+
acquiredResolve?.(true);
|
|
1032
|
+
await holdPromise;
|
|
1033
|
+
return true;
|
|
1034
|
+
});
|
|
1035
|
+
if (!await acquiredPromise) throw remoteError("DatabaseBusyError", `database ${dbName} is already owned by another tab`);
|
|
1036
|
+
this.requestPromise = request;
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
const controller = new AbortController();
|
|
1040
|
+
const timeout = setTimeout(() => controller.abort(), clampTimerDelayMs(ownerWaitMs));
|
|
1041
|
+
let acquiredResolve = null;
|
|
1042
|
+
const acquiredPromise = new Promise((resolve) => {
|
|
1043
|
+
acquiredResolve = resolve;
|
|
1044
|
+
});
|
|
1045
|
+
this.requestPromise = navigator.locks.request(lockName, {
|
|
1046
|
+
mode: "exclusive",
|
|
1047
|
+
signal: controller.signal
|
|
1048
|
+
}, async (_lock) => {
|
|
1049
|
+
clearTimeout(timeout);
|
|
1050
|
+
this.held = true;
|
|
1051
|
+
acquiredResolve?.();
|
|
1052
|
+
await holdPromise;
|
|
1053
|
+
return true;
|
|
1054
|
+
});
|
|
1055
|
+
try {
|
|
1056
|
+
await Promise.race([acquiredPromise, this.requestPromise]);
|
|
1057
|
+
} catch {
|
|
1058
|
+
if (!this.held) throw remoteError("DatabaseBusyError", `timed out waiting for ownership of ${dbName}`);
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
async release() {
|
|
1062
|
+
if (!this.held) return;
|
|
1063
|
+
this.releaseResolver?.();
|
|
1064
|
+
try {
|
|
1065
|
+
await this.requestPromise;
|
|
1066
|
+
} finally {
|
|
1067
|
+
this.releaseResolver = null;
|
|
1068
|
+
this.requestPromise = null;
|
|
1069
|
+
this.held = false;
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
};
|
|
1073
|
+
var DbWorker = class {
|
|
1074
|
+
options;
|
|
1075
|
+
dbName = null;
|
|
1076
|
+
engine = null;
|
|
1077
|
+
events = null;
|
|
1078
|
+
lease = null;
|
|
1079
|
+
wasmReady = null;
|
|
1080
|
+
persistenceBridge;
|
|
1081
|
+
txChanges = /* @__PURE__ */ new Map();
|
|
1082
|
+
txModes = /* @__PURE__ */ new Map();
|
|
1083
|
+
primaryCursors = /* @__PURE__ */ new Map();
|
|
1084
|
+
nextPrimaryCursorId = 1;
|
|
1085
|
+
/**
|
|
1086
|
+
* Shared committed schema at begin. Reconciliation replaces the array;
|
|
1087
|
+
* it never mutates the shared schema.
|
|
1088
|
+
*/
|
|
1089
|
+
txIndexSchemas = /* @__PURE__ */ new Map();
|
|
1090
|
+
indexesBySchema = /* @__PURE__ */ new WeakMap();
|
|
1091
|
+
txEnsuredRawStores = /* @__PURE__ */ new Map();
|
|
1092
|
+
txIndexSchemaChanged = /* @__PURE__ */ new Set();
|
|
1093
|
+
txStoreCompression = /* @__PURE__ */ new Map();
|
|
1094
|
+
committedIndexes = null;
|
|
1095
|
+
committedStoreCompression = null;
|
|
1096
|
+
openOptions = null;
|
|
1097
|
+
maintenanceOperation = null;
|
|
1098
|
+
constructor(options = {}) {
|
|
1099
|
+
this.options = options;
|
|
1100
|
+
this.persistenceBridge = options.persistence ?? new MainThreadPersistenceBridge();
|
|
1101
|
+
}
|
|
1102
|
+
async open(request) {
|
|
1103
|
+
if (this.engine) return;
|
|
1104
|
+
assertSecureContext();
|
|
1105
|
+
assertCapabilities();
|
|
1106
|
+
this.dbName = request.dbName;
|
|
1107
|
+
this.events = new BroadcastChannel(`db:${request.dbName}:events`);
|
|
1108
|
+
this.lease = new OwnershipLease();
|
|
1109
|
+
let engine = null;
|
|
1110
|
+
try {
|
|
1111
|
+
await this.lease.acquire(request.dbName, request.options.ownerWaitMs);
|
|
1112
|
+
engine = new (await (this.loadWasm())).WasmEngine();
|
|
1113
|
+
await engine.open(request.dbName, {
|
|
1114
|
+
create_if_missing: request.options.createIfMissing,
|
|
1115
|
+
cache_pages: request.options.cachePages
|
|
1116
|
+
});
|
|
1117
|
+
if (request.options.changeFeed) applyChangeFeedPolicy(engine, request.options.changeFeed);
|
|
1118
|
+
if (request.options.debugFailpoint) engine.set_failpoint(request.options.debugFailpoint);
|
|
1119
|
+
this.engine = engine;
|
|
1120
|
+
this.openOptions = request.options;
|
|
1121
|
+
this.clearRuntimeCaches();
|
|
1122
|
+
this.maintenanceOperation = null;
|
|
1123
|
+
this.events.postMessage({
|
|
1124
|
+
type: "owner_changed",
|
|
1125
|
+
dbName: request.dbName,
|
|
1126
|
+
txid: null
|
|
1127
|
+
});
|
|
1128
|
+
} catch (err) {
|
|
1129
|
+
await this.cleanupAfterFailedOpen(engine);
|
|
1130
|
+
throw remapError(err);
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
async close() {
|
|
1134
|
+
this.ensureNotInMaintenance();
|
|
1135
|
+
const dbName = this.dbName;
|
|
1136
|
+
this.clearRuntimeCaches();
|
|
1137
|
+
try {
|
|
1138
|
+
this.engine?.close();
|
|
1139
|
+
} catch (err) {
|
|
1140
|
+
throw remapError(err);
|
|
1141
|
+
} finally {
|
|
1142
|
+
this.engine = null;
|
|
1143
|
+
if (dbName && this.events) this.events.postMessage({
|
|
1144
|
+
type: "db_closed",
|
|
1145
|
+
dbName,
|
|
1146
|
+
txid: null
|
|
1147
|
+
});
|
|
1148
|
+
this.events?.close();
|
|
1149
|
+
this.events = null;
|
|
1150
|
+
if (dbName && this.lease) await this.lease.release();
|
|
1151
|
+
if (dbName) broadcastLifecycleEvent(dbName, "owner_changed");
|
|
1152
|
+
this.disposePersistenceBridge();
|
|
1153
|
+
this.lease = null;
|
|
1154
|
+
this.dbName = null;
|
|
1155
|
+
this.openOptions = null;
|
|
1156
|
+
this.maintenanceOperation = null;
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
async destroy() {
|
|
1160
|
+
this.ensureNotInMaintenance();
|
|
1161
|
+
const dbName = this.dbName;
|
|
1162
|
+
if (!dbName) return;
|
|
1163
|
+
let failure = null;
|
|
1164
|
+
try {
|
|
1165
|
+
this.rollbackAllTransactions();
|
|
1166
|
+
this.engine?.abandon();
|
|
1167
|
+
await (await this.loadWasm()).deleteDB(dbName);
|
|
1168
|
+
broadcastLifecycleEvent(dbName, "db_deleted");
|
|
1169
|
+
} catch (err) {
|
|
1170
|
+
failure = err;
|
|
1171
|
+
} finally {
|
|
1172
|
+
this.engine = null;
|
|
1173
|
+
this.clearRuntimeCaches();
|
|
1174
|
+
this.events?.close();
|
|
1175
|
+
this.events = null;
|
|
1176
|
+
if (this.lease) try {
|
|
1177
|
+
await this.lease.release();
|
|
1178
|
+
} catch (err) {
|
|
1179
|
+
if (failure === null) failure = err;
|
|
1180
|
+
}
|
|
1181
|
+
broadcastLifecycleEvent(dbName, "owner_changed");
|
|
1182
|
+
this.disposePersistenceBridge();
|
|
1183
|
+
this.lease = null;
|
|
1184
|
+
this.dbName = null;
|
|
1185
|
+
this.openOptions = null;
|
|
1186
|
+
this.maintenanceOperation = null;
|
|
1187
|
+
}
|
|
1188
|
+
if (failure !== null) throw remapError(failure);
|
|
1189
|
+
}
|
|
1190
|
+
async deleteDB(dbName) {
|
|
1191
|
+
assertSecureContext();
|
|
1192
|
+
assertCapabilities();
|
|
1193
|
+
const lease = new OwnershipLease();
|
|
1194
|
+
let failure = null;
|
|
1195
|
+
let acquired = false;
|
|
1196
|
+
try {
|
|
1197
|
+
await lease.acquire(dbName, 0);
|
|
1198
|
+
acquired = true;
|
|
1199
|
+
await (await this.loadWasm()).deleteDB(dbName);
|
|
1200
|
+
broadcastLifecycleEvent(dbName, "db_deleted");
|
|
1201
|
+
} catch (err) {
|
|
1202
|
+
failure = err;
|
|
1203
|
+
} finally {
|
|
1204
|
+
try {
|
|
1205
|
+
await lease.release();
|
|
1206
|
+
} catch (err) {
|
|
1207
|
+
if (failure === null) failure = err;
|
|
1208
|
+
}
|
|
1209
|
+
if (acquired) broadcastLifecycleEvent(dbName, "owner_changed");
|
|
1210
|
+
}
|
|
1211
|
+
if (failure !== null) throw remapError(failure);
|
|
1212
|
+
}
|
|
1213
|
+
begin(mode) {
|
|
1214
|
+
this.recoverEngineIfNeeded();
|
|
1215
|
+
const indexSchema = this.loadCommittedIndexSchema();
|
|
1216
|
+
const storeCompression = this.loadCommittedStoreCompression();
|
|
1217
|
+
const txId = fromWasmU64(this.withEngine((engine) => engine.begin_tx(mode)));
|
|
1218
|
+
this.txChanges.set(txId, /* @__PURE__ */ new Map());
|
|
1219
|
+
this.txModes.set(txId, mode);
|
|
1220
|
+
this.txIndexSchemas.set(txId, indexSchema);
|
|
1221
|
+
this.txStoreCompression.set(txId, {
|
|
1222
|
+
owned: false,
|
|
1223
|
+
values: storeCompression
|
|
1224
|
+
});
|
|
1225
|
+
return Promise.resolve(txId);
|
|
1226
|
+
}
|
|
1227
|
+
commit(txId) {
|
|
1228
|
+
const tracked = this.txChanges.get(txId) ?? /* @__PURE__ */ new Map();
|
|
1229
|
+
const indexOverride = this.txIndexSchemaChanged.has(txId) ? this.txIndexSchemas.get(txId) : void 0;
|
|
1230
|
+
const storeCompressionSnapshot = this.txStoreCompression.get(txId);
|
|
1231
|
+
const txMode = this.txModes.get(txId);
|
|
1232
|
+
let txid;
|
|
1233
|
+
try {
|
|
1234
|
+
txid = fromWasmU64(this.withEngine((engine) => engine.commit_tx(toWasmU64(txId))));
|
|
1235
|
+
} catch (err) {
|
|
1236
|
+
try {
|
|
1237
|
+
if (this.engine && !this.engine.needs_recovery()) this.withEngine((engine) => engine.rollback_tx(toWasmU64(txId)));
|
|
1238
|
+
} catch {}
|
|
1239
|
+
this.cleanupTxState(txId);
|
|
1240
|
+
const recovery = this.recoverEngineIfNeeded();
|
|
1241
|
+
if (!recovery?.pendingCommitted || recovery.pendingTxid === null || isNamedError(err, "RecoveryRequiredError") || isNamedError(err, "InjectedFailureError")) throw err;
|
|
1242
|
+
txid = recovery.pendingTxid;
|
|
1243
|
+
}
|
|
1244
|
+
if (indexOverride) this.committedIndexes = cloneNormalizedIndexDefinitions(indexOverride);
|
|
1245
|
+
if (txMode === "readwrite" && storeCompressionSnapshot) this.committedStoreCompression = storeCompressionSnapshot.values;
|
|
1246
|
+
this.cleanupTxState(txId);
|
|
1247
|
+
if (this.dbName && this.events) {
|
|
1248
|
+
const event = {
|
|
1249
|
+
type: "commit_applied",
|
|
1250
|
+
dbName: this.dbName,
|
|
1251
|
+
txid,
|
|
1252
|
+
stores: this.finalizeTrackedChanges(tracked)
|
|
1253
|
+
};
|
|
1254
|
+
this.events.postMessage(event);
|
|
1255
|
+
}
|
|
1256
|
+
return Promise.resolve(txid);
|
|
1257
|
+
}
|
|
1258
|
+
rollback(txId) {
|
|
1259
|
+
try {
|
|
1260
|
+
this.withEngine((engine) => engine.rollback_tx(toWasmU64(txId)));
|
|
1261
|
+
} finally {
|
|
1262
|
+
this.cleanupTxState(txId);
|
|
1263
|
+
}
|
|
1264
|
+
return Promise.resolve();
|
|
1265
|
+
}
|
|
1266
|
+
async autocommit(mode, command, args) {
|
|
1267
|
+
const txId = await this.begin(mode);
|
|
1268
|
+
const method = this[command];
|
|
1269
|
+
let result;
|
|
1270
|
+
try {
|
|
1271
|
+
result = await method.apply(this, [txId, ...args]);
|
|
1272
|
+
} catch (error) {
|
|
1273
|
+
try {
|
|
1274
|
+
await this.rollback(txId);
|
|
1275
|
+
} catch {}
|
|
1276
|
+
throw error;
|
|
1277
|
+
}
|
|
1278
|
+
if (mode === "readwrite") await this.commit(txId);
|
|
1279
|
+
else await this.rollback(txId);
|
|
1280
|
+
return result;
|
|
1281
|
+
}
|
|
1282
|
+
createStore(txId, name, options = {}) {
|
|
1283
|
+
const compression = isInternalStoreName(name) ? false : options.compression ?? "snappy";
|
|
1284
|
+
this.txEnsuredRawStores.get(txId)?.delete(name);
|
|
1285
|
+
this.withEngine((engine) => engine.create_store(toWasmU64(txId), name, { compression }));
|
|
1286
|
+
this.recordStoreCompressionCreate(txId, name, compression);
|
|
1287
|
+
this.ensureInternalStoresForStore(txId, name);
|
|
1288
|
+
this.markStoreTouched(txId, name);
|
|
1289
|
+
return Promise.resolve();
|
|
1290
|
+
}
|
|
1291
|
+
dropStore(txId, name) {
|
|
1292
|
+
const defs = this.indexesForStoreInTx(txId, name);
|
|
1293
|
+
this.txEnsuredRawStores.get(txId)?.delete(name);
|
|
1294
|
+
this.withEngine((engine) => engine.drop_store(toWasmU64(txId), name));
|
|
1295
|
+
this.recordStoreCompressionDrop(txId, name);
|
|
1296
|
+
for (const def of defs) this.dropRawStoreIfExists(txId, def.internalStore);
|
|
1297
|
+
this.recordStoreLevelChange(txId, name, "drop");
|
|
1298
|
+
return Promise.resolve();
|
|
1299
|
+
}
|
|
1300
|
+
clearStore(txId, name) {
|
|
1301
|
+
const defs = this.indexesForStoreInTx(txId, name);
|
|
1302
|
+
this.txEnsuredRawStores.get(txId)?.delete(name);
|
|
1303
|
+
this.withEngine((engine) => engine.clear_store(toWasmU64(txId), name));
|
|
1304
|
+
for (const def of defs) this.clearRawStoreIfExists(txId, def.internalStore);
|
|
1305
|
+
this.recordStoreLevelChange(txId, name, "clear");
|
|
1306
|
+
return Promise.resolve();
|
|
1307
|
+
}
|
|
1308
|
+
async get(txId, store, key) {
|
|
1309
|
+
return this.readStoreValue(txId, store, key);
|
|
1310
|
+
}
|
|
1311
|
+
async getMany(txId, store, keys) {
|
|
1312
|
+
const values = this.withEngine((engine) => engine.get_many(toWasmU64(txId), store, keys));
|
|
1313
|
+
const compressed = !isInternalStoreName(store) && this.storeCompressionForTx(txId, store) !== false;
|
|
1314
|
+
for (let index = 0; index < values.length; index += 1) {
|
|
1315
|
+
const value = values[index];
|
|
1316
|
+
if (value !== null) values[index] = normalizeWasmBytes(value);
|
|
1317
|
+
}
|
|
1318
|
+
return compressed ? this.decodeStoreValues(txId, store, values) : values;
|
|
1319
|
+
}
|
|
1320
|
+
async getManyPacked(txId, store, packedKeys) {
|
|
1321
|
+
const packed = normalizeWasmBytes(this.withEngine((engine) => engine.get_many_packed(toWasmU64(txId), store, packedKeys)));
|
|
1322
|
+
if (isInternalStoreName(store) || this.storeCompressionForTx(txId, store) === false) return packedOptionalValues(packed);
|
|
1323
|
+
return this.decodeStoreValues(txId, store, unpackPackedOptionalValues(packed));
|
|
1324
|
+
}
|
|
1325
|
+
has(txId, store, key) {
|
|
1326
|
+
return Promise.resolve(this.withEngine((engine) => engine.has(toWasmU64(txId), store, key)));
|
|
1327
|
+
}
|
|
1328
|
+
async put(txId, store, key, value, options = {}) {
|
|
1329
|
+
const defs = this.indexesForStoreInTx(txId, store);
|
|
1330
|
+
if (defs.length === 0) {
|
|
1331
|
+
const storedValue = await this.encodeStoreValueForWrite(txId, store, value);
|
|
1332
|
+
const baselineExists = this.withEngine((engine) => engine.put(toWasmU64(txId), store, key, storedValue, toWasmPutOptions(options)));
|
|
1333
|
+
this.recordPutChange(txId, store, key, baselineExists);
|
|
1334
|
+
return;
|
|
1335
|
+
}
|
|
1336
|
+
await this.putIndexedMany(txId, store, [key, value], defs, options);
|
|
1337
|
+
}
|
|
1338
|
+
async putMany(txId, store, entries, options = {}) {
|
|
1339
|
+
if (entries.length === 0) return;
|
|
1340
|
+
const defs = this.indexesForStoreInTx(txId, store);
|
|
1341
|
+
if (defs.length === 0) {
|
|
1342
|
+
const storedEntries = await this.encodeEntriesForWrite(txId, store, entries);
|
|
1343
|
+
let baselines;
|
|
1344
|
+
try {
|
|
1345
|
+
baselines = this.withEngine((engine) => engine.put_many(toWasmU64(txId), store, storedEntries, toWasmPutOptions(options)));
|
|
1346
|
+
} catch (error) {
|
|
1347
|
+
const partial = partialBooleanOutcomes(error);
|
|
1348
|
+
this.recordPutChanges(txId, store, entries, partial);
|
|
1349
|
+
throw error;
|
|
1350
|
+
}
|
|
1351
|
+
if (baselines.length !== entries.length) throw remoteError("InternalError", `put_many outcome count mismatch: ${baselines.length} != ${entries.length}`);
|
|
1352
|
+
this.recordPutChanges(txId, store, entries, baselines);
|
|
1353
|
+
return;
|
|
1354
|
+
}
|
|
1355
|
+
const items = new Array(entries.length * 2);
|
|
1356
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
1357
|
+
items[index * 2] = entries[index][0];
|
|
1358
|
+
items[index * 2 + 1] = entries[index][1];
|
|
1359
|
+
}
|
|
1360
|
+
await this.putIndexedMany(txId, store, items, defs, options);
|
|
1361
|
+
}
|
|
1362
|
+
async putManyPacked(txId, store, packedEntries, options = {}) {
|
|
1363
|
+
const defs = this.indexesForStoreInTx(txId, store);
|
|
1364
|
+
if (defs.length !== 0) {
|
|
1365
|
+
const items = unpackPackedBinaryList(packedEntries);
|
|
1366
|
+
if (items.length % 2 !== 0) throw remoteError("WorkerProtocolError", "packed putMany payload has an odd item count");
|
|
1367
|
+
await this.putIndexedMany(txId, store, items, defs, options);
|
|
1368
|
+
return;
|
|
1369
|
+
}
|
|
1370
|
+
const items = this.storeCompressionForTx(txId, store) === false ? null : unpackPackedBinaryList(packedEntries);
|
|
1371
|
+
if (items !== null && items.length % 2 !== 0) throw remoteError("WorkerProtocolError", "packed putMany payload has an odd item count");
|
|
1372
|
+
const keys = items === null ? unpackPackedBinaryPairKeys(packedEntries) : items.filter((_item, index) => index % 2 === 0);
|
|
1373
|
+
if (keys.length === 0) return;
|
|
1374
|
+
const storedEntries = items === null ? packedEntries : await this.encodePackedEntriesForWrite(txId, store, items, keys);
|
|
1375
|
+
let baselines;
|
|
1376
|
+
try {
|
|
1377
|
+
baselines = this.withEngine((engine) => engine.put_many_packed(toWasmU64(txId), store, storedEntries, toWasmPutOptions(options)));
|
|
1378
|
+
} catch (error) {
|
|
1379
|
+
const partial = partialBooleanOutcomes(error);
|
|
1380
|
+
this.recordPutKeyChanges(txId, store, keys, partial);
|
|
1381
|
+
throw error;
|
|
1382
|
+
}
|
|
1383
|
+
if (baselines.length !== keys.length) throw remoteError("InternalError", `put_many_packed outcome count mismatch: ${baselines.length} != ${keys.length}`);
|
|
1384
|
+
this.recordPutKeyChanges(txId, store, keys, baselines);
|
|
1385
|
+
}
|
|
1386
|
+
async delete(txId, store, key) {
|
|
1387
|
+
const defs = this.indexesForStoreInTx(txId, store);
|
|
1388
|
+
const oldValue = defs.length === 0 ? null : await this.readStoreValue(txId, store, key);
|
|
1389
|
+
const oldKeyFor = oldValue === null ? null : createIndexKeyExtractor(oldValue);
|
|
1390
|
+
const oldLogicalKeys = oldKeyFor === null ? [] : defs.map((def) => ({
|
|
1391
|
+
def,
|
|
1392
|
+
logicalKey: oldKeyFor(def)
|
|
1393
|
+
}));
|
|
1394
|
+
if (!this.withEngine((engine) => engine.delete(toWasmU64(txId), store, key))) {
|
|
1395
|
+
this.recordDeleteChange(txId, store, key, false);
|
|
1396
|
+
return false;
|
|
1397
|
+
}
|
|
1398
|
+
for (const entry of oldLogicalKeys) if (entry.logicalKey !== null) this.deleteIndexEntryIfPresent(txId, entry.def, entry.logicalKey, key);
|
|
1399
|
+
this.recordDeleteChange(txId, store, key, true);
|
|
1400
|
+
return true;
|
|
1401
|
+
}
|
|
1402
|
+
async deleteMany(txId, store, keys) {
|
|
1403
|
+
if (keys.length === 0) return;
|
|
1404
|
+
if (this.indexesForStoreInTx(txId, store).length === 0) {
|
|
1405
|
+
let deleted;
|
|
1406
|
+
try {
|
|
1407
|
+
deleted = this.withEngine((engine) => engine.delete_many(toWasmU64(txId), store, keys));
|
|
1408
|
+
} catch (error) {
|
|
1409
|
+
const partial = partialBooleanOutcomes(error);
|
|
1410
|
+
for (let i = 0; i < partial.length && i < keys.length; i += 1) this.recordDeleteChange(txId, store, keys[i], partial[i]);
|
|
1411
|
+
throw error;
|
|
1412
|
+
}
|
|
1413
|
+
if (deleted.length !== keys.length) throw remoteError("InternalError", `delete_many outcome count mismatch: ${deleted.length} != ${keys.length}`);
|
|
1414
|
+
for (let i = 0; i < keys.length; i += 1) this.recordDeleteChange(txId, store, keys[i], deleted[i]);
|
|
1415
|
+
return;
|
|
1416
|
+
}
|
|
1417
|
+
for (const key of keys) await this.delete(txId, store, key);
|
|
1418
|
+
}
|
|
1419
|
+
async deleteManyPacked(txId, store, packedKeys) {
|
|
1420
|
+
const defs = this.indexesForStoreInTx(txId, store);
|
|
1421
|
+
const keys = unpackPackedBinaryList(packedKeys);
|
|
1422
|
+
if (keys.length === 0) return;
|
|
1423
|
+
if (defs.length !== 0) {
|
|
1424
|
+
await this.deleteMany(txId, store, keys);
|
|
1425
|
+
return;
|
|
1426
|
+
}
|
|
1427
|
+
let deleted;
|
|
1428
|
+
try {
|
|
1429
|
+
deleted = this.withEngine((engine) => engine.delete_many_packed(toWasmU64(txId), store, packedKeys));
|
|
1430
|
+
} catch (error) {
|
|
1431
|
+
const partial = partialBooleanOutcomes(error);
|
|
1432
|
+
for (let i = 0; i < partial.length && i < keys.length; i += 1) this.recordDeleteChange(txId, store, keys[i], partial[i]);
|
|
1433
|
+
throw error;
|
|
1434
|
+
}
|
|
1435
|
+
if (deleted.length !== keys.length) throw remoteError("InternalError", `delete_many_packed outcome count mismatch: ${deleted.length} != ${keys.length}`);
|
|
1436
|
+
for (let i = 0; i < keys.length; i += 1) this.recordDeleteChange(txId, store, keys[i], deleted[i] === 1);
|
|
1437
|
+
}
|
|
1438
|
+
async applyBatch(txId, store, ops) {
|
|
1439
|
+
if (ops.length === 0) return;
|
|
1440
|
+
if (this.indexesForStoreInTx(txId, store).length === 0) {
|
|
1441
|
+
const storedOps = await this.encodeBatchOpsForWrite(txId, store, ops);
|
|
1442
|
+
let outcomes;
|
|
1443
|
+
try {
|
|
1444
|
+
outcomes = this.withEngine((engine) => engine.apply_batch(toWasmU64(txId), store, storedOps));
|
|
1445
|
+
} catch (error) {
|
|
1446
|
+
this.recordBatchOutcomes(txId, store, ops, partialBatchOutcomes(error));
|
|
1447
|
+
throw error;
|
|
1448
|
+
}
|
|
1449
|
+
if (outcomes.length !== ops.length) throw remoteError("InternalError", `apply_batch outcome count mismatch: ${outcomes.length} != ${ops.length}`);
|
|
1450
|
+
this.recordBatchOutcomes(txId, store, ops, outcomes);
|
|
1451
|
+
return;
|
|
1452
|
+
}
|
|
1453
|
+
for (const op of ops) if (op.kind === "put") await this.put(txId, store, op.key, op.value);
|
|
1454
|
+
else await this.delete(txId, store, op.key);
|
|
1455
|
+
}
|
|
1456
|
+
async applyBatchPacked(txId, store, packedOps) {
|
|
1457
|
+
if (this.indexesForStoreInTx(txId, store).length !== 0 || this.storeCompressionForTx(txId, store) !== false) {
|
|
1458
|
+
const ops = unpackPackedBatchOps(packedOps);
|
|
1459
|
+
if (ops.length === 0) return;
|
|
1460
|
+
await this.applyBatch(txId, store, ops);
|
|
1461
|
+
return;
|
|
1462
|
+
}
|
|
1463
|
+
const ops = unpackPackedBatchOpKeys(packedOps);
|
|
1464
|
+
if (ops.length === 0) return;
|
|
1465
|
+
let flags;
|
|
1466
|
+
try {
|
|
1467
|
+
flags = this.withEngine((engine) => engine.apply_batch_packed(toWasmU64(txId), store, packedOps));
|
|
1468
|
+
} catch (error) {
|
|
1469
|
+
this.recordBatchKeyOutcomes(txId, store, ops, partialBatchOutcomes(error));
|
|
1470
|
+
throw error;
|
|
1471
|
+
}
|
|
1472
|
+
if (flags.length !== ops.length) throw remoteError("InternalError", `apply_batch_packed outcome count mismatch: ${flags.length} != ${ops.length}`);
|
|
1473
|
+
for (let i = 0; i < ops.length; i += 1) {
|
|
1474
|
+
const op = ops[i];
|
|
1475
|
+
if (op.kind === "put") this.recordPutChange(txId, store, op.key, flags[i] === 1);
|
|
1476
|
+
else this.recordDeleteChange(txId, store, op.key, flags[i] === 1);
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
async scan(txId, store, range) {
|
|
1480
|
+
return this.readStoreScan(txId, store, range);
|
|
1481
|
+
}
|
|
1482
|
+
async scanPage(request) {
|
|
1483
|
+
if (!Number.isSafeInteger(request.maxRows) || request.maxRows <= 0 || request.maxRows > MAX_PACKED_BYTES || !Number.isSafeInteger(request.maxBytes) || request.maxBytes < 4 || request.maxBytes > MAX_PACKED_BYTES) throw remoteError("InvalidRangeError", "scan page budgets must be positive u32 integers; maxBytes must be at least 4");
|
|
1484
|
+
let cursorId = request.cursorId;
|
|
1485
|
+
let cursor = cursorId === void 0 ? void 0 : this.primaryCursors.get(cursorId);
|
|
1486
|
+
if (cursorId !== void 0) {
|
|
1487
|
+
if (!cursor) throw remoteError("TransactionClosedError", "scan cursor is closed");
|
|
1488
|
+
if (cursor.store !== request.store || cursor.callerTxId !== request.txId || !rangesEqual(cursor.range, request.range ?? EMPTY_RANGE)) throw remoteError("InvalidRangeError", "scan continuation does not match the cursor");
|
|
1489
|
+
} else {
|
|
1490
|
+
const ownsTx = request.txId === void 0;
|
|
1491
|
+
const txId = request.txId ?? await this.begin("readonly");
|
|
1492
|
+
const range = copyRange(request.range ?? EMPTY_RANGE);
|
|
1493
|
+
let engineId;
|
|
1494
|
+
try {
|
|
1495
|
+
engineId = this.withEngine((engine) => engine.open_scan_cursor(toWasmU64(txId), request.store, toWasmRange(range), false));
|
|
1496
|
+
} catch (error) {
|
|
1497
|
+
if (ownsTx) try {
|
|
1498
|
+
await this.rollback(txId);
|
|
1499
|
+
} catch {}
|
|
1500
|
+
throw error;
|
|
1501
|
+
}
|
|
1502
|
+
cursorId = this.nextPrimaryCursorId++;
|
|
1503
|
+
cursor = {
|
|
1504
|
+
engineId,
|
|
1505
|
+
txId,
|
|
1506
|
+
ownsTx,
|
|
1507
|
+
store: request.store,
|
|
1508
|
+
range,
|
|
1509
|
+
callerTxId: request.txId
|
|
1510
|
+
};
|
|
1511
|
+
this.primaryCursors.set(cursorId, cursor);
|
|
1512
|
+
}
|
|
1513
|
+
try {
|
|
1514
|
+
const page = this.withEngine((engine) => engine.scan_cursor_next(cursor.engineId, request.maxRows, request.maxBytes));
|
|
1515
|
+
const packet = normalizeWasmBytes(page.packet);
|
|
1516
|
+
const rowCount = packet.byteLength < 4 ? -1 : new DataView(packet.buffer, packet.byteOffset, 4).getUint32(0, true);
|
|
1517
|
+
if (rowCount !== page.rowCount || rowCount > request.maxRows || packet.byteLength > request.maxBytes) throw remoteError("CorruptionError", "scan cursor returned an invalid page budget or count");
|
|
1518
|
+
const rows = this.storeCompressionForTx(cursor.txId, cursor.store) !== false ? unpackPackedScanRows(packet) : null;
|
|
1519
|
+
let bytes = packet.byteLength;
|
|
1520
|
+
if (rows !== null) {
|
|
1521
|
+
bytes = 4;
|
|
1522
|
+
for (const row of rows) {
|
|
1523
|
+
row.value = await this.decodeStoreValue(cursor.txId, cursor.store, row.value);
|
|
1524
|
+
bytes += 8 + row.key.byteLength + row.value.byteLength;
|
|
1525
|
+
if (bytes > request.maxBytes) throw remoteError("ValueTooLargeError", "decoded scan page exceeds maxBytes");
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
if (page.exhausted) await this.closeCursor(cursorId);
|
|
1529
|
+
return {
|
|
1530
|
+
rows: rows ?? packedScanRows(packet),
|
|
1531
|
+
done: page.exhausted,
|
|
1532
|
+
bytes,
|
|
1533
|
+
...page.exhausted ? {} : { cursorId }
|
|
1534
|
+
};
|
|
1535
|
+
} catch (error) {
|
|
1536
|
+
try {
|
|
1537
|
+
await this.closeCursor(cursorId);
|
|
1538
|
+
} catch {}
|
|
1539
|
+
throw error;
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
async closeCursor(cursorId) {
|
|
1543
|
+
const cursor = this.primaryCursors.get(cursorId);
|
|
1544
|
+
if (!cursor) return;
|
|
1545
|
+
this.primaryCursors.delete(cursorId);
|
|
1546
|
+
try {
|
|
1547
|
+
this.withEngine((engine) => engine.close_scan_cursor(cursor.engineId));
|
|
1548
|
+
} finally {
|
|
1549
|
+
if (cursor.ownsTx) await this.rollback(cursor.txId);
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
async getByIndex(txId, store, indexName, key) {
|
|
1553
|
+
const def = this.resolveIndexDefinition(txId, store, indexName);
|
|
1554
|
+
const page = await this.loadVisibleIndexPage(txId, def, {
|
|
1555
|
+
gte: key,
|
|
1556
|
+
lte: key
|
|
1557
|
+
}, null, 1);
|
|
1558
|
+
return page.rows.length === 0 ? null : page.rows[0].value;
|
|
1559
|
+
}
|
|
1560
|
+
async scanByIndex(txId, store, indexName, range = {}) {
|
|
1561
|
+
const def = this.resolveIndexDefinition(txId, store, indexName);
|
|
1562
|
+
const rows = [];
|
|
1563
|
+
let remaining = range.limit ?? Number.POSITIVE_INFINITY;
|
|
1564
|
+
let cursor = null;
|
|
1565
|
+
while (remaining > 0) {
|
|
1566
|
+
const page = await this.loadVisibleIndexPage(txId, def, range, cursor, Math.min(remaining, INDEX_SCAN_PAGE_ROWS));
|
|
1567
|
+
rows.push(...page.rows);
|
|
1568
|
+
remaining -= page.rows.length;
|
|
1569
|
+
if (page.cursor === null) break;
|
|
1570
|
+
cursor = page.cursor;
|
|
1571
|
+
}
|
|
1572
|
+
return rows;
|
|
1573
|
+
}
|
|
1574
|
+
async scanByIndexPage(txId, store, indexName, range, cursor, limit, maxBytes = INDEX_SCAN_PAGE_BYTES) {
|
|
1575
|
+
if (!Number.isSafeInteger(limit) || limit <= 0) throw remoteError("InvalidRangeError", "index scan page limit must be a positive integer");
|
|
1576
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes < 4 || maxBytes > MAX_PACKED_BYTES) throw remoteError("InvalidRangeError", "index scan page maxBytes must be a u32 integer of at least 4");
|
|
1577
|
+
const def = this.resolveIndexDefinition(txId, store, indexName);
|
|
1578
|
+
return this.loadVisibleIndexPage(txId, def, range, cursor, limit, maxBytes);
|
|
1579
|
+
}
|
|
1580
|
+
getIndexes(txId) {
|
|
1581
|
+
return Promise.resolve(toPublicIndexDefinitions(txId === void 0 ? this.loadCommittedIndexSchema() : this.indexSchemaForTx(txId)));
|
|
1582
|
+
}
|
|
1583
|
+
async reconcileIndexes(txId, indexes) {
|
|
1584
|
+
const target = normalizeIndexDefinitions(indexes);
|
|
1585
|
+
this.ensureRawStore(txId, INDEX_METADATA_STORE);
|
|
1586
|
+
const current = this.readIndexSchemaInTx(txId);
|
|
1587
|
+
const targetStores = new Set(target.map((def) => def.internalStore));
|
|
1588
|
+
for (const def of current) if (!targetStores.has(def.internalStore)) this.dropRawStoreIfExists(txId, def.internalStore);
|
|
1589
|
+
this.clearRawStoreIfExists(txId, INDEX_METADATA_STORE);
|
|
1590
|
+
let documentStore = null;
|
|
1591
|
+
let cachedBytes = 0;
|
|
1592
|
+
let sourceRows = null;
|
|
1593
|
+
const canRefreshVisibility = typeof this.engine?.has_many === "function";
|
|
1594
|
+
const documents = /* @__PURE__ */ new Map();
|
|
1595
|
+
for (const def of target) {
|
|
1596
|
+
if (documentStore !== def.store) {
|
|
1597
|
+
documents.clear();
|
|
1598
|
+
cachedBytes = 0;
|
|
1599
|
+
sourceRows = null;
|
|
1600
|
+
documentStore = def.store;
|
|
1601
|
+
}
|
|
1602
|
+
this.ensureRawStore(txId, def.internalStore);
|
|
1603
|
+
this.clearRawStoreIfExists(txId, def.internalStore);
|
|
1604
|
+
if (this.rawStoreExistsInTx(txId, def.store)) {
|
|
1605
|
+
let rows;
|
|
1606
|
+
if (sourceRows !== null) {
|
|
1607
|
+
const visible = this.withEngine((engine) => engine.has_many(toWasmU64(txId), def.store, sourceRows.map((row) => row.key)));
|
|
1608
|
+
if (visible.length !== sourceRows.length) throw remoteError("InternalError", `has_many outcome count mismatch: ${visible.length} != ${sourceRows.length}`);
|
|
1609
|
+
rows = sourceRows.filter((_row, index) => visible[index]);
|
|
1610
|
+
} else {
|
|
1611
|
+
rows = await this.readStoreScan(txId, def.store, EMPTY_RANGE);
|
|
1612
|
+
if (canRefreshVisibility && rows.length <= RECONCILE_DOCUMENT_CACHE_ROWS) {
|
|
1613
|
+
let inputBytes = 0;
|
|
1614
|
+
for (const row of rows) {
|
|
1615
|
+
inputBytes += row.key.byteLength + row.value.byteLength;
|
|
1616
|
+
if (inputBytes > RECONCILE_DOCUMENT_CACHE_BYTES) break;
|
|
1617
|
+
}
|
|
1618
|
+
if (inputBytes <= RECONCILE_DOCUMENT_CACHE_BYTES) sourceRows = rows;
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
for (const row of rows) {
|
|
1622
|
+
const token = bytesToHex(row.key);
|
|
1623
|
+
const cached = documents.get(token);
|
|
1624
|
+
let document;
|
|
1625
|
+
if (cached && (cached.bytes === row.value || bytesEqual(cached.bytes, row.value))) {
|
|
1626
|
+
document = cached.document;
|
|
1627
|
+
cached.bytes = row.value;
|
|
1628
|
+
} else {
|
|
1629
|
+
document = decodeIndexedDocument(row.value);
|
|
1630
|
+
const previousBytes = cached?.inputBytes ?? 0;
|
|
1631
|
+
const inputBytes = row.key.byteLength + row.value.byteLength;
|
|
1632
|
+
if ((cached || documents.size < RECONCILE_DOCUMENT_CACHE_ROWS) && cachedBytes - previousBytes + inputBytes <= RECONCILE_DOCUMENT_CACHE_BYTES) {
|
|
1633
|
+
documents.set(token, {
|
|
1634
|
+
bytes: row.value,
|
|
1635
|
+
inputBytes,
|
|
1636
|
+
document
|
|
1637
|
+
});
|
|
1638
|
+
cachedBytes += inputBytes - previousBytes;
|
|
1639
|
+
} else if (cached) {
|
|
1640
|
+
documents.delete(token);
|
|
1641
|
+
cachedBytes -= previousBytes;
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
const logicalKey = extractLogicalIndexKeyFromDocument(def, document);
|
|
1645
|
+
if (logicalKey === null) continue;
|
|
1646
|
+
await this.assertUniqueIndexAvailability(txId, def, logicalKey, row.key);
|
|
1647
|
+
this.putIndexEntry(txId, def, this.buildIndexEntryKey(def, logicalKey, row.key), EMPTY_VALUE);
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
this.putRawValue(txId, INDEX_METADATA_STORE, encodeIndexMetadataKey(def.store, def.name), encodeIndexMetadataValue(def));
|
|
1651
|
+
}
|
|
1652
|
+
this.txIndexSchemas.set(txId, cloneNormalizedIndexDefinitions(target));
|
|
1653
|
+
this.txIndexSchemaChanged.add(txId);
|
|
1654
|
+
}
|
|
1655
|
+
listStores() {
|
|
1656
|
+
return Promise.resolve(this.currentStoreNames());
|
|
1657
|
+
}
|
|
1658
|
+
getVersion() {
|
|
1659
|
+
return Promise.resolve(fromWasmU64(this.withEngine((engine) => engine.get_schema_version())));
|
|
1660
|
+
}
|
|
1661
|
+
async changesSince(txId, options = {}) {
|
|
1662
|
+
const visibleStores = options.stores?.filter((store) => !isInternalStoreName(store));
|
|
1663
|
+
const engineOptions = {
|
|
1664
|
+
...options,
|
|
1665
|
+
stores: visibleStores ? [...visibleStores] : void 0,
|
|
1666
|
+
limit: visibleStores ? options.limit : void 0
|
|
1667
|
+
};
|
|
1668
|
+
const feed = this.withEngine((engine) => engine.changes_since(toWasmU64(txId), engineOptions));
|
|
1669
|
+
const allowedStores = visibleStores ? new Set(visibleStores) : null;
|
|
1670
|
+
const changes = [];
|
|
1671
|
+
for (const change of feed.changes) {
|
|
1672
|
+
if (isInternalStoreName(change.store)) continue;
|
|
1673
|
+
if (allowedStores && !allowedStores.has(change.store)) continue;
|
|
1674
|
+
const key = options.limit === void 0 || changes.length < options.limit ? normalizeWasmBytes(change.key) : null;
|
|
1675
|
+
let value;
|
|
1676
|
+
if (change.value !== void 0) value = await this.decodeStoreValueForFeed(change.store, normalizeWasmBytes(change.value));
|
|
1677
|
+
if (key === null) continue;
|
|
1678
|
+
changes.push({
|
|
1679
|
+
txId: fromWasmU64(change.txId),
|
|
1680
|
+
store: change.store,
|
|
1681
|
+
key,
|
|
1682
|
+
kind: change.kind,
|
|
1683
|
+
value
|
|
1684
|
+
});
|
|
1685
|
+
}
|
|
1686
|
+
return {
|
|
1687
|
+
latestTxId: fromWasmU64(feed.latestTxId),
|
|
1688
|
+
changes
|
|
1689
|
+
};
|
|
1690
|
+
}
|
|
1691
|
+
setSchemaVersion(txId, version) {
|
|
1692
|
+
this.withEngine((engine) => engine.set_schema_version(toWasmU64(txId), toWasmU64(version)));
|
|
1693
|
+
return Promise.resolve();
|
|
1694
|
+
}
|
|
1695
|
+
async exportSnapshot(options = {}) {
|
|
1696
|
+
const defs = cloneNormalizedIndexDefinitions(this.loadCommittedIndexSchema());
|
|
1697
|
+
return await wrapSnapshotWithCompression(wrapSnapshotWithIndexDefinitions(normalizeWasmBytes(this.withEngine((engine) => engine.export_snapshot())), defs), options.compression ?? false);
|
|
1698
|
+
}
|
|
1699
|
+
async importSnapshot(data) {
|
|
1700
|
+
this.rollbackAllTransactions();
|
|
1701
|
+
const parsed = unwrapSnapshotWithIndexDefinitions(await unwrapSnapshotCompression(data));
|
|
1702
|
+
this.withEngine((engine) => engine.import_snapshot(parsed.snapshot));
|
|
1703
|
+
this.committedIndexes = [];
|
|
1704
|
+
this.committedStoreCompression = null;
|
|
1705
|
+
await this.applyCommittedIndexSchema(parsed.indexes);
|
|
1706
|
+
}
|
|
1707
|
+
async reset() {
|
|
1708
|
+
const dbName = this.dbName;
|
|
1709
|
+
if (!dbName) throw remoteError("InternalError", "engine is not open");
|
|
1710
|
+
const defs = cloneNormalizedIndexDefinitions(this.loadCommittedIndexSchema());
|
|
1711
|
+
this.rollbackAllTransactions();
|
|
1712
|
+
const stores = this.currentStoreNames().map((store) => ({
|
|
1713
|
+
store,
|
|
1714
|
+
changes: [{
|
|
1715
|
+
key: /* @__PURE__ */ new Uint8Array(0),
|
|
1716
|
+
kind: "clear"
|
|
1717
|
+
}]
|
|
1718
|
+
}));
|
|
1719
|
+
const txid = fromWasmU64(this.withEngine((engine) => engine.reset()));
|
|
1720
|
+
this.committedIndexes = [];
|
|
1721
|
+
await this.applyCommittedIndexSchema(defs);
|
|
1722
|
+
if (this.events) {
|
|
1723
|
+
const event = {
|
|
1724
|
+
type: "commit_applied",
|
|
1725
|
+
dbName,
|
|
1726
|
+
txid,
|
|
1727
|
+
stores
|
|
1728
|
+
};
|
|
1729
|
+
this.events.postMessage(event);
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
async compact() {
|
|
1733
|
+
return this.runCompactionOperation("compact");
|
|
1734
|
+
}
|
|
1735
|
+
async rebuild() {
|
|
1736
|
+
return this.runCompactionOperation("rebuild");
|
|
1737
|
+
}
|
|
1738
|
+
stats() {
|
|
1739
|
+
const stats = this.withEngine((engine) => engine.stats());
|
|
1740
|
+
return Promise.resolve({
|
|
1741
|
+
...stats,
|
|
1742
|
+
db_id: fromWasmU64(stats.db_id),
|
|
1743
|
+
catalog_root_page_id: fromWasmU64(stats.catalog_root_page_id),
|
|
1744
|
+
next_page_id: fromWasmU64(stats.next_page_id),
|
|
1745
|
+
last_committed_txid: fromWasmU64(stats.last_committed_txid),
|
|
1746
|
+
last_replayed_wal_offset: fromWasmU64(stats.last_replayed_wal_offset),
|
|
1747
|
+
manifest_len: fromWasmU64(stats.manifest_len),
|
|
1748
|
+
main_len: fromWasmU64(stats.main_len),
|
|
1749
|
+
wal_len: fromWasmU64(stats.wal_len),
|
|
1750
|
+
health: normalizeEngineHealth(stats.health),
|
|
1751
|
+
store_count: this.currentStoreNames().length
|
|
1752
|
+
});
|
|
1753
|
+
}
|
|
1754
|
+
async storageInfo() {
|
|
1755
|
+
const [dbSize, estimate, persisted] = await Promise.all([
|
|
1756
|
+
this.dbDirectorySize(),
|
|
1757
|
+
estimateOriginStorage(),
|
|
1758
|
+
this.persistenceBridge.persisted()
|
|
1759
|
+
]);
|
|
1760
|
+
return {
|
|
1761
|
+
dbSize,
|
|
1762
|
+
originUsage: estimate.usage,
|
|
1763
|
+
originQuota: estimate.quota,
|
|
1764
|
+
persisted
|
|
1765
|
+
};
|
|
1766
|
+
}
|
|
1767
|
+
async requestPersistence() {
|
|
1768
|
+
return this.persistenceBridge.persist();
|
|
1769
|
+
}
|
|
1770
|
+
setFailpoint(failpoint) {
|
|
1771
|
+
this.withEngine((engine) => engine.set_failpoint(failpoint));
|
|
1772
|
+
return Promise.resolve();
|
|
1773
|
+
}
|
|
1774
|
+
requireDbName() {
|
|
1775
|
+
if (!this.dbName) throw remoteError("InternalError", "engine is not open");
|
|
1776
|
+
return this.dbName;
|
|
1777
|
+
}
|
|
1778
|
+
ensureNotInMaintenance() {
|
|
1779
|
+
if (!this.maintenanceOperation) return;
|
|
1780
|
+
throw remoteError("DatabaseBusyError", `database ${this.dbName ?? "database"} is busy: ${this.maintenanceOperation} in progress`);
|
|
1781
|
+
}
|
|
1782
|
+
currentCachePages() {
|
|
1783
|
+
return this.openOptions?.cachePages ?? 256;
|
|
1784
|
+
}
|
|
1785
|
+
async dbDirectorySize() {
|
|
1786
|
+
return (await this.loadWasm()).dbDirectorySize(this.requireDbName());
|
|
1787
|
+
}
|
|
1788
|
+
async applyCommittedIndexSchema(defs) {
|
|
1789
|
+
const normalized = cloneNormalizedIndexDefinitions(defs);
|
|
1790
|
+
if (normalized.length === 0) {
|
|
1791
|
+
this.committedIndexes = [];
|
|
1792
|
+
return;
|
|
1793
|
+
}
|
|
1794
|
+
const txId = fromWasmU64(this.withEngine((engine) => engine.begin_tx("readwrite"), { allowDuringMaintenance: true }));
|
|
1795
|
+
this.txChanges.set(txId, /* @__PURE__ */ new Map());
|
|
1796
|
+
this.txModes.set(txId, "readwrite");
|
|
1797
|
+
this.txStoreCompression.set(txId, {
|
|
1798
|
+
owned: false,
|
|
1799
|
+
values: this.loadCommittedStoreCompression()
|
|
1800
|
+
});
|
|
1801
|
+
try {
|
|
1802
|
+
await this.reconcileIndexes(txId, toPublicIndexDefinitions(normalized));
|
|
1803
|
+
this.withEngine((engine) => engine.commit_tx(toWasmU64(txId)), { allowDuringMaintenance: true });
|
|
1804
|
+
this.committedIndexes = cloneNormalizedIndexDefinitions(normalized);
|
|
1805
|
+
} catch (error) {
|
|
1806
|
+
try {
|
|
1807
|
+
this.withEngine((engine) => engine.rollback_tx(toWasmU64(txId)), { allowDuringMaintenance: true });
|
|
1808
|
+
} catch {}
|
|
1809
|
+
throw error;
|
|
1810
|
+
} finally {
|
|
1811
|
+
this.cleanupTxState(txId);
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
/**
|
|
1815
|
+
* Publishes a fresh generation only if the active generation has not changed.
|
|
1816
|
+
* A failed swap may have reached storage; read the control file before cleanup.
|
|
1817
|
+
* After publication, abandon the old engine to avoid a checkpoint into inactive files.
|
|
1818
|
+
*/
|
|
1819
|
+
async runCompactionOperation(operation) {
|
|
1820
|
+
const dbName = this.requireDbName();
|
|
1821
|
+
if (this.maintenanceOperation) throw remoteError("DatabaseBusyError", `database ${dbName} is busy: ${this.maintenanceOperation} in progress`);
|
|
1822
|
+
this.maintenanceOperation = operation;
|
|
1823
|
+
const start = performance.now();
|
|
1824
|
+
const wasm = await this.loadWasm();
|
|
1825
|
+
let rebuiltEngine = null;
|
|
1826
|
+
let generationName = null;
|
|
1827
|
+
let published = false;
|
|
1828
|
+
try {
|
|
1829
|
+
const sizeBefore = await wasm.dbDirectorySize(dbName);
|
|
1830
|
+
const defs = cloneNormalizedIndexDefinitions(this.loadCommittedIndexSchema());
|
|
1831
|
+
this.rollbackAllTransactions({ allowDuringMaintenance: true });
|
|
1832
|
+
const expectedGeneration = await wasm.readActiveGeneration(dbName);
|
|
1833
|
+
generationName = (await wasm.prepareRebuildTarget(dbName)).generationName;
|
|
1834
|
+
rebuiltEngine = new wasm.WasmEngine();
|
|
1835
|
+
await rebuiltEngine.openGeneration(dbName, generationName, {
|
|
1836
|
+
create_if_missing: true,
|
|
1837
|
+
cache_pages: this.currentCachePages()
|
|
1838
|
+
});
|
|
1839
|
+
const target = rebuiltEngine;
|
|
1840
|
+
this.withEngine((engine) => operation === "rebuild" ? engine.compact_into_skipping_stores(target, [...defs.map((def) => def.internalStore), INDEX_METADATA_STORE]) : engine.compact_into(target), { allowDuringMaintenance: true });
|
|
1841
|
+
if (operation === "rebuild" && defs.length > 0) {
|
|
1842
|
+
const previousEngine = this.engine;
|
|
1843
|
+
const previousMaintenance = this.maintenanceOperation;
|
|
1844
|
+
this.engine = target;
|
|
1845
|
+
this.maintenanceOperation = null;
|
|
1846
|
+
try {
|
|
1847
|
+
this.committedIndexes = [];
|
|
1848
|
+
this.committedStoreCompression = null;
|
|
1849
|
+
await this.applyCommittedIndexSchema(defs);
|
|
1850
|
+
} finally {
|
|
1851
|
+
this.engine = previousEngine;
|
|
1852
|
+
this.maintenanceOperation = previousMaintenance;
|
|
1853
|
+
this.committedStoreCompression = null;
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
try {
|
|
1857
|
+
await wasm.swapActiveGeneration(dbName, generationName, expectedGeneration);
|
|
1858
|
+
published = true;
|
|
1859
|
+
} catch (swapError) {
|
|
1860
|
+
published = await readActiveGenerationOrNull(wasm, dbName) === generationName;
|
|
1861
|
+
if (!published) throw swapError;
|
|
1862
|
+
}
|
|
1863
|
+
const previousEngine = this.engine;
|
|
1864
|
+
this.engine = rebuiltEngine;
|
|
1865
|
+
rebuiltEngine = null;
|
|
1866
|
+
this.committedIndexes = defs;
|
|
1867
|
+
this.committedStoreCompression = null;
|
|
1868
|
+
try {
|
|
1869
|
+
previousEngine?.abandon();
|
|
1870
|
+
} catch {}
|
|
1871
|
+
try {
|
|
1872
|
+
await wasm.cleanupInactiveEntries(dbName);
|
|
1873
|
+
} catch {}
|
|
1874
|
+
const sizeAfter = await wasm.dbDirectorySize(dbName);
|
|
1875
|
+
return {
|
|
1876
|
+
sizeBefore,
|
|
1877
|
+
sizeAfter,
|
|
1878
|
+
reclaimedBytes: Math.max(0, sizeBefore - sizeAfter),
|
|
1879
|
+
durationMs: performance.now() - start
|
|
1880
|
+
};
|
|
1881
|
+
} catch (error) {
|
|
1882
|
+
if (!published) {
|
|
1883
|
+
this.committedIndexes = null;
|
|
1884
|
+
this.committedStoreCompression = null;
|
|
1885
|
+
try {
|
|
1886
|
+
rebuiltEngine?.abandon();
|
|
1887
|
+
} catch {}
|
|
1888
|
+
if (generationName !== null) {
|
|
1889
|
+
const active = await readActiveGenerationOrNull(wasm, dbName);
|
|
1890
|
+
if (active !== void 0 && active !== generationName) try {
|
|
1891
|
+
await wasm.cleanupInactiveEntries(dbName);
|
|
1892
|
+
} catch {}
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
throw remapError(error);
|
|
1896
|
+
} finally {
|
|
1897
|
+
this.maintenanceOperation = null;
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
/**
|
|
1901
|
+
* Returns null when no recovery is needed. A recovery attempt invalidates
|
|
1902
|
+
* all open transactions, even if recovery fails.
|
|
1903
|
+
*/
|
|
1904
|
+
recoverEngineIfNeeded() {
|
|
1905
|
+
const engine = this.engine;
|
|
1906
|
+
if (!engine || !engine.needs_recovery()) return null;
|
|
1907
|
+
let report;
|
|
1908
|
+
try {
|
|
1909
|
+
report = engine.recover();
|
|
1910
|
+
} catch (error) {
|
|
1911
|
+
throw remoteError("RecoveryRequiredError", `database ${this.dbName ?? ""} needs recovery and recovery failed: ${remapError(error).message}`);
|
|
1912
|
+
} finally {
|
|
1913
|
+
this.clearRuntimeCaches();
|
|
1914
|
+
}
|
|
1915
|
+
return {
|
|
1916
|
+
pendingTxid: fromOptionalWasmU64(report.pendingTxid),
|
|
1917
|
+
pendingCommitted: report.pendingCommitted === true
|
|
1918
|
+
};
|
|
1919
|
+
}
|
|
1920
|
+
cleanupTxState(txId) {
|
|
1921
|
+
for (const [cursorId, cursor] of this.primaryCursors) if (cursor.txId === txId) {
|
|
1922
|
+
this.primaryCursors.delete(cursorId);
|
|
1923
|
+
try {
|
|
1924
|
+
this.engine?.close_scan_cursor(cursor.engineId);
|
|
1925
|
+
} catch {}
|
|
1926
|
+
}
|
|
1927
|
+
this.txChanges.delete(txId);
|
|
1928
|
+
this.txModes.delete(txId);
|
|
1929
|
+
this.txIndexSchemas.delete(txId);
|
|
1930
|
+
this.txEnsuredRawStores.delete(txId);
|
|
1931
|
+
this.txIndexSchemaChanged.delete(txId);
|
|
1932
|
+
this.txStoreCompression.delete(txId);
|
|
1933
|
+
}
|
|
1934
|
+
getOrCreateTrackedTxn(txId) {
|
|
1935
|
+
return getOrInsert(this.txChanges, txId, () => /* @__PURE__ */ new Map());
|
|
1936
|
+
}
|
|
1937
|
+
getOrCreateTrackedStore(txId, store) {
|
|
1938
|
+
return getOrInsert(this.getOrCreateTrackedTxn(txId), store, () => ({
|
|
1939
|
+
touched: false,
|
|
1940
|
+
storeLevel: null,
|
|
1941
|
+
keys: /* @__PURE__ */ new Map()
|
|
1942
|
+
}));
|
|
1943
|
+
}
|
|
1944
|
+
recordStoreLevelChange(txId, store, kind) {
|
|
1945
|
+
if (isInternalStoreName(store)) return;
|
|
1946
|
+
const trackedStore = this.getOrCreateTrackedStore(txId, store);
|
|
1947
|
+
trackedStore.touched = true;
|
|
1948
|
+
trackedStore.storeLevel = kind;
|
|
1949
|
+
trackedStore.keys.clear();
|
|
1950
|
+
}
|
|
1951
|
+
recordPutChange(txId, store, key, baselineExists) {
|
|
1952
|
+
this.recordKeyChange(txId, store, key, baselineExists, "put");
|
|
1953
|
+
}
|
|
1954
|
+
recordPutChanges(txId, store, entries, baselines) {
|
|
1955
|
+
if (isInternalStoreName(store)) return;
|
|
1956
|
+
const trackedStore = this.getOrCreateTrackedStore(txId, store);
|
|
1957
|
+
trackedStore.touched = true;
|
|
1958
|
+
const count = Math.min(entries.length, baselines.length);
|
|
1959
|
+
for (let i = 0; i < count; i += 1) this.setTrackedKeyChange(trackedStore, entries[i][0], baselines[i], "put");
|
|
1960
|
+
}
|
|
1961
|
+
recordPutKeyChanges(txId, store, keys, baselines) {
|
|
1962
|
+
if (isInternalStoreName(store)) return;
|
|
1963
|
+
const trackedStore = this.getOrCreateTrackedStore(txId, store);
|
|
1964
|
+
trackedStore.touched = true;
|
|
1965
|
+
const count = Math.min(keys.length, baselines.length);
|
|
1966
|
+
for (let i = 0; i < count; i += 1) this.setTrackedKeyChange(trackedStore, keys[i], baselines[i] === true || baselines[i] === 1, "put");
|
|
1967
|
+
}
|
|
1968
|
+
recordDeleteChange(txId, store, key, deleted) {
|
|
1969
|
+
if (deleted) this.recordKeyChange(txId, store, key, true, "delete");
|
|
1970
|
+
}
|
|
1971
|
+
recordKeyChange(txId, store, key, baselineExists, finalKind) {
|
|
1972
|
+
if (isInternalStoreName(store)) return;
|
|
1973
|
+
const trackedStore = this.getOrCreateTrackedStore(txId, store);
|
|
1974
|
+
trackedStore.touched = true;
|
|
1975
|
+
this.setTrackedKeyChange(trackedStore, key, baselineExists, finalKind);
|
|
1976
|
+
}
|
|
1977
|
+
setTrackedKeyChange(trackedStore, key, baselineExists, finalKind) {
|
|
1978
|
+
const keyId = bytesToHex(key);
|
|
1979
|
+
const existing = trackedStore.keys.get(keyId);
|
|
1980
|
+
trackedStore.keys.set(keyId, {
|
|
1981
|
+
key: existing?.key ?? key.slice(),
|
|
1982
|
+
baselineExists: existing?.baselineExists ?? baselineExists,
|
|
1983
|
+
finalKind
|
|
1984
|
+
});
|
|
1985
|
+
}
|
|
1986
|
+
markStoreTouched(txId, store) {
|
|
1987
|
+
if (isInternalStoreName(store)) return;
|
|
1988
|
+
this.getOrCreateTrackedStore(txId, store).touched = true;
|
|
1989
|
+
}
|
|
1990
|
+
recordBatchOutcomes(txId, store, ops, outcomes) {
|
|
1991
|
+
for (let i = 0; i < outcomes.length && i < ops.length; i += 1) {
|
|
1992
|
+
const op = ops[i];
|
|
1993
|
+
const outcome = outcomes[i];
|
|
1994
|
+
if (op.kind === "put" && outcome.kind === "put") this.recordPutChange(txId, store, op.key, outcome.baselineExists);
|
|
1995
|
+
else if (op.kind === "delete" && outcome.kind === "delete") this.recordDeleteChange(txId, store, op.key, outcome.deleted);
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
recordBatchKeyOutcomes(txId, store, ops, outcomes) {
|
|
1999
|
+
for (let i = 0; i < outcomes.length && i < ops.length; i += 1) {
|
|
2000
|
+
const op = ops[i];
|
|
2001
|
+
const outcome = outcomes[i];
|
|
2002
|
+
if (op.kind === "put" && outcome.kind === "put") this.recordPutChange(txId, store, op.key, outcome.baselineExists);
|
|
2003
|
+
else if (op.kind === "delete" && outcome.kind === "delete") this.recordDeleteChange(txId, store, op.key, outcome.deleted);
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
finalizeTrackedChanges(tracked) {
|
|
2007
|
+
const stores = [];
|
|
2008
|
+
for (const [store, trackedStore] of tracked) {
|
|
2009
|
+
if (!trackedStore.touched || isInternalStoreName(store)) continue;
|
|
2010
|
+
const changes = [];
|
|
2011
|
+
if (trackedStore.storeLevel !== null) changes.push({
|
|
2012
|
+
key: /* @__PURE__ */ new Uint8Array(0),
|
|
2013
|
+
kind: trackedStore.storeLevel
|
|
2014
|
+
});
|
|
2015
|
+
for (const change of trackedStore.keys.values()) {
|
|
2016
|
+
if (change.finalKind === "delete" && !change.baselineExists) continue;
|
|
2017
|
+
changes.push({
|
|
2018
|
+
key: change.key,
|
|
2019
|
+
kind: change.finalKind
|
|
2020
|
+
});
|
|
2021
|
+
}
|
|
2022
|
+
stores.push({
|
|
2023
|
+
store,
|
|
2024
|
+
changes
|
|
2025
|
+
});
|
|
2026
|
+
}
|
|
2027
|
+
return stores;
|
|
2028
|
+
}
|
|
2029
|
+
rollbackAllTransactions(options = {}) {
|
|
2030
|
+
if (!this.engine || this.txChanges.size === 0) {
|
|
2031
|
+
this.clearTxCaches();
|
|
2032
|
+
return;
|
|
2033
|
+
}
|
|
2034
|
+
const txIds = Array.from(this.txChanges.keys());
|
|
2035
|
+
for (const txId of txIds) try {
|
|
2036
|
+
this.withEngine((engine) => engine.rollback_tx(toWasmU64(txId)), options);
|
|
2037
|
+
} finally {
|
|
2038
|
+
this.cleanupTxState(txId);
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
currentStoreNames() {
|
|
2042
|
+
return this.allRawStoreNames().filter((store) => !isInternalStoreName(store));
|
|
2043
|
+
}
|
|
2044
|
+
allRawStoreNames() {
|
|
2045
|
+
return this.withEngine((engine) => engine.list_stores());
|
|
2046
|
+
}
|
|
2047
|
+
loadCommittedStoreCompression() {
|
|
2048
|
+
if (this.committedStoreCompression) return this.committedStoreCompression;
|
|
2049
|
+
const rows = this.withEngine((engine) => engine.list_store_configs());
|
|
2050
|
+
const loaded = /* @__PURE__ */ new Map();
|
|
2051
|
+
for (const row of rows) {
|
|
2052
|
+
if (typeof row.name !== "string") throw remoteError("CorruptionError", "invalid store config row: missing name");
|
|
2053
|
+
const flags = typeof row.flags === "bigint" ? Number(row.flags) : row.flags;
|
|
2054
|
+
if (typeof flags !== "number" || !Number.isSafeInteger(flags) || flags < 0) throw remoteError("CorruptionError", `invalid store flags for ${row.name}`);
|
|
2055
|
+
loaded.set(row.name, compressionFromStoreFlags(flags));
|
|
2056
|
+
}
|
|
2057
|
+
this.committedStoreCompression = loaded;
|
|
2058
|
+
return loaded;
|
|
2059
|
+
}
|
|
2060
|
+
ensureTxStoreCompression(txId) {
|
|
2061
|
+
const snapshot = this.txStoreCompression.get(txId);
|
|
2062
|
+
if (snapshot?.owned) return snapshot.values;
|
|
2063
|
+
const values = new Map(snapshot?.values);
|
|
2064
|
+
this.txStoreCompression.set(txId, {
|
|
2065
|
+
owned: true,
|
|
2066
|
+
values
|
|
2067
|
+
});
|
|
2068
|
+
return values;
|
|
2069
|
+
}
|
|
2070
|
+
recordStoreCompressionCreate(txId, store, compression) {
|
|
2071
|
+
if (isInternalStoreName(store)) return;
|
|
2072
|
+
this.ensureTxStoreCompression(txId).set(store, compression);
|
|
2073
|
+
}
|
|
2074
|
+
recordStoreCompressionDrop(txId, store) {
|
|
2075
|
+
if (isInternalStoreName(store)) return;
|
|
2076
|
+
this.ensureTxStoreCompression(txId).delete(store);
|
|
2077
|
+
}
|
|
2078
|
+
storeCompressionForTx(txId, store) {
|
|
2079
|
+
if (isInternalStoreName(store)) return false;
|
|
2080
|
+
const snapshot = this.txStoreCompression.get(txId);
|
|
2081
|
+
if (snapshot) return snapshot.values.get(store) ?? false;
|
|
2082
|
+
return this.loadCommittedStoreCompression().get(store) ?? false;
|
|
2083
|
+
}
|
|
2084
|
+
async encodeEntriesForWrite(txId, store, entries) {
|
|
2085
|
+
if (isInternalStoreName(store)) return entries;
|
|
2086
|
+
const compression = this.storeCompressionForTx(txId, store);
|
|
2087
|
+
if (compression === false) return entries;
|
|
2088
|
+
const storedEntries = [];
|
|
2089
|
+
for (const [key, value] of entries) storedEntries.push([key, await encodeStoreValueRecord(value, compression)]);
|
|
2090
|
+
return storedEntries;
|
|
2091
|
+
}
|
|
2092
|
+
async encodePackedEntriesForWrite(txId, store, items, keys) {
|
|
2093
|
+
const records = new Array(items.length / 2);
|
|
2094
|
+
for (let index = 0; index < records.length; index += 1) {
|
|
2095
|
+
const value = this.preparePutValue(txId, store, items[index * 2 + 1]);
|
|
2096
|
+
records[index] = value instanceof Promise ? await value : value;
|
|
2097
|
+
}
|
|
2098
|
+
return this.packPreparedPutEntries(keys, records);
|
|
2099
|
+
}
|
|
2100
|
+
preparePutValue(txId, store, value) {
|
|
2101
|
+
const compression = this.storeCompressionForTx(txId, store);
|
|
2102
|
+
return compression === "snappy" || compression === false ? prepareFastStoreValueRecord(value, compression) : encodeStoreValueRecord(value, compression);
|
|
2103
|
+
}
|
|
2104
|
+
packPreparedPutEntries(keys, records) {
|
|
2105
|
+
let total = 4 + keys.length * 8;
|
|
2106
|
+
for (let index = 0; index < keys.length; index += 1) {
|
|
2107
|
+
total += keys[index].byteLength + records[index].byteLength;
|
|
2108
|
+
if (total > MAX_PACKED_BYTES) throw remoteError("WorkerProtocolError", "compressed packed putMany exceeds the u32 byte limit");
|
|
2109
|
+
}
|
|
2110
|
+
const bytes = new Uint8Array(total);
|
|
2111
|
+
const view = new DataView(bytes.buffer);
|
|
2112
|
+
view.setUint32(0, keys.length * 2, true);
|
|
2113
|
+
let offset = 4 + keys.length * 8;
|
|
2114
|
+
for (let index = 0; index < records.length; index += 1) {
|
|
2115
|
+
const key = keys[index];
|
|
2116
|
+
const record = records[index];
|
|
2117
|
+
view.setUint32(4 + index * 8, key.byteLength, true);
|
|
2118
|
+
view.setUint32(8 + index * 8, record.byteLength, true);
|
|
2119
|
+
bytes.set(key, offset);
|
|
2120
|
+
offset += key.byteLength;
|
|
2121
|
+
if (record instanceof Uint8Array) {
|
|
2122
|
+
bytes.set(record, offset);
|
|
2123
|
+
offset += record.byteLength;
|
|
2124
|
+
} else offset = writePreparedStoreValueRecord(record, bytes, offset);
|
|
2125
|
+
}
|
|
2126
|
+
return bytes;
|
|
2127
|
+
}
|
|
2128
|
+
async putIndexedMany(txId, store, items, defs, options) {
|
|
2129
|
+
let keys = [];
|
|
2130
|
+
let values = [];
|
|
2131
|
+
let indexOps = [];
|
|
2132
|
+
let bytes = 4;
|
|
2133
|
+
const pendingPrimaries = /* @__PURE__ */ new Set();
|
|
2134
|
+
const pendingUniqueKeys = /* @__PURE__ */ new Set();
|
|
2135
|
+
const flush = () => {
|
|
2136
|
+
if (keys.length === 0) return;
|
|
2137
|
+
const batchKeys = keys;
|
|
2138
|
+
const batchValues = values;
|
|
2139
|
+
const batchOps = indexOps;
|
|
2140
|
+
keys = [];
|
|
2141
|
+
values = [];
|
|
2142
|
+
indexOps = [];
|
|
2143
|
+
bytes = 4;
|
|
2144
|
+
pendingPrimaries.clear();
|
|
2145
|
+
pendingUniqueKeys.clear();
|
|
2146
|
+
const packet = this.packPreparedPutEntries(batchKeys, batchValues);
|
|
2147
|
+
let baselines;
|
|
2148
|
+
try {
|
|
2149
|
+
baselines = this.withEngine((engine) => engine.put_many_indexed_packed(toWasmU64(txId), store, packet, batchOps, toWasmPutOptions(options)));
|
|
2150
|
+
} catch (error) {
|
|
2151
|
+
const partial = partialBooleanOutcomes(error);
|
|
2152
|
+
if (partial.length > 0) this.recordPutKeyChanges(txId, store, batchKeys, partial);
|
|
2153
|
+
throw error;
|
|
2154
|
+
}
|
|
2155
|
+
if (baselines.length !== batchKeys.length) throw remoteError("InternalError", `put_many_indexed_packed outcome count mismatch: ${baselines.length} != ${batchKeys.length}`);
|
|
2156
|
+
this.recordPutKeyChanges(txId, store, batchKeys, baselines);
|
|
2157
|
+
};
|
|
2158
|
+
for (let index = 0; index < items.length / 2; index += 1) {
|
|
2159
|
+
const key = items[index * 2];
|
|
2160
|
+
const value = items[index * 2 + 1];
|
|
2161
|
+
const primaryToken = bytesToHex(key);
|
|
2162
|
+
if (pendingPrimaries.has(primaryToken)) flush();
|
|
2163
|
+
try {
|
|
2164
|
+
const oldValue = await this.readStoreValue(txId, store, key);
|
|
2165
|
+
const oldKeyFor = oldValue === null ? null : createIndexKeyExtractor(oldValue);
|
|
2166
|
+
const newKeyFor = createIndexKeyExtractor(value);
|
|
2167
|
+
const plans = defs.map((def) => ({
|
|
2168
|
+
def,
|
|
2169
|
+
oldLogicalKey: oldKeyFor === null ? null : oldKeyFor(def),
|
|
2170
|
+
newLogicalKey: newKeyFor(def),
|
|
2171
|
+
newPhysicalKey: null
|
|
2172
|
+
}));
|
|
2173
|
+
if (plans.some((plan) => plan.def.unique && plan.newLogicalKey !== null && pendingUniqueKeys.has(`${plan.def.internalStore}:${bytesToHex(plan.newLogicalKey)}`))) {
|
|
2174
|
+
flush();
|
|
2175
|
+
index -= 1;
|
|
2176
|
+
continue;
|
|
2177
|
+
}
|
|
2178
|
+
this.ensureInternalStoresForDefinitions(txId, defs);
|
|
2179
|
+
for (const plan of plans) {
|
|
2180
|
+
if (plan.newLogicalKey === null) continue;
|
|
2181
|
+
plan.newPhysicalKey = this.buildIndexEntryKey(plan.def, plan.newLogicalKey, key);
|
|
2182
|
+
if (plan.oldLogicalKey === null || !bytesEqual(plan.oldLogicalKey, plan.newLogicalKey)) await this.assertUniqueIndexAvailability(txId, plan.def, plan.newLogicalKey, key);
|
|
2183
|
+
}
|
|
2184
|
+
const preparedValue = this.preparePutValue(txId, store, value);
|
|
2185
|
+
const storedValue = preparedValue instanceof Promise ? await preparedValue : preparedValue;
|
|
2186
|
+
const ops = [];
|
|
2187
|
+
for (const plan of plans) if (plan.oldLogicalKey !== null && (plan.newLogicalKey === null || !bytesEqual(plan.oldLogicalKey, plan.newLogicalKey))) ops.push({
|
|
2188
|
+
store: plan.def.internalStore,
|
|
2189
|
+
kind: "delete",
|
|
2190
|
+
key: encodeIndexEntryKey(plan.oldLogicalKey, key)
|
|
2191
|
+
});
|
|
2192
|
+
for (const plan of plans) {
|
|
2193
|
+
if (plan.newPhysicalKey !== null) ops.push({
|
|
2194
|
+
store: plan.def.internalStore,
|
|
2195
|
+
kind: "put",
|
|
2196
|
+
key: plan.newPhysicalKey
|
|
2197
|
+
});
|
|
2198
|
+
if (plan.def.unique) {
|
|
2199
|
+
for (const logicalKey of [plan.oldLogicalKey, plan.newLogicalKey]) if (logicalKey !== null) pendingUniqueKeys.add(`${plan.def.internalStore}:${bytesToHex(logicalKey)}`);
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
keys.push(key);
|
|
2203
|
+
values.push(storedValue);
|
|
2204
|
+
indexOps.push(ops);
|
|
2205
|
+
bytes += 8 + key.byteLength + storedValue.byteLength;
|
|
2206
|
+
for (const op of ops) bytes += op.key.byteLength;
|
|
2207
|
+
pendingPrimaries.add(primaryToken);
|
|
2208
|
+
} catch (error) {
|
|
2209
|
+
flush();
|
|
2210
|
+
throw error;
|
|
2211
|
+
}
|
|
2212
|
+
if (options.ttl !== void 0 || keys.length >= INDEX_PUT_BATCH_ROWS || bytes >= INDEX_PUT_BATCH_BYTES) flush();
|
|
2213
|
+
}
|
|
2214
|
+
flush();
|
|
2215
|
+
}
|
|
2216
|
+
async encodeBatchOpsForWrite(txId, store, ops) {
|
|
2217
|
+
if (isInternalStoreName(store)) return ops;
|
|
2218
|
+
const compression = this.storeCompressionForTx(txId, store);
|
|
2219
|
+
if (compression === false) return ops;
|
|
2220
|
+
const storedOps = [];
|
|
2221
|
+
for (const op of ops) storedOps.push(op.kind === "put" ? {
|
|
2222
|
+
kind: "put",
|
|
2223
|
+
key: op.key,
|
|
2224
|
+
value: await encodeStoreValueRecord(op.value, compression)
|
|
2225
|
+
} : op);
|
|
2226
|
+
return storedOps;
|
|
2227
|
+
}
|
|
2228
|
+
async encodeStoreValueForWrite(txId, store, value) {
|
|
2229
|
+
if (isInternalStoreName(store)) return value;
|
|
2230
|
+
return encodeStoreValueRecord(value, this.storeCompressionForTx(txId, store));
|
|
2231
|
+
}
|
|
2232
|
+
async decodeStoreValue(txId, store, value) {
|
|
2233
|
+
if (isInternalStoreName(store)) return value;
|
|
2234
|
+
if (this.storeCompressionForTx(txId, store) === false) return value;
|
|
2235
|
+
return decodeStoreValueRecord(value, { strict: true });
|
|
2236
|
+
}
|
|
2237
|
+
async decodeStoreValueForFeed(store, value) {
|
|
2238
|
+
if (isInternalStoreName(store)) return value;
|
|
2239
|
+
return decodeStoreValueRecord(value, { strict: (this.loadCommittedStoreCompression().get(store) ?? false) !== false });
|
|
2240
|
+
}
|
|
2241
|
+
async readStoreValue(txId, store, key) {
|
|
2242
|
+
const value = this.readRawValue(txId, store, key);
|
|
2243
|
+
return value === null ? null : this.decodeStoreValue(txId, store, value);
|
|
2244
|
+
}
|
|
2245
|
+
async readStoreScan(txId, store, range) {
|
|
2246
|
+
const rows = this.readRawScan(txId, store, range);
|
|
2247
|
+
if (isInternalStoreName(store) || this.storeCompressionForTx(txId, store) === false) return rows;
|
|
2248
|
+
const values = rows.map((row) => row.value);
|
|
2249
|
+
await this.decodeStoreValues(txId, store, values);
|
|
2250
|
+
for (let index = 0; index < rows.length; index += 1) rows[index].value = values[index];
|
|
2251
|
+
return rows;
|
|
2252
|
+
}
|
|
2253
|
+
async decodeStoreValues(txId, store, values) {
|
|
2254
|
+
const state = {
|
|
2255
|
+
next: 0,
|
|
2256
|
+
failed: false,
|
|
2257
|
+
failure: void 0
|
|
2258
|
+
};
|
|
2259
|
+
const decodeNext = async () => {
|
|
2260
|
+
while (!state.failed && state.next < values.length) {
|
|
2261
|
+
const index = state.next++;
|
|
2262
|
+
const value = values[index];
|
|
2263
|
+
if (value === null) continue;
|
|
2264
|
+
try {
|
|
2265
|
+
values[index] = await this.decodeStoreValue(txId, store, value);
|
|
2266
|
+
} catch (error) {
|
|
2267
|
+
if (!state.failed) {
|
|
2268
|
+
state.failed = true;
|
|
2269
|
+
state.failure = error;
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
};
|
|
2274
|
+
await Promise.all(Array.from({ length: Math.min(COMPRESSED_READ_CONCURRENCY, values.length) }, decodeNext));
|
|
2275
|
+
if (state.failed) throw state.failure;
|
|
2276
|
+
return values;
|
|
2277
|
+
}
|
|
2278
|
+
indexesForStoreInTx(txId, store) {
|
|
2279
|
+
const schema = this.indexSchemaForTx(txId);
|
|
2280
|
+
let byStore = this.indexesBySchema.get(schema);
|
|
2281
|
+
if (!byStore) {
|
|
2282
|
+
byStore = /* @__PURE__ */ new Map();
|
|
2283
|
+
for (const def of schema) {
|
|
2284
|
+
const defs = byStore.get(def.store);
|
|
2285
|
+
if (defs) defs.push(def);
|
|
2286
|
+
else byStore.set(def.store, [def]);
|
|
2287
|
+
}
|
|
2288
|
+
this.indexesBySchema.set(schema, byStore);
|
|
2289
|
+
}
|
|
2290
|
+
return byStore.get(store) ?? [];
|
|
2291
|
+
}
|
|
2292
|
+
resolveIndexDefinition(txId, store, indexName) {
|
|
2293
|
+
if (!this.rawStoreExistsInTx(txId, store)) throw remoteError("StoreNotFoundError", `store not found: ${store}`);
|
|
2294
|
+
const def = findIndexDefinition(this.indexesForStoreInTx(txId, store), store, indexName);
|
|
2295
|
+
if (!def) throw remoteError("IndexNotFoundError", `index not found: ${store}.${indexName}`);
|
|
2296
|
+
return def;
|
|
2297
|
+
}
|
|
2298
|
+
indexSchemaForTx(txId) {
|
|
2299
|
+
const override = this.txIndexSchemas.get(txId);
|
|
2300
|
+
if (override) return override;
|
|
2301
|
+
if (this.committedIndexes) return this.committedIndexes;
|
|
2302
|
+
const loaded = this.readIndexSchemaInTx(txId);
|
|
2303
|
+
this.committedIndexes = cloneNormalizedIndexDefinitions(loaded);
|
|
2304
|
+
return this.committedIndexes;
|
|
2305
|
+
}
|
|
2306
|
+
loadCommittedIndexSchema() {
|
|
2307
|
+
if (this.committedIndexes) return this.committedIndexes;
|
|
2308
|
+
const txId = fromWasmU64(this.withEngine((engine) => engine.begin_tx("readonly")));
|
|
2309
|
+
try {
|
|
2310
|
+
const loaded = this.readIndexSchemaInTx(txId);
|
|
2311
|
+
this.committedIndexes = cloneNormalizedIndexDefinitions(loaded);
|
|
2312
|
+
return this.committedIndexes;
|
|
2313
|
+
} finally {
|
|
2314
|
+
this.withEngine((engine) => engine.rollback_tx(toWasmU64(txId)));
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
readIndexSchemaInTx(txId) {
|
|
2318
|
+
if (!this.rawStoreExistsInTx(txId, "__browserdb:indexes")) return [];
|
|
2319
|
+
const defs = this.readRawScan(txId, INDEX_METADATA_STORE, EMPTY_RANGE).map((row) => decodeIndexMetadataValue(row.value));
|
|
2320
|
+
defs.sort(compareNormalizedIndexDefinitions);
|
|
2321
|
+
return defs;
|
|
2322
|
+
}
|
|
2323
|
+
rawStoreExistsInTx(txId, store) {
|
|
2324
|
+
try {
|
|
2325
|
+
this.readRawScan(txId, store, { limit: 0 });
|
|
2326
|
+
return true;
|
|
2327
|
+
} catch (error) {
|
|
2328
|
+
if (isNamedError(error, "StoreNotFoundError")) return false;
|
|
2329
|
+
throw error;
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
ensureRawStore(txId, store) {
|
|
2333
|
+
const cacheable = this.txModes.get(txId) === "readwrite";
|
|
2334
|
+
if (cacheable && this.txEnsuredRawStores.get(txId)?.has(store)) return;
|
|
2335
|
+
this.ignoreNamedError("StoreExistsError", () => {
|
|
2336
|
+
this.withEngine((engine) => engine.create_store(toWasmU64(txId), store));
|
|
2337
|
+
});
|
|
2338
|
+
if (cacheable) getOrInsert(this.txEnsuredRawStores, txId, () => /* @__PURE__ */ new Set()).add(store);
|
|
2339
|
+
}
|
|
2340
|
+
clearRawStoreIfExists(txId, store) {
|
|
2341
|
+
try {
|
|
2342
|
+
this.withEngine((engine) => engine.clear_store(toWasmU64(txId), store));
|
|
2343
|
+
} catch (error) {
|
|
2344
|
+
if (!isNamedError(error, "StoreNotFoundError")) throw error;
|
|
2345
|
+
this.txEnsuredRawStores.get(txId)?.delete(store);
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
dropRawStoreIfExists(txId, store) {
|
|
2349
|
+
this.txEnsuredRawStores.get(txId)?.delete(store);
|
|
2350
|
+
this.ignoreNamedError("StoreNotFoundError", () => {
|
|
2351
|
+
this.withEngine((engine) => engine.drop_store(toWasmU64(txId), store));
|
|
2352
|
+
});
|
|
2353
|
+
}
|
|
2354
|
+
ensureInternalStoresForDefinitions(txId, defs) {
|
|
2355
|
+
for (const def of defs) this.ensureRawStore(txId, def.internalStore);
|
|
2356
|
+
}
|
|
2357
|
+
ensureInternalStoresForStore(txId, store) {
|
|
2358
|
+
this.ensureInternalStoresForDefinitions(txId, this.indexesForStoreInTx(txId, store));
|
|
2359
|
+
}
|
|
2360
|
+
readRawValue(txId, store, key) {
|
|
2361
|
+
const value = this.withEngine((engine) => engine.get(toWasmU64(txId), store, key));
|
|
2362
|
+
return value === null ? null : normalizeWasmBytes(value);
|
|
2363
|
+
}
|
|
2364
|
+
readRawScan(txId, store, range) {
|
|
2365
|
+
const rows = this.withEngine((engine) => engine.scan(toWasmU64(txId), store, toWasmRange(range)));
|
|
2366
|
+
for (const row of rows) {
|
|
2367
|
+
row.key = normalizeWasmBytes(row.key);
|
|
2368
|
+
row.value = normalizeWasmBytes(row.value);
|
|
2369
|
+
}
|
|
2370
|
+
return rows;
|
|
2371
|
+
}
|
|
2372
|
+
readIndexKeyScan(txId, store, range) {
|
|
2373
|
+
if (typeof this.engine?.open_scan_cursor !== "function" || typeof this.engine?.scan_cursor_next !== "function" || typeof this.engine?.close_scan_cursor !== "function") return this.readRawScan(txId, store, range);
|
|
2374
|
+
const maxRows = range.limit ?? INDEX_SCAN_RAW_CHUNK_ROWS;
|
|
2375
|
+
const maxBytes = 4 + maxRows * 1032;
|
|
2376
|
+
const cursorId = this.withEngine((engine) => engine.open_scan_cursor(toWasmU64(txId), store, toWasmRange(range), true));
|
|
2377
|
+
try {
|
|
2378
|
+
const page = this.withEngine((engine) => engine.scan_cursor_next(cursorId, maxRows, maxBytes));
|
|
2379
|
+
const packet = normalizeWasmBytes(page.packet);
|
|
2380
|
+
const rows = unpackPackedScanRows(packet);
|
|
2381
|
+
if (rows.length !== page.rowCount || rows.length > maxRows || packet.byteLength > maxBytes || rows.some((row) => row.value.byteLength !== 0)) throw remoteError("CorruptionError", "index key cursor returned an invalid page");
|
|
2382
|
+
return rows;
|
|
2383
|
+
} finally {
|
|
2384
|
+
this.withEngine((engine) => engine.close_scan_cursor(cursorId));
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
putRawValue(txId, store, key, value) {
|
|
2388
|
+
this.withEngine((engine) => engine.put(toWasmU64(txId), store, key, value));
|
|
2389
|
+
}
|
|
2390
|
+
putIndexEntry(txId, def, physicalKey, value) {
|
|
2391
|
+
if (typeof this.engine?.put_index_entry_checked === "function") {
|
|
2392
|
+
const primaryKey = decodeIndexEntryKey(physicalKey).primaryKey;
|
|
2393
|
+
const state = this.readNativeValueState(txId, def.store, primaryKey);
|
|
2394
|
+
if (state?.revisionKnown) {
|
|
2395
|
+
this.withEngine((engine) => engine.put_index_entry_checked(toWasmU64(txId), def.store, def.internalStore, primaryKey, physicalKey, state.revisionEpoch, state.revisionOrdinal));
|
|
2396
|
+
return;
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
this.putRawValue(txId, def.internalStore, physicalKey, value);
|
|
2400
|
+
}
|
|
2401
|
+
readNativeValueState(txId, store, key) {
|
|
2402
|
+
if (typeof this.engine?.get_many_value_states !== "function") return null;
|
|
2403
|
+
try {
|
|
2404
|
+
const keys = new Uint8Array(8 + key.byteLength);
|
|
2405
|
+
const keyView = new DataView(keys.buffer);
|
|
2406
|
+
keyView.setUint32(0, 1, true);
|
|
2407
|
+
keyView.setUint32(4, key.byteLength, true);
|
|
2408
|
+
keys.set(key, 8);
|
|
2409
|
+
const packet = normalizeWasmBytes(this.withEngine((engine) => engine.get_many_value_states(toWasmU64(txId), store, keys)));
|
|
2410
|
+
return this.decodeNativeValueStates(packet, 1)?.[0] ?? null;
|
|
2411
|
+
} catch {
|
|
2412
|
+
return null;
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2415
|
+
packNativeKeys(keys) {
|
|
2416
|
+
let total = 4 + keys.length * 4;
|
|
2417
|
+
for (const key of keys) total += key.byteLength;
|
|
2418
|
+
const packet = new Uint8Array(total);
|
|
2419
|
+
const view = new DataView(packet.buffer);
|
|
2420
|
+
view.setUint32(0, keys.length, true);
|
|
2421
|
+
let offset = 4 + keys.length * 4;
|
|
2422
|
+
for (let index = 0; index < keys.length; index += 1) {
|
|
2423
|
+
const key = keys[index];
|
|
2424
|
+
view.setUint32(4 + index * 4, key.byteLength, true);
|
|
2425
|
+
packet.set(key, offset);
|
|
2426
|
+
offset += key.byteLength;
|
|
2427
|
+
}
|
|
2428
|
+
return packet;
|
|
2429
|
+
}
|
|
2430
|
+
decodeNativeValueStates(packet, count) {
|
|
2431
|
+
if (packet.byteLength !== 4 + count * 32) return null;
|
|
2432
|
+
const view = new DataView(packet.buffer, packet.byteOffset, packet.byteLength);
|
|
2433
|
+
if (view.getUint32(0, true) !== count) return null;
|
|
2434
|
+
const states = new Array(count);
|
|
2435
|
+
for (let index = 0; index < count; index += 1) {
|
|
2436
|
+
const offset = 4 + index * 32;
|
|
2437
|
+
const flags = view.getUint32(offset, true);
|
|
2438
|
+
const revisionEpoch = view.getBigUint64(offset + 12, true);
|
|
2439
|
+
const revisionOrdinal = view.getBigUint64(offset + 20, true);
|
|
2440
|
+
if ((flags & -8) !== 0 || (flags & 3) === 3 || (flags & 4) !== 0 && ((flags & 3) === 0 || revisionEpoch === 0n || revisionOrdinal === 0n)) return null;
|
|
2441
|
+
states[index] = {
|
|
2442
|
+
live: (flags & 1) !== 0,
|
|
2443
|
+
expired: (flags & 2) !== 0,
|
|
2444
|
+
revisionKnown: (flags & 4) !== 0,
|
|
2445
|
+
expiresAt: view.getBigUint64(offset + 4, true),
|
|
2446
|
+
revisionEpoch,
|
|
2447
|
+
revisionOrdinal,
|
|
2448
|
+
maxValueBytes: view.getUint32(offset + 28, true)
|
|
2449
|
+
};
|
|
2450
|
+
}
|
|
2451
|
+
return states;
|
|
2452
|
+
}
|
|
2453
|
+
inspectIndexedValue(txId, def, physicalKey, primaryKey) {
|
|
2454
|
+
const primary = this.readNativeValueState(txId, def.store, primaryKey);
|
|
2455
|
+
if (primary === null) return "unknown";
|
|
2456
|
+
if (!primary.live) {
|
|
2457
|
+
if (primary.expired && this.txModes.get(txId) === "readwrite") {
|
|
2458
|
+
if (this.withEngine((engine) => engine.has(toWasmU64(txId), def.store, primaryKey))) return "unknown";
|
|
2459
|
+
}
|
|
2460
|
+
return "missing";
|
|
2461
|
+
}
|
|
2462
|
+
if (!primary.revisionKnown) return "unknown";
|
|
2463
|
+
const index = this.readNativeValueState(txId, def.internalStore, physicalKey);
|
|
2464
|
+
return index?.live && index.revisionKnown && index.revisionEpoch === primary.revisionEpoch && index.revisionOrdinal === primary.revisionOrdinal ? "trusted" : "unknown";
|
|
2465
|
+
}
|
|
2466
|
+
deleteIndexEntryIfPresent(txId, def, logicalKey, primaryKey) {
|
|
2467
|
+
const physicalKey = this.buildIndexEntryKey(def, logicalKey, primaryKey);
|
|
2468
|
+
this.ignoreNamedError("StoreNotFoundError", () => {
|
|
2469
|
+
this.withEngine((engine) => engine.delete(toWasmU64(txId), def.internalStore, physicalKey));
|
|
2470
|
+
});
|
|
2471
|
+
}
|
|
2472
|
+
buildIndexEntryKey(def, logicalKey, primaryKey) {
|
|
2473
|
+
const physicalKey = encodeIndexEntryKey(logicalKey, primaryKey);
|
|
2474
|
+
if (physicalKey.byteLength > MAX_ENGINE_KEY_BYTES) throw remoteError("KeyTooLargeError", `index key too large for ${def.store}.${def.name}`);
|
|
2475
|
+
return physicalKey;
|
|
2476
|
+
}
|
|
2477
|
+
async assertUniqueIndexAvailability(txId, def, logicalKey, primaryKey) {
|
|
2478
|
+
if (!def.unique) return;
|
|
2479
|
+
const physical = indexKeyExactRange(logicalKey);
|
|
2480
|
+
let cursor = null;
|
|
2481
|
+
let chunkLimit = 1;
|
|
2482
|
+
for (;;) {
|
|
2483
|
+
const range = {
|
|
2484
|
+
...physical,
|
|
2485
|
+
limit: chunkLimit
|
|
2486
|
+
};
|
|
2487
|
+
if (cursor !== null) {
|
|
2488
|
+
range.gt = cursor;
|
|
2489
|
+
delete range.gte;
|
|
2490
|
+
}
|
|
2491
|
+
const rows = this.readIndexKeyScan(txId, def.internalStore, range);
|
|
2492
|
+
for (const row of rows) {
|
|
2493
|
+
cursor = row.key;
|
|
2494
|
+
const decoded = decodeIndexEntryKey(row.key);
|
|
2495
|
+
if (bytesEqual(decoded.primaryKey, primaryKey)) continue;
|
|
2496
|
+
const state = this.inspectIndexedValue(txId, def, row.key, decoded.primaryKey);
|
|
2497
|
+
if (state === "missing") {
|
|
2498
|
+
this.cleanupStaleIndexRow(txId, def, row.key);
|
|
2499
|
+
continue;
|
|
2500
|
+
}
|
|
2501
|
+
if (state === "trusted") throw remoteError("UniqueIndexConstraintError", `unique index constraint violation on ${def.store}.${def.name}`);
|
|
2502
|
+
const value = await this.readStoreValue(txId, def.store, decoded.primaryKey);
|
|
2503
|
+
if (value === null) {
|
|
2504
|
+
this.cleanupStaleIndexRow(txId, def, row.key);
|
|
2505
|
+
continue;
|
|
2506
|
+
}
|
|
2507
|
+
const currentLogicalKey = extractLogicalIndexKey(def, value);
|
|
2508
|
+
if (currentLogicalKey === null || !bytesEqual(currentLogicalKey, logicalKey)) {
|
|
2509
|
+
this.cleanupStaleIndexRow(txId, def, row.key);
|
|
2510
|
+
continue;
|
|
2511
|
+
}
|
|
2512
|
+
throw remoteError("UniqueIndexConstraintError", `unique index constraint violation on ${def.store}.${def.name}`);
|
|
2513
|
+
}
|
|
2514
|
+
if (rows.length < chunkLimit) return;
|
|
2515
|
+
chunkLimit = Math.min(INDEX_SCAN_RAW_CHUNK_ROWS, chunkLimit * 2);
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
/**
|
|
2519
|
+
* Resumes after a physical index key in the scan direction. Bounded raw
|
|
2520
|
+
* chunks prevent stale entries from forcing the whole range into memory.
|
|
2521
|
+
* The output limit counts only live rows.
|
|
2522
|
+
*/
|
|
2523
|
+
async loadVisibleIndexPage(txId, def, range, cursor, limit, maxBytes = INDEX_SCAN_PAGE_BYTES) {
|
|
2524
|
+
const { limit: _ignored, ...withoutLimit } = range;
|
|
2525
|
+
const physical = indexRangeToPhysicalRange(withoutLimit);
|
|
2526
|
+
const reverse = physical.reverse === true;
|
|
2527
|
+
let resumeAfter = cursor;
|
|
2528
|
+
const visible = [];
|
|
2529
|
+
let outputBytes = 4;
|
|
2530
|
+
let staleChunkFloor = 1;
|
|
2531
|
+
for (;;) {
|
|
2532
|
+
const chunkLimit = Math.min(INDEX_SCAN_RAW_CHUNK_ROWS, Math.max(limit - visible.length, staleChunkFloor));
|
|
2533
|
+
const chunkRange = {
|
|
2534
|
+
...physical,
|
|
2535
|
+
limit: chunkLimit
|
|
2536
|
+
};
|
|
2537
|
+
if (resumeAfter !== null) {
|
|
2538
|
+
if (reverse) {
|
|
2539
|
+
delete chunkRange.lte;
|
|
2540
|
+
chunkRange.lt = resumeAfter;
|
|
2541
|
+
} else {
|
|
2542
|
+
delete chunkRange.gte;
|
|
2543
|
+
chunkRange.gt = resumeAfter;
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
let rawRows;
|
|
2547
|
+
try {
|
|
2548
|
+
rawRows = this.readIndexKeyScan(txId, def.internalStore, chunkRange);
|
|
2549
|
+
} catch (error) {
|
|
2550
|
+
if (isNamedError(error, "StoreNotFoundError")) return {
|
|
2551
|
+
rows: [],
|
|
2552
|
+
cursor: null
|
|
2553
|
+
};
|
|
2554
|
+
throw error;
|
|
2555
|
+
}
|
|
2556
|
+
const visibleBefore = visible.length;
|
|
2557
|
+
let batchStart = 0;
|
|
2558
|
+
let batchEnd = 0;
|
|
2559
|
+
let batch = null;
|
|
2560
|
+
for (let index = 0; index < rawRows.length; index += 1) {
|
|
2561
|
+
if (index === batchEnd) {
|
|
2562
|
+
batchStart = index;
|
|
2563
|
+
batch = this.readIndexPrimaryValues(txId, def, rawRows, index, Math.min(rawRows.length, index + limit - visible.length), maxBytes - outputBytes);
|
|
2564
|
+
batchEnd = index + (batch?.entries.length ?? 1);
|
|
2565
|
+
}
|
|
2566
|
+
const row = rawRows[index];
|
|
2567
|
+
const resolved = batch ? await this.resolveVisibleIndexedValue(txId, def, row.key, batch.entries[index - batchStart], batch.values[index - batchStart], batch.trusted[index - batchStart]) : await this.resolveVisibleIndexedRow(txId, def, row.key);
|
|
2568
|
+
if (batch !== null) batch.values[index - batchStart] = null;
|
|
2569
|
+
if (!resolved) {
|
|
2570
|
+
resumeAfter = row.key;
|
|
2571
|
+
continue;
|
|
2572
|
+
}
|
|
2573
|
+
const nextBytes = outputBytes + 8 + resolved.key.byteLength + resolved.value.byteLength;
|
|
2574
|
+
if (nextBytes > maxBytes) {
|
|
2575
|
+
if (visible.length === 0) throw remoteError("ValueTooLargeError", "indexed scan row exceeds maxBytes");
|
|
2576
|
+
if (resumeAfter === null) throw remoteError("InternalError", "indexed scan lost its continuation key");
|
|
2577
|
+
return {
|
|
2578
|
+
rows: visible,
|
|
2579
|
+
cursor: resumeAfter.slice()
|
|
2580
|
+
};
|
|
2581
|
+
}
|
|
2582
|
+
visible.push(resolved);
|
|
2583
|
+
outputBytes = nextBytes;
|
|
2584
|
+
resumeAfter = row.key;
|
|
2585
|
+
if (visible.length >= limit) return {
|
|
2586
|
+
rows: visible,
|
|
2587
|
+
cursor: row.key.slice()
|
|
2588
|
+
};
|
|
2589
|
+
}
|
|
2590
|
+
if (rawRows.length < chunkLimit) return {
|
|
2591
|
+
rows: visible,
|
|
2592
|
+
cursor: null
|
|
2593
|
+
};
|
|
2594
|
+
staleChunkFloor = visible.length === visibleBefore ? Math.min(INDEX_SCAN_RAW_CHUNK_ROWS, chunkLimit * 2) : 1;
|
|
2595
|
+
}
|
|
2596
|
+
}
|
|
2597
|
+
readIndexPrimaryValues(txId, def, rows, start, end, remainingBytes) {
|
|
2598
|
+
if (this.txModes.get(txId) !== "readonly" || end - start <= 1 || typeof this.engine?.get_many_value_sizes !== "function" && typeof this.engine?.get_many_value_states !== "function") return null;
|
|
2599
|
+
let entries;
|
|
2600
|
+
let values;
|
|
2601
|
+
let trusted;
|
|
2602
|
+
let loaded;
|
|
2603
|
+
let selectedIndices;
|
|
2604
|
+
try {
|
|
2605
|
+
entries = rows.slice(start, end).map((row) => decodeIndexEntryKey(row.key));
|
|
2606
|
+
const packedKeys = this.packNativeKeys(entries.map((entry) => entry.primaryKey));
|
|
2607
|
+
let primaryStates = null;
|
|
2608
|
+
let indexStates = null;
|
|
2609
|
+
let sizes;
|
|
2610
|
+
if (typeof this.engine?.get_many_value_states === "function") {
|
|
2611
|
+
primaryStates = this.decodeNativeValueStates(normalizeWasmBytes(this.withEngine((engine) => engine.get_many_value_states(toWasmU64(txId), def.store, packedKeys))), entries.length);
|
|
2612
|
+
if (primaryStates === null) return null;
|
|
2613
|
+
sizes = Uint32Array.from(primaryStates, (state) => state.live ? state.maxValueBytes : MAX_PACKED_BYTES);
|
|
2614
|
+
if (primaryStates.some((state) => state.live && state.revisionKnown)) {
|
|
2615
|
+
const indexKeys = this.packNativeKeys(rows.slice(start, end).map((row) => row.key));
|
|
2616
|
+
indexStates = this.decodeNativeValueStates(normalizeWasmBytes(this.withEngine((engine) => engine.get_many_value_states(toWasmU64(txId), def.internalStore, indexKeys))), entries.length);
|
|
2617
|
+
if (indexStates === null) return null;
|
|
2618
|
+
}
|
|
2619
|
+
} else sizes = this.withEngine((engine) => engine.get_many_value_sizes(toWasmU64(txId), def.store, packedKeys));
|
|
2620
|
+
if (sizes.length !== entries.length) return null;
|
|
2621
|
+
let selected = 0;
|
|
2622
|
+
for (const entry of entries) {
|
|
2623
|
+
const size = sizes[selected];
|
|
2624
|
+
if (!Number.isSafeInteger(size) || size < 0 || size > MAX_PACKED_BYTES) return null;
|
|
2625
|
+
const required = size === MAX_PACKED_BYTES ? 0 : 8 + entry.primaryKey.byteLength + size;
|
|
2626
|
+
if (required > remainingBytes) break;
|
|
2627
|
+
remainingBytes -= required;
|
|
2628
|
+
selected += 1;
|
|
2629
|
+
}
|
|
2630
|
+
if (selected <= 1) return null;
|
|
2631
|
+
entries.length = selected;
|
|
2632
|
+
trusted = new Array(selected).fill(false);
|
|
2633
|
+
const selectedKeys = [];
|
|
2634
|
+
selectedIndices = [];
|
|
2635
|
+
values = new Array(selected).fill(null);
|
|
2636
|
+
for (let index = 0; index < selected; index += 1) {
|
|
2637
|
+
const primary = primaryStates?.[index];
|
|
2638
|
+
if (primary === void 0 || primary.live) {
|
|
2639
|
+
selectedKeys.push(entries[index].primaryKey);
|
|
2640
|
+
selectedIndices.push(index);
|
|
2641
|
+
}
|
|
2642
|
+
const proof = indexStates?.[index];
|
|
2643
|
+
trusted[index] = Boolean(primary?.live && primary.revisionKnown && proof?.live && proof.revisionKnown && primary.revisionEpoch === proof.revisionEpoch && primary.revisionOrdinal === proof.revisionOrdinal);
|
|
2644
|
+
}
|
|
2645
|
+
loaded = selectedKeys.length === 0 ? [] : this.withEngine((engine) => engine.get_many(toWasmU64(txId), def.store, selectedKeys));
|
|
2646
|
+
} catch {
|
|
2647
|
+
return null;
|
|
2648
|
+
}
|
|
2649
|
+
if (loaded.length !== selectedIndices.length) throw remoteError("InternalError", `get_many outcome count mismatch: ${loaded.length} != ${selectedIndices.length}`);
|
|
2650
|
+
for (let index = 0; index < loaded.length; index += 1) {
|
|
2651
|
+
values[selectedIndices[index]] = loaded[index];
|
|
2652
|
+
loaded[index] = null;
|
|
2653
|
+
}
|
|
2654
|
+
return {
|
|
2655
|
+
entries,
|
|
2656
|
+
values,
|
|
2657
|
+
trusted
|
|
2658
|
+
};
|
|
2659
|
+
}
|
|
2660
|
+
async resolveVisibleIndexedRow(txId, def, physicalKey) {
|
|
2661
|
+
const decoded = decodeIndexEntryKey(physicalKey);
|
|
2662
|
+
const state = this.inspectIndexedValue(txId, def, physicalKey, decoded.primaryKey);
|
|
2663
|
+
if (state === "missing") {
|
|
2664
|
+
this.cleanupStaleIndexRow(txId, def, physicalKey);
|
|
2665
|
+
return null;
|
|
2666
|
+
}
|
|
2667
|
+
const value = this.readRawValue(txId, def.store, decoded.primaryKey);
|
|
2668
|
+
return this.resolveVisibleIndexedValue(txId, def, physicalKey, decoded, value, state === "trusted");
|
|
2669
|
+
}
|
|
2670
|
+
async resolveVisibleIndexedValue(txId, def, physicalKey, decoded, rawValue, trusted = false) {
|
|
2671
|
+
if (rawValue === null) {
|
|
2672
|
+
this.cleanupStaleIndexRow(txId, def, physicalKey);
|
|
2673
|
+
return null;
|
|
2674
|
+
}
|
|
2675
|
+
const value = await this.decodeStoreValue(txId, def.store, normalizeWasmBytes(rawValue));
|
|
2676
|
+
if (!trusted) {
|
|
2677
|
+
const currentLogicalKey = extractLogicalIndexKey(def, value);
|
|
2678
|
+
if (currentLogicalKey === null || !bytesEqual(currentLogicalKey, decoded.logicalKey)) {
|
|
2679
|
+
this.cleanupStaleIndexRow(txId, def, physicalKey);
|
|
2680
|
+
return null;
|
|
2681
|
+
}
|
|
2682
|
+
}
|
|
2683
|
+
return {
|
|
2684
|
+
key: decoded.primaryKey,
|
|
2685
|
+
value
|
|
2686
|
+
};
|
|
2687
|
+
}
|
|
2688
|
+
cleanupStaleIndexRow(txId, def, physicalKey) {
|
|
2689
|
+
if (this.txModes.get(txId) !== "readwrite") return;
|
|
2690
|
+
try {
|
|
2691
|
+
this.withEngine((engine) => engine.delete(toWasmU64(txId), def.internalStore, physicalKey));
|
|
2692
|
+
} catch {}
|
|
2693
|
+
}
|
|
2694
|
+
async loadWasm() {
|
|
2695
|
+
if (!this.wasmReady) this.wasmReady = this.options.loadWasm ? this.options.loadWasm() : (async () => {
|
|
2696
|
+
const moduleUrl = new URL("/engine/moyodb_engine.js", import.meta.url).href;
|
|
2697
|
+
const wasmUrl = new URL("/engine/moyodb_engine_bg.wasm", import.meta.url).href;
|
|
2698
|
+
const wasmModule = await import(
|
|
2699
|
+
/* @vite-ignore */
|
|
2700
|
+
moduleUrl
|
|
2701
|
+
);
|
|
2702
|
+
await wasmModule.default({ module_or_path: wasmUrl });
|
|
2703
|
+
return wasmModule;
|
|
2704
|
+
})();
|
|
2705
|
+
return this.wasmReady;
|
|
2706
|
+
}
|
|
2707
|
+
withEngine(fn, options = {}) {
|
|
2708
|
+
if (this.maintenanceOperation && !options.allowDuringMaintenance) this.ensureNotInMaintenance();
|
|
2709
|
+
if (!this.engine) throw remoteError("InternalError", "engine is not open");
|
|
2710
|
+
try {
|
|
2711
|
+
return fn(this.engine);
|
|
2712
|
+
} catch (err) {
|
|
2713
|
+
throw remapError(err);
|
|
2714
|
+
}
|
|
2715
|
+
}
|
|
2716
|
+
ignoreNamedError(name, fn) {
|
|
2717
|
+
try {
|
|
2718
|
+
fn();
|
|
2719
|
+
} catch (error) {
|
|
2720
|
+
if (isNamedError(error, name)) return;
|
|
2721
|
+
throw error;
|
|
2722
|
+
}
|
|
2723
|
+
}
|
|
2724
|
+
clearTxCaches() {
|
|
2725
|
+
this.primaryCursors.clear();
|
|
2726
|
+
this.txChanges.clear();
|
|
2727
|
+
this.txModes.clear();
|
|
2728
|
+
this.txIndexSchemas.clear();
|
|
2729
|
+
this.txEnsuredRawStores.clear();
|
|
2730
|
+
this.txIndexSchemaChanged.clear();
|
|
2731
|
+
this.txStoreCompression.clear();
|
|
2732
|
+
}
|
|
2733
|
+
clearRuntimeCaches() {
|
|
2734
|
+
this.clearTxCaches();
|
|
2735
|
+
this.committedIndexes = null;
|
|
2736
|
+
this.committedStoreCompression = null;
|
|
2737
|
+
}
|
|
2738
|
+
async cleanupAfterFailedOpen(engine) {
|
|
2739
|
+
try {
|
|
2740
|
+
engine?.abandon();
|
|
2741
|
+
} catch {}
|
|
2742
|
+
this.engine = null;
|
|
2743
|
+
this.clearRuntimeCaches();
|
|
2744
|
+
this.events?.close();
|
|
2745
|
+
this.events = null;
|
|
2746
|
+
if (this.lease) try {
|
|
2747
|
+
await this.lease.release();
|
|
2748
|
+
} catch {}
|
|
2749
|
+
this.lease = null;
|
|
2750
|
+
this.disposePersistenceBridge();
|
|
2751
|
+
this.dbName = null;
|
|
2752
|
+
this.openOptions = null;
|
|
2753
|
+
this.maintenanceOperation = null;
|
|
2754
|
+
}
|
|
2755
|
+
disposePersistenceBridge() {
|
|
2756
|
+
this.persistenceBridge.close();
|
|
2757
|
+
}
|
|
2758
|
+
};
|
|
2759
|
+
function assertCapabilities() {
|
|
2760
|
+
if (typeof navigator.storage.getDirectory !== "function") throw remoteError("UnsupportedPlatformError", "navigator.storage.getDirectory is unavailable");
|
|
2761
|
+
if (typeof BroadcastChannel === "undefined") throw remoteError("UnsupportedPlatformError", "BroadcastChannel is unavailable");
|
|
2762
|
+
if (typeof FileSystemFileHandle === "undefined" || typeof FileSystemFileHandle.prototype.createSyncAccessHandle !== "function") throw remoteError("UnsupportedPlatformError", "createSyncAccessHandle is unavailable");
|
|
2763
|
+
}
|
|
2764
|
+
function assertSecureContext() {
|
|
2765
|
+
if (!self.isSecureContext) throw remoteError("UnsupportedPlatformError", "moyodb requires a secure context (HTTPS)");
|
|
2766
|
+
}
|
|
2767
|
+
function remoteError(code, message) {
|
|
2768
|
+
const error = new Error(message);
|
|
2769
|
+
error.name = code;
|
|
2770
|
+
Object.defineProperty(error, "code", {
|
|
2771
|
+
value: code,
|
|
2772
|
+
enumerable: true,
|
|
2773
|
+
configurable: true
|
|
2774
|
+
});
|
|
2775
|
+
return error;
|
|
2776
|
+
}
|
|
2777
|
+
function applyChangeFeedPolicy(engine, settings) {
|
|
2778
|
+
const current = engine.change_feed_policy();
|
|
2779
|
+
const currentRetain = fromOptionalWasmU64(current.retainTxids);
|
|
2780
|
+
if (current.enabled === settings.enabled && (!settings.enabled || currentRetain === settings.retainTxids)) return;
|
|
2781
|
+
const txId = engine.begin_tx("readwrite");
|
|
2782
|
+
try {
|
|
2783
|
+
engine.set_change_feed_policy(txId, {
|
|
2784
|
+
enabled: settings.enabled,
|
|
2785
|
+
retainTxids: settings.retainTxids
|
|
2786
|
+
});
|
|
2787
|
+
engine.commit_tx(txId);
|
|
2788
|
+
} catch (error) {
|
|
2789
|
+
try {
|
|
2790
|
+
engine.rollback_tx(txId);
|
|
2791
|
+
} catch {}
|
|
2792
|
+
throw remapError(error);
|
|
2793
|
+
}
|
|
2794
|
+
}
|
|
2795
|
+
/** Active generation, null for legacy storage, undefined if reading fails. */
|
|
2796
|
+
async function readActiveGenerationOrNull(wasm, dbName) {
|
|
2797
|
+
try {
|
|
2798
|
+
return await wasm.readActiveGeneration(dbName);
|
|
2799
|
+
} catch {
|
|
2800
|
+
return;
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
function normalizeEngineHealth(health) {
|
|
2804
|
+
if (health.state === "recoveryRequired") return {
|
|
2805
|
+
state: "recoveryRequired",
|
|
2806
|
+
reason: health.reason,
|
|
2807
|
+
pendingTxid: fromOptionalWasmU64(health.pendingTxid)
|
|
2808
|
+
};
|
|
2809
|
+
return { state: health.state };
|
|
2810
|
+
}
|
|
2811
|
+
function partialBooleanOutcomes(error) {
|
|
2812
|
+
if (!isRecord(error) || !Array.isArray(error.partial)) return [];
|
|
2813
|
+
return error.partial.filter((value) => typeof value === "boolean");
|
|
2814
|
+
}
|
|
2815
|
+
function partialBatchOutcomes(error) {
|
|
2816
|
+
if (!isRecord(error) || !Array.isArray(error.partial)) return [];
|
|
2817
|
+
return error.partial.filter((value) => {
|
|
2818
|
+
if (!isRecord(value) || typeof value.kind !== "string") return false;
|
|
2819
|
+
return value.kind === "put" && typeof value.baselineExists === "boolean" || value.kind === "delete" && typeof value.deleted === "boolean";
|
|
2820
|
+
});
|
|
2821
|
+
}
|
|
2822
|
+
function remapError(err) {
|
|
2823
|
+
if (err instanceof Error) return err;
|
|
2824
|
+
if (isRecord(err)) {
|
|
2825
|
+
const error = remoteError(typeof err.code === "string" ? err.code : typeof err.name === "string" ? err.name : "Error", typeof err.message === "string" ? err.message : "Unknown error");
|
|
2826
|
+
if ("partial" in err) Object.defineProperty(error, "partial", {
|
|
2827
|
+
value: err.partial,
|
|
2828
|
+
enumerable: true,
|
|
2829
|
+
configurable: true
|
|
2830
|
+
});
|
|
2831
|
+
return error;
|
|
2832
|
+
}
|
|
2833
|
+
return remoteError("Error", String(err));
|
|
2834
|
+
}
|
|
2835
|
+
//#endregion
|
|
2836
|
+
export { DbWorker, exposeWorkerApi };
|
|
2837
|
+
|
|
2838
|
+
//# sourceMappingURL=node-engine.js.map
|