@minnowdb/core 0.10.0 → 0.10.2

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.
Files changed (37) hide show
  1. package/dist/engine/auto-store.d.ts +15 -3
  2. package/dist/engine/auto-store.js +48 -6
  3. package/dist/engine/client.js +23 -7
  4. package/dist/engine/database.js +347 -862
  5. package/dist/engine/index-terms.js +627 -0
  6. package/dist/engine/index.d.ts +1 -1
  7. package/dist/engine/index.js +2 -1
  8. package/dist/engine/live-maintenance.js +220 -0
  9. package/dist/engine/schema.js +2 -1
  10. package/dist/engine/sql-functions.js +3 -2
  11. package/dist/engine/sql-quote.js +6 -0
  12. package/dist/engine/vector.js +1 -3
  13. package/dist/engine/worker-host.js +2 -0
  14. package/dist/engine/worker-server.js +4 -5
  15. package/dist/engine/worker-store-auto.js +2 -2
  16. package/dist/engine/write-coordinator.js +21 -1
  17. package/dist/storage/indexeddb.d.ts +18 -0
  18. package/dist/storage/indexeddb.js +293 -211
  19. package/dist/storage/opfs/coordination-helpers.js +54 -0
  20. package/dist/storage/opfs/index.d.ts +1 -1
  21. package/dist/storage/opfs/index.js +3 -2
  22. package/dist/storage/opfs/leader.js +44 -5
  23. package/dist/storage/opfs/rpc.js +3 -1
  24. package/dist/storage/opfs/store.d.ts +12 -0
  25. package/dist/storage/opfs/store.js +59 -12
  26. package/dist/storage/toolkit/wal.js +16 -0
  27. package/dist/storage/toolkit/wire.js +3 -3
  28. package/dist/storage/types.d.ts +35 -4
  29. package/dist/storage/types.js +17 -4
  30. package/dist/testing/block-store-conformance.js +40 -1
  31. package/dist/testing/index.d.ts +1 -0
  32. package/dist/testing/index.js +9 -0
  33. package/dist/testing/interaction-simulator.d.ts +319 -0
  34. package/dist/testing/interaction-simulator.js +1631 -0
  35. package/dist/transactions/index.d.ts +7 -0
  36. package/dist/transactions/index.js +17 -3
  37. package/package.json +4 -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>): 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)
@@ -1,5 +1,5 @@
1
1
  import { createLiveQueryPatch } from "./live-patch.js";
2
- import { BlockReadBatchTooLargeError, CompactionBacklogError, CompactionJobConflictError, ConnectionLostError, GarbageCollectionJobConflictError, IndexedDbSchemaUpgradeBlockedError, LeaseConflictError, LeaseExpiredError, LeaseOwnerConflictError, OpfsCoordinationError, OpfsDatabaseInUseError, OpfsUncertainOutcomeError, PostingBuildConflictError, SchemaConflictError, SnapshotImportConflictError, SnapshotManifestMissingError, StorageCorruptionError, StorageFormatVersionError, StorageResourceLimitError, TableInUseError, TableRecordConflictError, TempOwnerConflictError, TransactionRecordConflictError, UniqueIndexCoverageError, UniqueKeyBuildConflictError, UniqueKeyConflictError, UnknownOutcomeError, WriteConflictError } from "../storage/types.js";
2
+ import { BlockReadBatchTooLargeError, CompactionBacklogError, CompactionJobConflictError, ConnectionLostError, GarbageCollectionJobConflictError, IndexedDbSchemaUpgradeBlockedError, LeaseConflictError, LeaseExpiredError, LeaseOwnerConflictError, OpfsCoordinationError, OpfsDatabaseInUseError, OpfsUncertainOutcomeError, PostingBuildConflictError, SchemaConflictError, SnapshotImportConflictError, SnapshotManifestMissingError, StorageCorruptionError, StorageFormatVersionError, StorageUnresponsiveError, StorageResourceLimitError, TableInUseError, TableRecordConflictError, TempOwnerConflictError, TransactionRecordConflictError, UniqueIndexCoverageError, UniqueKeyBuildConflictError, UniqueKeyConflictError, UnknownOutcomeError, WriteConflictError } from "../storage/types.js";
3
3
  import { MAX_SNAPSHOT_STREAM_CHUNK_BYTES } from "../storage/snapshot.js";
