@minnowdb/core 0.7.0 → 0.7.1

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 (95) hide show
  1. package/dist/block-format/block.js +254 -325
  2. package/dist/block-format/checksum.js +30 -52
  3. package/dist/block-format/codecs.js +112 -125
  4. package/dist/block-format/column.js +187 -205
  5. package/dist/block-format/index.js +5 -1
  6. package/dist/block-format/physical.js +371 -387
  7. package/dist/block-format/types.js +4 -1
  8. package/dist/block-format/unicode.js +30 -39
  9. package/dist/date-value.js +67 -78
  10. package/dist/engine/artifact-cache.js +106 -116
  11. package/dist/engine/batch.js +36 -44
  12. package/dist/engine/buffered-writer.js +156 -158
  13. package/dist/engine/byte-estimates.js +20 -22
  14. package/dist/engine/cache-limits.js +24 -25
  15. package/dist/engine/cancellation.js +5 -3
  16. package/dist/engine/catalog.js +64 -73
  17. package/dist/engine/client.js +715 -780
  18. package/dist/engine/database.js +15941 -19313
  19. package/dist/engine/defaults.js +104 -111
  20. package/dist/engine/errors.js +113 -128
  21. package/dist/engine/fts.js +233 -280
  22. package/dist/engine/group-index.js +287 -321
  23. package/dist/engine/index.js +23 -12
  24. package/dist/engine/join-index.js +177 -186
  25. package/dist/engine/keyed-live.js +168 -173
  26. package/dist/engine/live-api.js +0 -1
  27. package/dist/engine/live-equal.js +32 -37
  28. package/dist/engine/live.js +603 -647
  29. package/dist/engine/memory.js +109 -111
  30. package/dist/engine/optimizer.d.ts +19 -1
  31. package/dist/engine/optimizer.js +2633 -2934
  32. package/dist/engine/point-read.js +122 -182
  33. package/dist/engine/query-api.js +12 -3
  34. package/dist/engine/query-cache.js +61 -69
  35. package/dist/engine/query.d.ts +6 -0
  36. package/dist/engine/query.js +8503 -9980
  37. package/dist/engine/result-wire.js +221 -252
  38. package/dist/engine/schema-wire.js +97 -112
  39. package/dist/engine/schema.js +1099 -1254
  40. package/dist/engine/sort-keys.js +315 -389
  41. package/dist/engine/sql-domains.js +646 -769
  42. package/dist/engine/sql-driver.js +0 -1
  43. package/dist/engine/sql-functions.js +1031 -1125
  44. package/dist/engine/sql-json.js +199 -218
  45. package/dist/engine/sql-semantics.js +572 -639
  46. package/dist/engine/typed-live.js +271 -293
  47. package/dist/engine/vector.js +4484 -5145
  48. package/dist/engine/worker-host.d.ts +8 -63
  49. package/dist/engine/worker-host.js +28 -1130
  50. package/dist/engine/worker-indexeddb.d.ts +1 -0
  51. package/dist/engine/worker-indexeddb.js +3 -0
  52. package/dist/engine/worker-memory.d.ts +1 -0
  53. package/dist/engine/worker-memory.js +3 -0
  54. package/dist/engine/worker-opfs.d.ts +1 -0
  55. package/dist/engine/worker-opfs.js +3 -0
  56. package/dist/engine/worker-server.d.ts +93 -0
  57. package/dist/engine/worker-server.js +999 -0
  58. package/dist/engine/worker-store-indexeddb.d.ts +2 -0
  59. package/dist/engine/worker-store-indexeddb.js +11 -0
  60. package/dist/engine/worker-store-memory.d.ts +2 -0
  61. package/dist/engine/worker-store-memory.js +6 -0
  62. package/dist/engine/worker-store-opfs.d.ts +2 -0
  63. package/dist/engine/worker-store-opfs.js +9 -0
  64. package/dist/engine/worker.js +0 -12
  65. package/dist/engine/write-block-planner.js +93 -100
  66. package/dist/plan/index.js +15 -16
  67. package/dist/plan/model.d.ts +6 -0
  68. package/dist/plan/model.js +20 -27
  69. package/dist/storage/index.js +0 -12
  70. package/dist/storage/indexeddb.js +11776 -13171
  71. package/dist/storage/memory.js +1198 -1241
  72. package/dist/storage/opfs/files.js +238 -295
  73. package/dist/storage/opfs/index.js +9 -3
  74. package/dist/storage/opfs/leader.js +4386 -4856
  75. package/dist/storage/opfs/rpc.js +223 -259
  76. package/dist/storage/opfs/snapshot-ledger.js +219 -260
  77. package/dist/storage/opfs/store.js +884 -1003
  78. package/dist/storage/persistence.js +59 -74
  79. package/dist/storage/snapshot-stream.js +788 -826
  80. package/dist/storage/snapshot.js +36 -37
  81. package/dist/storage/toolkit/extents.js +444 -520
  82. package/dist/storage/toolkit/index.js +28 -29
  83. package/dist/storage/toolkit/record-core.js +5792 -6377
  84. package/dist/storage/toolkit/sync-file.js +34 -42
  85. package/dist/storage/toolkit/wal.js +92 -127
  86. package/dist/storage/toolkit/wire.js +232 -256
  87. package/dist/storage/types.js +2948 -3250
  88. package/dist/testing/block-store-conformance.js +1556 -1629
  89. package/dist/testing/index.js +310 -291
  90. package/dist/testing/opfs-shim.js +280 -312
  91. package/dist/testing/simulator.js +496 -534
  92. package/dist/testing/sqllogictest.js +425 -365
  93. package/dist/transactions/index.js +1464 -1712
  94. package/dist/worker-protocol/index.js +70 -69
  95. package/package.json +20 -2
@@ -1,690 +1,646 @@
1
1
  import { LiveQueryLimitError } from "./errors.js";
2
- export const DEFAULT_LIVE_QUERY_MAX_GROUPS = 256;
3
- export const DEFAULT_LIVE_QUERY_MAX_SUBSCRIPTIONS = 1_024;
4
- export const MAX_LIVE_QUERY_GROUPS = 4_096;
5
- export const MAX_LIVE_QUERY_SUBSCRIPTIONS = 16_384;
6
- export const MAX_LIVE_QUERY_SETS_PER_DATABASE = 256;
7
- export { LiveQueryLimitError } from "./errors.js";
2
+ const DEFAULT_LIVE_QUERY_MAX_GROUPS = 256;
3
+ const DEFAULT_LIVE_QUERY_MAX_SUBSCRIPTIONS = 1024;
4
+ const MAX_LIVE_QUERY_GROUPS = 4096;
5
+ const MAX_LIVE_QUERY_SUBSCRIPTIONS = 16384;
6
+ const MAX_LIVE_QUERY_SETS_PER_DATABASE = 256;
7
+ import { LiveQueryLimitError as LiveQueryLimitError2 } from "./errors.js";
8
8
  const digestScratch = new DataView(new ArrayBuffer(8));
