@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.
@@ -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>): Promise<{
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 kind = await opfsAvailable() ? "opfs" : "indexeddb";
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
+ };
@@ -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 error = new Error(`The database worker sent a frame this client cannot read: ${reason}`, {
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(error);
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);