4
4
  import { MAX_DATABASE_RPC_IN_FLIGHT, WORKER_DIAGNOSTIC_HANDLE_ID, isWorkerErrorReport, parseRpcResponse, MIN_WORKER_KEEPALIVE_INTERVAL_MS, WORKER_KEEPALIVE_INTERVAL_MS, protocolVersion, rehydrateError as rehydrateSerializedError } from "../worker-protocol/index.js";
5
5
  import { definedVectors, toColumnarBatch } from "./batch.js";
@@ -65,6 +65,7 @@ const errorRegistry = new Map([
65
65
  PostingBuildConflictError,
66
66
  StorageCorruptionError,
67
67
  StorageFormatVersionError,
68
+ StorageUnresponsiveError,
68
69
  OpfsCoordinationError,
69
70
  OpfsDatabaseInUseError,
70
71
  OpfsUncertainOutcomeError,
@@ -188,6 +189,9 @@ class MinnowDatabaseClient {
188
189
  this.#events.clear();
189
190
  this.#transport = next;
190
191
  this.#ready = this.#attach(next);
192
+ if (this.#onVisibilityChange !== void 0 && typeof document !== "undefined") {
193
+ document.addEventListener("visibilitychange", this.#onVisibilityChange);
194
+ }
191
195
  this.#onVisibilityChange?.();
192
196
  await this.#ready;
193
197
  }
@@ -578,7 +582,7 @@ class MinnowDatabaseClient {
578
582
  try {
579
583
  await this.#post("rpc-call", null, "dispose", [], void 0, true, { timeoutMs });
580
584
  } finally {
581
- this.#fail(new Error("Database client is closed"));
585
+ this.#fail(new Error("Database client is closed"), true);
582
586
  this.#transport.removeEventListener?.("message", this.#onMessage);
583
587
  this.#transport.removeEventListener?.("error", this.#onError);
584
588
  this.#transport.removeEventListener?.("messageerror", this.#onMessageError);
@@ -750,25 +754,37 @@ class MinnowDatabaseClient {
750
754
  }
751
755
  #rejectUnreadable(message, cause) {
752
756
  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
- });
757
+ const text = `The database worker sent a frame this client cannot read: ${reason}`;
756
758
  const requestId = message.requestId;
757
759
  const pending = typeof requestId === "string" ? this.#pending.get(requestId) : void 0;
758
760
  if (pending === void 0 || typeof requestId !== "string") {
759
- this.#fail(error);
761
+ this.#fail(new DatabaseWorkerFailedError("messageerror", text, { cause }));
760
762
  return;
761
763
  }
764
+ const error = new Error(text, { cause });
762
765
  this.#pending.delete(requestId);
763
766
  pending.cleanup?.();
764
767
  pending.reject(pending.mayPublish ? new DatabaseWorkerOutcomeUnknownError(pending.method, requestId, { cause: error }) : error);
765
768
  }
766
- #fail(error) {
769
+ #fail(error, closing = false) {
767
770
  const first = this.#fatal === void 0;
768
771
  this.#fatal = error;
769
772
  const pending = [...this.#pending.values()];
770
773
  this.#pending.clear();
774
+ const routes = [...this.#events.values()];
771
775
  this.#events.clear();
776
+ for (const route of routes) {
777
+ if (!closing) {
778
+ try {
779
+ route.onError?.(error);
780
+ } catch {
781
+ }
782
+ }
783
+ try {
784
+ route.onComplete?.();
785
+ } catch {
786
+ }
787
+ }
772
788
  for (const call of pending) {
773
789
  call.cleanup?.();
774
790
  call.reject(call.mayPublish ? new DatabaseWorkerOutcomeUnknownError(call.method, call.requestId, { cause: error }) : error);