9
- /** Fast inequality filter. Equal digests are always followed by exact comparison. */
10
9
  function digestResult(result) {
11
- let hash = 0x811c9dc5;
12
- const mixByte = (byte) => {
13
- hash = Math.imul(hash ^ byte, 0x01000193) >>> 0;
14
- };
15
- const mixNumber = (value) => {
16
- digestScratch.setFloat64(0, value);
17
- for (let index = 0; index < 8; index += 1)
18
- mixByte(digestScratch.getUint8(index));
19
- };
20
- const mixString = (value) => {
21
- for (let index = 0; index < value.length; index += 1) {
22
- const code = value.charCodeAt(index);
23
- mixByte(code & 0xff);
24
- mixByte(code >>> 8);
25
- }
26
- mixByte(0xff);
27
- };
28
- for (const column of result.columns)
29
- mixString(column);
30
- for (const domain of result.columnDomains)
31
- mixString(JSON.stringify(domain));
32
- for (const row of result.rows) {
33
- for (const column of result.columns) {
34
- const value = row[column] ?? null;
35
- if (value === null)
36
- mixByte(1);
37
- else if (typeof value === "number") {
38
- mixByte(2);
39
- mixNumber(value);
40
- }
41
- else if (typeof value === "string") {
42
- mixByte(3);
43
- mixString(value);
44
- }
45
- else if (typeof value === "boolean")
46
- mixByte(value ? 4 : 5);
47
- else {
48
- mixByte(6);
49
- mixNumber(dateMilliseconds(value));
50
- }
51
- }
52
- mixByte(0xfe);
53
- }
54
- return hash;
10
+ let hash = 2166136261;
11
+ const mixByte = (byte) => {
12
+ hash = Math.imul(hash ^ byte, 16777619) >>> 0;
13
+ };
14
+ const mixNumber = (value) => {
15
+ digestScratch.setFloat64(0, value);
16
+ for (let index = 0; index < 8; index += 1)
17
+ mixByte(digestScratch.getUint8(index));
18
+ };
19
+ const mixString = (value) => {
20
+ for (let index = 0; index < value.length; index += 1) {
21
+ const code = value.charCodeAt(index);
22
+ mixByte(code & 255);
23
+ mixByte(code >>> 8);
24
+ }
25
+ mixByte(255);
26
+ };
27
+ for (const column of result.columns)
28
+ mixString(column);
29
+ for (const domain of result.columnDomains)
30
+ mixString(JSON.stringify(domain));
31
+ for (const row of result.rows) {
32
+ for (const column of result.columns) {
33
+ const value = row[column] ?? null;
34
+ if (value === null)
35
+ mixByte(1);
36
+ else if (typeof value === "number") {
37
+ mixByte(2);
38
+ mixNumber(value);
39
+ } else if (typeof value === "string") {
40
+ mixByte(3);
41
+ mixString(value);
42
+ } else if (typeof value === "boolean")
43
+ mixByte(value ? 4 : 5);
44
+ else {
45
+ mixByte(6);
46
+ mixNumber(dateMilliseconds(value));
47
+ }
48
+ }
49
+ mixByte(254);
50
+ }
51
+ return hash;
55
52
  }
56
53
  function sameQueryValue(left, right) {
57
- if (left instanceof Date || right instanceof Date) {
58
- return (left instanceof Date &&
59
- right instanceof Date &&
60
- Object.is(dateMilliseconds(left), dateMilliseconds(right)));
61
- }
62
- return Object.is(left, right);
54
+ if (left instanceof Date || right instanceof Date) {
55
+ return left instanceof Date && right instanceof Date && Object.is(dateMilliseconds(left), dateMilliseconds(right));
56
+ }
57
+ return Object.is(left, right);
63
58
  }
