@minnowdb/core 0.7.8 → 0.8.0

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,55 +1,14 @@
1
+ import { createLiveQueryPatch } from "./live-patch.js";
2
+ import { queryResultRetainedBytes } from "./query-cache.js";
3
+ import { encodeQueryIdentity } from "./query-identity.js";
1
4
  import { LiveQueryLimitError } from "./errors.js";
2
5
  const DEFAULT_LIVE_QUERY_MAX_GROUPS = 256;
3
6
  const DEFAULT_LIVE_QUERY_MAX_SUBSCRIPTIONS = 1024;
4
7
  const MAX_LIVE_QUERY_GROUPS = 4096;
5
8
  const MAX_LIVE_QUERY_SUBSCRIPTIONS = 16384;
6
9
  const MAX_LIVE_QUERY_SETS_PER_DATABASE = 256;
10
+ const LIVE_QUERY_EXECUTION_CONCURRENCY = 8;
7
11
  import { LiveQueryLimitError as LiveQueryLimitError2 } from "./errors.js";
8
- const digestScratch = new DataView(new ArrayBuffer(8));
9
- function digestResult(result) {
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;
52
- }
53
12
  function sameQueryValue(left, right) {
54
13
  if (left instanceof Date || right instanceof Date) {
55
14
  return left instanceof Date && right instanceof Date && Object.is(dateMilliseconds(left), dateMilliseconds(right));
@@ -67,12 +26,15 @@ function sameResult(left, right) {
67
26
  return false;
68
27
  }
69
28
  }
29
+ const columns = left.columns;
70
30
  for (let rowIndex = 0; rowIndex < left.rows.length; rowIndex += 1) {
71
31
  const leftRow = left.rows[rowIndex];
72
32
  const rightRow = right.rows[rowIndex];
73
33
  if (leftRow === void 0 || rightRow === void 0)
74
34
  return false;
75
- for (const column of left.columns) {
35
+ if (leftRow === rightRow)
36
+ continue;
37
+ for (const column of columns) {
76
38
  if (!sameQueryValue(leftRow[column] ?? null, rightRow[column] ?? null))
77
39
  return false;
78
40
  }
@@ -104,41 +66,6 @@ function cloneResult(result) {
104
66
  rows: result.rows.map((row) => cloneRow(row, columns))
105
67
  };
106
68
  }
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;
141
- }
142
69
  function queryKey(query) {
143
70
  return encodeQueryIdentity(typeof query === "string" ? ["sql", query] : query.kind === "sql-query" ? ["sql-query", query.sql, query.params] : ["typed-query", query.plan]);
144
71
  }
@@ -148,6 +75,9 @@ function sameProbe(left, right) {
148
75
  function versionOrdinal(version) {
149
76
  return version ?? -1;
150
77
  }
78
+ function newerProbe(left, right) {
79
+ return versionOrdinal(right.manifestVersion) > versionOrdinal(left.manifestVersion) ? right : left;
80
+ }
151
81
  function boundedLiveLimit(value, maximum, label) {
152
82
  if (!Number.isSafeInteger(value) || value < 1 || value > maximum) {
153
83
  throw new RangeError(`Live query ${label} limit must be between 1 and ${String(maximum)}`);
@@ -157,6 +87,34 @@ function boundedLiveLimit(value, maximum, label) {
157
87
  function catalogChangedBetween(previous, current) {
158
88
  return versionOrdinal(current.manifestVersion) < versionOrdinal(previous.manifestVersion) || current.schemaEpoch !== previous.schemaEpoch;
159
89
  }
90
+ function isResultSubscriber(subscriber) {
91
+ return subscriber.kind === "result" && !subscriber.closed;
92
+ }
93
+ function isObserver(subscriber) {
94
+ return subscriber.kind === "observer" && !subscriber.closed;
95
+ }
96
+ function groupExecutes(group) {
97
+ for (const subscriber of group.subscribers) {
98
+ if (subscriber.closed)
99
+ continue;
100
+ if (subscriber.kind === "result" || subscriber.options.suppressUnchanged === true)
101
+ return true;
102
+ }
103
+ return false;
104
+ }
105
+ function groupMemoizes(group) {
106
+ for (const subscriber of group.subscribers) {
107
+ if (!subscriber.closed && subscriber.kind === "observer")
108
+ return true;
109
+ }
110
+ return false;
111
+ }
112
+ function callLiveCallback(callback) {
113
+ try {
114
+ callback?.();
115
+ } catch {
116
+ }
117
+ }
160
118
  class LiveQuerySet {
161
119
  #host;
162
120
  #channel;
@@ -166,8 +124,12 @@ class LiveQuerySet {
166
124
  #groups = /* @__PURE__ */ new Map();
167
125
  #opening = /* @__PURE__ */ new Map();
168
126
  #groupsByTable = /* @__PURE__ */ new Map();
127
+ #lagging = /* @__PURE__ */ new Set();
169
128
  #maxGroups;
170
129
  #maxSubscriptions;
130
+ #maxRetainedBytes;
131
+ #sharedResults;
132
+ #incremental;
171
133
  #stats = {
172
134
  hints: 0,
173
135
  versionChecks: 0,
@@ -177,10 +139,17 @@ class LiveQuerySet {
177
139
  zoneSkips: 0,
178
140
  notificationsSuppressed: 0,
179
141
  invalidations: 0,
142
+ maintained: 0,
143
+ groupsVisited: 0,
144
+ retainedRows: 0,
145
+ retainedBytes: 0,
180
146
  sharedExecutions: 0,
181
147
  lastSweepMs: 0
182
148
  };
183
149
  #lastProbe;
150
+ #pendingProbe;
151
+ #executing = 0;
152
+ #executionWaiters = [];
184
153
  #sweepChain = Promise.resolve();
185
154
  #sweepQueued = false;
186
155
  #pollTimer;
@@ -194,6 +163,11 @@ class LiveQuerySet {
194
163
  }
195
164
  this.#maxGroups = boundedLiveLimit(options.maxGroups ?? DEFAULT_LIVE_QUERY_MAX_GROUPS, MAX_LIVE_QUERY_GROUPS, "group");
196
165
  this.#maxSubscriptions = boundedLiveLimit(options.maxSubscriptions ?? DEFAULT_LIVE_QUERY_MAX_SUBSCRIPTIONS, MAX_LIVE_QUERY_SUBSCRIPTIONS, "subscription");
166
+ this.#maxRetainedBytes = options.maxRetainedBytes ?? 64 * 1024 * 1024;
167
+ if (!Number.isSafeInteger(this.#maxRetainedBytes) || this.#maxRetainedBytes < 0)
168
+ throw new RangeError("Live query retained byte limit must be a non-negative safe integer");
169
+ this.#sharedResults = options.sharedResults === true;
170
+ this.#incremental = options.incremental !== false;
197
171
  this.#host = host;
198
172
  if (options.channel !== void 0) {
199
173
  this.#channel = options.channel;
@@ -214,7 +188,25 @@ class LiveQuerySet {
214
188
  }
215
189
  }
216
190
  get stats() {
217
- return { ...this.#stats };
191
+ let retainedRows = 0;
192
+ for (const group of this.#groups.values())
193
+ retainedRows += group.result?.rows.length ?? 0;
194
+ let retainedBytes = 0;
195
+ for (const group of this.#groups.values())
196
+ retainedBytes += group.retainedBytes;
197
+ return { ...this.#stats, retainedRows, retainedBytes };
198
+ }
199
+ #freshProbe() {
200
+ let pending = this.#pendingProbe;
201
+ if (pending === void 0) {
202
+ pending = Promise.resolve().then(() => {
203
+ if (this.#pendingProbe === pending)
204
+ this.#pendingProbe = void 0;
205
+ return this.#host.currentProbe();
206
+ });
207
+ this.#pendingProbe = pending;
208
+ }
209
+ return pending;
218
210
  }
219
211
  #throwIfClosed() {
220
212
  if (this.#closed)
@@ -232,22 +224,25 @@ class LiveQuerySet {
232
224
  let group;
233
225
  let subscriber;
234
226
  try {
235
- group = await this.#getOrOpenGroup(query);
227
+ const opened = await this.#getOrOpenGroup(query);
228
+ group = opened.group;
236
229
  this.#throwIfClosed();
237
230
  subscriber = {
238
231
  kind: "result",
239
232
  options,
240
233
  delivered: false,
241
- closed: false
234
+ closed: false,
235
+ seenDelivery: -1
242
236
  };
243
237
  group.subscribers.add(subscriber);
244
- await this.refresh();
238
+ await this.#settleGroup(group, opened.fresh, true);
245
239
  this.#throwIfClosed();
246
240
  if (!subscriber.delivered) {
247
241
  const result = group.result ?? (await this.#executeGroup(group)).result;
248
242
  this.#throwIfClosed();
249
- if (!subscriber.closed)
250
- this.#deliverResult(subscriber, result);
243
+ if (!subscriber.closed) {
244
+ this.#deliverResult(group, subscriber, result, { ...group.seenProbe, initial: true });
245
+ }
251
246
  }
252
247
  } catch (error) {
253
248
  if (group !== void 0 && subscriber !== void 0) {
@@ -261,13 +256,22 @@ class LiveQuerySet {
261
256
  }
262
257
  return this.#subscriptionHandle(group, subscriber);
263
258
  }
259
+ subscribePatches(query, options) {
260
+ return this.subscribe(query, {
261
+ sharedResults: true,
262
+ onChange: (result, delivery) => options.onPatch(createLiveQueryPatch(result, delivery), delivery),
263
+ ...options.onError === void 0 ? {} : { onError: options.onError.bind(options) },
264
+ ...options.onComplete === void 0 ? {} : { onComplete: options.onComplete.bind(options) }
265
+ });
266
+ }
264
267
  async observe(query, options) {
265
268
  this.#throwIfClosed();
266
269
  this.#reserveSubscription();
267
270
  let group;
268
271
  let subscriber;
269
272
  try {
270
- group = await this.#getOrOpenGroup(query);
273
+ const opened = await this.#getOrOpenGroup(query);
274
+ group = opened.group;
271
275
  this.#throwIfClosed();
272
276
  subscriber = {
273
277
  kind: "observer",
@@ -276,7 +280,7 @@ class LiveQuerySet {
276
280
  closed: false
277
281
  };
278
282
  group.subscribers.add(subscriber);
279
- await this.refresh();
283
+ await this.#settleGroup(group, opened.fresh, options.suppressUnchanged === true);
280
284
  this.#throwIfClosed();
281
285
  if (!subscriber.delivered) {
282
286
  this.#deliverInvalidation(subscriber, { ...group.seenProbe, initial: true });
@@ -293,6 +297,22 @@ class LiveQuerySet {
293
297
  }
294
298
  return this.#subscriptionHandle(group, subscriber);
295
299
  }
300
+ async #settleGroup(group, fresh, execute) {
301
+ if (execute && group.result === void 0) {
302
+ if (group.execution !== void 0) {
303
+ this.#stats.sharedExecutions += 1;
304
+ await group.execution;
305
+ } else {
306
+ await this.#executeGroup(group, {
307
+ probe: group.seenProbe,
308
+ memoize: groupMemoizes(group)
309
+ });
310
+ }
311
+ this.#throwIfClosed();
312
+ }
313
+ if (!fresh || this.#lagging.has(group))
314
+ await this.refresh();
315
+ }
296
316
  #subscriptionHandle(group, subscriber) {
297
317
  return {
298
318
  dependencyTableIds: [...group.dependencies],
@@ -308,7 +328,7 @@ class LiveQuerySet {
308
328
  this.#subscriptionCount -= 1;
309
329
  group.subscribers.delete(subscriber);
310
330
  if (complete)
311
- subscriber.options.onComplete?.();
331
+ callLiveCallback(() => subscriber.options.onComplete?.());
312
332
  this.#removeEmptyGroup(group);
313
333
  }
314
334
  #removeEmptyGroup(group) {
@@ -317,6 +337,7 @@ class LiveQuerySet {
317
337
  if (this.#groups.get(group.key) !== group)
318
338
  return;
319
339
  this.#groups.delete(group.key);
340
+ this.#lagging.delete(group);
320
341
  this.#unindexGroup(group, group.dependencies);
321
342
  }
322
343
  async #getOrOpenGroup(query) {
@@ -324,12 +345,12 @@ class LiveQuerySet {
324
345
  const existing = this.#groups.get(key);
325
346
  if (existing !== void 0) {
326
347
  this.#stats.sharedExecutions += 1;
327
- return existing;
348
+ return { group: existing, fresh: false };
328
349
  }
329
350
  const opening = this.#opening.get(key);
330
351
  if (opening !== void 0) {
331
352
  this.#stats.sharedExecutions += 1;
332
- return opening;
353
+ return { group: await opening, fresh: false };
333
354
  }
334
355
  if (this.#groups.size + this.#opening.size >= this.#maxGroups) {
335
356
  throw new LiveQueryLimitError("group", this.#maxGroups);
@@ -337,7 +358,7 @@ class LiveQuerySet {
337
358
  const created = this.#openGroup(key, query);
338
359
  this.#opening.set(key, created);
339
360
  try {
340
- return await created;
361
+ return { group: await created, fresh: true };
341
362
  } finally {
342
363
  this.#opening.delete(key);
343
364
  }
@@ -351,13 +372,18 @@ class LiveQuerySet {
351
372
  subscribers: /* @__PURE__ */ new Set(),
352
373
  seenProbe: after,
353
374
  result: void 0,
354
- digest: void 0,
355
- execution: void 0
375
+ retainedBytes: 0,
376
+ execution: void 0,
377
+ deliveries: 0,
378
+ maintenance: void 0,
379
+ unmaintainable: false
356
380
  };
357
381
  this.#groups.set(key, group);
358
382
  this.#indexGroup(group, dependencies);
359
- if (this.#lastProbe === void 0 || versionOrdinal(after.manifestVersion) < versionOrdinal(this.#lastProbe.manifestVersion)) {
383
+ if (this.#lastProbe === void 0)
360
384
  this.#lastProbe = after;
385
+ else if (versionOrdinal(after.manifestVersion) < versionOrdinal(this.#lastProbe.manifestVersion) || after.schemaEpoch !== this.#lastProbe.schemaEpoch) {
386
+ this.#lagging.add(group);
361
387
  }
362
388
  return group;
363
389
  }
@@ -388,12 +414,12 @@ class LiveQuerySet {
388
414
  this.#indexGroup(group, next);
389
415
  }
390
416
  async #stableDependencies(query) {
391
- let before = await this.#host.currentProbe();
417
+ let before = await this.#freshProbe();
392
418
  this.#throwIfClosed();
393
419
  for (let attempt = 0; attempt < 8; attempt += 1) {
394
- const dependencies = await this.#host.dependencyTableIds(query);
420
+ const dependencies = await this.#host.dependencyTableIds(query, before);
395
421
  this.#throwIfClosed();
396
- const after = await this.#host.currentProbe();
422
+ const after = await this.#freshProbe();
397
423
  this.#throwIfClosed();
398
424
  if (!catalogChangedBetween(before, after))
399
425
  return { dependencies, probe: after };
@@ -401,20 +427,35 @@ class LiveQuerySet {
401
427
  }
402
428
  throw new Error("Catalog kept changing while live-query dependencies were resolved");
403
429
  }
404
- async #executeGroup(group) {
430
+ async #executeGroup(group, context) {
405
431
  if (group.execution !== void 0) {
406
432
  this.#stats.sharedExecutions += 1;
407
433
  return group.execution;
408
434
  }
409
435
  const execution = (async () => {
410
- const executed = await this.#host.execute(group.query);
411
- const digest = digestResult(executed);
436
+ await this.#acquireExecutionSlot();
437
+ let executed;
438
+ let maintenance;
439
+ let retainedBytes;
440
+ try {
441
+ if (this.#incremental && this.#host.executeMaintainable !== void 0 && !group.unmaintainable) {
442
+ const maintained = await this.#host.executeMaintainable(group.query, context);
443
+ if (maintained === void 0)
444
+ group.unmaintainable = true;
445
+ else {
446
+ executed = maintained.result;
447
+ maintenance = maintained.state;
448
+ retainedBytes = maintained.retainedBytes;
449
+ }
450
+ }
451
+ executed ??= await this.#host.execute(group.query, context);
452
+ } finally {
453
+ this.#releaseExecutionSlot();
454
+ }
412
455
  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 };
456
+ const changed = previous === void 0 || !sameResult(previous, executed);
457
+ this.#retain(group, executed, maintenance, retainedBytes);
458
+ return { result: executed, changed };
418
459
  })();
419
460
  group.execution = execution;
420
461
  try {
@@ -424,11 +465,77 @@ class LiveQuerySet {
424
465
  group.execution = void 0;
425
466
  }
426
467
  }
427
- #deliverResult(subscriber, result) {
468
+ #retain(group, result, state, hint) {
469
+ const bytes = Math.max(queryResultRetainedBytes(result) + result.rows.length * 48, hint ?? (state === group.maintenance ? group.retainedBytes : 0));
470
+ let total = bytes;
471
+ for (const other of this.#groups.values())
472
+ if (other !== group)
473
+ total += other.retainedBytes;
474
+ if (!Number.isSafeInteger(total) || total > this.#maxRetainedBytes)
475
+ throw new LiveQueryLimitError("byte", this.#maxRetainedBytes);
476
+ group.result = result;
477
+ group.maintenance = state;
478
+ group.retainedBytes = bytes;
479
+ }
480
+ async #acquireExecutionSlot() {
481
+ if (this.#executing < LIVE_QUERY_EXECUTION_CONCURRENCY) {
482
+ this.#executing += 1;
483
+ return;
484
+ }
485
+ await new Promise((resolve) => {
486
+ this.#executionWaiters.push(resolve);
487
+ });
488
+ this.#executing += 1;
489
+ }
490
+ #releaseExecutionSlot() {
491
+ this.#executing -= 1;
492
+ this.#executionWaiters.shift()?.();
493
+ }
494
+ async #maintainGroup(group, tableIds, after, until, probe) {
495
+ const host = this.#host;
496
+ if (host.maintain === void 0 || group.result === void 0 || group.maintenance === void 0 || group.execution !== void 0) {
497
+ return void 0;
498
+ }
499
+ const retained = group.result;
500
+ const state = group.maintenance;
501
+ const verdict = { declined: false };
502
+ const execution = (async () => {
503
+ await this.#acquireExecutionSlot();
504
+ let maintained;
505
+ try {
506
+ maintained = await host.maintain?.(group.query, retained, state, tableIds, after, until, probe);
507
+ } catch {
508
+ maintained = void 0;
509
+ } finally {
510
+ this.#releaseExecutionSlot();
511
+ }
512
+ if (maintained === void 0) {
513
+ verdict.declined = true;
514
+ return { result: retained, changed: false };
515
+ }
516
+ this.#retain(group, maintained.result, maintained.state, maintained.retainedBytes);
517
+ return {
518
+ result: maintained.result,
519
+ changed: maintained.changed,
520
+ ...maintained.retained === void 0 ? {} : { retained: maintained.retained }
521
+ };
522
+ })();
523
+ group.execution = execution;
524
+ try {
525
+ const outcome = await execution;
526
+ return verdict.declined ? void 0 : outcome;
527
+ } finally {
528
+ if (group.execution === execution)
529
+ group.execution = void 0;
530
+ }
531
+ }
532
+ #deliverResult(group, subscriber, result, delivery, retained) {
428
533
  if (subscriber.closed)
429
534
  return;
430
535
  subscriber.delivered = true;
431
- subscriber.options.onChange(cloneResult(result));
536
+ const consecutive = subscriber.seenDelivery === group.deliveries - 1;
537
+ subscriber.options.onChange(this.#sharedResults || subscriber.options.sharedResults === true ? result : cloneResult(result), retained !== void 0 && consecutive && !delivery.initial ? { ...delivery, retained } : delivery);
538
+ subscriber.seenDelivery = group.deliveries;
432
539
  }
433
540
  #deliverInvalidation(subscriber, invalidation) {
434
541
  if (subscriber.closed)
@@ -456,6 +563,7 @@ class LiveQuerySet {
456
563
  const groups = [...this.#groups.values()];
457
564
  this.#groups.clear();
458
565
  this.#groupsByTable.clear();
566
+ this.#lagging.clear();
459
567
  for (const group of groups) {
460
568
  const subscribers = [...group.subscribers];
461
569
  group.subscribers.clear();
@@ -464,10 +572,10 @@ class LiveQuerySet {
464
572
  continue;
465
573
  subscriber.closed = true;
466
574
  this.#subscriptionCount -= 1;
467
- subscriber.options.onComplete?.();
575
+ callLiveCallback(() => subscriber.options.onComplete?.());
468
576
  }
469
577
  }
470
- this.#onClosed?.();
578
+ callLiveCallback(this.#onClosed);
471
579
  }
472
580
  #hint() {
473
581
  if (this.#closed)
@@ -489,19 +597,39 @@ class LiveQuerySet {
489
597
  #stillOpen() {
490
598
  return !this.#closed;
491
599
  }
600
+ async #sweepCandidates(last, current, changedSince) {
601
+ if (catalogChangedBetween(last, current))
602
+ return [...this.#groups.values()];
603
+ if (sameProbe(last, current))
604
+ return [...this.#lagging];
605
+ const changed = await changedSince(last.manifestVersion);
606
+ if (!this.#stillOpen())
607
+ return void 0;
608
+ if (changed === "all")
609
+ return [...this.#groups.values()];
610
+ const candidates = new Set(this.#lagging);
611
+ for (const tableId of changed) {
612
+ const groups = this.#groupsByTable.get(tableId);
613
+ if (groups === void 0)
614
+ continue;
615
+ for (const group of groups)
616
+ candidates.add(group);
617
+ }
618
+ return [...candidates];
619
+ }
492
620
  async #sweep() {
493
621
  if (this.#closed || this.#groups.size === 0)
494
622
  return;
495
623
  this.#stats.versionChecks += 1;
496
- const current = await this.#host.currentProbe();
624
+ const current = await this.#freshProbe();
497
625
  if (!this.#stillOpen())
498
626
  return;
499
627
  const last = this.#lastProbe ?? current;
500
- const anyLagging = [...this.#groups.values()].some((group) => !sameProbe(group.seenProbe, current));
501
- if (sameProbe(last, current) && !anyLagging)
628
+ if (sameProbe(last, current) && this.#lagging.size === 0)
502
629
  return;
503
630
  const started = performance.now();
504
631
  this.#stats.sweeps += 1;
632
+ const rerunsBefore = this.#stats.reruns;
505
633
  const windowCache = /* @__PURE__ */ new Map();
506
634
  const changedSince = (after) => {
507
635
  const key = versionOrdinal(after);
@@ -512,101 +640,139 @@ class LiveQuerySet {
512
640
  }
513
641
  return pending;
514
642
  };
515
- for (const group of [...this.#groups.values()]) {
643
+ const candidates = await this.#sweepCandidates(last, current, changedSince);
644
+ if (candidates === void 0)
645
+ return;
646
+ for (const group of candidates) {
516
647
  if (!this.#stillOpen())
517
648
  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) {
523
- try {
524
- await this.#refreshDependencies(group);
525
- } catch (error) {
526
- this.#notifyGroupError(group, error);
527
- continue;
528
- }
649
+ await this.#sweepGroup(group, last, current, changedSince);
650
+ }
651
+ this.#lastProbe = current;
652
+ this.#stats.rerunsAvoided += this.#groups.size - (this.#stats.reruns - rerunsBefore);
653
+ this.#stats.lastSweepMs = performance.now() - started;
654
+ }
655
+ async #sweepGroup(group, last, current, changedSince) {
656
+ if (!this.#stillOpen())
657
+ return;
658
+ if (group.subscribers.size === 0 || this.#groups.get(group.key) !== group)
659
+ return;
660
+ this.#stats.groupsVisited += 1;
661
+ if (sameProbe(group.seenProbe, current)) {
662
+ this.#lagging.delete(group);
663
+ return;
664
+ }
665
+ const prior = this.#lagging.has(group) ? group.seenProbe : newerProbe(group.seenProbe, last);
666
+ const catalogChanged = catalogChangedBetween(prior, current);
667
+ if (catalogChanged) {
668
+ group.maintenance = void 0;
669
+ group.unmaintainable = false;
670
+ try {
671
+ await this.#refreshDependencies(group);
672
+ } catch (error) {
673
+ this.#lagging.add(group);
674
+ this.#notifyGroupError(group, error);
675
+ return;
529
676
  }
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;
541
- }
677
+ }
678
+ let relevant = [];
679
+ let affected = catalogChanged;
680
+ if (!affected && prior.manifestVersion !== current.manifestVersion) {
681
+ const changed = await changedSince(prior.manifestVersion);
682
+ if (!this.#stillOpen())
683
+ return;
684
+ if (changed === "all")
685
+ affected = true;
686
+ else {
687
+ relevant = [...group.dependencies].filter((tableId) => changed.has(tableId));
688
+ affected = relevant.length > 0;
542
689
  }
543
- if (!affected) {
544
- this.#stats.rerunsAvoided += 1;
545
- group.seenProbe = current;
546
- continue;
690
+ }
691
+ if (!affected) {
692
+ this.#settleProbe(group, current);
693
+ return;
694
+ }
695
+ if (!catalogChanged && relevant.length > 0 && current.manifestVersion !== null && this.#host.changeCanAffect !== void 0) {
696
+ let canAffect;
697
+ try {
698
+ canAffect = await this.#host.changeCanAffect(group.query, relevant, prior.manifestVersion, current.manifestVersion);
699
+ } catch {
700
+ canAffect = true;
547
701
  }
548
- if (!catalogChanged && relevant.length > 0 && current.manifestVersion !== null && this.#host.changeCanAffect !== void 0) {
549
- let canAffect;
550
- try {
551
- canAffect = await this.#host.changeCanAffect(group.query, relevant, prior.manifestVersion, current.manifestVersion);
552
- } catch {
553
- canAffect = true;
554
- }
555
- if (!this.#stillOpen())
702
+ if (!this.#stillOpen())
703
+ return;
704
+ if (!canAffect) {
705
+ this.#stats.zoneSkips += 1;
706
+ this.#settleProbe(group, current);
707
+ return;
708
+ }
709
+ }
710
+ try {
711
+ let execution;
712
+ if (groupExecutes(group)) {
713
+ if (group.result === void 0 && group.execution !== void 0) {
714
+ this.#lagging.add(group);
556
715
  return;
557
- if (!canAffect) {
558
- this.#stats.rerunsAvoided += 1;
559
- this.#stats.zoneSkips += 1;
560
- group.seenProbe = current;
561
- continue;
562
716
  }
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;
717
+ if (!catalogChanged && relevant.length > 0 && current.manifestVersion !== null) {
718
+ execution = await this.#maintainGroup(group, relevant, prior.manifestVersion, current.manifestVersion, current);
719
+ if (!this.#stillOpen() || this.#groups.get(group.key) !== group)
720
+ return;
721
+ }
722
+ if (execution !== void 0)
723
+ this.#stats.maintained += 1;
724
+ else {
571
725
  this.#stats.reruns += 1;
572
- execution = await this.#executeGroup(group);
573
- if (!this.#stillOpen() || !this.#groups.has(group.key))
574
- continue;
726
+ execution = await this.#executeGroup(group, {
727
+ probe: current,
728
+ memoize: groupMemoizes(group)
729
+ });
730
+ if (!this.#stillOpen() || this.#groups.get(group.key) !== group)
731
+ return;
575
732
  }
576
- for (const observer of observers) {
733
+ }
734
+ const invalidation = { ...current, initial: false };
735
+ if (execution?.changed === true)
736
+ group.deliveries += 1;
737
+ for (const subscriber of [...group.subscribers]) {
738
+ if (isObserver(subscriber)) {
739
+ if (execution !== void 0 && !execution.changed && subscriber.options.suppressUnchanged === true) {
740
+ this.#stats.notificationsSuppressed += 1;
741
+ continue;
742
+ }
577
743
  try {
578
- this.#deliverInvalidation(observer, { ...current, initial: false });
744
+ this.#deliverInvalidation(subscriber, invalidation);
579
745
  this.#stats.invalidations += 1;
580
746
  } catch (error) {
581
- observer.options.onError?.(error);
747
+ callLiveCallback(() => subscriber.options.onError?.(error));
582
748
  }
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
- }
592
- }
593
- } else {
594
- this.#stats.notificationsSuppressed += resultSubscribers.length;
749
+ } else if (isResultSubscriber(subscriber) && execution !== void 0) {
750
+ if (!execution.changed) {
751
+ this.#stats.notificationsSuppressed += 1;
752
+ continue;
753
+ }
754
+ try {
755
+ this.#deliverResult(group, subscriber, execution.result, invalidation, execution.retained);
756
+ } catch (error) {
757
+ callLiveCallback(() => subscriber.options.onError?.(error));
595
758
  }
596
759
  }
597
- group.seenProbe = current;
598
- } catch (error) {
599
- this.#notifyGroupError(group, error);
600
760
  }
761
+ this.#settleProbe(group, current);
762
+ } catch (error) {
763
+ this.#lagging.add(group);
764
+ this.#notifyGroupError(group, error);
601
765
  }
602
- this.#lastProbe = current;
603
- this.#stats.lastSweepMs = performance.now() - started;
766
+ }
767
+ #settleProbe(group, probe) {
768
+ group.seenProbe = probe;
769
+ this.#lagging.delete(group);
604
770
  }
605
771
  #notifyGroupError(group, error) {
606
772
  for (const subscriber of group.subscribers) {
607
773
  if (subscriber.closed)
608
774
  continue;
609
- subscriber.options.onError?.(error);
775
+ callLiveCallback(() => subscriber.options.onError?.(error));
610
776
  }
611
777
  }
612
778
  async #changedTablesSince(after, until) {