@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
|
@@ -1,24 +1,27 @@
|
|
|
1
1
|
import { setAttributionHooks } from "./attribution-hooks.js";
|
|
2
2
|
|
|
3
|
-
import { NOT_PENDING, CONFIG_PLUMBING,
|
|
3
|
+
import { NOT_PENDING, CONFIG_PLUMBING, CONFIG_OVERRIDE, CONFIG_GUESS, CONFIG_VERDICT } from "./constants.js";
|
|
4
4
|
|
|
5
5
|
import { OBSERVE, liveRootOwners, emitDiagnostic, ownerPath, reportDiagnostic, isSuppressed, anyExcluded, isExcluded, GRAPH_SIZE_WARN_AT, noteFanOut } from "./dev.js";
|
|
6
6
|
|
|
7
|
+
import { blockersOf, GlobalQueue } from "./scheduler.js";
|
|
8
|
+
|
|
7
9
|
/** A fallback shown for less than this is a flash: feedback for a wait too short to need it. */ const FALLBACK_FLASH_MS = 150;
|
|
8
10
|
|
|
9
11
|
/**
|
|
10
|
-
* Under an owner the observer marked as its own (`OBSERVE.exclude
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* exists, before that a flag
|
|
12
|
+
* Under an owner the observer marked as its own (`OBSERVE.exclude`, and not
|
|
13
|
+
* re-admitted by a nearer `OBSERVE.include`), or framework plumbing itself
|
|
14
|
+
* (`CONFIG_PLUMBING` — the HMR memo between a component's root and its
|
|
15
|
+
* body, which is nobody's node): the engine records nothing about the node.
|
|
16
|
+
* Plumbing is a bit read and excludes the node alone, not what it owns; the
|
|
17
|
+
* observer exclusion is cached per node once any exists, before that a flag
|
|
18
|
+
* read.
|
|
16
19
|
*/ function excludedNode(e) {
|
|
17
|
-
if ((e.
|
|
20
|
+
if ((e.u & CONFIG_PLUMBING) !== 0) return true;
|
|
18
21
|
if (!anyExcluded()) return false;
|
|
19
22
|
const n = e;
|
|
20
|
-
if (n
|
|
21
|
-
return n
|
|
23
|
+
if (n.me === undefined) n.me = isExcluded(e);
|
|
24
|
+
return n.me;
|
|
22
25
|
}
|
|
23
26
|
|
|
24
27
|
let attributionActive = false;
|
|
@@ -41,7 +44,7 @@ const defaultOptions = {
|
|
|
41
44
|
count: 120,
|
|
42
45
|
windowMs: 1e3
|
|
43
46
|
},
|
|
44
|
-
wideDeps:
|
|
47
|
+
wideDeps: 200,
|
|
45
48
|
hotTime: {
|
|
46
49
|
budgetMs: 8,
|
|
47
50
|
windowMs: 1e3
|
|
@@ -118,7 +121,7 @@ function nodeName(e) {
|
|
|
118
121
|
}
|
|
119
122
|
|
|
120
123
|
/** The record vocabulary for a computation's kind: effects carry a `_type`, memos do not. */ function nodeKind(e) {
|
|
121
|
-
return e.
|
|
124
|
+
return e.i ? "effect" : "memo";
|
|
122
125
|
}
|
|
123
126
|
|
|
124
127
|
/**
|
|
@@ -223,17 +226,24 @@ const interactionStack = [];
|
|
|
223
226
|
|
|
224
227
|
function interactionStart(e) {
|
|
225
228
|
interactionStack.push(currentInteraction);
|
|
229
|
+
// No `ref.event`: the WeakMap answers undefined.
|
|
230
|
+
const n = eventInteractions.get(e.event);
|
|
231
|
+
if (n?.joinable && openInteractions.has(n)) {
|
|
232
|
+
n.open = true;
|
|
233
|
+
currentInteraction = n.event.origin;
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
226
236
|
// One clock read: the frame opens now; the interaction began at `ref.at`
|
|
227
237
|
// when the runtime dated it (the event's own timestamp), else now too.
|
|
228
|
-
const
|
|
229
|
-
const
|
|
238
|
+
const t = now();
|
|
239
|
+
const o = {
|
|
230
240
|
kind: "interaction",
|
|
231
241
|
name: e.type,
|
|
232
|
-
at: e.at ??
|
|
242
|
+
at: e.at ?? t
|
|
233
243
|
};
|
|
234
|
-
if (e.target)
|
|
235
|
-
currentInteraction =
|
|
236
|
-
openInteraction(t,
|
|
244
|
+
if (e.target) o.target = targetLabel(e.target);
|
|
245
|
+
currentInteraction = o;
|
|
246
|
+
openInteraction(o, t, e.event);
|
|
237
247
|
}
|
|
238
248
|
|
|
239
249
|
function interactionEnd(e) {
|
|
@@ -317,7 +327,7 @@ const effectFrames = new WeakMap;
|
|
|
317
327
|
const n = effectStack[effectStack.length - 1];
|
|
318
328
|
if (n !== undefined) effectFrames.set(e, {
|
|
319
329
|
node: n,
|
|
320
|
-
causes: n.
|
|
330
|
+
causes: n.ge
|
|
321
331
|
});
|
|
322
332
|
}
|
|
323
333
|
|
|
@@ -335,7 +345,7 @@ function pushFrame(e, n, t, o) {
|
|
|
335
345
|
if (s !== undefined) i.interaction = s;
|
|
336
346
|
if (o !== undefined) {
|
|
337
347
|
const e = o;
|
|
338
|
-
if (e.
|
|
348
|
+
if (e.we !== undefined) i.run = e.we;
|
|
339
349
|
// The frame → node map is filled by the first write inside the callback
|
|
340
350
|
// (noteEffectOrigin), not here: most callbacks never write.
|
|
341
351
|
effectStack.push(o);
|
|
@@ -373,15 +383,22 @@ function popFrame(e) {
|
|
|
373
383
|
return;
|
|
374
384
|
}
|
|
375
385
|
}
|
|
376
|
-
|
|
386
|
+
// The initial navigation is the document's: its request is the time origin
|
|
387
|
+
// unless the router says otherwise, and it left nowhere.
|
|
388
|
+
const n = e.initial === true;
|
|
389
|
+
const t = {
|
|
377
390
|
kind: e.kind,
|
|
378
|
-
at: e.at ?? now()
|
|
391
|
+
at: e.at ?? (n ? 0 : now())
|
|
379
392
|
};
|
|
380
|
-
if (e.from !== undefined)
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
393
|
+
if (!n && e.from !== undefined) t.from = e.from;
|
|
394
|
+
// Declared beats ambient: a router that awaited before writing hands back
|
|
395
|
+
// the origin it captured in the request (`undefined` when the request ran
|
|
396
|
+
// under none — still a declaration); what is on the stack now is whatever
|
|
397
|
+
// happened to be running.
|
|
398
|
+
const o = "interaction" in e ? interactionOf(e.interaction) : enclosingInteraction();
|
|
399
|
+
if (o !== undefined) t.interaction = o;
|
|
400
|
+
originFrames.push(t);
|
|
401
|
+
openNavigation(t, e);
|
|
385
402
|
}
|
|
386
403
|
|
|
387
404
|
function originEnd() {
|
|
@@ -415,7 +432,8 @@ function originEnd() {
|
|
|
415
432
|
if (e.to !== undefined && e.to !== n) t.push(e.to);
|
|
416
433
|
const o = navStates.get(e)?.event.redirects;
|
|
417
434
|
if (o !== undefined) t.push(`redirected from ${o.map(e => e.to ?? e.name ?? "?").join(" → ")}`);
|
|
418
|
-
|
|
435
|
+
const i = navStates.get(e)?.event.initial === true;
|
|
436
|
+
return `${i ? "initial " : ""}navigation to ${n}${t.length > 0 ? ` (${t.join(", ")})` : ""}`;
|
|
419
437
|
}
|
|
420
438
|
|
|
421
439
|
default:
|
|
@@ -424,12 +442,6 @@ function originEnd() {
|
|
|
424
442
|
}
|
|
425
443
|
|
|
426
444
|
/** Record a root change (setSignal / refresh / async landing) on the node. */
|
|
427
|
-
/** Live subscriber count, walked on demand — the core keeps no counter. */ function countSubscribers(e) {
|
|
428
|
-
let n = 0;
|
|
429
|
-
for (let t = e.u; t !== null; t = t.Pe) n++;
|
|
430
|
-
return n;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
445
|
/**
|
|
434
446
|
* HUGE_FAN_OUT from the engine's lower `fanOut` threshold (see dev.ts): a
|
|
435
447
|
* committed root invalidation reaching hundreds of subscribers re-runs all
|
|
@@ -438,12 +450,20 @@ function originEnd() {
|
|
|
438
450
|
* was a post-construction field that forked node shapes). Stops at
|
|
439
451
|
* GRAPH_SIZE_WARN_AT, where the always-on core check takes over, so the two
|
|
440
452
|
* never fire for the same write; the once-per-node dedupe is the core's.
|
|
453
|
+
* HMR plumbing subscribers (the refresh memo of every mounted instance, on
|
|
454
|
+
* a component's registration signal) are not counted toward `fanOut`; the
|
|
455
|
+
* handover is on the full list, which is what the core counts.
|
|
441
456
|
*/ function checkFanOut(e, n) {
|
|
442
457
|
const t = options.fanOut;
|
|
443
458
|
if (typeof t !== "number") return;
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
459
|
+
let o = 0;
|
|
460
|
+
let i = 0;
|
|
461
|
+
for (let n = e.t; n !== null; n = n.l) {
|
|
462
|
+
o++;
|
|
463
|
+
if (!(n.o.u & CONFIG_PLUMBING)) i++;
|
|
464
|
+
}
|
|
465
|
+
if (i < t || o >= GRAPH_SIZE_WARN_AT) return;
|
|
466
|
+
noteFanOut(e, i, n);
|
|
447
467
|
}
|
|
448
468
|
|
|
449
469
|
function stampWrite(e, n, t = NO_VALUES, o = NO_VALUES) {
|
|
@@ -463,8 +483,8 @@ function stampWrite(e, n, t = NO_VALUES, o = NO_VALUES) {
|
|
|
463
483
|
i.origin = n === "async" ? asyncOrigin(e) : currentOrigin();
|
|
464
484
|
i.at = now();
|
|
465
485
|
i.stack = captureStack();
|
|
466
|
-
const s = e.
|
|
467
|
-
e.
|
|
486
|
+
const s = e.ve;
|
|
487
|
+
e.ve = i;
|
|
468
488
|
noteNavigationWrite(s, i);
|
|
469
489
|
noteInteractionWrite(i.origin, excludedNode(e));
|
|
470
490
|
if (n === "write") trackEffectWrite(e, i, o);
|
|
@@ -475,7 +495,7 @@ function stampWrite(e, n, t = NO_VALUES, o = NO_VALUES) {
|
|
|
475
495
|
}
|
|
476
496
|
|
|
477
497
|
/** Record a derived change (memo produced a new value) with its causes. */ function stampDerived(e, n) {
|
|
478
|
-
e.
|
|
498
|
+
e.ve = {
|
|
479
499
|
seq: ++changeSeq,
|
|
480
500
|
kind: "derived",
|
|
481
501
|
name: nodeName(e),
|
|
@@ -490,19 +510,19 @@ function stampWrite(e, n, t = NO_VALUES, o = NO_VALUES) {
|
|
|
490
510
|
* run's links. A refresh() stamp on the node itself also counts — that is a
|
|
491
511
|
* self-invalidation, not a dep change.
|
|
492
512
|
*/ function collectCauses(e) {
|
|
493
|
-
const n = e.
|
|
513
|
+
const n = e.ke ?? 0;
|
|
494
514
|
const t = [];
|
|
495
|
-
const o = e.
|
|
515
|
+
const o = e.ve;
|
|
496
516
|
if (o !== undefined && o.seq > n && o.kind === "refresh") t.push(o);
|
|
497
|
-
for (let o = e.
|
|
498
|
-
const e = o.
|
|
517
|
+
for (let o = e._; o !== null; o = o.p) {
|
|
518
|
+
const e = o.I.ve;
|
|
499
519
|
if (e !== undefined && e.seq > n) t.push(e);
|
|
500
520
|
}
|
|
501
521
|
return t;
|
|
502
522
|
}
|
|
503
523
|
|
|
504
524
|
/** Advance the node's seen-cursor to the present. Call after every run. */ function markSeen(e) {
|
|
505
|
-
e.
|
|
525
|
+
e.ke = changeSeq;
|
|
506
526
|
}
|
|
507
527
|
|
|
508
528
|
/**
|
|
@@ -514,32 +534,39 @@ function stampWrite(e, n, t = NO_VALUES, o = NO_VALUES) {
|
|
|
514
534
|
* still owed) and not part of the subscription diff.
|
|
515
535
|
*/ function captureDeps(e) {
|
|
516
536
|
const n = [];
|
|
517
|
-
for (let t = e.
|
|
518
|
-
n.push(t.
|
|
519
|
-
if (t === e.
|
|
537
|
+
for (let t = e._; t !== null; t = t.p) {
|
|
538
|
+
n.push(t.I);
|
|
539
|
+
if (t === e.S) break;
|
|
520
540
|
}
|
|
521
541
|
return n;
|
|
522
542
|
}
|
|
523
543
|
|
|
524
544
|
/**
|
|
525
545
|
* Wide-scope warning — the coarse-read / helper-leak signature: one scope
|
|
526
|
-
* subscribed to
|
|
546
|
+
* subscribed to hundreds of sources re-runs when ANY of them change. Fired from
|
|
527
547
|
* recordRerun for re-runs and directly from recompute for creation runs (a
|
|
528
|
-
* memo can be born too wide). Re-warns only on 50% further growth.
|
|
548
|
+
* memo can be born too wide). Re-warns only on 50% further growth. Counts
|
|
549
|
+
* what the scope's author can act on: HMR plumbing sources are not counted,
|
|
550
|
+
* and a framework scope wide by construction (CONFIG_WIDE) is not judged —
|
|
551
|
+
* the always-on HUGE_FAN_IN still is.
|
|
529
552
|
*/ function checkDepWidth(e) {
|
|
530
553
|
const n = options.wideDeps;
|
|
531
|
-
|
|
554
|
+
// Only dev sets CONFIG_WIDE (see constants.ts).
|
|
555
|
+
if (n === false || false) return;
|
|
532
556
|
let t = 0;
|
|
533
557
|
const o = [];
|
|
534
|
-
for (let n = e.
|
|
535
|
-
|
|
536
|
-
if (
|
|
537
|
-
|
|
558
|
+
for (let n = e._; n !== null; n = n.p) {
|
|
559
|
+
// HMR plumbing is a dev-only hop the scope cannot read past: not a source.
|
|
560
|
+
if (!(n.I.u & CONFIG_PLUMBING)) {
|
|
561
|
+
t++;
|
|
562
|
+
if (o.length < 12) o.push(nodeName(n.I));
|
|
563
|
+
}
|
|
564
|
+
if (n === e.S) break;
|
|
538
565
|
// the validated prefix, as captureDeps
|
|
539
566
|
}
|
|
540
567
|
const i = e;
|
|
541
|
-
if (t < n || t < (i.
|
|
542
|
-
i.
|
|
568
|
+
if (t < n || t < (i.ye ?? 0) * 1.5) return;
|
|
569
|
+
i.ye = t;
|
|
543
570
|
const s = `[WIDE_SCOPE_DEPS] ${nodeKind(e)} "${nodeName(e)}" is subscribed to ${t} sources — ` + `it re-runs when any of them change. Narrow its reads or split it into smaller memos. ` + `Sources: ${o.join(", ")}${t > o.length ? ", …" : ""}`;
|
|
544
571
|
reportDiagnostic(emitDiagnostic({
|
|
545
572
|
code: "WIDE_SCOPE_DEPS",
|
|
@@ -568,14 +595,14 @@ const HOT_FANOUT_FIRST_MILESTONE = 5;
|
|
|
568
595
|
if (t === false) return;
|
|
569
596
|
const o = e;
|
|
570
597
|
const i = Date.now();
|
|
571
|
-
if (o
|
|
572
|
-
o
|
|
573
|
-
o.
|
|
574
|
-
o.
|
|
575
|
-
}
|
|
576
|
-
o.
|
|
577
|
-
if (o.
|
|
578
|
-
o.
|
|
598
|
+
if (o.$e === undefined || i - o.$e > t.windowMs) {
|
|
599
|
+
o.$e = i;
|
|
600
|
+
o.be = 0;
|
|
601
|
+
o.Fe = false;
|
|
602
|
+
}
|
|
603
|
+
o.be = (o.be ?? 0) + 1;
|
|
604
|
+
if (o.Fe || o.be < t.count) return;
|
|
605
|
+
o.Fe = true;
|
|
579
606
|
// Root-cause key: the set of originating writes behind this scope's latest
|
|
580
607
|
// re-run. Scopes hot from the SAME roots share one aggregation window.
|
|
581
608
|
const s = new Set;
|
|
@@ -592,10 +619,10 @@ const HOT_FANOUT_FIRST_MILESTONE = 5;
|
|
|
592
619
|
hotCauses.set(r, a);
|
|
593
620
|
}
|
|
594
621
|
a.scopes++;
|
|
595
|
-
a.runs += o.
|
|
622
|
+
a.runs += o.be;
|
|
596
623
|
if (a.scopes === 1) {
|
|
597
624
|
const s = n.map(e => `"${e.name}" (${e.kind})`).join(", ");
|
|
598
|
-
const r = `[HOT_SCOPE_RERUNS] ${nodeKind(e)} "${nodeName(e)}" re-ran ${o.
|
|
625
|
+
const r = `[HOT_SCOPE_RERUNS] ${nodeKind(e)} "${nodeName(e)}" re-ran ${o.be} times ` + `in ${Math.max(1, i - o.$e)}ms — a hot signal is likely leaking into this ` + `scope. Latest cause: ${s || "(untracked pull)"}`;
|
|
599
626
|
reportDiagnostic(emitDiagnostic({
|
|
600
627
|
code: "HOT_SCOPE_RERUNS",
|
|
601
628
|
kind: "perf",
|
|
@@ -603,7 +630,7 @@ const HOT_FANOUT_FIRST_MILESTONE = 5;
|
|
|
603
630
|
message: r,
|
|
604
631
|
nodeName: nodeName(e),
|
|
605
632
|
data: {
|
|
606
|
-
runs: o.
|
|
633
|
+
runs: o.be,
|
|
607
634
|
windowMs: t.windowMs,
|
|
608
635
|
causes: n.map(e => e.name)
|
|
609
636
|
}
|
|
@@ -641,16 +668,16 @@ const HOT_FANOUT_FIRST_MILESTONE = 5;
|
|
|
641
668
|
if (o === false) return;
|
|
642
669
|
const i = e;
|
|
643
670
|
const s = now();
|
|
644
|
-
if (i.
|
|
645
|
-
i.
|
|
646
|
-
i.
|
|
647
|
-
i.
|
|
648
|
-
}
|
|
649
|
-
i.
|
|
650
|
-
if (i.
|
|
651
|
-
i.
|
|
671
|
+
if (i.Le === undefined || s - i.Le > o.windowMs) {
|
|
672
|
+
i.Le = s;
|
|
673
|
+
i.Ce = 0;
|
|
674
|
+
i.Me = false;
|
|
675
|
+
}
|
|
676
|
+
i.Ce = (i.Ce ?? 0) + n;
|
|
677
|
+
if (i.Me || i.Ce < o.budgetMs) return;
|
|
678
|
+
i.Me = true;
|
|
652
679
|
const r = t.map(e => `"${e.name}" (${e.kind})`).join(", ");
|
|
653
|
-
const a = `[HOT_SCOPE_TIME] ${nodeKind(e)} "${nodeName(e)}" spent ` + `${i.
|
|
680
|
+
const a = `[HOT_SCOPE_TIME] ${nodeKind(e)} "${nodeName(e)}" spent ` + `${i.Ce.toFixed(1)}ms of compute inside one ${o.windowMs}ms window ` + `(budget ${o.budgetMs}ms). Latest cause: ${r || "(untracked pull)"}`;
|
|
654
681
|
reportDiagnostic(emitDiagnostic({
|
|
655
682
|
code: "HOT_SCOPE_TIME",
|
|
656
683
|
kind: "perf",
|
|
@@ -658,7 +685,7 @@ const HOT_FANOUT_FIRST_MILESTONE = 5;
|
|
|
658
685
|
message: a,
|
|
659
686
|
nodeName: nodeName(e),
|
|
660
687
|
data: {
|
|
661
|
-
spentMs: i.
|
|
688
|
+
spentMs: i.Ce,
|
|
662
689
|
budgetMs: o.budgetMs,
|
|
663
690
|
windowMs: o.windowMs,
|
|
664
691
|
causes: t.map(e => e.name)
|
|
@@ -673,31 +700,34 @@ const HOT_FANOUT_FIRST_MILESTONE = 5;
|
|
|
673
700
|
* names it while it happens, with the input that keeps triggering it. The
|
|
674
701
|
* fix is upstream: an equality boundary on the part of the input the scope
|
|
675
702
|
* depends on, or a narrower read. Plain runs only — a held or overlay run
|
|
676
|
-
* may be replayed and is never blamed as waste.
|
|
703
|
+
* may be replayed and is never blamed as waste. A run that committed
|
|
704
|
+
* `undefined` arrives here as changed (see `recomputeEnd`): its work is a
|
|
705
|
+
* side effect (a projection's draft writes, a write from inside a memo), not
|
|
706
|
+
* its output.
|
|
677
707
|
*/ function checkWastedRecompute(e, n, t, o, i, s) {
|
|
678
708
|
const r = options.wastedRecompute;
|
|
679
709
|
if (r === false || t !== "plain") return;
|
|
680
710
|
// This runs on every re-run: fields on the node (one property read each,
|
|
681
711
|
// like hotRuns) and the run's own `at` — no clock read, no map lookup.
|
|
682
712
|
const a = e;
|
|
683
|
-
if (a.
|
|
684
|
-
a.
|
|
685
|
-
a.
|
|
686
|
-
a.
|
|
687
|
-
a.
|
|
688
|
-
a.
|
|
689
|
-
}
|
|
690
|
-
a.
|
|
713
|
+
if (a.He === undefined || n - a.He > r.windowMs) {
|
|
714
|
+
a.He = n;
|
|
715
|
+
a.We = 0;
|
|
716
|
+
a.Ge = 0;
|
|
717
|
+
a.qe = 0;
|
|
718
|
+
a.Be = false;
|
|
719
|
+
}
|
|
720
|
+
a.We = a.We + 1;
|
|
691
721
|
if (!o) {
|
|
692
|
-
a.
|
|
693
|
-
a.
|
|
722
|
+
a.Ge = a.Ge + 1;
|
|
723
|
+
a.qe = a.qe + i;
|
|
694
724
|
}
|
|
695
|
-
if (a.
|
|
696
|
-
a.
|
|
725
|
+
if (a.Be || a.We < r.minRuns || a.Ge / a.We < r.ratio || a.qe < r.budgetMs) return;
|
|
726
|
+
a.Be = true;
|
|
697
727
|
const c = {
|
|
698
|
-
runs: a.
|
|
699
|
-
wasted: a.
|
|
700
|
-
wastedMs: a.
|
|
728
|
+
runs: a.We,
|
|
729
|
+
wasted: a.Ge,
|
|
730
|
+
wastedMs: a.qe
|
|
701
731
|
};
|
|
702
732
|
const d = s.map(e => `"${e.name}" (${e.kind})`).join(", ");
|
|
703
733
|
const f = `[WASTED_RECOMPUTE] ${nodeKind(e)} "${nodeName(e)}" re-ran ${c.runs} times in ` + `${r.windowMs}ms and ${c.wasted} of those produced the same value — ` + `${c.wastedMs.toFixed(1)}ms of compute the equality gate then discarded. Its inputs ` + `change without changing its result: put an equality boundary upstream (a memo over the ` + `part of the input it reads, or an \`equals\` on the source), or read a narrower slice ` + `(the property, not the object). Latest cause: ${d || "(untracked pull)"}`;
|
|
@@ -720,14 +750,14 @@ const HOT_FANOUT_FIRST_MILESTONE = 5;
|
|
|
720
750
|
function recordRerun(e, n, t, o, i, s) {
|
|
721
751
|
const r = n.causes;
|
|
722
752
|
const a = e;
|
|
723
|
-
const c = a.
|
|
753
|
+
const c = a.ge;
|
|
724
754
|
const d = n.interaction;
|
|
725
755
|
if (excludedNode(e)) {
|
|
726
756
|
// The observer's own computation: keep the per-node bookkeeping its
|
|
727
757
|
// effect phase reads (see effectRunStart) and record nothing.
|
|
728
|
-
a.
|
|
729
|
-
a.
|
|
730
|
-
a.
|
|
758
|
+
a.xe = d;
|
|
759
|
+
a.we = undefined;
|
|
760
|
+
a.ge = r;
|
|
731
761
|
return;
|
|
732
762
|
}
|
|
733
763
|
// The facts every run leaves on the node, record or not: the run sequence
|
|
@@ -736,10 +766,10 @@ function recordRerun(e, n, t, o, i, s) {
|
|
|
736
766
|
// cause list of its own, so it takes this run's interaction and causes
|
|
737
767
|
// (see effectRunStart / pushFrame).
|
|
738
768
|
const f = ++runSeq;
|
|
739
|
-
const u = a.
|
|
740
|
-
a.
|
|
741
|
-
a.
|
|
742
|
-
a.
|
|
769
|
+
const u = a.Ke = (a.Ke ?? 0) + 1;
|
|
770
|
+
a.xe = d;
|
|
771
|
+
a.we = f;
|
|
772
|
+
a.ge = r;
|
|
743
773
|
noteInteractionRun(d, t.selfMs, false);
|
|
744
774
|
if (openFlush !== null) noteFlushRun(openFlush, false, d);
|
|
745
775
|
// The checks read the facts, not the record, so they run with or without
|
|
@@ -856,8 +886,10 @@ function shallowEquivalent(e, n) {
|
|
|
856
886
|
for (let e = 0; e < t.length; e++) if (t[e] !== o[e]) return false;
|
|
857
887
|
return true;
|
|
858
888
|
}
|
|
859
|
-
|
|
860
|
-
|
|
889
|
+
// Every own key, symbols included: a symbol-keyed box holding a fresh value
|
|
890
|
+
// each run (web's `dynamic` boxes its in-flight promise) is not equivalent.
|
|
891
|
+
const o = Reflect.ownKeys(e);
|
|
892
|
+
if (o.length > UNSTABLE_KEY_CAP || o.length !== Reflect.ownKeys(n).length) return false;
|
|
861
893
|
for (const t of o) {
|
|
862
894
|
if (!(t in n) || e[t] !== n[t]) return false;
|
|
863
895
|
}
|
|
@@ -878,15 +910,15 @@ function shallowEquivalent(e, n) {
|
|
|
878
910
|
const i = e;
|
|
879
911
|
if (n === t || // paranoia: changed runs should never hit this
|
|
880
912
|
!isPlainShape(n) || !isPlainShape(t) || !shallowEquivalent(n, t)) {
|
|
881
|
-
i.
|
|
882
|
-
i.
|
|
913
|
+
i.Ve = 0;
|
|
914
|
+
i.je = false;
|
|
883
915
|
return;
|
|
884
916
|
}
|
|
885
|
-
i.
|
|
886
|
-
if (i.
|
|
887
|
-
i.
|
|
917
|
+
i.Ve = (i.Ve ?? 0) + 1;
|
|
918
|
+
if (i.je || i.Ve < o) return;
|
|
919
|
+
i.je = true;
|
|
888
920
|
const s = Array.isArray(t) ? "array" : "object";
|
|
889
|
-
const r = `[UNSTABLE_MEMO_OUTPUT] memo "${nodeName(e)}" produced a new-but-equivalent ${s} on ` + `${i.
|
|
921
|
+
const r = `[UNSTABLE_MEMO_OUTPUT] memo "${nodeName(e)}" produced a new-but-equivalent ${s} on ` + `${i.Ve} consecutive runs — its equality gate never closes, so every ` + `subscriber re-runs on every upstream change. Return stable references or pass an ` + `\`equals\` option.`;
|
|
890
922
|
reportDiagnostic(emitDiagnostic({
|
|
891
923
|
code: "UNSTABLE_MEMO_OUTPUT",
|
|
892
924
|
kind: "perf",
|
|
@@ -894,7 +926,7 @@ function shallowEquivalent(e, n) {
|
|
|
894
926
|
message: r,
|
|
895
927
|
nodeName: nodeName(e),
|
|
896
928
|
data: {
|
|
897
|
-
runs: i.
|
|
929
|
+
runs: i.Ve,
|
|
898
930
|
shape: s
|
|
899
931
|
}
|
|
900
932
|
}, e));
|
|
@@ -1033,17 +1065,17 @@ const copyReported = new WeakSet;
|
|
|
1033
1065
|
const s = i !== undefined && i.kind === "effect" ? effectFrames.get(i) : undefined;
|
|
1034
1066
|
const r = s === undefined ? 0 : devId(s.node);
|
|
1035
1067
|
recordNodes.set(n, e);
|
|
1036
|
-
o.
|
|
1037
|
-
if (s !== undefined && t !== undefined && t === s.node.
|
|
1068
|
+
o.Ye = o.Ye === undefined || o.Ye === r ? r : null;
|
|
1069
|
+
if (s !== undefined && t !== undefined && t === s.node.pe) {
|
|
1038
1070
|
copyWrites.add(n);
|
|
1039
|
-
o.
|
|
1040
|
-
o.
|
|
1041
|
-
if (o.
|
|
1042
|
-
} else o.
|
|
1071
|
+
o.ze = o.Xe === r ? (o.ze ?? 0) + 1 : 1;
|
|
1072
|
+
o.Xe = r;
|
|
1073
|
+
if (o.ze >= 2 && o.Ye === r) checkCopyEffect(s.node, e);
|
|
1074
|
+
} else o.ze = 0;
|
|
1043
1075
|
}
|
|
1044
1076
|
|
|
1045
1077
|
/** The effect's source whose current value the compute output is, if any (the prop-to-state port). */ function passthroughSource(e) {
|
|
1046
|
-
for (let n = e.
|
|
1078
|
+
for (let n = e._; n !== null; n = n.p) if (n.I.pe === e.pe) return nodeName(n.I);
|
|
1047
1079
|
return undefined;
|
|
1048
1080
|
}
|
|
1049
1081
|
|
|
@@ -1116,7 +1148,7 @@ function checkCopyEffect(e, n) {
|
|
|
1116
1148
|
c.count++;
|
|
1117
1149
|
const d = copyWrites.has(s);
|
|
1118
1150
|
const f = recordNodes.get(s);
|
|
1119
|
-
const u = f !== undefined && f.
|
|
1151
|
+
const u = f !== undefined && f.Ye === devId(i.node);
|
|
1120
1152
|
// Derivable outright: the value is the compute output and nothing else
|
|
1121
1153
|
// writes the signal. A copy INTO a signal that has other writers is the
|
|
1122
1154
|
// "reset editable state from a source" shape — the tear is real, but a memo
|
|
@@ -1132,7 +1164,7 @@ function checkCopyEffect(e, n) {
|
|
|
1132
1164
|
copyReported.add(f);
|
|
1133
1165
|
}
|
|
1134
1166
|
if (h === "warn") c.warned = true;
|
|
1135
|
-
const p = e.
|
|
1167
|
+
const p = e.i ? "effect" : "memo";
|
|
1136
1168
|
const m = nodeName(i.node);
|
|
1137
1169
|
const g = l ? copyRepair(i.node, s.name) : d ? `The written value is the effect's compute output, but "${s.name}" has other ` + `writers — editable state reset from a source. If the reset is the intent, the tear ` + `is its cost; if "${s.name}" only ever mirrors the source, drop the local copy ` + `and read the source.` : u ? `Nothing else writes "${s.name}" — it is derived state: make it a memo over what ` + `the effect reads and every reader gets it in the same flush.` : `If "${s.name}" is computed from what the effect reads, make it a memo so readers ` + `get it in the same flush; if the write reads something outside the graph (layout, ` + `time), the tear is the cost of measuring.`;
|
|
1138
1170
|
const w = `[EFFECT_RELAY_TEAR] ${p} "${nodeName(e)}" ran twice for one write of ` + `"${r.name}": once in the flush where "${r.name}" changed, and again after ` + `effect "${m}" relayed it by writing "${s.name}" — the first frame showed the ` + `new "${r.name}" with the stale "${s.name}"` + (c.count > 1 ? ` (${c.count} times so far)` : "") + `. ${g}`;
|
|
@@ -1223,7 +1255,7 @@ const listIdentityWarned = new WeakSet;
|
|
|
1223
1255
|
function recordId(e) {
|
|
1224
1256
|
if (e === null || typeof e !== "object") return undefined;
|
|
1225
1257
|
const n = e;
|
|
1226
|
-
return n.id ?? n.key ?? n.
|
|
1258
|
+
return n.id ?? n.key ?? n.Qe ?? undefined;
|
|
1227
1259
|
}
|
|
1228
1260
|
|
|
1229
1261
|
function checkListIdentity(e, n, t, o, i) {
|
|
@@ -1380,7 +1412,7 @@ function trackFlightStart(e, n) {
|
|
|
1380
1412
|
const o = t - n.origin;
|
|
1381
1413
|
for (const n of folds) n.flightLanded?.(e, o);
|
|
1382
1414
|
if (records.observed("flight")) emitFlight(e, n, t, "landed");
|
|
1383
|
-
const i = e.
|
|
1415
|
+
const i = e.ve;
|
|
1384
1416
|
// Only a stamp this landing produced may carry the measurement — a stale
|
|
1385
1417
|
// async record from a previous landing must not be re-labeled.
|
|
1386
1418
|
if (i !== undefined && i.kind === "async" && i.seq > n.startSeq) landedFlights.set(i, {
|
|
@@ -1416,8 +1448,8 @@ function checkWaterfall(e, n, t) {
|
|
|
1416
1448
|
}
|
|
1417
1449
|
if (i < 2) return;
|
|
1418
1450
|
const r = e;
|
|
1419
|
-
if ((r.
|
|
1420
|
-
r.
|
|
1451
|
+
if ((r.Ze ?? 0) >= i) return;
|
|
1452
|
+
r.Ze = i;
|
|
1421
1453
|
const a = [ ...n.slice(n.length - (i - 1)), {
|
|
1422
1454
|
name: nodeName(e),
|
|
1423
1455
|
ms: t
|
|
@@ -1451,10 +1483,10 @@ let activeHold = null;
|
|
|
1451
1483
|
|
|
1452
1484
|
let holdLog = [];
|
|
1453
1485
|
|
|
1454
|
-
/**
|
|
1455
|
-
*
|
|
1456
|
-
*
|
|
1457
|
-
return
|
|
1486
|
+
/** A lane's derivation (L2: a node carrying the lane's value that is not a
|
|
1487
|
+
* written guess — lanes.ts) is a lane pass's result, not a write anyone
|
|
1488
|
+
* made: not an acknowledgement. */ function isCompanion(e) {
|
|
1489
|
+
return (e.u & (CONFIG_OVERRIDE | CONFIG_GUESS)) === CONFIG_OVERRIDE;
|
|
1458
1490
|
}
|
|
1459
1491
|
|
|
1460
1492
|
const HOLD_CENSUS_CAP = 1e4;
|
|
@@ -1475,13 +1507,14 @@ function acknowledge(e, n, t, o) {
|
|
|
1475
1507
|
}); else if (r !== undefined) s.reader ??= r;
|
|
1476
1508
|
}
|
|
1477
1509
|
|
|
1478
|
-
|
|
1510
|
+
/** The declarations made against the hold: the guesses of its lanes
|
|
1511
|
+
* (lanes.ts `_laneGuesses`) and its `affects()` marks (`_marks`). */ function censusRegistrations(e, n) {
|
|
1479
1512
|
const t = {
|
|
1480
1513
|
left: HOLD_CENSUS_CAP
|
|
1481
1514
|
};
|
|
1482
|
-
|
|
1483
|
-
for (const
|
|
1484
|
-
for (const o of e.
|
|
1515
|
+
const o = GlobalQueue.Je?.(e);
|
|
1516
|
+
if (o !== undefined) for (const e of o) acknowledge(n, "optimistic", nodeName(e), reachesEffect(e, t));
|
|
1517
|
+
if (e.T !== null) for (const o of e.T) acknowledge(n, "affects", nodeName(o), reachesEffect(o, t));
|
|
1485
1518
|
}
|
|
1486
1519
|
|
|
1487
1520
|
/**
|
|
@@ -1495,9 +1528,9 @@ function censusRegistrations(e, n) {
|
|
|
1495
1528
|
const o = [ e ];
|
|
1496
1529
|
while (o.length > 0 && n.left-- > 0) {
|
|
1497
1530
|
const e = o.pop();
|
|
1498
|
-
for (let n = e.
|
|
1499
|
-
const e = n.
|
|
1500
|
-
if (e.
|
|
1531
|
+
for (let n = e.t; n !== null; n = n.l) {
|
|
1532
|
+
const e = n.o;
|
|
1533
|
+
if (e.i) return e;
|
|
1501
1534
|
if (!t.has(e)) {
|
|
1502
1535
|
t.add(e);
|
|
1503
1536
|
o.push(e);
|
|
@@ -1507,7 +1540,10 @@ function censusRegistrations(e, n) {
|
|
|
1507
1540
|
return null;
|
|
1508
1541
|
}
|
|
1509
1542
|
|
|
1510
|
-
/**
|
|
1543
|
+
/** Verdict readers an effect reads, anywhere downstream of the hold's nodes
|
|
1544
|
+
* (L2: a pass that entered an `isPending`/`latest` window is marked
|
|
1545
|
+
* CONFIG_VERDICT; the observe tier notes which windows — verdict.ts
|
|
1546
|
+
* `_devWindows`). The reader acknowledges the node it derives from. */ function censusCompanions(e, n) {
|
|
1511
1547
|
const t = new Set;
|
|
1512
1548
|
const o = [ ...e ];
|
|
1513
1549
|
const i = {
|
|
@@ -1517,14 +1553,16 @@ function censusRegistrations(e, n) {
|
|
|
1517
1553
|
const e = o.pop();
|
|
1518
1554
|
if (t.has(e)) continue;
|
|
1519
1555
|
t.add(e);
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1556
|
+
for (let t = e.t; t !== null; t = t.l) {
|
|
1557
|
+
const s = t.o;
|
|
1558
|
+
if (s.u & CONFIG_VERDICT) {
|
|
1559
|
+
const t = s.en ?? 0;
|
|
1560
|
+
const o = s.i ? s : reachesEffect(s, i);
|
|
1561
|
+
if (t & 1 && o !== null) acknowledge(n, "isPending", nodeName(e), o);
|
|
1562
|
+
if (t & 2 && o !== null) acknowledge(n, "latest", nodeName(e), o);
|
|
1563
|
+
}
|
|
1564
|
+
o.push(s);
|
|
1526
1565
|
}
|
|
1527
|
-
for (let n = e.u; n !== null; n = n.Pe) o.push(n.Ae);
|
|
1528
1566
|
}
|
|
1529
1567
|
}
|
|
1530
1568
|
|
|
@@ -1552,8 +1590,8 @@ function trackHoldStart(e) {
|
|
|
1552
1590
|
if (options.holds === false) return;
|
|
1553
1591
|
const n = holdState(e);
|
|
1554
1592
|
n.flushes++;
|
|
1555
|
-
if (e.tt
|
|
1556
|
-
for (const
|
|
1593
|
+
if (e.tt > 0) n.action = true;
|
|
1594
|
+
for (const t of blockersOf(e)) n.blockers.add(t);
|
|
1557
1595
|
censusRegistrations(e, n);
|
|
1558
1596
|
activeHold = n;
|
|
1559
1597
|
}
|
|
@@ -1590,9 +1628,9 @@ function trackHoldSettled(e) {
|
|
|
1590
1628
|
let i;
|
|
1591
1629
|
let s;
|
|
1592
1630
|
let r = -Infinity;
|
|
1593
|
-
for (const n of e.
|
|
1594
|
-
if (typeof n.
|
|
1595
|
-
const e = n.
|
|
1631
|
+
for (const n of e.nn) {
|
|
1632
|
+
if (typeof n.re === "function" || isCompanion(n)) continue;
|
|
1633
|
+
const e = n.ve;
|
|
1596
1634
|
if (e === undefined || e.kind !== "write") continue;
|
|
1597
1635
|
if (o === null) o = n;
|
|
1598
1636
|
const a = {
|
|
@@ -1619,7 +1657,7 @@ function trackHoldSettled(e) {
|
|
|
1619
1657
|
return;
|
|
1620
1658
|
}
|
|
1621
1659
|
censusRegistrations(e, n);
|
|
1622
|
-
censusCompanions([ ...e.
|
|
1660
|
+
censusCompanions([ ...e.nn, ...n.blockers ], n);
|
|
1623
1661
|
const a = now();
|
|
1624
1662
|
// The hold began no later than its first parked flush; an interaction stamp
|
|
1625
1663
|
// reaches further back (dispatch). A node rewritten mid-hold keeps only its
|
|
@@ -1778,8 +1816,10 @@ function checkLongHold(e, n) {
|
|
|
1778
1816
|
// derived override promotes rather than reverts. Same predicate the hold
|
|
1779
1817
|
// census uses to skip them.
|
|
1780
1818
|
if (!options.optimisticReverts || isCompanion(e)) return;
|
|
1781
|
-
|
|
1782
|
-
|
|
1819
|
+
// Method call, as every commit path does: store slot nodes share one
|
|
1820
|
+
// comparator that reads `this._host` (#3687).
|
|
1821
|
+
const i = e.tn;
|
|
1822
|
+
if (i && i.call(e, n, t)) return;
|
|
1783
1823
|
const s = nodeName(e);
|
|
1784
1824
|
// The two values are user data: quoted only under `values: "full"`; the
|
|
1785
1825
|
// other levels say what happened without saying what was shown.
|
|
@@ -1835,33 +1875,33 @@ function checkLongHold(e, n) {
|
|
|
1835
1875
|
const meet = e => {
|
|
1836
1876
|
if (t.has(e)) return;
|
|
1837
1877
|
t.add(e);
|
|
1838
|
-
if (typeof e.
|
|
1878
|
+
if (typeof e.re === "function") n.computations++; else n.signals++;
|
|
1839
1879
|
o.push(e);
|
|
1840
1880
|
};
|
|
1841
1881
|
for (const t of e) {
|
|
1842
1882
|
n.owners++;
|
|
1843
1883
|
// Iterative: a deep tree must not grow the call stack.
|
|
1844
|
-
let e = t.
|
|
1884
|
+
let e = t.sn;
|
|
1845
1885
|
for (;;) {
|
|
1846
1886
|
while (e !== null) {
|
|
1847
1887
|
n.owners++;
|
|
1848
|
-
if (typeof e.
|
|
1849
|
-
if (e.
|
|
1850
|
-
e = e.
|
|
1888
|
+
if (typeof e.re === "function") meet(e);
|
|
1889
|
+
if (e.sn !== null) i.push(e);
|
|
1890
|
+
e = e.rn;
|
|
1851
1891
|
}
|
|
1852
1892
|
const t = i.pop();
|
|
1853
1893
|
if (t === undefined) break;
|
|
1854
|
-
e = t.
|
|
1894
|
+
e = t.sn;
|
|
1855
1895
|
}
|
|
1856
1896
|
}
|
|
1857
1897
|
for (let e = 0; e < o.length; e++) {
|
|
1858
1898
|
const t = o[e];
|
|
1859
1899
|
// Whoever reads this node: owned computations are already known; an
|
|
1860
1900
|
// ownerless one is met only here.
|
|
1861
|
-
for (let e = t.
|
|
1862
|
-
if (typeof t.
|
|
1901
|
+
for (let e = t.t; e !== null; e = e.l) meet(e.o);
|
|
1902
|
+
if (typeof t.re === "function") for (let e = t._; e !== null; e = e.p) {
|
|
1863
1903
|
n.edges++;
|
|
1864
|
-
meet(e.
|
|
1904
|
+
meet(e.I);
|
|
1865
1905
|
}
|
|
1866
1906
|
}
|
|
1867
1907
|
return n;
|
|
@@ -1889,7 +1929,10 @@ function trackGraph(e) {
|
|
|
1889
1929
|
};
|
|
1890
1930
|
if (o !== undefined) i.route = o;
|
|
1891
1931
|
records.emit("graph", i, undefined);
|
|
1892
|
-
|
|
1932
|
+
// The initial declaration is not a visit (it settles before route content
|
|
1933
|
+
// mounts — the same gate the feedback folds apply as `event.initial !== true`);
|
|
1934
|
+
// `initial` is `true | undefined`, set only by openNavigation, so truthiness is exact.
|
|
1935
|
+
if (n === false || o === undefined || e.initial) return;
|
|
1893
1936
|
let s = routeCounts.get(o);
|
|
1894
1937
|
if (s === undefined) routeCounts.set(o, s = []);
|
|
1895
1938
|
s.push(t);
|
|
@@ -1942,10 +1985,13 @@ function trackGraph(e) {
|
|
|
1942
1985
|
* One source abandoning flight after flight inside a window: every input
|
|
1943
1986
|
* asked again and the earlier answers were thrown away — the
|
|
1944
1987
|
* request-per-keystroke signature `feedback().flights[].abandoned` counts,
|
|
1945
|
-
* as a finding while it happens. Warned once per window per source.
|
|
1988
|
+
* as a finding while it happens. Warned once per window per source. A
|
|
1989
|
+
* framework node superseding its own flights by design (CONFIG_SUPERSEDES)
|
|
1990
|
+
* is not judged; its flights are still recorded.
|
|
1946
1991
|
*/ function checkAbandonedFlights(e, n) {
|
|
1947
1992
|
const t = options.abandonedFlights;
|
|
1948
|
-
|
|
1993
|
+
// Only dev sets CONFIG_SUPERSEDES (see constants.ts).
|
|
1994
|
+
if (t === false || false || excludedNode(e)) return;
|
|
1949
1995
|
const o = Date.now();
|
|
1950
1996
|
let i = abandonWindows.get(e);
|
|
1951
1997
|
if (i === undefined || o - i.start > t.windowMs) {
|
|
@@ -2070,6 +2116,7 @@ function openNavigation(e, n) {
|
|
|
2070
2116
|
writes: 0,
|
|
2071
2117
|
origin: e
|
|
2072
2118
|
};
|
|
2119
|
+
if (n.initial === true) t.initial = true;
|
|
2073
2120
|
if (e.from !== undefined) t.from = e.from;
|
|
2074
2121
|
if (e.interaction !== undefined) t.interaction = e.interaction;
|
|
2075
2122
|
const o = {
|
|
@@ -2139,8 +2186,8 @@ function closeNavigation(e) {
|
|
|
2139
2186
|
|
|
2140
2187
|
/** holdStart: `t`'s staged writes are parked — their navigations settle with `t`. */ function markNavigationsHeld(e) {
|
|
2141
2188
|
if (openNavs.size === 0) return;
|
|
2142
|
-
for (const n of e.
|
|
2143
|
-
const e = n.
|
|
2189
|
+
for (const n of e.nn) {
|
|
2190
|
+
const e = n.ve?.origin;
|
|
2144
2191
|
if (e?.kind !== "navigation") continue;
|
|
2145
2192
|
const t = navStates.get(e);
|
|
2146
2193
|
if (t !== undefined) t.held = true;
|
|
@@ -2149,8 +2196,8 @@ function closeNavigation(e) {
|
|
|
2149
2196
|
|
|
2150
2197
|
/** transitionSettled: `t` commits — the navigations whose writes it staged are done. */ function settleNavigations(e, n) {
|
|
2151
2198
|
if (openNavs.size === 0) return;
|
|
2152
|
-
for (const t of e.
|
|
2153
|
-
const e = t.
|
|
2199
|
+
for (const t of e.nn) {
|
|
2200
|
+
const e = t.ve?.origin;
|
|
2154
2201
|
if (e?.kind !== "navigation") continue;
|
|
2155
2202
|
const o = navStates.get(e);
|
|
2156
2203
|
if (o === undefined || !openNavs.has(o)) continue;
|
|
@@ -2295,7 +2342,10 @@ function settleNavigation(e, n, t) {
|
|
|
2295
2342
|
o.settledMs = now() - o.at;
|
|
2296
2343
|
o.outcome = n;
|
|
2297
2344
|
if (t !== undefined) o.hold = t;
|
|
2298
|
-
|
|
2345
|
+
// The initial declaration is not a navigation the person waited on: its
|
|
2346
|
+
// `settledMs` is document start → router built, which would read as the
|
|
2347
|
+
// route's responsiveness in the feedback tables. Consumers get the record.
|
|
2348
|
+
if (o.initial !== true) for (const e of folds) e.navigation?.(o);
|
|
2299
2349
|
records.emit("navigation", o, undefined);
|
|
2300
2350
|
trackGraph(o);
|
|
2301
2351
|
// The interaction that performed it may have been waiting only on this.
|
|
@@ -2305,12 +2355,14 @@ function settleNavigation(e, n, t) {
|
|
|
2305
2355
|
|
|
2306
2356
|
const interactionStates = new WeakMap;
|
|
2307
2357
|
|
|
2358
|
+
/** `InteractionRef.event` → the record its frames join (while `joinable`). Weak: the Event is never held. */ const eventInteractions = new WeakMap;
|
|
2359
|
+
|
|
2308
2360
|
/** Opened, not yet settled. */ const openInteractions = new Set;
|
|
2309
2361
|
|
|
2310
2362
|
let interactionLog = [];
|
|
2311
2363
|
|
|
2312
|
-
function openInteraction(e, n) {
|
|
2313
|
-
const
|
|
2364
|
+
function openInteraction(e, n, t) {
|
|
2365
|
+
const o = {
|
|
2314
2366
|
name: e.name,
|
|
2315
2367
|
at: e.at,
|
|
2316
2368
|
handlerMs: 0,
|
|
@@ -2322,26 +2374,37 @@ function openInteraction(e, n) {
|
|
|
2322
2374
|
navigations: [],
|
|
2323
2375
|
origin: e
|
|
2324
2376
|
};
|
|
2325
|
-
if (e.target !== undefined)
|
|
2377
|
+
if (e.target !== undefined) o.target = e.target;
|
|
2326
2378
|
// The runtime may date the frame from the event's own timestamp (before
|
|
2327
2379
|
// any queued task ran); the gap to here is the input delay the browser's
|
|
2328
2380
|
// INP counts first.
|
|
2329
|
-
if (n >
|
|
2330
|
-
const
|
|
2331
|
-
event:
|
|
2381
|
+
if (n > o.at) o.inputDelayMs = n - o.at;
|
|
2382
|
+
const i = {
|
|
2383
|
+
event: o,
|
|
2332
2384
|
open: true,
|
|
2333
2385
|
opened: n,
|
|
2334
2386
|
writeDrain: drainSeq,
|
|
2335
2387
|
heldIn: new Set,
|
|
2336
2388
|
held: false,
|
|
2337
2389
|
excludedWrites: 0,
|
|
2338
|
-
awaiting:
|
|
2339
|
-
actioned: false
|
|
2390
|
+
awaiting: 0,
|
|
2391
|
+
actioned: false,
|
|
2392
|
+
joinable: t !== undefined
|
|
2340
2393
|
};
|
|
2341
|
-
interactionStates.set(e,
|
|
2342
|
-
openInteractions.add(
|
|
2343
|
-
interactionLog.push(
|
|
2394
|
+
interactionStates.set(e, i);
|
|
2395
|
+
openInteractions.add(i);
|
|
2396
|
+
interactionLog.push(o);
|
|
2344
2397
|
if (interactionLog.length > options.historyLimit) interactionLog.shift();
|
|
2398
|
+
if (t !== undefined) {
|
|
2399
|
+
eventInteractions.set(t, i);
|
|
2400
|
+
// A microtask is too early: native dispatch runs microtasks between
|
|
2401
|
+
// listeners. The callback must not close over `key`: a throttled timer
|
|
2402
|
+
// would hold the Event strongly long past its dispatch.
|
|
2403
|
+
setTimeout(() => {
|
|
2404
|
+
i.joinable = false;
|
|
2405
|
+
if (openInteractions.has(i)) maybeSettleInteraction(i, i.settleAt);
|
|
2406
|
+
});
|
|
2407
|
+
}
|
|
2345
2408
|
}
|
|
2346
2409
|
|
|
2347
2410
|
/**
|
|
@@ -2362,19 +2425,19 @@ function openInteraction(e, n) {
|
|
|
2362
2425
|
// screen could have shown the wait.
|
|
2363
2426
|
const i = n !== null && (typeof n === "object" || typeof n === "function") && typeof n.then === "function" ? n : null;
|
|
2364
2427
|
if (i !== null) {
|
|
2365
|
-
t.awaiting
|
|
2428
|
+
t.awaiting++;
|
|
2366
2429
|
let e = false;
|
|
2367
2430
|
let n;
|
|
2368
2431
|
const settle = () => {
|
|
2369
2432
|
if (e) return;
|
|
2370
2433
|
e = true;
|
|
2371
2434
|
if (n !== undefined) clearTimeout(n);
|
|
2372
|
-
if (!openInteractions.has(t)) return;
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2435
|
+
if (!openInteractions.has(t) || --t.awaiting > 0) return;
|
|
2436
|
+
const o = now();
|
|
2437
|
+
// From the last handler's return: the frames of one event share a record.
|
|
2438
|
+
t.event.continuationMs = o - (t.opened + t.event.handlerMs);
|
|
2376
2439
|
checkUntrackedAsyncHandler(t);
|
|
2377
|
-
maybeSettleInteraction(t,
|
|
2440
|
+
maybeSettleInteraction(t, o);
|
|
2378
2441
|
};
|
|
2379
2442
|
n = setTimeout(settle, ASYNC_HANDLER_CAP_MS);
|
|
2380
2443
|
// A pending cap must not hold a Node process (tests, SSR harnesses) open.
|
|
@@ -2452,8 +2515,8 @@ function openInteraction(e, n) {
|
|
|
2452
2515
|
|
|
2453
2516
|
/** holdStart: `t` parked writes — the interactions that performed them wait for `t`. */ function markInteractionsHeld(e) {
|
|
2454
2517
|
if (openInteractions.size === 0) return;
|
|
2455
|
-
for (const n of e.
|
|
2456
|
-
const t = openInteractionOf(n.
|
|
2518
|
+
for (const n of e.nn) {
|
|
2519
|
+
const t = openInteractionOf(n.ve?.origin);
|
|
2457
2520
|
if (t !== undefined) {
|
|
2458
2521
|
t.heldIn.add(e);
|
|
2459
2522
|
t.held = true;
|
|
@@ -2476,12 +2539,16 @@ function openInteraction(e, n) {
|
|
|
2476
2539
|
|
|
2477
2540
|
function maybeSettleInteraction(e, n = now()) {
|
|
2478
2541
|
const t = e.event;
|
|
2479
|
-
if (e.open || e.awaiting || e.heldIn.size > 0) return;
|
|
2542
|
+
if (e.open || e.awaiting > 0 || e.heldIn.size > 0) return;
|
|
2480
2543
|
// A drain must have committed the last write (the handler's, or a redirect
|
|
2481
2544
|
// hop's after the click's own drain) — the handler returning is not the
|
|
2482
2545
|
// screen having it.
|
|
2483
2546
|
if (t.writes > 0 && drainSeq <= e.writeDrain) return;
|
|
2484
2547
|
for (const e of t.navigations) if (e.outcome === undefined) return;
|
|
2548
|
+
if (e.joinable) {
|
|
2549
|
+
e.settleAt = n;
|
|
2550
|
+
return;
|
|
2551
|
+
}
|
|
2485
2552
|
openInteractions.delete(e);
|
|
2486
2553
|
// Every write went to an excluded subject and nothing of the app's ran: the
|
|
2487
2554
|
// click was on the observer's own UI (a devtools panel's button). Not a
|
|
@@ -2536,7 +2603,7 @@ const engineHooks = {
|
|
|
2536
2603
|
prevDeps: n || !wantsRerun() ? null : captureDeps(e),
|
|
2537
2604
|
// Mirror recompute's own prev-value resolution: an earlier run in the
|
|
2538
2605
|
// same flush may still be holding in _pendingValue.
|
|
2539
|
-
prevValue: e.
|
|
2606
|
+
prevValue: e.Ae !== NOT_PENDING ? e.Ae : e.pe,
|
|
2540
2607
|
// A create run inherits the interaction of whatever is building it: the
|
|
2541
2608
|
// enclosing recompute (a parent's fn creating children) or, at the top
|
|
2542
2609
|
// of the recompute stack, the effect callback / handler frame.
|
|
@@ -2562,8 +2629,8 @@ const engineHooks = {
|
|
|
2562
2629
|
// committed compute output is an unchanged run. `undefined` outputs are
|
|
2563
2630
|
// exempt — a side-effect-only compute's work IS its effect phase, and
|
|
2564
2631
|
// identity of `undefined` proves nothing.
|
|
2565
|
-
if (t && r.causes !== null && e.
|
|
2566
|
-
const n = e.
|
|
2632
|
+
if (t && r.causes !== null && e.i) {
|
|
2633
|
+
const n = e.Ae !== NOT_PENDING ? e.Ae : e.pe;
|
|
2567
2634
|
if (n !== undefined && n === r.prevValue) t = false;
|
|
2568
2635
|
}
|
|
2569
2636
|
// Unstable-output check: memos only, non-create, plain runs with a
|
|
@@ -2571,7 +2638,12 @@ const engineHooks = {
|
|
|
2571
2638
|
// plain-flush memo commits and in `_value` for direct ones. Overlay runs
|
|
2572
2639
|
// are excluded — an optimistic re-derive legitimately produces fresh
|
|
2573
2640
|
// equivalents while the lane settles.
|
|
2574
|
-
if (r.causes !== null && t && !o && !i && !e.
|
|
2641
|
+
if (r.causes !== null && t && !o && !i && !e.i) checkUnstableOutput(e, r.prevValue, e.Ae !== NOT_PENDING ? e.Ae : e.pe);
|
|
2642
|
+
// The same exemption for memos: a run that committed `undefined` did its
|
|
2643
|
+
// work as a side effect (a projection's draft writes or reconcile, a
|
|
2644
|
+
// write from inside a memo), so core's `undefined === undefined` gate
|
|
2645
|
+
// proves nothing. Reported as changed, so no consumer counts it as waste.
|
|
2646
|
+
if (!t && (e.Ae !== NOT_PENDING ? e.Ae : e.pe) === undefined) t = true;
|
|
2575
2647
|
if (r.causes !== null) recordRerun(e, r, {
|
|
2576
2648
|
selfMs: c,
|
|
2577
2649
|
totalMs: a
|
|
@@ -2583,7 +2655,7 @@ const engineHooks = {
|
|
|
2583
2655
|
noteInteractionRun(r.interaction, c, true);
|
|
2584
2656
|
// The first effect callback runs with no re-run record to inherit from;
|
|
2585
2657
|
// hand it the interaction that built the node (see effectRunStart).
|
|
2586
|
-
e.
|
|
2658
|
+
e.xe = r.interaction;
|
|
2587
2659
|
if (openFlush !== null) noteFlushRun(openFlush, true, r.interaction);
|
|
2588
2660
|
if (records.observed("create")) {
|
|
2589
2661
|
const n = {
|
|
@@ -2613,9 +2685,9 @@ const engineHooks = {
|
|
|
2613
2685
|
trackFlightStart(e, n);
|
|
2614
2686
|
},
|
|
2615
2687
|
asyncStart(e) {
|
|
2616
|
-
asyncStartSeq = e.
|
|
2617
|
-
asyncStartTime = e.
|
|
2618
|
-
asyncStartValue = e.
|
|
2688
|
+
asyncStartSeq = e.ve?.seq ?? 0;
|
|
2689
|
+
asyncStartTime = e.Ne;
|
|
2690
|
+
asyncStartValue = e.pe;
|
|
2619
2691
|
},
|
|
2620
2692
|
asyncEnd(e, n, t, o) {
|
|
2621
2693
|
if (o) {
|
|
@@ -2625,7 +2697,7 @@ const engineHooks = {
|
|
|
2625
2697
|
// (or `_time`, for a same-reference commit under `equals: false`), and
|
|
2626
2698
|
// a transition hold parks the value in `_pendingValue`. A landing the
|
|
2627
2699
|
// equality gate swallowed moves none of them and must leave no stamp.
|
|
2628
|
-
const o = e.
|
|
2700
|
+
const o = e.pe !== asyncStartValue || e.Ne !== asyncStartTime || e.Ae === t;
|
|
2629
2701
|
if (o) stampWrite(e, "async", n === undefined ? NO_VALUES : n, t);
|
|
2630
2702
|
// Flight over either way — an equality-swallowed landing still spent
|
|
2631
2703
|
// the wall time (finalizeFlight only chains through a fresh stamp).
|
|
@@ -2635,12 +2707,12 @@ const engineHooks = {
|
|
|
2635
2707
|
// Landed through setSignal: reclassify its "write" stamp as an async
|
|
2636
2708
|
// landing — but only if it actually stamped (the value changed) since
|
|
2637
2709
|
// asyncStart; a no-change landing must leave no fresh stamp behind.
|
|
2638
|
-
const i = e.
|
|
2710
|
+
const i = e.ve;
|
|
2639
2711
|
if (i !== undefined && i.seq > asyncStartSeq && i.kind === "write") stampWrite(e, "async", NO_VALUES, t);
|
|
2640
2712
|
finalizeFlight(e);
|
|
2641
2713
|
},
|
|
2642
2714
|
effectRunStart(e) {
|
|
2643
|
-
pushFrame("effect", nodeName(e), e.
|
|
2715
|
+
pushFrame("effect", nodeName(e), e.xe, e);
|
|
2644
2716
|
// The frame's own `at` doubles as the record's start: set only while
|
|
2645
2717
|
// listened, so a listener arriving mid-callback finds no start and the
|
|
2646
2718
|
// end emits nothing for it — and an unlistened callback pays no clock read.
|