@minnowdb/core 0.10.0 → 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 +15 -3
- package/dist/engine/auto-store.js +48 -6
- package/dist/engine/client-audit-harness.js +123 -0
- package/dist/engine/client.js +21 -6
- package/dist/engine/database.js +223 -56
- package/dist/engine/worker-host.js +2 -0
- package/dist/engine/worker-server.js +4 -5
- package/dist/engine/worker-store-auto.js +2 -2
- package/dist/engine/write-coordinator.js +21 -1
- package/dist/storage/indexeddb-audit-helpers.js +269 -0
- package/dist/storage/indexeddb.js +107 -169
- 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 +44 -5
- package/dist/storage/opfs/power-loss-model.js +62 -0
- package/dist/storage/opfs/rpc.js +1 -0
- package/dist/storage/opfs/store.d.ts +12 -0
- package/dist/storage/opfs/store.js +59 -12
- package/dist/storage/toolkit/wal.js +16 -0
- package/dist/storage/toolkit/wire.js +3 -3
- package/dist/storage/types.d.ts +2 -2
- package/dist/transactions/index.d.ts +7 -0
- package/dist/transactions/index.js +17 -3
- package/package.json +1 -1
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the composition root knows that this module must not import: whether an OPFS database
|
|
3
|
+
* directory of a name exists. The OPFS adapter provides it (`opfsDatabaseExists`); a root that
|
|
4
|
+
* cannot bundle OPFS leaves it out, and OPFS is then never chosen for an unknown name anyway.
|
|
5
|
+
*/
|
|
6
|
+
export interface AutoStoreProbes {
|
|
7
|
+
opfsDatabaseExists?: (name: string) => Promise<boolean>;
|
|
8
|
+
}
|
|
1
9
|
/**
|
|
2
10
|
* The `{ kind: "auto" }` store: OPFS where this context can hold synchronous access handles,
|
|
3
11
|
* IndexedDB where it cannot (Safari's private browsing, a page context, an older build).
|
|
@@ -11,6 +19,8 @@ export type AutoStoreKind = "opfs" | "indexeddb";
|
|
|
11
19
|
/** Test seams: the OPFS probe and the IndexedDB factory that keeps the choices. */
|
|
12
20
|
export declare const autoStoreTestHooks: {
|
|
13
21
|
opfsAvailable?: () => Promise<boolean>;
|
|
22
|
+
/** Whether an OPFS database directory of this name exists; Node has no OPFS to ask. */
|
|
23
|
+
opfsDatabaseExists?: (name: string) => Promise<boolean>;
|
|
14
24
|
indexedDB?: IDBFactory;
|
|
15
25
|
};
|
|
16
26
|
/**
|
|
@@ -24,15 +34,17 @@ export declare function opfsAvailable(): Promise<boolean>;
|
|
|
24
34
|
* instant in contexts that disagree still end up on the one store the record names; the
|
|
25
35
|
* caller reports through `settleAutoStoreChoice` whether that reservation produced a database.
|
|
26
36
|
*/
|
|
27
|
-
export declare function resolveAutoStoreKind(name: string): Promise<{
|
|
37
|
+
export declare function resolveAutoStoreKind(name: string, probes?: AutoStoreProbes): Promise<{
|
|
28
38
|
kind: AutoStoreKind;
|
|
29
39
|
reserved: boolean;
|
|
30
40
|
}>;
|
|
31
41
|
/**
|
|
32
42
|
* Opens the store `auto` resolves to. A reservation that produced no database is released
|
|
33
|
-
* again, so a first open that failed does not pin the name to a store that never held data
|
|
43
|
+
* again, so a first open that failed does not pin the name to a store that never held data —
|
|
44
|
+
* but only when the store really holds none: another connection may have opened the same
|
|
45
|
+
* reservation successfully in the meantime, and its database must keep its memory.
|
|
34
46
|
*/
|
|
35
|
-
export declare function openAutoStore<Store>(name: string, open: (kind: AutoStoreKind) => Promise<Store
|
|
47
|
+
export declare function openAutoStore<Store>(name: string, open: (kind: AutoStoreKind) => Promise<Store>, probes?: AutoStoreProbes): Promise<{
|
|
36
48
|
store: Store;
|
|
37
49
|
kind: AutoStoreKind;
|
|
38
50
|
}>;
|
|
@@ -19,7 +19,7 @@ async function opfsAvailable() {
|
|
|
19
19
|
return false;
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
|
-
async function resolveAutoStoreKind(name) {
|
|
22
|
+
async function resolveAutoStoreKind(name, probes = {}) {
|
|
23
23
|
const remembered = await readChoice(name);
|
|
24
24
|
if (remembered === "indexeddb")
|
|
25
25
|
return { kind: "indexeddb", reserved: false };
|
|
@@ -28,22 +28,64 @@ async function resolveAutoStoreKind(name) {
|
|
|
28
28
|
return { kind: "opfs", reserved: false };
|
|
29
29
|
throw new DatabaseStoreUnavailableError("opfs", name, `Database "${name}" lives on the OPFS store, which this context cannot open; it is not reopened on IndexedDB, where it would be empty`);
|
|
30
30
|
}
|
|
31
|
-
const
|
|
31
|
+
const existing = await existingDatabaseStore(name, probes);
|
|
32
|
+
let kind;
|
|
33
|
+
if (existing === "opfs" && !await opfsAvailable()) {
|
|
34
|
+
throw new DatabaseStoreUnavailableError("opfs", name, `Database "${name}" lives on the OPFS store, which this context cannot open; it is not reopened on IndexedDB, where it would be empty`);
|
|
35
|
+
} else if (existing !== void 0)
|
|
36
|
+
kind = existing;
|
|
37
|
+
else
|
|
38
|
+
kind = await opfsAvailable() ? "opfs" : "indexeddb";
|
|
32
39
|
const reserved = await reserveChoice(name, kind);
|
|
33
40
|
if (reserved)
|
|
34
41
|
return { kind, reserved: true };
|
|
35
|
-
return resolveAutoStoreKind(name);
|
|
42
|
+
return resolveAutoStoreKind(name, probes);
|
|
36
43
|
}
|
|
37
|
-
async function openAutoStore(name, open) {
|
|
38
|
-
const { kind, reserved } = await resolveAutoStoreKind(name);
|
|
44
|
+
async function openAutoStore(name, open, probes = {}) {
|
|
45
|
+
const { kind, reserved } = await resolveAutoStoreKind(name, probes);
|
|
39
46
|
try {
|
|
40
47
|
return { store: await open(kind), kind };
|
|
41
48
|
} catch (error) {
|
|
42
|
-
if (reserved)
|
|
49
|
+
if (reserved && await existingDatabaseStore(name, probes).catch(() => kind) !== kind) {
|
|
43
50
|
await forgetStoreChoice(name).catch(() => void 0);
|
|
51
|
+
}
|
|
44
52
|
throw error;
|
|
45
53
|
}
|
|
46
54
|
}
|
|
55
|
+
async function existingDatabaseStore(name, probes) {
|
|
56
|
+
const opfsExists = autoStoreTestHooks.opfsDatabaseExists ?? probes.opfsDatabaseExists;
|
|
57
|
+
if (opfsExists !== void 0 && await opfsExists(name))
|
|
58
|
+
return "opfs";
|
|
59
|
+
if (await indexedDbDatabaseExists(name))
|
|
60
|
+
return "indexeddb";
|
|
61
|
+
return void 0;
|
|
62
|
+
}
|
|
63
|
+
async function indexedDbDatabaseExists(name) {
|
|
64
|
+
const factory = choiceFactory();
|
|
65
|
+
if (factory === void 0)
|
|
66
|
+
return false;
|
|
67
|
+
const list = factory.databases;
|
|
68
|
+
if (typeof list === "function") {
|
|
69
|
+
try {
|
|
70
|
+
return (await list.call(factory)).some((entry) => entry.name === name);
|
|
71
|
+
} catch {
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return new Promise((resolve) => {
|
|
75
|
+
const request = factory.open(name);
|
|
76
|
+
let created = false;
|
|
77
|
+
request.addEventListener("upgradeneeded", () => {
|
|
78
|
+
created = true;
|
|
79
|
+
request.transaction?.abort();
|
|
80
|
+
});
|
|
81
|
+
request.addEventListener("success", () => {
|
|
82
|
+
request.result.close();
|
|
83
|
+
resolve(!created);
|
|
84
|
+
});
|
|
85
|
+
request.addEventListener("error", () => resolve(false));
|
|
86
|
+
request.addEventListener("blocked", () => resolve(true));
|
|
87
|
+
});
|
|
88
|
+
}
|
|
47
89
|
async function forgetStoreChoice(name) {
|
|
48
90
|
const database = await openChoices();
|
|
49
91
|
if (database === void 0)
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { MemoryBlockStore } from "../storage/index.js";
|
|
2
|
+
function createBoundary() {
|
|
3
|
+
const clientListeners = /* @__PURE__ */ new Map();
|
|
4
|
+
const workerListeners = /* @__PURE__ */ new Map();
|
|
5
|
+
let chain = Promise.resolve();
|
|
6
|
+
let severed = false;
|
|
7
|
+
const sentByClient = [];
|
|
8
|
+
const sentByWorker = [];
|
|
9
|
+
const deliver = (target, message, transfer) => {
|
|
10
|
+
const data = structuredClone(message, transfer === void 0 ? void 0 : { transfer });
|
|
11
|
+
if (severed)
|
|
12
|
+
return;
|
|
13
|
+
chain = chain.then(() => {
|
|
14
|
+
if (severed)
|
|
15
|
+
return;
|
|
16
|
+
for (const listener of target.get("message") ?? [])
|
|
17
|
+
listener({ data });
|
|
18
|
+
});
|
|
19
|
+
};
|
|
20
|
+
const add = (map, type, listener) => {
|
|
21
|
+
const list = map.get(type) ?? [];
|
|
22
|
+
list.push(listener);
|
|
23
|
+
map.set(type, list);
|
|
24
|
+
};
|
|
25
|
+
const remove = (map, type, listener) => {
|
|
26
|
+
const list = map.get(type) ?? [];
|
|
27
|
+
const index = list.indexOf(listener);
|
|
28
|
+
if (index >= 0)
|
|
29
|
+
list.splice(index, 1);
|
|
30
|
+
};
|
|
31
|
+
const clientSide = {
|
|
32
|
+
postMessage: (message, options) => {
|
|
33
|
+
sentByClient.push(structuredClone(message));
|
|
34
|
+
deliver(workerListeners, message, options?.transfer);
|
|
35
|
+
},
|
|
36
|
+
addEventListener: (type, listener) => {
|
|
37
|
+
add(clientListeners, type, listener);
|
|
38
|
+
},
|
|
39
|
+
removeEventListener: (type, listener) => {
|
|
40
|
+
remove(clientListeners, type, listener);
|
|
41
|
+
},
|
|
42
|
+
terminate: () => {
|
|
43
|
+
severed = true;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
const workerSide = {
|
|
47
|
+
postMessage: (message, options) => {
|
|
48
|
+
sentByWorker.push(structuredClone(message));
|
|
49
|
+
deliver(clientListeners, message, options?.transfer);
|
|
50
|
+
},
|
|
51
|
+
addEventListener: (type, listener) => {
|
|
52
|
+
add(workerListeners, type, listener);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
return {
|
|
56
|
+
sentByClient,
|
|
57
|
+
sentByWorker,
|
|
58
|
+
clientSide,
|
|
59
|
+
workerSide,
|
|
60
|
+
sever: () => {
|
|
61
|
+
severed = true;
|
|
62
|
+
},
|
|
63
|
+
injectToClient: (frame) => {
|
|
64
|
+
for (const listener of clientListeners.get("message") ?? []) {
|
|
65
|
+
listener({ data: frame });
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
injectToWorker: (frame) => {
|
|
69
|
+
for (const listener of workerListeners.get("message") ?? []) {
|
|
70
|
+
listener({ data: frame });
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
emitWorkerGlobal: (type, event) => {
|
|
74
|
+
for (const listener of workerListeners.get(type) ?? [])
|
|
75
|
+
listener(event);
|
|
76
|
+
},
|
|
77
|
+
emitTransport: (type, event) => {
|
|
78
|
+
for (const listener of clientListeners.get(type) ?? [])
|
|
79
|
+
listener(event);
|
|
80
|
+
},
|
|
81
|
+
flush: async () => {
|
|
82
|
+
await chain;
|
|
83
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function settled(ms = 20) {
|
|
88
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
89
|
+
}
|
|
90
|
+
function faultyStore(fault, options = {}) {
|
|
91
|
+
const inner = new MemoryBlockStore();
|
|
92
|
+
const calls = [];
|
|
93
|
+
const hidden = new Set(options.hide ?? []);
|
|
94
|
+
const store = new Proxy(inner, {
|
|
95
|
+
get(target, property) {
|
|
96
|
+
if (typeof property === "string" && hidden.has(property))
|
|
97
|
+
return void 0;
|
|
98
|
+
const value = target[property];
|
|
99
|
+
if (typeof value !== "function" || typeof property !== "string")
|
|
100
|
+
return value;
|
|
101
|
+
const method = property;
|
|
102
|
+
return (...args) => {
|
|
103
|
+
const run = () => value.apply(inner, args);
|
|
104
|
+
const isAsync = value.constructor?.name === "AsyncFunction";
|
|
105
|
+
if (!isAsync)
|
|
106
|
+
return run();
|
|
107
|
+
calls.push({ method, args });
|
|
108
|
+
return fault(method, args, run);
|
|
109
|
+
};
|
|
110
|
+
},
|
|
111
|
+
has(target, property) {
|
|
112
|
+
if (typeof property === "string" && hidden.has(property))
|
|
113
|
+
return false;
|
|
114
|
+
return Reflect.has(target, property);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
return { store, calls };
|
|
118
|
+
}
|
|
119
|
+
export {
|
|
120
|
+
createBoundary,
|
|
121
|
+
faultyStore,
|
|
122
|
+
settled
|
|
123
|
+
};
|
package/dist/engine/client.js
CHANGED
|
@@ -188,6 +188,9 @@ class MinnowDatabaseClient {
|
|
|
188
188
|
this.#events.clear();
|
|
189
189
|
this.#transport = next;
|
|
190
190
|
this.#ready = this.#attach(next);
|
|
191
|
+
if (this.#onVisibilityChange !== void 0 && typeof document !== "undefined") {
|
|
192
|
+
document.addEventListener("visibilitychange", this.#onVisibilityChange);
|
|
193
|
+
}
|
|
191
194
|
this.#onVisibilityChange?.();
|
|
192
195
|
await this.#ready;
|
|
193
196
|
}
|
|
@@ -578,7 +581,7 @@ class MinnowDatabaseClient {
|
|
|
578
581
|
try {
|
|
579
582
|
await this.#post("rpc-call", null, "dispose", [], void 0, true, { timeoutMs });
|
|
580
583
|
} finally {
|
|
581
|
-
this.#fail(new Error("Database client is closed"));
|
|
584
|
+
this.#fail(new Error("Database client is closed"), true);
|
|
582
585
|
this.#transport.removeEventListener?.("message", this.#onMessage);
|
|
583
586
|
this.#transport.removeEventListener?.("error", this.#onError);
|
|
584
587
|
this.#transport.removeEventListener?.("messageerror", this.#onMessageError);
|
|
@@ -750,25 +753,37 @@ class MinnowDatabaseClient {
|
|
|
750
753
|
}
|
|
751
754
|
#rejectUnreadable(message, cause) {
|
|
752
755
|
const reason = cause instanceof Error ? cause.message : String(cause);
|
|
753
|
-
const
|
|
754
|
-
cause
|
|
755
|
-
});
|
|
756
|
+
const text = `The database worker sent a frame this client cannot read: ${reason}`;
|
|
756
757
|
const requestId = message.requestId;
|
|
757
758
|
const pending = typeof requestId === "string" ? this.#pending.get(requestId) : void 0;
|
|
758
759
|
if (pending === void 0 || typeof requestId !== "string") {
|
|
759
|
-
this.#fail(
|
|
760
|
+
this.#fail(new DatabaseWorkerFailedError("messageerror", text, { cause }));
|
|
760
761
|
return;
|
|
761
762
|
}
|
|
763
|
+
const error = new Error(text, { cause });
|
|
762
764
|
this.#pending.delete(requestId);
|
|
763
765
|
pending.cleanup?.();
|
|
764
766
|
pending.reject(pending.mayPublish ? new DatabaseWorkerOutcomeUnknownError(pending.method, requestId, { cause: error }) : error);
|
|
765
767
|
}
|
|
766
|
-
#fail(error) {
|
|
768
|
+
#fail(error, closing = false) {
|
|
767
769
|
const first = this.#fatal === void 0;
|
|
768
770
|
this.#fatal = error;
|
|
769
771
|
const pending = [...this.#pending.values()];
|
|
770
772
|
this.#pending.clear();
|
|
773
|
+
const routes = [...this.#events.values()];
|
|
771
774
|
this.#events.clear();
|
|
775
|
+
for (const route of routes) {
|
|
776
|
+
if (!closing) {
|
|
777
|
+
try {
|
|
778
|
+
route.onError?.(error);
|
|
779
|
+
} catch {
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
try {
|
|
783
|
+
route.onComplete?.();
|
|
784
|
+
} catch {
|
|
785
|
+
}
|
|
786
|
+
}
|
|
772
787
|
for (const call of pending) {
|
|
773
788
|
call.cleanup?.();
|
|
774
789
|
call.reject(call.mayPublish ? new DatabaseWorkerOutcomeUnknownError(call.method, call.requestId, { cause: error }) : error);
|