@minnowdb/core 0.9.1 → 0.10.1
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/dist/engine/auto-store.d.ts +52 -0
- package/dist/engine/auto-store.js +157 -0
- package/dist/engine/buffered-writer.d.ts +2 -0
- package/dist/engine/buffered-writer.js +15 -2
- package/dist/engine/client-audit-harness.js +123 -0
- package/dist/engine/client.d.ts +55 -6
- package/dist/engine/client.js +176 -46
- package/dist/engine/database.d.ts +15 -1
- package/dist/engine/database.js +1276 -251
- package/dist/engine/errors.d.ts +61 -2
- package/dist/engine/errors.js +116 -3
- package/dist/engine/index.d.ts +1 -0
- package/dist/engine/index.js +2 -0
- package/dist/engine/live.d.ts +24 -1
- package/dist/engine/live.js +33 -9
- package/dist/engine/scope-write-set.js +36 -0
- package/dist/engine/worker-auto.d.ts +1 -0
- package/dist/engine/worker-auto.js +3 -0
- package/dist/engine/worker-host.d.ts +2 -1
- package/dist/engine/worker-host.js +19 -1
- package/dist/engine/worker-server.d.ts +53 -1
- package/dist/engine/worker-server.js +122 -19
- package/dist/engine/worker-store-auto.js +36 -0
- package/dist/engine/worker-store-opfs.js +3 -2
- package/dist/engine/write-coordinator.js +44 -2
- package/dist/storage/indexeddb-audit-helpers.js +269 -0
- package/dist/storage/indexeddb.js +599 -374
- package/dist/storage/opfs/coordination-helpers.js +54 -0
- package/dist/storage/opfs/index.d.ts +1 -1
- package/dist/storage/opfs/index.js +3 -2
- package/dist/storage/opfs/leader.js +243 -17
- package/dist/storage/opfs/power-loss-model.js +62 -0
- package/dist/storage/opfs/rpc.js +24 -43
- package/dist/storage/opfs/store.d.ts +32 -0
- package/dist/storage/opfs/store.js +531 -65
- package/dist/storage/toolkit/record-core.js +67 -38
- package/dist/storage/toolkit/wal.js +16 -0
- package/dist/storage/toolkit/wire.d.ts +1 -1
- package/dist/storage/toolkit/wire.js +4 -4
- package/dist/storage/types.d.ts +31 -10
- package/dist/storage/types.js +27 -16
- package/dist/testing/opfs-shim.js +14 -6
- package/dist/transactions/index.d.ts +19 -0
- package/dist/transactions/index.js +99 -25
- package/dist/worker-protocol/index.d.ts +50 -2
- package/dist/worker-protocol/index.js +106 -4
- package/package.json +7 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { MAX_SNAPSHOT_STREAM_CHUNK_BYTES } from "../storage/snapshot.js";
|
|
2
|
-
import {
|
|
2
|
+
import { MAX_DATABASE_RPC_IN_FLIGHT, parseRpcRequest, rpcEvent, rpcFailure, rpcResult, serializeError, MIN_WORKER_KEEPALIVE_INTERVAL_MS, WORKER_KEEPALIVE_INTERVAL_MS, workerErrorEvent, workerKeepaliveEvent } from "../worker-protocol/index.js";
|
|
3
3
|
import { MinnowDatabase } from "./database.js";
|
|
4
4
|
import { encodeQueryResult, encodeQueryRows } from "./result-wire.js";
|
|
5
5
|
import { deserializeSchema, serializeMigrationSteps } from "./schema-wire.js";
|
|
@@ -156,6 +156,40 @@ function abortError(message) {
|
|
|
156
156
|
error.name = "AbortError";
|
|
157
157
|
return error;
|
|
158
158
|
}
|
|
159
|
+
const errorSinks = /* @__PURE__ */ new WeakMap();
|
|
160
|
+
function workerErrorReporter(scope) {
|
|
161
|
+
const existing = errorSinks.get(scope);
|
|
162
|
+
if (existing !== void 0)
|
|
163
|
+
return existing;
|
|
164
|
+
const report = (kind, error, context) => {
|
|
165
|
+
try {
|
|
166
|
+
scope.postMessage(workerErrorEvent({ kind, context, error: serializeError(error) }));
|
|
167
|
+
} catch {
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
errorSinks.set(scope, report);
|
|
171
|
+
const listen = (type, listener) => {
|
|
172
|
+
try {
|
|
173
|
+
scope.addEventListener(type, listener);
|
|
174
|
+
} catch {
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
listen("error", (event) => {
|
|
178
|
+
const where = typeof event.filename === "string" && event.filename !== "" ? `${event.filename}:${String(event.lineno)}:${String(event.colno)}` : "worker global scope";
|
|
179
|
+
const error = event.error instanceof Error ? event.error : new Error(typeof event.message === "string" ? event.message : "Uncaught error");
|
|
180
|
+
report("uncaught", error, where);
|
|
181
|
+
event.preventDefault?.();
|
|
182
|
+
});
|
|
183
|
+
listen("unhandledrejection", (event) => {
|
|
184
|
+
report("unhandled-rejection", event.reason, "worker global scope");
|
|
185
|
+
event.preventDefault?.();
|
|
186
|
+
});
|
|
187
|
+
listen("messageerror", (event) => {
|
|
188
|
+
report("messageerror", new Error("A frame sent to the database worker could not be deserialized"), "worker inbound channel");
|
|
189
|
+
event.preventDefault?.();
|
|
190
|
+
});
|
|
191
|
+
return report;
|
|
192
|
+
}
|
|
159
193
|
const MAX_WORKER_HANDLES_PER_CONNECTION = 256;
|
|
160
194
|
const DEFAULT_WRITE_HANDLE_IDLE_TIMEOUT_MS = 3e4;
|
|
161
195
|
const MAX_TIMER_DELAY_MS = 2147483647;
|
|
@@ -174,10 +208,16 @@ class DatabaseRpcServer {
|
|
|
174
208
|
#inFlightRpcDrain;
|
|
175
209
|
#resolveInFlightRpcDrain;
|
|
176
210
|
#writeHandleIdleTimeoutMs;
|
|
211
|
+
#report;
|
|
212
|
+
#storeKind;
|
|
213
|
+
#keepaliveIntervalMs;
|
|
177
214
|
constructor(database, scope, options) {
|
|
178
215
|
this.database = database;
|
|
179
216
|
this.scope = scope;
|
|
180
217
|
this.options = options;
|
|
218
|
+
this.#report = workerErrorReporter(scope);
|
|
219
|
+
this.#storeKind = options.storeKind;
|
|
220
|
+
this.#keepaliveIntervalMs = keepaliveInterval(options.keepaliveIntervalMs);
|
|
181
221
|
this.#writeHandleIdleTimeoutMs = options.writeHandleIdleTimeoutMs ?? DEFAULT_WRITE_HANDLE_IDLE_TIMEOUT_MS;
|
|
182
222
|
if (!Number.isSafeInteger(this.#writeHandleIdleTimeoutMs) || this.#writeHandleIdleTimeoutMs <= 0 || this.#writeHandleIdleTimeoutMs > MAX_TIMER_DELAY_MS) {
|
|
183
223
|
throw new RangeError(`Worker write-handle idle timeout must be a positive whole number no greater than ${String(MAX_TIMER_DELAY_MS)}`);
|
|
@@ -185,7 +225,10 @@ class DatabaseRpcServer {
|
|
|
185
225
|
}
|
|
186
226
|
async handle(request) {
|
|
187
227
|
if (request.kind === "rpc-init") {
|
|
188
|
-
this.scope.postMessage(rpcResult(request.requestId, {
|
|
228
|
+
this.scope.postMessage(rpcResult(request.requestId, {
|
|
229
|
+
ready: true,
|
|
230
|
+
...this.#storeKind === void 0 ? {} : { store: this.#storeKind }
|
|
231
|
+
}));
|
|
189
232
|
return;
|
|
190
233
|
}
|
|
191
234
|
if (request.kind === "rpc-cancel") {
|
|
@@ -199,6 +242,13 @@ class DatabaseRpcServer {
|
|
|
199
242
|
}
|
|
200
243
|
if (!bypassLimit)
|
|
201
244
|
this.#inFlightRpcCount += 1;
|
|
245
|
+
const keepalive = setInterval(() => {
|
|
246
|
+
try {
|
|
247
|
+
this.scope.postMessage(workerKeepaliveEvent(request.requestId));
|
|
248
|
+
} catch {
|
|
249
|
+
}
|
|
250
|
+
}, this.#keepaliveIntervalMs);
|
|
251
|
+
unrefTimer(keepalive);
|
|
202
252
|
const abort = request.method === "query" || request.method === "execute" ? new AbortController() : void 0;
|
|
203
253
|
if (abort !== void 0)
|
|
204
254
|
this.#requestAborts.set(request.requestId, abort);
|
|
@@ -226,6 +276,7 @@ class DatabaseRpcServer {
|
|
|
226
276
|
} catch (error) {
|
|
227
277
|
this.scope.postMessage(rpcFailure(request.requestId, error));
|
|
228
278
|
} finally {
|
|
279
|
+
clearInterval(keepalive);
|
|
229
280
|
if (abort !== void 0)
|
|
230
281
|
this.#requestAborts.delete(request.requestId);
|
|
231
282
|
if (!bypassLimit)
|
|
@@ -487,7 +538,7 @@ class DatabaseRpcServer {
|
|
|
487
538
|
if (handle === void 0)
|
|
488
539
|
throw new Error(`Unknown handle: ${handleId}`);
|
|
489
540
|
if (handle.type === "write") {
|
|
490
|
-
this.#beginWriteHandleCall(handle);
|
|
541
|
+
await this.#beginWriteHandleCall(handle);
|
|
491
542
|
try {
|
|
492
543
|
return await this.#callWriteHandle(handleId, handle, method, args, context);
|
|
493
544
|
} finally {
|
|
@@ -796,12 +847,11 @@ class DatabaseRpcServer {
|
|
|
796
847
|
#releaseHandleId(id) {
|
|
797
848
|
this.#reservedHandleIds.delete(id);
|
|
798
849
|
}
|
|
799
|
-
#beginWriteHandleCall(handle) {
|
|
850
|
+
async #beginWriteHandleCall(handle) {
|
|
851
|
+
while (handle.activeCalls !== 0)
|
|
852
|
+
await handle.activeCallDone;
|
|
800
853
|
if (!handle.open)
|
|
801
854
|
throw new Error("Write handle is closed");
|
|
802
|
-
if (handle.activeCalls !== 0) {
|
|
803
|
-
throw new Error("Write handle already has a call in flight");
|
|
804
|
-
}
|
|
805
855
|
handle.activeCalls = 1;
|
|
806
856
|
handle.activeCallDone = new Promise((resolve) => {
|
|
807
857
|
handle.finishActiveCall = resolve;
|
|
@@ -831,7 +881,9 @@ class DatabaseRpcServer {
|
|
|
831
881
|
handle.idleTimer = void 0;
|
|
832
882
|
if (!handle.open || handle.activeCalls !== 0 || this.#handles.get(handleId) !== handle)
|
|
833
883
|
return;
|
|
834
|
-
void this.#settleWriteHandle(handleId, handle, false).catch(() =>
|
|
884
|
+
void this.#settleWriteHandle(handleId, handle, false).catch((error) => {
|
|
885
|
+
this.#report("maintenance", error, "idle write handle rollback");
|
|
886
|
+
});
|
|
835
887
|
}, this.#writeHandleIdleTimeoutMs);
|
|
836
888
|
unrefTimer(handle.idleTimer);
|
|
837
889
|
}
|
|
@@ -897,7 +949,8 @@ class DatabaseRpcServer {
|
|
|
897
949
|
handleCleanup.push(handle.task.catch(() => void 0));
|
|
898
950
|
} else if (handle.type === "live-set")
|
|
899
951
|
this.#closeLiveSet(handleId, handle);
|
|
900
|
-
} catch {
|
|
952
|
+
} catch (error) {
|
|
953
|
+
this.#report("maintenance", error, "dispose handle");
|
|
901
954
|
}
|
|
902
955
|
}
|
|
903
956
|
await Promise.allSettled(handleCleanup);
|
|
@@ -922,6 +975,7 @@ class DatabaseRpcServer {
|
|
|
922
975
|
}
|
|
923
976
|
function exposeDatabase(database, scope, options = {}) {
|
|
924
977
|
const server = new DatabaseRpcServer(database, scope, options);
|
|
978
|
+
const report = workerErrorReporter(scope);
|
|
925
979
|
scope.addEventListener("message", (event) => {
|
|
926
980
|
let request;
|
|
927
981
|
try {
|
|
@@ -930,15 +984,28 @@ function exposeDatabase(database, scope, options = {}) {
|
|
|
930
984
|
const requestId = requestIdOf(event.data);
|
|
931
985
|
if (requestId !== void 0)
|
|
932
986
|
scope.postMessage(rpcFailure(requestId, error));
|
|
987
|
+
else
|
|
988
|
+
report("messageerror", error, "unreadable request frame");
|
|
933
989
|
return;
|
|
934
990
|
}
|
|
935
|
-
if (request !== null)
|
|
936
|
-
void server.handle(request)
|
|
991
|
+
if (request !== null) {
|
|
992
|
+
void server.handle(request).catch((error) => {
|
|
993
|
+
report("uncaught", error, `${request.kind} dispatch`);
|
|
994
|
+
});
|
|
995
|
+
}
|
|
937
996
|
});
|
|
938
997
|
}
|
|
939
998
|
function attachWorkerHost(scope, createStore, options = {}) {
|
|
940
999
|
let initialized;
|
|
941
1000
|
let initFailure;
|
|
1001
|
+
const report = workerErrorReporter(scope);
|
|
1002
|
+
const storeOptions = {
|
|
1003
|
+
...options,
|
|
1004
|
+
onDiagnostic: (error, context) => {
|
|
1005
|
+
report("coordination", error, context);
|
|
1006
|
+
options.onDiagnostic?.(error, context);
|
|
1007
|
+
}
|
|
1008
|
+
};
|
|
942
1009
|
scope.addEventListener("message", (event) => {
|
|
943
1010
|
let request;
|
|
944
1011
|
try {
|
|
@@ -947,13 +1014,15 @@ function attachWorkerHost(scope, createStore, options = {}) {
|
|
|
947
1014
|
const requestId = requestIdOf(event.data);
|
|
948
1015
|
if (requestId !== void 0)
|
|
949
1016
|
scope.postMessage(rpcFailure(requestId, error));
|
|
1017
|
+
else
|
|
1018
|
+
report("messageerror", error, "unreadable request frame");
|
|
950
1019
|
return;
|
|
951
1020
|
}
|
|
952
1021
|
if (request === null)
|
|
953
1022
|
return;
|
|
954
1023
|
if (request.kind === "rpc-init" && initialized === void 0) {
|
|
955
1024
|
initFailure = void 0;
|
|
956
|
-
const attempt = createServer(scope, request.payload, createStore,
|
|
1025
|
+
const attempt = createServer(scope, request.payload, createStore, storeOptions, report);
|
|
957
1026
|
initialized = attempt;
|
|
958
1027
|
attempt.catch((error) => {
|
|
959
1028
|
if (initialized === attempt) {
|
|
@@ -969,25 +1038,54 @@ function attachWorkerHost(scope, createStore, options = {}) {
|
|
|
969
1038
|
return;
|
|
970
1039
|
}
|
|
971
1040
|
void pending.then((server) => server.handle(request)).catch((error) => {
|
|
972
|
-
|
|
1041
|
+
try {
|
|
1042
|
+
scope.postMessage(rpcFailure(request.requestId, error));
|
|
1043
|
+
} catch (postError) {
|
|
1044
|
+
report("uncaught", postError, `${request.kind} failure frame`);
|
|
1045
|
+
}
|
|
973
1046
|
});
|
|
974
1047
|
});
|
|
975
1048
|
}
|
|
976
|
-
async function createServer(scope, payload, createStore, options) {
|
|
977
|
-
const
|
|
978
|
-
const
|
|
1049
|
+
async function createServer(scope, payload, createStore, options, report) {
|
|
1050
|
+
const opened = await createStore(payload.store, options);
|
|
1051
|
+
const { store, kind } = isOpenedStore(opened) ? opened : { store: opened, kind: payload.store.kind === "auto" ? void 0 : payload.store.kind };
|
|
1052
|
+
let database;
|
|
1053
|
+
try {
|
|
1054
|
+
database = new MinnowDatabase(store, {
|
|
1055
|
+
...payload.options ?? {},
|
|
1056
|
+
onBackgroundError: (error, context) => report("maintenance", error, context)
|
|
1057
|
+
});
|
|
1058
|
+
} catch (error) {
|
|
1059
|
+
try {
|
|
1060
|
+
store.close();
|
|
1061
|
+
} catch (closeError) {
|
|
1062
|
+
report("maintenance", closeError, "store close after failed construction");
|
|
1063
|
+
}
|
|
1064
|
+
throw error;
|
|
1065
|
+
}
|
|
979
1066
|
return new DatabaseRpcServer(database, scope, {
|
|
980
1067
|
...payload.options?.transactionIdleTimeoutMs === void 0 ? {} : { writeHandleIdleTimeoutMs: payload.options.transactionIdleTimeoutMs },
|
|
981
1068
|
onDispose: () => store.close(),
|
|
982
1069
|
onVisibility: (visible) => {
|
|
983
1070
|
store.setForeground?.(visible);
|
|
984
|
-
}
|
|
1071
|
+
},
|
|
1072
|
+
...kind === void 0 ? {} : { storeKind: kind },
|
|
1073
|
+
...payload.keepaliveIntervalMs === void 0 ? {} : { keepaliveIntervalMs: payload.keepaliveIntervalMs }
|
|
985
1074
|
});
|
|
986
1075
|
}
|
|
1076
|
+
function keepaliveInterval(value) {
|
|
1077
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
1078
|
+
return WORKER_KEEPALIVE_INTERVAL_MS;
|
|
1079
|
+
return Math.min(WORKER_KEEPALIVE_INTERVAL_MS, Math.max(MIN_WORKER_KEEPALIVE_INTERVAL_MS, value));
|
|
1080
|
+
}
|
|
1081
|
+
function isOpenedStore(value) {
|
|
1082
|
+
return typeof value.kind === "string" && typeof value.store === "object";
|
|
1083
|
+
}
|
|
987
1084
|
const storeKindLabels = {
|
|
988
1085
|
indexeddb: "IndexedDB",
|
|
989
1086
|
opfs: "OPFS",
|
|
990
|
-
memory: "memory"
|
|
1087
|
+
memory: "memory",
|
|
1088
|
+
auto: "OPFS-or-IndexedDB"
|
|
991
1089
|
};
|
|
992
1090
|
function singleStoreFactory(kind, open) {
|
|
993
1091
|
return (descriptor, options) => {
|
|
@@ -997,6 +1095,9 @@ function singleStoreFactory(kind, open) {
|
|
|
997
1095
|
return open(descriptor, options);
|
|
998
1096
|
};
|
|
999
1097
|
}
|
|
1098
|
+
function unsupportedStoreKindError(bundled, requested) {
|
|
1099
|
+
return new Error(unsupportedStoreKindMessage(bundled, requested));
|
|
1100
|
+
}
|
|
1000
1101
|
function unsupportedStoreKindMessage(bundled, requested) {
|
|
1001
1102
|
const requestedLabel = Object.hasOwn(storeKindLabels, requested) ? storeKindLabels[requested] : void 0;
|
|
1002
1103
|
const requestedEntry = requestedLabel === void 0 ? "" : ` or "@minnowdb/core/worker/${requested}" for the ${requestedLabel} store`;
|
|
@@ -1018,5 +1119,7 @@ export {
|
|
|
1018
1119
|
MAX_WORKER_HANDLES_PER_CONNECTION,
|
|
1019
1120
|
attachWorkerHost,
|
|
1020
1121
|
exposeDatabase,
|
|
1021
|
-
singleStoreFactory
|
|
1122
|
+
singleStoreFactory,
|
|
1123
|
+
unsupportedStoreKindError,
|
|
1124
|
+
workerErrorReporter
|
|
1022
1125
|
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { IndexedDbBlockStore } from "../storage/indexeddb.js";
|
|
2
|
+
import { OpfsBlockStore, opfsDatabaseExists } from "../storage/opfs/index.js";
|
|
3
|
+
import { openAutoStore } from "./auto-store.js";
|
|
4
|
+
import { unsupportedStoreKindError } from "./worker-server.js";
|
|
5
|
+
const autoWorkerStore = async (descriptor, options) => {
|
|
6
|
+
const diagnostic = options.onDiagnostic === void 0 ? {} : { onDiagnostic: options.onDiagnostic };
|
|
7
|
+
switch (descriptor.kind) {
|
|
8
|
+
case "opfs":
|
|
9
|
+
return OpfsBlockStore.open({
|
|
10
|
+
name: descriptor.name,
|
|
11
|
+
...descriptor.durability === void 0 ? {} : { durability: descriptor.durability },
|
|
12
|
+
...diagnostic
|
|
13
|
+
});
|
|
14
|
+
case "indexeddb":
|
|
15
|
+
return IndexedDbBlockStore.open({
|
|
16
|
+
name: descriptor.name,
|
|
17
|
+
...descriptor.durability === void 0 ? {} : { durability: descriptor.durability },
|
|
18
|
+
...descriptor.uniqueKeyCacheBytes === void 0 ? {} : { uniqueKeyCacheBytes: descriptor.uniqueKeyCacheBytes }
|
|
19
|
+
});
|
|
20
|
+
case "auto":
|
|
21
|
+
return openAutoStore(descriptor.name, (kind) => kind === "opfs" ? OpfsBlockStore.open({
|
|
22
|
+
name: descriptor.name,
|
|
23
|
+
...descriptor.opfs?.durability === void 0 ? {} : { durability: descriptor.opfs.durability },
|
|
24
|
+
...diagnostic
|
|
25
|
+
}) : IndexedDbBlockStore.open({
|
|
26
|
+
name: descriptor.name,
|
|
27
|
+
...descriptor.indexeddb?.durability === void 0 ? {} : { durability: descriptor.indexeddb.durability },
|
|
28
|
+
...descriptor.indexeddb?.uniqueKeyCacheBytes === void 0 ? {} : { uniqueKeyCacheBytes: descriptor.indexeddb.uniqueKeyCacheBytes }
|
|
29
|
+
}), { opfsDatabaseExists: (name) => opfsDatabaseExists({ name }) });
|
|
30
|
+
default:
|
|
31
|
+
throw unsupportedStoreKindError("auto", descriptor.kind);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
export {
|
|
35
|
+
autoWorkerStore
|
|
36
|
+
};
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { OpfsBlockStore } from "../storage/opfs/index.js";
|
|
2
2
|
import { singleStoreFactory } from "./worker-server.js";
|
|
3
|
-
const opfsWorkerStore = singleStoreFactory("opfs", (descriptor) => OpfsBlockStore.open({
|
|
3
|
+
const opfsWorkerStore = singleStoreFactory("opfs", (descriptor, options) => OpfsBlockStore.open({
|
|
4
4
|
name: descriptor.name,
|
|
5
|
-
...descriptor.durability === void 0 ? {} : { durability: descriptor.durability }
|
|
5
|
+
...descriptor.durability === void 0 ? {} : { durability: descriptor.durability },
|
|
6
|
+
...options.onDiagnostic === void 0 ? {} : { onDiagnostic: options.onDiagnostic }
|
|
6
7
|
}));
|
|
7
8
|
export {
|
|
8
9
|
opfsWorkerStore
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
const anonymous = /* @__PURE__ */ new WeakMap();
|
|
2
2
|
const named = /* @__PURE__ */ new Map();
|
|
3
|
-
|
|
3
|
+
const bypassing = /* @__PURE__ */ new Set();
|
|
4
|
+
const NOT_GRANTED = /* @__PURE__ */ Symbol("write admission lock not granted");
|
|
5
|
+
const WRITE_ADMISSION_WAIT_MS = 1e4;
|
|
6
|
+
function _resetWriteAdmissionForTests() {
|
|
7
|
+
bypassing.clear();
|
|
8
|
+
}
|
|
9
|
+
async function coordinateWrite(store, run, signal, options = {}) {
|
|
4
10
|
signal.throwIfAborted();
|
|
5
11
|
const name = store.liveQueryChannelName;
|
|
6
12
|
let queue = name === void 0 ? anonymous.get(store) : named.get(name);
|
|
@@ -19,9 +25,43 @@ async function coordinateWrite(store, run, signal) {
|
|
|
19
25
|
admitted = true;
|
|
20
26
|
return run();
|
|
21
27
|
};
|
|
28
|
+
const admissionWaitMs = options.admissionWaitMs ?? WRITE_ADMISSION_WAIT_MS;
|
|
22
29
|
const operation = queue.tail.then(async () => {
|
|
23
30
|
signal.throwIfAborted();
|
|
24
|
-
|
|
31
|
+
if (name === void 0 || locks === void 0)
|
|
32
|
+
return enter();
|
|
33
|
+
const lockName = `minnowdb-write:${name}`;
|
|
34
|
+
if (bypassing.has(name)) {
|
|
35
|
+
const result = await locks.request(lockName, { ifAvailable: true }, async (lock) => {
|
|
36
|
+
if (lock === null)
|
|
37
|
+
return NOT_GRANTED;
|
|
38
|
+
bypassing.delete(name);
|
|
39
|
+
return enter();
|
|
40
|
+
});
|
|
41
|
+
if (result !== NOT_GRANTED)
|
|
42
|
+
return result;
|
|
43
|
+
signal.throwIfAborted();
|
|
44
|
+
return await enter();
|
|
45
|
+
}
|
|
46
|
+
const startedAt = Date.now();
|
|
47
|
+
const wait = { ranOut: false };
|
|
48
|
+
const waitTimer = setTimeout(() => {
|
|
49
|
+
wait.ranOut = true;
|
|
50
|
+
lockController.abort(new Error("Write admission wait ran out"));
|
|
51
|
+
}, admissionWaitMs);
|
|
52
|
+
waitTimer.unref?.();
|
|
53
|
+
try {
|
|
54
|
+
return await locks.request(lockName, { signal: lockController.signal }, enter);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
if (!wait.ranOut || admitted)
|
|
57
|
+
throw error;
|
|
58
|
+
signal.throwIfAborted();
|
|
59
|
+
bypassing.add(name);
|
|
60
|
+
options.onAdmissionWaitExceeded?.(Date.now() - startedAt);
|
|
61
|
+
return await enter();
|
|
62
|
+
} finally {
|
|
63
|
+
clearTimeout(waitTimer);
|
|
64
|
+
}
|
|
25
65
|
});
|
|
26
66
|
const settled = operation.then(() => void 0, () => void 0);
|
|
27
67
|
queue.tail = settled;
|
|
@@ -50,5 +90,7 @@ async function coordinateWrite(store, run, signal) {
|
|
|
50
90
|
}
|
|
51
91
|
}
|
|
52
92
|
export {
|
|
93
|
+
WRITE_ADMISSION_WAIT_MS,
|
|
94
|
+
_resetWriteAdmissionForTests,
|
|
53
95
|
coordinateWrite
|
|
54
96
|
};
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { IndexedDbBlockStore } from "./indexeddb.js";
|
|
2
|
+
const NOW = "2026-09-12T12:00:00.000Z";
|
|
3
|
+
async function openStore(indexedDB, name = crypto.randomUUID(), durability) {
|
|
4
|
+
return IndexedDbBlockStore.open({ name, indexedDB, ...durability ? { durability } : {} });
|
|
5
|
+
}
|
|
6
|
+
const EVENTS_TABLE = {
|
|
7
|
+
managed: false,
|
|
8
|
+
id: "events",
|
|
9
|
+
name: "events",
|
|
10
|
+
columns: [{ id: "value", name: "value", type: "number", nullable: false }],
|
|
11
|
+
revision: 0,
|
|
12
|
+
createdAt: NOW
|
|
13
|
+
};
|
|
14
|
+
function activeTransaction(id, snapshotVersion) {
|
|
15
|
+
return {
|
|
16
|
+
id,
|
|
17
|
+
ownerId: `owner-${id}`,
|
|
18
|
+
expiresAt: "2026-09-12T12:30:00.000Z",
|
|
19
|
+
snapshotVersion,
|
|
20
|
+
pendingBlockIds: [],
|
|
21
|
+
pendingSegmentIds: [],
|
|
22
|
+
status: "active",
|
|
23
|
+
revision: 0,
|
|
24
|
+
startedAt: NOW,
|
|
25
|
+
updatedAt: NOW,
|
|
26
|
+
committedVersion: null
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function segment(id, transactionId, blockId, commitOrdinal = 0, rowIdStart = 1n) {
|
|
30
|
+
return {
|
|
31
|
+
id,
|
|
32
|
+
tableId: "events",
|
|
33
|
+
transactionId,
|
|
34
|
+
rowCount: 1,
|
|
35
|
+
rowIdStart,
|
|
36
|
+
rowIdEndExclusive: rowIdStart + 1n,
|
|
37
|
+
columnBlockIds: { value: [blockId] },
|
|
38
|
+
kind: "insert",
|
|
39
|
+
level: 0,
|
|
40
|
+
logicalOrder: 0,
|
|
41
|
+
commitOrdinal,
|
|
42
|
+
rowIdSpans: [],
|
|
43
|
+
createdAt: NOW
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
async function rawOpen(indexedDB, name) {
|
|
47
|
+
return new Promise((resolve, reject) => {
|
|
48
|
+
const request = indexedDB.open(name);
|
|
49
|
+
request.onsuccess = () => resolve(request.result);
|
|
50
|
+
request.onerror = () => reject(request.error ?? new Error("raw IndexedDB open failed"));
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
async function readRawValue(indexedDB, name, storeName, key) {
|
|
54
|
+
const database = await rawOpen(indexedDB, name);
|
|
55
|
+
try {
|
|
56
|
+
const transaction = database.transaction(storeName, "readonly");
|
|
57
|
+
const value = await new Promise((resolve, reject) => {
|
|
58
|
+
const request = transaction.objectStore(storeName).get(key);
|
|
59
|
+
request.onsuccess = () => resolve(request.result);
|
|
60
|
+
request.onerror = () => reject(request.error ?? new Error("raw IndexedDB read failed"));
|
|
61
|
+
});
|
|
62
|
+
await new Promise((resolve, reject) => {
|
|
63
|
+
transaction.oncomplete = () => resolve();
|
|
64
|
+
transaction.onabort = () => reject(transaction.error ?? new Error("raw read aborted"));
|
|
65
|
+
});
|
|
66
|
+
return value;
|
|
67
|
+
} finally {
|
|
68
|
+
database.close();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async function readRawKeys(indexedDB, name, storeName) {
|
|
72
|
+
const database = await rawOpen(indexedDB, name);
|
|
73
|
+
try {
|
|
74
|
+
const transaction = database.transaction(storeName, "readonly");
|
|
75
|
+
const keys = await new Promise((resolve, reject) => {
|
|
76
|
+
const request = transaction.objectStore(storeName).getAllKeys();
|
|
77
|
+
request.onsuccess = () => resolve(request.result);
|
|
78
|
+
request.onerror = () => reject(request.error ?? new Error("raw IndexedDB read failed"));
|
|
79
|
+
});
|
|
80
|
+
await new Promise((resolve, reject) => {
|
|
81
|
+
transaction.oncomplete = () => resolve();
|
|
82
|
+
transaction.onabort = () => reject(transaction.error ?? new Error("raw read aborted"));
|
|
83
|
+
});
|
|
84
|
+
return keys;
|
|
85
|
+
} finally {
|
|
86
|
+
database.close();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function instrumentFactory(indexedDB) {
|
|
90
|
+
const state = {
|
|
91
|
+
factory: indexedDB,
|
|
92
|
+
transactions: [],
|
|
93
|
+
requestCounts: /* @__PURE__ */ new Map(),
|
|
94
|
+
setHook(hook) {
|
|
95
|
+
currentHook = hook;
|
|
96
|
+
},
|
|
97
|
+
reset() {
|
|
98
|
+
state.transactions.length = 0;
|
|
99
|
+
state.requestCounts.clear();
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
let currentHook;
|
|
103
|
+
const wrappedScanners = /* @__PURE__ */ new WeakSet();
|
|
104
|
+
const countScans = (target, label) => {
|
|
105
|
+
if (wrappedScanners.has(target))
|
|
106
|
+
return;
|
|
107
|
+
wrappedScanners.add(target);
|
|
108
|
+
for (const method of [
|
|
109
|
+
"openCursor",
|
|
110
|
+
"openKeyCursor",
|
|
111
|
+
"getAll",
|
|
112
|
+
"getAllKeys",
|
|
113
|
+
"count"
|
|
114
|
+
]) {
|
|
115
|
+
const original = target[method].bind(target);
|
|
116
|
+
Object.defineProperty(target, method, {
|
|
117
|
+
configurable: true,
|
|
118
|
+
value: (...args) => {
|
|
119
|
+
const key = `${label}:${method}`;
|
|
120
|
+
state.requestCounts.set(key, (state.requestCounts.get(key) ?? 0) + 1);
|
|
121
|
+
const request = original(...args);
|
|
122
|
+
if (method === "openCursor" || method === "openKeyCursor") {
|
|
123
|
+
request.addEventListener("success", () => {
|
|
124
|
+
const stepKey = `${label}:cursor-steps`;
|
|
125
|
+
if (request.result !== null && request.result !== void 0) {
|
|
126
|
+
state.requestCounts.set(stepKey, (state.requestCounts.get(stepKey) ?? 0) + 1);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
return request;
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
const wrappedStores = /* @__PURE__ */ new WeakSet();
|
|
136
|
+
const originalOpen = indexedDB.open.bind(indexedDB);
|
|
137
|
+
Object.defineProperty(indexedDB, "open", {
|
|
138
|
+
configurable: true,
|
|
139
|
+
value: (name, version) => {
|
|
140
|
+
const request = version === void 0 ? originalOpen(name) : originalOpen(name, version);
|
|
141
|
+
request.addEventListener("success", () => {
|
|
142
|
+
const database = request.result;
|
|
143
|
+
const originalTransaction = database.transaction.bind(database);
|
|
144
|
+
Object.defineProperty(database, "transaction", {
|
|
145
|
+
configurable: true,
|
|
146
|
+
value: (stores, mode, options) => {
|
|
147
|
+
const transaction = originalTransaction(stores, mode, options);
|
|
148
|
+
const entry = {
|
|
149
|
+
transaction,
|
|
150
|
+
stores: typeof stores === "string" ? [stores] : [...stores],
|
|
151
|
+
mode: mode ?? "readonly",
|
|
152
|
+
options,
|
|
153
|
+
completed: false,
|
|
154
|
+
aborted: false
|
|
155
|
+
};
|
|
156
|
+
transaction.addEventListener("complete", () => {
|
|
157
|
+
entry.completed = true;
|
|
158
|
+
});
|
|
159
|
+
transaction.addEventListener("abort", () => {
|
|
160
|
+
entry.aborted = true;
|
|
161
|
+
});
|
|
162
|
+
state.transactions.push(entry);
|
|
163
|
+
const originalObjectStore = transaction.objectStore.bind(transaction);
|
|
164
|
+
Object.defineProperty(transaction, "objectStore", {
|
|
165
|
+
configurable: true,
|
|
166
|
+
value: (storeName) => {
|
|
167
|
+
const store = originalObjectStore(storeName);
|
|
168
|
+
if (wrappedStores.has(store))
|
|
169
|
+
return store;
|
|
170
|
+
wrappedStores.add(store);
|
|
171
|
+
countScans(store, storeName);
|
|
172
|
+
const originalIndex = store.index.bind(store);
|
|
173
|
+
Object.defineProperty(store, "index", {
|
|
174
|
+
configurable: true,
|
|
175
|
+
value: (indexName) => {
|
|
176
|
+
const index = originalIndex(indexName);
|
|
177
|
+
countScans(index, `${storeName}.${indexName}`);
|
|
178
|
+
return index;
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
for (const method of ["put", "add", "delete", "get", "getKey"]) {
|
|
182
|
+
const original = store[method].bind(store);
|
|
183
|
+
Object.defineProperty(store, method, {
|
|
184
|
+
configurable: true,
|
|
185
|
+
value: (...args) => {
|
|
186
|
+
state.requestCounts.set(storeName, (state.requestCounts.get(storeName) ?? 0) + 1);
|
|
187
|
+
const [first, second] = args;
|
|
188
|
+
const isWrite = method === "put" || method === "add";
|
|
189
|
+
const decision = currentHook?.({
|
|
190
|
+
transaction,
|
|
191
|
+
storeName,
|
|
192
|
+
method,
|
|
193
|
+
key: isWrite ? second : first,
|
|
194
|
+
value: isWrite ? first : void 0
|
|
195
|
+
});
|
|
196
|
+
if (decision === "throw-quota") {
|
|
197
|
+
const failing = original(...args);
|
|
198
|
+
const queue = transaction._requests;
|
|
199
|
+
const entry2 = queue.find((candidate) => candidate.request === failing);
|
|
200
|
+
if (entry2 === void 0)
|
|
201
|
+
throw new Error("request not queued");
|
|
202
|
+
entry2.operation = () => {
|
|
203
|
+
throw new DOMException("The quota has been exceeded.", "QuotaExceededError");
|
|
204
|
+
};
|
|
205
|
+
return failing;
|
|
206
|
+
}
|
|
207
|
+
const result = original(...args);
|
|
208
|
+
if (decision === "abort") {
|
|
209
|
+
try {
|
|
210
|
+
transaction.abort();
|
|
211
|
+
} catch {
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return result;
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
return store;
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
return transaction;
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
return request;
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
return state;
|
|
229
|
+
}
|
|
230
|
+
async function stageBlocks(store, record, ids) {
|
|
231
|
+
let current = record;
|
|
232
|
+
for (let start = 0; start < ids.length; start += 64) {
|
|
233
|
+
const slice = ids.slice(start, start + 64);
|
|
234
|
+
current = await store.stageTransactionArtifacts({
|
|
235
|
+
transactionId: record.id,
|
|
236
|
+
expectedRevision: current.revision,
|
|
237
|
+
blocks: slice.map((id) => ({ id, bytes: Uint8Array.of(1) })),
|
|
238
|
+
segments: [],
|
|
239
|
+
updatedAt: NOW
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
return current;
|
|
243
|
+
}
|
|
244
|
+
async function stageSegments(store, record, ids, blockId) {
|
|
245
|
+
let current = record;
|
|
246
|
+
for (let start = 0; start < ids.length; start += 64) {
|
|
247
|
+
const slice = ids.slice(start, start + 64);
|
|
248
|
+
current = await store.stageTransactionArtifacts({
|
|
249
|
+
transactionId: record.id,
|
|
250
|
+
expectedRevision: current.revision,
|
|
251
|
+
blocks: [],
|
|
252
|
+
segments: slice.map((id, offset) => segment(id, record.id, blockId, current.pendingSegmentIds.length + offset, BigInt(current.pendingSegmentIds.length + offset + 1))),
|
|
253
|
+
updatedAt: NOW
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
return current;
|
|
257
|
+
}
|
|
258
|
+
export {
|
|
259
|
+
EVENTS_TABLE,
|
|
260
|
+
NOW,
|
|
261
|
+
activeTransaction,
|
|
262
|
+
instrumentFactory,
|
|
263
|
+
openStore,
|
|
264
|
+
readRawKeys,
|
|
265
|
+
readRawValue,
|
|
266
|
+
segment,
|
|
267
|
+
stageBlocks,
|
|
268
|
+
stageSegments
|
|
269
|
+
};
|