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