64
59
  function sameResult(left, right) {
65
- if (left.columns.length !== right.columns.length || left.rows.length !== right.rows.length) {
60
+ if (left.columns.length !== right.columns.length || left.rows.length !== right.rows.length) {
61
+ return false;
62
+ }
63
+ for (let index = 0; index < left.columns.length; index += 1) {
64
+ if (left.columns[index] !== right.columns[index])
65
+ return false;
66
+ if (JSON.stringify(left.columnDomains[index]) !== JSON.stringify(right.columnDomains[index])) {
67
+ return false;
68
+ }
69
+ }
70
+ for (let rowIndex = 0; rowIndex < left.rows.length; rowIndex += 1) {
71
+ const leftRow = left.rows[rowIndex];
72
+ const rightRow = right.rows[rowIndex];
73
+ if (leftRow === void 0 || rightRow === void 0)
74
+ return false;
75
+ for (const column of left.columns) {
76
+ if (!sameQueryValue(leftRow[column] ?? null, rightRow[column] ?? null))
66
77
  return false;
67
78
  }
68
- for (let index = 0; index < left.columns.length; index += 1) {
69
- if (left.columns[index] !== right.columns[index])
70
- return false;
71
- if (JSON.stringify(left.columnDomains[index]) !== JSON.stringify(right.columnDomains[index])) {
72
- return false;
73
- }
74
- }
75
- for (let rowIndex = 0; rowIndex < left.rows.length; rowIndex += 1) {
76
- const leftRow = left.rows[rowIndex];
77
- const rightRow = right.rows[rowIndex];
78
- if (leftRow === undefined || rightRow === undefined)
79
- return false;
80
- for (const column of left.columns) {
81
- if (!sameQueryValue(leftRow[column] ?? null, rightRow[column] ?? null))
82
- return false;
83
- }
84
- }
85
- return true;
79
+ }
80
+ return true;
86
81
  }
87
82
  function cloneRow(row, columns) {
88
- const cloned = {};
89
- for (const column of columns) {
90
- const value = row[column] ?? null;
91
- const copy = value instanceof Date ? copyDate(value) : value;
92
- if (column === "__proto__") {
93
- Object.defineProperty(cloned, column, {
94
- value: copy,
95
- enumerable: true,
96
- configurable: true,
97
- writable: true,
98
- });
99
- }
100
- else
101
- cloned[column] = copy;
102
- }
103
- return cloned;
83
+ const cloned = {};
84
+ for (const column of columns) {
85
+ const value = row[column] ?? null;
86
+ const copy = value instanceof Date ? copyDate(value) : value;
87
+ if (column === "__proto__") {
88
+ Object.defineProperty(cloned, column, {
89
+ value: copy,
90
+ enumerable: true,
91
+ configurable: true,
92
+ writable: true
93
+ });
94
+ } else
95
+ cloned[column] = copy;
96
+ }
97
+ return cloned;
104
98
  }
105
99
  function cloneResult(result) {
106
- const columns = [...result.columns];
107
- return {
108
- columns,
109
- columnDomains: structuredClone(result.columnDomains),
110
- rows: result.rows.map((row) => cloneRow(row, columns)),
111
- };
100
+ const columns = [...result.columns];
101
+ return {
102
+ columns,
103
+ columnDomains: structuredClone(result.columnDomains),
104
+ rows: result.rows.map((row) => cloneRow(row, columns))
105
+ };
112
106
  }
113
- /** Type-tagged, length-delimited structural identity. Unlike JSON, it preserves Date vs string,
114
- * -0, non-finite numbers, and undefined fields, so deduplication cannot merge distinct plans. */
115
- function encodeQueryIdentity(value, ancestors = new Set()) {
116
- if (value === null)
117
- return "z";
118
- if (typeof value === "undefined")
119
- return "u";
120
- if (typeof value === "boolean")
121
- return value ? "b1" : "b0";
122
- if (typeof value === "number") {
123
- if (Number.isNaN(value))
124
- return "nNaN;";
125
- if (Object.is(value, -0))
126
- return "n-0;";
127
- return `n${String(value)};`;
128
- }
129
- if (typeof value === "string")
130
- return `s${String(value.length)}:${value}`;
131
- if (value instanceof Date)
132
- return `d${String(dateMilliseconds(value))};`;
133
- if (typeof value !== "object") {
134
- throw new TypeError(`Unsupported live-query identity value: ${typeof value}`);
135
- }
136
- if (ancestors.has(value))
137
- throw new TypeError("Live-query identity contains a cycle");
138
- ancestors.add(value);
139
- let encoded;
140
- if (Array.isArray(value)) {
141
- encoded = `a${String(value.length)}[${value
142
- .map((item) => encodeQueryIdentity(item, ancestors))
143
- .join("")}]`;
144
- }
145
- else {
146
- const record = value;
147
- const keys = Object.keys(record).sort();
148
- encoded = `o${String(keys.length)}{${keys
149
- .map((key) => `${encodeQueryIdentity(key, ancestors)}${encodeQueryIdentity(record[key], ancestors)}`)
150
- .join("")}}`;
151
- }
152
- ancestors.delete(value);
153
- return encoded;
107
+ function encodeQueryIdentity(value, ancestors = /* @__PURE__ */ new Set()) {
108
+ if (value === null)
109
+ return "z";
110
+ if (typeof value === "undefined")
111
+ return "u";
112
+ if (typeof value === "boolean")
113
+ return value ? "b1" : "b0";
114
+ if (typeof value === "number") {
115
+ if (Number.isNaN(value))
116
+ return "nNaN;";
117
+ if (Object.is(value, -0))
118
+ return "n-0;";
119
+ return `n${String(value)};`;
120
+ }
121
+ if (typeof value === "string")
122
+ return `s${String(value.length)}:${value}`;
123
+ if (value instanceof Date)
124
+ return `d${String(dateMilliseconds(value))};`;
125
+ if (typeof value !== "object") {
126
+ throw new TypeError(`Unsupported live-query identity value: ${typeof value}`);
127
+ }
128
+ if (ancestors.has(value))
129
+ throw new TypeError("Live-query identity contains a cycle");
130
+ ancestors.add(value);
131
+ let encoded;
132
+ if (Array.isArray(value)) {
133
+ encoded = `a${String(value.length)}[${value.map((item) => encodeQueryIdentity(item, ancestors)).join("")}]`;
134
+ } else {
135
+ const record = value;
136
+ const keys = Object.keys(record).sort();
137
+ encoded = `o${String(keys.length)}{${keys.map((key) => `${encodeQueryIdentity(key, ancestors)}${encodeQueryIdentity(record[key], ancestors)}`).join("")}}`;
138
+ }
139
+ ancestors.delete(value);
140
+ return encoded;
154
141
  }
155
142
  function queryKey(query) {
156
- return encodeQueryIdentity(typeof query === "string"
157
- ? ["sql", query]
158
- : query.kind === "sql-query"
159
- ? ["sql-query", query.sql, query.params]
160
- : ["typed-query", query.plan]);
143
+ return encodeQueryIdentity(typeof query === "string" ? ["sql", query] : query.kind === "sql-query" ? ["sql-query", query.sql, query.params] : ["typed-query", query.plan]);
161
144
  }
162
145
  function sameProbe(left, right) {
163
- return left.manifestVersion === right.manifestVersion && left.schemaEpoch === right.schemaEpoch;
146
+ return left.manifestVersion === right.manifestVersion && left.schemaEpoch === right.schemaEpoch;
164
147
  }
165
148
  function versionOrdinal(version) {
166
- return version ?? -1;
149
+ return version ?? -1;
167
150
  }
168
151
  function boundedLiveLimit(value, maximum, label) {
169
- if (!Number.isSafeInteger(value) || value < 1 || value > maximum) {
170
- throw new RangeError(`Live query ${label} limit must be between 1 and ${String(maximum)}`);
171
- }
172
- return value;
152
+ if (!Number.isSafeInteger(value) || value < 1 || value > maximum) {
153
+ throw new RangeError(`Live query ${label} limit must be between 1 and ${String(maximum)}`);
154
+ }
155
+ return value;
173
156
  }
174
- /** Structural changes have their own counter so ordinary commits and physical-index state churn
175
- * do not force dependency resolution. A manifest reset is still widened: it can only result from
176
- * replacing or rebuilding the durable database state. */
177
157
  function catalogChangedBetween(previous, current) {
178
- return (versionOrdinal(current.manifestVersion) < versionOrdinal(previous.manifestVersion) ||
179
- current.schemaEpoch !== previous.schemaEpoch);
158
+ return versionOrdinal(current.manifestVersion) < versionOrdinal(previous.manifestVersion) || current.schemaEpoch !== previous.schemaEpoch;
180
159
  }
181
- export class LiveQuerySet {
182
- #host;
183
- #channel;
184
- #channelListener = () => {
160
+ class LiveQuerySet {
161
+ #host;
162
+ #channel;
163
+ #channelListener = () => {
164
+ this.#hint();
165
+ };
166
+ #groups = /* @__PURE__ */ new Map();
167
+ #opening = /* @__PURE__ */ new Map();
168
+ #groupsByTable = /* @__PURE__ */ new Map();
169
+ #maxGroups;
170
+ #maxSubscriptions;
171
+ #stats = {
172
+ hints: 0,
173
+ versionChecks: 0,
174
+ sweeps: 0,
175
+ reruns: 0,
176
+ rerunsAvoided: 0,
177
+ zoneSkips: 0,
178
+ notificationsSuppressed: 0,
179
+ invalidations: 0,
180
+ sharedExecutions: 0,
181
+ lastSweepMs: 0
182
+ };
183
+ #lastProbe;
184
+ #sweepChain = Promise.resolve();
185
+ #sweepQueued = false;
186
+ #pollTimer;
187
+ #closed = false;
188
+ #subscriptionCount = 0;
189
+ #ownsChannel;
190
+ #onClosed;
191
+ constructor(host, options = {}) {
192
+ if (options.pollIntervalMs !== void 0 && (!Number.isSafeInteger(options.pollIntervalMs) || options.pollIntervalMs <= 0)) {
193
+ throw new RangeError("Live query poll interval must be a positive whole number");
194
+ }
195
+ this.#maxGroups = boundedLiveLimit(options.maxGroups ?? DEFAULT_LIVE_QUERY_MAX_GROUPS, MAX_LIVE_QUERY_GROUPS, "group");
196
+ this.#maxSubscriptions = boundedLiveLimit(options.maxSubscriptions ?? DEFAULT_LIVE_QUERY_MAX_SUBSCRIPTIONS, MAX_LIVE_QUERY_SUBSCRIPTIONS, "subscription");
197
+ this.#host = host;
198
+ if (options.channel !== void 0) {
199
+ this.#channel = options.channel;
200
+ this.#ownsChannel = false;
201
+ } else if (options.channelName !== void 0 && typeof BroadcastChannel !== "undefined") {
202
+ this.#channel = new BroadcastChannel(options.channelName);
203
+ this.#ownsChannel = true;
204
+ } else {
205
+ this.#channel = void 0;
206
+ this.#ownsChannel = false;
207
+ }
208
+ this.#onClosed = options.onClosed;
209
+ this.#channel?.addEventListener("message", this.#channelListener);
210
+ if (options.pollIntervalMs !== void 0) {
211
+ this.#pollTimer = setInterval(() => {
185
212
  this.#hint();
213
+ }, options.pollIntervalMs);
214
+ }
215
+ }
216
+ get stats() {
217
+ return { ...this.#stats };
218
+ }
219
+ #throwIfClosed() {
220
+ if (this.#closed)
221
+ throw new Error("Live query set is closed");
222
+ }
223
+ #reserveSubscription() {
224
+ if (this.#subscriptionCount >= this.#maxSubscriptions) {
225
+ throw new LiveQueryLimitError("subscription", this.#maxSubscriptions);
226
+ }
227
+ this.#subscriptionCount += 1;
228
+ }
229
+ async subscribe(query, options) {
230
+ this.#throwIfClosed();
231
+ this.#reserveSubscription();
232
+ let group;
233
+ let subscriber;
234
+ try {
235
+ group = await this.#getOrOpenGroup(query);
236
+ this.#throwIfClosed();
237
+ subscriber = {
238
+ kind: "result",
239
+ options,
240
+ delivered: false,
241
+ closed: false
242
+ };
243
+ group.subscribers.add(subscriber);
244
+ await this.refresh();
245
+ this.#throwIfClosed();
246
+ if (!subscriber.delivered) {
247
+ const result = group.result ?? (await this.#executeGroup(group)).result;
248
+ this.#throwIfClosed();
249
+ if (!subscriber.closed)
250
+ this.#deliverResult(subscriber, result);
251
+ }
252
+ } catch (error) {
253
+ if (group !== void 0 && subscriber !== void 0) {
254
+ this.#removeSubscriber(group, subscriber, false);
255
+ } else {
256
+ this.#subscriptionCount -= 1;
257
+ if (group !== void 0)
258
+ this.#removeEmptyGroup(group);
259
+ }
260
+ throw error;
261
+ }
262
+ return this.#subscriptionHandle(group, subscriber);
263
+ }
264
+ async observe(query, options) {
265
+ this.#throwIfClosed();
266
+ this.#reserveSubscription();
267
+ let group;
268
+ let subscriber;
269
+ try {
270
+ group = await this.#getOrOpenGroup(query);
271
+ this.#throwIfClosed();
272
+ subscriber = {
273
+ kind: "observer",
274
+ options,
275
+ delivered: false,
276
+ closed: false
277
+ };
278
+ group.subscribers.add(subscriber);
279
+ await this.refresh();
280
+ this.#throwIfClosed();
281
+ if (!subscriber.delivered) {
282
+ this.#deliverInvalidation(subscriber, { ...group.seenProbe, initial: true });
283
+ }
284
+ } catch (error) {
285
+ if (group !== void 0 && subscriber !== void 0) {
286
+ this.#removeSubscriber(group, subscriber, false);
287
+ } else {
288
+ this.#subscriptionCount -= 1;
289
+ if (group !== void 0)
290
+ this.#removeEmptyGroup(group);
291
+ }
292
+ throw error;
293
+ }
294
+ return this.#subscriptionHandle(group, subscriber);
295
+ }
296
+ #subscriptionHandle(group, subscriber) {
297
+ return {
298
+ dependencyTableIds: [...group.dependencies],
299
+ close: () => {
300
+ this.#removeSubscriber(group, subscriber, true);
301
+ }
186
302
  };
187
- #groups = new Map();
188
- #opening = new Map();
189
- #groupsByTable = new Map();
190
- #maxGroups;
191
- #maxSubscriptions;
192
- #stats = {
193
- hints: 0,
194
- versionChecks: 0,
195
- sweeps: 0,
196
- reruns: 0,
197
- rerunsAvoided: 0,
198
- zoneSkips: 0,
199
- notificationsSuppressed: 0,
200
- invalidations: 0,
201
- sharedExecutions: 0,
202
- lastSweepMs: 0,
303
+ }
304
+ #removeSubscriber(group, subscriber, complete) {
305
+ if (subscriber.closed)
306
+ return;
307
+ subscriber.closed = true;
308
+ this.#subscriptionCount -= 1;
309
+ group.subscribers.delete(subscriber);
310
+ if (complete)
311
+ subscriber.options.onComplete?.();
312
+ this.#removeEmptyGroup(group);
313
+ }
314
+ #removeEmptyGroup(group) {
315
+ if (group.subscribers.size !== 0)
316
+ return;
317
+ if (this.#groups.get(group.key) !== group)
318
+ return;
319
+ this.#groups.delete(group.key);
320
+ this.#unindexGroup(group, group.dependencies);
321
+ }
322
+ async #getOrOpenGroup(query) {
323
+ const key = queryKey(query);
324
+ const existing = this.#groups.get(key);
325
+ if (existing !== void 0) {
326
+ this.#stats.sharedExecutions += 1;
327
+ return existing;
328
+ }
329
+ const opening = this.#opening.get(key);
330
+ if (opening !== void 0) {
331
+ this.#stats.sharedExecutions += 1;
332
+ return opening;
333
+ }
334
+ if (this.#groups.size + this.#opening.size >= this.#maxGroups) {
335
+ throw new LiveQueryLimitError("group", this.#maxGroups);
336
+ }
337
+ const created = this.#openGroup(key, query);
338
+ this.#opening.set(key, created);
339
+ try {
340
+ return await created;
341
+ } finally {
342
+ this.#opening.delete(key);
343
+ }
344
+ }
345
+ async #openGroup(key, query) {
346
+ const { dependencies, probe: after } = await this.#stableDependencies(query);
347
+ const group = {
348
+ key,
349
+ query,
350
+ dependencies,
351
+ subscribers: /* @__PURE__ */ new Set(),
352
+ seenProbe: after,
353
+ result: void 0,
354
+ digest: void 0,
355
+ execution: void 0
203
356
  };
