@solidjs/signals 2.0.0-rc.13 → 2.0.0-rc.15
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 +2059 -3768
- package/dist/dev.attribution.js +237 -101
- package/dist/dev.js +7466 -8497
- package/dist/observe/affects.js +128 -104
- package/dist/observe/boundaries.js +492 -665
- package/dist/observe/core/action.js +63 -66
- package/dist/observe/core/async.js +243 -379
- package/dist/observe/core/attribution-queries.js +1 -1
- package/dist/observe/core/attribution.js +394 -253
- package/dist/observe/core/constants.js +120 -187
- package/dist/observe/core/context.js +3 -3
- package/dist/observe/core/core.js +874 -1440
- package/dist/observe/core/dev.js +3 -3
- package/dist/observe/core/effect.js +73 -84
- package/dist/observe/core/external.js +5 -5
- package/dist/observe/core/graph.js +62 -71
- package/dist/observe/core/heap.js +75 -98
- package/dist/observe/core/invariants.js +1 -34
- package/dist/observe/core/lanes.js +750 -181
- package/dist/observe/core/owner.js +80 -101
- package/dist/observe/core/scheduler.js +719 -1356
- package/dist/observe/core/verdict.js +314 -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 +144 -216
- package/dist/observe/store/affects.js +142 -0
- package/dist/observe/store/hooks.js +13 -0
- package/dist/observe/store/index.js +27 -24
- package/dist/observe/store/optimistic.js +571 -0
- package/dist/observe/store/projection.js +308 -0
- package/dist/observe/store/reconcile.js +259 -0
- package/dist/observe/store/store.js +1988 -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 +130 -106
- package/dist/prod/boundaries.js +476 -654
- package/dist/prod/core/action.js +71 -74
- package/dist/prod/core/async.js +306 -444
- package/dist/prod/core/constants.js +117 -186
- package/dist/prod/core/context.js +3 -3
- package/dist/prod/core/core.js +888 -1462
- package/dist/prod/core/effect.js +88 -99
- package/dist/prod/core/external.js +5 -5
- package/dist/prod/core/graph.js +62 -71
- package/dist/prod/core/heap.js +75 -98
- package/dist/prod/core/invariants.js +1 -32
- package/dist/prod/core/lanes.js +712 -181
- package/dist/prod/core/owner.js +75 -96
- package/dist/prod/core/scheduler.js +686 -1335
- package/dist/prod/core/verdict.js +309 -621
- package/dist/prod/flatten.js +77 -0
- package/dist/prod/index.js +15 -13
- package/dist/prod/map.js +142 -145
- package/dist/prod/reveal.js +212 -0
- package/dist/prod/signals.js +155 -227
- package/dist/prod/store/affects.js +142 -0
- package/dist/prod/store/hooks.js +13 -0
- package/dist/prod/store/index.js +26 -19
- package/dist/prod/store/optimistic.js +566 -0
- package/dist/prod/store/projection.js +301 -0
- package/dist/prod/store/reconcile.js +259 -0
- package/dist/prod/store/store.js +1883 -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 +66 -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 +26 -11
- package/dist/types/core/constants.d.ts +130 -180
- package/dist/types/core/core.d.ts +134 -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 +74 -86
- package/dist/types/core/owner.d.ts +3 -3
- package/dist/types/core/scheduler.d.ts +255 -331
- package/dist/types/core/types.d.ts +45 -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 +12 -0
- package/dist/types/store/hooks.d.ts +48 -0
- package/dist/types/store/index.d.ts +34 -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 +132 -145
- 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
package/dist/dev.attribution.js
CHANGED
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
2
|
+
bv as OBSERVE,
|
|
3
|
+
bF as setAttributionHooks,
|
|
4
|
+
bG as liveRootOwners,
|
|
5
|
+
N as NOT_PENDING,
|
|
6
|
+
ap as emitDiagnostic,
|
|
7
|
+
bH as ownerPath,
|
|
8
|
+
aD as reportDiagnostic,
|
|
9
|
+
bI as isSuppressed,
|
|
10
|
+
bJ as blockersOf,
|
|
11
|
+
bK as CONFIG_PLUMBING,
|
|
12
|
+
bL as anyExcluded,
|
|
13
|
+
bM as isExcluded,
|
|
14
|
+
bN as CONFIG_WIDE,
|
|
15
|
+
a as CONFIG_OVERRIDE,
|
|
16
|
+
C as CONFIG_GUESS,
|
|
17
|
+
G as GlobalQueue,
|
|
18
|
+
b as CONFIG_VERDICT,
|
|
19
|
+
bO as GRAPH_SIZE_WARN_AT,
|
|
20
|
+
bP as noteFanOut,
|
|
21
|
+
bQ as CONFIG_SUPERSEDES,
|
|
16
22
|
aV as $REFRESH
|
|
17
23
|
} from "./dev-shared.js";
|
|
18
24
|
|
|
@@ -48,7 +54,7 @@ const defaultOptions = {
|
|
|
48
54
|
stacks: false,
|
|
49
55
|
historyLimit: 200,
|
|
50
56
|
hotRuns: { count: 120, windowMs: 1000 },
|
|
51
|
-
wideDeps:
|
|
57
|
+
wideDeps: 200,
|
|
52
58
|
hotTime: { budgetMs: 8, windowMs: 1000 },
|
|
53
59
|
unstableMemos: 4,
|
|
54
60
|
wastedRecompute: { minRuns: 5, ratio: 0.8, budgetMs: 2, windowMs: 1000 },
|
|
@@ -192,13 +198,20 @@ function targetLabel(target) {
|
|
|
192
198
|
}
|
|
193
199
|
function interactionStart(ref) {
|
|
194
200
|
interactionStack.push(currentInteraction);
|
|
201
|
+
// No `ref.event`: the WeakMap answers undefined.
|
|
202
|
+
const joined = eventInteractions.get(ref.event);
|
|
203
|
+
if (joined?.joinable && openInteractions.has(joined)) {
|
|
204
|
+
joined.open = true;
|
|
205
|
+
currentInteraction = joined.event.origin;
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
195
208
|
// One clock read: the frame opens now; the interaction began at `ref.at`
|
|
196
209
|
// when the runtime dated it (the event's own timestamp), else now too.
|
|
197
210
|
const opened = now();
|
|
198
211
|
const origin = { kind: "interaction", name: ref.type, at: ref.at ?? opened };
|
|
199
212
|
if (ref.target) origin.target = targetLabel(ref.target);
|
|
200
213
|
currentInteraction = origin;
|
|
201
|
-
openInteraction(origin, opened);
|
|
214
|
+
openInteraction(origin, opened, ref.event);
|
|
202
215
|
}
|
|
203
216
|
function interactionEnd(returned) {
|
|
204
217
|
const closing = currentInteraction;
|
|
@@ -385,12 +398,6 @@ function formatOrigin(origin) {
|
|
|
385
398
|
}
|
|
386
399
|
}
|
|
387
400
|
/** Record a root change (setSignal / refresh / async landing) on the node. */
|
|
388
|
-
/** Live subscriber count, walked on demand — the core keeps no counter. */
|
|
389
|
-
function countSubscribers(node) {
|
|
390
|
-
let n = 0;
|
|
391
|
-
for (let s = node._subs; s !== null; s = s._nextSub) n++;
|
|
392
|
-
return n;
|
|
393
|
-
}
|
|
394
401
|
/**
|
|
395
402
|
* HUGE_FAN_OUT from the engine's lower `fanOut` threshold (see dev.ts): a
|
|
396
403
|
* committed root invalidation reaching hundreds of subscribers re-runs all
|
|
@@ -399,12 +406,20 @@ function countSubscribers(node) {
|
|
|
399
406
|
* was a post-construction field that forked node shapes). Stops at
|
|
400
407
|
* GRAPH_SIZE_WARN_AT, where the always-on core check takes over, so the two
|
|
401
408
|
* never fire for the same write; the once-per-node dedupe is the core's.
|
|
409
|
+
* HMR plumbing subscribers (the refresh memo of every mounted instance, on
|
|
410
|
+
* a component's registration signal) are not counted toward `fanOut`; the
|
|
411
|
+
* handover is on the full list, which is what the core counts.
|
|
402
412
|
*/
|
|
403
413
|
function checkFanOut(node, kind) {
|
|
404
414
|
const limit = options.fanOut;
|
|
405
415
|
if (typeof limit !== "number") return;
|
|
406
|
-
|
|
407
|
-
|
|
416
|
+
let all = 0;
|
|
417
|
+
let subs = 0;
|
|
418
|
+
for (let s = node._subs; s !== null; s = s._nextSub) {
|
|
419
|
+
all++;
|
|
420
|
+
if (!(s._sub._config & CONFIG_PLUMBING)) subs++;
|
|
421
|
+
}
|
|
422
|
+
if (subs < limit || all >= GRAPH_SIZE_WARN_AT) return;
|
|
408
423
|
noteFanOut(node, subs, kind);
|
|
409
424
|
}
|
|
410
425
|
function stampWrite(node, kind, prev = NO_VALUES, value = NO_VALUES) {
|
|
@@ -483,18 +498,25 @@ function captureDeps(el) {
|
|
|
483
498
|
}
|
|
484
499
|
/**
|
|
485
500
|
* Wide-scope warning — the coarse-read / helper-leak signature: one scope
|
|
486
|
-
* subscribed to
|
|
501
|
+
* subscribed to hundreds of sources re-runs when ANY of them change. Fired from
|
|
487
502
|
* recordRerun for re-runs and directly from recompute for creation runs (a
|
|
488
|
-
* memo can be born too wide). Re-warns only on 50% further growth.
|
|
503
|
+
* memo can be born too wide). Re-warns only on 50% further growth. Counts
|
|
504
|
+
* what the scope's author can act on: HMR plumbing sources are not counted,
|
|
505
|
+
* and a framework scope wide by construction (CONFIG_WIDE) is not judged —
|
|
506
|
+
* the always-on HUGE_FAN_IN still is.
|
|
489
507
|
*/
|
|
490
508
|
function checkDepWidth(el) {
|
|
491
509
|
const limit = options.wideDeps;
|
|
492
|
-
|
|
510
|
+
// Only dev sets CONFIG_WIDE (see constants.ts).
|
|
511
|
+
if (limit === false || el._config & CONFIG_WIDE) return;
|
|
493
512
|
let count = 0;
|
|
494
513
|
const names = [];
|
|
495
514
|
for (let l = el._deps; l !== null; l = l._nextDep) {
|
|
496
|
-
|
|
497
|
-
if (
|
|
515
|
+
// HMR plumbing is a dev-only hop the scope cannot read past: not a source.
|
|
516
|
+
if (!(l._dep._config & CONFIG_PLUMBING)) {
|
|
517
|
+
count++;
|
|
518
|
+
if (names.length < 12) names.push(nodeName(l._dep));
|
|
519
|
+
}
|
|
498
520
|
if (l === el._depsTail) break; // the validated prefix, as captureDeps
|
|
499
521
|
}
|
|
500
522
|
const node = el;
|
|
@@ -650,7 +672,10 @@ function checkHotTime(el, selfMs, causes) {
|
|
|
650
672
|
* names it while it happens, with the input that keeps triggering it. The
|
|
651
673
|
* fix is upstream: an equality boundary on the part of the input the scope
|
|
652
674
|
* depends on, or a narrower read. Plain runs only — a held or overlay run
|
|
653
|
-
* may be replayed and is never blamed as waste.
|
|
675
|
+
* may be replayed and is never blamed as waste. A run that committed
|
|
676
|
+
* `undefined` arrives here as changed (see `recomputeEnd`): its work is a
|
|
677
|
+
* side effect (a projection's draft writes, a write from inside a memo), not
|
|
678
|
+
* its output.
|
|
654
679
|
*/
|
|
655
680
|
function checkWastedRecompute(el, at, phase, changed, selfMs, causes) {
|
|
656
681
|
const cfg = options.wastedRecompute;
|
|
@@ -851,8 +876,10 @@ function shallowEquivalent(a, b) {
|
|
|
851
876
|
for (let i = 0; i < arrA.length; i++) if (arrA[i] !== arrB[i]) return false;
|
|
852
877
|
return true;
|
|
853
878
|
}
|
|
854
|
-
|
|
855
|
-
|
|
879
|
+
// Every own key, symbols included: a symbol-keyed box holding a fresh value
|
|
880
|
+
// each run (web's `dynamic` boxes its in-flight promise) is not equivalent.
|
|
881
|
+
const keys = Reflect.ownKeys(a);
|
|
882
|
+
if (keys.length > UNSTABLE_KEY_CAP || keys.length !== Reflect.ownKeys(b).length) return false;
|
|
856
883
|
for (const key of keys) {
|
|
857
884
|
if (!(key in b) || a[key] !== b[key]) return false;
|
|
858
885
|
}
|
|
@@ -1486,14 +1513,11 @@ function checkWaterfall(el, chain, ms) {
|
|
|
1486
1513
|
const holdStates = new WeakMap();
|
|
1487
1514
|
let activeHold = null;
|
|
1488
1515
|
let holdLog = [];
|
|
1489
|
-
/**
|
|
1490
|
-
*
|
|
1491
|
-
*
|
|
1516
|
+
/** A lane's derivation (L2: a node carrying the lane's value that is not a
|
|
1517
|
+
* written guess — lanes.ts) is a lane pass's result, not a write anyone
|
|
1518
|
+
* made: not an acknowledgement. */
|
|
1492
1519
|
function isCompanion(node) {
|
|
1493
|
-
return (
|
|
1494
|
-
(!!node._x && node._x._parentSource !== undefined) ||
|
|
1495
|
-
(node._config & CONFIG_DERIVED_OVERRIDE) !== 0
|
|
1496
|
-
);
|
|
1520
|
+
return (node._config & (CONFIG_OVERRIDE | CONFIG_GUESS)) === CONFIG_OVERRIDE;
|
|
1497
1521
|
}
|
|
1498
1522
|
const HOLD_CENSUS_CAP = 10_000;
|
|
1499
1523
|
function acknowledge(state, kind, source, reader) {
|
|
@@ -1509,15 +1533,17 @@ function acknowledge(state, kind, source, reader) {
|
|
|
1509
1533
|
);
|
|
1510
1534
|
else if (path !== undefined) existing.reader ??= path;
|
|
1511
1535
|
}
|
|
1536
|
+
/** The declarations made against the hold: the guesses of its lanes
|
|
1537
|
+
* (lanes.ts `_laneGuesses`) and its `affects()` marks (`_marks`). */
|
|
1512
1538
|
function censusRegistrations(t, state) {
|
|
1513
1539
|
const budget = { left: HOLD_CENSUS_CAP };
|
|
1514
|
-
|
|
1515
|
-
|
|
1540
|
+
const guesses = GlobalQueue._laneGuesses?.(t);
|
|
1541
|
+
if (guesses !== undefined)
|
|
1542
|
+
for (const node of guesses)
|
|
1516
1543
|
acknowledge(state, "optimistic", nodeName(node), reachesEffect(node, budget));
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
acknowledge(state, "affects", nodeName(node), reachesEffect(node, budget));
|
|
1544
|
+
if (t._marks !== null)
|
|
1545
|
+
for (const node of t._marks)
|
|
1546
|
+
acknowledge(state, "affects", nodeName(node), reachesEffect(node, budget));
|
|
1521
1547
|
}
|
|
1522
1548
|
/**
|
|
1523
1549
|
* Does anything that paints read `companion` — an effect, through however many
|
|
@@ -1542,7 +1568,10 @@ function reachesEffect(companion, budget) {
|
|
|
1542
1568
|
}
|
|
1543
1569
|
return null;
|
|
1544
1570
|
}
|
|
1545
|
-
/**
|
|
1571
|
+
/** Verdict readers an effect reads, anywhere downstream of the hold's nodes
|
|
1572
|
+
* (L2: a pass that entered an `isPending`/`latest` window is marked
|
|
1573
|
+
* CONFIG_VERDICT; the observe tier notes which windows — verdict.ts
|
|
1574
|
+
* `_devWindows`). The reader acknowledges the node it derives from. */
|
|
1546
1575
|
function censusCompanions(roots, state) {
|
|
1547
1576
|
const visited = new Set();
|
|
1548
1577
|
const stack = [...roots];
|
|
@@ -1551,20 +1580,16 @@ function censusCompanions(roots, state) {
|
|
|
1551
1580
|
const node = stack.pop();
|
|
1552
1581
|
if (visited.has(node)) continue;
|
|
1553
1582
|
visited.add(node);
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
)
|
|
1563
|
-
acknowledge(state, "latest", nodeName(node), reader);
|
|
1564
|
-
for (let child = x._child ?? null; child !== null; child = child._nextChild ?? null)
|
|
1565
|
-
stack.push(child);
|
|
1583
|
+
for (let s = node._subs; s !== null; s = s._nextSub) {
|
|
1584
|
+
const sub = s._sub;
|
|
1585
|
+
if (sub._config & CONFIG_VERDICT) {
|
|
1586
|
+
const windows = sub._devWindows ?? 0;
|
|
1587
|
+
const reader = sub._type ? sub : reachesEffect(sub, budget);
|
|
1588
|
+
if (windows & 1 && reader !== null) acknowledge(state, "isPending", nodeName(node), reader);
|
|
1589
|
+
if (windows & 2 && reader !== null) acknowledge(state, "latest", nodeName(node), reader);
|
|
1590
|
+
}
|
|
1591
|
+
stack.push(sub);
|
|
1566
1592
|
}
|
|
1567
|
-
for (let s = node._subs; s !== null; s = s._nextSub) stack.push(s._sub);
|
|
1568
1593
|
}
|
|
1569
1594
|
}
|
|
1570
1595
|
function holdState(t) {
|
|
@@ -1590,9 +1615,8 @@ function trackHoldStart(t) {
|
|
|
1590
1615
|
if (options.holds === false) return;
|
|
1591
1616
|
const state = holdState(t);
|
|
1592
1617
|
state.flushes++;
|
|
1593
|
-
if (t.
|
|
1594
|
-
for (const
|
|
1595
|
-
if (reporters.size > 0) state.blockers.add(source);
|
|
1618
|
+
if (t._open > 0) state.action = true;
|
|
1619
|
+
for (const source of blockersOf(t)) state.blockers.add(source);
|
|
1596
1620
|
censusRegistrations(t, state);
|
|
1597
1621
|
activeHold = state;
|
|
1598
1622
|
}
|
|
@@ -1628,7 +1652,7 @@ function trackHoldSettled(t) {
|
|
|
1628
1652
|
let interaction;
|
|
1629
1653
|
let origin;
|
|
1630
1654
|
let lastJoinAt = -Infinity;
|
|
1631
|
-
for (const node of t.
|
|
1655
|
+
for (const node of t._nodes) {
|
|
1632
1656
|
if (typeof node._fn === "function" || isCompanion(node)) continue;
|
|
1633
1657
|
const change = node._devChange;
|
|
1634
1658
|
if (change === undefined || change.kind !== "write") continue;
|
|
@@ -1658,7 +1682,7 @@ function trackHoldSettled(t) {
|
|
|
1658
1682
|
return;
|
|
1659
1683
|
}
|
|
1660
1684
|
censusRegistrations(t, state);
|
|
1661
|
-
censusCompanions([...t.
|
|
1685
|
+
censusCompanions([...t._nodes, ...state.blockers], state);
|
|
1662
1686
|
const end = now();
|
|
1663
1687
|
// The hold began no later than its first parked flush; an interaction stamp
|
|
1664
1688
|
// reaches further back (dispatch). A node rewritten mid-hold keeps only its
|
|
@@ -1889,8 +1913,17 @@ function checkOptimisticRevert(el, shown, truth, how) {
|
|
|
1889
1913
|
);
|
|
1890
1914
|
}
|
|
1891
1915
|
// --- Graph growth -----------------------------------------------------------------
|
|
1892
|
-
/**
|
|
1916
|
+
/**
|
|
1917
|
+
* Per route: at its last `visits` compared settles, oldest first, the
|
|
1918
|
+
* graph's size and the part of it that was already alive when that
|
|
1919
|
+
* navigation opened.
|
|
1920
|
+
*/
|
|
1893
1921
|
const routeCounts = new Map();
|
|
1922
|
+
/**
|
|
1923
|
+
* The route the app last arrived on: a navigation that stays on it (only
|
|
1924
|
+
* its params or query change) continues the visit and is not compared.
|
|
1925
|
+
*/
|
|
1926
|
+
let visitRoute;
|
|
1894
1927
|
/**
|
|
1895
1928
|
* The live graph's size — a walk, not a counter: nothing is charged at node
|
|
1896
1929
|
* creation, disposal, write or re-run; the engine asks at a navigation's
|
|
@@ -1905,26 +1938,42 @@ const routeCounts = new Map();
|
|
|
1905
1938
|
* counted unless a subscription still reaches them.
|
|
1906
1939
|
*/
|
|
1907
1940
|
function graphSize() {
|
|
1941
|
+
return walkGraph();
|
|
1942
|
+
}
|
|
1943
|
+
/**
|
|
1944
|
+
* The walk behind `graphSize()`. Given `before` alone, it adds every node it
|
|
1945
|
+
* meets to that set: what is alive as a navigation opens. Given `before` and
|
|
1946
|
+
* `survived`, it also counts into `survived` the nodes it meets that are in
|
|
1947
|
+
* the set, and an edge when both its ends are: what outlived the navigation.
|
|
1948
|
+
* Nothing is retained — the set is weak.
|
|
1949
|
+
*/
|
|
1950
|
+
function walkGraph(before, survived) {
|
|
1908
1951
|
const roots = liveRootOwners();
|
|
1909
|
-
const size = { roots:
|
|
1952
|
+
const size = { roots: 0, owners: 0, computations: 0, signals: 0, edges: 0 };
|
|
1910
1953
|
const seen = new Set();
|
|
1911
1954
|
// A worklist, not recursion: a subscriber chain can be thousands deep.
|
|
1912
1955
|
const work = [];
|
|
1913
1956
|
const stack = [];
|
|
1957
|
+
const count = (node, key) => {
|
|
1958
|
+
size[key]++;
|
|
1959
|
+
if (survived !== undefined) {
|
|
1960
|
+
if (before.has(node)) survived[key]++;
|
|
1961
|
+
} else if (before !== undefined) before.add(node);
|
|
1962
|
+
};
|
|
1914
1963
|
const meet = node => {
|
|
1915
1964
|
if (seen.has(node)) return;
|
|
1916
1965
|
seen.add(node);
|
|
1917
|
-
|
|
1918
|
-
else size.signals++;
|
|
1966
|
+
count(node, typeof node._fn === "function" ? "computations" : "signals");
|
|
1919
1967
|
work.push(node);
|
|
1920
1968
|
};
|
|
1921
1969
|
for (const root of roots) {
|
|
1922
|
-
|
|
1970
|
+
count(root, "roots");
|
|
1971
|
+
count(root, "owners");
|
|
1923
1972
|
// Iterative: a deep tree must not grow the call stack.
|
|
1924
1973
|
let child = root._firstChild;
|
|
1925
1974
|
for (;;) {
|
|
1926
1975
|
while (child !== null) {
|
|
1927
|
-
|
|
1976
|
+
count(child, "owners");
|
|
1928
1977
|
if (typeof child._fn === "function") meet(child);
|
|
1929
1978
|
if (child._firstChild !== null) stack.push(child);
|
|
1930
1979
|
child = child._nextSibling;
|
|
@@ -1943,10 +1992,20 @@ function graphSize() {
|
|
|
1943
1992
|
for (let link = node._deps; link !== null; link = link._nextDep) {
|
|
1944
1993
|
size.edges++;
|
|
1945
1994
|
meet(link._dep);
|
|
1995
|
+
if (survived !== undefined && before.has(node) && before.has(link._dep)) survived.edges++;
|
|
1946
1996
|
}
|
|
1947
1997
|
}
|
|
1948
1998
|
return size;
|
|
1949
1999
|
}
|
|
2000
|
+
/**
|
|
2001
|
+
* openNavigation, while the growth check is on: what is alive as a
|
|
2002
|
+
* navigation opens, for its settle to tell apart from what it created.
|
|
2003
|
+
*/
|
|
2004
|
+
function snapshotGraph() {
|
|
2005
|
+
const before = new WeakSet();
|
|
2006
|
+
walkGraph(before);
|
|
2007
|
+
return before;
|
|
2008
|
+
}
|
|
1950
2009
|
/**
|
|
1951
2010
|
* settleNavigation: the app has finished moving to a route — count the live
|
|
1952
2011
|
* graph. One record per settle for a listener; the growth check compares
|
|
@@ -1955,43 +2014,77 @@ function graphSize() {
|
|
|
1955
2014
|
* `createRoot` in an effect with no dispose, a subscription a component
|
|
1956
2015
|
* registered outside its owner. Nothing here runs per node; the walk is
|
|
1957
2016
|
* the cost, at navigation cadence.
|
|
2017
|
+
*
|
|
2018
|
+
* A count taken at an arrival also holds everything the visit just created,
|
|
2019
|
+
* so a page whose correctly owned content is bigger each visit climbs too.
|
|
2020
|
+
* What tells the two apart is what outlived the navigation: the nodes alive
|
|
2021
|
+
* when it opened (`NavState.before`) that are still alive at its settle. A
|
|
2022
|
+
* visit's own content is not among them; what an earlier visit left behind
|
|
2023
|
+
* is, whatever the content does. A series reports only when both climb.
|
|
1958
2024
|
*/
|
|
1959
2025
|
const GRAPH_SERIES = ["owners", "computations", "signals", "edges"];
|
|
1960
|
-
function trackGraph(
|
|
2026
|
+
function trackGraph(state) {
|
|
2027
|
+
const navigation = state.event;
|
|
2028
|
+
const before = state.before;
|
|
2029
|
+
state.before = undefined;
|
|
1961
2030
|
const cfg = options.graphGrowth;
|
|
1962
2031
|
if (cfg === false && !records.observed("graph")) return;
|
|
1963
|
-
const
|
|
2032
|
+
const survived = before && {
|
|
2033
|
+
roots: 0,
|
|
2034
|
+
owners: 0,
|
|
2035
|
+
computations: 0,
|
|
2036
|
+
signals: 0,
|
|
2037
|
+
edges: 0
|
|
2038
|
+
};
|
|
2039
|
+
const size = walkGraph(before, survived);
|
|
1964
2040
|
const route = navigation.name ?? navigation.to;
|
|
1965
2041
|
const event = { at: now(), ...size, navigation };
|
|
1966
2042
|
if (route !== undefined) event.route = route;
|
|
1967
2043
|
records.emit("graph", event, undefined);
|
|
1968
2044
|
if (cfg === false || route === undefined) return;
|
|
2045
|
+
// A navigation that stays on the route pattern continues the visit: the
|
|
2046
|
+
// page it kept outlived the move by design, so comparing it would read the
|
|
2047
|
+
// page itself as growth. Only arrivals that enter the route are compared —
|
|
2048
|
+
// a leak made during a visit is still alive when the next one starts. A
|
|
2049
|
+
// superseded navigation never showed, so the app is still where it was.
|
|
2050
|
+
const left = visitRoute;
|
|
2051
|
+
if (navigation.outcome !== "superseded") visitRoute = route;
|
|
2052
|
+
// The initial declaration is not a visit (it settles before route content
|
|
2053
|
+
// mounts — the same gate the feedback folds apply as `event.initial !== true`);
|
|
2054
|
+
// `initial` is `true | undefined`, set only by openNavigation, so truthiness is exact.
|
|
2055
|
+
// A navigation opened before the check was on has no snapshot to compare.
|
|
2056
|
+
if (navigation.initial || route === left || survived === undefined) return;
|
|
1969
2057
|
let history = routeCounts.get(route);
|
|
1970
2058
|
if (history === undefined) routeCounts.set(route, (history = []));
|
|
1971
|
-
history.push(size);
|
|
2059
|
+
history.push({ size, survived });
|
|
1972
2060
|
if (history.length > cfg.visits) history.shift();
|
|
1973
2061
|
if (history.length < cfg.visits) return;
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
2062
|
+
const totals = history.map(h => h.size);
|
|
2063
|
+
const kept = history.map(h => h.survived);
|
|
2064
|
+
const climbs = (series, key) => {
|
|
2065
|
+
for (let i = 1; i < series.length; i++) if (series[i][key] <= series[i - 1][key]) return false;
|
|
2066
|
+
return series[series.length - 1][key] >= series[0][key] * cfg.ratio;
|
|
2067
|
+
};
|
|
2068
|
+
// Any series climbing on every visit to the ratio — in the totals and in
|
|
2069
|
+
// what outlived each navigation — is growth; which ones did says what
|
|
2070
|
+
// leaked — computations with owners flat is an ownerless effect per visit,
|
|
2071
|
+
// edges alone is a subscription per visit to something long-lived, owners
|
|
2072
|
+
// is an undisposed root.
|
|
2073
|
+
const grew = GRAPH_SERIES.filter(key => climbs(totals, key) && climbs(kept, key));
|
|
1983
2074
|
if (grew.length === 0) return;
|
|
1984
2075
|
// The count is the whole graph's, so a leak shows at every route's settle;
|
|
1985
2076
|
// the first route to complete its climb reports, naming the others seen.
|
|
1986
2077
|
const routes = [...routeCounts.keys()];
|
|
1987
|
-
const
|
|
2078
|
+
const join = (series, key) => series.map(h => h[key]).join(" → ");
|
|
2079
|
+
const series = grew.map(key => `${key} ${join(kept, key)} (of ${join(totals, key)})`).join("; ");
|
|
1988
2080
|
const shape = grew.includes("owners")
|
|
1989
2081
|
? "a createRoot() in an effect or handler with no dispose, or a Portal or panel mounted per visit"
|
|
1990
2082
|
: grew.includes("computations")
|
|
1991
2083
|
? "an effect or memo created with no owner (a module-level or callback createEffect) that only its sources keep alive"
|
|
1992
2084
|
: "a subscription per visit to something long-lived — a global store or signal read by a computation that outlives the visit";
|
|
1993
2085
|
const message =
|
|
1994
|
-
`[GRAPH_GROWTH] the live graph grew on ${cfg.visits} consecutive visits to ${route}
|
|
2086
|
+
`[GRAPH_GROWTH] the live graph grew on ${cfg.visits} consecutive visits to ${route}, and so did ` +
|
|
2087
|
+
`the part that outlived each navigation there: ${series} ` +
|
|
1995
2088
|
`(${size.roots} roots)${routes.length > 1 ? `, across visits to ${routes.join(", ")}` : ""}. Each ` +
|
|
1996
2089
|
`visit left something behind that the next did not reclaim — the shape says ${shape}. Dispose ` +
|
|
1997
2090
|
`what a visit creates (onCleanup, or return the disposer from onSettled) and own it under the ` +
|
|
@@ -1999,7 +2092,8 @@ function trackGraph(navigation) {
|
|
|
1999
2092
|
const data = {
|
|
2000
2093
|
route,
|
|
2001
2094
|
grew,
|
|
2002
|
-
history:
|
|
2095
|
+
history: totals,
|
|
2096
|
+
survived: kept,
|
|
2003
2097
|
roots: size.roots,
|
|
2004
2098
|
routes
|
|
2005
2099
|
};
|
|
@@ -2019,11 +2113,14 @@ const abandonWindows = new WeakMap();
|
|
|
2019
2113
|
* One source abandoning flight after flight inside a window: every input
|
|
2020
2114
|
* asked again and the earlier answers were thrown away — the
|
|
2021
2115
|
* request-per-keystroke signature `feedback().flights[].abandoned` counts,
|
|
2022
|
-
* as a finding while it happens. Warned once per window per source.
|
|
2116
|
+
* as a finding while it happens. Warned once per window per source. A
|
|
2117
|
+
* framework node superseding its own flights by design (CONFIG_SUPERSEDES)
|
|
2118
|
+
* is not judged; its flights are still recorded.
|
|
2023
2119
|
*/
|
|
2024
2120
|
function checkAbandonedFlights(el, abandoned) {
|
|
2025
2121
|
const cfg = options.abandonedFlights;
|
|
2026
|
-
|
|
2122
|
+
// Only dev sets CONFIG_SUPERSEDES (see constants.ts).
|
|
2123
|
+
if (cfg === false || el._config & CONFIG_SUPERSEDES || excludedNode(el)) return;
|
|
2027
2124
|
const at = Date.now();
|
|
2028
2125
|
let win = abandonWindows.get(el);
|
|
2029
2126
|
if (win === undefined || at - win.start > cfg.windowMs) {
|
|
@@ -2129,6 +2226,14 @@ const openNavs = new Set();
|
|
|
2129
2226
|
let navigationLog = [];
|
|
2130
2227
|
/** Drains completed since enable() — the clock `writeDrain` reads. */
|
|
2131
2228
|
let drainSeq = 0;
|
|
2229
|
+
/**
|
|
2230
|
+
* Navigations settled in a landing, waiting for their graph count: the hook
|
|
2231
|
+
* fires before the landing's commits dispose the frame parked for the page
|
|
2232
|
+
* left, which the walk would still reach through the sources both pages read
|
|
2233
|
+
* (#3989). `land()` runs only inside `flush()`'s drain, so `flushEnd` counts
|
|
2234
|
+
* them, after those commits — the instant a one-drain navigation is counted.
|
|
2235
|
+
*/
|
|
2236
|
+
const landedGraphs = [];
|
|
2132
2237
|
/**
|
|
2133
2238
|
* Copy what the router currently says onto the frame (what writes stamped —
|
|
2134
2239
|
* `formatOrigin` reads it) and the event. Called when the frame opens, when a
|
|
@@ -2163,6 +2268,8 @@ function openNavigation(frame, ref) {
|
|
|
2163
2268
|
held: false,
|
|
2164
2269
|
writeDrain: drainSeq
|
|
2165
2270
|
};
|
|
2271
|
+
// The initial declaration is never compared (see `trackGraph`).
|
|
2272
|
+
if (options.graphGrowth !== false && ref.initial !== true) state.before = snapshotGraph();
|
|
2166
2273
|
syncNavigation(state);
|
|
2167
2274
|
navStates.set(frame, state);
|
|
2168
2275
|
openNavs.add(state);
|
|
@@ -2221,7 +2328,7 @@ function noteNavigationWrite(prior, record) {
|
|
|
2221
2328
|
/** holdStart: `t`'s staged writes are parked — their navigations settle with `t`. */
|
|
2222
2329
|
function markNavigationsHeld(t) {
|
|
2223
2330
|
if (openNavs.size === 0) return;
|
|
2224
|
-
for (const node of t.
|
|
2331
|
+
for (const node of t._nodes) {
|
|
2225
2332
|
const origin = node._devChange?.origin;
|
|
2226
2333
|
if (origin?.kind !== "navigation") continue;
|
|
2227
2334
|
const state = navStates.get(origin);
|
|
@@ -2231,14 +2338,14 @@ function markNavigationsHeld(t) {
|
|
|
2231
2338
|
/** transitionSettled: `t` commits — the navigations whose writes it staged are done. */
|
|
2232
2339
|
function settleNavigations(t, hold) {
|
|
2233
2340
|
if (openNavs.size === 0) return;
|
|
2234
|
-
for (const node of t.
|
|
2341
|
+
for (const node of t._nodes) {
|
|
2235
2342
|
const origin = node._devChange?.origin;
|
|
2236
2343
|
if (origin?.kind !== "navigation") continue;
|
|
2237
2344
|
const state = navStates.get(origin);
|
|
2238
2345
|
if (state === undefined || !openNavs.has(state)) continue;
|
|
2239
2346
|
// A navigation whose frame is still open when its transition commits
|
|
2240
2347
|
// (`until()` inside the frame) settles here too: its writes are through.
|
|
2241
|
-
settleNavigation(state, state.held ? "held" : "committed", hold);
|
|
2348
|
+
settleNavigation(state, state.held ? "held" : "committed", hold, true);
|
|
2242
2349
|
}
|
|
2243
2350
|
}
|
|
2244
2351
|
let openFlush = null;
|
|
@@ -2354,11 +2461,14 @@ function trackFlushEnd() {
|
|
|
2354
2461
|
drainSeq++;
|
|
2355
2462
|
// The swaps this drain committed have rendered.
|
|
2356
2463
|
displayFallbacks();
|
|
2464
|
+
// Landed first: they settled before anything this drain settles below.
|
|
2465
|
+
for (let i = 0; i < landedGraphs.length; i++) trackGraph(landedGraphs[i]);
|
|
2466
|
+
landedGraphs.length = 0;
|
|
2357
2467
|
for (const state of openNavs)
|
|
2358
2468
|
if (state.open === 0 && !state.held) settleNavigation(state, "committed");
|
|
2359
2469
|
for (const state of openInteractions) maybeSettleInteraction(state);
|
|
2360
2470
|
}
|
|
2361
|
-
function settleNavigation(state, outcome, hold) {
|
|
2471
|
+
function settleNavigation(state, outcome, hold, landed) {
|
|
2362
2472
|
if (!openNavs.has(state)) return;
|
|
2363
2473
|
openNavs.delete(state);
|
|
2364
2474
|
syncNavigation(state);
|
|
@@ -2371,16 +2481,19 @@ function settleNavigation(state, outcome, hold) {
|
|
|
2371
2481
|
// route's responsiveness in the feedback tables. Consumers get the record.
|
|
2372
2482
|
if (event.initial !== true) for (const f of folds) f.navigation?.(event);
|
|
2373
2483
|
records.emit("navigation", event, undefined);
|
|
2374
|
-
|
|
2484
|
+
if (landed) landedGraphs.push(state);
|
|
2485
|
+
else trackGraph(state);
|
|
2375
2486
|
// The interaction that performed it may have been waiting only on this.
|
|
2376
2487
|
const under = openInteractionOf(event.interaction);
|
|
2377
2488
|
if (under !== undefined) maybeSettleInteraction(under);
|
|
2378
2489
|
}
|
|
2379
2490
|
const interactionStates = new WeakMap();
|
|
2491
|
+
/** `InteractionRef.event` → the record its frames join (while `joinable`). Weak: the Event is never held. */
|
|
2492
|
+
const eventInteractions = new WeakMap();
|
|
2380
2493
|
/** Opened, not yet settled. */
|
|
2381
2494
|
const openInteractions = new Set();
|
|
2382
2495
|
let interactionLog = [];
|
|
2383
|
-
function openInteraction(frame, opened) {
|
|
2496
|
+
function openInteraction(frame, opened, key) {
|
|
2384
2497
|
const event = {
|
|
2385
2498
|
name: frame.name,
|
|
2386
2499
|
at: frame.at,
|
|
@@ -2406,13 +2519,24 @@ function openInteraction(frame, opened) {
|
|
|
2406
2519
|
heldIn: new Set(),
|
|
2407
2520
|
held: false,
|
|
2408
2521
|
excludedWrites: 0,
|
|
2409
|
-
awaiting:
|
|
2410
|
-
actioned: false
|
|
2522
|
+
awaiting: 0,
|
|
2523
|
+
actioned: false,
|
|
2524
|
+
joinable: key !== undefined
|
|
2411
2525
|
};
|
|
2412
2526
|
interactionStates.set(frame, state);
|
|
2413
2527
|
openInteractions.add(state);
|
|
2414
2528
|
interactionLog.push(event);
|
|
2415
2529
|
if (interactionLog.length > options.historyLimit) interactionLog.shift();
|
|
2530
|
+
if (key !== undefined) {
|
|
2531
|
+
eventInteractions.set(key, state);
|
|
2532
|
+
// A microtask is too early: native dispatch runs microtasks between
|
|
2533
|
+
// listeners. The callback must not close over `key`: a throttled timer
|
|
2534
|
+
// would hold the Event strongly long past its dispatch.
|
|
2535
|
+
setTimeout(() => {
|
|
2536
|
+
state.joinable = false;
|
|
2537
|
+
if (openInteractions.has(state)) maybeSettleInteraction(state, state.settleAt);
|
|
2538
|
+
});
|
|
2539
|
+
}
|
|
2416
2540
|
}
|
|
2417
2541
|
/**
|
|
2418
2542
|
* How long the record waits on a handler's returned promise before settling
|
|
@@ -2438,17 +2562,17 @@ function closeInteraction(frame, returned) {
|
|
|
2438
2562
|
? returned
|
|
2439
2563
|
: null;
|
|
2440
2564
|
if (thenable !== null) {
|
|
2441
|
-
state.awaiting
|
|
2565
|
+
state.awaiting++;
|
|
2442
2566
|
let done = false;
|
|
2443
2567
|
let timer;
|
|
2444
2568
|
const settle = () => {
|
|
2445
2569
|
if (done) return;
|
|
2446
2570
|
done = true;
|
|
2447
2571
|
if (timer !== undefined) clearTimeout(timer);
|
|
2448
|
-
if (!openInteractions.has(state)) return;
|
|
2449
|
-
state.awaiting = false;
|
|
2572
|
+
if (!openInteractions.has(state) || --state.awaiting > 0) return;
|
|
2450
2573
|
const at = now();
|
|
2451
|
-
|
|
2574
|
+
// From the last handler's return: the frames of one event share a record.
|
|
2575
|
+
state.event.continuationMs = at - (state.opened + state.event.handlerMs);
|
|
2452
2576
|
checkUntrackedAsyncHandler(state);
|
|
2453
2577
|
maybeSettleInteraction(state, at);
|
|
2454
2578
|
};
|
|
@@ -2528,7 +2652,7 @@ function noteInteractionNavigation(event) {
|
|
|
2528
2652
|
/** holdStart: `t` parked writes — the interactions that performed them wait for `t`. */
|
|
2529
2653
|
function markInteractionsHeld(t) {
|
|
2530
2654
|
if (openInteractions.size === 0) return;
|
|
2531
|
-
for (const node of t.
|
|
2655
|
+
for (const node of t._nodes) {
|
|
2532
2656
|
const state = openInteractionOf(node._devChange?.origin);
|
|
2533
2657
|
if (state !== undefined) {
|
|
2534
2658
|
state.heldIn.add(t);
|
|
@@ -2551,12 +2675,16 @@ function settleInteractionsHeld(t, hold) {
|
|
|
2551
2675
|
}
|
|
2552
2676
|
function maybeSettleInteraction(state, end = now()) {
|
|
2553
2677
|
const event = state.event;
|
|
2554
|
-
if (state.open || state.awaiting || state.heldIn.size > 0) return;
|
|
2678
|
+
if (state.open || state.awaiting > 0 || state.heldIn.size > 0) return;
|
|
2555
2679
|
// A drain must have committed the last write (the handler's, or a redirect
|
|
2556
2680
|
// hop's after the click's own drain) — the handler returning is not the
|
|
2557
2681
|
// screen having it.
|
|
2558
2682
|
if (event.writes > 0 && drainSeq <= state.writeDrain) return;
|
|
2559
2683
|
for (const nav of event.navigations) if (nav.outcome === undefined) return;
|
|
2684
|
+
if (state.joinable) {
|
|
2685
|
+
state.settleAt = end;
|
|
2686
|
+
return;
|
|
2687
|
+
}
|
|
2560
2688
|
openInteractions.delete(state);
|
|
2561
2689
|
// Every write went to an excluded subject and nothing of the app's ran: the
|
|
2562
2690
|
// click was on the observer's own UI (a devtools panel's button). Not a
|
|
@@ -2653,6 +2781,12 @@ const engineHooks = {
|
|
|
2653
2781
|
frame.prevValue,
|
|
2654
2782
|
el._pendingValue !== NOT_PENDING ? el._pendingValue : el._value
|
|
2655
2783
|
);
|
|
2784
|
+
// The same exemption for memos: a run that committed `undefined` did its
|
|
2785
|
+
// work as a side effect (a projection's draft writes or reconcile, a
|
|
2786
|
+
// write from inside a memo), so core's `undefined === undefined` gate
|
|
2787
|
+
// proves nothing. Reported as changed, so no consumer counts it as waste.
|
|
2788
|
+
if (!changed && (el._pendingValue !== NOT_PENDING ? el._pendingValue : el._value) === undefined)
|
|
2789
|
+
changed = true;
|
|
2656
2790
|
if (frame.causes !== null)
|
|
2657
2791
|
recordRerun(
|
|
2658
2792
|
el,
|
|
@@ -2832,8 +2966,10 @@ function resetTracking() {
|
|
|
2832
2966
|
stagedFallbacks.delete(DRAIN);
|
|
2833
2967
|
trackingGen++;
|
|
2834
2968
|
openNavs.clear();
|
|
2969
|
+
landedGraphs.length = 0;
|
|
2835
2970
|
openInteractions.clear();
|
|
2836
2971
|
routeCounts.clear();
|
|
2972
|
+
visitRoute = undefined;
|
|
2837
2973
|
drainSeq = 0;
|
|
2838
2974
|
originFrames.length = 0;
|
|
2839
2975
|
effectStack.length = 0;
|