@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
@@ -0,0 +1,220 @@
1
+ import { dateMilliseconds } from "../date-value.js";
2
+ import { estimateValuesBytes } from "./byte-estimates.js";
3
+ import { planMemoKey, sameQueryRow as sameLiveRow } from "./query-cache.js";
4
+ import { externalizeQueryResult } from "./query.js";
5
+ import { compareSqlValues } from "./sql-semantics.js";
6
+ const LIVE_HIDDEN_PREFIX = "__minnow_live_";
7
+ const LIVE_KEY_ALIAS = `${LIVE_HIDDEN_PREFIX}key`;
8
+ const LIVE_ORDER_ALIAS = `${LIVE_HIDDEN_PREFIX}order_`;
9
+ const LIVE_MAINTENANCE_MAX_DELTA_ROWS = 2048;
10
+ const LIVE_WINDOW_MARGIN_MIN = 16;
11
+ const LIVE_WINDOW_MARGIN_MAX = 64;
12
+ function liveKeyToken(value) {
13
+ if (typeof value === "number")
14
+ return `n:${String(value)}`;
15
+ if (typeof value === "string")
16
+ return `s:${value}`;
17
+ if (typeof value === "boolean")
18
+ return value ? "b:1" : "b:0";
19
+ if (value instanceof Date)
20
+ return `d:${String(dateMilliseconds(value))}`;
21
+ return "z";
22
+ }
23
+ function splitLiveHiddenColumns(executed, state) {
24
+ const publicCount = state.publicColumns.length;
25
+ const hidden = executed.columns.slice(publicCount);
26
+ const count = executed.rows.length;
27
+ const keys = new Array(count);
28
+ const order = state.orderTerms.map(() => new Array(count));
29
+ for (let index = 0; index < count; index += 1) {
30
+ const row = executed.rows[index] ?? {};
31
+ keys[index] = liveKeyToken(row[LIVE_KEY_ALIAS] ?? null);
32
+ for (const [term, { alias }] of state.orderTerms.entries()) {
33
+ const values = order[term];
34
+ if (values !== void 0)
35
+ values[index] = row[alias] ?? null;
36
+ }
37
+ for (const column of hidden)
38
+ Reflect.deleteProperty(row, column);
39
+ }
40
+ return {
41
+ result: externalizeQueryResult({
42
+ columns: executed.columns.slice(0, publicCount),
43
+ columnDomains: executed.columnDomains.slice(0, publicCount),
44
+ rows: executed.rows
45
+ }),
46
+ keys,
47
+ order
48
+ };
49
+ }
50
+ function liveMaintainedOutcome(state, previous, previousIndex) {
51
+ const positions = /* @__PURE__ */ new Map();
52
+ let retainedBytes = state.retainedBytes ?? 128 + planMemoKey(state.fullPlan).length * 2 + planMemoKey(state.deltaPlan).length * 2;
53
+ for (const [index, key] of state.keys.entries()) {
54
+ if (positions.has(key))
55
+ throw new TypeError("Duplicate live input key");
56
+ positions.set(key, index);
57
+ if (state.retainedBytes === void 0)
58
+ retainedBytes += liveRowStateBytes(state, index);
59
+ }
60
+ state = { ...state, positions, retainedBytes };
61
+ const visibleCount = state.limit === void 0 ? state.rows.length : Math.min(state.rows.length, state.limit);
62
+ const visible = state.rows.slice(0, visibleCount);
63
+ if (previous === void 0) {
64
+ return {
65
+ result: {
66
+ columns: [...state.publicColumns],
67
+ columnDomains: [...state.columnDomains],
68
+ rows: visible
69
+ },
70
+ state,
71
+ retainedBytes,
72
+ changed: true
73
+ };
74
+ }
75
+ const previousCount = previous.rows.length;
76
+ const retained = new Int32Array(visibleCount);
77
+ let same = visibleCount === previousCount;
78
+ for (let index = 0; index < visibleCount; index += 1) {
79
+ const was = previousIndex?.[index] ?? -1;
80
+ if (was >= 0 && was < previousCount) {
81
+ retained[index] = was;
82
+ if (was !== index)
83
+ same = false;
84
+ continue;
85
+ }
86
+ retained[index] = -1;
87
+ const before = previous.rows[index];
88
+ const now = visible[index];
89
+ if (before !== void 0 && now !== void 0 && sameLiveRow(before, now, previous.columns)) {
90
+ visible[index] = before;
91
+ retained[index] = index;
92
+ } else
93
+ same = false;
94
+ }
95
+ if (same)
96
+ return { result: previous, state, retainedBytes, changed: false };
97
+ return {
98
+ result: {
99
+ columns: [...state.publicColumns],
100
+ columnDomains: [...state.columnDomains],
101
+ rows: visible
102
+ },
103
+ state,
104
+ retainedBytes,
105
+ changed: true,
106
+ retained
107
+ };
108
+ }
109
+ function liveRowStateBytes(state, index) {
110
+ let bytes = 112 + (state.keys[index]?.length ?? 0) * 2 + estimateValuesBytes(Object.values(state.rows[index] ?? {})) * 2;
111
+ for (const values of state.order)
112
+ bytes += 8 + estimateValuesBytes([values[index]]) * 2;
113
+ return bytes;
114
+ }
115
+ function filterLiveRows(state, keep) {
116
+ const rows = [];
117
+ const keys = [];
118
+ const order = state.order.map(() => new Array());
119
+ const previous = [];
120
+ for (let index = 0; index < state.rows.length; index += 1) {
121
+ const row = state.rows[index];
122
+ if (row === void 0 || !keep(index))
123
+ continue;
124
+ rows.push(row);
125
+ keys.push(state.keys[index] ?? "z");
126
+ for (const [term, values] of state.order.entries()) {
127
+ order[term]?.push(values[index] ?? null);
128
+ }
129
+ previous.push(index);
130
+ }
131
+ return { rows, keys, order, previousIndex: Int32Array.from(previous) };
132
+ }
133
+ function trimLiveRows(rows, count) {
134
+ return {
135
+ rows: rows.rows.slice(0, count),
136
+ keys: rows.keys.slice(0, count),
137
+ order: rows.order.map((values) => values.slice(0, count)),
138
+ previousIndex: rows.previousIndex.slice(0, count)
139
+ };
140
+ }
141
+ function liveOrderComparator(terms) {
142
+ return (leftOrder, leftIndex, rightOrder, rightIndex) => {
143
+ for (const [term, { descending, nulls }] of terms.entries()) {
144
+ const a = leftOrder[term]?.[leftIndex] ?? null;
145
+ const b = rightOrder[term]?.[rightIndex] ?? null;
146
+ if (a === null || b === null) {
147
+ if (a === null && b === null)
148
+ continue;
149
+ const nullsFirst = nulls === "first" || nulls === void 0 && descending;
150
+ return a === null ? nullsFirst ? -1 : 1 : nullsFirst ? 1 : -1;
151
+ }
152
+ let comparison = compareSqlValues(a, b);
153
+ if (descending)
154
+ comparison = -comparison;
155
+ if (comparison !== 0)
156
+ return comparison;
157
+ }
158
+ return 0;
159
+ };
160
+ }
161
+ function mergeLiveRows(kept, added, compare, ordered) {
162
+ if (added.rows.length === 0)
163
+ return kept;
164
+ const terms = kept.order.length;
165
+ const total = kept.rows.length + added.rows.length;
166
+ const rows = new Array(total);
167
+ const keys = new Array(total);
168
+ const order = Array.from({ length: terms }, () => new Array(total));
169
+ const previousIndex = new Int32Array(total);
170
+ const take = (source, from, to) => {
171
+ rows[to] = source.rows[from] ?? {};
172
+ keys[to] = source.keys[from] ?? "z";
173
+ for (let term = 0; term < terms; term += 1) {
174
+ const values = order[term];
175
+ if (values !== void 0)
176
+ values[to] = source.order[term]?.[from] ?? null;
177
+ }
178
+ previousIndex[to] = source.previousIndex[from] ?? -1;
179
+ };
180
+ if (!ordered) {
181
+ for (let index = 0; index < kept.rows.length; index += 1)
182
+ take(kept, index, index);
183
+ for (let index = 0; index < added.rows.length; index += 1) {
184
+ take(added, index, kept.rows.length + index);
185
+ }
186
+ return { rows, keys, order, previousIndex };
187
+ }
188
+ const addedIndexes = added.rows.map((_, index) => index);
189
+ addedIndexes.sort((left, right) => compare(added.order, left, added.order, right));
190
+ let keptIndex = 0;
191
+ let addedPosition = 0;
192
+ for (let to = 0; to < total; to += 1) {
193
+ const addedIndex = addedIndexes[addedPosition];
194
+ const takeAdded = addedIndex !== void 0 && (keptIndex >= kept.rows.length || compare(added.order, addedIndex, kept.order, keptIndex) < 0);
195
+ if (takeAdded) {
196
+ take(added, addedIndex, to);
197
+ addedPosition += 1;
198
+ } else {
199
+ take(kept, keptIndex, to);
200
+ keptIndex += 1;
201
+ }
202
+ }
203
+ return { rows, keys, order, previousIndex };
204
+ }
205
+ export {
206
+ LIVE_HIDDEN_PREFIX,
207
+ LIVE_KEY_ALIAS,
208
+ LIVE_MAINTENANCE_MAX_DELTA_ROWS,
209
+ LIVE_ORDER_ALIAS,
210
+ LIVE_WINDOW_MARGIN_MAX,
211
+ LIVE_WINDOW_MARGIN_MIN,
212
+ filterLiveRows,
213
+ liveKeyToken,
214
+ liveMaintainedOutcome,
215
+ liveOrderComparator,
216
+ liveRowStateBytes,
217
+ mergeLiveRows,
218
+ splitLiveHiddenColumns,
219
+ trimLiveRows
220
+ };
@@ -1,4 +1,5 @@
1
1
  import { copyDate, dateMilliseconds } from "../date-value.js";
