@minnowdb/core 0.7.8 → 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.
- package/dist/engine/client.d.ts +2 -2
- package/dist/engine/client.js +6 -4
- package/dist/engine/database.js +457 -18
- package/dist/engine/keyed-live.js +55 -18
- package/dist/engine/live.d.ts +95 -8
- package/dist/engine/live.js +311 -148
- package/dist/engine/typed-live.d.ts +12 -1
- package/dist/engine/typed-live.js +89 -12
- package/dist/engine/worker-server.js +12 -5
- package/dist/worker-protocol/index.d.ts +1 -1
- package/dist/worker-protocol/index.js +1 -1
- package/package.json +1 -1
|
@@ -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
|
|
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
|
|
9
|
+
return NaN;
|
|
11
10
|
if (Object.is(value, -0))
|
|
12
|
-
return "
|
|
13
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
76
|
+
row: kept,
|
|
61
77
|
from: old.index,
|
|
62
78
|
to: next.index
|
|
63
79
|
});
|
|
64
80
|
}
|
|
65
81
|
}
|
|
66
|
-
|
|
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
|
-
|
|
151
|
-
|
|
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
|
-
|
|
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 =
|
|
192
|
+
this.#rows = rows;
|
|
193
|
+
this.#sourceRows = snapshot.rows;
|
|
157
194
|
this.#hasReadySnapshot = true;
|
|
158
195
|
this.#snapshot = {
|
|
159
196
|
status: "ready",
|
|
160
|
-
rows
|
|
197
|
+
rows,
|
|
161
198
|
changes: Object.freeze(changes),
|
|
162
199
|
initial,
|
|
163
200
|
version: snapshot.version
|
package/dist/engine/live.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
10
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
84
|
-
|
|
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
|
|
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;
|