@solidjs/signals 2.0.0-rc.13 → 2.0.0-rc.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dev-shared.js +2000 -3768
- package/dist/dev.attribution.js +132 -79
- package/dist/dev.js +7226 -8488
- package/dist/observe/affects.js +125 -104
- package/dist/observe/boundaries.js +458 -665
- package/dist/observe/core/action.js +63 -66
- package/dist/observe/core/async.js +240 -383
- package/dist/observe/core/attribution-queries.js +1 -1
- package/dist/observe/core/attribution.js +240 -183
- package/dist/observe/core/constants.js +115 -187
- package/dist/observe/core/context.js +3 -3
- package/dist/observe/core/core.js +840 -1438
- package/dist/observe/core/dev.js +3 -3
- package/dist/observe/core/effect.js +75 -86
- package/dist/observe/core/external.js +5 -5
- package/dist/observe/core/graph.js +61 -70
- package/dist/observe/core/heap.js +75 -98
- package/dist/observe/core/invariants.js +1 -34
- package/dist/observe/core/lanes.js +704 -181
- package/dist/observe/core/owner.js +80 -101
- package/dist/observe/core/scheduler.js +708 -1351
- package/dist/observe/core/verdict.js +302 -621
- package/dist/observe/flatten.js +79 -0
- package/dist/observe/index.js +15 -13
- package/dist/observe/map.js +232 -235
- package/dist/observe/reveal.js +212 -0
- package/dist/observe/signals.js +133 -215
- package/dist/observe/store/affects.js +135 -0
- package/dist/observe/store/index.js +29 -24
- package/dist/observe/store/optimistic.js +538 -0
- package/dist/observe/store/projection.js +302 -0
- package/dist/observe/store/reconcile.js +257 -0
- package/dist/observe/store/store.js +1945 -277
- package/dist/observe/store/storePath.js +1 -1
- package/dist/{prod/store/next → observe/store}/target.js +17 -18
- package/dist/observe/store/types.js +73 -0
- package/dist/observe/store/utils.js +1 -13
- package/dist/prod/affects.js +127 -106
- package/dist/prod/boundaries.js +442 -654
- package/dist/prod/core/action.js +71 -74
- package/dist/prod/core/async.js +307 -442
- package/dist/prod/core/constants.js +112 -186
- package/dist/prod/core/context.js +3 -3
- package/dist/prod/core/core.js +852 -1458
- package/dist/prod/core/effect.js +88 -99
- package/dist/prod/core/external.js +5 -5
- package/dist/prod/core/graph.js +61 -70
- package/dist/prod/core/heap.js +75 -98
- package/dist/prod/core/invariants.js +1 -32
- package/dist/prod/core/lanes.js +666 -181
- package/dist/prod/core/owner.js +75 -96
- package/dist/prod/core/scheduler.js +677 -1332
- package/dist/prod/core/verdict.js +297 -621
- package/dist/prod/flatten.js +77 -0
- package/dist/prod/index.js +15 -13
- package/dist/prod/map.js +143 -146
- package/dist/prod/reveal.js +212 -0
- package/dist/prod/signals.js +144 -226
- package/dist/prod/store/affects.js +135 -0
- package/dist/prod/store/index.js +28 -19
- package/dist/prod/store/optimistic.js +533 -0
- package/dist/prod/store/projection.js +295 -0
- package/dist/prod/store/reconcile.js +257 -0
- package/dist/prod/store/store.js +1840 -272
- package/dist/prod/store/storePath.js +1 -1
- package/dist/{observe/store/next → prod/store}/target.js +13 -17
- package/dist/prod/store/types.js +73 -0
- package/dist/prod/store/utils.js +1 -11
- package/dist/types/affects.d.ts +11 -30
- package/dist/types/boundaries.d.ts +62 -176
- package/dist/types/carved.d.ts +1 -0
- package/dist/types/core/action.d.ts +5 -6
- package/dist/types/core/async.d.ts +10 -5
- package/dist/types/core/attribution-hooks.d.ts +20 -1
- package/dist/types/core/attribution.d.ts +20 -9
- package/dist/types/core/constants.d.ts +125 -180
- package/dist/types/core/core.d.ts +104 -264
- package/dist/types/core/dev.d.ts +2 -2
- package/dist/types/core/effect.d.ts +0 -7
- package/dist/types/core/graph.d.ts +7 -1
- package/dist/types/core/heap.d.ts +5 -7
- package/dist/types/core/index.d.ts +5 -5
- package/dist/types/core/invariants.d.ts +0 -29
- package/dist/types/core/lanes.d.ts +65 -87
- package/dist/types/core/owner.d.ts +3 -3
- package/dist/types/core/scheduler.d.ts +249 -331
- package/dist/types/core/types.d.ts +51 -85
- package/dist/types/core/verdict.d.ts +3 -0
- package/dist/types/flatten.d.ts +8 -0
- package/dist/types/index.d.ts +6 -4
- package/dist/types/reveal.d.ts +19 -0
- package/dist/types/signals.d.ts +35 -91
- package/dist/types/store/affects.d.ts +1 -0
- package/dist/types/store/index.d.ts +35 -15
- package/dist/types/store/optimistic.d.ts +23 -0
- package/dist/types/store/projection.d.ts +18 -0
- package/dist/types/store/reconcile.d.ts +3 -0
- package/dist/types/store/store.d.ts +165 -143
- package/dist/types/store/storePath.d.ts +1 -1
- package/dist/types/store/target.d.ts +151 -0
- package/dist/types/store/types.d.ts +84 -0
- package/package.json +1 -1
- package/dist/observe/core/optimistic.js +0 -669
- package/dist/observe/store/next/optimistic.js +0 -717
- package/dist/observe/store/next/projection.js +0 -266
- package/dist/observe/store/next/reconcile.js +0 -375
- package/dist/observe/store/next/store.js +0 -2608
- package/dist/prod/core/optimistic.js +0 -658
- package/dist/prod/store/next/optimistic.js +0 -710
- package/dist/prod/store/next/projection.js +0 -257
- package/dist/prod/store/next/reconcile.js +0 -373
- package/dist/prod/store/next/store.js +0 -2484
- package/dist/types/core/optimistic.d.ts +0 -6
- package/dist/types/store/next/optimistic.d.ts +0 -25
- package/dist/types/store/next/projection.d.ts +0 -27
- package/dist/types/store/next/reconcile.d.ts +0 -10
- package/dist/types/store/next/store.d.ts +0 -145
- package/dist/types/store/next/target.d.ts +0 -196
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { setAttributionHooks } from "./attribution-hooks.js";
|
|
2
2
|
|
|
3
|
-
import { NOT_PENDING, CONFIG_PLUMBING,
|
|
3
|
+
import { NOT_PENDING, CONFIG_PLUMBING, CONFIG_OVERRIDE, CONFIG_GUESS, CONFIG_VERDICT } from "./constants.js";
|
|
4
4
|
|
|
5
5
|
import { OBSERVE, liveRootOwners, emitDiagnostic, ownerPath, reportDiagnostic, isSuppressed, anyExcluded, isExcluded, GRAPH_SIZE_WARN_AT, noteFanOut } from "./dev.js";
|
|
6
6
|
|
|
7
|
+
import { blockersOf, GlobalQueue } from "./scheduler.js";
|
|
8
|
+
|
|
7
9
|
/** A fallback shown for less than this is a flash: feedback for a wait too short to need it. */ const FALLBACK_FLASH_MS = 150;
|
|
8
10
|
|
|
9
11
|
/**
|
|
@@ -15,11 +17,11 @@ import { OBSERVE, liveRootOwners, emitDiagnostic, ownerPath, reportDiagnostic, i
|
|
|
15
17
|
* observer exclusion is cached per node once any exists, before that a flag
|
|
16
18
|
* read.
|
|
17
19
|
*/ function excludedNode(e) {
|
|
18
|
-
if ((e.
|
|
20
|
+
if ((e.u & CONFIG_PLUMBING) !== 0) return true;
|
|
19
21
|
if (!anyExcluded()) return false;
|
|
20
22
|
const n = e;
|
|
21
|
-
if (n
|
|
22
|
-
return n
|
|
23
|
+
if (n.me === undefined) n.me = isExcluded(e);
|
|
24
|
+
return n.me;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
let attributionActive = false;
|
|
@@ -42,7 +44,7 @@ const defaultOptions = {
|
|
|
42
44
|
count: 120,
|
|
43
45
|
windowMs: 1e3
|
|
44
46
|
},
|
|
45
|
-
wideDeps:
|
|
47
|
+
wideDeps: 200,
|
|
46
48
|
hotTime: {
|
|
47
49
|
budgetMs: 8,
|
|
48
50
|
windowMs: 1e3
|
|
@@ -119,7 +121,7 @@ function nodeName(e) {
|
|
|
119
121
|
}
|
|
120
122
|
|
|
121
123
|
/** The record vocabulary for a computation's kind: effects carry a `_type`, memos do not. */ function nodeKind(e) {
|
|
122
|
-
return e.
|
|
124
|
+
return e.i ? "effect" : "memo";
|
|
123
125
|
}
|
|
124
126
|
|
|
125
127
|
/**
|
|
@@ -224,17 +226,24 @@ const interactionStack = [];
|
|
|
224
226
|
|
|
225
227
|
function interactionStart(e) {
|
|
226
228
|
interactionStack.push(currentInteraction);
|
|
229
|
+
// No `ref.event`: the WeakMap answers undefined.
|
|
230
|
+
const n = eventInteractions.get(e.event);
|
|
231
|
+
if (n?.joinable && openInteractions.has(n)) {
|
|
232
|
+
n.open = true;
|
|
233
|
+
currentInteraction = n.event.origin;
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
227
236
|
// One clock read: the frame opens now; the interaction began at `ref.at`
|
|
228
237
|
// when the runtime dated it (the event's own timestamp), else now too.
|
|
229
|
-
const
|
|
230
|
-
const
|
|
238
|
+
const t = now();
|
|
239
|
+
const o = {
|
|
231
240
|
kind: "interaction",
|
|
232
241
|
name: e.type,
|
|
233
|
-
at: e.at ??
|
|
242
|
+
at: e.at ?? t
|
|
234
243
|
};
|
|
235
|
-
if (e.target)
|
|
236
|
-
currentInteraction =
|
|
237
|
-
openInteraction(t,
|
|
244
|
+
if (e.target) o.target = targetLabel(e.target);
|
|
245
|
+
currentInteraction = o;
|
|
246
|
+
openInteraction(o, t, e.event);
|
|
238
247
|
}
|
|
239
248
|
|
|
240
249
|
function interactionEnd(e) {
|
|
@@ -318,7 +327,7 @@ const effectFrames = new WeakMap;
|
|
|
318
327
|
const n = effectStack[effectStack.length - 1];
|
|
319
328
|
if (n !== undefined) effectFrames.set(e, {
|
|
320
329
|
node: n,
|
|
321
|
-
causes: n.
|
|
330
|
+
causes: n.ge
|
|
322
331
|
});
|
|
323
332
|
}
|
|
324
333
|
|
|
@@ -336,7 +345,7 @@ function pushFrame(e, n, t, o) {
|
|
|
336
345
|
if (s !== undefined) i.interaction = s;
|
|
337
346
|
if (o !== undefined) {
|
|
338
347
|
const e = o;
|
|
339
|
-
if (e.
|
|
348
|
+
if (e.we !== undefined) i.run = e.we;
|
|
340
349
|
// The frame → node map is filled by the first write inside the callback
|
|
341
350
|
// (noteEffectOrigin), not here: most callbacks never write.
|
|
342
351
|
effectStack.push(o);
|
|
@@ -433,12 +442,6 @@ function originEnd() {
|
|
|
433
442
|
}
|
|
434
443
|
|
|
435
444
|
/** Record a root change (setSignal / refresh / async landing) on the node. */
|
|
436
|
-
/** Live subscriber count, walked on demand — the core keeps no counter. */ function countSubscribers(e) {
|
|
437
|
-
let n = 0;
|
|
438
|
-
for (let t = e.u; t !== null; t = t.Pe) n++;
|
|
439
|
-
return n;
|
|
440
|
-
}
|
|
441
|
-
|
|
442
445
|
/**
|
|
443
446
|
* HUGE_FAN_OUT from the engine's lower `fanOut` threshold (see dev.ts): a
|
|
444
447
|
* committed root invalidation reaching hundreds of subscribers re-runs all
|
|
@@ -447,12 +450,20 @@ function originEnd() {
|
|
|
447
450
|
* was a post-construction field that forked node shapes). Stops at
|
|
448
451
|
* GRAPH_SIZE_WARN_AT, where the always-on core check takes over, so the two
|
|
449
452
|
* never fire for the same write; the once-per-node dedupe is the core's.
|
|
453
|
+
* HMR plumbing subscribers (the refresh memo of every mounted instance, on
|
|
454
|
+
* a component's registration signal) are not counted toward `fanOut`; the
|
|
455
|
+
* handover is on the full list, which is what the core counts.
|
|
450
456
|
*/ function checkFanOut(e, n) {
|
|
451
457
|
const t = options.fanOut;
|
|
452
458
|
if (typeof t !== "number") return;
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
459
|
+
let o = 0;
|
|
460
|
+
let i = 0;
|
|
461
|
+
for (let n = e.t; n !== null; n = n.l) {
|
|
462
|
+
o++;
|
|
463
|
+
if (!(n.o.u & CONFIG_PLUMBING)) i++;
|
|
464
|
+
}
|
|
465
|
+
if (i < t || o >= GRAPH_SIZE_WARN_AT) return;
|
|
466
|
+
noteFanOut(e, i, n);
|
|
456
467
|
}
|
|
457
468
|
|
|
458
469
|
function stampWrite(e, n, t = NO_VALUES, o = NO_VALUES) {
|
|
@@ -472,8 +483,8 @@ function stampWrite(e, n, t = NO_VALUES, o = NO_VALUES) {
|
|
|
472
483
|
i.origin = n === "async" ? asyncOrigin(e) : currentOrigin();
|
|
473
484
|
i.at = now();
|
|
474
485
|
i.stack = captureStack();
|
|
475
|
-
const s = e.
|
|
476
|
-
e.
|
|
486
|
+
const s = e.ve;
|
|
487
|
+
e.ve = i;
|
|
477
488
|
noteNavigationWrite(s, i);
|
|
478
489
|
noteInteractionWrite(i.origin, excludedNode(e));
|
|
479
490
|
if (n === "write") trackEffectWrite(e, i, o);
|
|
@@ -484,7 +495,7 @@ function stampWrite(e, n, t = NO_VALUES, o = NO_VALUES) {
|
|
|
484
495
|
}
|
|
485
496
|
|
|
486
497
|
/** Record a derived change (memo produced a new value) with its causes. */ function stampDerived(e, n) {
|
|
487
|
-
e.
|
|
498
|
+
e.ve = {
|
|
488
499
|
seq: ++changeSeq,
|
|
489
500
|
kind: "derived",
|
|
490
501
|
name: nodeName(e),
|
|
@@ -499,19 +510,19 @@ function stampWrite(e, n, t = NO_VALUES, o = NO_VALUES) {
|
|
|
499
510
|
* run's links. A refresh() stamp on the node itself also counts — that is a
|
|
500
511
|
* self-invalidation, not a dep change.
|
|
501
512
|
*/ function collectCauses(e) {
|
|
502
|
-
const n = e.
|
|
513
|
+
const n = e.ke ?? 0;
|
|
503
514
|
const t = [];
|
|
504
|
-
const o = e.
|
|
515
|
+
const o = e.ve;
|
|
505
516
|
if (o !== undefined && o.seq > n && o.kind === "refresh") t.push(o);
|
|
506
|
-
for (let o = e.
|
|
507
|
-
const e = o.
|
|
517
|
+
for (let o = e._; o !== null; o = o.p) {
|
|
518
|
+
const e = o.I.ve;
|
|
508
519
|
if (e !== undefined && e.seq > n) t.push(e);
|
|
509
520
|
}
|
|
510
521
|
return t;
|
|
511
522
|
}
|
|
512
523
|
|
|
513
524
|
/** Advance the node's seen-cursor to the present. Call after every run. */ function markSeen(e) {
|
|
514
|
-
e.
|
|
525
|
+
e.ke = changeSeq;
|
|
515
526
|
}
|
|
516
527
|
|
|
517
528
|
/**
|
|
@@ -523,32 +534,39 @@ function stampWrite(e, n, t = NO_VALUES, o = NO_VALUES) {
|
|
|
523
534
|
* still owed) and not part of the subscription diff.
|
|
524
535
|
*/ function captureDeps(e) {
|
|
525
536
|
const n = [];
|
|
526
|
-
for (let t = e.
|
|
527
|
-
n.push(t.
|
|
528
|
-
if (t === e.
|
|
537
|
+
for (let t = e._; t !== null; t = t.p) {
|
|
538
|
+
n.push(t.I);
|
|
539
|
+
if (t === e.S) break;
|
|
529
540
|
}
|
|
530
541
|
return n;
|
|
531
542
|
}
|
|
532
543
|
|
|
533
544
|
/**
|
|
534
545
|
* Wide-scope warning — the coarse-read / helper-leak signature: one scope
|
|
535
|
-
* subscribed to
|
|
546
|
+
* subscribed to hundreds of sources re-runs when ANY of them change. Fired from
|
|
536
547
|
* recordRerun for re-runs and directly from recompute for creation runs (a
|
|
537
|
-
* memo can be born too wide). Re-warns only on 50% further growth.
|
|
548
|
+
* memo can be born too wide). Re-warns only on 50% further growth. Counts
|
|
549
|
+
* what the scope's author can act on: HMR plumbing sources are not counted,
|
|
550
|
+
* and a framework scope wide by construction (CONFIG_WIDE) is not judged —
|
|
551
|
+
* the always-on HUGE_FAN_IN still is.
|
|
538
552
|
*/ function checkDepWidth(e) {
|
|
539
553
|
const n = options.wideDeps;
|
|
540
|
-
|
|
554
|
+
// Only dev sets CONFIG_WIDE (see constants.ts).
|
|
555
|
+
if (n === false || false) return;
|
|
541
556
|
let t = 0;
|
|
542
557
|
const o = [];
|
|
543
|
-
for (let n = e.
|
|
544
|
-
|
|
545
|
-
if (
|
|
546
|
-
|
|
558
|
+
for (let n = e._; n !== null; n = n.p) {
|
|
559
|
+
// HMR plumbing is a dev-only hop the scope cannot read past: not a source.
|
|
560
|
+
if (!(n.I.u & CONFIG_PLUMBING)) {
|
|
561
|
+
t++;
|
|
562
|
+
if (o.length < 12) o.push(nodeName(n.I));
|
|
563
|
+
}
|
|
564
|
+
if (n === e.S) break;
|
|
547
565
|
// the validated prefix, as captureDeps
|
|
548
566
|
}
|
|
549
567
|
const i = e;
|
|
550
|
-
if (t < n || t < (i.
|
|
551
|
-
i.
|
|
568
|
+
if (t < n || t < (i.ye ?? 0) * 1.5) return;
|
|
569
|
+
i.ye = t;
|
|
552
570
|
const s = `[WIDE_SCOPE_DEPS] ${nodeKind(e)} "${nodeName(e)}" is subscribed to ${t} sources — ` + `it re-runs when any of them change. Narrow its reads or split it into smaller memos. ` + `Sources: ${o.join(", ")}${t > o.length ? ", …" : ""}`;
|
|
553
571
|
reportDiagnostic(emitDiagnostic({
|
|
554
572
|
code: "WIDE_SCOPE_DEPS",
|
|
@@ -577,14 +595,14 @@ const HOT_FANOUT_FIRST_MILESTONE = 5;
|
|
|
577
595
|
if (t === false) return;
|
|
578
596
|
const o = e;
|
|
579
597
|
const i = Date.now();
|
|
580
|
-
if (o
|
|
581
|
-
o
|
|
582
|
-
o.
|
|
583
|
-
o.
|
|
584
|
-
}
|
|
585
|
-
o.
|
|
586
|
-
if (o.
|
|
587
|
-
o.
|
|
598
|
+
if (o.$e === undefined || i - o.$e > t.windowMs) {
|
|
599
|
+
o.$e = i;
|
|
600
|
+
o.be = 0;
|
|
601
|
+
o.Fe = false;
|
|
602
|
+
}
|
|
603
|
+
o.be = (o.be ?? 0) + 1;
|
|
604
|
+
if (o.Fe || o.be < t.count) return;
|
|
605
|
+
o.Fe = true;
|
|
588
606
|
// Root-cause key: the set of originating writes behind this scope's latest
|
|
589
607
|
// re-run. Scopes hot from the SAME roots share one aggregation window.
|
|
590
608
|
const s = new Set;
|
|
@@ -601,10 +619,10 @@ const HOT_FANOUT_FIRST_MILESTONE = 5;
|
|
|
601
619
|
hotCauses.set(r, a);
|
|
602
620
|
}
|
|
603
621
|
a.scopes++;
|
|
604
|
-
a.runs += o.
|
|
622
|
+
a.runs += o.be;
|
|
605
623
|
if (a.scopes === 1) {
|
|
606
624
|
const s = n.map(e => `"${e.name}" (${e.kind})`).join(", ");
|
|
607
|
-
const r = `[HOT_SCOPE_RERUNS] ${nodeKind(e)} "${nodeName(e)}" re-ran ${o.
|
|
625
|
+
const r = `[HOT_SCOPE_RERUNS] ${nodeKind(e)} "${nodeName(e)}" re-ran ${o.be} times ` + `in ${Math.max(1, i - o.$e)}ms — a hot signal is likely leaking into this ` + `scope. Latest cause: ${s || "(untracked pull)"}`;
|
|
608
626
|
reportDiagnostic(emitDiagnostic({
|
|
609
627
|
code: "HOT_SCOPE_RERUNS",
|
|
610
628
|
kind: "perf",
|
|
@@ -612,7 +630,7 @@ const HOT_FANOUT_FIRST_MILESTONE = 5;
|
|
|
612
630
|
message: r,
|
|
613
631
|
nodeName: nodeName(e),
|
|
614
632
|
data: {
|
|
615
|
-
runs: o.
|
|
633
|
+
runs: o.be,
|
|
616
634
|
windowMs: t.windowMs,
|
|
617
635
|
causes: n.map(e => e.name)
|
|
618
636
|
}
|
|
@@ -650,16 +668,16 @@ const HOT_FANOUT_FIRST_MILESTONE = 5;
|
|
|
650
668
|
if (o === false) return;
|
|
651
669
|
const i = e;
|
|
652
670
|
const s = now();
|
|
653
|
-
if (i.
|
|
654
|
-
i.
|
|
655
|
-
i.
|
|
656
|
-
i.
|
|
657
|
-
}
|
|
658
|
-
i.
|
|
659
|
-
if (i.
|
|
660
|
-
i.
|
|
671
|
+
if (i.Le === undefined || s - i.Le > o.windowMs) {
|
|
672
|
+
i.Le = s;
|
|
673
|
+
i.Ce = 0;
|
|
674
|
+
i.Me = false;
|
|
675
|
+
}
|
|
676
|
+
i.Ce = (i.Ce ?? 0) + n;
|
|
677
|
+
if (i.Me || i.Ce < o.budgetMs) return;
|
|
678
|
+
i.Me = true;
|
|
661
679
|
const r = t.map(e => `"${e.name}" (${e.kind})`).join(", ");
|
|
662
|
-
const a = `[HOT_SCOPE_TIME] ${nodeKind(e)} "${nodeName(e)}" spent ` + `${i.
|
|
680
|
+
const a = `[HOT_SCOPE_TIME] ${nodeKind(e)} "${nodeName(e)}" spent ` + `${i.Ce.toFixed(1)}ms of compute inside one ${o.windowMs}ms window ` + `(budget ${o.budgetMs}ms). Latest cause: ${r || "(untracked pull)"}`;
|
|
663
681
|
reportDiagnostic(emitDiagnostic({
|
|
664
682
|
code: "HOT_SCOPE_TIME",
|
|
665
683
|
kind: "perf",
|
|
@@ -667,7 +685,7 @@ const HOT_FANOUT_FIRST_MILESTONE = 5;
|
|
|
667
685
|
message: a,
|
|
668
686
|
nodeName: nodeName(e),
|
|
669
687
|
data: {
|
|
670
|
-
spentMs: i.
|
|
688
|
+
spentMs: i.Ce,
|
|
671
689
|
budgetMs: o.budgetMs,
|
|
672
690
|
windowMs: o.windowMs,
|
|
673
691
|
causes: t.map(e => e.name)
|
|
@@ -682,31 +700,34 @@ const HOT_FANOUT_FIRST_MILESTONE = 5;
|
|
|
682
700
|
* names it while it happens, with the input that keeps triggering it. The
|
|
683
701
|
* fix is upstream: an equality boundary on the part of the input the scope
|
|
684
702
|
* depends on, or a narrower read. Plain runs only — a held or overlay run
|
|
685
|
-
* may be replayed and is never blamed as waste.
|
|
703
|
+
* may be replayed and is never blamed as waste. A run that committed
|
|
704
|
+
* `undefined` arrives here as changed (see `recomputeEnd`): its work is a
|
|
705
|
+
* side effect (a projection's draft writes, a write from inside a memo), not
|
|
706
|
+
* its output.
|
|
686
707
|
*/ function checkWastedRecompute(e, n, t, o, i, s) {
|
|
687
708
|
const r = options.wastedRecompute;
|
|
688
709
|
if (r === false || t !== "plain") return;
|
|
689
710
|
// This runs on every re-run: fields on the node (one property read each,
|
|
690
711
|
// like hotRuns) and the run's own `at` — no clock read, no map lookup.
|
|
691
712
|
const a = e;
|
|
692
|
-
if (a.
|
|
693
|
-
a.
|
|
694
|
-
a.
|
|
695
|
-
a.
|
|
696
|
-
a.
|
|
697
|
-
a.
|
|
698
|
-
}
|
|
699
|
-
a.
|
|
713
|
+
if (a.He === undefined || n - a.He > r.windowMs) {
|
|
714
|
+
a.He = n;
|
|
715
|
+
a.We = 0;
|
|
716
|
+
a.Ge = 0;
|
|
717
|
+
a.qe = 0;
|
|
718
|
+
a.Be = false;
|
|
719
|
+
}
|
|
720
|
+
a.We = a.We + 1;
|
|
700
721
|
if (!o) {
|
|
701
|
-
a.
|
|
702
|
-
a.
|
|
722
|
+
a.Ge = a.Ge + 1;
|
|
723
|
+
a.qe = a.qe + i;
|
|
703
724
|
}
|
|
704
|
-
if (a.
|
|
705
|
-
a.
|
|
725
|
+
if (a.Be || a.We < r.minRuns || a.Ge / a.We < r.ratio || a.qe < r.budgetMs) return;
|
|
726
|
+
a.Be = true;
|
|
706
727
|
const c = {
|
|
707
|
-
runs: a.
|
|
708
|
-
wasted: a.
|
|
709
|
-
wastedMs: a.
|
|
728
|
+
runs: a.We,
|
|
729
|
+
wasted: a.Ge,
|
|
730
|
+
wastedMs: a.qe
|
|
710
731
|
};
|
|
711
732
|
const d = s.map(e => `"${e.name}" (${e.kind})`).join(", ");
|
|
712
733
|
const f = `[WASTED_RECOMPUTE] ${nodeKind(e)} "${nodeName(e)}" re-ran ${c.runs} times in ` + `${r.windowMs}ms and ${c.wasted} of those produced the same value — ` + `${c.wastedMs.toFixed(1)}ms of compute the equality gate then discarded. Its inputs ` + `change without changing its result: put an equality boundary upstream (a memo over the ` + `part of the input it reads, or an \`equals\` on the source), or read a narrower slice ` + `(the property, not the object). Latest cause: ${d || "(untracked pull)"}`;
|
|
@@ -729,14 +750,14 @@ const HOT_FANOUT_FIRST_MILESTONE = 5;
|
|
|
729
750
|
function recordRerun(e, n, t, o, i, s) {
|
|
730
751
|
const r = n.causes;
|
|
731
752
|
const a = e;
|
|
732
|
-
const c = a.
|
|
753
|
+
const c = a.ge;
|
|
733
754
|
const d = n.interaction;
|
|
734
755
|
if (excludedNode(e)) {
|
|
735
756
|
// The observer's own computation: keep the per-node bookkeeping its
|
|
736
757
|
// effect phase reads (see effectRunStart) and record nothing.
|
|
737
|
-
a.
|
|
738
|
-
a.
|
|
739
|
-
a.
|
|
758
|
+
a.xe = d;
|
|
759
|
+
a.we = undefined;
|
|
760
|
+
a.ge = r;
|
|
740
761
|
return;
|
|
741
762
|
}
|
|
742
763
|
// The facts every run leaves on the node, record or not: the run sequence
|
|
@@ -745,10 +766,10 @@ function recordRerun(e, n, t, o, i, s) {
|
|
|
745
766
|
// cause list of its own, so it takes this run's interaction and causes
|
|
746
767
|
// (see effectRunStart / pushFrame).
|
|
747
768
|
const f = ++runSeq;
|
|
748
|
-
const u = a.
|
|
749
|
-
a.
|
|
750
|
-
a.
|
|
751
|
-
a.
|
|
769
|
+
const u = a.Ke = (a.Ke ?? 0) + 1;
|
|
770
|
+
a.xe = d;
|
|
771
|
+
a.we = f;
|
|
772
|
+
a.ge = r;
|
|
752
773
|
noteInteractionRun(d, t.selfMs, false);
|
|
753
774
|
if (openFlush !== null) noteFlushRun(openFlush, false, d);
|
|
754
775
|
// The checks read the facts, not the record, so they run with or without
|
|
@@ -865,8 +886,10 @@ function shallowEquivalent(e, n) {
|
|
|
865
886
|
for (let e = 0; e < t.length; e++) if (t[e] !== o[e]) return false;
|
|
866
887
|
return true;
|
|
867
888
|
}
|
|
868
|
-
|
|
869
|
-
|
|
889
|
+
// Every own key, symbols included: a symbol-keyed box holding a fresh value
|
|
890
|
+
// each run (web's `dynamic` boxes its in-flight promise) is not equivalent.
|
|
891
|
+
const o = Reflect.ownKeys(e);
|
|
892
|
+
if (o.length > UNSTABLE_KEY_CAP || o.length !== Reflect.ownKeys(n).length) return false;
|
|
870
893
|
for (const t of o) {
|
|
871
894
|
if (!(t in n) || e[t] !== n[t]) return false;
|
|
872
895
|
}
|
|
@@ -887,15 +910,15 @@ function shallowEquivalent(e, n) {
|
|
|
887
910
|
const i = e;
|
|
888
911
|
if (n === t || // paranoia: changed runs should never hit this
|
|
889
912
|
!isPlainShape(n) || !isPlainShape(t) || !shallowEquivalent(n, t)) {
|
|
890
|
-
i.
|
|
891
|
-
i.
|
|
913
|
+
i.Ve = 0;
|
|
914
|
+
i.je = false;
|
|
892
915
|
return;
|
|
893
916
|
}
|
|
894
|
-
i.
|
|
895
|
-
if (i.
|
|
896
|
-
i.
|
|
917
|
+
i.Ve = (i.Ve ?? 0) + 1;
|
|
918
|
+
if (i.je || i.Ve < o) return;
|
|
919
|
+
i.je = true;
|
|
897
920
|
const s = Array.isArray(t) ? "array" : "object";
|
|
898
|
-
const r = `[UNSTABLE_MEMO_OUTPUT] memo "${nodeName(e)}" produced a new-but-equivalent ${s} on ` + `${i.
|
|
921
|
+
const r = `[UNSTABLE_MEMO_OUTPUT] memo "${nodeName(e)}" produced a new-but-equivalent ${s} on ` + `${i.Ve} consecutive runs — its equality gate never closes, so every ` + `subscriber re-runs on every upstream change. Return stable references or pass an ` + `\`equals\` option.`;
|
|
899
922
|
reportDiagnostic(emitDiagnostic({
|
|
900
923
|
code: "UNSTABLE_MEMO_OUTPUT",
|
|
901
924
|
kind: "perf",
|
|
@@ -903,7 +926,7 @@ function shallowEquivalent(e, n) {
|
|
|
903
926
|
message: r,
|
|
904
927
|
nodeName: nodeName(e),
|
|
905
928
|
data: {
|
|
906
|
-
runs: i.
|
|
929
|
+
runs: i.Ve,
|
|
907
930
|
shape: s
|
|
908
931
|
}
|
|
909
932
|
}, e));
|
|
@@ -1042,17 +1065,17 @@ const copyReported = new WeakSet;
|
|
|
1042
1065
|
const s = i !== undefined && i.kind === "effect" ? effectFrames.get(i) : undefined;
|
|
1043
1066
|
const r = s === undefined ? 0 : devId(s.node);
|
|
1044
1067
|
recordNodes.set(n, e);
|
|
1045
|
-
o.
|
|
1046
|
-
if (s !== undefined && t !== undefined && t === s.node.
|
|
1068
|
+
o.Ye = o.Ye === undefined || o.Ye === r ? r : null;
|
|
1069
|
+
if (s !== undefined && t !== undefined && t === s.node.pe) {
|
|
1047
1070
|
copyWrites.add(n);
|
|
1048
|
-
o.
|
|
1049
|
-
o.
|
|
1050
|
-
if (o.
|
|
1051
|
-
} else o.
|
|
1071
|
+
o.ze = o.Xe === r ? (o.ze ?? 0) + 1 : 1;
|
|
1072
|
+
o.Xe = r;
|
|
1073
|
+
if (o.ze >= 2 && o.Ye === r) checkCopyEffect(s.node, e);
|
|
1074
|
+
} else o.ze = 0;
|
|
1052
1075
|
}
|
|
1053
1076
|
|
|
1054
1077
|
/** The effect's source whose current value the compute output is, if any (the prop-to-state port). */ function passthroughSource(e) {
|
|
1055
|
-
for (let n = e.
|
|
1078
|
+
for (let n = e._; n !== null; n = n.p) if (n.I.pe === e.pe) return nodeName(n.I);
|
|
1056
1079
|
return undefined;
|
|
1057
1080
|
}
|
|
1058
1081
|
|
|
@@ -1125,7 +1148,7 @@ function checkCopyEffect(e, n) {
|
|
|
1125
1148
|
c.count++;
|
|
1126
1149
|
const d = copyWrites.has(s);
|
|
1127
1150
|
const f = recordNodes.get(s);
|
|
1128
|
-
const u = f !== undefined && f.
|
|
1151
|
+
const u = f !== undefined && f.Ye === devId(i.node);
|
|
1129
1152
|
// Derivable outright: the value is the compute output and nothing else
|
|
1130
1153
|
// writes the signal. A copy INTO a signal that has other writers is the
|
|
1131
1154
|
// "reset editable state from a source" shape — the tear is real, but a memo
|
|
@@ -1141,7 +1164,7 @@ function checkCopyEffect(e, n) {
|
|
|
1141
1164
|
copyReported.add(f);
|
|
1142
1165
|
}
|
|
1143
1166
|
if (h === "warn") c.warned = true;
|
|
1144
|
-
const p = e.
|
|
1167
|
+
const p = e.i ? "effect" : "memo";
|
|
1145
1168
|
const m = nodeName(i.node);
|
|
1146
1169
|
const g = l ? copyRepair(i.node, s.name) : d ? `The written value is the effect's compute output, but "${s.name}" has other ` + `writers — editable state reset from a source. If the reset is the intent, the tear ` + `is its cost; if "${s.name}" only ever mirrors the source, drop the local copy ` + `and read the source.` : u ? `Nothing else writes "${s.name}" — it is derived state: make it a memo over what ` + `the effect reads and every reader gets it in the same flush.` : `If "${s.name}" is computed from what the effect reads, make it a memo so readers ` + `get it in the same flush; if the write reads something outside the graph (layout, ` + `time), the tear is the cost of measuring.`;
|
|
1147
1170
|
const w = `[EFFECT_RELAY_TEAR] ${p} "${nodeName(e)}" ran twice for one write of ` + `"${r.name}": once in the flush where "${r.name}" changed, and again after ` + `effect "${m}" relayed it by writing "${s.name}" — the first frame showed the ` + `new "${r.name}" with the stale "${s.name}"` + (c.count > 1 ? ` (${c.count} times so far)` : "") + `. ${g}`;
|
|
@@ -1232,7 +1255,7 @@ const listIdentityWarned = new WeakSet;
|
|
|
1232
1255
|
function recordId(e) {
|
|
1233
1256
|
if (e === null || typeof e !== "object") return undefined;
|
|
1234
1257
|
const n = e;
|
|
1235
|
-
return n.id ?? n.key ?? n.
|
|
1258
|
+
return n.id ?? n.key ?? n.Qe ?? undefined;
|
|
1236
1259
|
}
|
|
1237
1260
|
|
|
1238
1261
|
function checkListIdentity(e, n, t, o, i) {
|
|
@@ -1389,7 +1412,7 @@ function trackFlightStart(e, n) {
|
|
|
1389
1412
|
const o = t - n.origin;
|
|
1390
1413
|
for (const n of folds) n.flightLanded?.(e, o);
|
|
1391
1414
|
if (records.observed("flight")) emitFlight(e, n, t, "landed");
|
|
1392
|
-
const i = e.
|
|
1415
|
+
const i = e.ve;
|
|
1393
1416
|
// Only a stamp this landing produced may carry the measurement — a stale
|
|
1394
1417
|
// async record from a previous landing must not be re-labeled.
|
|
1395
1418
|
if (i !== undefined && i.kind === "async" && i.seq > n.startSeq) landedFlights.set(i, {
|
|
@@ -1425,8 +1448,8 @@ function checkWaterfall(e, n, t) {
|
|
|
1425
1448
|
}
|
|
1426
1449
|
if (i < 2) return;
|
|
1427
1450
|
const r = e;
|
|
1428
|
-
if ((r.
|
|
1429
|
-
r.
|
|
1451
|
+
if ((r.Ze ?? 0) >= i) return;
|
|
1452
|
+
r.Ze = i;
|
|
1430
1453
|
const a = [ ...n.slice(n.length - (i - 1)), {
|
|
1431
1454
|
name: nodeName(e),
|
|
1432
1455
|
ms: t
|
|
@@ -1460,10 +1483,10 @@ let activeHold = null;
|
|
|
1460
1483
|
|
|
1461
1484
|
let holdLog = [];
|
|
1462
1485
|
|
|
1463
|
-
/**
|
|
1464
|
-
*
|
|
1465
|
-
*
|
|
1466
|
-
return
|
|
1486
|
+
/** A lane's derivation (L2: a node carrying the lane's value that is not a
|
|
1487
|
+
* written guess — lanes.ts) is a lane pass's result, not a write anyone
|
|
1488
|
+
* made: not an acknowledgement. */ function isCompanion(e) {
|
|
1489
|
+
return (e.u & (CONFIG_OVERRIDE | CONFIG_GUESS)) === CONFIG_OVERRIDE;
|
|
1467
1490
|
}
|
|
1468
1491
|
|
|
1469
1492
|
const HOLD_CENSUS_CAP = 1e4;
|
|
@@ -1484,13 +1507,14 @@ function acknowledge(e, n, t, o) {
|
|
|
1484
1507
|
}); else if (r !== undefined) s.reader ??= r;
|
|
1485
1508
|
}
|
|
1486
1509
|
|
|
1487
|
-
|
|
1510
|
+
/** The declarations made against the hold: the guesses of its lanes
|
|
1511
|
+
* (lanes.ts `_laneGuesses`) and its `affects()` marks (`_marks`). */ function censusRegistrations(e, n) {
|
|
1488
1512
|
const t = {
|
|
1489
1513
|
left: HOLD_CENSUS_CAP
|
|
1490
1514
|
};
|
|
1491
|
-
|
|
1492
|
-
for (const
|
|
1493
|
-
for (const o of e.
|
|
1515
|
+
const o = GlobalQueue.Je?.(e);
|
|
1516
|
+
if (o !== undefined) for (const e of o) acknowledge(n, "optimistic", nodeName(e), reachesEffect(e, t));
|
|
1517
|
+
if (e.T !== null) for (const o of e.T) acknowledge(n, "affects", nodeName(o), reachesEffect(o, t));
|
|
1494
1518
|
}
|
|
1495
1519
|
|
|
1496
1520
|
/**
|
|
@@ -1504,9 +1528,9 @@ function censusRegistrations(e, n) {
|
|
|
1504
1528
|
const o = [ e ];
|
|
1505
1529
|
while (o.length > 0 && n.left-- > 0) {
|
|
1506
1530
|
const e = o.pop();
|
|
1507
|
-
for (let n = e.
|
|
1508
|
-
const e = n.
|
|
1509
|
-
if (e.
|
|
1531
|
+
for (let n = e.t; n !== null; n = n.l) {
|
|
1532
|
+
const e = n.o;
|
|
1533
|
+
if (e.i) return e;
|
|
1510
1534
|
if (!t.has(e)) {
|
|
1511
1535
|
t.add(e);
|
|
1512
1536
|
o.push(e);
|
|
@@ -1516,7 +1540,10 @@ function censusRegistrations(e, n) {
|
|
|
1516
1540
|
return null;
|
|
1517
1541
|
}
|
|
1518
1542
|
|
|
1519
|
-
/**
|
|
1543
|
+
/** Verdict readers an effect reads, anywhere downstream of the hold's nodes
|
|
1544
|
+
* (L2: a pass that entered an `isPending`/`latest` window is marked
|
|
1545
|
+
* CONFIG_VERDICT; the observe tier notes which windows — verdict.ts
|
|
1546
|
+
* `_devWindows`). The reader acknowledges the node it derives from. */ function censusCompanions(e, n) {
|
|
1520
1547
|
const t = new Set;
|
|
1521
1548
|
const o = [ ...e ];
|
|
1522
1549
|
const i = {
|
|
@@ -1526,14 +1553,16 @@ function censusRegistrations(e, n) {
|
|
|
1526
1553
|
const e = o.pop();
|
|
1527
1554
|
if (t.has(e)) continue;
|
|
1528
1555
|
t.add(e);
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1556
|
+
for (let t = e.t; t !== null; t = t.l) {
|
|
1557
|
+
const s = t.o;
|
|
1558
|
+
if (s.u & CONFIG_VERDICT) {
|
|
1559
|
+
const t = s.en ?? 0;
|
|
1560
|
+
const o = s.i ? s : reachesEffect(s, i);
|
|
1561
|
+
if (t & 1 && o !== null) acknowledge(n, "isPending", nodeName(e), o);
|
|
1562
|
+
if (t & 2 && o !== null) acknowledge(n, "latest", nodeName(e), o);
|
|
1563
|
+
}
|
|
1564
|
+
o.push(s);
|
|
1535
1565
|
}
|
|
1536
|
-
for (let n = e.u; n !== null; n = n.Pe) o.push(n.Ae);
|
|
1537
1566
|
}
|
|
1538
1567
|
}
|
|
1539
1568
|
|
|
@@ -1561,8 +1590,8 @@ function trackHoldStart(e) {
|
|
|
1561
1590
|
if (options.holds === false) return;
|
|
1562
1591
|
const n = holdState(e);
|
|
1563
1592
|
n.flushes++;
|
|
1564
|
-
if (e.tt
|
|
1565
|
-
for (const
|
|
1593
|
+
if (e.tt > 0) n.action = true;
|
|
1594
|
+
for (const t of blockersOf(e)) n.blockers.add(t);
|
|
1566
1595
|
censusRegistrations(e, n);
|
|
1567
1596
|
activeHold = n;
|
|
1568
1597
|
}
|
|
@@ -1599,9 +1628,9 @@ function trackHoldSettled(e) {
|
|
|
1599
1628
|
let i;
|
|
1600
1629
|
let s;
|
|
1601
1630
|
let r = -Infinity;
|
|
1602
|
-
for (const n of e.
|
|
1603
|
-
if (typeof n.
|
|
1604
|
-
const e = n.
|
|
1631
|
+
for (const n of e.nn) {
|
|
1632
|
+
if (typeof n.re === "function" || isCompanion(n)) continue;
|
|
1633
|
+
const e = n.ve;
|
|
1605
1634
|
if (e === undefined || e.kind !== "write") continue;
|
|
1606
1635
|
if (o === null) o = n;
|
|
1607
1636
|
const a = {
|
|
@@ -1628,7 +1657,7 @@ function trackHoldSettled(e) {
|
|
|
1628
1657
|
return;
|
|
1629
1658
|
}
|
|
1630
1659
|
censusRegistrations(e, n);
|
|
1631
|
-
censusCompanions([ ...e.
|
|
1660
|
+
censusCompanions([ ...e.nn, ...n.blockers ], n);
|
|
1632
1661
|
const a = now();
|
|
1633
1662
|
// The hold began no later than its first parked flush; an interaction stamp
|
|
1634
1663
|
// reaches further back (dispatch). A node rewritten mid-hold keeps only its
|
|
@@ -1789,7 +1818,7 @@ function checkLongHold(e, n) {
|
|
|
1789
1818
|
if (!options.optimisticReverts || isCompanion(e)) return;
|
|
1790
1819
|
// Method call, as every commit path does: store slot nodes share one
|
|
1791
1820
|
// comparator that reads `this._host` (#3687).
|
|
1792
|
-
const i = e.
|
|
1821
|
+
const i = e.tn;
|
|
1793
1822
|
if (i && i.call(e, n, t)) return;
|
|
1794
1823
|
const s = nodeName(e);
|
|
1795
1824
|
// The two values are user data: quoted only under `values: "full"`; the
|
|
@@ -1846,33 +1875,33 @@ function checkLongHold(e, n) {
|
|
|
1846
1875
|
const meet = e => {
|
|
1847
1876
|
if (t.has(e)) return;
|
|
1848
1877
|
t.add(e);
|
|
1849
|
-
if (typeof e.
|
|
1878
|
+
if (typeof e.re === "function") n.computations++; else n.signals++;
|
|
1850
1879
|
o.push(e);
|
|
1851
1880
|
};
|
|
1852
1881
|
for (const t of e) {
|
|
1853
1882
|
n.owners++;
|
|
1854
1883
|
// Iterative: a deep tree must not grow the call stack.
|
|
1855
|
-
let e = t.
|
|
1884
|
+
let e = t.sn;
|
|
1856
1885
|
for (;;) {
|
|
1857
1886
|
while (e !== null) {
|
|
1858
1887
|
n.owners++;
|
|
1859
|
-
if (typeof e.
|
|
1860
|
-
if (e.
|
|
1861
|
-
e = e.
|
|
1888
|
+
if (typeof e.re === "function") meet(e);
|
|
1889
|
+
if (e.sn !== null) i.push(e);
|
|
1890
|
+
e = e.rn;
|
|
1862
1891
|
}
|
|
1863
1892
|
const t = i.pop();
|
|
1864
1893
|
if (t === undefined) break;
|
|
1865
|
-
e = t.
|
|
1894
|
+
e = t.sn;
|
|
1866
1895
|
}
|
|
1867
1896
|
}
|
|
1868
1897
|
for (let e = 0; e < o.length; e++) {
|
|
1869
1898
|
const t = o[e];
|
|
1870
1899
|
// Whoever reads this node: owned computations are already known; an
|
|
1871
1900
|
// ownerless one is met only here.
|
|
1872
|
-
for (let e = t.
|
|
1873
|
-
if (typeof t.
|
|
1901
|
+
for (let e = t.t; e !== null; e = e.l) meet(e.o);
|
|
1902
|
+
if (typeof t.re === "function") for (let e = t._; e !== null; e = e.p) {
|
|
1874
1903
|
n.edges++;
|
|
1875
|
-
meet(e.
|
|
1904
|
+
meet(e.I);
|
|
1876
1905
|
}
|
|
1877
1906
|
}
|
|
1878
1907
|
return n;
|
|
@@ -1900,7 +1929,10 @@ function trackGraph(e) {
|
|
|
1900
1929
|
};
|
|
1901
1930
|
if (o !== undefined) i.route = o;
|
|
1902
1931
|
records.emit("graph", i, undefined);
|
|
1903
|
-
|
|
1932
|
+
// The initial declaration is not a visit (it settles before route content
|
|
1933
|
+
// mounts — the same gate the feedback folds apply as `event.initial !== true`);
|
|
1934
|
+
// `initial` is `true | undefined`, set only by openNavigation, so truthiness is exact.
|
|
1935
|
+
if (n === false || o === undefined || e.initial) return;
|
|
1904
1936
|
let s = routeCounts.get(o);
|
|
1905
1937
|
if (s === undefined) routeCounts.set(o, s = []);
|
|
1906
1938
|
s.push(t);
|
|
@@ -1953,10 +1985,13 @@ function trackGraph(e) {
|
|
|
1953
1985
|
* One source abandoning flight after flight inside a window: every input
|
|
1954
1986
|
* asked again and the earlier answers were thrown away — the
|
|
1955
1987
|
* request-per-keystroke signature `feedback().flights[].abandoned` counts,
|
|
1956
|
-
* as a finding while it happens. Warned once per window per source.
|
|
1988
|
+
* as a finding while it happens. Warned once per window per source. A
|
|
1989
|
+
* framework node superseding its own flights by design (CONFIG_SUPERSEDES)
|
|
1990
|
+
* is not judged; its flights are still recorded.
|
|
1957
1991
|
*/ function checkAbandonedFlights(e, n) {
|
|
1958
1992
|
const t = options.abandonedFlights;
|
|
1959
|
-
|
|
1993
|
+
// Only dev sets CONFIG_SUPERSEDES (see constants.ts).
|
|
1994
|
+
if (t === false || false || excludedNode(e)) return;
|
|
1960
1995
|
const o = Date.now();
|
|
1961
1996
|
let i = abandonWindows.get(e);
|
|
1962
1997
|
if (i === undefined || o - i.start > t.windowMs) {
|
|
@@ -2151,8 +2186,8 @@ function closeNavigation(e) {
|
|
|
2151
2186
|
|
|
2152
2187
|
/** holdStart: `t`'s staged writes are parked — their navigations settle with `t`. */ function markNavigationsHeld(e) {
|
|
2153
2188
|
if (openNavs.size === 0) return;
|
|
2154
|
-
for (const n of e.
|
|
2155
|
-
const e = n.
|
|
2189
|
+
for (const n of e.nn) {
|
|
2190
|
+
const e = n.ve?.origin;
|
|
2156
2191
|
if (e?.kind !== "navigation") continue;
|
|
2157
2192
|
const t = navStates.get(e);
|
|
2158
2193
|
if (t !== undefined) t.held = true;
|
|
@@ -2161,8 +2196,8 @@ function closeNavigation(e) {
|
|
|
2161
2196
|
|
|
2162
2197
|
/** transitionSettled: `t` commits — the navigations whose writes it staged are done. */ function settleNavigations(e, n) {
|
|
2163
2198
|
if (openNavs.size === 0) return;
|
|
2164
|
-
for (const t of e.
|
|
2165
|
-
const e = t.
|
|
2199
|
+
for (const t of e.nn) {
|
|
2200
|
+
const e = t.ve?.origin;
|
|
2166
2201
|
if (e?.kind !== "navigation") continue;
|
|
2167
2202
|
const o = navStates.get(e);
|
|
2168
2203
|
if (o === undefined || !openNavs.has(o)) continue;
|
|
@@ -2320,12 +2355,14 @@ function settleNavigation(e, n, t) {
|
|
|
2320
2355
|
|
|
2321
2356
|
const interactionStates = new WeakMap;
|
|
2322
2357
|
|
|
2358
|
+
/** `InteractionRef.event` → the record its frames join (while `joinable`). Weak: the Event is never held. */ const eventInteractions = new WeakMap;
|
|
2359
|
+
|
|
2323
2360
|
/** Opened, not yet settled. */ const openInteractions = new Set;
|
|
2324
2361
|
|
|
2325
2362
|
let interactionLog = [];
|
|
2326
2363
|
|
|
2327
|
-
function openInteraction(e, n) {
|
|
2328
|
-
const
|
|
2364
|
+
function openInteraction(e, n, t) {
|
|
2365
|
+
const o = {
|
|
2329
2366
|
name: e.name,
|
|
2330
2367
|
at: e.at,
|
|
2331
2368
|
handlerMs: 0,
|
|
@@ -2337,26 +2374,37 @@ function openInteraction(e, n) {
|
|
|
2337
2374
|
navigations: [],
|
|
2338
2375
|
origin: e
|
|
2339
2376
|
};
|
|
2340
|
-
if (e.target !== undefined)
|
|
2377
|
+
if (e.target !== undefined) o.target = e.target;
|
|
2341
2378
|
// The runtime may date the frame from the event's own timestamp (before
|
|
2342
2379
|
// any queued task ran); the gap to here is the input delay the browser's
|
|
2343
2380
|
// INP counts first.
|
|
2344
|
-
if (n >
|
|
2345
|
-
const
|
|
2346
|
-
event:
|
|
2381
|
+
if (n > o.at) o.inputDelayMs = n - o.at;
|
|
2382
|
+
const i = {
|
|
2383
|
+
event: o,
|
|
2347
2384
|
open: true,
|
|
2348
2385
|
opened: n,
|
|
2349
2386
|
writeDrain: drainSeq,
|
|
2350
2387
|
heldIn: new Set,
|
|
2351
2388
|
held: false,
|
|
2352
2389
|
excludedWrites: 0,
|
|
2353
|
-
awaiting:
|
|
2354
|
-
actioned: false
|
|
2390
|
+
awaiting: 0,
|
|
2391
|
+
actioned: false,
|
|
2392
|
+
joinable: t !== undefined
|
|
2355
2393
|
};
|
|
2356
|
-
interactionStates.set(e,
|
|
2357
|
-
openInteractions.add(
|
|
2358
|
-
interactionLog.push(
|
|
2394
|
+
interactionStates.set(e, i);
|
|
2395
|
+
openInteractions.add(i);
|
|
2396
|
+
interactionLog.push(o);
|
|
2359
2397
|
if (interactionLog.length > options.historyLimit) interactionLog.shift();
|
|
2398
|
+
if (t !== undefined) {
|
|
2399
|
+
eventInteractions.set(t, i);
|
|
2400
|
+
// A microtask is too early: native dispatch runs microtasks between
|
|
2401
|
+
// listeners. The callback must not close over `key`: a throttled timer
|
|
2402
|
+
// would hold the Event strongly long past its dispatch.
|
|
2403
|
+
setTimeout(() => {
|
|
2404
|
+
i.joinable = false;
|
|
2405
|
+
if (openInteractions.has(i)) maybeSettleInteraction(i, i.settleAt);
|
|
2406
|
+
});
|
|
2407
|
+
}
|
|
2360
2408
|
}
|
|
2361
2409
|
|
|
2362
2410
|
/**
|
|
@@ -2377,19 +2425,19 @@ function openInteraction(e, n) {
|
|
|
2377
2425
|
// screen could have shown the wait.
|
|
2378
2426
|
const i = n !== null && (typeof n === "object" || typeof n === "function") && typeof n.then === "function" ? n : null;
|
|
2379
2427
|
if (i !== null) {
|
|
2380
|
-
t.awaiting
|
|
2428
|
+
t.awaiting++;
|
|
2381
2429
|
let e = false;
|
|
2382
2430
|
let n;
|
|
2383
2431
|
const settle = () => {
|
|
2384
2432
|
if (e) return;
|
|
2385
2433
|
e = true;
|
|
2386
2434
|
if (n !== undefined) clearTimeout(n);
|
|
2387
|
-
if (!openInteractions.has(t)) return;
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2435
|
+
if (!openInteractions.has(t) || --t.awaiting > 0) return;
|
|
2436
|
+
const o = now();
|
|
2437
|
+
// From the last handler's return: the frames of one event share a record.
|
|
2438
|
+
t.event.continuationMs = o - (t.opened + t.event.handlerMs);
|
|
2391
2439
|
checkUntrackedAsyncHandler(t);
|
|
2392
|
-
maybeSettleInteraction(t,
|
|
2440
|
+
maybeSettleInteraction(t, o);
|
|
2393
2441
|
};
|
|
2394
2442
|
n = setTimeout(settle, ASYNC_HANDLER_CAP_MS);
|
|
2395
2443
|
// A pending cap must not hold a Node process (tests, SSR harnesses) open.
|
|
@@ -2467,8 +2515,8 @@ function openInteraction(e, n) {
|
|
|
2467
2515
|
|
|
2468
2516
|
/** holdStart: `t` parked writes — the interactions that performed them wait for `t`. */ function markInteractionsHeld(e) {
|
|
2469
2517
|
if (openInteractions.size === 0) return;
|
|
2470
|
-
for (const n of e.
|
|
2471
|
-
const t = openInteractionOf(n.
|
|
2518
|
+
for (const n of e.nn) {
|
|
2519
|
+
const t = openInteractionOf(n.ve?.origin);
|
|
2472
2520
|
if (t !== undefined) {
|
|
2473
2521
|
t.heldIn.add(e);
|
|
2474
2522
|
t.held = true;
|
|
@@ -2491,12 +2539,16 @@ function openInteraction(e, n) {
|
|
|
2491
2539
|
|
|
2492
2540
|
function maybeSettleInteraction(e, n = now()) {
|
|
2493
2541
|
const t = e.event;
|
|
2494
|
-
if (e.open || e.awaiting || e.heldIn.size > 0) return;
|
|
2542
|
+
if (e.open || e.awaiting > 0 || e.heldIn.size > 0) return;
|
|
2495
2543
|
// A drain must have committed the last write (the handler's, or a redirect
|
|
2496
2544
|
// hop's after the click's own drain) — the handler returning is not the
|
|
2497
2545
|
// screen having it.
|
|
2498
2546
|
if (t.writes > 0 && drainSeq <= e.writeDrain) return;
|
|
2499
2547
|
for (const e of t.navigations) if (e.outcome === undefined) return;
|
|
2548
|
+
if (e.joinable) {
|
|
2549
|
+
e.settleAt = n;
|
|
2550
|
+
return;
|
|
2551
|
+
}
|
|
2500
2552
|
openInteractions.delete(e);
|
|
2501
2553
|
// Every write went to an excluded subject and nothing of the app's ran: the
|
|
2502
2554
|
// click was on the observer's own UI (a devtools panel's button). Not a
|
|
@@ -2551,7 +2603,7 @@ const engineHooks = {
|
|
|
2551
2603
|
prevDeps: n || !wantsRerun() ? null : captureDeps(e),
|
|
2552
2604
|
// Mirror recompute's own prev-value resolution: an earlier run in the
|
|
2553
2605
|
// same flush may still be holding in _pendingValue.
|
|
2554
|
-
prevValue: e.
|
|
2606
|
+
prevValue: e.Ae !== NOT_PENDING ? e.Ae : e.pe,
|
|
2555
2607
|
// A create run inherits the interaction of whatever is building it: the
|
|
2556
2608
|
// enclosing recompute (a parent's fn creating children) or, at the top
|
|
2557
2609
|
// of the recompute stack, the effect callback / handler frame.
|
|
@@ -2577,8 +2629,8 @@ const engineHooks = {
|
|
|
2577
2629
|
// committed compute output is an unchanged run. `undefined` outputs are
|
|
2578
2630
|
// exempt — a side-effect-only compute's work IS its effect phase, and
|
|
2579
2631
|
// identity of `undefined` proves nothing.
|
|
2580
|
-
if (t && r.causes !== null && e.
|
|
2581
|
-
const n = e.
|
|
2632
|
+
if (t && r.causes !== null && e.i) {
|
|
2633
|
+
const n = e.Ae !== NOT_PENDING ? e.Ae : e.pe;
|
|
2582
2634
|
if (n !== undefined && n === r.prevValue) t = false;
|
|
2583
2635
|
}
|
|
2584
2636
|
// Unstable-output check: memos only, non-create, plain runs with a
|
|
@@ -2586,7 +2638,12 @@ const engineHooks = {
|
|
|
2586
2638
|
// plain-flush memo commits and in `_value` for direct ones. Overlay runs
|
|
2587
2639
|
// are excluded — an optimistic re-derive legitimately produces fresh
|
|
2588
2640
|
// equivalents while the lane settles.
|
|
2589
|
-
if (r.causes !== null && t && !o && !i && !e.
|
|
2641
|
+
if (r.causes !== null && t && !o && !i && !e.i) checkUnstableOutput(e, r.prevValue, e.Ae !== NOT_PENDING ? e.Ae : e.pe);
|
|
2642
|
+
// The same exemption for memos: a run that committed `undefined` did its
|
|
2643
|
+
// work as a side effect (a projection's draft writes or reconcile, a
|
|
2644
|
+
// write from inside a memo), so core's `undefined === undefined` gate
|
|
2645
|
+
// proves nothing. Reported as changed, so no consumer counts it as waste.
|
|
2646
|
+
if (!t && (e.Ae !== NOT_PENDING ? e.Ae : e.pe) === undefined) t = true;
|
|
2590
2647
|
if (r.causes !== null) recordRerun(e, r, {
|
|
2591
2648
|
selfMs: c,
|
|
2592
2649
|
totalMs: a
|
|
@@ -2598,7 +2655,7 @@ const engineHooks = {
|
|
|
2598
2655
|
noteInteractionRun(r.interaction, c, true);
|
|
2599
2656
|
// The first effect callback runs with no re-run record to inherit from;
|
|
2600
2657
|
// hand it the interaction that built the node (see effectRunStart).
|
|
2601
|
-
e.
|
|
2658
|
+
e.xe = r.interaction;
|
|
2602
2659
|
if (openFlush !== null) noteFlushRun(openFlush, true, r.interaction);
|
|
2603
2660
|
if (records.observed("create")) {
|
|
2604
2661
|
const n = {
|
|
@@ -2628,9 +2685,9 @@ const engineHooks = {
|
|
|
2628
2685
|
trackFlightStart(e, n);
|
|
2629
2686
|
},
|
|
2630
2687
|
asyncStart(e) {
|
|
2631
|
-
asyncStartSeq = e.
|
|
2632
|
-
asyncStartTime = e.
|
|
2633
|
-
asyncStartValue = e.
|
|
2688
|
+
asyncStartSeq = e.ve?.seq ?? 0;
|
|
2689
|
+
asyncStartTime = e.Ne;
|
|
2690
|
+
asyncStartValue = e.pe;
|
|
2634
2691
|
},
|
|
2635
2692
|
asyncEnd(e, n, t, o) {
|
|
2636
2693
|
if (o) {
|
|
@@ -2640,7 +2697,7 @@ const engineHooks = {
|
|
|
2640
2697
|
// (or `_time`, for a same-reference commit under `equals: false`), and
|
|
2641
2698
|
// a transition hold parks the value in `_pendingValue`. A landing the
|
|
2642
2699
|
// equality gate swallowed moves none of them and must leave no stamp.
|
|
2643
|
-
const o = e.
|
|
2700
|
+
const o = e.pe !== asyncStartValue || e.Ne !== asyncStartTime || e.Ae === t;
|
|
2644
2701
|
if (o) stampWrite(e, "async", n === undefined ? NO_VALUES : n, t);
|
|
2645
2702
|
// Flight over either way — an equality-swallowed landing still spent
|
|
2646
2703
|
// the wall time (finalizeFlight only chains through a fresh stamp).
|
|
@@ -2650,12 +2707,12 @@ const engineHooks = {
|
|
|
2650
2707
|
// Landed through setSignal: reclassify its "write" stamp as an async
|
|
2651
2708
|
// landing — but only if it actually stamped (the value changed) since
|
|
2652
2709
|
// asyncStart; a no-change landing must leave no fresh stamp behind.
|
|
2653
|
-
const i = e.
|
|
2710
|
+
const i = e.ve;
|
|
2654
2711
|
if (i !== undefined && i.seq > asyncStartSeq && i.kind === "write") stampWrite(e, "async", NO_VALUES, t);
|
|
2655
2712
|
finalizeFlight(e);
|
|
2656
2713
|
},
|
|
2657
2714
|
effectRunStart(e) {
|
|
2658
|
-
pushFrame("effect", nodeName(e), e.
|
|
2715
|
+
pushFrame("effect", nodeName(e), e.xe, e);
|
|
2659
2716
|
// The frame's own `at` doubles as the record's start: set only while
|
|
2660
2717
|
// listened, so a listener arriving mid-callback finds no start and the
|
|
2661
2718
|
// end emits nothing for it — and an unlistened callback pays no clock read.
|