@minnowdb/core 0.7.7 → 0.7.10

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,23 +1,29 @@
1
1
  import { dateMilliseconds } from "../date-value.js";
2
2
  import { sameLiveValue } from "./live-equal.js";
3
+ const dateTokens = /* @__PURE__ */ new Map();
3
4
  function keyToken(value, name) {
4
- if (typeof value === "string")
5
- return `s:${String(value.length)}:${value}`;
6
- if (typeof value === "boolean")
7
- return value ? "b:1" : "b:0";
5
+ if (typeof value === "string" || typeof value === "boolean")
6
+ return value;
8
7
  if (typeof value === "number") {
9
8
  if (Number.isNaN(value))
10
- return "n:NaN";
9
+ return NaN;
11
10
  if (Object.is(value, -0))
12
- return "n:-0";
13
- return `n:${String(value)}`;
11
+ return /* @__PURE__ */ Symbol.for("minnow.live.key.-0");
12
+ return value;
14
13
  }
15
14
  if (value instanceof Date) {
16
15
  const time = dateMilliseconds(value);
17
16
  if (!Number.isFinite(time)) {
18
17
  throw new TypeError(`Live query key ${String(name)} must be a valid Date`);
19
18
  }
20
- return `d:${String(time)}`;
19
+ let token = dateTokens.get(time);
20
+ if (token === void 0) {
21
+ token = /* @__PURE__ */ Symbol(`minnow.live.key.date:${String(time)}`);
22
+ dateTokens.set(time, token);
23
+ if (dateTokens.size > 65536)
24
+ dateTokens.clear();
25
+ }
26
+ return token;
21
27
  }
22
28
  throw new TypeError(`Live query key ${String(name)} must be a non-null string, number, boolean, or Date`);
23
29
  }
@@ -35,8 +41,7 @@ function indexRows(rows, key) {
35
41
  }
36
42
  return indexed;
37
43
  }
