@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.
@@ -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 immutableRows(rows) {
3
- return Object.freeze([...rows]);
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.#schedule({ manifestVersion: version, catalogEpoch: 0, initial: false });
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(invalidation) {
178
+ #schedule(work) {
113
179
  this.#invalidationSequence += 1;
114
- this.#queued = invalidation;
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 invalidation = this.#queued;
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
- const rows = immutableRows(await this.#source.execute(abort.signal));
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
- if (this.#snapshot.status === "ready" && this.#snapshot.version === invalidation.manifestVersion && sameLiveValue(previous, rows)) {
140
- continue;
141
- }
142
- if (sameLiveValue(previous, rows) && this.#snapshot.status !== "loading") {
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
- this.scope.postMessage(rpcEvent(subscriptionId, "change", encoded.payload), {
673
- transfer: encoded.transfer
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
  },
@@ -1,4 +1,4 @@
1
- export declare const protocolVersion: 4;
1
+ export declare const protocolVersion: 6;
2
2
  /** Outstanding request/response pairs retained by either side of one database RPC connection. */
3
3
  export declare const MAX_DATABASE_RPC_IN_FLIGHT = 256;
4
4
  export type RpcRequest = {
@@ -1,4 +1,4 @@
1
- const protocolVersion = 4;
1
+ const protocolVersion = 6;
2
2
  const MAX_DATABASE_RPC_IN_FLIGHT = 256;
3
3
  function serializeError(error) {
4
4
  if (!(error instanceof Error)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@minnowdb/core",
3
- "version": "0.7.8",
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",