2
+ import { quoteSqlIdentifier } from "./sql-quote.js";
2
3
  import { validateColumnDefault, validateEnumValues, validateSqlDomain } from "../storage/types.js";
3
4
  import { childExpressions, compileCheckExpression, expressionColumns, hasAggregate, validateDefaultExpression } from "./query.js";
4
5
  import { externalSqlDomainValue, normalizeSqlDomainValue } from "./sql-domains.js";
@@ -927,7 +928,7 @@ function planMigration(catalog, definition, options = {}) {
927
928
  }
928
929
  function typedTable(database, definition) {
929
930
  const columnNames = Object.keys(definition.columns);
930
- const quote = (name) => `"${name.replaceAll('"', '""')}"`;
931
+ const quote = quoteSqlIdentifier;
931
932
  const scalarUniqueKey = Object.entries(definition.columns).find(([, columnDefinition]) => columnDefinition.isUnique)?.[0];
932
933
  const keyColumns = definition.primaryKey.length > 0 ? [...definition.primaryKey] : scalarUniqueKey === void 0 ? [] : [scalarUniqueKey];
933
934
  const normalizedRows = (rows) => {
@@ -1,3 +1,4 @@
1
+ import { quoteSqlIdentifier } from "./sql-quote.js";
1
2
  import { dateIsoString, dateMilliseconds, dateUtcDate, dateUtcDay, dateUtcFullYear, dateUtcHours, dateUtcMinutes, dateUtcMonth, dateUtcSeconds } from "../date-value.js";
2
3
  import { MAX_SQL_SCALAR_RESULT_CHARACTERS } from "./cache-limits.js";
3
4
  import { dateDomainValue, exactNumericRounded, exactNumericUnary, externalSqlDomainValue, intervalDomainValue, isDateDomainValue, isExactNumeric, protectedSqlTextValue } from "./sql-domains.js";
@@ -467,7 +468,7 @@ function formatText(template, values) {
467
468
  if (value === null || value === void 0)
468
469
  throw new TypeError("FORMAT %I does not accept NULL");
469
470
  const name = rendered(value);
470
- return /^[a-z_][a-z0-9_]*$/.test(name) ? name : `"${name.replace(/"/g, '""')}"`;
471
+ return /^[a-z_][a-z0-9_]*$/.test(name) ? name : quoteSqlIdentifier(name);
471
472
  }
472
473
  if (value === null || value === void 0)
473
474
  return "NULL";
@@ -902,7 +903,7 @@ const simpleScalarFunctions = /* @__PURE__ */ new Map([
902
903
  returns: "string",
903
904
  evaluate: (values) => {
904
905
  const name = text("QUOTE_IDENT", values[0]);
905
- return /^[a-z_][a-z0-9_]*$/.test(name) ? name : bounded(`"${name.replace(/"/g, '""')}"`, "QUOTE_IDENT");
906
+ return /^[a-z_][a-z0-9_]*$/.test(name) ? name : bounded(quoteSqlIdentifier(name), "QUOTE_IDENT");
906
907
  }
907
908
  }
908
909
  ],
@@ -0,0 +1,6 @@
1
+ function quoteSqlIdentifier(identifier) {
2
+ return `"${identifier.replaceAll('"', '""')}"`;
3
+ }
4
+ export {
5
+ quoteSqlIdentifier
6
+ };
@@ -1308,7 +1308,6 @@ async function executeBoundPlanAsync(plan, memory, options) {
1308
1308
  const rows = plan.grouped ? finishGroups(plan, groups.values(), memory) : output.finish();
1309
1309
  return finishResult(plan, rows, memory);
1310
1310
  }
1311
- const SELECTED_ROW_COALESCE_GAP = 32;
1312
1311
  async function scanSelectedRows(plan, selection, groups, output, memory, options) {
1313
1312
  const scanRows = plan.sourceTables[plan.scanSource]?.rowCount ?? 0;
1314
1313
  let index = 0;
@@ -1329,9 +1328,8 @@ async function scanSelectedRows(plan, selection, groups, output, memory, options
1329
1328
  index += 1;
1330
1329
  while (index < selection.length) {
1331
1330
  const next = selection[index] ?? 0;
1332
- if (next >= windowEnd || next - end > SELECTED_ROW_COALESCE_GAP || next >= begin + DEFAULT_BATCH_ROWS) {
1331
+ if (next !== end || next >= windowEnd || next >= begin + DEFAULT_BATCH_ROWS)
1333
1332
  break;
1334
- }
1335
1333
  end = next + 1;
1336
1334
  index += 1;
1337
1335
  }
@@ -14,6 +14,8 @@ async function createStore(descriptor, options) {
14
14
  ...descriptor.opfs?.durability === void 0 ? {} : { durability: descriptor.opfs.durability }
15
15
  } : { kind, name: descriptor.name, ...descriptor.indexeddb }, options);
16
16
  return opened;
17
+ }, {
18
+ opfsDatabaseExists: async (name) => (await import("../storage/opfs/index.js")).opfsDatabaseExists({ name })
17
19
  });
18
20
  }