38
- function diffRows(previousRows, rows, key) {
39
- const previous = indexRows(previousRows, key);
44
+ function diffRows(previous, previousRows, rows, key) {
40
45
  const current = indexRows(rows, key);
41
46
  const changes = [];
42
47
  for (const [token, old] of previous) {
@@ -44,26 +49,44 @@ function diffRows(previousRows, rows, key) {
44
49
  continue;
45
50
  changes.push({ type: "delete", key: old.row[key], previous: old.row, index: old.index });
46
51
  }
52
+ let reused = 0;
53
+ const reconciled = new Array(rows.length);
47
54
  for (const [token, next] of current) {
48
55
  const old = previous.get(token);
49
56
  if (old === void 0) {
57
+ reconciled[next.index] = next.row;
50
58
  changes.push({ type: "insert", row: next.row, index: next.index });
51
59
  continue;
52
60
  }
53
- if (!sameLiveValue(old.row, next.row)) {
61
+ let kept = next.row;
62
+ if (old.row === next.row || sameLiveValue(old.row, next.row)) {
63
+ kept = old.row;
64
+ if (old.row !== next.row) {
65
+ current.set(token, { row: old.row, index: next.index });
66
+ reused += 1;
67
+ }
68
+ } else {
54
69
  changes.push({ type: "update", row: next.row, previous: old.row, index: next.index });
55
70
  }
71
+ reconciled[next.index] = kept;
56
72
  if (old.index !== next.index) {
57
73
  changes.push({
58
74
  type: "move",
59
75
  key: next.row[key],
60
- row: next.row,
76
+ row: kept,
61
77
  from: old.index,
62
78
  to: next.index
63
79
  });
64
80
  }
65
81
  }
66
- return changes;
82
+ if (changes.length === 0 && rows.length === previousRows.length) {
83
+ return { changes, rows: previousRows, index: current };
84
+ }
85
+ return {
86
+ changes,
87
+ rows: reused === 0 ? rows : Object.freeze(reconciled),
88
+ index: current
89
+ };
67
90
  }
68
91
  class KeyedLiveQuery {
69
92
  #source;
@@ -72,6 +95,8 @@ class KeyedLiveQuery {
72
95
  #listeners = /* @__PURE__ */ new Set();
73
96
  #sourceUnsubscribe;
74
97
  #rows = [];
98
+ #sourceRows;
99
+ #index;
75
100
  #snapshot = { status: "loading", rows: [] };
76
101
  #hasReadySnapshot = false;
77
102
  #closed = false;
@@ -141,23 +166,35 @@ class KeyedLiveQuery {
141
166
  this.#emit();
142
167
  return;
143
168
  }
169
+ if (this.#snapshot.status === "ready" && snapshot.rows === this.#sourceRows && this.#snapshot.version === snapshot.version) {
170
+ return;
171
+ }
144
172
  try {
145
173
  if (this.#maxRows !== void 0 && snapshot.rows.length > this.#maxRows) {
146
174
  throw new RangeError(`Live query window returned ${String(snapshot.rows.length)} rows; maximum is ${String(this.#maxRows)}`);
147
175
  }
148
176
  const initial = !this.#hasReadySnapshot;
149
177
  let changes;
150
- if (initial) {
151
- indexRows(snapshot.rows, this.#key);
178
+ let rows;
179
+ if (initial || this.#index === void 0) {
180
+ this.#index = indexRows(snapshot.rows, this.#key);
181
+ rows = snapshot.rows;
152
182
  changes = snapshot.rows.map((row, index) => ({ type: "insert", row, index }));
183
+ } else if (snapshot.rows === this.#sourceRows) {
184
+ rows = this.#rows;
185
+ changes = [];
153
186
  } else {
154
- changes = diffRows(this.#rows, snapshot.rows, this.#key);
187
+ const diff = diffRows(this.#index, this.#rows, snapshot.rows, this.#key);
188
+ this.#index = diff.index;
189
+ rows = diff.rows;
190
+ changes = diff.changes;
155
191
  }
156
- this.#rows = snapshot.rows;
192
+ this.#rows = rows;
193
+ this.#sourceRows = snapshot.rows;
157
194
  this.#hasReadySnapshot = true;
158
195
  this.#snapshot = {
159
196
  status: "ready",
160
- rows: snapshot.rows,
197
+ rows,
161
198
  changes: Object.freeze(changes),
162
199
  initial,
163
200
  version: snapshot.version
@@ -6,8 +6,11 @@ import { type CompiledQuery, type QueryResult, type QueryValue } from "./query.j
6
6
  * table sets then decide which prepared queries may be stale. Hints may be lost, duplicated, or
7
7
  * reordered without changing correctness.
8
8
  *
9
- * Equal statements share one dependency record and one execution per sweep. Results retain an
10
- * exact private snapshot: a 32-bit digest is only a fast inequality check, never proof of equality.
9
+ * A sweep costs what changed, not what is subscribed: the commit window's table set selects the
10
+ * groups to visit through a per-table index, and a group nobody visits keeps its result on the
11
+ * strength of the invariant that every window touching one of its tables would have visited it.
12
+ * Equal statements share one dependency record and one execution per sweep. Results are compared
13
+ * exactly, row by row, so an unchanged result never reaches a subscriber.
11
14
  */
12
15
  export interface LiveQueryHintChannel {
13
16
  postMessage(message: unknown): void;
@@ -24,6 +27,20 @@ export interface LiveQuerySetOptions {
24
27
  readonly maxGroups?: number;
25
28
  /** Maximum result/observer subscriptions retained by this set. Defaults to 1,024. */
26
29
  readonly maxSubscriptions?: number;
30
+ /**
31
+ * Hand `onChange` the set's retained result instead of a private copy. The result is shared
32
+ * with every equal subscription and with the next change comparison, so a subscriber must
33
+ * treat it as read-only. A consumer that only reads it synchronously — the worker host that
34
+ * encodes it for the channel, a renderer that copies what it displays — saves one full copy
35
+ * per subscriber per change.
36
+ */
37
+ readonly sharedResults?: boolean;
38
+ /**
39
+ * Whether the set may patch a retained result from a commit's rows instead of re-running the
40
+ * statement, for statements the host can maintain that way. Defaults to true; false makes
41
+ * every relevant commit a full execution, which is useful when comparing the two.
42
+ */
43
+ readonly incremental?: boolean;
27
44
  /** Called once when the set closes; the owner uses this to drop its reference. */
28
45
  readonly onClosed?: () => void;
29
46
  }
@@ -33,8 +50,21 @@ export declare const MAX_LIVE_QUERY_GROUPS = 4096;
33
50
  export declare const MAX_LIVE_QUERY_SUBSCRIPTIONS = 16384;
34
51
  export declare const MAX_LIVE_QUERY_SETS_PER_DATABASE = 256;
35
52
  export { LiveQueryLimitError } from "./errors.js";
53
+ /** What a delivered result reflects: the probe it is current as of, and whether it is the first. */
54
+ export interface LiveQueryDelivery {
55
+ readonly manifestVersion: number | null;
56
+ readonly catalogEpoch: number;
57
+ readonly initial: boolean;
58
+ /**
59
+ * For each row, the index it held in this subscription's previous delivery, or -1 for a row
60
+ * that is new or changed. Present when the engine kept row objects across the change — a
61
+ * patched result keeps every untouched row — and absent after a full execution or on the
62
+ * first delivery. A consumer that keys on row identity substitutes its own previous objects.
63
+ */
64
+ readonly retained?: Int32Array;
65
+ }
36
66
  export interface LiveQuerySubscribeOptions {
37
- onChange(result: QueryResult): void;
67
+ onChange(result: QueryResult, delivery: LiveQueryDelivery): void;
38
68
  onError?(error: unknown): void;
39
69
  /** Called once when the subscription ends because the subscription or its set closed. */
40
70
  onComplete?(): void;
@@ -48,6 +78,14 @@ export interface LiveQueryObserveOptions {
48
78
  onInvalidate(invalidation: LiveQueryInvalidation): void;
49
79
  onError?(error: unknown): void;
50
80
  onComplete?(): void;
81
+ /**
82
+ * Execute the statement inside the set on every relevant commit and invalidate only when the
83
+ * rows changed. The engine keeps that execution in its result memo, so an adapter that then
84
+ * re-executes the same statement at the same version is served from cache rather than from a
85
+ * second scan. A commit that leaves the rows as they were costs one execution and reaches no
86
+ * observer at all — nothing crosses a worker channel and nothing re-renders.
87
+ */
88
+ readonly suppressUnchanged?: boolean;
51
89
  }
52
90
  export interface LiveQuerySubscription {
53
91
  readonly dependencyTableIds: readonly string[];
@@ -58,12 +96,24 @@ export interface LiveQueryStats {
58
96
  versionChecks: number;
59
97
  sweeps: number;
60
98
  reruns: number;
99
+ /** Subscribed groups a sweep did not re-run: nothing they read changed, or a proof said so. */
61
100
  rerunsAvoided: number;
62
101
  /** Re-runs skipped because the data layer proved the commits could not change the result. */
63
102
  zoneSkips: number;
103
+ /** Deliveries withheld because an execution produced exactly the rows already delivered. */
64
104
  notificationsSuppressed: number;
65
- /** Observer-only invalidations delivered without executing the statement inside the set. */
105
+ /** Observer invalidations delivered. */
66
106
  invalidations: number;
107
+ /** Re-runs answered by patching the retained result with the commit's rows instead. */
108
+ maintained: number;
109
+ /** Groups a sweep looked at: those whose tables the commits changed, plus any left lagging. */
110
+ groupsVisited: number;
111
+ /**
112
+ * Rows the set currently retains across every group's last result — what its subscriptions
113
+ * display, counted once per distinct statement. A window's margin beyond its visible rows is
114
+ * bounded by the engine at 64 rows and is not included.
115
+ */
116
+ retainedRows: number;
67
117
  /** Work avoided because equal statements shared one query group or in-flight execution. */
68
118
  sharedExecutions: number;
69
119
  lastSweepMs: number;
@@ -77,11 +127,48 @@ export type LiveQueryInput = string | {
77
127
  kind: "typed-query";
78
128
  plan: CompiledQuery;
79
129
  };
80
- interface LiveQueryHost {
130
+ /** What the set already knows when it asks the host to execute a statement. */
131
+ export interface LiveQueryExecuteContext {
132
+ /**
133
+ * A freshness probe the set read moments ago. The host may start execution from it instead of
134
+ * reading its own; a result may still observe a newer commit, and the set treats the probe as
135
+ * a lower bound on what the result reflects.
136
+ */
137
+ readonly probe: CatalogProbe;
138
+ /**
139
+ * Whether the host should keep the result in its memo. The set retains its own copy, so a
140
+ * memo entry only pays off when another caller — an adapter re-executing after an
141
+ * invalidation — will ask for the same statement at the same version.
142
+ */
143
+ readonly memoize: boolean;
144
+ }
145
+ /** A maintainable statement's execution: its result and the host's opaque state for patching it. */
146
+ export interface LiveMaintainedExecution {
147
+ readonly result: QueryResult;
148
+ readonly state: unknown;
149
+ }
150
+ export interface LiveMaintainedChange extends LiveMaintainedExecution {
151
+ readonly changed: boolean;
152
+ /** For each row of `result`, its index in the previous result, or -1; see `LiveQueryDelivery`. */
153
+ readonly retained?: Int32Array;
154
+ }
155
+ export interface LiveQueryHost {
81
156
  currentProbe(): Promise<CatalogProbe>;
82
157
  manifestPage(afterVersion: number | null, limit: number): Promise<StoragePage<Manifest, number>>;
83
- dependencyTableIds(query: LiveQueryInput): Promise<Set<string>>;
84
- execute(query: LiveQueryInput): Promise<QueryResult>;
158
+ /** The base tables the statement reads, resolved through views; `probe` is a recent read. */
159
+ dependencyTableIds(query: LiveQueryInput, probe?: CatalogProbe): Promise<Set<string>>;
160
+ /** Executes the statement; the returned result belongs to the set and is never shared. */
161
+ execute(query: LiveQueryInput, context?: LiveQueryExecuteContext): Promise<QueryResult>;
162
+ /**
163
+ * Executes a statement the host can later maintain incrementally, or returns undefined when
164
+ * the statement's shape rules that out; the set then executes it in full from then on.
165
+ */
166
+ executeMaintainable?(query: LiveQueryInput, context?: LiveQueryExecuteContext): Promise<LiveMaintainedExecution | undefined>;
167
+ /**
168
+ * Patches a retained result with the row changes the commits in (after, until] made to
169
+ * `tableIds`, or returns undefined when only a full execution can answer.
170
+ */
171
+ maintain?(query: LiveQueryInput, result: QueryResult, state: unknown, tableIds: readonly string[], after: number | null, until: number, probe: CatalogProbe): Promise<LiveMaintainedChange | undefined>;
85
172
  /** Returns false only on proof that the commit window cannot affect the statement. */
86
173
  changeCanAffect?(query: LiveQueryInput, tableIds: readonly string[], after: number | null, until: number): Promise<boolean>;
87
174
  }
@@ -91,7 +178,7 @@ export declare class LiveQuerySet {
91
178
  get stats(): LiveQueryStats;
92
179
  /** Registers a query, delivers its current result, and shares work with equal statements. */
93
180
  subscribe(query: LiveQueryInput, options: LiveQuerySubscribeOptions): Promise<LiveQuerySubscription>;
94
- /** Observes invalidation without executing the statement inside the set. */
181
+ /** Observes invalidation; the statement executes inside the set only when asked to compare. */
95
182
  observe(query: LiveQueryInput, options: LiveQueryObserveOptions): Promise<LiveQuerySubscription>;
96
183
  /** Called by the owning database after each local write commit; also hints other tabs. */
97
184
  notifyLocalCommit(): void;