@minnowdb/core 0.9.0 → 0.10.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/dist/engine/auto-store.d.ts +40 -0
- package/dist/engine/auto-store.js +115 -0
- package/dist/engine/buffered-writer.d.ts +2 -0
- package/dist/engine/buffered-writer.js +15 -2
- package/dist/engine/client.d.ts +55 -6
- package/dist/engine/client.js +155 -40
- package/dist/engine/database.d.ts +15 -1
- package/dist/engine/database.js +1085 -227
- 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 +17 -1
- package/dist/engine/worker-server.d.ts +53 -1
- package/dist/engine/worker-server.js +118 -14
- package/dist/engine/worker-store-auto.js +36 -0
- package/dist/engine/worker-store-opfs.js +3 -2
- package/dist/engine/write-coordinator.js +24 -2
- package/dist/storage/indexeddb.js +494 -207
- package/dist/storage/opfs/leader.js +201 -14
- package/dist/storage/opfs/rpc.js +23 -43
- package/dist/storage/opfs/store.d.ts +20 -0
- package/dist/storage/opfs/store.js +501 -70
- package/dist/storage/toolkit/record-core.js +67 -38
- package/dist/storage/toolkit/wire.d.ts +1 -1
- package/dist/storage/toolkit/wire.js +1 -1
- package/dist/storage/types.d.ts +29 -8
- package/dist/storage/types.js +27 -16
- package/dist/testing/opfs-shim.js +14 -6
- package/dist/transactions/index.d.ts +12 -0
- package/dist/transactions/index.js +83 -23
- 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)
|
|
@@ -831,7 +882,9 @@ class DatabaseRpcServer {
|
|
|
831
882
|
handle.idleTimer = void 0;
|
|
832
883
|
if (!handle.open || handle.activeCalls !== 0 || this.#handles.get(handleId) !== handle)
|
|
833
884
|
return;
|
|
834
|
-
void this.#settleWriteHandle(handleId, handle, false).catch(() =>
|
|
885
|
+
void this.#settleWriteHandle(handleId, handle, false).catch((error) => {
|
|
886
|
+
this.#report("maintenance", error, "idle write handle rollback");
|
|
887
|
+
});
|
|
835
888
|
}, this.#writeHandleIdleTimeoutMs);
|
|
836
889
|
unrefTimer(handle.idleTimer);
|
|
837
890
|
}
|
|
@@ -897,7 +950,8 @@ class DatabaseRpcServer {
|
|
|
897
950
|
handleCleanup.push(handle.task.catch(() => void 0));
|
|
898
951
|
} else if (handle.type === "live-set")
|
|
899
952
|
this.#closeLiveSet(handleId, handle);
|
|
900
|
-
} catch {
|
|
953
|
+
} catch (error) {
|
|
954
|
+
this.#report("maintenance", error, "dispose handle");
|
|
901
955
|
}
|
|
902
956
|
}
|
|
903
957
|
await Promise.allSettled(handleCleanup);
|
|
@@ -922,6 +976,7 @@ class DatabaseRpcServer {
|
|
|
922
976
|
}
|
|
923
977
|
function exposeDatabase(database, scope, options = {}) {
|
|
924
978
|
const server = new DatabaseRpcServer(database, scope, options);
|
|
979
|
+
const report = workerErrorReporter(scope);
|
|
925
980
|
scope.addEventListener("message", (event) => {
|
|
926
981
|
let request;
|
|
927
982
|
try {
|
|
@@ -930,15 +985,28 @@ function exposeDatabase(database, scope, options = {}) {
|
|
|
930
985
|
const requestId = requestIdOf(event.data);
|
|
931
986
|
if (requestId !== void 0)
|
|
932
987
|
scope.postMessage(rpcFailure(requestId, error));
|
|
988
|
+
else
|
|
989
|
+
report("messageerror", error, "unreadable request frame");
|
|
933
990
|
return;
|
|
934
991
|
}
|
|
935
|
-
if (request !== null)
|
|
936
|
-
void server.handle(request)
|
|
992
|
+
if (request !== null) {
|
|
993
|
+
void server.handle(request).catch((error) => {
|
|
994
|
+
report("uncaught", error, `${request.kind} dispatch`);
|
|
995
|
+
});
|
|
996
|
+
}
|
|
937
997
|
});
|
|
938
998
|
}
|
|
939
999
|
function attachWorkerHost(scope, createStore, options = {}) {
|
|
940
1000
|
let initialized;
|
|
941
1001
|
let initFailure;
|
|
1002
|
+
const report = workerErrorReporter(scope);
|
|
1003
|
+
const storeOptions = {
|
|
1004
|
+
...options,
|
|
1005
|
+
onDiagnostic: (error, context) => {
|
|
1006
|
+
report("coordination", error, context);
|
|
1007
|
+
options.onDiagnostic?.(error, context);
|
|
1008
|
+
}
|
|
1009
|
+
};
|
|
942
1010
|
scope.addEventListener("message", (event) => {
|
|
943
1011
|
let request;
|
|
944
1012
|
try {
|
|
@@ -947,13 +1015,15 @@ function attachWorkerHost(scope, createStore, options = {}) {
|
|
|
947
1015
|
const requestId = requestIdOf(event.data);
|
|
948
1016
|
if (requestId !== void 0)
|
|
949
1017
|
scope.postMessage(rpcFailure(requestId, error));
|
|
1018
|
+
else
|
|
1019
|
+
report("messageerror", error, "unreadable request frame");
|
|
950
1020
|
return;
|
|
951
1021
|
}
|
|
952
1022
|
if (request === null)
|
|
953
1023
|
return;
|
|
954
1024
|
if (request.kind === "rpc-init" && initialized === void 0) {
|
|
955
1025
|
initFailure = void 0;
|
|
956
|
-
const attempt = createServer(scope, request.payload, createStore,
|
|
1026
|
+
const attempt = createServer(scope, request.payload, createStore, storeOptions, report);
|
|
957
1027
|
initialized = attempt;
|
|
958
1028
|
attempt.catch((error) => {
|
|
959
1029
|
if (initialized === attempt) {
|
|
@@ -969,25 +1039,54 @@ function attachWorkerHost(scope, createStore, options = {}) {
|
|
|
969
1039
|
return;
|
|
970
1040
|
}
|
|
971
1041
|
void pending.then((server) => server.handle(request)).catch((error) => {
|
|
972
|
-
|
|
1042
|
+
try {
|
|
1043
|
+
scope.postMessage(rpcFailure(request.requestId, error));
|
|
1044
|
+
} catch (postError) {
|
|
1045
|
+
report("uncaught", postError, `${request.kind} failure frame`);
|
|
1046
|
+
}
|
|
973
1047
|
});
|
|
974
1048
|
});
|
|
975
1049
|
}
|
|
976
|
-
async function createServer(scope, payload, createStore, options) {
|
|
977
|
-
const
|
|
978
|
-
const
|
|
1050
|
+
async function createServer(scope, payload, createStore, options, report) {
|
|
1051
|
+
const opened = await createStore(payload.store, options);
|
|
1052
|
+
const { store, kind } = isOpenedStore(opened) ? opened : { store: opened, kind: payload.store.kind === "auto" ? void 0 : payload.store.kind };
|
|
1053
|
+
let database;
|
|
1054
|
+
try {
|
|
1055
|
+
database = new MinnowDatabase(store, {
|
|
1056
|
+
...payload.options ?? {},
|
|
1057
|
+
onBackgroundError: (error, context) => report("maintenance", error, context)
|
|
1058
|
+
});
|
|
1059
|
+
} catch (error) {
|
|
1060
|
+
try {
|
|
1061
|
+
store.close();
|
|
1062
|
+
} catch (closeError) {
|
|
1063
|
+
report("maintenance", closeError, "store close after failed construction");
|
|
1064
|
+
}
|
|
1065
|
+
throw error;
|
|
1066
|
+
}
|
|
979
1067
|
return new DatabaseRpcServer(database, scope, {
|
|
980
1068
|
...payload.options?.transactionIdleTimeoutMs === void 0 ? {} : { writeHandleIdleTimeoutMs: payload.options.transactionIdleTimeoutMs },
|
|
981
1069
|
onDispose: () => store.close(),
|
|
982
1070
|
onVisibility: (visible) => {
|
|
983
1071
|
store.setForeground?.(visible);
|
|
984
|
-
}
|
|
1072
|
+
},
|
|
1073
|
+
...kind === void 0 ? {} : { storeKind: kind },
|
|
1074
|
+
...payload.keepaliveIntervalMs === void 0 ? {} : { keepaliveIntervalMs: payload.keepaliveIntervalMs }
|
|
985
1075
|
});
|
|
986
1076
|
}
|
|
1077
|
+
function keepaliveInterval(value) {
|
|
1078
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
1079
|
+
return WORKER_KEEPALIVE_INTERVAL_MS;
|
|
1080
|
+
return Math.min(WORKER_KEEPALIVE_INTERVAL_MS, Math.max(MIN_WORKER_KEEPALIVE_INTERVAL_MS, value));
|
|
1081
|
+
}
|
|
1082
|
+
function isOpenedStore(value) {
|
|
1083
|
+
return typeof value.kind === "string" && typeof value.store === "object";
|
|
1084
|
+
}
|
|
987
1085
|
const storeKindLabels = {
|
|
988
1086
|
indexeddb: "IndexedDB",
|
|
989
1087
|
opfs: "OPFS",
|
|
990
|
-
memory: "memory"
|
|
1088
|
+
memory: "memory",
|
|
1089
|
+
auto: "OPFS-or-IndexedDB"
|
|
991
1090
|
};
|
|
992
1091
|
function singleStoreFactory(kind, open) {
|
|
993
1092
|
return (descriptor, options) => {
|
|
@@ -997,6 +1096,9 @@ function singleStoreFactory(kind, open) {
|
|
|
997
1096
|
return open(descriptor, options);
|
|
998
1097
|
};
|
|
999
1098
|
}
|
|
1099
|
+
function unsupportedStoreKindError(bundled, requested) {
|
|
1100
|
+
return new Error(unsupportedStoreKindMessage(bundled, requested));
|
|
1101
|
+
}
|
|
1000
1102
|
function unsupportedStoreKindMessage(bundled, requested) {
|
|
1001
1103
|
const requestedLabel = Object.hasOwn(storeKindLabels, requested) ? storeKindLabels[requested] : void 0;
|
|
1002
1104
|
const requestedEntry = requestedLabel === void 0 ? "" : ` or "@minnowdb/core/worker/${requested}" for the ${requestedLabel} store`;
|
|
@@ -1018,5 +1120,7 @@ export {
|
|
|
1018
1120
|
MAX_WORKER_HANDLES_PER_CONNECTION,
|
|
1019
1121
|
attachWorkerHost,
|
|
1020
1122
|
exposeDatabase,
|
|
1021
|
-
singleStoreFactory
|
|
1123
|
+
singleStoreFactory,
|
|
1124
|
+
unsupportedStoreKindError,
|
|
1125
|
+
workerErrorReporter
|
|
1022
1126
|
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { IndexedDbBlockStore } from "../storage/indexeddb.js";
|
|
2
|
+
import { OpfsBlockStore } 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
|
+
}));
|
|
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,7 @@
|
|
|
1
1
|
const anonymous = /* @__PURE__ */ new WeakMap();
|
|
2
2
|
const named = /* @__PURE__ */ new Map();
|
|
3
|
-
|
|
3
|
+
const WRITE_ADMISSION_WAIT_MS = 1e4;
|
|
4
|
+
async function coordinateWrite(store, run, signal, options = {}) {
|
|
4
5
|
signal.throwIfAborted();
|
|
5
6
|
const name = store.liveQueryChannelName;
|
|
6
7
|
let queue = name === void 0 ? anonymous.get(store) : named.get(name);
|
|
@@ -19,9 +20,29 @@ async function coordinateWrite(store, run, signal) {
|
|
|
19
20
|
admitted = true;
|
|
20
21
|
return run();
|
|
21
22
|
};
|
|
23
|
+
const admissionWaitMs = options.admissionWaitMs ?? WRITE_ADMISSION_WAIT_MS;
|
|
22
24
|
const operation = queue.tail.then(async () => {
|
|
23
25
|
signal.throwIfAborted();
|
|
24
|
-
|
|
26
|
+
if (name === void 0 || locks === void 0)
|
|
27
|
+
return enter();
|
|
28
|
+
const startedAt = Date.now();
|
|
29
|
+
const wait = { ranOut: false };
|
|
30
|
+
const waitTimer = setTimeout(() => {
|
|
31
|
+
wait.ranOut = true;
|
|
32
|
+
lockController.abort(new Error("Write admission wait ran out"));
|
|
33
|
+
}, admissionWaitMs);
|
|
34
|
+
waitTimer.unref?.();
|
|
35
|
+
try {
|
|
36
|
+
return await locks.request(`minnowdb-write:${name}`, { signal: lockController.signal }, enter);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
if (!wait.ranOut || admitted)
|
|
39
|
+
throw error;
|
|
40
|
+
signal.throwIfAborted();
|
|
41
|
+
options.onAdmissionWaitExceeded?.(Date.now() - startedAt);
|
|
42
|
+
return await enter();
|
|
43
|
+
} finally {
|
|
44
|
+
clearTimeout(waitTimer);
|
|
45
|
+
}
|
|
25
46
|
});
|
|
26
47
|
const settled = operation.then(() => void 0, () => void 0);
|
|
27
48
|
queue.tail = settled;
|
|
@@ -50,5 +71,6 @@ async function coordinateWrite(store, run, signal) {
|
|
|
50
71
|
}
|
|
51
72
|
}
|
|
52
73
|
export {
|
|
74
|
+
WRITE_ADMISSION_WAIT_MS,
|
|
53
75
|
coordinateWrite
|
|
54
76
|
};
|