@minnowdb/core 0.8.0 → 0.9.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.
Files changed (66) hide show
  1. package/dist/engine/artifact-cache.js +3 -2
  2. package/dist/engine/buffered-writer.js +12 -2
  3. package/dist/engine/client.d.ts +8 -2
  4. package/dist/engine/client.js +106 -22
  5. package/dist/engine/database.d.ts +5 -3
  6. package/dist/engine/database.js +2044 -1674
  7. package/dist/engine/errors.d.ts +19 -0
  8. package/dist/engine/errors.js +29 -0
  9. package/dist/engine/index.d.ts +1 -0
  10. package/dist/engine/index.js +4 -0
  11. package/dist/engine/live-accept.js +13 -0
  12. package/dist/engine/live-aggregate.js +54 -16
  13. package/dist/engine/live.d.ts +2 -0
  14. package/dist/engine/live.js +109 -108
  15. package/dist/engine/query-cache.js +17 -2
  16. package/dist/engine/query.d.ts +1 -2
  17. package/dist/engine/query.js +4 -7
  18. package/dist/engine/result-state.d.ts +7 -0
  19. package/dist/engine/result-state.js +15 -0
  20. package/dist/engine/vector.d.ts +4 -0
  21. package/dist/engine/vector.js +14 -14
  22. package/dist/engine/windows.js +15 -14
  23. package/dist/engine/worker-server.d.ts +1 -1
  24. package/dist/engine/worker-server.js +28 -15
  25. package/dist/engine/write-coordinator.js +54 -0
  26. package/dist/storage/indexeddb.js +134 -89
  27. package/dist/storage/opfs/index.d.ts +1 -1
  28. package/dist/storage/opfs/index.js +3 -1
  29. package/dist/storage/opfs/leader.js +20 -9
  30. package/dist/storage/opfs/rpc.js +3 -1
  31. package/dist/storage/opfs/store.d.ts +2 -0
  32. package/dist/storage/opfs/store.js +128 -40
  33. package/dist/storage/toolkit/record-core.js +2 -1
  34. package/dist/storage/types.d.ts +14 -0
  35. package/dist/storage/types.js +21 -0
  36. package/dist/transactions/index.d.ts +3 -0
  37. package/dist/transactions/index.js +4 -1
  38. package/dist/worker-protocol/index.d.ts +1 -1
  39. package/dist/worker-protocol/index.js +1 -1
  40. package/package.json +2 -2
  41. package/dist/date-value.d.ts +0 -20
  42. package/dist/engine/artifact-cache.d.ts +0 -29
  43. package/dist/engine/byte-estimates.d.ts +0 -11
  44. package/dist/engine/cancellation.d.ts +0 -2
  45. package/dist/engine/defaults.d.ts +0 -29
  46. package/dist/engine/group-index.d.ts +0 -33
  47. package/dist/engine/join-index.d.ts +0 -10
  48. package/dist/engine/live-aggregate.d.ts +0 -12
  49. package/dist/engine/live-equal.d.ts +0 -7
  50. package/dist/engine/point-read.d.ts +0 -59
  51. package/dist/engine/query-cache.d.ts +0 -21
  52. package/dist/engine/query-generations.d.ts +0 -8
  53. package/dist/engine/query-identity.d.ts +0 -3
  54. package/dist/engine/result-wire.d.ts +0 -70
  55. package/dist/engine/sort-keys.d.ts +0 -73
  56. package/dist/engine/sql-domains.d.ts +0 -97
  57. package/dist/engine/sql-functions.d.ts +0 -11
  58. package/dist/engine/sql-json.d.ts +0 -40
  59. package/dist/engine/sql-semantics.d.ts +0 -66
  60. package/dist/engine/worker-store-indexeddb.d.ts +0 -2
  61. package/dist/engine/worker-store-memory.d.ts +0 -2
  62. package/dist/engine/worker-store-opfs.d.ts +0 -2
  63. package/dist/engine/write-block-planner.d.ts +0 -19
  64. package/dist/storage/opfs/leader.d.ts +0 -460
  65. package/dist/storage/opfs/rpc.d.ts +0 -82
  66. package/dist/storage/opfs/snapshot-ledger.d.ts +0 -41