204
- #lastProbe;
205
- #sweepChain = Promise.resolve();
206
- #sweepQueued = false;
207
- #pollTimer;
208
- #closed = false;
209
- #subscriptionCount = 0;
210
- #ownsChannel;
211
- #onClosed;
212
- constructor(host, options = {}) {
213
- if (options.pollIntervalMs !== undefined &&
214
- (!Number.isSafeInteger(options.pollIntervalMs) || options.pollIntervalMs <= 0)) {
215
- throw new RangeError("Live query poll interval must be a positive whole number");
216
- }
217
- this.#maxGroups = boundedLiveLimit(options.maxGroups ?? DEFAULT_LIVE_QUERY_MAX_GROUPS, MAX_LIVE_QUERY_GROUPS, "group");
218
- this.#maxSubscriptions = boundedLiveLimit(options.maxSubscriptions ?? DEFAULT_LIVE_QUERY_MAX_SUBSCRIPTIONS, MAX_LIVE_QUERY_SUBSCRIPTIONS, "subscription");
219
- this.#host = host;
220
- if (options.channel !== undefined) {
221
- this.#channel = options.channel;
222
- this.#ownsChannel = false;
223
- }
224
- else if (options.channelName !== undefined && typeof BroadcastChannel !== "undefined") {
225
- this.#channel = new BroadcastChannel(options.channelName);
226
- this.#ownsChannel = true;
227
- }
228
- else {
229
- this.#channel = undefined;
230
- this.#ownsChannel = false;
231
- }
232
- this.#onClosed = options.onClosed;
233
- this.#channel?.addEventListener("message", this.#channelListener);
234
- if (options.pollIntervalMs !== undefined) {
235
- this.#pollTimer = setInterval(() => {
236
- this.#hint();
237
- }, options.pollIntervalMs);
238
- }
239
- }
240
- get stats() {
241
- return { ...this.#stats };
242
- }
243
- #throwIfClosed() {
244
- if (this.#closed)
245
- throw new Error("Live query set is closed");
246
- }
247
- #reserveSubscription() {
248
- if (this.#subscriptionCount >= this.#maxSubscriptions) {
249
- throw new LiveQueryLimitError("subscription", this.#maxSubscriptions);
250
- }
251
- this.#subscriptionCount += 1;
252
- }
253
- /** Registers a query, delivers its current result, and shares work with equal statements. */
254
- async subscribe(query, options) {
255
- this.#throwIfClosed();
256
- this.#reserveSubscription();
257
- let group;
258
- let subscriber;
259
- try {
260
- group = await this.#getOrOpenGroup(query);
261
- this.#throwIfClosed();
262
- subscriber = {
263
- kind: "result",
264
- options,
265
- delivered: false,
266
- closed: false,
267
- };
268
- group.subscribers.add(subscriber);
269
- // Reconcile before exposing a shared cached result to a late subscriber.
270
- await this.refresh();
271
- this.#throwIfClosed();
272
- if (!subscriber.delivered) {
273
- const result = group.result ?? (await this.#executeGroup(group)).result;
274
- this.#throwIfClosed();
275
- if (!subscriber.closed)
276
- this.#deliverResult(subscriber, result);
277
- }
278
- }
279
- catch (error) {
280
- if (group !== undefined && subscriber !== undefined) {
281
- this.#removeSubscriber(group, subscriber, false);
282
- }
283
- else {
284
- this.#subscriptionCount -= 1;
285
- if (group !== undefined)
286
- this.#removeEmptyGroup(group);
287
- }
288
- throw error;
289
- }
290
- return this.#subscriptionHandle(group, subscriber);
291
- }
292
- /** Observes invalidation without executing the statement inside the set. */
293
- async observe(query, options) {
294
- this.#throwIfClosed();
295
- this.#reserveSubscription();
296
- let group;
297
- let subscriber;
298
- try {
299
- group = await this.#getOrOpenGroup(query);
300
- this.#throwIfClosed();
301
- subscriber = {
302
- kind: "observer",
303
- options,
304
- delivered: false,
305
- closed: false,
306
- };
307
- group.subscribers.add(subscriber);
308
- await this.refresh();
309
- this.#throwIfClosed();
310
- if (!subscriber.delivered) {
311
- this.#deliverInvalidation(subscriber, { ...group.seenProbe, initial: true });
312
- }
313
- }
314
- catch (error) {
315
- if (group !== undefined && subscriber !== undefined) {
316
- this.#removeSubscriber(group, subscriber, false);
317
- }
318
- else {
319
- this.#subscriptionCount -= 1;
320
- if (group !== undefined)
321
- this.#removeEmptyGroup(group);
322
- }
323
- throw error;
324
- }
325
- return this.#subscriptionHandle(group, subscriber);
326
- }
327
- #subscriptionHandle(group, subscriber) {
328
- return {
329
- dependencyTableIds: [...group.dependencies],
330
- close: () => {
331
- this.#removeSubscriber(group, subscriber, true);
332
- },
333
- };
334
- }
335
- #removeSubscriber(group, subscriber, complete) {
357
+ this.#groups.set(key, group);
358
+ this.#indexGroup(group, dependencies);
359
+ if (this.#lastProbe === void 0 || versionOrdinal(after.manifestVersion) < versionOrdinal(this.#lastProbe.manifestVersion)) {
360
+ this.#lastProbe = after;
361
+ }
362
+ return group;
363
+ }
364
+ #indexGroup(group, dependencies) {
365
+ for (const tableId of dependencies) {
366
+ let groups = this.#groupsByTable.get(tableId);
367
+ if (groups === void 0) {
368
+ groups = /* @__PURE__ */ new Set();
369
+ this.#groupsByTable.set(tableId, groups);
370
+ }
371
+ groups.add(group);
372
+ }
373
+ }
374
+ #unindexGroup(group, dependencies) {
375
+ for (const tableId of dependencies) {
376
+ const groups = this.#groupsByTable.get(tableId);
377
+ groups?.delete(group);
378
+ if (groups?.size === 0)
379
+ this.#groupsByTable.delete(tableId);
380
+ }
381
+ }
382
+ async #refreshDependencies(group) {
383
+ const { dependencies: next } = await this.#stableDependencies(group.query);
384
+ if (!this.#stillOpen() || !this.#groups.has(group.key))
385
+ return;
386
+ this.#unindexGroup(group, group.dependencies);
387
+ group.dependencies = next;
388
+ this.#indexGroup(group, next);
389
+ }
390
+ async #stableDependencies(query) {
391
+ let before = await this.#host.currentProbe();
392
+ this.#throwIfClosed();
393
+ for (let attempt = 0; attempt < 8; attempt += 1) {
394
+ const dependencies = await this.#host.dependencyTableIds(query);
395
+ this.#throwIfClosed();
396
+ const after = await this.#host.currentProbe();
397
+ this.#throwIfClosed();
398
+ if (!catalogChangedBetween(before, after))
399
+ return { dependencies, probe: after };
400
+ before = after;
401
+ }
402
+ throw new Error("Catalog kept changing while live-query dependencies were resolved");
403
+ }
404
+ async #executeGroup(group) {
405
+ if (group.execution !== void 0) {
406
+ this.#stats.sharedExecutions += 1;
407
+ return group.execution;
408
+ }
409
+ const execution = (async () => {
410
+ const executed = await this.#host.execute(group.query);
411
+ const digest = digestResult(executed);
412
+ const previous = group.result;
413
+ const changed = previous === void 0 || group.digest !== digest || !sameResult(previous, executed);
414
+ const retained = cloneResult(executed);
415
+ group.result = retained;
416
+ group.digest = digest;
417
+ return { result: retained, changed };
418
+ })();
419
+ group.execution = execution;
420
+ try {
421
+ return await execution;
422
+ } finally {
423
+ if (group.execution === execution)
424
+ group.execution = void 0;
425
+ }
426
+ }
427
+ #deliverResult(subscriber, result) {
428
+ if (subscriber.closed)
429
+ return;
430
+ subscriber.delivered = true;
431
+ subscriber.options.onChange(cloneResult(result));
432
+ }
433
+ #deliverInvalidation(subscriber, invalidation) {
434
+ if (subscriber.closed)
435
+ return;
436
+ subscriber.delivered = true;
437
+ subscriber.options.onInvalidate(invalidation);
438
+ }
439
+ notifyLocalCommit() {
440
+ this.#channel?.postMessage("minnow-commit");
441
+ this.#hint();
442
+ }
443
+ async refresh() {
444
+ this.#hint();
445
+ await this.#sweepChain;
446
+ }
447
+ close() {
448
+ if (this.#closed)
449
+ return;
450
+ this.#closed = true;
451
+ this.#channel?.removeEventListener("message", this.#channelListener);
452
+ if (this.#ownsChannel)
453
+ this.#channel?.close?.();
454
+ if (this.#pollTimer !== void 0)
455
+ clearInterval(this.#pollTimer);
456
+ const groups = [...this.#groups.values()];
457
+ this.#groups.clear();
458
+ this.#groupsByTable.clear();
459
+ for (const group of groups) {
460
+ const subscribers = [...group.subscribers];
461
+ group.subscribers.clear();
462
+ for (const subscriber of subscribers) {
336
463
  if (subscriber.closed)
337
- return;
464
+ continue;
338
465
  subscriber.closed = true;
339
466
  this.#subscriptionCount -= 1;
340
- group.subscribers.delete(subscriber);
341
- if (complete)
342
- subscriber.options.onComplete?.();
343
- this.#removeEmptyGroup(group);
344
- }
345
- #removeEmptyGroup(group) {
346
- if (group.subscribers.size !== 0)
347
- return;
348
- if (this.#groups.get(group.key) !== group)
349
- return;
350
- this.#groups.delete(group.key);
351
- this.#unindexGroup(group, group.dependencies);
352
- }
353
- async #getOrOpenGroup(query) {
354
- const key = queryKey(query);
355
- const existing = this.#groups.get(key);
356
- if (existing !== undefined) {
357
- this.#stats.sharedExecutions += 1;
358
- return existing;
359
- }
360
- const opening = this.#opening.get(key);
361
- if (opening !== undefined) {
362
- this.#stats.sharedExecutions += 1;
363
- return opening;
364
- }
365
- if (this.#groups.size + this.#opening.size >= this.#maxGroups) {
366
- throw new LiveQueryLimitError("group", this.#maxGroups);
367
- }
368
- const created = this.#openGroup(key, query);
369
- this.#opening.set(key, created);
467
+ subscriber.options.onComplete?.();
468
+ }
469
+ }
470
+ this.#onClosed?.();
471
+ }
472
+ #hint() {
473
+ if (this.#closed)
474
+ return;
475
+ this.#stats.hints += 1;
476
+ if (this.#sweepQueued)
477
+ return;
478
+ this.#sweepQueued = true;
479
+ this.#sweepChain = this.#sweepChain.then(async () => {
480
+ this.#sweepQueued = false;
481
+ try {
482
+ await this.#sweep();
483
+ } catch (error) {
484
+ for (const group of this.#groups.values())
485
+ this.#notifyGroupError(group, error);
486
+ }
487
+ });
488
+ }
489
+ #stillOpen() {
490
+ return !this.#closed;
491
+ }
492
+ async #sweep() {
493
+ if (this.#closed || this.#groups.size === 0)
494
+ return;
495
+ this.#stats.versionChecks += 1;
496
+ const current = await this.#host.currentProbe();
497
+ if (!this.#stillOpen())
498
+ return;
499
+ const last = this.#lastProbe ?? current;
500
+ const anyLagging = [...this.#groups.values()].some((group) => !sameProbe(group.seenProbe, current));
501
+ if (sameProbe(last, current) && !anyLagging)
502
+ return;
503
+ const started = performance.now();
504
+ this.#stats.sweeps += 1;
505
+ const windowCache = /* @__PURE__ */ new Map();
506
+ const changedSince = (after) => {
507
+ const key = versionOrdinal(after);
508
+ let pending = windowCache.get(key);
509
+ if (pending === void 0) {
510
+ pending = this.#changedTablesSince(after, current.manifestVersion);
511
+ windowCache.set(key, pending);
512
+ }
513
+ return pending;
514
+ };
515
+ for (const group of [...this.#groups.values()]) {
516
+ if (!this.#stillOpen())
517
+ return;
518
+ if (group.subscribers.size === 0 || sameProbe(group.seenProbe, current))
519
+ continue;
520
+ const prior = group.seenProbe;
521
+ const catalogChanged = catalogChangedBetween(prior, current);
522
+ if (catalogChanged) {
370
523
  try {
371
- return await created;
372
- }
373
- finally {
374
- this.#opening.delete(key);
375
- }
376
- }
377
- async #openGroup(key, query) {
378
- const { dependencies, probe: after } = await this.#stableDependencies(query);
379
- const group = {
380
- key,
381
- query,
382
- dependencies,
383
- subscribers: new Set(),
384
- seenProbe: after,
385
- result: undefined,
386
- digest: undefined,
387
- execution: undefined,
388
- };
389
- this.#groups.set(key, group);
390
- this.#indexGroup(group, dependencies);
391
- if (this.#lastProbe === undefined ||
392
- versionOrdinal(after.manifestVersion) < versionOrdinal(this.#lastProbe.manifestVersion)) {
393
- this.#lastProbe = after;
394
- }
395
- return group;
396
- }
397
- #indexGroup(group, dependencies) {
398
- for (const tableId of dependencies) {
399
- let groups = this.#groupsByTable.get(tableId);
400
- if (groups === undefined) {
401
- groups = new Set();
402
- this.#groupsByTable.set(tableId, groups);
403
- }
404
- groups.add(group);
405
- }
406
- }
407
- #unindexGroup(group, dependencies) {
408
- for (const tableId of dependencies) {
409
- const groups = this.#groupsByTable.get(tableId);
410
- groups?.delete(group);
411
- if (groups?.size === 0)
412
- this.#groupsByTable.delete(tableId);
524
+ await this.#refreshDependencies(group);
525
+ } catch (error) {
526
+ this.#notifyGroupError(group, error);
527
+ continue;
413
528
  }
414
- }
415
- async #refreshDependencies(group) {
416
- const { dependencies: next } = await this.#stableDependencies(group.query);
417
- if (!this.#stillOpen() || !this.#groups.has(group.key))
418
- return;
419
- this.#unindexGroup(group, group.dependencies);
420
- group.dependencies = next;
421
- this.#indexGroup(group, next);
422
- }
423
- /** Resolve a view/table set inside a catalog-stable probe window or refuse stale indexing. */
424
- async #stableDependencies(query) {
425
- let before = await this.#host.currentProbe();
426
- this.#throwIfClosed();
427
- for (let attempt = 0; attempt < 8; attempt += 1) {
428
- const dependencies = await this.#host.dependencyTableIds(query);
429
- this.#throwIfClosed();
430
- const after = await this.#host.currentProbe();
431
- this.#throwIfClosed();
432
- if (!catalogChangedBetween(before, after))
433
- return { dependencies, probe: after };
434
- before = after;
435
- }
436
- throw new Error("Catalog kept changing while live-query dependencies were resolved");
437
- }
438
- async #executeGroup(group) {
439
- if (group.execution !== undefined) {
440
- this.#stats.sharedExecutions += 1;
441
- return group.execution;
529
+ }
530
+ let relevant = [];
531
+ let affected = catalogChanged;
532
+ if (!affected && prior.manifestVersion !== current.manifestVersion) {
533
+ const changed = await changedSince(prior.manifestVersion);
534
+ if (!this.#stillOpen())
535
+ return;
536
+ if (changed === "all")
537
+ affected = true;
538
+ else {
539
+ relevant = [...group.dependencies].filter((tableId) => changed.has(tableId));
540
+ affected = relevant.length > 0;
442
541
  }
443
- const execution = (async () => {
444
- const executed = await this.#host.execute(group.query);
445
- const digest = digestResult(executed);
446
- const previous = group.result;
447
- const changed = previous === undefined || group.digest !== digest || !sameResult(previous, executed);
448
- const retained = cloneResult(executed);
449
- group.result = retained;
450
- group.digest = digest;
451
- return { result: retained, changed };
452
- })();
453
- group.execution = execution;
542
+ }
543
+ if (!affected) {
544
+ this.#stats.rerunsAvoided += 1;
545
+ group.seenProbe = current;
546
+ continue;
547
+ }
548
+ if (!catalogChanged && relevant.length > 0 && current.manifestVersion !== null && this.#host.changeCanAffect !== void 0) {
549
+ let canAffect;
454
550
  try {
455
- return await execution;
456
- }
457
- finally {
458
- if (group.execution === execution)
459
- group.execution = undefined;
460
- }
461
- }
462
- #deliverResult(subscriber, result) {
463
- if (subscriber.closed)
464
- return;
465
- subscriber.delivered = true;
466
- subscriber.options.onChange(cloneResult(result));
467
- }
468
- #deliverInvalidation(subscriber, invalidation) {
469
- if (subscriber.closed)
470
- return;
471
- subscriber.delivered = true;
472
- subscriber.options.onInvalidate(invalidation);
473
- }
474
- /** Called by the owning database after each local write commit; also hints other tabs. */
475
- notifyLocalCommit() {
476
- this.#channel?.postMessage("minnow-commit");
477
- this.#hint();
478
- }
479
- /** Runs one authoritative probe and selective re-execution sweep. */
480
- async refresh() {
481
- this.#hint();
482
- await this.#sweepChain;
483
- }
484
- close() {
485
- if (this.#closed)
486
- return;
487
- this.#closed = true;
488
- this.#channel?.removeEventListener("message", this.#channelListener);
489
- if (this.#ownsChannel)
490
- this.#channel?.close?.();
491
- if (this.#pollTimer !== undefined)
492
- clearInterval(this.#pollTimer);
493
- const groups = [...this.#groups.values()];
494
- this.#groups.clear();
495
- this.#groupsByTable.clear();
496
- for (const group of groups) {
497
- const subscribers = [...group.subscribers];
498
- group.subscribers.clear();
499
- for (const subscriber of subscribers) {
500
- if (subscriber.closed)
501
- continue;
502
- subscriber.closed = true;
503
- this.#subscriptionCount -= 1;
504
- subscriber.options.onComplete?.();
505
- }
551
+ canAffect = await this.#host.changeCanAffect(group.query, relevant, prior.manifestVersion, current.manifestVersion);
552
+ } catch {
553
+ canAffect = true;
506
554
  }
507
- this.#onClosed?.();
508
- }
509
- #hint() {
510
- if (this.#closed)
511
- return;
512
- this.#stats.hints += 1;
513
- if (this.#sweepQueued)
514
- return;
515
- this.#sweepQueued = true;
516
- this.#sweepChain = this.#sweepChain.then(async () => {
517
- this.#sweepQueued = false;
518
- try {
519
- await this.#sweep();
520
- }
521
- catch (error) {
522
- for (const group of this.#groups.values())
523
- this.#notifyGroupError(group, error);
524
- }
525
- });
526
- }
527
- #stillOpen() {
528
- return !this.#closed;
529
- }
530
- async #sweep() {
531
- if (this.#closed || this.#groups.size === 0)
532
- return;
533
- this.#stats.versionChecks += 1;
534
- const current = await this.#host.currentProbe();
535
555
  if (!this.#stillOpen())
536
- return;
537
- const last = this.#lastProbe ?? current;
538
- const anyLagging = [...this.#groups.values()].some((group) => !sameProbe(group.seenProbe, current));
539
- if (sameProbe(last, current) && !anyLagging)
540
- return;
541
- const started = performance.now();
542
- this.#stats.sweeps += 1;
543
- const windowCache = new Map();
544
- const changedSince = (after) => {
545
- const key = versionOrdinal(after);
546
- let pending = windowCache.get(key);
547
- if (pending === undefined) {
548
- pending = this.#changedTablesSince(after, current.manifestVersion);
549
- windowCache.set(key, pending);
550
- }
551
- return pending;
552
- };
553
- for (const group of [...this.#groups.values()]) {
554
- if (!this.#stillOpen())
555
- return;
556
- if (group.subscribers.size === 0 || sameProbe(group.seenProbe, current))
557
- continue;
558
- const prior = group.seenProbe;
559
- const catalogChanged = catalogChangedBetween(prior, current);
560
- if (catalogChanged) {
561
- try {
562
- await this.#refreshDependencies(group);
563
- }
564
- catch (error) {
565
- this.#notifyGroupError(group, error);
566
- continue;
567
- }
568
- }
569
- let relevant = [];
570
- let affected = catalogChanged;
571
- if (!affected && prior.manifestVersion !== current.manifestVersion) {
572
- const changed = await changedSince(prior.manifestVersion);
573
- if (!this.#stillOpen())
574
- return;
575
- if (changed === "all")
576
- affected = true;
577
- else {
578
- relevant = [...group.dependencies].filter((tableId) => changed.has(tableId));
579
- affected = relevant.length > 0;
580
- }
581
- }
582
- if (!affected) {
583
- this.#stats.rerunsAvoided += 1;
584
- group.seenProbe = current;
585
- continue;
586
- }
587
- if (!catalogChanged &&
588
- relevant.length > 0 &&
589
- current.manifestVersion !== null &&
590
- this.#host.changeCanAffect !== undefined) {
591
- let canAffect;
592
- try {
593
- canAffect = await this.#host.changeCanAffect(group.query, relevant, prior.manifestVersion, current.manifestVersion);
594
- }
595
- catch {
596
- canAffect = true;
597
- }
598
- if (!this.#stillOpen())
599
- return;
600
- if (!canAffect) {
601
- this.#stats.rerunsAvoided += 1;
602
- this.#stats.zoneSkips += 1;
603
- group.seenProbe = current;
604
- continue;
605
- }
606
- }
607
- const resultSubscribers = [...group.subscribers].filter((subscriber) => subscriber.kind === "result" && !subscriber.closed);
608
- const observers = [...group.subscribers].filter((subscriber) => subscriber.kind === "observer" && !subscriber.closed);
609
- try {
610
- let execution;
611
- if (resultSubscribers.length > 0) {
612
- // An initial delivery may be executing against the pre-sweep snapshot. Waiting for it
613
- // here can deadlock callers that intentionally hold that execute while awaiting this
614
- // refresh. Leave the group dirty; once the initial delivery lands, the next refresh
615
- // re-runs it against `current`.
616
- if (group.result === undefined && group.execution !== undefined)
617
- continue;
618
- this.#stats.reruns += 1;
619
- execution = await this.#executeGroup(group);
620
- if (!this.#stillOpen() || !this.#groups.has(group.key))
621
- continue;
622
- }
623
- for (const observer of observers) {
624
- try {
625
- this.#deliverInvalidation(observer, { ...current, initial: false });
626
- this.#stats.invalidations += 1;
627
- }
628
- catch (error) {
629
- observer.options.onError?.(error);
630
- }
631
- }
632
- if (execution !== undefined) {
633
- if (execution.changed) {
634
- for (const subscriber of resultSubscribers) {
635
- try {
636
- this.#deliverResult(subscriber, execution.result);
637
- }
638
- catch (error) {
639
- subscriber.options.onError?.(error);
640
- }
641
- }
642
- }
643
- else {
644
- this.#stats.notificationsSuppressed += resultSubscribers.length;
645
- }
646
- }
647
- group.seenProbe = current;
648
- }
649
- catch (error) {
650
- // Do not advance seenProbe. A refresh with no newer commit retries the dirty group.
651
- this.#notifyGroupError(group, error);
652
- }
556
+ return;
557
+ if (!canAffect) {
558
+ this.#stats.rerunsAvoided += 1;
559
+ this.#stats.zoneSkips += 1;
560
+ group.seenProbe = current;
561
+ continue;
653
562
  }
654
- this.#lastProbe = current;
655
- this.#stats.lastSweepMs = performance.now() - started;
656
- }
657
- #notifyGroupError(group, error) {
658
- for (const subscriber of group.subscribers) {
659
- if (subscriber.closed)
660
- continue;
661
- subscriber.options.onError?.(error);
563
+ }
564
+ const resultSubscribers = [...group.subscribers].filter((subscriber) => subscriber.kind === "result" && !subscriber.closed);
565
+ const observers = [...group.subscribers].filter((subscriber) => subscriber.kind === "observer" && !subscriber.closed);
566
+ try {
567
+ let execution;
568
+ if (resultSubscribers.length > 0) {
569
+ if (group.result === void 0 && group.execution !== void 0)
570
+ continue;
571
+ this.#stats.reruns += 1;
572
+ execution = await this.#executeGroup(group);
573
+ if (!this.#stillOpen() || !this.#groups.has(group.key))
574
+ continue;
662
575
  }
663
- }
664
- /** Unions persisted table change sets in (after, until], widening on any history gap. */
665
- async #changedTablesSince(after, until) {
666
- if (until === null || after === until)
667
- return new Set();
668
- const changed = new Set();
669
- let cursor = after;
670
- let expected = (after ?? -1) + 1;
671
- for (;;) {
672
- const page = await this.#host.manifestPage(cursor, 64);
673
- for (const manifest of page.records) {
674
- if (manifest.version > until)
675
- return changed;
676
- if (manifest.version !== expected)
677
- return "all";
678
- expected += 1;
679
- for (const tableId of manifest.changedTableIds)
680
- changed.add(tableId);
681
- if (manifest.version === until)
682
- return changed;
576
+ for (const observer of observers) {
577
+ try {
578
+ this.#deliverInvalidation(observer, { ...current, initial: false });
579
+ this.#stats.invalidations += 1;
580
+ } catch (error) {
581
+ observer.options.onError?.(error);
582
+ }
583
+ }
584
+ if (execution !== void 0) {
585
+ if (execution.changed) {
586
+ for (const subscriber of resultSubscribers) {
587
+ try {
588
+ this.#deliverResult(subscriber, execution.result);
589
+ } catch (error) {
590
+ subscriber.options.onError?.(error);
591
+ }
683
592
  }
684
- if (page.nextCursor === null)
685
- return expected > until ? changed : "all";
686
- cursor = page.nextCursor;
593
+ } else {
594
+ this.#stats.notificationsSuppressed += resultSubscribers.length;
595
+ }
687
596
  }
688
- }
597
+ group.seenProbe = current;
598
+ } catch (error) {
599
+ this.#notifyGroupError(group, error);
600
+ }
601
+ }
602
+ this.#lastProbe = current;
603
+ this.#stats.lastSweepMs = performance.now() - started;
604
+ }
605
+ #notifyGroupError(group, error) {
606
+ for (const subscriber of group.subscribers) {
607
+ if (subscriber.closed)
608
+ continue;
609
+ subscriber.options.onError?.(error);
610
+ }
611
+ }
612
+ async #changedTablesSince(after, until) {
613
+ if (until === null || after === until)
614
+ return /* @__PURE__ */ new Set();
615
+ const changed = /* @__PURE__ */ new Set();
616
+ let cursor = after;
617
+ let expected = (after ?? -1) + 1;
618
+ for (; ; ) {
619
+ const page = await this.#host.manifestPage(cursor, 64);
620
+ for (const manifest of page.records) {
621
+ if (manifest.version > until)
622
+ return changed;
623
+ if (manifest.version !== expected)
624
+ return "all";
625
+ expected += 1;
626
+ for (const tableId of manifest.changedTableIds)
627
+ changed.add(tableId);
628
+ if (manifest.version === until)
629
+ return changed;
630
+ }
631
+ if (page.nextCursor === null)
632
+ return expected > until ? changed : "all";
633
+ cursor = page.nextCursor;
634
+ }
635
+ }
689
636
  }
690
637
  import { copyDate, dateMilliseconds } from "../date-value.js";
638
+ export {
639
+ DEFAULT_LIVE_QUERY_MAX_GROUPS,
640
+ DEFAULT_LIVE_QUERY_MAX_SUBSCRIPTIONS,
641
+ LiveQueryLimitError2 as LiveQueryLimitError,
642
+ LiveQuerySet,
643
+ MAX_LIVE_QUERY_GROUPS,
644
+ MAX_LIVE_QUERY_SETS_PER_DATABASE,
645
+ MAX_LIVE_QUERY_SUBSCRIPTIONS
646
+ };