@minnowdb/core 0.7.10 → 0.9.0

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 (75) 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 +9 -0
  4. package/dist/engine/client.js +112 -20
  5. package/dist/engine/database.d.ts +5 -3
  6. package/dist/engine/database.js +2199 -1675
  7. package/dist/engine/errors.d.ts +21 -2
  8. package/dist/engine/errors.js +30 -1
  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 +264 -0
  13. package/dist/engine/live-patch.d.ts +21 -0
  14. package/dist/engine/live-patch.js +31 -0
  15. package/dist/engine/live.d.ts +14 -0
  16. package/dist/engine/live.js +146 -142
  17. package/dist/engine/optimizer.js +11 -6
  18. package/dist/engine/query-cache.js +19 -15
  19. package/dist/engine/query-generations.js +61 -0
  20. package/dist/engine/query-identity.js +41 -0
  21. package/dist/engine/query.d.ts +7 -13
  22. package/dist/engine/query.js +298 -435
  23. package/dist/engine/result-state.d.ts +7 -0
  24. package/dist/engine/result-state.js +15 -0
  25. package/dist/engine/sql-domains.js +121 -0
  26. package/dist/engine/sql-semantics.js +7 -4
  27. package/dist/engine/typed-live.js +28 -20
  28. package/dist/engine/vector.d.ts +4 -0
  29. package/dist/engine/vector.js +14 -14
  30. package/dist/engine/windows.d.ts +12 -0
  31. package/dist/engine/windows.js +387 -0
  32. package/dist/engine/worker-server.d.ts +1 -1
  33. package/dist/engine/worker-server.js +28 -15
  34. package/dist/engine/write-coordinator.js +54 -0
  35. package/dist/plan/model.d.ts +1 -1
  36. package/dist/storage/indexeddb.js +134 -89
  37. package/dist/storage/opfs/index.d.ts +1 -1
  38. package/dist/storage/opfs/index.js +3 -1
  39. package/dist/storage/opfs/leader.js +20 -9
  40. package/dist/storage/opfs/rpc.js +3 -1
  41. package/dist/storage/opfs/store.d.ts +2 -0
  42. package/dist/storage/opfs/store.js +119 -43
  43. package/dist/storage/toolkit/record-core.js +2 -1
  44. package/dist/storage/types.d.ts +14 -0
  45. package/dist/storage/types.js +21 -0
  46. package/dist/transactions/index.d.ts +3 -0
  47. package/dist/transactions/index.js +4 -1
  48. package/dist/worker-protocol/index.d.ts +1 -1
  49. package/dist/worker-protocol/index.js +1 -1
  50. package/package.json +2 -2
  51. package/postgres-feature-profile.json +6 -1
  52. package/sql-feature-matrix.json +20 -27
  53. package/dist/date-value.d.ts +0 -20
  54. package/dist/engine/artifact-cache.d.ts +0 -29
  55. package/dist/engine/byte-estimates.d.ts +0 -11
  56. package/dist/engine/cancellation.d.ts +0 -2
  57. package/dist/engine/defaults.d.ts +0 -29
  58. package/dist/engine/group-index.d.ts +0 -33
  59. package/dist/engine/join-index.d.ts +0 -10
  60. package/dist/engine/live-equal.d.ts +0 -7
  61. package/dist/engine/point-read.d.ts +0 -59
  62. package/dist/engine/query-cache.d.ts +0 -21
  63. package/dist/engine/result-wire.d.ts +0 -70
  64. package/dist/engine/sort-keys.d.ts +0 -73
  65. package/dist/engine/sql-domains.d.ts +0 -93
  66. package/dist/engine/sql-functions.d.ts +0 -11
  67. package/dist/engine/sql-json.d.ts +0 -40
  68. package/dist/engine/sql-semantics.d.ts +0 -66
  69. package/dist/engine/worker-store-indexeddb.d.ts +0 -2
  70. package/dist/engine/worker-store-memory.d.ts +0 -2
  71. package/dist/engine/worker-store-opfs.d.ts +0 -2
  72. package/dist/engine/write-block-planner.d.ts +0 -19
  73. package/dist/storage/opfs/leader.d.ts +0 -460
  74. package/dist/storage/opfs/rpc.d.ts +0 -82
  75. 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,3 +1,4 @@
1
+ import { type LiveQueryPatch, type LiveQueryPatchOptions } from "./live-patch.js";
1
2
  import { type CompactionJobRecord, type GarbageCollectionJobRecord, type StorageIntegrityMode, type StorageIntegrityReport, type StorageStats, type InterruptedSnapshotImport, type InterruptedSnapshotImportAbortResult } from "../storage/types.js";