@@ -52,11 +52,12 @@ class ArtifactCache {
52
52
  return entry.payload;
53
53
  }
54
54
  put(key, payload, bytes) {
55
- if (!this.enabled || bytes > this.#limitBytes)
56
- return;
57
55
  if (!Number.isSafeInteger(bytes) || bytes < 0) {
58
56
  throw new RangeError("Artifact cache entry bytes must be a non-negative whole number");
59
57
  }
58
+ bytes += 96 + key.length * 2;
59
+ if (!this.enabled || !Number.isSafeInteger(bytes) || bytes > this.#limitBytes)
60
+ return;
60
61
  const existing = this.#entries.get(key);
61
62
  if (existing !== void 0) {
62
63
  this.#usedBytes -= existing.bytes;
@@ -47,6 +47,16 @@ class BufferedTableWriter {
47
47
  });
48
48
  }
49
49
  async flush() {
50
+ const adds = this.#addTail;
51
+ await adds;
52
+ let result;
53
+ if (this.#inFlight !== void 0)
54
+ result = await this.#inFlight;
55
+ if (this.#rows.length > 0)
56
+ result = await this.#flushBatch();
57
+ return result;
58
+ }
59
+ async #flushBatch() {
50
60
  if (this.#inFlight !== void 0)
51
61
  return this.#inFlight;
52
62
  if (this.#rows.length === 0)
@@ -102,7 +112,7 @@ class BufferedTableWriter {
102
112
  if (this.#inFlight !== void 0)
103
113
  await this.#inFlight;
104
114
  else
105
- await this.flush();
115
+ await this.#flushBatch();
106
116
  }
107
117
  }
108
118
  async #addSerial(row) {
@@ -115,7 +125,7 @@ class BufferedTableWriter {
115
125
  }
116
126
  if (this.#inFlight !== void 0)
117
127
  await this.#inFlight;
118
- return this.flush();
128
+ return this.#flushBatch();
119
129
  }
120
130
  #scheduleAgeFlush() {
121
131
  if (this.#timer !== void 0 || this.#rows.length === 0 || this.#closed)
@@ -1,4 +1,4 @@
1
- import { type LiveQueryPatchOptions } from "./live-patch.js";
1
+ import { type LiveQueryPatch, type LiveQueryPatchOptions } from "./live-patch.js";
2
2
  import { type CompactionJobRecord, type GarbageCollectionJobRecord, type StorageIntegrityMode, type StorageIntegrityReport, type StorageStats, type InterruptedSnapshotImport, type InterruptedSnapshotImportAbortResult } from "../storage/types.js";
3
3
  import { type BatchRow } from "./batch.js";
4
4
  import type { Catalog } from "./catalog.js";
@@ -47,6 +47,8 @@ export interface MinnowDatabaseClientOptions<TSchema extends AnySchema = Untyped
47
47
  store?: StoreDescriptor;
48
48
  /** Cloneable database options applied when the worker constructs the database. */
49
49
  databaseOptions?: WireDatabaseOptions;
50
+ /** Maximum response wait, including initialization; defaults to 60 seconds. */
51
+ requestTimeoutMs?: number;
50
52
  }
51
53
  export interface ClientLiveQueryOptions {
52
54
  /** BroadcastChannel name the worker uses to exchange cross-tab commit hints. */
@@ -56,6 +58,8 @@ export interface ClientLiveQueryOptions {
56
58
  export interface CloseClientOptions {
57
59
  /** Also terminate the worker after disposing; only meaningful when the transport can. */
58
60
  terminateWorker?: boolean;
61
+ /** Grace allowed for disposal before closing the transport; defaults to 5 seconds. */
62
+ timeoutMs?: number;
59
63
  }
60
64
  export interface ClientMigrationResult {
61
65
  createdTables: string[];
@@ -67,6 +71,7 @@ export interface ClientMigrationResult {
67
71
  }
68
72
  interface EventRoute {
69
73
  onChange?: (result: QueryResult, delivery: LiveQueryDelivery) => void;
74
+ onPatch?: (patch: LiveQueryPatch, delivery: LiveQueryDelivery) => void;
70
75
  onInvalidate?: (invalidation: LiveQueryInvalidation) => void;
71
76
  onError?: (error: unknown) => void;
72
77
  onComplete?: () => void;
@@ -75,6 +80,7 @@ interface EventRoute {
75
80
  onStats?: (stats: QueryExecutionStats) => void;
76
81
  }
77
82
  interface RpcCallControls {
83
+ timeoutMs?: number;
78
84
  signal?: AbortSignal | undefined;
79
85
  onStats?: ((stats: QueryExecutionStats) => void) | undefined;
80
86
  }
@@ -265,7 +271,7 @@ export declare class ClientLiveQuerySet {
265
271
  constructor(client: MinnowDatabaseClient, handleId: string, created: Promise<unknown>);
266
272
  /** Registers a query (SQL or a compiled-plan envelope) and re-runs it on relevant changes. */
267
273
  subscribe(query: LiveQueryInput, options: LiveQuerySubscribeOptions): Promise<ClientLiveSubscription>;
268
- /** Delivers changed row payloads; the worker transport still sends its result snapshot. */
274
+ /** Transfers resets followed by changed row payloads and retained positions across the worker. */
269
275
  subscribePatches(query: LiveQueryInput, options: LiveQueryPatchOptions): Promise<ClientLiveSubscription>;
270
276
  /** Registers dependency observation while leaving execution/result mapping to an adapter. */
271
277
  observe(query: LiveQueryInput, options: LiveQueryObserveOptions): Promise<ClientLiveSubscription>;
@@ -1,9 +1,9 @@
1
1
  import { createLiveQueryPatch } from "./live-patch.js";
2
- import { BlockReadBatchTooLargeError, CompactionBacklogError, CompactionJobConflictError, GarbageCollectionJobConflictError, IndexedDbSchemaUpgradeBlockedError, LeaseConflictError, LeaseExpiredError, LeaseOwnerConflictError, PostingBuildConflictError, SnapshotManifestMissingError, SnapshotImportConflictError, SchemaConflictError, StorageResourceLimitError, TableInUseError, TableRecordConflictError, TempOwnerConflictError, TransactionRecordConflictError, UniqueKeyBuildConflictError, UniqueKeyConflictError, UniqueIndexCoverageError, WriteConflictError, StorageCorruptionError, StorageFormatVersionError, OpfsUncertainOutcomeError } from "../storage/types.js";
2
+ import { BlockReadBatchTooLargeError, CompactionBacklogError, CompactionJobConflictError, GarbageCollectionJobConflictError, IndexedDbSchemaUpgradeBlockedError, LeaseConflictError, LeaseExpiredError, LeaseOwnerConflictError, PostingBuildConflictError, SnapshotManifestMissingError, SnapshotImportConflictError, SchemaConflictError, StorageResourceLimitError, TableInUseError, TableRecordConflictError, TempOwnerConflictError, TransactionRecordConflictError, UniqueKeyBuildConflictError, UniqueKeyConflictError, UniqueIndexCoverageError, WriteConflictError, StorageCorruptionError, StorageFormatVersionError, OpfsCoordinationError, OpfsDatabaseInUseError, OpfsUncertainOutcomeError } from "../storage/types.js";
3
3
  import { MAX_SNAPSHOT_STREAM_CHUNK_BYTES } from "../storage/snapshot.js";
4
4
  import { parseRpcResponse, MAX_DATABASE_RPC_IN_FLIGHT, protocolVersion } from "../worker-protocol/index.js";
5
5
  import { definedVectors, toColumnarBatch } from "./batch.js";
6
- import { CompactionJobCancelledError, CompactionMemoryBudgetError, CompactionWriteAmplificationError, MaintenanceBacklogError, DatabaseReadBacklogError, LiveQueryLimitError, MissingKeyError, SqlCompileError, UnknownTableError, UniqueConstraintError, VisibleSegmentCursorStaleError } from "./errors.js";
6
+ import { CompactionJobCancelledError, CompactionMemoryBudgetError, CompactionWriteAmplificationError, MaintenanceBacklogError, DatabaseReadBacklogError, TransactionExpiredError, DatabaseWorkerTimeoutError, DatabaseWorkerOutcomeUnknownError, LiveQueryLimitError, MissingKeyError, SqlCompileError, UnknownTableError, UniqueConstraintError, VisibleSegmentCursorStaleError } from "./errors.js";
7
7
  import { QueryMemoryBudgetError } from "./memory.js";
8
8
  import { decodeQueryResult } from "./result-wire.js";
9
9
  import { serializeSchema } from "./schema-wire.js";
@@ -33,6 +33,9 @@ const errorRegistry = new Map([
33
33
  CompactionJobCancelledError,
34
34
  MaintenanceBacklogError,
35
35
  DatabaseReadBacklogError,
36
+ TransactionExpiredError,
37
+ DatabaseWorkerTimeoutError,
38
+ DatabaseWorkerOutcomeUnknownError,
36
39
  LiveQueryLimitError,
37
40
  SqlCompileError,
38
41
  QueryMemoryBudgetError,
@@ -59,6 +62,8 @@ const errorRegistry = new Map([
59
62
  PostingBuildConflictError,
60
63
  StorageCorruptionError,
61
64
  StorageFormatVersionError,
65
+ OpfsCoordinationError,
66
+ OpfsDatabaseInUseError,
62
67
  OpfsUncertainOutcomeError
63
68
  ].map((constructor) => [constructor.name, constructor]));
64
69
  function rehydrateError(serialized) {
@@ -98,6 +103,8 @@ class MinnowDatabaseClient {
98
103
  return this;
99
104
  }
100
105
  #transport;
106
+ #requestTimeoutMs;
107
+ #closePromise;
101
108
  #pending = /* @__PURE__ */ new Map();
102
109
  #events = /* @__PURE__ */ new Map();
103
110
  #ready;
@@ -114,6 +121,7 @@ class MinnowDatabaseClient {
114
121
  this.#fail(new Error("A database worker message could not be deserialized"));
115
122
  };
116
123
  constructor(transport, options = {}) {
124
+ this.#requestTimeoutMs = clientDeadline(options.requestTimeoutMs ?? 6e4);
117
125
  this.#schema = options.schema;
118
126
  this.#transport = transport;
119
127
  transport.addEventListener("message", this.#onMessage);
@@ -289,10 +297,10 @@ class MinnowDatabaseClient {
289
297
  try {
290
298
  const session = {
291
299
  version: opened.version,
292
- query: (sql, options = {}) => this.query(sql, {
293
- ...options,
294
- ...opened.version === null ? {} : { version: opened.version }
295
- })
300
+ query: async (sql, options = {}) => {
301
+ const { signal, onStats, ...wireOptions } = options;
302
+ return decodeQueryResult(await this._invokeControlled(opened.handleId, "query", [sql, wireOptions, onStats !== void 0], { signal, onStats }));
303
+ }
296
304
  };
297
305
  return await action(session);
298
306
  } finally {
@@ -301,18 +309,33 @@ class MinnowDatabaseClient {
301
309
  }
302
310
  async write(action) {
303
311
  const opened = await this.#call("writeOpen", []);
304
- const stage = (op, tableName, input, options) => this._invoke(opened.handleId, "stage", [
312
+ let tail = Promise.resolve();
313
+ let accepting = true;
314
+ let pending = 0;
315
+ const enqueue = (run) => {
316
+ if (!accepting)
317
+ return Promise.reject(new Error("The write scope has ended"));
318
+ if (pending >= MAX_DATABASE_RPC_IN_FLIGHT)
319
+ return Promise.reject(new RangeError("Too many pending write scope calls; await a statement"));
320
+ pending += 1;
321
+ const task = tail.then(run);
322
+ tail = task.then(() => void 0, () => void 0);
323
+ return task.finally(() => {
324
+ pending -= 1;
325
+ });
326
+ };
327
+ const stage = (op, tableName, input, options) => enqueue(() => this._invoke(opened.handleId, "stage", [
305
328
  op,
306
329
  tableName,
307
330
  input,
308
331
  options
309
- ]);
332
+ ]));
310
333
  const session = {
311
- query: async (sql, options = {}) => {
334
+ query: (sql, options = {}) => enqueue(async () => {
312
335
  const { signal, onStats, ...wireOptions } = options;
313
336
  return decodeQueryResult(await this._invokeControlled(opened.handleId, "query", [sql, wireOptions, onStats !== void 0], { signal, onStats }));
314
- },
315
- execute: (sql, params) => this._invoke(opened.handleId, "execute", params === void 0 ? [sql] : [sql, params]),
337
+ }),
338
+ execute: (sql, params) => enqueue(() => this._invoke(opened.handleId, "execute", params === void 0 ? [sql] : [sql, params])),
316
339
  insertBatch: (tableName, input) => stage("insertBatch", tableName, input),
317
340
  upsertBatch: (tableName, input, options) => stage("upsertBatch", tableName, input, options),
318
341
  updateBatch: (tableName, input) => stage("updateBatch", tableName, input),
@@ -320,9 +343,13 @@ class MinnowDatabaseClient {
320
343
  };
321
344
  try {
322
345
  const result = await action(session);
346
+ accepting = false;
347
+ await tail;
323
348
  const committed = await this._invoke(opened.handleId, "commit", []);
324
349
  return { result, version: committed.version };
325
350
  } catch (error) {
351
+ accepting = false;
352
+ await tail;
326
353
  await this._invoke(opened.handleId, "abort", []).catch(() => void 0);
327
354
  throw error;
328
355
  }
@@ -470,17 +497,22 @@ class MinnowDatabaseClient {
470
497
  async listGarbageCollectionJobs() {
471
498
  return await this.#call("listGarbageCollectionJobs", []);
472
499
  }
473
- async close(options = {}) {
474
- if (this.#closed)
475
- return;
500
+ close(options = {}) {
501
+ if (this.#closePromise !== void 0)
502
+ return this.#closePromise;
503
+ const timeoutMs = clientDeadline(options.timeoutMs ?? 5e3);
476
504
  this.#closed = true;
505
+ this.#closePromise = this.#closeTransport(options, timeoutMs);
506
+ return this.#closePromise;
507
+ }
508
+ async #closeTransport(options, timeoutMs) {
477
509
  if (this.#onVisibilityChange !== void 0 && typeof document !== "undefined") {
478
510
  document.removeEventListener("visibilitychange", this.#onVisibilityChange);
479
511
  }
480
512
  try {
481
- await this.#post("rpc-call", null, "dispose", []);
513
+ await this.#post("rpc-call", null, "dispose", [], void 0, true, { timeoutMs });
482
514
  } finally {
483
- this.#events.clear();
515
+ this.#fail(new Error("Database client is closed"));
484
516
  this.#transport.removeEventListener?.("message", this.#onMessage);
485
517
  this.#transport.removeEventListener?.("error", this.#onError);
486
518
  this.#transport.removeEventListener?.("messageerror", this.#onMessageError);
@@ -517,14 +549,29 @@ class MinnowDatabaseClient {
517
549
  throw new RangeError(`A database worker connection cannot hold more than ${String(MAX_DATABASE_RPC_IN_FLIGHT)} in-flight requests`);
518
550
  }
519
551
  const requestId = crypto.randomUUID();
552
+ const timeoutMs = controls.timeoutMs ?? this.#requestTimeoutMs;
553
+ const mayPublish = rpcMayPublish(method, args);
520
554
  return new Promise((resolve, reject) => {
555
+ const timer = setTimeout(() => this.#fail(new DatabaseWorkerTimeoutError(method, timeoutMs)), timeoutMs);
556
+ timer.unref?.();
521
557
  const onAbort = () => {
522
558
  try {
523
559
  this.#transport.postMessage({ version: protocolVersion, requestId, kind: "rpc-cancel" });
524
560
  } catch {
525
561
  }
562
+ const call = this.#pending.get(requestId);
563
+ if (call === void 0)
564
+ return;
565
+ this.#pending.delete(requestId);
566
+ call.cleanup?.();
567
+ const error = new Error("Database request was cancelled", {
568
+ cause: controls.signal?.reason
569
+ });
570
+ error.name = "AbortError";
571
+ call.reject(mayPublish ? new DatabaseWorkerOutcomeUnknownError(method, requestId, { cause: error }) : error);
526
572
  };
527
573
  const cleanup = () => {
574
+ clearTimeout(timer);
528
575
  controls.signal?.removeEventListener("abort", onAbort);
529
576
  if (controls.onStats !== void 0)
530
577
  this.#events.delete(requestId);
@@ -533,7 +580,7 @@ class MinnowDatabaseClient {
533
580
  if (controls.onStats !== void 0) {
534
581
  this.#events.set(requestId, { onStats: controls.onStats });
535
582
  }
536
- this.#pending.set(requestId, { resolve, reject, cleanup });
583
+ this.#pending.set(requestId, { resolve, reject, cleanup, method, requestId, mayPublish });
537
584
  try {
538
585
  this.#transport.postMessage(kind === "rpc-init" ? { version: protocolVersion, requestId, kind, payload: args[0] } : { version: protocolVersion, requestId, kind, handleId, method, args }, transfer === void 0 ? void 0 : { transfer });
539
586
  } catch (error) {
@@ -560,6 +607,24 @@ class MinnowDatabaseClient {
560
607
  if (response.event === "change") {
561
608
  const { result, delivery } = response.payload;
562
609
  route.onChange?.(decodeQueryResult(result), delivery);
610
+ } else if (response.event === "patch") {
611
+ const { result: payload, delivery } = response.payload;
612
+ const result = decodeQueryResult(payload);
613
+ const retained = delivery.retained;
614
+ if (!(retained instanceof Int32Array))
615
+ throw new TypeError("Live patch is missing retained positions");
616
+ const changedRows = [];
617
+ for (let index = 0; index < retained.length; index += 1) {
618
+ if ((retained[index] ?? -1) >= 0)
619
+ continue;
620
+ const row = result.rows[changedRows.length];
621
+ if (row === void 0)
622
+ throw new TypeError("Live patch is missing a changed row");
623
+ changedRows.push({ index, row });
624
+ }
625
+ if (changedRows.length !== result.rows.length)
626
+ throw new TypeError("Live patch has excess rows");
627
+ route.onPatch?.({ type: "patch", retained, changedRows }, delivery);
563
628
  } else if (response.event === "invalidate") {
564
629
  route.onInvalidate?.(response.payload);
565
630
  } else if (response.event === "error") {
@@ -597,7 +662,7 @@ class MinnowDatabaseClient {
597
662
  }
598
663
  this.#pending.delete(requestId);
599
664
  pending.cleanup?.();
600
- pending.reject(error);
665
+ pending.reject(pending.mayPublish ? new DatabaseWorkerOutcomeUnknownError(pending.method, requestId, { cause: error }) : error);
601
666
  }
602
667
  #fail(error) {
603
668
  this.#fatal = error;
@@ -606,10 +671,20 @@ class MinnowDatabaseClient {
606
671
  this.#events.clear();
607
672
  for (const call of pending) {
608
673
  call.cleanup?.();
609
- call.reject(error);
674
+ call.reject(call.mayPublish ? new DatabaseWorkerOutcomeUnknownError(call.method, call.requestId, { cause: error }) : error);
610
675
  }
611
676
  }
612
677
  }
678
+ function clientDeadline(value) {
679
+ if (!Number.isSafeInteger(value) || value < 1 || value > 2147483647)
680
+ throw new RangeError("Worker timeout must be a positive timer interval");
681
+ return value;
682
+ }
683
+ function rpcMayPublish(method, args) {
684
+ if (method === "execute" && typeof args[0] === "string" && /^\s*SELECT\b/iu.test(args[0]))
685
+ return false;
686
+ return /^(?:insert|upsert|update|delete|create|drop|migrate|commit|flush|execute|runStatement|import|finish|add|close|dispose)/u.test(method);
687
+ }
613
688
  class ClientBufferedWriter {
614
689
  client;
615
690
  handleId;
@@ -660,11 +735,17 @@ class ClientLiveQuerySet {
660
735
  }
661
736
  #created;
662
737
  async subscribe(query, options) {
738
+ return this.#subscribe(query, options);
739
+ }
740
+ async #subscribe(query, options, onPatch) {
741
+ if (typeof query !== "string")
742
+ query = structuredClone(query);
663
743
  await this.#created;
664
744
  const subscriptionId = crypto.randomUUID();
665
745
  const state = { completed: false };
666
746
  this.client._routeEvents(subscriptionId, {
667
747
  onChange: options.onChange.bind(options),
748
+ ...onPatch === void 0 ? {} : { onPatch },
668
749
  ...options.onError === void 0 ? {} : { onError: options.onError.bind(options) },
669
750
  onComplete: () => {
670
751
  state.completed = true;
@@ -675,7 +756,8 @@ class ClientLiveQuerySet {
675
756
  try {
676
757
  const created = await this.client._invoke(this.handleId, "subscribe", [
677
758
  subscriptionId,
678
- query
759
+ query,
760
+ ...onPatch === void 0 ? [] : [{ patches: true }]
679
761
  ]);
680
762
  this.#subscriptionIds.add(subscriptionId);
681
763
  return new ClientLiveSubscription(this.client, subscriptionId, created.dependencyTableIds, () => this.#subscriptionIds.delete(subscriptionId), state);
@@ -685,13 +767,15 @@ class ClientLiveQuerySet {
685
767
  }
686
768
  }
687
769
  subscribePatches(query, options) {
688
- return this.subscribe(query, {
770
+ return this.#subscribe(query, {
689
771
  onChange: (result, delivery) => options.onPatch(createLiveQueryPatch(result, delivery), delivery),
690
772
  ...options.onError === void 0 ? {} : { onError: options.onError.bind(options) },
691
773
  ...options.onComplete === void 0 ? {} : { onComplete: options.onComplete.bind(options) }
692
- });
774
+ }, options.onPatch.bind(options));
693
775
  }
694
776
  async observe(query, options) {
777
+ if (typeof query !== "string")
778
+ query = structuredClone(query);
695
779
  await this.#created;
696
780
  const subscriptionId = crypto.randomUUID();
697
781
  const state = { completed: false };
@@ -257,7 +257,7 @@ export interface QueryOptions {
257
257
  * data with the default on measures cache lookups, not query execution.
258
258
  */
259
259
  memoize?: boolean;
260
- readonly version?: number;
260
+ readonly version?: number | null;
261
261
  /**
262
262
  * Values for the statement's `?`/`$n` placeholders, in order. Required exactly when the
263
263
  * statement has placeholders; the compiled plan is cached on the SQL text and re-bound per
@@ -472,6 +472,8 @@ export interface MinnowDatabaseOptions<TSchema extends AnySchema = UntypedSchema
472
472
  targetBlockBytes?: number;
473
473
  rowsPerBlock?: number;
474
474
  maxCommitRetries?: number;
475
+ /** Coordinate autocommit writers across instances and, with Web Locks, tabs. Default true. */
476
+ coordinateWrites?: boolean;
475
477
  now?: () => Date;
476
478
  createId?: () => string;
477
479
  /** Durable spill-owner lease lifetime; renewed while a spilling query runs. */
@@ -812,8 +814,8 @@ export declare class MinnowDatabase<TSchema extends AnySchema = UntypedSchema> {
812
814
  * Creates a live-query set over this database. Local commits hint it directly, an optional
813
815
  * channel carries cross-tab hints, and an optional poll interval bounds staleness without any
814
816
  * hint; every hint path converges on the durable manifest version, so missed messages delay a
815
- * refresh but never produce a stale result. Equal subscriptions retain one private exact
816
- * result snapshot plus a digest that rejects most inequality before exact comparison.
817
+ * refresh but never produce a stale result. Equal subscriptions retain one private result
818
+ * snapshot, compared exactly before delivery.
817
819
  */
818
820
  liveQueries(options?: LiveQuerySetOptions): LiveQuerySet;
819
821
  /**