@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,7 +1,10 @@
|
|
|
1
|
-
import type { LiveQueryInput, LiveQueryObserveOptions } from "./live.js";
|
|
1
|
+
import type { LiveQueryInput, LiveQueryObserveOptions, LiveQuerySubscribeOptions } from "./live.js";
|
|
2
|
+
import type { QueryResult } from "./query.js";
|
|
2
3
|
/** The structural live-query surface shared by MinnowDatabase and its worker client. */
|
|
3
4
|
export interface LiveQueryBackend {
|
|
4
5
|
observe(query: LiveQueryInput, options: LiveQueryObserveOptions): Promise<LiveQuerySubscriptionLike>;
|
|
6
|
+
/** Result delivery, used when the source can decode the engine's result itself. */
|
|
7
|
+
subscribe?(query: LiveQueryInput, options: LiveQuerySubscribeOptions): Promise<LiveQuerySubscriptionLike>;
|
|
5
8
|
refresh(): Promise<void>;
|
|
6
9
|
close(): void | Promise<void>;
|
|
7
10
|
}
|
|
@@ -22,6 +25,14 @@ export interface LiveQueryDriver {
|
|
|
22
25
|
export interface LiveQuerySource<out TRow> {
|
|
23
26
|
readonly query: LiveQueryInput;
|
|
24
27
|
execute(signal?: AbortSignal): Promise<readonly TRow[]>;
|
|
28
|
+
/**
|
|
29
|
+
* Turns a result the engine delivered into the adapter's rows. With it, the query subscribes
|
|
30
|
+
* for results rather than invalidations: the engine executes or patches the statement where
|
|
31
|
+
* the data is, compares, and hands over a changed result once — over a worker channel, as
|
|
32
|
+
* one columnar transfer — and `execute` is never called after the statement is registered.
|
|
33
|
+
* Without it, an invalidation is followed by `execute`, which the engine's memo serves.
|
|
34
|
+
*/
|
|
35
|
+
decode?(result: QueryResult): readonly TRow[] | Promise<readonly TRow[]>;
|
|
25
36
|
}
|
|
26
37
|
export type LiveSnapshot<TRow> = {
|
|
27
38
|
readonly status: "loading";
|
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
import { sameLiveValue } from "./live-equal.js";
|
|
2
|
-
function
|
|
3
|
-
|
|
2
|
+
function reconcileRows(previous, next, retained) {
|
|
3
|
+
const rows = new Array(next.length);
|
|
4
|
+
let changed = previous.length !== next.length;
|
|
5
|
+
const provenance = retained?.length === next.length ? retained : void 0;
|
|
6
|
+
for (let index = 0; index < next.length; index += 1) {
|
|
7
|
+
const row = next[index];
|
|
8
|
+
const was = provenance?.[index] ?? -1;
|
|
9
|
+
if (was >= 0 && was < previous.length) {
|
|
10
|
+
rows[index] = previous[was];
|
|
11
|
+
if (was !== index)
|
|
12
|
+
changed = true;
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
const before = previous[index];
|
|
16
|
+
if (index < previous.length && sameLiveValue(before, row))
|
|
17
|
+
rows[index] = before;
|
|
18
|
+
else {
|
|
19
|
+
rows[index] = row;
|
|
20
|
+
changed = true;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return changed ? Object.freeze(rows) : void 0;
|
|
4
24
|
}
|
|
5
25
|
class LiveQuery {
|
|
6
26
|
#listeners = /* @__PURE__ */ new Set();
|
|
@@ -11,6 +31,9 @@ class LiveQuery {
|
|
|
11
31
|
#subscription;
|
|
12
32
|
#observationGeneration = 0;
|
|
13
33
|
#queued;
|
|
34
|
+
#lastDelivered;
|
|
35
|
+
#deliveriesReceived = 0;
|
|
36
|
+
#rowsFromDelivery = 0;
|
|
14
37
|
#execution;
|
|
15
38
|
#executionAbort;
|
|
16
39
|
#invalidationSequence = 0;
|
|
@@ -52,10 +75,24 @@ class LiveQuery {
|
|
|
52
75
|
}
|
|
53
76
|
if (this.#invalidationSequence === sequence) {
|
|
54
77
|
const version = this.#snapshot.status === "loading" ? null : this.#snapshot.version;
|
|
55
|
-
this.#
|
|
78
|
+
if (this.#decodes()) {
|
|
79
|
+
const last = this.#lastDelivered;
|
|
80
|
+
if (this.#snapshot.status === "error" && last !== void 0)
|
|
81
|
+
this.#schedule(last);
|
|
82
|
+
} else
|
|
83
|
+
this.#schedule({ manifestVersion: version, catalogEpoch: 0, initial: false });
|
|
56
84
|
}
|
|
57
85
|
await this.#waitForIdle();
|
|
58
86
|
}
|
|
87
|
+
#decodes() {
|
|
88
|
+
return this.#source.decode !== void 0 && this.#backend.subscribe !== void 0;
|
|
89
|
+
}
|
|
90
|
+
async #decodeDelivered(result) {
|
|
91
|
+
const source = this.#source;
|
|
92
|
+
if (source.decode === void 0)
|
|
93
|
+
throw new TypeError("Live query source lost its decoder");
|
|
94
|
+
return source.decode(result);
|
|
95
|
+
}
|
|
59
96
|
close() {
|
|
60
97
|
if (this.#closed)
|
|
61
98
|
return;
|
|
@@ -75,7 +112,36 @@ class LiveQuery {
|
|
|
75
112
|
if (this.#subscription !== void 0 || this.#closed)
|
|
76
113
|
return;
|
|
77
114
|
const generation = this.#observationGeneration += 1;
|
|
115
|
+
if (this.#source.decode !== void 0 && this.#backend.subscribe !== void 0) {
|
|
116
|
+
const subscription2 = this.#backend.subscribe(this.#source.query, {
|
|
117
|
+
onChange: (result, delivery) => {
|
|
118
|
+
if (generation !== this.#observationGeneration || this.#closed)
|
|
119
|
+
return;
|
|
120
|
+
this.#deliveriesReceived += 1;
|
|
121
|
+
this.#schedule({ result, delivery, sequence: this.#deliveriesReceived });
|
|
122
|
+
},
|
|
123
|
+
onError: (error) => {
|
|
124
|
+
if (generation !== this.#observationGeneration || this.#closed)
|
|
125
|
+
return;
|
|
126
|
+
this.#setError(error, this.#currentVersion());
|
|
127
|
+
},
|
|
128
|
+
onComplete: () => {
|
|
129
|
+
if (generation !== this.#observationGeneration)
|
|
130
|
+
return;
|
|
131
|
+
this.#subscription = void 0;
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
this.#subscription = subscription2;
|
|
135
|
+
subscription2.catch((error) => {
|
|
136
|
+
if (generation !== this.#observationGeneration || this.#closed)
|
|
137
|
+
return;
|
|
138
|
+
this.#subscription = void 0;
|
|
139
|
+
this.#setError(error, this.#currentVersion());
|
|
140
|
+
});
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
78
143
|
const subscription = this.#backend.observe(this.#source.query, {
|
|
144
|
+
suppressUnchanged: true,
|
|
79
145
|
onInvalidate: (invalidation) => {
|
|
80
146
|
if (generation !== this.#observationGeneration || this.#closed)
|
|
81
147
|
return;
|
|
@@ -109,9 +175,9 @@ class LiveQuery {
|
|
|
109
175
|
void subscription.then((handle) => handle.close()).catch(() => void 0);
|
|
110
176
|
}
|
|
111
177
|
}
|
|
112
|
-
#schedule(
|
|
178
|
+
#schedule(work) {
|
|
113
179
|
this.#invalidationSequence += 1;
|
|
114
|
-
this.#queued =
|
|
180
|
+
this.#queued = work;
|
|
115
181
|
if (this.#execution !== void 0)
|
|
116
182
|
return;
|
|
117
183
|
const execution = this.#drain();
|
|
@@ -125,24 +191,35 @@ class LiveQuery {
|
|
|
125
191
|
}
|
|
126
192
|
async #drain() {
|
|
127
193
|
while (this.#queued !== void 0 && !this.#closed) {
|
|
128
|
-
const
|
|
194
|
+
const work = this.#queued;
|
|
129
195
|
this.#queued = void 0;
|
|
130
196
|
const abort = new AbortController();
|
|
131
197
|
this.#executionAbort = abort;
|
|
198
|
+
const invalidation = "result" in work ? work.delivery : work;
|
|
132
199
|
try {
|
|
133
|
-
|
|
200
|
+
let executed;
|
|
201
|
+
if ("result" in work) {
|
|
202
|
+
this.#lastDelivered = work;
|
|
203
|
+
executed = await this.#decodeDelivered(work.result);
|
|
204
|
+
this.#lastDelivered = void 0;
|
|
205
|
+
} else
|
|
206
|
+
executed = await this.#source.execute(abort.signal);
|
|
134
207
|
if (this.#executionWasCancelled(abort))
|
|
135
208
|
continue;
|
|
136
209
|
if (this.#hasQueuedInvalidation())
|
|
137
210
|
continue;
|
|
138
211
|
const previous = this.#snapshot.rows;
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
212
|
+
const provenance = "result" in work && this.#rowsFromDelivery === work.sequence - 1 ? work.delivery.retained : void 0;
|
|
213
|
+
const rows = reconcileRows(previous, executed, provenance);
|
|
214
|
+
if ("result" in work)
|
|
215
|
+
this.#rowsFromDelivery = work.sequence;
|
|
216
|
+
if (rows === void 0) {
|
|
217
|
+
if (this.#snapshot.status === "ready" && this.#snapshot.version === invalidation.manifestVersion) {
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
143
220
|
this.#snapshot = {
|
|
144
221
|
status: "ready",
|
|
145
|
-
rows: previous,
|
|
222
|
+
rows: this.#snapshot.status === "loading" ? Object.freeze([...previous]) : previous,
|
|
146
223
|
version: invalidation.manifestVersion
|
|
147
224
|
};
|
|
148
225
|
} else {
|
|
@@ -379,7 +379,8 @@ class DatabaseRpcServer {
|
|
|
379
379
|
try {
|
|
380
380
|
set = this.database.liveQueries({
|
|
381
381
|
...channelName === void 0 ? {} : { channelName },
|
|
382
|
-
...pollIntervalMs === void 0 ? {} : { pollIntervalMs }
|
|
382
|
+
...pollIntervalMs === void 0 ? {} : { pollIntervalMs },
|
|
383
|
+
sharedResults: true
|
|
383
384
|
});
|
|
384
385
|
this.#publishHandle(handleId, { type: "live-set", set, subscriptionIds: /* @__PURE__ */ new Set() });
|
|
385
386
|
} catch (error) {
|
|
@@ -667,11 +668,15 @@ class DatabaseRpcServer {
|
|
|
667
668
|
let subscription;
|
|
668
669
|
try {
|
|
669
670
|
subscription = await handle.set.subscribe(query, {
|
|
670
|
-
onChange: (result) => {
|
|
671
|
+
onChange: (result, delivery) => {
|
|
671
672
|
const encoded = encodeQueryResult(result);
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
673
|
+
const retained = delivery.retained?.slice();
|
|
674
|
+
if (retained !== void 0)
|
|
675
|
+
encoded.transfer.push(retained.buffer);
|
|
676
|
+
this.scope.postMessage(rpcEvent(subscriptionId, "change", {
|
|
677
|
+
result: encoded.payload,
|
|
678
|
+
delivery: retained === void 0 ? delivery : { ...delivery, retained }
|
|
679
|
+
}), { transfer: encoded.transfer });
|
|
675
680
|
},
|
|
676
681
|
onError: (error) => {
|
|
677
682
|
this.scope.postMessage(rpcEvent(subscriptionId, "error", serializeError(error)));
|
|
@@ -700,9 +705,11 @@ class DatabaseRpcServer {
|
|
|
700
705
|
case "observe": {
|
|
701
706
|
const subscriptionId = this.#claimHandleId(args[0]);
|
|
702
707
|
const query = args[1];
|
|
708
|
+
const { suppressUnchanged } = args[2] ?? {};
|
|
703
709
|
let subscription;
|
|
704
710
|
try {
|
|
705
711
|
subscription = await handle.set.observe(query, {
|
|
712
|
+
...suppressUnchanged === true ? { suppressUnchanged: true } : {},
|
|
706
713
|
onInvalidate: (invalidation) => {
|
|
707
714
|
this.scope.postMessage(rpcEvent(subscriptionId, "invalidate", invalidation));
|
|
708
715
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@minnowdb/core",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.10",
|
|
4
4
|
"description": "A columnar SQL database for the browser: PostgreSQL-style SQL over durable IndexedDB or OPFS data, with no server or WebAssembly module.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Eric Wilhite",
|