2
3
  import { type BatchRow } from "./batch.js";
3
4
  import type { Catalog } from "./catalog.js";
@@ -46,6 +47,8 @@ export interface MinnowDatabaseClientOptions<TSchema extends AnySchema = Untyped
46
47
  store?: StoreDescriptor;
47
48
  /** Cloneable database options applied when the worker constructs the database. */
48
49
  databaseOptions?: WireDatabaseOptions;
50
+ /** Maximum response wait, including initialization; defaults to 60 seconds. */
51
+ requestTimeoutMs?: number;
49
52
  }
50
53
  export interface ClientLiveQueryOptions {
51
54
  /** BroadcastChannel name the worker uses to exchange cross-tab commit hints. */
@@ -55,6 +58,8 @@ export interface ClientLiveQueryOptions {
55
58
  export interface CloseClientOptions {
56
59
  /** Also terminate the worker after disposing; only meaningful when the transport can. */
57
60
  terminateWorker?: boolean;
61
+ /** Grace allowed for disposal before closing the transport; defaults to 5 seconds. */
62
+ timeoutMs?: number;
58
63
  }
59
64
  export interface ClientMigrationResult {
60
65
  createdTables: string[];
@@ -66,6 +71,7 @@ export interface ClientMigrationResult {
66
71
  }
67
72
  interface EventRoute {
68
73
  onChange?: (result: QueryResult, delivery: LiveQueryDelivery) => void;
74
+ onPatch?: (patch: LiveQueryPatch, delivery: LiveQueryDelivery) => void;
69
75
  onInvalidate?: (invalidation: LiveQueryInvalidation) => void;
70
76
  onError?: (error: unknown) => void;
71
77
  onComplete?: () => void;
@@ -74,6 +80,7 @@ interface EventRoute {
74
80
  onStats?: (stats: QueryExecutionStats) => void;
75
81
  }
76
82
  interface RpcCallControls {
83
+ timeoutMs?: number;
77
84
  signal?: AbortSignal | undefined;
78
85
  onStats?: ((stats: QueryExecutionStats) => void) | undefined;
79
86
  }
@@ -264,6 +271,8 @@ export declare class ClientLiveQuerySet {
264
271
  constructor(client: MinnowDatabaseClient, handleId: string, created: Promise<unknown>);
265
272
  /** Registers a query (SQL or a compiled-plan envelope) and re-runs it on relevant changes. */
266
273
  subscribe(query: LiveQueryInput, options: LiveQuerySubscribeOptions): Promise<ClientLiveSubscription>;
274
+ /** Transfers resets followed by changed row payloads and retained positions across the worker. */
275
+ subscribePatches(query: LiveQueryInput, options: LiveQueryPatchOptions): Promise<ClientLiveSubscription>;
267
276
  /** Registers dependency observation while leaving execution/result mapping to an adapter. */
268
277
  observe(query: LiveQueryInput, options: LiveQueryObserveOptions): Promise<ClientLiveSubscription>;
269
278
  stats(): Promise<LiveQueryStats>;
@@ -1,8 +1,9 @@
1
- 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";
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, OpfsCoordinationError, OpfsDatabaseInUseError, OpfsUncertainOutcomeError } from "../storage/types.js";
2
3
  import { MAX_SNAPSHOT_STREAM_CHUNK_BYTES } from "../storage/snapshot.js";
3
4
  import { parseRpcResponse, MAX_DATABASE_RPC_IN_FLIGHT, protocolVersion } from "../worker-protocol/index.js";
4
5
  import { definedVectors, toColumnarBatch } from "./batch.js";
5
- 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";
6
7
  import { QueryMemoryBudgetError } from "./memory.js";
7
8
  import { decodeQueryResult } from "./result-wire.js";
8
9
  import { serializeSchema } from "./schema-wire.js";
@@ -32,6 +33,9 @@ const errorRegistry = new Map([
32
33
  CompactionJobCancelledError,
33
34
  MaintenanceBacklogError,
34
35
  DatabaseReadBacklogError,
36
+ TransactionExpiredError,
37
+ DatabaseWorkerTimeoutError,
38
+ DatabaseWorkerOutcomeUnknownError,
35
39
  LiveQueryLimitError,
36
40
  SqlCompileError,
37
41
  QueryMemoryBudgetError,
@@ -58,6 +62,8 @@ const errorRegistry = new Map([
58
62
  PostingBuildConflictError,
59
63
  StorageCorruptionError,
60
64
  StorageFormatVersionError,
65
+ OpfsCoordinationError,
66
+ OpfsDatabaseInUseError,
61
67
  OpfsUncertainOutcomeError
62
68
  ].map((constructor) => [constructor.name, constructor]));
63
69
  function rehydrateError(serialized) {
@@ -97,6 +103,8 @@ class MinnowDatabaseClient {
97
103
  return this;
98
104
  }
99
105
  #transport;
106
+ #requestTimeoutMs;
107
+ #closePromise;
100
108
  #pending = /* @__PURE__ */ new Map();
101
109
  #events = /* @__PURE__ */ new Map();
102
110
  #ready;
@@ -113,6 +121,7 @@ class MinnowDatabaseClient {
113
121
  this.#fail(new Error("A database worker message could not be deserialized"));
114
122
  };
115
123
  constructor(transport, options = {}) {
124
+ this.#requestTimeoutMs = clientDeadline(options.requestTimeoutMs ?? 6e4);
116
125
  this.#schema = options.schema;
117
126
  this.#transport = transport;
118
127
  transport.addEventListener("message", this.#onMessage);
@@ -288,10 +297,10 @@ class MinnowDatabaseClient {
288
297
  try {
289
298
  const session = {
290
299
  version: opened.version,
291
- query: (sql, options = {}) => this.query(sql, {
292
- ...options,
293
- ...opened.version === null ? {} : { version: opened.version }
294
- })
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
+ }
295
304
  };
296
305
  return await action(session);
297
306
  } finally {
@@ -300,18 +309,33 @@ class MinnowDatabaseClient {
300
309
  }
301
310
  async write(action) {
302
311
  const opened = await this.#call("writeOpen", []);
303
- 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", [
304
328
  op,
305
329
  tableName,
306
330
  input,
307
331
  options
308
- ]);
332
+ ]));
309
333
  const session = {
310
- query: async (sql, options = {}) => {
334
+ query: (sql, options = {}) => enqueue(async () => {
311
335
  const { signal, onStats, ...wireOptions } = options;
312
336
  return decodeQueryResult(await this._invokeControlled(opened.handleId, "query", [sql, wireOptions, onStats !== void 0], { signal, onStats }));
313
- },
314
- 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])),
315
339
  insertBatch: (tableName, input) => stage("insertBatch", tableName, input),
316
340
  upsertBatch: (tableName, input, options) => stage("upsertBatch", tableName, input, options),
317
341
  updateBatch: (tableName, input) => stage("updateBatch", tableName, input),
@@ -319,9 +343,13 @@ class MinnowDatabaseClient {
319
343
  };
320
344
  try {
321
345
  const result = await action(session);
346
+ accepting = false;
347
+ await tail;
322
348
  const committed = await this._invoke(opened.handleId, "commit", []);
323
349
  return { result, version: committed.version };
324
350
  } catch (error) {
351
+ accepting = false;
352
+ await tail;
325
353
  await this._invoke(opened.handleId, "abort", []).catch(() => void 0);
326
354
  throw error;
327
355
  }
@@ -469,17 +497,22 @@ class MinnowDatabaseClient {
469
497
  async listGarbageCollectionJobs() {
470
498
  return await this.#call("listGarbageCollectionJobs", []);
471
499
  }
472
- async close(options = {}) {
473
- if (this.#closed)
474
- return;
500
+ close(options = {}) {
501
+ if (this.#closePromise !== void 0)
502
+ return this.#closePromise;
503
+ const timeoutMs = clientDeadline(options.timeoutMs ?? 5e3);
475
504
  this.#closed = true;
505
+ this.#closePromise = this.#closeTransport(options, timeoutMs);
506
+ return this.#closePromise;
507
+ }
508
+ async #closeTransport(options, timeoutMs) {
476
509
  if (this.#onVisibilityChange !== void 0 && typeof document !== "undefined") {
477
510
  document.removeEventListener("visibilitychange", this.#onVisibilityChange);
478
511
  }
479
512
  try {
480
- await this.#post("rpc-call", null, "dispose", []);
513
+ await this.#post("rpc-call", null, "dispose", [], void 0, true, { timeoutMs });
481
514
  } finally {
482
- this.#events.clear();
515
+ this.#fail(new Error("Database client is closed"));
483
516
  this.#transport.removeEventListener?.("message", this.#onMessage);
484
517
  this.#transport.removeEventListener?.("error", this.#onError);
485
518
  this.#transport.removeEventListener?.("messageerror", this.#onMessageError);
@@ -516,14 +549,29 @@ class MinnowDatabaseClient {
516
549
  throw new RangeError(`A database worker connection cannot hold more than ${String(MAX_DATABASE_RPC_IN_FLIGHT)} in-flight requests`);
517
550
  }
518
551
  const requestId = crypto.randomUUID();
552
+ const timeoutMs = controls.timeoutMs ?? this.#requestTimeoutMs;
553
+ const mayPublish = rpcMayPublish(method, args);
519
554
  return new Promise((resolve, reject) => {
555
+ const timer = setTimeout(() => this.#fail(new DatabaseWorkerTimeoutError(method, timeoutMs)), timeoutMs);
556
+ timer.unref?.();
520
557
  const onAbort = () => {
521
558
  try {
522
559
  this.#transport.postMessage({ version: protocolVersion, requestId, kind: "rpc-cancel" });
523
560
  } catch {
524
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);
525
572
  };
526
573
  const cleanup = () => {
574
+ clearTimeout(timer);
527
575
  controls.signal?.removeEventListener("abort", onAbort);
528
576
  if (controls.onStats !== void 0)
529
577
  this.#events.delete(requestId);
@@ -532,7 +580,7 @@ class MinnowDatabaseClient {
532
580
  if (controls.onStats !== void 0) {
533
581
  this.#events.set(requestId, { onStats: controls.onStats });
534
582
  }
535
- this.#pending.set(requestId, { resolve, reject, cleanup });
583
+ this.#pending.set(requestId, { resolve, reject, cleanup, method, requestId, mayPublish });
536
584
  try {
537
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 });
538
586
  } catch (error) {
@@ -559,6 +607,24 @@ class MinnowDatabaseClient {
559
607
  if (response.event === "change") {
560
608
  const { result, delivery } = response.payload;
561
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);
562
628
  } else if (response.event === "invalidate") {
563
629
  route.onInvalidate?.(response.payload);
564
630
  } else if (response.event === "error") {
@@ -596,7 +662,7 @@ class MinnowDatabaseClient {
596
662
  }
597
663
  this.#pending.delete(requestId);
598
664
  pending.cleanup?.();
599
- pending.reject(error);
665
+ pending.reject(pending.mayPublish ? new DatabaseWorkerOutcomeUnknownError(pending.method, requestId, { cause: error }) : error);
600
666
  }
601
667
  #fail(error) {
602
668
  this.#fatal = error;
@@ -605,10 +671,20 @@ class MinnowDatabaseClient {
605
671
  this.#events.clear();
606
672
  for (const call of pending) {
607
673
  call.cleanup?.();
608
- call.reject(error);
674
+ call.reject(call.mayPublish ? new DatabaseWorkerOutcomeUnknownError(call.method, call.requestId, { cause: error }) : error);
609
675
  }
610
676
  }
611
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
+ }
612
688
  class ClientBufferedWriter {
613
689
  client;
614
690
  handleId;
@@ -659,11 +735,17 @@ class ClientLiveQuerySet {
659
735
  }
660
736
  #created;
661
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);
662
743
  await this.#created;
663
744
  const subscriptionId = crypto.randomUUID();
664
745
  const state = { completed: false };
665
746
  this.client._routeEvents(subscriptionId, {
666
747
  onChange: options.onChange.bind(options),
748
+ ...onPatch === void 0 ? {} : { onPatch },
667
749
  ...options.onError === void 0 ? {} : { onError: options.onError.bind(options) },
668
750
  onComplete: () => {
669
751
  state.completed = true;
@@ -674,7 +756,8 @@ class ClientLiveQuerySet {
674
756
  try {
675
757
  const created = await this.client._invoke(this.handleId, "subscribe", [
676
758
  subscriptionId,
677
- query
759
+ query,
760
+ ...onPatch === void 0 ? [] : [{ patches: true }]
678
761
  ]);
679
762
  this.#subscriptionIds.add(subscriptionId);
680
763
  return new ClientLiveSubscription(this.client, subscriptionId, created.dependencyTableIds, () => this.#subscriptionIds.delete(subscriptionId), state);
@@ -683,7 +766,16 @@ class ClientLiveQuerySet {
683
766
  throw error;
684
767
  }
685
768
  }
769
+ subscribePatches(query, options) {
770
+ return this.#subscribe(query, {
771
+ onChange: (result, delivery) => options.onPatch(createLiveQueryPatch(result, delivery), delivery),
772
+ ...options.onError === void 0 ? {} : { onError: options.onError.bind(options) },
773
+ ...options.onComplete === void 0 ? {} : { onComplete: options.onComplete.bind(options) }
774
+ }, options.onPatch.bind(options));
775
+ }
686
776
  async observe(query, options) {
777
+ if (typeof query !== "string")
778
+ query = structuredClone(query);
687
779
  await this.#created;
688
780
  const subscriptionId = crypto.randomUUID();
689
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
  /**