19
21
  if (descriptor.kind === "memory") {
@@ -538,7 +538,7 @@ class DatabaseRpcServer {
538
538
  if (handle === void 0)
539
539
  throw new Error(`Unknown handle: ${handleId}`);
540
540
  if (handle.type === "write") {
541
- this.#beginWriteHandleCall(handle);
541
+ await this.#beginWriteHandleCall(handle);
542
542
  try {
543
543
  return await this.#callWriteHandle(handleId, handle, method, args, context);
544
544
  } finally {
@@ -847,12 +847,11 @@ class DatabaseRpcServer {
847
847
  #releaseHandleId(id) {
848
848
  this.#reservedHandleIds.delete(id);
849
849
  }
850
- #beginWriteHandleCall(handle) {
850
+ async #beginWriteHandleCall(handle) {
851
+ while (handle.activeCalls !== 0)
852
+ await handle.activeCallDone;
851
853
  if (!handle.open)
852
854
  throw new Error("Write handle is closed");
853
- if (handle.activeCalls !== 0) {
854
- throw new Error("Write handle already has a call in flight");
855
- }
856
855
  handle.activeCalls = 1;
857
856
  handle.activeCallDone = new Promise((resolve) => {
858
857
  handle.finishActiveCall = resolve;
@@ -1,5 +1,5 @@
1
1
  import { IndexedDbBlockStore } from "../storage/indexeddb.js";
2
- import { OpfsBlockStore } from "../storage/opfs/index.js";
2
+ import { OpfsBlockStore, opfsDatabaseExists } from "../storage/opfs/index.js";
3
3
  import { openAutoStore } from "./auto-store.js";
4
4
  import { unsupportedStoreKindError } from "./worker-server.js";
5
5
  const autoWorkerStore = async (descriptor, options) => {
@@ -26,7 +26,7 @@ const autoWorkerStore = async (descriptor, options) => {
26
26
  name: descriptor.name,
27
27
  ...descriptor.indexeddb?.durability === void 0 ? {} : { durability: descriptor.indexeddb.durability },
28
28
  ...descriptor.indexeddb?.uniqueKeyCacheBytes === void 0 ? {} : { uniqueKeyCacheBytes: descriptor.indexeddb.uniqueKeyCacheBytes }
29
- }));
29
+ }), { opfsDatabaseExists: (name) => opfsDatabaseExists({ name }) });
30
30
  default:
31
31
  throw unsupportedStoreKindError("auto", descriptor.kind);
32
32
  }
@@ -1,6 +1,11 @@
1
1
  const anonymous = /* @__PURE__ */ new WeakMap();
2
2
  const named = /* @__PURE__ */ new Map();
3
+ const bypassing = /* @__PURE__ */ new Set();
4
+ const NOT_GRANTED = /* @__PURE__ */ Symbol("write admission lock not granted");
3
5
  const WRITE_ADMISSION_WAIT_MS = 1e4;
6
+ function _resetWriteAdmissionForTests() {
7
+ bypassing.clear();
8
+ }
4
9
  async function coordinateWrite(store, run, signal, options = {}) {
5
10
  signal.throwIfAborted();
6
11
  const name = store.liveQueryChannelName;
@@ -25,6 +30,19 @@ async function coordinateWrite(store, run, signal, options = {}) {
25
30
  signal.throwIfAborted();
26
31
  if (name === void 0 || locks === void 0)
27
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
+ }
28
46
  const startedAt = Date.now();
29
47
  const wait = { ranOut: false };
30
48
  const waitTimer = setTimeout(() => {
@@ -33,11 +51,12 @@ async function coordinateWrite(store, run, signal, options = {}) {
33
51
  }, admissionWaitMs);
34
52
  waitTimer.unref?.();
35
53
  try {
36
- return await locks.request(`minnowdb-write:${name}`, { signal: lockController.signal }, enter);
54
+ return await locks.request(lockName, { signal: lockController.signal }, enter);
37
55
  } catch (error) {
38
56
  if (!wait.ranOut || admitted)
39
57
  throw error;
40
58
  signal.throwIfAborted();
59
+ bypassing.add(name);
41
60
  options.onAdmissionWaitExceeded?.(Date.now() - startedAt);
42
61
  return await enter();
43
62
  } finally {
@@ -72,5 +91,6 @@ async function coordinateWrite(store, run, signal, options = {}) {
72
91
  }
73
92
  export {
74
93
  WRITE_ADMISSION_WAIT_MS,
94
+ _resetWriteAdmissionForTests,
75
95
  coordinateWrite
76
96
  };
@@ -9,6 +9,12 @@ export interface IndexedDbBlockStoreOptions {
9
9
  * 8 MiB so a table's row count cannot silently become an unbounded resident-memory cost.
10
10
  */
11
11
  uniqueKeyCacheBytes?: number;
12
+ /**
13
+ * How long the connection may answer nothing at all, while it has work outstanding, before
14
+ * every waiting call fails with `StorageUnresponsiveError` and the store refuses new work.
15
+ * Defaults to `INDEXEDDB_UNRESPONSIVE_AFTER_MS`.
16
+ */
17
+ unresponsiveAfterMs?: number;
12
18
  }
13
19
  export declare class IndexedDbBlockStore implements BlockStore {
14
20
  #private;
@@ -189,3 +195,15 @@ export declare class IndexedDbBlockStore implements BlockStore {
189
195
  snapshotPeakRetainedBytes: number;
190
196
  };
191
197
  }
198
+ /**
199
+ * How long a connection may go without a single IndexedDB event, while it has work outstanding,
200
+ * before the adapter calls it unresponsive. Nothing in IndexedDB can be cancelled and nothing
201
+ * reports progress, so this is the only way for a store to tell a wedged browser from a busy
202
+ * one: any event anywhere on the connection resets the deadline, which means a transaction
203
+ * queued behind a genuinely long one is never mistaken for a wedge — only total silence is.
204
+ *
205
+ * See `StorageUnresponsiveError` for the wedge this bounds. Thirty seconds is far longer than
206
+ * any single request takes and comfortably inside the worker client's own request deadline, so
207
+ * the typed error is what a caller sees rather than a lost worker.
208
+ */
209
+ export declare const INDEXEDDB_UNRESPONSIVE_AFTER_MS = 30000;