@solidjs/signals 2.0.0-rc.8 → 2.0.0-rc.9
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 +1399 -285
- package/dist/dev.attribution.js +459 -307
- package/dist/dev.js +1864 -435
- package/dist/observe/affects.js +3 -1
- package/dist/observe/attribution.js +7 -1
- package/dist/observe/boundaries.js +209 -154
- package/dist/observe/core/action.js +18 -8
- package/dist/observe/core/async.js +220 -110
- package/dist/observe/core/attribution-costs.js +66 -0
- package/dist/observe/core/attribution-feedback.js +282 -0
- package/dist/observe/core/attribution-hooks.js +23 -1
- package/dist/observe/core/attribution-queries.js +28 -0
- package/dist/observe/core/attribution.js +262 -485
- package/dist/observe/core/constants.js +34 -1
- package/dist/observe/core/context.js +3 -3
- package/dist/observe/core/core.js +867 -367
- package/dist/observe/core/dev.js +78 -17
- package/dist/observe/core/effect.js +67 -51
- package/dist/observe/core/error-hooks.js +71 -0
- package/dist/observe/core/external.js +4 -4
- package/dist/observe/core/graph.js +37 -37
- package/dist/observe/core/heap.js +45 -45
- package/dist/observe/core/invariants.js +2 -0
- package/dist/observe/core/lanes.js +86 -49
- package/dist/observe/core/optimistic.js +242 -95
- package/dist/observe/core/owner.js +98 -84
- package/dist/observe/core/scheduler.js +543 -305
- package/dist/observe/core/verdict.js +247 -129
- package/dist/observe/index.js +7 -3
- package/dist/observe/map.js +126 -124
- package/dist/observe/signals.js +33 -17
- package/dist/observe/store/index.js +2 -0
- package/dist/observe/store/next/optimistic.js +141 -132
- package/dist/observe/store/next/projection.js +34 -21
- package/dist/observe/store/next/reconcile.js +58 -56
- package/dist/observe/store/next/store.js +260 -146
- package/dist/observe/store/store.js +5 -3
- package/dist/observe/store/utils.js +948 -135
- package/dist/prod/attribution.js +26 -17
- package/dist/prod/boundaries.js +128 -76
- package/dist/prod/core/action.js +16 -8
- package/dist/prod/core/async.js +251 -143
- package/dist/prod/core/constants.js +34 -1
- package/dist/prod/core/context.js +3 -3
- package/dist/prod/core/core.js +843 -347
- package/dist/prod/core/dev.js +17 -1
- package/dist/prod/core/effect.js +48 -34
- package/dist/prod/core/error-hooks.js +71 -0
- package/dist/prod/core/external.js +4 -4
- package/dist/prod/core/graph.js +37 -37
- package/dist/prod/core/heap.js +45 -45
- package/dist/prod/core/lanes.js +90 -53
- package/dist/prod/core/optimistic.js +247 -100
- package/dist/prod/core/owner.js +57 -45
- package/dist/prod/core/scheduler.js +543 -305
- package/dist/prod/core/verdict.js +245 -127
- package/dist/prod/index.js +7 -3
- package/dist/prod/map.js +112 -112
- package/dist/prod/signals.js +28 -12
- package/dist/prod/store/next/optimistic.js +135 -128
- package/dist/prod/store/next/projection.js +31 -20
- package/dist/prod/store/next/store.js +325 -252
- package/dist/prod/store/store.js +2 -2
- package/dist/prod/store/utils.js +946 -135
- package/dist/types/attribution.d.ts +7 -2
- package/dist/types/attribution.prod.d.ts +10 -1
- package/dist/types/boundaries.d.ts +10 -1
- package/dist/types/core/action.d.ts +12 -5
- package/dist/types/core/attribution-costs.d.ts +35 -0
- package/dist/types/core/attribution-feedback.d.ts +133 -0
- package/dist/types/core/attribution-hooks.d.ts +28 -3
- package/dist/types/core/attribution-queries.d.ts +10 -0
- package/dist/types/core/attribution.d.ts +51 -172
- package/dist/types/core/constants.d.ts +33 -0
- package/dist/types/core/core.d.ts +186 -5
- package/dist/types/core/dev.d.ts +129 -9
- package/dist/types/core/error-hooks.d.ts +71 -0
- package/dist/types/core/index.d.ts +3 -1
- package/dist/types/core/invariants.d.ts +4 -0
- package/dist/types/core/lanes.d.ts +31 -4
- package/dist/types/core/scheduler.d.ts +95 -2
- package/dist/types/core/types.d.ts +3 -0
- package/dist/types/index.d.ts +2 -2
- package/dist/types/signals.d.ts +8 -0
- package/dist/types/store/index.d.ts +2 -1
- package/dist/types/store/next/optimistic.d.ts +1 -1
- package/dist/types/store/next/store.d.ts +6 -5
- package/dist/types/store/next/target.d.ts +1 -1
- package/dist/types/store/utils.d.ts +177 -6
- package/package.json +1 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { setAttributionHooks } from "./attribution-hooks.js";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { NOT_PENDING, CONFIG_DERIVED_OVERRIDE } from "./constants.js";
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { reportDiagnostic, emitDiagnostic, isSuppressed, recordSubject, anyExcluded, isExcluded, GRAPH_SIZE_WARN_AT, ownerPath } from "./dev.js";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Under an owner the observer marked as its own (`OBSERVE.exclude`): the
|
|
@@ -68,50 +68,22 @@ function clearListeners() {
|
|
|
68
68
|
for (const e in recordListeners) recordListeners[e].clear();
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
const now = typeof performance !== "undefined" ? () => performance.now() : () => Date.now();
|
|
71
|
+
/** @internal The engine's clock: `performance.now()` where it exists. */ const now = typeof performance !== "undefined" ? () => performance.now() : () => Date.now();
|
|
72
72
|
|
|
73
73
|
const frames = [];
|
|
74
74
|
|
|
75
|
-
const
|
|
75
|
+
const folds = [];
|
|
76
76
|
|
|
77
|
-
|
|
77
|
+
/** @internal */ function registerFold(e) {
|
|
78
|
+
folds.push(e);
|
|
79
|
+
}
|
|
78
80
|
|
|
79
|
-
function rootsOf(e, n) {
|
|
81
|
+
/** @internal Root cause names of a cause chain — the writes/landings/refreshes the chain bottoms out in. */ function rootsOf(e, n) {
|
|
80
82
|
for (const t of e) {
|
|
81
83
|
if (t.kind === "derived" && t.causes && t.causes.length > 0) rootsOf(t.causes, n); else n.add(t.name);
|
|
82
84
|
}
|
|
83
85
|
}
|
|
84
86
|
|
|
85
|
-
function recordCosts(e) {
|
|
86
|
-
let n = scopeCosts.get(e.node);
|
|
87
|
-
if (n === undefined) {
|
|
88
|
-
n = {
|
|
89
|
-
name: e.nodeName,
|
|
90
|
-
kind: e.nodeKind,
|
|
91
|
-
runs: 0,
|
|
92
|
-
selfMs: 0,
|
|
93
|
-
wastedMs: 0,
|
|
94
|
-
overlayMs: 0
|
|
95
|
-
};
|
|
96
|
-
scopeCosts.set(e.node, n);
|
|
97
|
-
}
|
|
98
|
-
n.runs++;
|
|
99
|
-
n.selfMs += e.selfMs;
|
|
100
|
-
if (e.phase !== "plain") n.overlayMs += e.selfMs; else if (!e.changed && !e.held) n.wastedMs += e.selfMs;
|
|
101
|
-
const t = new Set;
|
|
102
|
-
rootsOf(e.causes, t);
|
|
103
|
-
for (const n of t) {
|
|
104
|
-
let t = writeCosts.get(n);
|
|
105
|
-
if (t === undefined) writeCosts.set(n, t = {
|
|
106
|
-
name: n,
|
|
107
|
-
runs: 0,
|
|
108
|
-
downstreamMs: 0
|
|
109
|
-
});
|
|
110
|
-
t.runs++;
|
|
111
|
-
t.downstreamMs += e.selfMs;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
87
|
function nodeName(e) {
|
|
116
88
|
return e._name ?? "anonymous";
|
|
117
89
|
}
|
|
@@ -215,6 +187,40 @@ function currentOrigin() {
|
|
|
215
187
|
return currentInteraction ?? EXTERNAL_ORIGIN;
|
|
216
188
|
}
|
|
217
189
|
|
|
190
|
+
/** The root origin a cause list traces back to — the stamp of the nearest root write, derived links walked. */ function originIn(e) {
|
|
191
|
+
for (const n of e) {
|
|
192
|
+
const e = n.kind === "derived" ? n.causes !== undefined ? originIn(n.causes) : undefined : n.origin;
|
|
193
|
+
if (e !== undefined && e.kind !== "external") return e;
|
|
194
|
+
}
|
|
195
|
+
return undefined;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* The `currentOrigin` hook: what a runtime recording its own fact right now
|
|
200
|
+
* (a server-function call) should stamp it with. Inside a recompute the
|
|
201
|
+
* fact belongs to the change that caused the run — a `createAsync` calling
|
|
202
|
+
* the server on a navigation's write is the navigation's, and through it the
|
|
203
|
+
* click's — walked down past create runs the way `trackFlightStart` does,
|
|
204
|
+
* since a node born inside a parent's run inherits the parent's causality.
|
|
205
|
+
* Outside one — or when the causes were themselves external (a memo a
|
|
206
|
+
* handler pulls, stale from a timer's write) — it is the write's answer
|
|
207
|
+
* (`currentOrigin`), minus the external sentinel: "none known" is
|
|
208
|
+
* `undefined` on a record, as `HoldEvent.origin` has it. The objects
|
|
209
|
+
* returned are the engine's own frames, so the caller's record joins
|
|
210
|
+
* `InteractionEvent.origin` / `NavigationEvent.origin` by identity.
|
|
211
|
+
*/ function ambientOrigin() {
|
|
212
|
+
for (let e = frames.length - 1; e >= 0; e--) {
|
|
213
|
+
const n = frames[e].causes;
|
|
214
|
+
if (n !== null) {
|
|
215
|
+
const e = originIn(n);
|
|
216
|
+
if (e !== undefined) return e;
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
const e = currentOrigin();
|
|
221
|
+
return e === EXTERNAL_ORIGIN ? undefined : e;
|
|
222
|
+
}
|
|
223
|
+
|
|
218
224
|
const effectFrames = new WeakMap;
|
|
219
225
|
|
|
220
226
|
/** The interaction the innermost open frame runs under, else the ambient one. */ function enclosingInteraction() {
|
|
@@ -234,7 +240,7 @@ function pushFrame(e, n, t, i) {
|
|
|
234
240
|
if (e.we !== undefined) o.run = e.we;
|
|
235
241
|
effectFrames.set(o, {
|
|
236
242
|
node: i,
|
|
237
|
-
causes: e.
|
|
243
|
+
causes: e.ve
|
|
238
244
|
});
|
|
239
245
|
}
|
|
240
246
|
originFrames.push(o);
|
|
@@ -320,7 +326,7 @@ function originEnd() {
|
|
|
320
326
|
/** Record a root change (setSignal / refresh / async landing) on the node. */
|
|
321
327
|
/** Live subscriber count, walked on demand — the core keeps no counter. */ function countSubscribers(e) {
|
|
322
328
|
let n = 0;
|
|
323
|
-
for (let t = e.u; t !== null; t = t.
|
|
329
|
+
for (let t = e.u; t !== null; t = t.ie) n++;
|
|
324
330
|
return n;
|
|
325
331
|
}
|
|
326
332
|
|
|
@@ -339,8 +345,8 @@ function originEnd() {
|
|
|
339
345
|
const i = countSubscribers(e);
|
|
340
346
|
const o = e;
|
|
341
347
|
if (i < t || i >= GRAPH_SIZE_WARN_AT) return;
|
|
342
|
-
if (i < (o
|
|
343
|
-
o
|
|
348
|
+
if (i < (o.$e ?? 0) * 2) return;
|
|
349
|
+
o.$e = i;
|
|
344
350
|
const r = n === "refresh" ? "refresh of" : n === "async" ? "async landing on" : "write to";
|
|
345
351
|
const s = `[WIDE_WRITE] ${r} "${nodeName(e)}" reached ${i} subscribers — every one ` + `re-runs this flush. If consumers ask keyed questions of this value (for example every ` + `row comparing against one selected id), invert it: keep the answer in a store used as a ` + `map keyed by id, so each consumer reads its own key and only the keys that flipped update.`;
|
|
346
352
|
reportDiagnostic(emitDiagnostic({
|
|
@@ -369,10 +375,10 @@ function stampWrite(e, n, t = NO_VALUES, i = NO_VALUES) {
|
|
|
369
375
|
o.origin = n === "async" ? asyncOrigin(e) : currentOrigin();
|
|
370
376
|
o.at = now();
|
|
371
377
|
o.stack = captureStack();
|
|
372
|
-
const r = e
|
|
373
|
-
e
|
|
378
|
+
const r = e.ke;
|
|
379
|
+
e.ke = o;
|
|
374
380
|
noteNavigationWrite(r, o);
|
|
375
|
-
noteInteractionWrite(o.origin);
|
|
381
|
+
noteInteractionWrite(o.origin, excludedNode(e));
|
|
376
382
|
if (n === "write") trackEffectWrite(e, o, i);
|
|
377
383
|
// stampWrite is the single funnel for committed root invalidations (sync
|
|
378
384
|
// writes, refresh(), async landings), which makes it the one place the
|
|
@@ -381,7 +387,7 @@ function stampWrite(e, n, t = NO_VALUES, i = NO_VALUES) {
|
|
|
381
387
|
}
|
|
382
388
|
|
|
383
389
|
/** Record a derived change (memo produced a new value) with its causes. */ function stampDerived(e, n) {
|
|
384
|
-
e
|
|
390
|
+
e.ke = {
|
|
385
391
|
seq: ++changeSeq,
|
|
386
392
|
kind: "derived",
|
|
387
393
|
name: nodeName(e),
|
|
@@ -397,10 +403,10 @@ function stampWrite(e, n, t = NO_VALUES, i = NO_VALUES) {
|
|
|
397
403
|
*/ function collectCauses(e) {
|
|
398
404
|
const n = e.Le ?? 0;
|
|
399
405
|
const t = [];
|
|
400
|
-
const i = e
|
|
406
|
+
const i = e.ke;
|
|
401
407
|
if (i !== undefined && i.seq > n && i.kind === "refresh") t.push(i);
|
|
402
|
-
for (let i = e.
|
|
403
|
-
const e = i.
|
|
408
|
+
for (let i = e.ee; i !== null; i = i.te) {
|
|
409
|
+
const e = i.re.ke;
|
|
404
410
|
if (e !== undefined && e.seq > n) t.push(e);
|
|
405
411
|
}
|
|
406
412
|
return t;
|
|
@@ -410,9 +416,19 @@ function stampWrite(e, n, t = NO_VALUES, i = NO_VALUES) {
|
|
|
410
416
|
e.Le = changeSeq;
|
|
411
417
|
}
|
|
412
418
|
|
|
413
|
-
/**
|
|
419
|
+
/**
|
|
420
|
+
* Snapshot the dep identities of the node's last pass (call before a run
|
|
421
|
+
* replaces them, or after it to read the fresh set). The validated prefix
|
|
422
|
+
* [`_deps`..`_depsTail`] is that pass's set; links past the tail are a
|
|
423
|
+
* previous pass's, kept linked while the frame they fed is still the
|
|
424
|
+
* committed one (A30 — a staged memo pass, or an effect pass whose run is
|
|
425
|
+
* still owed) and not part of the subscription diff.
|
|
426
|
+
*/ function captureDeps(e) {
|
|
414
427
|
const n = [];
|
|
415
|
-
for (let t = e.
|
|
428
|
+
for (let t = e.ee; t !== null; t = t.te) {
|
|
429
|
+
n.push(t.re);
|
|
430
|
+
if (t === e.Fe) break;
|
|
431
|
+
}
|
|
416
432
|
return n;
|
|
417
433
|
}
|
|
418
434
|
|
|
@@ -426,14 +442,16 @@ function stampWrite(e, n, t = NO_VALUES, i = NO_VALUES) {
|
|
|
426
442
|
if (n === false) return;
|
|
427
443
|
let t = 0;
|
|
428
444
|
const i = [];
|
|
429
|
-
for (let n = e.
|
|
445
|
+
for (let n = e.ee; n !== null; n = n.te) {
|
|
430
446
|
t++;
|
|
431
|
-
if (i.length < 12) i.push(nodeName(n.
|
|
432
|
-
|
|
447
|
+
if (i.length < 12) i.push(nodeName(n.re));
|
|
448
|
+
if (n === e.Fe) break;
|
|
449
|
+
// the validated prefix, as captureDeps
|
|
450
|
+
}
|
|
433
451
|
const o = e;
|
|
434
|
-
if (t < n || t < (o.
|
|
435
|
-
o.
|
|
436
|
-
const r = e.
|
|
452
|
+
if (t < n || t < (o.We ?? 0) * 1.5) return;
|
|
453
|
+
o.We = t;
|
|
454
|
+
const r = e.Pe ? "effect" : "memo";
|
|
437
455
|
const s = `[WIDE_SCOPE_DEPS] ${r} "${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: ${i.join(", ")}${t > i.length ? ", …" : ""}`;
|
|
438
456
|
reportDiagnostic(emitDiagnostic({
|
|
439
457
|
code: "WIDE_SCOPE_DEPS",
|
|
@@ -462,14 +480,14 @@ const HOT_FANOUT_FIRST_MILESTONE = 5;
|
|
|
462
480
|
if (t === false) return;
|
|
463
481
|
const i = e;
|
|
464
482
|
const o = Date.now();
|
|
465
|
-
if (i.
|
|
466
|
-
i.
|
|
467
|
-
i.
|
|
468
|
-
i.
|
|
483
|
+
if (i.Me === undefined || o - i.Me > t.windowMs) {
|
|
484
|
+
i.Me = o;
|
|
485
|
+
i.He = 0;
|
|
486
|
+
i.qe = false;
|
|
469
487
|
}
|
|
470
|
-
i.
|
|
471
|
-
if (i.
|
|
472
|
-
i.
|
|
488
|
+
i.He = (i.He ?? 0) + 1;
|
|
489
|
+
if (i.qe || i.He < t.count) return;
|
|
490
|
+
i.qe = true;
|
|
473
491
|
// Root-cause key: the set of originating writes behind this scope's latest
|
|
474
492
|
// re-run. Scopes hot from the SAME roots share one aggregation window.
|
|
475
493
|
const r = new Set;
|
|
@@ -486,10 +504,10 @@ const HOT_FANOUT_FIRST_MILESTONE = 5;
|
|
|
486
504
|
hotCauses.set(s, a);
|
|
487
505
|
}
|
|
488
506
|
a.scopes++;
|
|
489
|
-
a.runs += i.
|
|
507
|
+
a.runs += i.He;
|
|
490
508
|
if (a.scopes === 1) {
|
|
491
509
|
const r = n.causes.map(e => `"${e.name}" (${e.kind})`).join(", ");
|
|
492
|
-
const s = `[HOT_SCOPE_RERUNS] ${n.nodeKind} "${n.nodeName}" re-ran ${i.
|
|
510
|
+
const s = `[HOT_SCOPE_RERUNS] ${n.nodeKind} "${n.nodeName}" re-ran ${i.He} times ` + `in ${Math.max(1, o - i.Me)}ms — a hot signal is likely leaking into this ` + `scope. Latest cause: ${r || "(untracked pull)"}`;
|
|
493
511
|
reportDiagnostic(emitDiagnostic({
|
|
494
512
|
code: "HOT_SCOPE_RERUNS",
|
|
495
513
|
kind: "perf",
|
|
@@ -497,7 +515,7 @@ const HOT_FANOUT_FIRST_MILESTONE = 5;
|
|
|
497
515
|
message: s,
|
|
498
516
|
nodeName: n.nodeName,
|
|
499
517
|
data: {
|
|
500
|
-
runs: i.
|
|
518
|
+
runs: i.He,
|
|
501
519
|
windowMs: t.windowMs,
|
|
502
520
|
causes: n.causes.map(e => e.name)
|
|
503
521
|
}
|
|
@@ -535,16 +553,16 @@ const HOT_FANOUT_FIRST_MILESTONE = 5;
|
|
|
535
553
|
if (t === false) return;
|
|
536
554
|
const i = e;
|
|
537
555
|
const o = now();
|
|
538
|
-
if (i.
|
|
539
|
-
i.
|
|
540
|
-
i.
|
|
541
|
-
i.
|
|
556
|
+
if (i.xe === undefined || o - i.xe > t.windowMs) {
|
|
557
|
+
i.xe = o;
|
|
558
|
+
i.je = 0;
|
|
559
|
+
i.Ve = false;
|
|
542
560
|
}
|
|
543
|
-
i.
|
|
544
|
-
if (i.
|
|
545
|
-
i.
|
|
561
|
+
i.je = (i.je ?? 0) + n.selfMs;
|
|
562
|
+
if (i.Ve || i.je < t.budgetMs) return;
|
|
563
|
+
i.Ve = true;
|
|
546
564
|
const r = n.causes.map(e => `"${e.name}" (${e.kind})`).join(", ");
|
|
547
|
-
const s = `[HOT_SCOPE_TIME] ${n.nodeKind} "${n.nodeName}" spent ` + `${i.
|
|
565
|
+
const s = `[HOT_SCOPE_TIME] ${n.nodeKind} "${n.nodeName}" spent ` + `${i.je.toFixed(1)}ms of compute inside one ${t.windowMs}ms window ` + `(budget ${t.budgetMs}ms). Latest cause: ${r || "(untracked pull)"}`;
|
|
548
566
|
reportDiagnostic(emitDiagnostic({
|
|
549
567
|
code: "HOT_SCOPE_TIME",
|
|
550
568
|
kind: "perf",
|
|
@@ -552,7 +570,7 @@ const HOT_FANOUT_FIRST_MILESTONE = 5;
|
|
|
552
570
|
message: s,
|
|
553
571
|
nodeName: n.nodeName,
|
|
554
572
|
data: {
|
|
555
|
-
spentMs: i.
|
|
573
|
+
spentMs: i.je,
|
|
556
574
|
budgetMs: t.budgetMs,
|
|
557
575
|
windowMs: t.windowMs,
|
|
558
576
|
causes: n.causes.map(e => e.name)
|
|
@@ -564,34 +582,34 @@ function recordRerun(e, n, t, i, o, r) {
|
|
|
564
582
|
const s = n.causes;
|
|
565
583
|
const a = n.prevDeps;
|
|
566
584
|
const c = e;
|
|
567
|
-
const
|
|
585
|
+
const f = c.ve;
|
|
568
586
|
if (excludedNode(e)) {
|
|
569
587
|
// The observer's own computation: keep the per-node bookkeeping its
|
|
570
588
|
// effect phase reads (see effectRunStart) and record nothing.
|
|
571
|
-
c.
|
|
589
|
+
c.Ye = n.interaction;
|
|
572
590
|
c.we = undefined;
|
|
573
|
-
c.
|
|
591
|
+
c.ve = s;
|
|
574
592
|
return;
|
|
575
593
|
}
|
|
576
594
|
// Subscription diff: `prevDeps` was captured at run entry; `_deps` now
|
|
577
595
|
// holds the fresh set. A changed set is the "helper edit changed distant
|
|
578
596
|
// call sites" signal — surfaced per-event and in the console format.
|
|
579
|
-
const
|
|
597
|
+
const d = captureDeps(e);
|
|
580
598
|
const u = new Set(a);
|
|
581
|
-
const l = new Set(
|
|
599
|
+
const l = new Set(d);
|
|
582
600
|
const h = [];
|
|
583
601
|
const m = [];
|
|
584
|
-
for (const e of
|
|
602
|
+
for (const e of d) if (!u.has(e)) h.push(nodeName(e));
|
|
585
603
|
for (const e of a) if (!l.has(e)) m.push(nodeName(e));
|
|
586
604
|
const p = {
|
|
587
605
|
run: ++runSeq,
|
|
588
606
|
at: n.start,
|
|
589
|
-
nodeRuns: c.
|
|
590
|
-
nodeKind: e.
|
|
607
|
+
nodeRuns: c.Be = (c.Be ?? 0) + 1,
|
|
608
|
+
nodeKind: e.Pe ? "effect" : "memo",
|
|
591
609
|
nodeName: nodeName(e),
|
|
592
|
-
|
|
610
|
+
nodeId: devId(e),
|
|
593
611
|
causes: s,
|
|
594
|
-
depCount:
|
|
612
|
+
depCount: d.length,
|
|
595
613
|
depsAdded: h,
|
|
596
614
|
depsRemoved: m,
|
|
597
615
|
selfMs: t.selfMs,
|
|
@@ -605,16 +623,19 @@ function recordRerun(e, n, t, i, o, r) {
|
|
|
605
623
|
// The effect phase runs later in the flush with no cause list of its own:
|
|
606
624
|
// it inherits this run's interaction and is joined to this run's causes
|
|
607
625
|
// (see effectRunStart / pushFrame).
|
|
608
|
-
c.
|
|
626
|
+
c.Ye = g;
|
|
609
627
|
c.we = p.run;
|
|
610
|
-
c.
|
|
628
|
+
c.ve = s;
|
|
629
|
+
// The record is serializable and never carries the node; keep the node
|
|
630
|
+
// beside it for `OBSERVE.subjectOf` and the engine's own joins (effect
|
|
631
|
+
// cycles, relay tears, `why()`), for as long as anyone holds the record.
|
|
632
|
+
recordSubject(p, e);
|
|
611
633
|
history.push(p);
|
|
612
634
|
if (history.length > options.historyLimit) history.shift();
|
|
613
|
-
|
|
614
|
-
recordFeedbackRun(p);
|
|
635
|
+
for (const n of folds) n.rerun?.(e, p);
|
|
615
636
|
noteInteractionRun(g, t.selfMs, false);
|
|
616
637
|
if (p.nodeKind === "effect") checkEffectCycle(e, s);
|
|
617
|
-
checkRelayTear(e, s,
|
|
638
|
+
checkRelayTear(e, s, f);
|
|
618
639
|
checkHotRuns(e, p);
|
|
619
640
|
checkHotTime(e, p);
|
|
620
641
|
checkDepWidth(e);
|
|
@@ -710,15 +731,15 @@ function shallowEquivalent(e, n) {
|
|
|
710
731
|
const o = e;
|
|
711
732
|
if (n === t || // paranoia: changed runs should never hit this
|
|
712
733
|
!isPlainShape(n) || !isPlainShape(t) || !shallowEquivalent(n, t)) {
|
|
713
|
-
o.
|
|
714
|
-
o.
|
|
734
|
+
o.ze = 0;
|
|
735
|
+
o.Ke = false;
|
|
715
736
|
return;
|
|
716
737
|
}
|
|
717
|
-
o.
|
|
718
|
-
if (o.
|
|
719
|
-
o.
|
|
738
|
+
o.ze = (o.ze ?? 0) + 1;
|
|
739
|
+
if (o.Ke || o.ze < i) return;
|
|
740
|
+
o.Ke = true;
|
|
720
741
|
const r = Array.isArray(t) ? "array" : "object";
|
|
721
|
-
const s = `[UNSTABLE_MEMO_OUTPUT] memo "${nodeName(e)}" produced a new-but-equivalent ${r} on ` + `${o.
|
|
742
|
+
const s = `[UNSTABLE_MEMO_OUTPUT] memo "${nodeName(e)}" produced a new-but-equivalent ${r} on ` + `${o.ze} consecutive runs — its equality gate never closes, so every ` + `subscriber re-runs on every upstream change. Return stable references or pass an ` + `\`equals\` option.`;
|
|
722
743
|
reportDiagnostic(emitDiagnostic({
|
|
723
744
|
code: "UNSTABLE_MEMO_OUTPUT",
|
|
724
745
|
kind: "perf",
|
|
@@ -726,7 +747,7 @@ function shallowEquivalent(e, n) {
|
|
|
726
747
|
message: s,
|
|
727
748
|
nodeName: nodeName(e),
|
|
728
749
|
data: {
|
|
729
|
-
runs: o.
|
|
750
|
+
runs: o.ze,
|
|
730
751
|
shape: r
|
|
731
752
|
}
|
|
732
753
|
}, e));
|
|
@@ -860,17 +881,17 @@ const copyReported = new WeakSet;
|
|
|
860
881
|
const r = o !== undefined && o.kind === "effect" ? effectFrames.get(o) : undefined;
|
|
861
882
|
const s = r === undefined ? 0 : devId(r.node);
|
|
862
883
|
recordNodes.set(n, e);
|
|
863
|
-
i.
|
|
864
|
-
if (r !== undefined && t !== undefined && t === r.node.
|
|
884
|
+
i.Xe = i.Xe === undefined || i.Xe === s ? s : null;
|
|
885
|
+
if (r !== undefined && t !== undefined && t === r.node.Ae) {
|
|
865
886
|
copyWrites.add(n);
|
|
866
|
-
i.
|
|
867
|
-
i.
|
|
868
|
-
if (i.
|
|
869
|
-
} else i.
|
|
887
|
+
i.Ze = i.Je === s ? (i.Ze ?? 0) + 1 : 1;
|
|
888
|
+
i.Je = s;
|
|
889
|
+
if (i.Ze >= 2 && i.Xe === s) checkCopyEffect(r.node, e);
|
|
890
|
+
} else i.Ze = 0;
|
|
870
891
|
}
|
|
871
892
|
|
|
872
893
|
/** The effect's source whose current value the compute output is, if any (the prop-to-state port). */ function passthroughSource(e) {
|
|
873
|
-
for (let n = e.
|
|
894
|
+
for (let n = e.ee; n !== null; n = n.te) if (n.re.Ae === e.Ae) return nodeName(n.re);
|
|
874
895
|
return undefined;
|
|
875
896
|
}
|
|
876
897
|
|
|
@@ -941,27 +962,27 @@ function checkCopyEffect(e, n) {
|
|
|
941
962
|
warned: false
|
|
942
963
|
});
|
|
943
964
|
c.count++;
|
|
944
|
-
const
|
|
945
|
-
const
|
|
946
|
-
const u =
|
|
965
|
+
const f = copyWrites.has(r);
|
|
966
|
+
const d = recordNodes.get(r);
|
|
967
|
+
const u = d !== undefined && d.Xe === devId(o.node);
|
|
947
968
|
// Derivable outright: the value is the compute output and nothing else
|
|
948
969
|
// writes the signal. A copy INTO a signal that has other writers is the
|
|
949
970
|
// "reset editable state from a source" shape — the tear is real, but a memo
|
|
950
971
|
// is not the answer, so it stays advisory like any other non-derivable tear.
|
|
951
|
-
const l =
|
|
972
|
+
const l = f && u;
|
|
952
973
|
const h = l || c.count >= RELAY_WARN_AT ? "warn" : "info";
|
|
953
974
|
// First sighting always reports (advisory); afterwards only the escalation.
|
|
954
975
|
if (c.count > 1 && (h !== "warn" || c.warned)) return;
|
|
955
976
|
// One verdict per derivable signal: the copy report (checkCopyEffect) and
|
|
956
977
|
// the tear report carry the same repair.
|
|
957
|
-
if (l &&
|
|
958
|
-
if (copyReported.has(
|
|
959
|
-
copyReported.add(
|
|
978
|
+
if (l && d !== undefined) {
|
|
979
|
+
if (copyReported.has(d)) return;
|
|
980
|
+
copyReported.add(d);
|
|
960
981
|
}
|
|
961
982
|
if (h === "warn") c.warned = true;
|
|
962
|
-
const m = e.
|
|
983
|
+
const m = e.Pe ? "effect" : "memo";
|
|
963
984
|
const p = nodeName(o.node);
|
|
964
|
-
const g = l ? copyRepair(o.node, r.name) :
|
|
985
|
+
const g = l ? copyRepair(o.node, r.name) : f ? `The written value is the effect's compute output, but "${r.name}" has other ` + `writers — editable state reset from a source. If the reset is the intent, the tear ` + `is its cost; if "${r.name}" only ever mirrors the source, drop the local copy ` + `and read the source.` : u ? `Nothing else writes "${r.name}" — it is derived state: make it a memo over what ` + `the effect reads and every reader gets it in the same flush.` : `If "${r.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.`;
|
|
965
986
|
const w = `[EFFECT_RELAY_TEAR] ${m} "${nodeName(e)}" ran twice for one write of ` + `"${s.name}": once in the flush where "${s.name}" changed, and again after ` + `effect "${p}" relayed it by writing "${r.name}" — the first frame showed the ` + `new "${s.name}" with the stale "${r.name}"` + (c.count > 1 ? ` (${c.count} times so far)` : "") + `. ${g}`;
|
|
966
987
|
emitDiagnostic({
|
|
967
988
|
code: "EFFECT_RELAY_TEAR",
|
|
@@ -974,8 +995,8 @@ function checkCopyEffect(e, n) {
|
|
|
974
995
|
root: s.name,
|
|
975
996
|
relay: p,
|
|
976
997
|
wrote: r.name,
|
|
977
|
-
copy:
|
|
978
|
-
passthrough:
|
|
998
|
+
copy: f,
|
|
999
|
+
passthrough: f ? passthroughSource(o.node) ?? null : null,
|
|
979
1000
|
soleWriter: u,
|
|
980
1001
|
occurrences: c.count
|
|
981
1002
|
}
|
|
@@ -997,33 +1018,37 @@ function checkCopyEffect(e, n) {
|
|
|
997
1018
|
// per store path.
|
|
998
1019
|
const immutableReported = new Set;
|
|
999
1020
|
|
|
1000
|
-
function checkImmutableUpdate(e, n, t, i, o) {
|
|
1021
|
+
function checkImmutableUpdate(e, n, t, i, o, r) {
|
|
1001
1022
|
if (immutableReported.has(e) || t < 2) return;
|
|
1002
1023
|
// Push/filter/splice copies change the length by a little; a wholesale
|
|
1003
1024
|
// resize is a different operation even if some items survive.
|
|
1004
1025
|
if (n && Math.abs(o - t) > Math.max(1, t >> 2)) return;
|
|
1005
1026
|
// At least half the leaves carried over unchanged, and at least one did.
|
|
1006
1027
|
if (i === 0 || i * 2 < t) return;
|
|
1007
|
-
const
|
|
1008
|
-
const
|
|
1009
|
-
const
|
|
1010
|
-
const
|
|
1011
|
-
|
|
1028
|
+
const s = t - i;
|
|
1029
|
+
const a = n ? "array" : "object";
|
|
1030
|
+
const c = n ? `mutate the draft in place (push/splice/index assignment) so only the touched ` + `indices notify` : `assign the leaf on the draft (\`${e}.<key> = …\`) so only readers of that key re-run`;
|
|
1031
|
+
const f = `[IMMUTABLE_UPDATE_IN_STORE] "${e}" was replaced with a fresh ${a} whose ` + `${n ? "items" : "leaves"} are mostly the same values (${i} of ${t} unchanged` + `${s > 0 ? `, ${s} changed` : ""}) — a spread-copy update. The store already ` + `tracks ${n ? "items" : "leaves"}; a new container makes every reader of "${e}" ` + `re-run for the ${s === 1 ? "one that" : "few that"} moved. Instead, ${c}. For ` + `data arriving from outside (a fetch result), merge it with reconcile(data, key)(${e}).`;
|
|
1032
|
+
// The subject is the store's own owner, not the writer's context: the
|
|
1033
|
+
// finding is about the store, and its writes legitimately arrive from
|
|
1034
|
+
// outside the graph (an event handler, an adapter). Falls back to the
|
|
1035
|
+
// ambient context (emitDiagnostic's default) when the store recorded none.
|
|
1036
|
+
const d = emitDiagnostic({
|
|
1012
1037
|
code: "IMMUTABLE_UPDATE_IN_STORE",
|
|
1013
1038
|
kind: "perf",
|
|
1014
1039
|
severity: "warn",
|
|
1015
|
-
message:
|
|
1040
|
+
message: f,
|
|
1016
1041
|
nodeName: e,
|
|
1017
1042
|
data: {
|
|
1018
1043
|
path: e,
|
|
1019
|
-
shape:
|
|
1044
|
+
shape: a,
|
|
1020
1045
|
total: t,
|
|
1021
1046
|
unchanged: i,
|
|
1022
|
-
changed:
|
|
1047
|
+
changed: s
|
|
1023
1048
|
}
|
|
1024
|
-
});
|
|
1025
|
-
// Paths are not unique across stores:
|
|
1026
|
-
//
|
|
1049
|
+
}, r);
|
|
1050
|
+
// Paths are not unique across stores: an excluded owner's store (an
|
|
1051
|
+
// adapter's own "store.list") must not spend the app's once-per-path slot.
|
|
1027
1052
|
if (isSuppressed(d)) return;
|
|
1028
1053
|
immutableReported.add(e);
|
|
1029
1054
|
}
|
|
@@ -1046,7 +1071,7 @@ const listIdentityWarned = new WeakSet;
|
|
|
1046
1071
|
function recordId(e) {
|
|
1047
1072
|
if (e === null || typeof e !== "object") return undefined;
|
|
1048
1073
|
const n = e;
|
|
1049
|
-
return n.id ?? n.key ?? n.
|
|
1074
|
+
return n.id ?? n.key ?? n.Qe ?? undefined;
|
|
1050
1075
|
}
|
|
1051
1076
|
|
|
1052
1077
|
function checkListIdentity(e, n, t, i, o) {
|
|
@@ -1073,15 +1098,15 @@ function checkListIdentity(e, n, t, i, o) {
|
|
|
1073
1098
|
}
|
|
1074
1099
|
if (s === 0 || a * 2 < s) return;
|
|
1075
1100
|
listIdentityWarned.add(e);
|
|
1076
|
-
const
|
|
1077
|
-
const
|
|
1078
|
-
const u = `[UNSTABLE_LIST_IDENTITY] list "${
|
|
1101
|
+
const f = nodeName(e);
|
|
1102
|
+
const d = o ? `The key function returned different keys for equivalent records — return a stable ` + `field (\`keyed: item => item.id\`), not the object or a computed value that changes ` + `with the fetch.` : `Key the list by a stable field (\`keyed: item => item.id\`), or merge the data into ` + `a store with reconcile(data, "id") so the same records keep the same identity.`;
|
|
1103
|
+
const u = `[UNSTABLE_LIST_IDENTITY] list "${f}" recreated ${t.length} of ${i} rows on ` + `an update where the entering items are equivalent to the ones they replaced ` + `(${a} of ${s} sampled pairs identical field-for-field) — fresh objects ` + `for the same records, so identity keying threw away every row's DOM and state and ` + `rebuilt it. ${d}`;
|
|
1079
1104
|
reportDiagnostic(emitDiagnostic({
|
|
1080
1105
|
code: "UNSTABLE_LIST_IDENTITY",
|
|
1081
1106
|
kind: "perf",
|
|
1082
1107
|
severity: "warn",
|
|
1083
1108
|
message: u,
|
|
1084
|
-
nodeName:
|
|
1109
|
+
nodeName: f,
|
|
1085
1110
|
data: {
|
|
1086
1111
|
removed: n.length,
|
|
1087
1112
|
created: t.length,
|
|
@@ -1133,21 +1158,19 @@ function trackFlightStart(e, n) {
|
|
|
1133
1158
|
if (i === t) flightOrigins.set(n, t);
|
|
1134
1159
|
// Census: a flight still in the air when the node starts another was
|
|
1135
1160
|
// superseded — its answer will be discarded.
|
|
1136
|
-
const
|
|
1137
|
-
o.flights++;
|
|
1138
|
-
if (liveFlights.has(e)) o.abandoned++;
|
|
1161
|
+
for (const n of folds) n.flightStart?.(e, liveFlights.has(e));
|
|
1139
1162
|
// Nearest enclosing frame with causes: create runs carry null (a node born
|
|
1140
1163
|
// inside a parent's recompute inherits the parent's causality — the
|
|
1141
1164
|
// boundary-reveal case, and the lazy sibling whose first pull is gated
|
|
1142
1165
|
// behind an earlier not-ready read), so walk down to the first re-run frame.
|
|
1143
|
-
let
|
|
1166
|
+
let o = null;
|
|
1144
1167
|
for (let e = frames.length - 1; e >= 0; e--) {
|
|
1145
1168
|
if (frames[e].causes !== null) {
|
|
1146
|
-
|
|
1169
|
+
o = frames[e].causes;
|
|
1147
1170
|
break;
|
|
1148
1171
|
}
|
|
1149
1172
|
}
|
|
1150
|
-
const
|
|
1173
|
+
const r = {
|
|
1151
1174
|
origin: i,
|
|
1152
1175
|
startSeq: changeSeq,
|
|
1153
1176
|
chain: []
|
|
@@ -1155,19 +1178,19 @@ function trackFlightStart(e, n) {
|
|
|
1155
1178
|
// Provenance: the flight belongs to whatever interaction caused the
|
|
1156
1179
|
// recompute that started it (a create run under a click's handler — a
|
|
1157
1180
|
// freshly mounted async node — inherits the ambient interaction instead).
|
|
1158
|
-
const
|
|
1159
|
-
if (
|
|
1160
|
-
if (options.waterfalls !== false &&
|
|
1161
|
-
let e = flightCauseIn(
|
|
1181
|
+
const s = o !== null ? interactionIn(o) : currentInteraction ?? undefined;
|
|
1182
|
+
if (s !== undefined) r.interaction = s;
|
|
1183
|
+
if (options.waterfalls !== false && o !== null) {
|
|
1184
|
+
let e = flightCauseIn(o);
|
|
1162
1185
|
// The sequentiality test. A marked/previously-seen flight whose origin
|
|
1163
1186
|
// predates the upstream landing was in the air alongside it: parallel.
|
|
1164
1187
|
if (e !== null && i < e.landedAt) e = null;
|
|
1165
|
-
if (e !== null)
|
|
1188
|
+
if (e !== null) r.chain = [ ...e.chain, {
|
|
1166
1189
|
name: e.name,
|
|
1167
1190
|
ms: e.ms
|
|
1168
1191
|
} ];
|
|
1169
1192
|
}
|
|
1170
|
-
liveFlights.set(e,
|
|
1193
|
+
liveFlights.set(e, r);
|
|
1171
1194
|
}
|
|
1172
1195
|
|
|
1173
1196
|
/**
|
|
@@ -1180,14 +1203,11 @@ function trackFlightStart(e, n) {
|
|
|
1180
1203
|
liveFlights.delete(e);
|
|
1181
1204
|
const t = now();
|
|
1182
1205
|
const i = t - n.origin;
|
|
1183
|
-
const
|
|
1184
|
-
o.
|
|
1185
|
-
o.landedMs += i;
|
|
1186
|
-
if (i > o.worstMs) o.worstMs = i;
|
|
1187
|
-
const r = e.$e;
|
|
1206
|
+
for (const n of folds) n.flightLanded?.(e, i);
|
|
1207
|
+
const o = e.ke;
|
|
1188
1208
|
// Only a stamp this landing produced may carry the measurement — a stale
|
|
1189
1209
|
// async record from a previous landing must not be re-labeled.
|
|
1190
|
-
if (
|
|
1210
|
+
if (o !== undefined && o.kind === "async" && o.seq > n.startSeq) landedFlights.set(o, {
|
|
1191
1211
|
name: nodeName(e),
|
|
1192
1212
|
ms: i,
|
|
1193
1213
|
chain: n.chain,
|
|
@@ -1220,24 +1240,24 @@ function checkWaterfall(e, n, t) {
|
|
|
1220
1240
|
}
|
|
1221
1241
|
if (o < 2) return;
|
|
1222
1242
|
const s = e;
|
|
1223
|
-
if ((s.
|
|
1224
|
-
s.
|
|
1243
|
+
if ((s.en ?? 0) >= o) return;
|
|
1244
|
+
s.en = o;
|
|
1225
1245
|
const a = [ ...n.slice(n.length - (o - 1)), {
|
|
1226
1246
|
name: nodeName(e),
|
|
1227
1247
|
ms: t
|
|
1228
1248
|
} ];
|
|
1229
1249
|
const c = a.map(e => `"${e.name}" (${e.ms.toFixed(0)}ms)`).join(" → ");
|
|
1230
|
-
const
|
|
1250
|
+
const f = `[ASYNC_WATERFALL] ${o} sequential async flights — ${c} — ` + `${r.toFixed(0)}ms serialized: each began only after the previous resolved ` + `(as far as this graph can see). If a later request doesn't need the earlier ` + `response, derive both from the same inputs so they start together; if the ` + `dependency is intrinsic, preload the dependent data or join the requests ` + `server-side. If this work WAS already started elsewhere (a preloader or request ` + `cache), have that layer stamp its promises with attribution.markFlight() ` + `from "@solidjs/signals/attribution".`;
|
|
1231
1251
|
// Depth 2 is advisory-only (structured consumers see it; the console does
|
|
1232
1252
|
// not): a 2-chain can be an intrinsic data dependency or an unmarked
|
|
1233
1253
|
// preload. A 3+ chain that survived the origin test is near-certainly
|
|
1234
1254
|
// structural — that one earns the console.
|
|
1235
|
-
const
|
|
1255
|
+
const d = o > 2 ? "warn" : "info";
|
|
1236
1256
|
emitDiagnostic({
|
|
1237
1257
|
code: "ASYNC_WATERFALL",
|
|
1238
1258
|
kind: "perf",
|
|
1239
|
-
severity:
|
|
1240
|
-
message:
|
|
1259
|
+
severity: d,
|
|
1260
|
+
message: f,
|
|
1241
1261
|
nodeName: nodeName(e),
|
|
1242
1262
|
data: {
|
|
1243
1263
|
chain: a.map(e => ({
|
|
@@ -1255,8 +1275,10 @@ let activeHold = null;
|
|
|
1255
1275
|
|
|
1256
1276
|
let holdLog = [];
|
|
1257
1277
|
|
|
1258
|
-
/** Companions are optimistic nodes too; `_parentSource` marks them.
|
|
1259
|
-
|
|
1278
|
+
/** Companions are optimistic nodes too; `_parentSource` marks them. So is a
|
|
1279
|
+
* memo carrying a DERIVED override (lanes stage, #3479) — a lane pass's
|
|
1280
|
+
* result, not a write anyone made: neither is an acknowledgement. */ function isCompanion(e) {
|
|
1281
|
+
return !!e.o && e.o.nn !== undefined || (e.C & CONFIG_DERIVED_OVERRIDE) !== 0;
|
|
1260
1282
|
}
|
|
1261
1283
|
|
|
1262
1284
|
const HOLD_CENSUS_CAP = 1e4;
|
|
@@ -1281,8 +1303,8 @@ function censusRegistrations(e, n) {
|
|
|
1281
1303
|
const t = {
|
|
1282
1304
|
left: HOLD_CENSUS_CAP
|
|
1283
1305
|
};
|
|
1284
|
-
for (const i of e.
|
|
1285
|
-
for (const t of e.
|
|
1306
|
+
for (const i of e.tn) if (!isCompanion(i)) acknowledge(n, "optimistic", nodeName(i), reachesEffect(i, t));
|
|
1307
|
+
for (const t of e.rn) acknowledge(n, "optimistic", t?._name ?? "store", null);
|
|
1286
1308
|
for (const i of e.A) acknowledge(n, "affects", nodeName(i), reachesEffect(i, t));
|
|
1287
1309
|
}
|
|
1288
1310
|
|
|
@@ -1297,9 +1319,9 @@ function censusRegistrations(e, n) {
|
|
|
1297
1319
|
const i = [ e ];
|
|
1298
1320
|
while (i.length > 0 && n.left-- > 0) {
|
|
1299
1321
|
const e = i.pop();
|
|
1300
|
-
for (let n = e.u; n !== null; n = n.
|
|
1301
|
-
const e = n.
|
|
1302
|
-
if (e.
|
|
1322
|
+
for (let n = e.u; n !== null; n = n.ie) {
|
|
1323
|
+
const e = n.se;
|
|
1324
|
+
if (e.Pe) return e;
|
|
1303
1325
|
if (!t.has(e)) {
|
|
1304
1326
|
t.add(e);
|
|
1305
1327
|
i.push(e);
|
|
@@ -1322,11 +1344,11 @@ function censusRegistrations(e, n) {
|
|
|
1322
1344
|
const r = e.o;
|
|
1323
1345
|
if (r) {
|
|
1324
1346
|
let t;
|
|
1325
|
-
if (r.
|
|
1326
|
-
if (r.
|
|
1347
|
+
if (r.he !== undefined && (t = reachesEffect(r.he, o))) acknowledge(n, "isPending", nodeName(e), t);
|
|
1348
|
+
if (r.Ge !== undefined && (t = reachesEffect(r.Ge, o))) acknowledge(n, "latest", nodeName(e), t);
|
|
1327
1349
|
for (let e = r.i ?? null; e !== null; e = e.ue ?? null) i.push(e);
|
|
1328
1350
|
}
|
|
1329
|
-
for (let n = e.u; n !== null; n = n.
|
|
1351
|
+
for (let n = e.u; n !== null; n = n.ie) i.push(n.se);
|
|
1330
1352
|
}
|
|
1331
1353
|
}
|
|
1332
1354
|
|
|
@@ -1354,8 +1376,8 @@ function trackHoldStart(e) {
|
|
|
1354
1376
|
if (options.holds === false) return;
|
|
1355
1377
|
const n = holdState(e);
|
|
1356
1378
|
n.flushes++;
|
|
1357
|
-
if (e.
|
|
1358
|
-
for (const [t, i] of e.
|
|
1379
|
+
if (e.dt.length > 0) n.action = true;
|
|
1380
|
+
for (const [t, i] of e.ot) if (i.size > 0) n.blockers.add(t);
|
|
1359
1381
|
censusRegistrations(e, n);
|
|
1360
1382
|
activeHold = n;
|
|
1361
1383
|
}
|
|
@@ -1392,9 +1414,9 @@ function trackHoldSettled(e) {
|
|
|
1392
1414
|
let o;
|
|
1393
1415
|
let r;
|
|
1394
1416
|
let s = -Infinity;
|
|
1395
|
-
for (const n of e.
|
|
1396
|
-
if (typeof n.
|
|
1397
|
-
const e = n
|
|
1417
|
+
for (const n of e.sn) {
|
|
1418
|
+
if (typeof n.ht === "function" || isCompanion(n)) continue;
|
|
1419
|
+
const e = n.ke;
|
|
1398
1420
|
if (e === undefined || e.kind !== "write") continue;
|
|
1399
1421
|
if (i === null) i = n;
|
|
1400
1422
|
const a = {
|
|
@@ -1421,18 +1443,18 @@ function trackHoldSettled(e) {
|
|
|
1421
1443
|
return;
|
|
1422
1444
|
}
|
|
1423
1445
|
censusRegistrations(e, n);
|
|
1424
|
-
censusCompanions([ ...e.
|
|
1446
|
+
censusCompanions([ ...e.sn, ...n.blockers ], n);
|
|
1425
1447
|
const a = now();
|
|
1426
1448
|
// The hold began no later than its first parked flush; an interaction stamp
|
|
1427
1449
|
// reaches further back (dispatch). A node rewritten mid-hold keeps only its
|
|
1428
1450
|
// latest record, so the surviving interaction may be a later one — the
|
|
1429
1451
|
// flush clock keeps the first wait from being forgotten.
|
|
1430
1452
|
const c = Math.min(n.start, o !== undefined ? o.at : Infinity);
|
|
1431
|
-
const
|
|
1432
|
-
const
|
|
1453
|
+
const f = a - c;
|
|
1454
|
+
const d = {
|
|
1433
1455
|
at: c,
|
|
1434
|
-
holdMs:
|
|
1435
|
-
tailMs: s === -Infinity ?
|
|
1456
|
+
holdMs: f,
|
|
1457
|
+
tailMs: s === -Infinity ? f : Math.min(f, a - s),
|
|
1436
1458
|
flushes: n.flushes,
|
|
1437
1459
|
heldWrites: t,
|
|
1438
1460
|
blockers: [ ...n.blockers ].map(nodeName),
|
|
@@ -1440,20 +1462,21 @@ function trackHoldSettled(e) {
|
|
|
1440
1462
|
paintedDuringHold: n.painted,
|
|
1441
1463
|
action: n.action
|
|
1442
1464
|
};
|
|
1443
|
-
if (o !== undefined)
|
|
1444
|
-
if (r !== undefined)
|
|
1445
|
-
holdLog.push(
|
|
1465
|
+
if (o !== undefined) d.interaction = o;
|
|
1466
|
+
if (r !== undefined) d.origin = r;
|
|
1467
|
+
holdLog.push(d);
|
|
1446
1468
|
if (holdLog.length > options.historyLimit) holdLog.shift();
|
|
1447
1469
|
// Bottom-up delivery: the hold, then the navigations it held, then the
|
|
1448
1470
|
// interactions those belong to — each record complete when its parent is.
|
|
1449
|
-
emitRecord("hold",
|
|
1450
|
-
settleNavigations(e,
|
|
1451
|
-
settleInteractionsHeld(e,
|
|
1452
|
-
|
|
1453
|
-
if (isSilentHold(
|
|
1471
|
+
emitRecord("hold", d);
|
|
1472
|
+
settleNavigations(e, d);
|
|
1473
|
+
settleInteractionsHeld(e, d);
|
|
1474
|
+
for (const e of folds) e.hold?.(d);
|
|
1475
|
+
if (isSilentHold(d)) checkSilentHold(d, i); else checkLongHold(d, i);
|
|
1454
1476
|
}
|
|
1455
1477
|
|
|
1456
|
-
/** `isLongHold` — the tail outlasted `longHolds.infoMs`. */
|
|
1478
|
+
/** `isLongHold` — the tail outlasted `longHolds.infoMs`. */
|
|
1479
|
+
/** @internal */ function isLongHold(e) {
|
|
1457
1480
|
const n = options.longHolds;
|
|
1458
1481
|
return n !== false && n !== undefined && e.tailMs >= n.infoMs;
|
|
1459
1482
|
}
|
|
@@ -1516,15 +1539,15 @@ function checkSilentHold(e, n) {
|
|
|
1516
1539
|
const s = holdActor(e);
|
|
1517
1540
|
const a = s ? `${s} ` : "";
|
|
1518
1541
|
let c = e.action ? `[SILENT_HOLD] ${a}${a ? "started an action that" : "an action"} held ${o} for ` + `${i}ms${r} and the screen showed nothing for the whole round-trip: no optimistic ` + `value, no isPending() reader, no affects() mark, and no effect ran while it was held. ` + `Pair the action with a createOptimistic/createOptimisticStore write for the expected ` + `outcome (it reverts on failure), or co-write a createOptimistic(false) "saving" flag ` + `the UI reads.` : `[SILENT_HOLD] ${a}${a ? "wrote" : "writes to"} ${o}${a ? "; the write was" : " were"} ` + `held ${i}ms${r} and the screen showed nothing for the wait: no ` + `isPending()/latest() reader downstream, no optimistic value, no affects() mark, and no ` + `effect ran while it was held — the interaction was dead for ${i}ms. Show the wait: ` + `read isPending(() => ${e.blockers[0] ?? "source"}()) to render a busy state, or ` + `latest(${e.heldWrites[0].name}) to reveal the new input immediately while the data ` + `catches up. The hold itself is correct — do not "fix" this by moving the write off the ` + `async path.`;
|
|
1519
|
-
const
|
|
1520
|
-
if (
|
|
1521
|
-
const
|
|
1542
|
+
const f = isLongHold(e);
|
|
1543
|
+
if (f) c += ` ${boundaryRepair(e)}`;
|
|
1544
|
+
const d = e.holdMs >= t.warnMs ? "warn" : "info";
|
|
1522
1545
|
const u = holdData(e);
|
|
1523
|
-
u.long =
|
|
1546
|
+
u.long = f;
|
|
1524
1547
|
emitDiagnostic({
|
|
1525
1548
|
code: "SILENT_HOLD",
|
|
1526
1549
|
kind: "responsiveness",
|
|
1527
|
-
severity:
|
|
1550
|
+
severity: d,
|
|
1528
1551
|
message: c,
|
|
1529
1552
|
nodeName: nodeName(n),
|
|
1530
1553
|
data: u
|
|
@@ -1541,8 +1564,8 @@ function checkLongHold(e, n) {
|
|
|
1541
1564
|
const s = holdActor(e);
|
|
1542
1565
|
const a = s ? `${s} ` : "";
|
|
1543
1566
|
const c = e.acknowledgements.length > 0 ? `${e.acknowledgements.map(e => `"${e.kind}:${e.source}"`).join(", ")} said it was pending` : `an effect painted meanwhile`;
|
|
1544
|
-
const
|
|
1545
|
-
const
|
|
1567
|
+
const f = e.tailMs < e.holdMs - 1 ? ` after the last input (${e.holdMs.toFixed(0)}ms in all)` : "";
|
|
1568
|
+
const d = `[LONG_HOLD] ${a}${a ? "wrote" : "writes to"} ${o}; the screen kept the old ` + `content for ${i}ms${f}${r} — ${c}, but the hold ran on well past ` + `the point where "loading" over stale content reads as broken. ${boundaryRepair(e)}`;
|
|
1546
1569
|
const u = e.tailMs >= t.warnMs ? "warn" : "info";
|
|
1547
1570
|
const l = holdData(e);
|
|
1548
1571
|
l.acknowledgements = e.acknowledgements;
|
|
@@ -1550,7 +1573,7 @@ function checkLongHold(e, n) {
|
|
|
1550
1573
|
code: "LONG_HOLD",
|
|
1551
1574
|
kind: "responsiveness",
|
|
1552
1575
|
severity: u,
|
|
1553
|
-
message:
|
|
1576
|
+
message: d,
|
|
1554
1577
|
nodeName: nodeName(n),
|
|
1555
1578
|
data: l
|
|
1556
1579
|
}, n);
|
|
@@ -1661,8 +1684,8 @@ function closeNavigation(e) {
|
|
|
1661
1684
|
|
|
1662
1685
|
/** holdStart: `t`'s staged writes are parked — their navigations settle with `t`. */ function markNavigationsHeld(e) {
|
|
1663
1686
|
if (openNavs.size === 0) return;
|
|
1664
|
-
for (const n of e.
|
|
1665
|
-
const e = n
|
|
1687
|
+
for (const n of e.sn) {
|
|
1688
|
+
const e = n.ke?.origin;
|
|
1666
1689
|
if (e?.kind !== "navigation") continue;
|
|
1667
1690
|
const t = navStates.get(e);
|
|
1668
1691
|
if (t !== undefined) t.held = true;
|
|
@@ -1671,8 +1694,8 @@ function closeNavigation(e) {
|
|
|
1671
1694
|
|
|
1672
1695
|
/** transitionSettled: `t` commits — the navigations whose writes it staged are done. */ function settleNavigations(e, n) {
|
|
1673
1696
|
if (openNavs.size === 0) return;
|
|
1674
|
-
for (const t of e.
|
|
1675
|
-
const e = t
|
|
1697
|
+
for (const t of e.sn) {
|
|
1698
|
+
const e = t.ke?.origin;
|
|
1676
1699
|
if (e?.kind !== "navigation") continue;
|
|
1677
1700
|
const i = navStates.get(e);
|
|
1678
1701
|
if (i === undefined || !openNavs.has(i)) continue;
|
|
@@ -1696,7 +1719,7 @@ function settleNavigation(e, n, t) {
|
|
|
1696
1719
|
i.settledMs = now() - i.at;
|
|
1697
1720
|
i.outcome = n;
|
|
1698
1721
|
if (t !== undefined) i.hold = t;
|
|
1699
|
-
|
|
1722
|
+
for (const e of folds) e.navigation?.(i);
|
|
1700
1723
|
emitRecord("navigation", i);
|
|
1701
1724
|
// The interaction that performed it may have been waiting only on this.
|
|
1702
1725
|
const o = openInteractionOf(i.interaction);
|
|
@@ -1728,7 +1751,8 @@ function openInteraction(e) {
|
|
|
1728
1751
|
open: true,
|
|
1729
1752
|
writeDrain: drainSeq,
|
|
1730
1753
|
heldIn: new Set,
|
|
1731
|
-
held: false
|
|
1754
|
+
held: false,
|
|
1755
|
+
excludedWrites: 0
|
|
1732
1756
|
};
|
|
1733
1757
|
interactionStates.set(e, t);
|
|
1734
1758
|
openInteractions.add(t);
|
|
@@ -1752,11 +1776,15 @@ function openInteraction(e) {
|
|
|
1752
1776
|
return t !== undefined && openInteractions.has(t) ? t : undefined;
|
|
1753
1777
|
}
|
|
1754
1778
|
|
|
1755
|
-
/** stampWrite: a root write stamped `origin`. */ function noteInteractionWrite(e) {
|
|
1756
|
-
const
|
|
1757
|
-
if (
|
|
1758
|
-
n
|
|
1759
|
-
|
|
1779
|
+
/** stampWrite: a root write stamped `origin`. */ function noteInteractionWrite(e, n) {
|
|
1780
|
+
const t = openInteractionOf(e);
|
|
1781
|
+
if (t === undefined) return;
|
|
1782
|
+
if (n) {
|
|
1783
|
+
t.excludedWrites++;
|
|
1784
|
+
return;
|
|
1785
|
+
}
|
|
1786
|
+
t.event.writes++;
|
|
1787
|
+
t.writeDrain = drainSeq;
|
|
1760
1788
|
}
|
|
1761
1789
|
|
|
1762
1790
|
/** recordRerun / a create run: work attributed to the interaction. */ function noteInteractionRun(e, n, t) {
|
|
@@ -1773,8 +1801,8 @@ function openInteraction(e) {
|
|
|
1773
1801
|
|
|
1774
1802
|
/** holdStart: `t` parked writes — the interactions that performed them wait for `t`. */ function markInteractionsHeld(e) {
|
|
1775
1803
|
if (openInteractions.size === 0) return;
|
|
1776
|
-
for (const n of e.
|
|
1777
|
-
const t = openInteractionOf(n
|
|
1804
|
+
for (const n of e.sn) {
|
|
1805
|
+
const t = openInteractionOf(n.ke?.origin);
|
|
1778
1806
|
if (t !== undefined) {
|
|
1779
1807
|
t.heldIn.add(e);
|
|
1780
1808
|
t.held = true;
|
|
@@ -1804,6 +1832,14 @@ function maybeSettleInteraction(e, n = now()) {
|
|
|
1804
1832
|
if (t.writes > 0 && drainSeq <= e.writeDrain) return;
|
|
1805
1833
|
for (const e of t.navigations) if (e.outcome === undefined) return;
|
|
1806
1834
|
openInteractions.delete(e);
|
|
1835
|
+
// Every write went to an excluded subject and nothing of the app's ran: the
|
|
1836
|
+
// click was on the observer's own UI (a devtools panel's button). Not a
|
|
1837
|
+
// fact about the app — forget it rather than report a dead interaction.
|
|
1838
|
+
if (t.writes === 0 && e.excludedWrites > 0 && t.runs === 0 && t.created === 0) {
|
|
1839
|
+
const e = interactionLog.indexOf(t);
|
|
1840
|
+
if (e !== -1) interactionLog.splice(e, 1);
|
|
1841
|
+
return;
|
|
1842
|
+
}
|
|
1807
1843
|
t.settledMs = n - t.at;
|
|
1808
1844
|
t.outcome = t.writes === 0 ? "idle" : e.held ? "held" : "committed";
|
|
1809
1845
|
emitRecord("interaction", t);
|
|
@@ -1818,239 +1854,10 @@ function maybeSettleInteraction(e, n = now()) {
|
|
|
1818
1854
|
return n;
|
|
1819
1855
|
}
|
|
1820
1856
|
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
const feedbackInteractions = new Map;
|
|
1824
|
-
|
|
1825
|
-
const feedbackNavigations = new Map;
|
|
1826
|
-
|
|
1827
|
-
const flightStats = new Map;
|
|
1828
|
-
|
|
1829
|
-
const fallbackStats = new Map;
|
|
1830
|
-
|
|
1831
|
-
const FALLBACK_FLASH_MS = 150;
|
|
1832
|
-
|
|
1833
|
-
function flightBucket(e) {
|
|
1834
|
-
let n = flightStats.get(e);
|
|
1835
|
-
if (n === undefined) {
|
|
1836
|
-
n = {
|
|
1837
|
-
source: nodeName(e),
|
|
1838
|
-
flights: 0,
|
|
1839
|
-
landed: 0,
|
|
1840
|
-
abandoned: 0,
|
|
1841
|
-
landedMs: 0,
|
|
1842
|
-
worstMs: 0
|
|
1843
|
-
};
|
|
1844
|
-
flightStats.set(e, n);
|
|
1845
|
-
}
|
|
1846
|
-
return n;
|
|
1847
|
-
}
|
|
1848
|
-
|
|
1849
|
-
function trackFallback(e, n, t) {
|
|
1850
|
-
let i = fallbackStats.get(e);
|
|
1851
|
-
if (i === undefined) {
|
|
1852
|
-
i = {
|
|
1853
|
-
row: {
|
|
1854
|
-
boundary: "boundary",
|
|
1855
|
-
shows: 0,
|
|
1856
|
-
shownMs: 0,
|
|
1857
|
-
worstMs: 0,
|
|
1858
|
-
flashes: 0
|
|
1859
|
-
},
|
|
1860
|
-
shownAt: null
|
|
1861
|
-
};
|
|
1862
|
-
fallbackStats.set(e, i);
|
|
1863
|
-
}
|
|
1864
|
-
// The first show can fire before the subtree exists; name on first sight.
|
|
1865
|
-
if (i.row.boundary === "boundary" && n !== undefined) {
|
|
1866
|
-
const e = ownerPath(n);
|
|
1867
|
-
if (e !== undefined) i.row.boundary = e.join(" › ");
|
|
1868
|
-
}
|
|
1869
|
-
if (t) {
|
|
1870
|
-
if (i.shownAt === null) {
|
|
1871
|
-
i.shownAt = now();
|
|
1872
|
-
i.row.shows++;
|
|
1873
|
-
}
|
|
1874
|
-
return;
|
|
1875
|
-
}
|
|
1876
|
-
if (i.shownAt === null) return;
|
|
1877
|
-
const o = now() - i.shownAt;
|
|
1878
|
-
i.shownAt = null;
|
|
1879
|
-
i.row.shownMs += o;
|
|
1880
|
-
if (o > i.row.worstMs) i.row.worstMs = o;
|
|
1881
|
-
if (o < FALLBACK_FLASH_MS) i.row.flashes++;
|
|
1882
|
-
}
|
|
1883
|
-
|
|
1884
|
-
/** No affordance answered and nothing painted while held. */ function isSilentHold(e) {
|
|
1857
|
+
/** @internal No affordance answered and nothing painted while held. */ function isSilentHold(e) {
|
|
1885
1858
|
return e.paintedDuringHold === 0 && e.acknowledgements.length === 0;
|
|
1886
1859
|
}
|
|
1887
1860
|
|
|
1888
|
-
function interactionBucket(e) {
|
|
1889
|
-
const n = formatOrigin(e);
|
|
1890
|
-
let t = feedbackInteractions.get(n);
|
|
1891
|
-
if (t === undefined) {
|
|
1892
|
-
t = {
|
|
1893
|
-
row: {
|
|
1894
|
-
interaction: n,
|
|
1895
|
-
dispatches: 0,
|
|
1896
|
-
runs: 0,
|
|
1897
|
-
selfMs: 0,
|
|
1898
|
-
worstDispatchMs: 0,
|
|
1899
|
-
holds: 0,
|
|
1900
|
-
heldMs: 0,
|
|
1901
|
-
silentMs: 0,
|
|
1902
|
-
worstHoldMs: 0
|
|
1903
|
-
},
|
|
1904
|
-
dispatches: new Map
|
|
1905
|
-
};
|
|
1906
|
-
feedbackInteractions.set(n, t);
|
|
1907
|
-
}
|
|
1908
|
-
const i = e.at ?? 0;
|
|
1909
|
-
if (!t.dispatches.has(i)) {
|
|
1910
|
-
t.dispatches.set(i, 0);
|
|
1911
|
-
t.row.dispatches++;
|
|
1912
|
-
}
|
|
1913
|
-
return t;
|
|
1914
|
-
}
|
|
1915
|
-
|
|
1916
|
-
function recordFeedbackRun(e) {
|
|
1917
|
-
if (e.interaction === undefined) return;
|
|
1918
|
-
const n = interactionBucket(e.interaction);
|
|
1919
|
-
n.row.runs++;
|
|
1920
|
-
n.row.selfMs += e.selfMs;
|
|
1921
|
-
const t = e.interaction.at ?? 0;
|
|
1922
|
-
const i = n.dispatches.get(t) + e.selfMs;
|
|
1923
|
-
n.dispatches.set(t, i);
|
|
1924
|
-
if (i > n.row.worstDispatchMs) n.row.worstDispatchMs = i;
|
|
1925
|
-
}
|
|
1926
|
-
|
|
1927
|
-
function recordFeedbackHold(e) {
|
|
1928
|
-
const n = [ ...e.blockers ].sort();
|
|
1929
|
-
const t = n.join("\0");
|
|
1930
|
-
let i = feedbackSources.get(t);
|
|
1931
|
-
if (i === undefined) {
|
|
1932
|
-
i = {
|
|
1933
|
-
row: {
|
|
1934
|
-
sources: n,
|
|
1935
|
-
holds: 0,
|
|
1936
|
-
heldMs: 0,
|
|
1937
|
-
worstMs: 0,
|
|
1938
|
-
silent: 0,
|
|
1939
|
-
silentMs: 0,
|
|
1940
|
-
latestOnly: 0,
|
|
1941
|
-
long: 0,
|
|
1942
|
-
longMs: 0,
|
|
1943
|
-
acknowledgedBy: [],
|
|
1944
|
-
interactions: [],
|
|
1945
|
-
writes: [],
|
|
1946
|
-
actions: 0
|
|
1947
|
-
},
|
|
1948
|
-
acks: new Map,
|
|
1949
|
-
interactions: new Map,
|
|
1950
|
-
writes: new Set
|
|
1951
|
-
};
|
|
1952
|
-
feedbackSources.set(t, i);
|
|
1953
|
-
}
|
|
1954
|
-
const o = i.row;
|
|
1955
|
-
const r = isSilentHold(e);
|
|
1956
|
-
o.holds++;
|
|
1957
|
-
o.heldMs += e.holdMs;
|
|
1958
|
-
if (e.holdMs > o.worstMs) o.worstMs = e.holdMs;
|
|
1959
|
-
if (r) {
|
|
1960
|
-
o.silent++;
|
|
1961
|
-
o.silentMs += e.holdMs;
|
|
1962
|
-
} else if (e.acknowledgements.length > 0 && e.acknowledgements.every(e => e.kind === "latest")) o.latestOnly++;
|
|
1963
|
-
if (isLongHold(e)) {
|
|
1964
|
-
o.long++;
|
|
1965
|
-
o.longMs += e.tailMs;
|
|
1966
|
-
}
|
|
1967
|
-
if (e.action) o.actions++;
|
|
1968
|
-
for (const n of e.acknowledgements) {
|
|
1969
|
-
const e = `${n.kind}:${n.source}`;
|
|
1970
|
-
i.acks.set(e, (i.acks.get(e) ?? 0) + 1);
|
|
1971
|
-
}
|
|
1972
|
-
for (const n of e.heldWrites) i.writes.add(n.name);
|
|
1973
|
-
if (e.interaction !== undefined) {
|
|
1974
|
-
const n = formatOrigin(e.interaction);
|
|
1975
|
-
i.interactions.set(n, (i.interactions.get(n) ?? 0) + 1);
|
|
1976
|
-
const t = interactionBucket(e.interaction);
|
|
1977
|
-
t.row.holds++;
|
|
1978
|
-
t.row.heldMs += e.holdMs;
|
|
1979
|
-
if (r) t.row.silentMs += e.holdMs;
|
|
1980
|
-
if (e.holdMs > t.row.worstHoldMs) t.row.worstHoldMs = e.holdMs;
|
|
1981
|
-
}
|
|
1982
|
-
}
|
|
1983
|
-
|
|
1984
|
-
function recordFeedbackNavigation(e) {
|
|
1985
|
-
const n = e.name ?? e.to;
|
|
1986
|
-
if (n === undefined) return;
|
|
1987
|
-
let t = feedbackNavigations.get(n);
|
|
1988
|
-
if (t === undefined) {
|
|
1989
|
-
t = {
|
|
1990
|
-
name: n,
|
|
1991
|
-
navigations: 0,
|
|
1992
|
-
settledMs: 0,
|
|
1993
|
-
worstMs: 0,
|
|
1994
|
-
held: 0,
|
|
1995
|
-
heldMs: 0,
|
|
1996
|
-
silent: 0,
|
|
1997
|
-
superseded: 0,
|
|
1998
|
-
redirected: 0
|
|
1999
|
-
};
|
|
2000
|
-
feedbackNavigations.set(n, t);
|
|
2001
|
-
}
|
|
2002
|
-
t.navigations++;
|
|
2003
|
-
if (e.redirects !== undefined) t.redirected++;
|
|
2004
|
-
if (e.outcome === "superseded") {
|
|
2005
|
-
t.superseded++;
|
|
2006
|
-
return;
|
|
2007
|
-
}
|
|
2008
|
-
const i = e.settledMs;
|
|
2009
|
-
t.settledMs += i;
|
|
2010
|
-
if (i > t.worstMs) t.worstMs = i;
|
|
2011
|
-
if (e.outcome === "held") {
|
|
2012
|
-
t.held++;
|
|
2013
|
-
t.heldMs += e.hold?.holdMs ?? i;
|
|
2014
|
-
if (e.hold !== undefined && isSilentHold(e.hold)) t.silent++;
|
|
2015
|
-
}
|
|
2016
|
-
}
|
|
2017
|
-
|
|
2018
|
-
function rankedCounts(e, n) {
|
|
2019
|
-
return [ ...e ].sort((e, n) => n[1] - e[1]).map(([e, t]) => ({
|
|
2020
|
-
[n]: e,
|
|
2021
|
-
holds: t
|
|
2022
|
-
}));
|
|
2023
|
-
}
|
|
2024
|
-
|
|
2025
|
-
function feedbackTables() {
|
|
2026
|
-
const e = [ ...feedbackNavigations.values() ].map(e => ({
|
|
2027
|
-
...e
|
|
2028
|
-
})).sort((e, n) => n.heldMs - e.heldMs || n.settledMs - e.settledMs);
|
|
2029
|
-
const n = [ ...flightStats.values() ].map(e => ({
|
|
2030
|
-
...e
|
|
2031
|
-
})).sort((e, n) => n.abandoned - e.abandoned || n.flights - e.flights);
|
|
2032
|
-
const t = [ ...fallbackStats.values() ].map(e => ({
|
|
2033
|
-
...e.row
|
|
2034
|
-
})).sort((e, n) => n.flashes - e.flashes || n.shownMs - e.shownMs);
|
|
2035
|
-
const i = [ ...feedbackSources.values() ].map(e => ({
|
|
2036
|
-
...e.row,
|
|
2037
|
-
acknowledgedBy: rankedCounts(e.acks, "by"),
|
|
2038
|
-
interactions: rankedCounts(e.interactions, "interaction"),
|
|
2039
|
-
writes: [ ...e.writes ]
|
|
2040
|
-
})).sort((e, n) => n.silentMs - e.silentMs || n.heldMs - e.heldMs);
|
|
2041
|
-
// Ranked by the total time the user spent on it: held plus synchronous work.
|
|
2042
|
-
const o = [ ...feedbackInteractions.values() ].map(e => ({
|
|
2043
|
-
...e.row
|
|
2044
|
-
})).sort((e, n) => n.heldMs + n.selfMs - (e.heldMs + e.selfMs));
|
|
2045
|
-
return {
|
|
2046
|
-
sources: i,
|
|
2047
|
-
interactions: o,
|
|
2048
|
-
navigations: e,
|
|
2049
|
-
flights: n,
|
|
2050
|
-
fallbacks: t
|
|
2051
|
-
};
|
|
2052
|
-
}
|
|
2053
|
-
|
|
2054
1861
|
// The engine's implementation of the core's dev hook points. Installed by
|
|
2055
1862
|
// enable(), uninstalled by disable() — while uninstalled the core pays one
|
|
2056
1863
|
// null check per site and nothing else.
|
|
@@ -2077,7 +1884,7 @@ const engineHooks = {
|
|
|
2077
1884
|
prevDeps: n ? null : captureDeps(e),
|
|
2078
1885
|
// Mirror recompute's own prev-value resolution: an earlier run in the
|
|
2079
1886
|
// same flush may still be holding in _pendingValue.
|
|
2080
|
-
prevValue: e.
|
|
1887
|
+
prevValue: e._e !== NOT_PENDING ? e._e : e.Ae,
|
|
2081
1888
|
// A create run inherits the interaction of whatever is building it: the
|
|
2082
1889
|
// enclosing recompute (a parent's fn creating children) or, at the top
|
|
2083
1890
|
// of the recompute stack, the effect callback / handler frame.
|
|
@@ -2103,8 +1910,8 @@ const engineHooks = {
|
|
|
2103
1910
|
// committed compute output is an unchanged run. `undefined` outputs are
|
|
2104
1911
|
// exempt — a side-effect-only compute's work IS its effect phase, and
|
|
2105
1912
|
// identity of `undefined` proves nothing.
|
|
2106
|
-
if (t && s.causes !== null && e.
|
|
2107
|
-
const n = e.
|
|
1913
|
+
if (t && s.causes !== null && e.Pe) {
|
|
1914
|
+
const n = e._e !== NOT_PENDING ? e._e : e.Ae;
|
|
2108
1915
|
if (n !== undefined && n === s.prevValue) t = false;
|
|
2109
1916
|
}
|
|
2110
1917
|
// Unstable-output check: memos only, non-create, plain runs with a
|
|
@@ -2112,7 +1919,7 @@ const engineHooks = {
|
|
|
2112
1919
|
// plain-flush memo commits and in `_value` for direct ones. Overlay runs
|
|
2113
1920
|
// are excluded — an optimistic re-derive legitimately produces fresh
|
|
2114
1921
|
// equivalents while the lane settles.
|
|
2115
|
-
if (s.causes !== null && t && !i && !o && !e.
|
|
1922
|
+
if (s.causes !== null && t && !i && !o && !e.Pe) checkUnstableOutput(e, s.prevValue, e._e !== NOT_PENDING ? e._e : e.Ae);
|
|
2116
1923
|
if (s.causes !== null) recordRerun(e, s, {
|
|
2117
1924
|
selfMs: c,
|
|
2118
1925
|
totalMs: a
|
|
@@ -2135,9 +1942,9 @@ const engineHooks = {
|
|
|
2135
1942
|
trackFlightStart(e, n);
|
|
2136
1943
|
},
|
|
2137
1944
|
asyncStart(e) {
|
|
2138
|
-
asyncStartSeq = e
|
|
2139
|
-
asyncStartTime = e.
|
|
2140
|
-
asyncStartValue = e.
|
|
1945
|
+
asyncStartSeq = e.ke?.seq ?? 0;
|
|
1946
|
+
asyncStartTime = e.fe;
|
|
1947
|
+
asyncStartValue = e.Ae;
|
|
2141
1948
|
},
|
|
2142
1949
|
asyncEnd(e, n, t, i) {
|
|
2143
1950
|
if (i) {
|
|
@@ -2147,7 +1954,7 @@ const engineHooks = {
|
|
|
2147
1954
|
// (or `_time`, for a same-reference commit under `equals: false`), and
|
|
2148
1955
|
// a transition hold parks the value in `_pendingValue`. A landing the
|
|
2149
1956
|
// equality gate swallowed moves none of them and must leave no stamp.
|
|
2150
|
-
const i = e.
|
|
1957
|
+
const i = e.Ae !== asyncStartValue || e.fe !== asyncStartTime || e._e === t;
|
|
2151
1958
|
if (i) stampWrite(e, "async", n === undefined ? NO_VALUES : n, t);
|
|
2152
1959
|
// Flight over either way — an equality-swallowed landing still spent
|
|
2153
1960
|
// the wall time (finalizeFlight only chains through a fresh stamp).
|
|
@@ -2157,12 +1964,12 @@ const engineHooks = {
|
|
|
2157
1964
|
// Landed through setSignal: reclassify its "write" stamp as an async
|
|
2158
1965
|
// landing — but only if it actually stamped (the value changed) since
|
|
2159
1966
|
// asyncStart; a no-change landing must leave no fresh stamp behind.
|
|
2160
|
-
const o = e
|
|
1967
|
+
const o = e.ke;
|
|
2161
1968
|
if (o !== undefined && o.seq > asyncStartSeq && o.kind === "write") stampWrite(e, "async", NO_VALUES, t);
|
|
2162
1969
|
finalizeFlight(e);
|
|
2163
1970
|
},
|
|
2164
1971
|
effectRunStart(e) {
|
|
2165
|
-
pushFrame("effect", nodeName(e), e.
|
|
1972
|
+
pushFrame("effect", nodeName(e), e.Ye, e);
|
|
2166
1973
|
},
|
|
2167
1974
|
effectRunEnd() {
|
|
2168
1975
|
popFrame("effect");
|
|
@@ -2194,14 +2001,17 @@ const engineHooks = {
|
|
|
2194
2001
|
transitionMerged(e, n) {
|
|
2195
2002
|
trackHoldMerge(e, n);
|
|
2196
2003
|
},
|
|
2197
|
-
storeReplaced(e, n, t, i, o) {
|
|
2198
|
-
checkImmutableUpdate(e, n, t, i, o);
|
|
2004
|
+
storeReplaced(e, n, t, i, o, r) {
|
|
2005
|
+
checkImmutableUpdate(e, n, t, i, o, r);
|
|
2199
2006
|
},
|
|
2200
2007
|
listChurn(e, n, t, i, o) {
|
|
2201
2008
|
checkListIdentity(e, n, t, i, o);
|
|
2202
2009
|
},
|
|
2203
2010
|
boundaryFallback(e, n, t) {
|
|
2204
|
-
|
|
2011
|
+
for (const i of folds) i.fallback?.(e, n, t);
|
|
2012
|
+
},
|
|
2013
|
+
currentOrigin() {
|
|
2014
|
+
return ambientOrigin();
|
|
2205
2015
|
}
|
|
2206
2016
|
};
|
|
2207
2017
|
|
|
@@ -2212,14 +2022,9 @@ const attribution = {
|
|
|
2212
2022
|
...e
|
|
2213
2023
|
};
|
|
2214
2024
|
frames.length = 0;
|
|
2215
|
-
scopeCosts.clear();
|
|
2216
|
-
writeCosts.clear();
|
|
2217
2025
|
waterfallLog = [];
|
|
2218
2026
|
holdLog = [];
|
|
2219
2027
|
activeHold = null;
|
|
2220
|
-
feedbackSources.clear();
|
|
2221
|
-
feedbackInteractions.clear();
|
|
2222
|
-
feedbackNavigations.clear();
|
|
2223
2028
|
navigationLog = [];
|
|
2224
2029
|
openNavs.clear();
|
|
2225
2030
|
interactionLog = [];
|
|
@@ -2228,26 +2033,20 @@ const attribution = {
|
|
|
2228
2033
|
reportedCycles.clear();
|
|
2229
2034
|
relays.clear();
|
|
2230
2035
|
immutableReported.clear();
|
|
2231
|
-
flightStats.clear();
|
|
2232
|
-
fallbackStats.clear();
|
|
2233
2036
|
originFrames.length = 0;
|
|
2234
2037
|
interactionStack.length = 0;
|
|
2235
2038
|
currentInteraction = null;
|
|
2236
2039
|
hotCauses.clear();
|
|
2040
|
+
for (const e of folds) e.reset?.();
|
|
2237
2041
|
setAttributionHooks(engineHooks);
|
|
2238
2042
|
},
|
|
2239
2043
|
disable() {
|
|
2240
2044
|
clearListeners();
|
|
2241
2045
|
history = [];
|
|
2242
2046
|
frames.length = 0;
|
|
2243
|
-
scopeCosts.clear();
|
|
2244
|
-
writeCosts.clear();
|
|
2245
2047
|
waterfallLog = [];
|
|
2246
2048
|
holdLog = [];
|
|
2247
2049
|
activeHold = null;
|
|
2248
|
-
feedbackSources.clear();
|
|
2249
|
-
feedbackInteractions.clear();
|
|
2250
|
-
feedbackNavigations.clear();
|
|
2251
2050
|
navigationLog = [];
|
|
2252
2051
|
openNavs.clear();
|
|
2253
2052
|
interactionLog = [];
|
|
@@ -2256,12 +2055,11 @@ const attribution = {
|
|
|
2256
2055
|
reportedCycles.clear();
|
|
2257
2056
|
relays.clear();
|
|
2258
2057
|
immutableReported.clear();
|
|
2259
|
-
flightStats.clear();
|
|
2260
|
-
fallbackStats.clear();
|
|
2261
2058
|
originFrames.length = 0;
|
|
2262
2059
|
interactionStack.length = 0;
|
|
2263
2060
|
currentInteraction = null;
|
|
2264
2061
|
hotCauses.clear();
|
|
2062
|
+
for (const e of folds) e.reset?.();
|
|
2265
2063
|
setAttributionHooks(null);
|
|
2266
2064
|
},
|
|
2267
2065
|
subscribe(e, n) {
|
|
@@ -2274,22 +2072,6 @@ const attribution = {
|
|
|
2274
2072
|
history() {
|
|
2275
2073
|
return history;
|
|
2276
2074
|
},
|
|
2277
|
-
why(e) {
|
|
2278
|
-
const n = e?.[$REFRESH] ?? e;
|
|
2279
|
-
return history.filter(e => e.node === n);
|
|
2280
|
-
},
|
|
2281
|
-
subscriptions(e) {
|
|
2282
|
-
const n = e?.[$REFRESH] ?? e;
|
|
2283
|
-
const t = [];
|
|
2284
|
-
for (let e = n?.te ?? null; e !== null; e = e.ne) t.push(nodeName(e.oe));
|
|
2285
|
-
return t;
|
|
2286
|
-
},
|
|
2287
|
-
costs() {
|
|
2288
|
-
return {
|
|
2289
|
-
scopes: [ ...scopeCosts.values() ].sort((e, n) => n.selfMs - e.selfMs),
|
|
2290
|
-
writes: [ ...writeCosts.values() ].sort((e, n) => n.downstreamMs - e.downstreamMs)
|
|
2291
|
-
};
|
|
2292
|
-
},
|
|
2293
2075
|
waterfalls() {
|
|
2294
2076
|
return waterfallLog;
|
|
2295
2077
|
},
|
|
@@ -2302,17 +2084,12 @@ const attribution = {
|
|
|
2302
2084
|
interactions() {
|
|
2303
2085
|
return interactionLog;
|
|
2304
2086
|
},
|
|
2305
|
-
feedback() {
|
|
2306
|
-
return feedbackTables();
|
|
2307
|
-
},
|
|
2308
2087
|
markFlight(e, n = now()) {
|
|
2309
2088
|
// Earliest wins: re-marking (a cache re-serving the same promise) must
|
|
2310
2089
|
// not move the origin later.
|
|
2311
2090
|
const t = flightOrigins.get(e);
|
|
2312
2091
|
if (t === undefined || n < t) flightOrigins.set(e, n);
|
|
2313
|
-
}
|
|
2314
|
-
format: formatRerun,
|
|
2315
|
-
formatOrigin: formatOrigin
|
|
2092
|
+
}
|
|
2316
2093
|
};
|
|
2317
2094
|
|
|
2318
|
-
export { attribution, formatOrigin, formatRerun };
|
|
2095
|
+
export { attribution, formatOrigin, formatRerun, isLongHold, isSilentHold, nodeName, now, registerFold, rootsOf };
|