@solidjs/signals 2.0.0-rc.11 → 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 +2056 -3773
- package/dist/dev.attribution.js +159 -91
- package/dist/dev.js +7263 -8384
- 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 +271 -199
- package/dist/observe/core/constants.js +115 -178
- package/dist/observe/core/context.js +3 -3
- package/dist/observe/core/core.js +840 -1428
- package/dist/observe/core/dev.js +41 -18
- 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 -177
- package/dist/prod/core/context.js +3 -3
- package/dist/prod/core/core.js +852 -1448
- 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 +48 -1
- package/dist/types/core/attribution.d.ts +29 -9
- package/dist/types/core/constants.d.ts +125 -171
- package/dist/types/core/core.d.ts +104 -264
- package/dist/types/core/dev.d.ts +60 -7
- 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 +6 -6
- 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 +7 -5
- package/dist/types/reveal.d.ts +19 -0
- package/dist/types/signals.d.ts +53 -92
- 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 -2480
- 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 -2356
- 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 -143
- package/dist/types/store/next/target.d.ts +0 -196
package/dist/dev.attribution.js
CHANGED
|
@@ -1,30 +1,37 @@
|
|
|
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. */
|
|
20
26
|
const FALLBACK_FLASH_MS = 150;
|
|
21
27
|
/**
|
|
22
|
-
* Under an owner the observer marked as its own (`OBSERVE.exclude
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* exists, before that a flag
|
|
28
|
+
* Under an owner the observer marked as its own (`OBSERVE.exclude`, and not
|
|
29
|
+
* re-admitted by a nearer `OBSERVE.include`), or framework plumbing itself
|
|
30
|
+
* (`CONFIG_PLUMBING` — the HMR memo between a component's root and its
|
|
31
|
+
* body, which is nobody's node): the engine records nothing about the node.
|
|
32
|
+
* Plumbing is a bit read and excludes the node alone, not what it owns; the
|
|
33
|
+
* observer exclusion is cached per node once any exists, before that a flag
|
|
34
|
+
* read.
|
|
28
35
|
*/
|
|
29
36
|
function excludedNode(el) {
|
|
30
37
|
if ((el._config & CONFIG_PLUMBING) !== 0) return true;
|
|
@@ -47,7 +54,7 @@ const defaultOptions = {
|
|
|
47
54
|
stacks: false,
|
|
48
55
|
historyLimit: 200,
|
|
49
56
|
hotRuns: { count: 120, windowMs: 1000 },
|
|
50
|
-
wideDeps:
|
|
57
|
+
wideDeps: 200,
|
|
51
58
|
hotTime: { budgetMs: 8, windowMs: 1000 },
|
|
52
59
|
unstableMemos: 4,
|
|
53
60
|
wastedRecompute: { minRuns: 5, ratio: 0.8, budgetMs: 2, windowMs: 1000 },
|
|
@@ -191,13 +198,20 @@ function targetLabel(target) {
|
|
|
191
198
|
}
|
|
192
199
|
function interactionStart(ref) {
|
|
193
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
|
+
}
|
|
194
208
|
// One clock read: the frame opens now; the interaction began at `ref.at`
|
|
195
209
|
// when the runtime dated it (the event's own timestamp), else now too.
|
|
196
210
|
const opened = now();
|
|
197
211
|
const origin = { kind: "interaction", name: ref.type, at: ref.at ?? opened };
|
|
198
212
|
if (ref.target) origin.target = targetLabel(ref.target);
|
|
199
213
|
currentInteraction = origin;
|
|
200
|
-
openInteraction(origin, opened);
|
|
214
|
+
openInteraction(origin, opened, ref.event);
|
|
201
215
|
}
|
|
202
216
|
function interactionEnd(returned) {
|
|
203
217
|
const closing = currentInteraction;
|
|
@@ -333,9 +347,16 @@ function originStart(ref) {
|
|
|
333
347
|
return;
|
|
334
348
|
}
|
|
335
349
|
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
const
|
|
350
|
+
// The initial navigation is the document's: its request is the time origin
|
|
351
|
+
// unless the router says otherwise, and it left nowhere.
|
|
352
|
+
const initial = ref.initial === true;
|
|
353
|
+
const frame = { kind: ref.kind, at: ref.at ?? (initial ? 0 : now()) };
|
|
354
|
+
if (!initial && ref.from !== undefined) frame.from = ref.from;
|
|
355
|
+
// Declared beats ambient: a router that awaited before writing hands back
|
|
356
|
+
// the origin it captured in the request (`undefined` when the request ran
|
|
357
|
+
// under none — still a declaration); what is on the stack now is whatever
|
|
358
|
+
// happened to be running.
|
|
359
|
+
const under = "interaction" in ref ? interactionOf(ref.interaction) : enclosingInteraction();
|
|
339
360
|
if (under !== undefined) frame.interaction = under;
|
|
340
361
|
originFrames.push(frame);
|
|
341
362
|
openNavigation(frame, ref);
|
|
@@ -369,19 +390,14 @@ function formatOrigin(origin) {
|
|
|
369
390
|
notes.push(
|
|
370
391
|
`redirected from ${redirects.map(hop => hop.to ?? hop.name ?? "?").join(" → ")}`
|
|
371
392
|
);
|
|
372
|
-
|
|
393
|
+
const initial = navStates.get(origin)?.event.initial === true;
|
|
394
|
+
return `${initial ? "initial " : ""}navigation to ${name}${notes.length > 0 ? ` (${notes.join(", ")})` : ""}`;
|
|
373
395
|
}
|
|
374
396
|
default:
|
|
375
397
|
return "outside the reactive system";
|
|
376
398
|
}
|
|
377
399
|
}
|
|
378
400
|
/** Record a root change (setSignal / refresh / async landing) on the node. */
|
|
379
|
-
/** Live subscriber count, walked on demand — the core keeps no counter. */
|
|
380
|
-
function countSubscribers(node) {
|
|
381
|
-
let n = 0;
|
|
382
|
-
for (let s = node._subs; s !== null; s = s._nextSub) n++;
|
|
383
|
-
return n;
|
|
384
|
-
}
|
|
385
401
|
/**
|
|
386
402
|
* HUGE_FAN_OUT from the engine's lower `fanOut` threshold (see dev.ts): a
|
|
387
403
|
* committed root invalidation reaching hundreds of subscribers re-runs all
|
|
@@ -390,12 +406,20 @@ function countSubscribers(node) {
|
|
|
390
406
|
* was a post-construction field that forked node shapes). Stops at
|
|
391
407
|
* GRAPH_SIZE_WARN_AT, where the always-on core check takes over, so the two
|
|
392
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.
|
|
393
412
|
*/
|
|
394
413
|
function checkFanOut(node, kind) {
|
|
395
414
|
const limit = options.fanOut;
|
|
396
415
|
if (typeof limit !== "number") return;
|
|
397
|
-
|
|
398
|
-
|
|
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;
|
|
399
423
|
noteFanOut(node, subs, kind);
|
|
400
424
|
}
|
|
401
425
|
function stampWrite(node, kind, prev = NO_VALUES, value = NO_VALUES) {
|
|
@@ -474,18 +498,25 @@ function captureDeps(el) {
|
|
|
474
498
|
}
|
|
475
499
|
/**
|
|
476
500
|
* Wide-scope warning — the coarse-read / helper-leak signature: one scope
|
|
477
|
-
* subscribed to
|
|
501
|
+
* subscribed to hundreds of sources re-runs when ANY of them change. Fired from
|
|
478
502
|
* recordRerun for re-runs and directly from recompute for creation runs (a
|
|
479
|
-
* 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.
|
|
480
507
|
*/
|
|
481
508
|
function checkDepWidth(el) {
|
|
482
509
|
const limit = options.wideDeps;
|
|
483
|
-
|
|
510
|
+
// Only dev sets CONFIG_WIDE (see constants.ts).
|
|
511
|
+
if (limit === false || el._config & CONFIG_WIDE) return;
|
|
484
512
|
let count = 0;
|
|
485
513
|
const names = [];
|
|
486
514
|
for (let l = el._deps; l !== null; l = l._nextDep) {
|
|
487
|
-
|
|
488
|
-
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
|
+
}
|
|
489
520
|
if (l === el._depsTail) break; // the validated prefix, as captureDeps
|
|
490
521
|
}
|
|
491
522
|
const node = el;
|
|
@@ -641,7 +672,10 @@ function checkHotTime(el, selfMs, causes) {
|
|
|
641
672
|
* names it while it happens, with the input that keeps triggering it. The
|
|
642
673
|
* fix is upstream: an equality boundary on the part of the input the scope
|
|
643
674
|
* depends on, or a narrower read. Plain runs only — a held or overlay run
|
|
644
|
-
* 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.
|
|
645
679
|
*/
|
|
646
680
|
function checkWastedRecompute(el, at, phase, changed, selfMs, causes) {
|
|
647
681
|
const cfg = options.wastedRecompute;
|
|
@@ -842,8 +876,10 @@ function shallowEquivalent(a, b) {
|
|
|
842
876
|
for (let i = 0; i < arrA.length; i++) if (arrA[i] !== arrB[i]) return false;
|
|
843
877
|
return true;
|
|
844
878
|
}
|
|
845
|
-
|
|
846
|
-
|
|
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;
|
|
847
883
|
for (const key of keys) {
|
|
848
884
|
if (!(key in b) || a[key] !== b[key]) return false;
|
|
849
885
|
}
|
|
@@ -1477,14 +1513,11 @@ function checkWaterfall(el, chain, ms) {
|
|
|
1477
1513
|
const holdStates = new WeakMap();
|
|
1478
1514
|
let activeHold = null;
|
|
1479
1515
|
let holdLog = [];
|
|
1480
|
-
/**
|
|
1481
|
-
*
|
|
1482
|
-
*
|
|
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. */
|
|
1483
1519
|
function isCompanion(node) {
|
|
1484
|
-
return (
|
|
1485
|
-
(!!node._x && node._x._parentSource !== undefined) ||
|
|
1486
|
-
(node._config & CONFIG_DERIVED_OVERRIDE) !== 0
|
|
1487
|
-
);
|
|
1520
|
+
return (node._config & (CONFIG_OVERRIDE | CONFIG_GUESS)) === CONFIG_OVERRIDE;
|
|
1488
1521
|
}
|
|
1489
1522
|
const HOLD_CENSUS_CAP = 10_000;
|
|
1490
1523
|
function acknowledge(state, kind, source, reader) {
|
|
@@ -1500,15 +1533,17 @@ function acknowledge(state, kind, source, reader) {
|
|
|
1500
1533
|
);
|
|
1501
1534
|
else if (path !== undefined) existing.reader ??= path;
|
|
1502
1535
|
}
|
|
1536
|
+
/** The declarations made against the hold: the guesses of its lanes
|
|
1537
|
+
* (lanes.ts `_laneGuesses`) and its `affects()` marks (`_marks`). */
|
|
1503
1538
|
function censusRegistrations(t, state) {
|
|
1504
1539
|
const budget = { left: HOLD_CENSUS_CAP };
|
|
1505
|
-
|
|
1506
|
-
|
|
1540
|
+
const guesses = GlobalQueue._laneGuesses?.(t);
|
|
1541
|
+
if (guesses !== undefined)
|
|
1542
|
+
for (const node of guesses)
|
|
1507
1543
|
acknowledge(state, "optimistic", nodeName(node), reachesEffect(node, budget));
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
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));
|
|
1512
1547
|
}
|
|
1513
1548
|
/**
|
|
1514
1549
|
* Does anything that paints read `companion` — an effect, through however many
|
|
@@ -1533,7 +1568,10 @@ function reachesEffect(companion, budget) {
|
|
|
1533
1568
|
}
|
|
1534
1569
|
return null;
|
|
1535
1570
|
}
|
|
1536
|
-
/**
|
|
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. */
|
|
1537
1575
|
function censusCompanions(roots, state) {
|
|
1538
1576
|
const visited = new Set();
|
|
1539
1577
|
const stack = [...roots];
|
|
@@ -1542,20 +1580,16 @@ function censusCompanions(roots, state) {
|
|
|
1542
1580
|
const node = stack.pop();
|
|
1543
1581
|
if (visited.has(node)) continue;
|
|
1544
1582
|
visited.add(node);
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
)
|
|
1554
|
-
acknowledge(state, "latest", nodeName(node), reader);
|
|
1555
|
-
for (let child = x._child ?? null; child !== null; child = child._nextChild ?? null)
|
|
1556
|
-
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);
|
|
1557
1592
|
}
|
|
1558
|
-
for (let s = node._subs; s !== null; s = s._nextSub) stack.push(s._sub);
|
|
1559
1593
|
}
|
|
1560
1594
|
}
|
|
1561
1595
|
function holdState(t) {
|
|
@@ -1581,9 +1615,8 @@ function trackHoldStart(t) {
|
|
|
1581
1615
|
if (options.holds === false) return;
|
|
1582
1616
|
const state = holdState(t);
|
|
1583
1617
|
state.flushes++;
|
|
1584
|
-
if (t.
|
|
1585
|
-
for (const
|
|
1586
|
-
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);
|
|
1587
1620
|
censusRegistrations(t, state);
|
|
1588
1621
|
activeHold = state;
|
|
1589
1622
|
}
|
|
@@ -1619,7 +1652,7 @@ function trackHoldSettled(t) {
|
|
|
1619
1652
|
let interaction;
|
|
1620
1653
|
let origin;
|
|
1621
1654
|
let lastJoinAt = -Infinity;
|
|
1622
|
-
for (const node of t.
|
|
1655
|
+
for (const node of t._nodes) {
|
|
1623
1656
|
if (typeof node._fn === "function" || isCompanion(node)) continue;
|
|
1624
1657
|
const change = node._devChange;
|
|
1625
1658
|
if (change === undefined || change.kind !== "write") continue;
|
|
@@ -1649,7 +1682,7 @@ function trackHoldSettled(t) {
|
|
|
1649
1682
|
return;
|
|
1650
1683
|
}
|
|
1651
1684
|
censusRegistrations(t, state);
|
|
1652
|
-
censusCompanions([...t.
|
|
1685
|
+
censusCompanions([...t._nodes, ...state.blockers], state);
|
|
1653
1686
|
const end = now();
|
|
1654
1687
|
// The hold began no later than its first parked flush; an interaction stamp
|
|
1655
1688
|
// reaches further back (dispatch). A node rewritten mid-hold keeps only its
|
|
@@ -1844,8 +1877,10 @@ function checkOptimisticRevert(el, shown, truth, how) {
|
|
|
1844
1877
|
// derived override promotes rather than reverts. Same predicate the hold
|
|
1845
1878
|
// census uses to skip them.
|
|
1846
1879
|
if (!options.optimisticReverts || isCompanion(el)) return;
|
|
1880
|
+
// Method call, as every commit path does: store slot nodes share one
|
|
1881
|
+
// comparator that reads `this._host` (#3687).
|
|
1847
1882
|
const equals = el._equals;
|
|
1848
|
-
if (equals && equals(shown, truth)) return;
|
|
1883
|
+
if (equals && equals.call(el, shown, truth)) return;
|
|
1849
1884
|
const source = nodeName(el);
|
|
1850
1885
|
// The two values are user data: quoted only under `values: "full"`; the
|
|
1851
1886
|
// other levels say what happened without saying what was shown.
|
|
@@ -1954,7 +1989,10 @@ function trackGraph(navigation) {
|
|
|
1954
1989
|
const event = { at: now(), ...size, navigation };
|
|
1955
1990
|
if (route !== undefined) event.route = route;
|
|
1956
1991
|
records.emit("graph", event, undefined);
|
|
1957
|
-
|
|
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;
|
|
1958
1996
|
let history = routeCounts.get(route);
|
|
1959
1997
|
if (history === undefined) routeCounts.set(route, (history = []));
|
|
1960
1998
|
history.push(size);
|
|
@@ -2008,11 +2046,14 @@ const abandonWindows = new WeakMap();
|
|
|
2008
2046
|
* One source abandoning flight after flight inside a window: every input
|
|
2009
2047
|
* asked again and the earlier answers were thrown away — the
|
|
2010
2048
|
* request-per-keystroke signature `feedback().flights[].abandoned` counts,
|
|
2011
|
-
* 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.
|
|
2012
2052
|
*/
|
|
2013
2053
|
function checkAbandonedFlights(el, abandoned) {
|
|
2014
2054
|
const cfg = options.abandonedFlights;
|
|
2015
|
-
|
|
2055
|
+
// Only dev sets CONFIG_SUPERSEDES (see constants.ts).
|
|
2056
|
+
if (cfg === false || el._config & CONFIG_SUPERSEDES || excludedNode(el)) return;
|
|
2016
2057
|
const at = Date.now();
|
|
2017
2058
|
let win = abandonWindows.get(el);
|
|
2018
2059
|
if (win === undefined || at - win.start > cfg.windowMs) {
|
|
@@ -2142,6 +2183,7 @@ function syncNavigation(state) {
|
|
|
2142
2183
|
}
|
|
2143
2184
|
function openNavigation(frame, ref) {
|
|
2144
2185
|
const event = { at: frame.at, writes: 0, origin: frame };
|
|
2186
|
+
if (ref.initial === true) event.initial = true;
|
|
2145
2187
|
if (frame.from !== undefined) event.from = frame.from;
|
|
2146
2188
|
if (frame.interaction !== undefined) event.interaction = frame.interaction;
|
|
2147
2189
|
const state = {
|
|
@@ -2209,7 +2251,7 @@ function noteNavigationWrite(prior, record) {
|
|
|
2209
2251
|
/** holdStart: `t`'s staged writes are parked — their navigations settle with `t`. */
|
|
2210
2252
|
function markNavigationsHeld(t) {
|
|
2211
2253
|
if (openNavs.size === 0) return;
|
|
2212
|
-
for (const node of t.
|
|
2254
|
+
for (const node of t._nodes) {
|
|
2213
2255
|
const origin = node._devChange?.origin;
|
|
2214
2256
|
if (origin?.kind !== "navigation") continue;
|
|
2215
2257
|
const state = navStates.get(origin);
|
|
@@ -2219,7 +2261,7 @@ function markNavigationsHeld(t) {
|
|
|
2219
2261
|
/** transitionSettled: `t` commits — the navigations whose writes it staged are done. */
|
|
2220
2262
|
function settleNavigations(t, hold) {
|
|
2221
2263
|
if (openNavs.size === 0) return;
|
|
2222
|
-
for (const node of t.
|
|
2264
|
+
for (const node of t._nodes) {
|
|
2223
2265
|
const origin = node._devChange?.origin;
|
|
2224
2266
|
if (origin?.kind !== "navigation") continue;
|
|
2225
2267
|
const state = navStates.get(origin);
|
|
@@ -2354,7 +2396,10 @@ function settleNavigation(state, outcome, hold) {
|
|
|
2354
2396
|
event.settledMs = now() - event.at;
|
|
2355
2397
|
event.outcome = outcome;
|
|
2356
2398
|
if (hold !== undefined) event.hold = hold;
|
|
2357
|
-
|
|
2399
|
+
// The initial declaration is not a navigation the person waited on: its
|
|
2400
|
+
// `settledMs` is document start → router built, which would read as the
|
|
2401
|
+
// route's responsiveness in the feedback tables. Consumers get the record.
|
|
2402
|
+
if (event.initial !== true) for (const f of folds) f.navigation?.(event);
|
|
2358
2403
|
records.emit("navigation", event, undefined);
|
|
2359
2404
|
trackGraph(event);
|
|
2360
2405
|
// The interaction that performed it may have been waiting only on this.
|
|
@@ -2362,10 +2407,12 @@ function settleNavigation(state, outcome, hold) {
|
|
|
2362
2407
|
if (under !== undefined) maybeSettleInteraction(under);
|
|
2363
2408
|
}
|
|
2364
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();
|
|
2365
2412
|
/** Opened, not yet settled. */
|
|
2366
2413
|
const openInteractions = new Set();
|
|
2367
2414
|
let interactionLog = [];
|
|
2368
|
-
function openInteraction(frame, opened) {
|
|
2415
|
+
function openInteraction(frame, opened, key) {
|
|
2369
2416
|
const event = {
|
|
2370
2417
|
name: frame.name,
|
|
2371
2418
|
at: frame.at,
|
|
@@ -2391,13 +2438,24 @@ function openInteraction(frame, opened) {
|
|
|
2391
2438
|
heldIn: new Set(),
|
|
2392
2439
|
held: false,
|
|
2393
2440
|
excludedWrites: 0,
|
|
2394
|
-
awaiting:
|
|
2395
|
-
actioned: false
|
|
2441
|
+
awaiting: 0,
|
|
2442
|
+
actioned: false,
|
|
2443
|
+
joinable: key !== undefined
|
|
2396
2444
|
};
|
|
2397
2445
|
interactionStates.set(frame, state);
|
|
2398
2446
|
openInteractions.add(state);
|
|
2399
2447
|
interactionLog.push(event);
|
|
2400
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
|
+
}
|
|
2401
2459
|
}
|
|
2402
2460
|
/**
|
|
2403
2461
|
* How long the record waits on a handler's returned promise before settling
|
|
@@ -2423,17 +2481,17 @@ function closeInteraction(frame, returned) {
|
|
|
2423
2481
|
? returned
|
|
2424
2482
|
: null;
|
|
2425
2483
|
if (thenable !== null) {
|
|
2426
|
-
state.awaiting
|
|
2484
|
+
state.awaiting++;
|
|
2427
2485
|
let done = false;
|
|
2428
2486
|
let timer;
|
|
2429
2487
|
const settle = () => {
|
|
2430
2488
|
if (done) return;
|
|
2431
2489
|
done = true;
|
|
2432
2490
|
if (timer !== undefined) clearTimeout(timer);
|
|
2433
|
-
if (!openInteractions.has(state)) return;
|
|
2434
|
-
state.awaiting = false;
|
|
2491
|
+
if (!openInteractions.has(state) || --state.awaiting > 0) return;
|
|
2435
2492
|
const at = now();
|
|
2436
|
-
|
|
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);
|
|
2437
2495
|
checkUntrackedAsyncHandler(state);
|
|
2438
2496
|
maybeSettleInteraction(state, at);
|
|
2439
2497
|
};
|
|
@@ -2513,7 +2571,7 @@ function noteInteractionNavigation(event) {
|
|
|
2513
2571
|
/** holdStart: `t` parked writes — the interactions that performed them wait for `t`. */
|
|
2514
2572
|
function markInteractionsHeld(t) {
|
|
2515
2573
|
if (openInteractions.size === 0) return;
|
|
2516
|
-
for (const node of t.
|
|
2574
|
+
for (const node of t._nodes) {
|
|
2517
2575
|
const state = openInteractionOf(node._devChange?.origin);
|
|
2518
2576
|
if (state !== undefined) {
|
|
2519
2577
|
state.heldIn.add(t);
|
|
@@ -2536,12 +2594,16 @@ function settleInteractionsHeld(t, hold) {
|
|
|
2536
2594
|
}
|
|
2537
2595
|
function maybeSettleInteraction(state, end = now()) {
|
|
2538
2596
|
const event = state.event;
|
|
2539
|
-
if (state.open || state.awaiting || state.heldIn.size > 0) return;
|
|
2597
|
+
if (state.open || state.awaiting > 0 || state.heldIn.size > 0) return;
|
|
2540
2598
|
// A drain must have committed the last write (the handler's, or a redirect
|
|
2541
2599
|
// hop's after the click's own drain) — the handler returning is not the
|
|
2542
2600
|
// screen having it.
|
|
2543
2601
|
if (event.writes > 0 && drainSeq <= state.writeDrain) return;
|
|
2544
2602
|
for (const nav of event.navigations) if (nav.outcome === undefined) return;
|
|
2603
|
+
if (state.joinable) {
|
|
2604
|
+
state.settleAt = end;
|
|
2605
|
+
return;
|
|
2606
|
+
}
|
|
2545
2607
|
openInteractions.delete(state);
|
|
2546
2608
|
// Every write went to an excluded subject and nothing of the app's ran: the
|
|
2547
2609
|
// click was on the observer's own UI (a devtools panel's button). Not a
|
|
@@ -2638,6 +2700,12 @@ const engineHooks = {
|
|
|
2638
2700
|
frame.prevValue,
|
|
2639
2701
|
el._pendingValue !== NOT_PENDING ? el._pendingValue : el._value
|
|
2640
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;
|
|
2641
2709
|
if (frame.causes !== null)
|
|
2642
2710
|
recordRerun(
|
|
2643
2711
|
el,
|