@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
package/dist/dev.attribution.js
CHANGED
|
@@ -1,19 +1,25 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
2
|
+
br as OBSERVE,
|
|
3
|
+
bB as setAttributionHooks,
|
|
4
|
+
bC as liveRootOwners,
|
|
5
|
+
N as NOT_PENDING,
|
|
6
|
+
an as emitDiagnostic,
|
|
7
|
+
bD as ownerPath,
|
|
8
|
+
aB as reportDiagnostic,
|
|
9
|
+
bE as isSuppressed,
|
|
10
|
+
bF as blockersOf,
|
|
11
|
+
bG as CONFIG_PLUMBING,
|
|
12
|
+
bH as anyExcluded,
|
|
13
|
+
bI as isExcluded,
|
|
14
|
+
bJ as CONFIG_WIDE,
|
|
15
|
+
a as CONFIG_OVERRIDE,
|
|
16
|
+
C as CONFIG_GUESS,
|
|
17
|
+
G as GlobalQueue,
|
|
18
|
+
b as CONFIG_VERDICT,
|
|
19
|
+
bK as GRAPH_SIZE_WARN_AT,
|
|
20
|
+
bL as noteFanOut,
|
|
21
|
+
bM as CONFIG_SUPERSEDES,
|
|
22
|
+
aT as $REFRESH
|
|
17
23
|
} from "./dev-shared.js";
|
|
18
24
|
|
|
19
25
|
/** A fallback shown for less than this is a flash: feedback for a wait too short to need it. */
|
|
@@ -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
|
|
@@ -1965,7 +1989,10 @@ function trackGraph(navigation) {
|
|
|
1965
1989
|
const event = { at: now(), ...size, navigation };
|
|
1966
1990
|
if (route !== undefined) event.route = route;
|
|
1967
1991
|
records.emit("graph", event, undefined);
|
|
1968
|
-
|
|
1992
|
+
// The initial declaration is not a visit (it settles before route content
|
|
1993
|
+
// mounts — the same gate the feedback folds apply as `event.initial !== true`);
|
|
1994
|
+
// `initial` is `true | undefined`, set only by openNavigation, so truthiness is exact.
|
|
1995
|
+
if (cfg === false || route === undefined || navigation.initial) return;
|
|
1969
1996
|
let history = routeCounts.get(route);
|
|
1970
1997
|
if (history === undefined) routeCounts.set(route, (history = []));
|
|
1971
1998
|
history.push(size);
|
|
@@ -2019,11 +2046,14 @@ const abandonWindows = new WeakMap();
|
|
|
2019
2046
|
* One source abandoning flight after flight inside a window: every input
|
|
2020
2047
|
* asked again and the earlier answers were thrown away — the
|
|
2021
2048
|
* request-per-keystroke signature `feedback().flights[].abandoned` counts,
|
|
2022
|
-
* as a finding while it happens. Warned once per window per source.
|
|
2049
|
+
* as a finding while it happens. Warned once per window per source. A
|
|
2050
|
+
* framework node superseding its own flights by design (CONFIG_SUPERSEDES)
|
|
2051
|
+
* is not judged; its flights are still recorded.
|
|
2023
2052
|
*/
|
|
2024
2053
|
function checkAbandonedFlights(el, abandoned) {
|
|
2025
2054
|
const cfg = options.abandonedFlights;
|
|
2026
|
-
|
|
2055
|
+
// Only dev sets CONFIG_SUPERSEDES (see constants.ts).
|
|
2056
|
+
if (cfg === false || el._config & CONFIG_SUPERSEDES || excludedNode(el)) return;
|
|
2027
2057
|
const at = Date.now();
|
|
2028
2058
|
let win = abandonWindows.get(el);
|
|
2029
2059
|
if (win === undefined || at - win.start > cfg.windowMs) {
|
|
@@ -2221,7 +2251,7 @@ function noteNavigationWrite(prior, record) {
|
|
|
2221
2251
|
/** holdStart: `t`'s staged writes are parked — their navigations settle with `t`. */
|
|
2222
2252
|
function markNavigationsHeld(t) {
|
|
2223
2253
|
if (openNavs.size === 0) return;
|
|
2224
|
-
for (const node of t.
|
|
2254
|
+
for (const node of t._nodes) {
|
|
2225
2255
|
const origin = node._devChange?.origin;
|
|
2226
2256
|
if (origin?.kind !== "navigation") continue;
|
|
2227
2257
|
const state = navStates.get(origin);
|
|
@@ -2231,7 +2261,7 @@ function markNavigationsHeld(t) {
|
|
|
2231
2261
|
/** transitionSettled: `t` commits — the navigations whose writes it staged are done. */
|
|
2232
2262
|
function settleNavigations(t, hold) {
|
|
2233
2263
|
if (openNavs.size === 0) return;
|
|
2234
|
-
for (const node of t.
|
|
2264
|
+
for (const node of t._nodes) {
|
|
2235
2265
|
const origin = node._devChange?.origin;
|
|
2236
2266
|
if (origin?.kind !== "navigation") continue;
|
|
2237
2267
|
const state = navStates.get(origin);
|
|
@@ -2377,10 +2407,12 @@ function settleNavigation(state, outcome, hold) {
|
|
|
2377
2407
|
if (under !== undefined) maybeSettleInteraction(under);
|
|
2378
2408
|
}
|
|
2379
2409
|
const interactionStates = new WeakMap();
|
|
2410
|
+
/** `InteractionRef.event` → the record its frames join (while `joinable`). Weak: the Event is never held. */
|
|
2411
|
+
const eventInteractions = new WeakMap();
|
|
2380
2412
|
/** Opened, not yet settled. */
|
|
2381
2413
|
const openInteractions = new Set();
|
|
2382
2414
|
let interactionLog = [];
|
|
2383
|
-
function openInteraction(frame, opened) {
|
|
2415
|
+
function openInteraction(frame, opened, key) {
|
|
2384
2416
|
const event = {
|
|
2385
2417
|
name: frame.name,
|
|
2386
2418
|
at: frame.at,
|
|
@@ -2406,13 +2438,24 @@ function openInteraction(frame, opened) {
|
|
|
2406
2438
|
heldIn: new Set(),
|
|
2407
2439
|
held: false,
|
|
2408
2440
|
excludedWrites: 0,
|
|
2409
|
-
awaiting:
|
|
2410
|
-
actioned: false
|
|
2441
|
+
awaiting: 0,
|
|
2442
|
+
actioned: false,
|
|
2443
|
+
joinable: key !== undefined
|
|
2411
2444
|
};
|
|
2412
2445
|
interactionStates.set(frame, state);
|
|
2413
2446
|
openInteractions.add(state);
|
|
2414
2447
|
interactionLog.push(event);
|
|
2415
2448
|
if (interactionLog.length > options.historyLimit) interactionLog.shift();
|
|
2449
|
+
if (key !== undefined) {
|
|
2450
|
+
eventInteractions.set(key, state);
|
|
2451
|
+
// A microtask is too early: native dispatch runs microtasks between
|
|
2452
|
+
// listeners. The callback must not close over `key`: a throttled timer
|
|
2453
|
+
// would hold the Event strongly long past its dispatch.
|
|
2454
|
+
setTimeout(() => {
|
|
2455
|
+
state.joinable = false;
|
|
2456
|
+
if (openInteractions.has(state)) maybeSettleInteraction(state, state.settleAt);
|
|
2457
|
+
});
|
|
2458
|
+
}
|
|
2416
2459
|
}
|
|
2417
2460
|
/**
|
|
2418
2461
|
* How long the record waits on a handler's returned promise before settling
|
|
@@ -2438,17 +2481,17 @@ function closeInteraction(frame, returned) {
|
|
|
2438
2481
|
? returned
|
|
2439
2482
|
: null;
|
|
2440
2483
|
if (thenable !== null) {
|
|
2441
|
-
state.awaiting
|
|
2484
|
+
state.awaiting++;
|
|
2442
2485
|
let done = false;
|
|
2443
2486
|
let timer;
|
|
2444
2487
|
const settle = () => {
|
|
2445
2488
|
if (done) return;
|
|
2446
2489
|
done = true;
|
|
2447
2490
|
if (timer !== undefined) clearTimeout(timer);
|
|
2448
|
-
if (!openInteractions.has(state)) return;
|
|
2449
|
-
state.awaiting = false;
|
|
2491
|
+
if (!openInteractions.has(state) || --state.awaiting > 0) return;
|
|
2450
2492
|
const at = now();
|
|
2451
|
-
|
|
2493
|
+
// From the last handler's return: the frames of one event share a record.
|
|
2494
|
+
state.event.continuationMs = at - (state.opened + state.event.handlerMs);
|
|
2452
2495
|
checkUntrackedAsyncHandler(state);
|
|
2453
2496
|
maybeSettleInteraction(state, at);
|
|
2454
2497
|
};
|
|
@@ -2528,7 +2571,7 @@ function noteInteractionNavigation(event) {
|
|
|
2528
2571
|
/** holdStart: `t` parked writes — the interactions that performed them wait for `t`. */
|
|
2529
2572
|
function markInteractionsHeld(t) {
|
|
2530
2573
|
if (openInteractions.size === 0) return;
|
|
2531
|
-
for (const node of t.
|
|
2574
|
+
for (const node of t._nodes) {
|
|
2532
2575
|
const state = openInteractionOf(node._devChange?.origin);
|
|
2533
2576
|
if (state !== undefined) {
|
|
2534
2577
|
state.heldIn.add(t);
|
|
@@ -2551,12 +2594,16 @@ function settleInteractionsHeld(t, hold) {
|
|
|
2551
2594
|
}
|
|
2552
2595
|
function maybeSettleInteraction(state, end = now()) {
|
|
2553
2596
|
const event = state.event;
|
|
2554
|
-
if (state.open || state.awaiting || state.heldIn.size > 0) return;
|
|
2597
|
+
if (state.open || state.awaiting > 0 || state.heldIn.size > 0) return;
|
|
2555
2598
|
// A drain must have committed the last write (the handler's, or a redirect
|
|
2556
2599
|
// hop's after the click's own drain) — the handler returning is not the
|
|
2557
2600
|
// screen having it.
|
|
2558
2601
|
if (event.writes > 0 && drainSeq <= state.writeDrain) return;
|
|
2559
2602
|
for (const nav of event.navigations) if (nav.outcome === undefined) return;
|
|
2603
|
+
if (state.joinable) {
|
|
2604
|
+
state.settleAt = end;
|
|
2605
|
+
return;
|
|
2606
|
+
}
|
|
2560
2607
|
openInteractions.delete(state);
|
|
2561
2608
|
// Every write went to an excluded subject and nothing of the app's ran: the
|
|
2562
2609
|
// click was on the observer's own UI (a devtools panel's button). Not a
|
|
@@ -2653,6 +2700,12 @@ const engineHooks = {
|
|
|
2653
2700
|
frame.prevValue,
|
|
2654
2701
|
el._pendingValue !== NOT_PENDING ? el._pendingValue : el._value
|
|
2655
2702
|
);
|
|
2703
|
+
// The same exemption for memos: a run that committed `undefined` did its
|
|
2704
|
+
// work as a side effect (a projection's draft writes or reconcile, a
|
|
2705
|
+
// write from inside a memo), so core's `undefined === undefined` gate
|
|
2706
|
+
// proves nothing. Reported as changed, so no consumer counts it as waste.
|
|
2707
|
+
if (!changed && (el._pendingValue !== NOT_PENDING ? el._pendingValue : el._value) === undefined)
|
|
2708
|
+
changed = true;
|
|
2656
2709
|
if (frame.causes !== null)
|
|
2657
2710
|
recordRerun(
|
|
2658
2711
|
el,
|