@solidjs/signals 2.0.0-rc.15 → 2.0.0-rc.17
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 +2268 -1734
- package/dist/dev.attribution.js +75 -52
- package/dist/dev.js +1348 -658
- package/dist/observe/affects-marks.js +86 -0
- package/dist/observe/affects.js +22 -73
- package/dist/observe/boundaries.js +192 -190
- package/dist/observe/core/async.js +177 -131
- package/dist/observe/core/attribution-queries.js +1 -1
- package/dist/observe/core/attribution.js +176 -159
- package/dist/observe/core/constants.js +17 -3
- package/dist/observe/core/context.js +5 -3
- package/dist/observe/core/core.js +520 -364
- package/dist/observe/core/dev.js +66 -38
- package/dist/observe/core/effect.js +50 -48
- package/dist/observe/core/error.js +5 -3
- package/dist/observe/core/external.js +5 -6
- package/dist/observe/core/graph.js +60 -44
- package/dist/observe/core/heap.js +33 -33
- package/dist/observe/core/identities.js +62 -0
- package/dist/observe/core/lanes.js +238 -260
- package/dist/observe/core/owner.js +117 -79
- package/dist/observe/core/scheduler.js +381 -217
- package/dist/observe/core/truth.js +45 -0
- package/dist/observe/core/verdict.js +91 -50
- package/dist/observe/deferred.js +184 -0
- package/dist/observe/index.js +7 -1
- package/dist/observe/map.js +345 -260
- package/dist/observe/reveal.js +76 -76
- package/dist/observe/signals.js +47 -31
- package/dist/observe/store/affects.js +18 -18
- package/dist/observe/store/edges.js +39 -0
- package/dist/observe/store/optimistic.js +146 -100
- package/dist/observe/store/projection.js +68 -55
- package/dist/observe/store/store.js +180 -118
- package/dist/prod/affects-marks.js +86 -0
- package/dist/prod/affects.js +23 -72
- package/dist/prod/boundaries.js +156 -154
- package/dist/prod/core/async.js +173 -127
- package/dist/prod/core/constants.js +17 -3
- package/dist/prod/core/context.js +5 -3
- package/dist/prod/core/core.js +495 -343
- package/dist/prod/core/effect.js +70 -68
- package/dist/prod/core/error.js +5 -3
- package/dist/prod/core/external.js +5 -6
- package/dist/prod/core/graph.js +60 -44
- package/dist/prod/core/heap.js +38 -38
- package/dist/prod/core/identities.js +62 -0
- package/dist/prod/core/lanes.js +230 -252
- package/dist/prod/core/owner.js +114 -77
- package/dist/prod/core/scheduler.js +327 -163
- package/dist/prod/core/truth.js +45 -0
- package/dist/prod/core/verdict.js +90 -49
- package/dist/prod/deferred.js +182 -0
- package/dist/prod/index.js +7 -1
- package/dist/prod/map.js +360 -275
- package/dist/prod/reveal.js +76 -76
- package/dist/prod/signals.js +42 -26
- package/dist/prod/store/affects.js +6 -6
- package/dist/prod/store/edges.js +39 -0
- package/dist/prod/store/optimistic.js +143 -98
- package/dist/prod/store/projection.js +25 -19
- package/dist/prod/store/store.js +157 -100
- package/dist/types/affects-marks.d.ts +14 -0
- package/dist/types/boundaries.d.ts +1 -1
- package/dist/types/core/async.d.ts +10 -0
- package/dist/types/core/constants.d.ts +13 -1
- package/dist/types/core/core.d.ts +54 -5
- package/dist/types/core/dev.d.ts +51 -13
- package/dist/types/core/error.d.ts +9 -1
- package/dist/types/core/graph.d.ts +11 -0
- package/dist/types/core/identities.d.ts +17 -0
- package/dist/types/core/index.d.ts +2 -1
- package/dist/types/core/lanes.d.ts +2 -14
- package/dist/types/core/owner.d.ts +9 -3
- package/dist/types/core/scheduler.d.ts +70 -32
- package/dist/types/core/truth.d.ts +16 -0
- package/dist/types/core/types.d.ts +21 -0
- package/dist/types/core/verdict.d.ts +32 -0
- package/dist/types/deferred.d.ts +28 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/types/map.d.ts +2 -2
- package/dist/types/signals.d.ts +28 -7
- package/dist/types/store/edges.d.ts +9 -0
- package/dist/types/store/index.d.ts +10 -3
- package/dist/types/store/optimistic.d.ts +3 -2
- package/dist/types/store/projection.d.ts +4 -3
- package/dist/types/store/store.d.ts +1 -1
- package/dist/types/store/target.d.ts +16 -8
- package/dist/types/store/types.d.ts +14 -0
- package/package.json +1 -1
package/dist/dev-shared.js
CHANGED
|
@@ -23,18 +23,18 @@
|
|
|
23
23
|
* ```
|
|
24
24
|
*/
|
|
25
25
|
class NotReadyError extends Error {
|
|
26
|
-
source;
|
|
27
26
|
constructor(source) {
|
|
28
27
|
super();
|
|
29
28
|
this.source = source;
|
|
30
29
|
}
|
|
31
30
|
}
|
|
32
31
|
class StatusError extends Error {
|
|
33
|
-
source;
|
|
34
32
|
constructor(source, original) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
// Best-effort message: formatting must never replace the original (#4112).
|
|
34
|
+
try {
|
|
35
|
+
var message = String(original instanceof Error ? original.message : original);
|
|
36
|
+
} catch {}
|
|
37
|
+
super(message, { cause: original });
|
|
38
38
|
this.source = source;
|
|
39
39
|
}
|
|
40
40
|
}
|
|
@@ -61,11 +61,26 @@ class NoOwnerError extends Error {
|
|
|
61
61
|
}
|
|
62
62
|
class ContextNotFoundError extends Error {
|
|
63
63
|
constructor() {
|
|
64
|
-
super(
|
|
65
|
-
"Context must either be created with a default value or a value must be provided before accessing it."
|
|
66
|
-
);
|
|
64
|
+
super(contextNotFoundMessage(arguments[0], arguments[1]));
|
|
67
65
|
}
|
|
68
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Dev message for a context read with no value. `undefined` means "not
|
|
69
|
+
* provided" for context, from either side — no default, or a provider
|
|
70
|
+
* passing `undefined` — so the message says which happened and points at
|
|
71
|
+
* `null` as the explicit empty value.
|
|
72
|
+
*/
|
|
73
|
+
function contextNotFoundMessage(context, providedUndefined) {
|
|
74
|
+
if (!context)
|
|
75
|
+
return "Context must either be created with a default value or a value must be provided before accessing it.";
|
|
76
|
+
const description = context.id.description;
|
|
77
|
+
const name = description ? `Context "${description}"` : "Context";
|
|
78
|
+
if (providedUndefined)
|
|
79
|
+
return `${name} was provided with \`undefined\`, which counts as not provided: \`undefined\` means "no value" for context. To provide an empty value, create the context with a \`null\` default (\`createContext<T | null>(null)\`) and pass \`null\`.`;
|
|
80
|
+
if (context._undefinedDefault)
|
|
81
|
+
return `${name} has no provider above this read, and \`createContext(undefined)\` does not give it a default: \`undefined\` means "no value" for context. Mount a provider above this read, or use \`null\` as an explicit empty default: \`createContext<T | null>(null)\`.`;
|
|
82
|
+
return `${name} has no provider above this read and no default value. Mount a provider above this read, or create the context with a default; for an optional context, use \`null\` as the empty default (\`createContext<T | null>(null)\`), not \`undefined\`.`;
|
|
83
|
+
}
|
|
69
84
|
|
|
70
85
|
const REACTIVE_NONE = 0;
|
|
71
86
|
const REACTIVE_CHECK = 1 << 0;
|
|
@@ -191,6 +206,9 @@ const CONFIG_SLOT_NODE = 1 << 12;
|
|
|
191
206
|
* leaf twin of REACTIVE_MANUAL_WRITE). Set by the user setter, cleared by
|
|
192
207
|
* the derive's own write to the key. */
|
|
193
208
|
const CONFIG_MANUAL_WRITE = 1 << 18;
|
|
209
|
+
/** D-41: the staged value in `_pendingValue` is an error — an
|
|
210
|
+
* answer of the held frame, published at its landing like a value. */
|
|
211
|
+
const CONFIG_STAGED_ERROR = 1 << 21;
|
|
194
212
|
/** Lanes — the node carries a written guess (`optimisticWrite`): its own
|
|
195
213
|
* source recomputing it, or a plain write landing on it, is the truth
|
|
196
214
|
* (`supersede`). A lane's derived staging is not. */
|
|
@@ -199,15 +217,21 @@ const CONFIG_GUESS = 1 << 13;
|
|
|
199
217
|
* pending is a question for its next pass — the verdict changed — so the
|
|
200
218
|
* propagation re-derives it instead of marking it pending (as a kept-tail
|
|
201
219
|
* link does, A30). Set at the window, never cleared: a reader that stopped
|
|
202
|
-
* asking re-derives once and goes pending through its own read.
|
|
203
|
-
|
|
220
|
+
* asking re-derives once and goes pending through its own read. The same
|
|
221
|
+
* bit as REACTIVE_PROBED: a pass's probe (or a read of a verdict memo,
|
|
222
|
+
* core.ts `read`) carries into the config at its end without a shift. */
|
|
223
|
+
const CONFIG_VERDICT = 1 << 15;
|
|
204
224
|
/** A dependency's status is a question for this node's pass, not a fact
|
|
205
225
|
* about its current one: the propagation re-derives it instead of marking
|
|
206
226
|
* it, for errors as for pending (as CONFIG_VERDICT). A boundary's output
|
|
207
227
|
* (show the content, or the fallback) and its `on` node (boundaries.ts); a
|
|
208
228
|
* promise over an expression (`resolve`/`until`, signals.ts: the pass reads
|
|
209
229
|
* the error and rejects, or re-asks a pending source). */
|
|
210
|
-
const CONFIG_REDERIVE = 1 <<
|
|
230
|
+
const CONFIG_REDERIVE = 1 << 14;
|
|
231
|
+
/** An answered `createDeferred` node (deferred.ts): held because a
|
|
232
|
+
* downstream hold staged its landing, its re-pass is a lagging question,
|
|
233
|
+
* not an answer, and joins no transaction (D-40). */
|
|
234
|
+
const CONFIG_LAGS = 1 << 19;
|
|
211
235
|
/** The reader wants the truth, not the lane's guess (`until`, signals.ts):
|
|
212
236
|
* a read of displayed optimism serves the base the guess covers, makes the
|
|
213
237
|
* pass no lane's, and the truth landing wakes it even when it confirms the
|
|
@@ -259,9 +283,13 @@ const CONFIG_WIDE = 1 << 26;
|
|
|
259
283
|
* creation by dev builds only (the observe artifact mangles the option name,
|
|
260
284
|
* as it does `_wide`). */
|
|
261
285
|
const CONFIG_SUPERSEDES = 1 << 27;
|
|
286
|
+
const STATUS_NONE = 0;
|
|
262
287
|
const STATUS_PENDING = 1 << 0;
|
|
263
288
|
const STATUS_ERROR = 1 << 1;
|
|
264
289
|
const STATUS_UNINITIALIZED = 1 << 2;
|
|
290
|
+
/** `catchHook`'s answer when the catcher is a boundary re-armed by
|
|
291
|
+
* `on` (truthy, as any catch): what it collects is its own, not the hold's. */
|
|
292
|
+
const CAUGHT_REARMED = 2;
|
|
265
293
|
const EFFECT_RENDER = 1;
|
|
266
294
|
const EFFECT_USER = 2;
|
|
267
295
|
const EFFECT_TRACKED = 3;
|
|
@@ -410,13 +438,21 @@ const diagnostics = {
|
|
|
410
438
|
};
|
|
411
439
|
}
|
|
412
440
|
};
|
|
441
|
+
// Flight identity → earliest declared start (see `AttributionSlot.markFlight`).
|
|
442
|
+
// Weak: a stamp must not keep a settled promise alive.
|
|
443
|
+
const flightOrigins = new WeakMap();
|
|
413
444
|
const attributionSlot = {
|
|
414
445
|
get installed() {
|
|
415
446
|
return attrHooks;
|
|
416
447
|
},
|
|
417
448
|
withInteraction,
|
|
418
449
|
withOrigin,
|
|
419
|
-
currentOrigin
|
|
450
|
+
currentOrigin,
|
|
451
|
+
markFlight(flight, startedAt = performance.now()) {
|
|
452
|
+
// Earliest wins (`undefined <= n` is false: the first mark always lands).
|
|
453
|
+
if (!(flightOrigins.get(flight) <= startedAt)) flightOrigins.set(flight, startedAt);
|
|
454
|
+
},
|
|
455
|
+
flightOrigin: flight => flightOrigins.get(flight)
|
|
420
456
|
};
|
|
421
457
|
const RECORDS = Symbol.for("@solidjs/signals/observe/records");
|
|
422
458
|
function recordsChannel() {
|
|
@@ -518,10 +554,11 @@ function isSuppressed(entry) {
|
|
|
518
554
|
return suppressedEvents.has(entry);
|
|
519
555
|
}
|
|
520
556
|
// The repair guide: the `reactivity-diagnostics` skill `solid-js` ships,
|
|
521
|
-
// one
|
|
522
|
-
//
|
|
523
|
-
|
|
524
|
-
|
|
557
|
+
// one page per code on the docs site. The slug is the code lowercased with
|
|
558
|
+
// `_` → `-` (`STRICT_READ_UNTRACKED` → `strict-read-untracked`). The host
|
|
559
|
+
// flips to docs.solidjs.com at stable (one search-and-replace, with the JSDoc
|
|
560
|
+
// links).
|
|
561
|
+
const GUIDE_URL = "https://v2.solidjs.com/reference/solid-js/diagnostics/";
|
|
525
562
|
const DEV = {
|
|
526
563
|
hooks,
|
|
527
564
|
getChildren,
|
|
@@ -531,7 +568,7 @@ const DEV = {
|
|
|
531
568
|
getObservers,
|
|
532
569
|
report: reportDiagnostic,
|
|
533
570
|
guideUrl(code) {
|
|
534
|
-
return
|
|
571
|
+
return GUIDE_URL + code.toLowerCase().replace(/_/g, "-");
|
|
535
572
|
}
|
|
536
573
|
};
|
|
537
574
|
/**
|
|
@@ -570,15 +607,29 @@ function ownerPath(subject) {
|
|
|
570
607
|
}
|
|
571
608
|
return path.length ? path.reverse() : undefined;
|
|
572
609
|
}
|
|
610
|
+
/**
|
|
611
|
+
* The innermost source location on the subject's chain: the subject's own
|
|
612
|
+
* `_loc` (a signal or computation created with `{ loc }`), else the first
|
|
613
|
+
* `_loc` up the owners — a signal hops to its registering owner, then
|
|
614
|
+
* `_parent` upward (a component root carries its JSX site). The same walk
|
|
615
|
+
* `ownerPath` makes; `undefined` when nothing on the chain was located.
|
|
616
|
+
*/
|
|
617
|
+
function locationOf(subject) {
|
|
618
|
+
// Owners have a `_parent` key (null on a root); a signal has none and hops
|
|
619
|
+
// to its registering `_owner` instead.
|
|
620
|
+
for (let node = subject; node; node = node._parent !== undefined ? node._parent : node._owner)
|
|
621
|
+
if (node._loc) return node._loc;
|
|
622
|
+
return undefined;
|
|
623
|
+
}
|
|
573
624
|
/**
|
|
574
625
|
* Records a diagnostic on the structured channel (listeners, captures) and
|
|
575
626
|
* returns the entry. `subject` locates it: the current reactive `context` by
|
|
576
627
|
* default (right for the synchronous rule checks — they fire inside the
|
|
577
628
|
* scope that misbehaved); pass the node for scheduler-time findings whose
|
|
578
629
|
* ambient context is the flush, or `null` for events that have no location
|
|
579
|
-
* by nature. An `ownerPath` already on the event wins over the
|
|
580
|
-
* (hosts whose owners are not signals' owners compute their
|
|
581
|
-
* output is a separate, dev-tier step — see `reportDiagnostic`.
|
|
630
|
+
* by nature. An `ownerPath` (or `loc`) already on the event wins over the
|
|
631
|
+
* subject walk (hosts whose owners are not signals' owners compute their
|
|
632
|
+
* own). Console output is a separate, dev-tier step — see `reportDiagnostic`.
|
|
582
633
|
*/
|
|
583
634
|
function emitDiagnostic(event, subject = context) {
|
|
584
635
|
const entry = {
|
|
@@ -595,6 +646,10 @@ function emitDiagnostic(event, subject = context) {
|
|
|
595
646
|
const path = ownerPath(subject);
|
|
596
647
|
if (path) entry.ownerPath = path;
|
|
597
648
|
}
|
|
649
|
+
if (entry.loc === undefined) {
|
|
650
|
+
const loc = locationOf(subject);
|
|
651
|
+
if (loc !== undefined) entry.loc = loc;
|
|
652
|
+
}
|
|
598
653
|
const live = subject ?? undefined;
|
|
599
654
|
if (live !== undefined) eventSubjects.set(entry, live);
|
|
600
655
|
for (const listener of diagnosticListeners) listener(entry, live);
|
|
@@ -631,12 +686,44 @@ function takeFooter(entry) {
|
|
|
631
686
|
* `reportDiagnostic(emitDiagnostic(…))`. Weak, keyed by the entry.
|
|
632
687
|
*/
|
|
633
688
|
const eventSubjects = new WeakMap();
|
|
689
|
+
/**
|
|
690
|
+
* The `at` line of a console report: the event's `loc` as the compiler wrote
|
|
691
|
+
* it (`src/App.tsx:18:3`), or — in a browser, where a dev server serves the
|
|
692
|
+
* project root at `location.origin` — `http://localhost:5173/src/App.tsx:18:3`,
|
|
693
|
+
* which Chrome and Firefox link to the Sources panel. A path that leaves the
|
|
694
|
+
* root (`../`) or is absolute is not under the origin and prints as-is.
|
|
695
|
+
*/
|
|
696
|
+
function locationLine(loc) {
|
|
697
|
+
if (
|
|
698
|
+
typeof document !== "undefined" &&
|
|
699
|
+
typeof location !== "undefined" &&
|
|
700
|
+
!loc.startsWith("..") &&
|
|
701
|
+
!loc.startsWith("/")
|
|
702
|
+
)
|
|
703
|
+
return `${location.origin}/${loc}`;
|
|
704
|
+
return loc;
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* The locating lines of a console report: `\n in <App> › <TodoRow> › effect`
|
|
708
|
+
* (the owner path) and `\n at src/App.tsx:18:3` (see `locationLine`), each
|
|
709
|
+
* when the entry carries it; `""` when it carries neither. Shared by
|
|
710
|
+
* `reportDiagnostic` and the sites that print their own console entry (the
|
|
711
|
+
* halt, which logs its cause beside the message).
|
|
712
|
+
*/
|
|
713
|
+
function locationLines(entry) {
|
|
714
|
+
let text = "";
|
|
715
|
+
if (entry.ownerPath) text += `\n in ${entry.ownerPath.join(" › ")}`;
|
|
716
|
+
if (entry.loc) text += `\n at ${locationLine(entry.loc)}`;
|
|
717
|
+
return text;
|
|
718
|
+
}
|
|
634
719
|
/**
|
|
635
720
|
* The console face of a diagnostic — ONE entry per finding: the message, the
|
|
636
721
|
* owner path (`in <App> › <TodoRow> › effect`) so a human can locate it, the
|
|
637
|
-
*
|
|
638
|
-
*
|
|
639
|
-
*
|
|
722
|
+
* source location (`at src/App.tsx:18:3` — see `locationLine`) when a node on
|
|
723
|
+
* the chain was compiled with one, the once-per-code footer as trailing
|
|
724
|
+
* lines, and — when the subject is a binding effect the rendering runtime
|
|
725
|
+
* tagged — the element it writes, as a second console argument (hover
|
|
726
|
+
* highlights it, click jumps to Elements).
|
|
640
727
|
* Severity picks the console method. Call sites report the entry
|
|
641
728
|
* `emitDiagnostic` returned so the structured and console channels never
|
|
642
729
|
* disagree. Dev-tier: in an observe build this is a no-op, so wiring paths
|
|
@@ -645,8 +732,7 @@ const eventSubjects = new WeakMap();
|
|
|
645
732
|
*/
|
|
646
733
|
function reportDiagnostic(entry) {
|
|
647
734
|
if (suppressedEvents.has(entry)) return;
|
|
648
|
-
let text = entry.message;
|
|
649
|
-
if (entry.ownerPath) text += `\n in ${entry.ownerPath.join(" › ")}`;
|
|
735
|
+
let text = entry.message + locationLines(entry);
|
|
650
736
|
const footer = takeFooter(entry);
|
|
651
737
|
if (footer) text += `\n${footer}`;
|
|
652
738
|
const element = eventSubjects.get(entry)?._devElement;
|
|
@@ -929,9 +1015,10 @@ function checkPostAwaitRead(dep, holder, key, nodeName, throwsPending) {
|
|
|
929
1015
|
if (callbackDepth !== 0) return;
|
|
930
1016
|
if (disposalDepth !== 0) {
|
|
931
1017
|
// Teardown is synchronous, so a depth still raised on the next microtask
|
|
932
|
-
// was left behind by a
|
|
933
|
-
// try/finally, which would survive into prod
|
|
934
|
-
// later check would stay
|
|
1018
|
+
// was left behind by a throw that escaped it (owner.ts brackets it
|
|
1019
|
+
// without try/finally, which would survive into prod; a cleanup's own
|
|
1020
|
+
// throw is caught there). Clear it then, or every later check would stay
|
|
1021
|
+
// silent.
|
|
935
1022
|
if (!disposalHealQueued) {
|
|
936
1023
|
disposalHealQueued = true;
|
|
937
1024
|
queueMicrotask(healDisposalDepth);
|
|
@@ -1052,1514 +1139,1704 @@ function devCheckQuiescent(isQueuedForCommit) {
|
|
|
1052
1139
|
return;
|
|
1053
1140
|
}
|
|
1054
1141
|
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
_verdict: null,
|
|
1070
|
-
_open: 0,
|
|
1071
|
-
_acted: false,
|
|
1072
|
-
_links: null,
|
|
1073
|
-
_shown: false,
|
|
1074
|
-
_held: false,
|
|
1075
|
-
_marks: null
|
|
1076
|
-
};
|
|
1077
|
-
if (!lane) transactions.push(t);
|
|
1078
|
-
return t;
|
|
1079
|
-
}
|
|
1080
|
-
// Lanes (plan sec. 28, 2026-10-02; lanes.ts). A lane is "a new base of a transition":
|
|
1081
|
-
// the sub-frame an optimistic write opens in the transition of the frame it
|
|
1082
|
-
// is made in. It sees the screen plus its own guesses (plan sec. 19), breaks out of
|
|
1083
|
-
// the parent's hold — its effects run now — and holds itself if its own
|
|
1084
|
-
// derivations hit async. It ends when the parent lands: a guess reverts to
|
|
1085
|
-
// the truth it covered, or lands as the truth that superseded it. A frame
|
|
1086
|
-
// that does not park has nothing to be optimistic over: the write is void.
|
|
1087
|
-
//
|
|
1088
|
-
// The seat of a pass is its node's: a node carrying a lane's value runs as
|
|
1089
|
-
// the lane's whoever dirtied it; a derivation's tracked read of a lane's
|
|
1090
|
-
// value moves its pass into the lane; a leaf's never does (it reads the
|
|
1091
|
-
// screen). Lane work's values go to the lane slot, its runs to the lane's
|
|
1092
|
-
// queues; the rest of the frame is the flush's.
|
|
1093
|
-
/** The lane the running pass is work of; set at `recompute`'s head from the
|
|
1094
|
-
* node, moved by a derivation's lane read, saved and restored around the
|
|
1095
|
-
* pass. */
|
|
1096
|
-
let passLane = null;
|
|
1097
|
-
function setPassLane(l) {
|
|
1098
|
-
passLane = l;
|
|
1099
|
-
}
|
|
1100
|
-
/** The transaction this flush parks into: set by the first join, merged
|
|
1101
|
-
* with every further one. Consumed at the seam. A join outside a flush (a
|
|
1102
|
-
* mount's pending observer, a write to a held node, an action opening)
|
|
1103
|
-
* schedules the seam that consumes it: membership is the tick's — the same
|
|
1104
|
-
* synchronous frame — and the next tick's writes are not this one's. */
|
|
1105
|
-
let flushTransaction = null;
|
|
1106
|
-
/** The transaction the passes running OUTSIDE a flush this tick derive from
|
|
1107
|
-
* (a mainline mount, a mainline re-pass): the passes' own (A29, creation-
|
|
1108
|
-
* time form — "the entry is the pass's alone"): their results are staged
|
|
1109
|
-
* into it, and nothing else in the tick is — a write made after the mount
|
|
1110
|
-
* is a mainline write, a render effect mounted beside it a stale reader.
|
|
1111
|
-
* Inside a flush, or in a tick that already has its transaction (an
|
|
1112
|
-
* action's body), the frame joins instead (`flushTransaction`) — except a
|
|
1113
|
-
* first pass a loading boundary that has not shown content catches (`own`,
|
|
1114
|
-
* A29's boundary exemption, #3540): it is the boundary's, not the tick's,
|
|
1115
|
-
* and a flush that has joined nothing keeps it pass-scoped as outside one.
|
|
1116
|
-
* Cleared by the flush the join schedules, and at the end of a flush that
|
|
1117
|
-
* set it. */
|
|
1118
|
-
let passTx = null;
|
|
1119
|
-
function joinPassTx(t, own) {
|
|
1120
|
-
if ((globalQueue._running && !own) || flushTransaction !== null) return joinFuture(t);
|
|
1121
|
-
if (passTx === null) passTx = resolveTx(t);
|
|
1122
|
-
else merge(resolveTx(t), passTx);
|
|
1123
|
-
schedule();
|
|
1124
|
-
}
|
|
1125
|
-
/** The transaction holding a node (CONFIG_HELD set), resolved through
|
|
1126
|
-
* merges; the path is compressed. */
|
|
1127
|
-
function txOf(n) {
|
|
1128
|
-
return (n._x._transaction = resolveTx(n._x._transaction));
|
|
1129
|
-
}
|
|
1130
|
-
/** `t` resolved through merges while it is still parked or open; null once
|
|
1131
|
-
* it has landed. */
|
|
1132
|
-
function liveTx(t) {
|
|
1133
|
-
t = resolveTx(t);
|
|
1134
|
-
return transactions.indexOf(t) !== -1 ? t : null;
|
|
1135
|
-
}
|
|
1136
|
-
function resolveTx(t) {
|
|
1137
|
-
while (t._into !== null) t = t._into;
|
|
1138
|
-
return t;
|
|
1139
|
-
}
|
|
1140
|
-
/** The flush joins transaction `t` — or needs one of its own (`null`: a pass
|
|
1141
|
-
* went pending with nothing held). Joining a second one merges it into the
|
|
1142
|
-
* flush's: the work that touched both entangles them (A15). */
|
|
1143
|
-
function joinFuture(t) {
|
|
1144
|
-
const f = flushTransaction;
|
|
1145
|
-
if (f === null) {
|
|
1146
|
-
flushTransaction = t === null ? newTransaction(false) : resolveTx(t);
|
|
1147
|
-
} else if (t !== null) merge(resolveTx(t), f);
|
|
1148
|
-
if (!globalQueue._running) schedule();
|
|
1149
|
-
}
|
|
1150
|
-
/** `t` merges into `f`: the work that touched both entangles them (A15).
|
|
1151
|
-
* A lane never merges: it is a sub-frame, not a peer — its work is routed
|
|
1152
|
-
* per pass (`passLane`). */
|
|
1153
|
-
function merge(t, f) {
|
|
1154
|
-
// Both ends resolved: a stale pointer to a transaction already merged
|
|
1155
|
-
// into the other would close a cycle `resolveTx` never leaves.
|
|
1156
|
-
t = resolveTx(t);
|
|
1157
|
-
f = resolveTx(f);
|
|
1158
|
-
if (t === f || t._lane || f._lane) return;
|
|
1159
|
-
if (attrHooks !== null) attrHooks.transitionMerged(f, t);
|
|
1160
|
-
t._into = f;
|
|
1161
|
-
f._open += t._open;
|
|
1162
|
-
f._acted ||= t._acted;
|
|
1163
|
-
append(f._nodes, t._nodes);
|
|
1164
|
-
append(f._queues[0], t._queues[0]);
|
|
1165
|
-
append(f._queues[1], t._queues[1]);
|
|
1166
|
-
append(f._reruns, t._reruns);
|
|
1167
|
-
if (t._marks !== null) append((f._marks ??= []), t._marks);
|
|
1168
|
-
transactions.splice(transactions.indexOf(t), 1);
|
|
1142
|
+
// L2 (step 2): the parked frame. A pass parks the previous frame's children
|
|
1143
|
+
// and `_disposal` list on `_x._pendingFirstChild` / `_x._pendingDisposal`
|
|
1144
|
+
// (`parkChildren`, core.ts); they keep reacting until the node's commit
|
|
1145
|
+
// disposes them (`zombie` walk below) or the owner dies. `markDisposal` flags
|
|
1146
|
+
// the parked subtree REACTIVE_ZOMBIE: the splice guard and the reawaken path
|
|
1147
|
+
// key off it, and the heap defers a zombie's recompute past the seam (no
|
|
1148
|
+
// second heap — the deferral list in scheduler.ts).
|
|
1149
|
+
function markDisposal(el) {
|
|
1150
|
+
let child = el._firstChild;
|
|
1151
|
+
while (child) {
|
|
1152
|
+
child._flags |= REACTIVE_ZOMBIE;
|
|
1153
|
+
markDisposal(child);
|
|
1154
|
+
child = child._nextSibling;
|
|
1155
|
+
}
|
|
1169
1156
|
}
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
* the flight, and a reader that stopped reading releases it (O3, #3494).
|
|
1180
|
-
* An errored pass (NotReady included) did not stop: it never got there, and
|
|
1181
|
-
* its full list stands (A30) — the reads it did not reach still observe. A
|
|
1182
|
-
* flight nobody renders holds nothing (A29). Only the nodes `t` still owns
|
|
1183
|
-
* count: one a lane took over since is the lane's to wait on. A guess whose
|
|
1184
|
-
* own truth is in flight blocks the lane's parent, not the lane — it stands
|
|
1185
|
-
* in for the flight (A17: "visible until its own fetch settles"). */
|
|
1186
|
-
function blocked(t) {
|
|
1187
|
-
// The transaction being judged (a lane consulted on its behalf judges for
|
|
1188
|
-
// it): a zombie whose removal it stages is moot for it alone (`onScreen`).
|
|
1189
|
-
const prev = judge;
|
|
1190
|
-
judge ??= t;
|
|
1191
|
-
// An action still running in it holds it open (action.ts). Its lanes
|
|
1192
|
-
// (lanes.ts): a lane under `t` blocks it while blocked itself.
|
|
1193
|
-
const r =
|
|
1194
|
-
t._open !== 0 ||
|
|
1195
|
-
blockedBy(t._nodes, t) ||
|
|
1196
|
-
(!!GlobalQueue._lanesBlocked && GlobalQueue._lanesBlocked(t));
|
|
1197
|
-
judge = prev;
|
|
1198
|
-
return r;
|
|
1157
|
+
function dispose(node) {
|
|
1158
|
+
// Direct disposal is death, not dormancy: strip the observation lifecycle
|
|
1159
|
+
// so a later read freezes at the last committed value instead of
|
|
1160
|
+
// reawakening the node (#3024). The teardown itself (heap removal — a node
|
|
1161
|
+
// left queued would be recomputed and resurrected by the next flush (#2983)
|
|
1162
|
+
// — dep unlinking, child disposal) is exactly unobserved()'s body; only
|
|
1163
|
+
// this flag distinguishes death from dormancy.
|
|
1164
|
+
node._config &= ~CONFIG_AUTO_DISPOSE;
|
|
1165
|
+
unobserved(node);
|
|
1199
1166
|
}
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
)
|
|
1215
|
-
out.push(n);
|
|
1167
|
+
// The first error a cleanup threw, boxed so a thrown `undefined` still
|
|
1168
|
+
// counts. A throwing cleanup stops neither its siblings nor the rest of the
|
|
1169
|
+
// tree: every cleanup runs. The error is rethrown once the teardown is over:
|
|
1170
|
+
// by a root's dispose call outside a flush (`disposeRootSelf`), otherwise when
|
|
1171
|
+
// the flush ends (`flush`; `runCleanup` schedules one), so a flush is never
|
|
1172
|
+
// left half-applied. A halt drops it: the error that halted is the one
|
|
1173
|
+
// reported, and a stale one must not surface from a later, unrelated dispose.
|
|
1174
|
+
let cleanupError;
|
|
1175
|
+
/** Runs a cleanup or `unobserved` hook; a throw is collected, not raised. @internal */
|
|
1176
|
+
function runCleanup(fn) {
|
|
1177
|
+
try {
|
|
1178
|
+
fn();
|
|
1179
|
+
} catch (e) {
|
|
1180
|
+
cleanupError ||= [e];
|
|
1181
|
+
schedule();
|
|
1216
1182
|
}
|
|
1217
|
-
return out;
|
|
1218
1183
|
}
|
|
1219
|
-
/**
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
function ownFlights(t) {
|
|
1225
|
-
return blockedBy(t._nodes, t, true);
|
|
1184
|
+
/** Rethrows the error `runCleanup` collected, once; `drop` discards it. @internal */
|
|
1185
|
+
function rethrowCleanupError(drop) {
|
|
1186
|
+
const error = cleanupError;
|
|
1187
|
+
cleanupError = undefined;
|
|
1188
|
+
if (error && !drop) throw error[0];
|
|
1226
1189
|
}
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1190
|
+
function disposeChildren(node, self = false, zombie) {
|
|
1191
|
+
const flags = node._flags;
|
|
1192
|
+
if (flags & REACTIVE_DISPOSED) return;
|
|
1193
|
+
// A parked frame dies with its owner (#3024): the commit that would retire
|
|
1194
|
+
// it never comes once the node is DISPOSED, so it drains here or its
|
|
1195
|
+
// cleanups never run and it stays subscribed, rerunning in a torn-down
|
|
1196
|
+
// tree (#3561). Death only (`self`): a re-pass leaves the frame reacting
|
|
1197
|
+
// until the commit.
|
|
1198
|
+
if (
|
|
1199
|
+
self &&
|
|
1200
|
+
!zombie &&
|
|
1201
|
+
node._x !== null &&
|
|
1202
|
+
(node._x._pendingFirstChild !== null || node._x._pendingDisposal !== null)
|
|
1203
|
+
)
|
|
1204
|
+
disposeChildren(node, false, true);
|
|
1205
|
+
if (self) {
|
|
1206
|
+
node._flags = flags | REACTIVE_DISPOSED;
|
|
1207
|
+
if (node._parent === null && node._root) unregisterRoot(node);
|
|
1236
1208
|
}
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1209
|
+
if (self && true) clearSignals(node);
|
|
1210
|
+
if (self && node._fn) {
|
|
1211
|
+
if (node._x !== null) node._x._inFlight = null;
|
|
1212
|
+
// A death that can unblock the future (L2, `futureBlocked`): a held
|
|
1213
|
+
// flight's — a transaction's or a lane's (its `_x._transaction`) — or a
|
|
1214
|
+
// stale reader's (REACTIVE_FRAME_READ — a frame that derived from one).
|
|
1215
|
+
// Nothing else re-examines a dead node, so make sure a seam comes.
|
|
1242
1216
|
if (
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
n._config & CONFIG_GUESS ||
|
|
1246
|
-
(n._x._transaction !== null && txOf(n) !== owner) ||
|
|
1247
|
-
(own && !ownSource(n, owner))
|
|
1217
|
+
(node._statusFlags & STATUS_PENDING && node._x?._transaction != null) ||
|
|
1218
|
+
flags & REACTIVE_FRAME_READ
|
|
1248
1219
|
)
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1220
|
+
schedule();
|
|
1221
|
+
}
|
|
1222
|
+
let child = zombie ? node._x._pendingFirstChild : node._firstChild;
|
|
1223
|
+
if (zombie) node._x._pendingFirstChild = null;
|
|
1224
|
+
else node._firstChild = null;
|
|
1225
|
+
while (child) {
|
|
1226
|
+
const n = child;
|
|
1227
|
+
// Owner teardown is death regardless of the child's own lifecycle
|
|
1228
|
+
// (#3024): strip AUTO_DISPOSE so a post-disposal read freezes at the
|
|
1229
|
+
// last committed value instead of reawakening in a torn-down tree.
|
|
1230
|
+
// Runs before the recursion so already-dormant children (whose
|
|
1231
|
+
// disposeChildren call early-returns on REACTIVE_DISPOSED) die too.
|
|
1232
|
+
// Only unobserved()'s own node keeps its dormancy — it is never in
|
|
1233
|
+
// this loop; its children are rebuilt fresh on reawaken.
|
|
1234
|
+
n._config &= ~CONFIG_AUTO_DISPOSE;
|
|
1235
|
+
// Heap removal must not be gated on `_deps`: a dependency-free
|
|
1236
|
+
// computation queued by refresh() has a null dep list but still sits in
|
|
1237
|
+
// the dirty heap, and left there the post-disposal flush recomputes it —
|
|
1238
|
+
// recompute() rewriting `_flags` clears REACTIVE_DISPOSED and the node
|
|
1239
|
+
// comes back to life (post-unmount runs, leaked cleanups, #2983).
|
|
1240
|
+
// deleteFromHeap self-guards on the in-heap flags (and tolerates plain
|
|
1241
|
+
// Owners, whose _flags is undefined), so no gate here.
|
|
1242
|
+
deleteFromHeap(n, dirtyQueue);
|
|
1243
|
+
clearDeps(n);
|
|
1244
|
+
// The chain is detached above so a node a cleanup links mid-drain lands
|
|
1245
|
+
// on the fresh head and survives. Pointing each drained child's prev at
|
|
1246
|
+
// itself routes its later splice onto the detached chain, never the head,
|
|
1247
|
+
// and keeps the dev owner-chain-head invariant honest for those children.
|
|
1248
|
+
child._prevSibling = child;
|
|
1249
|
+
disposeChildren(child, true);
|
|
1250
|
+
// Read after, not before: a sibling this disposal made dormant spliced
|
|
1251
|
+
// itself out of the detached chain, and a cleanup may then have linked
|
|
1252
|
+
// it at the fresh head, which rewrote the `_nextSibling` a pre-read
|
|
1253
|
+
// would still be holding.
|
|
1254
|
+
child = child._nextSibling;
|
|
1255
|
+
}
|
|
1256
|
+
if (!zombie) node._childCount = 0;
|
|
1257
|
+
// O(1) splice out of parent's chain on individual dispose. Skipped during
|
|
1258
|
+
// batch dispose (parent already disposed) and for a zombie (it sits on the
|
|
1259
|
+
// parent's parked frame, drained whole by the commit). We leave
|
|
1260
|
+
// node._nextSibling intact so outer walks that already advanced past us
|
|
1261
|
+
// still reach later siblings.
|
|
1262
|
+
if (
|
|
1263
|
+
self &&
|
|
1264
|
+
!zombie &&
|
|
1265
|
+
!(flags & REACTIVE_ZOMBIE) &&
|
|
1266
|
+
node._parent !== null &&
|
|
1267
|
+
!(node._parent._flags & REACTIVE_DISPOSED)
|
|
1268
|
+
) {
|
|
1269
|
+
const prev = node._prevSibling;
|
|
1270
|
+
const next = node._nextSibling;
|
|
1271
|
+
// A node with no predecessor must be the chain's head; otherwise the
|
|
1272
|
+
// write below would clobber the head with a stale `_nextSibling`,
|
|
1273
|
+
// orphaning every live child ahead of it (#3543).
|
|
1274
|
+
assertInvariant(
|
|
1275
|
+
prev !== null || node._parent._firstChild === node,
|
|
1276
|
+
"owner-chain-head",
|
|
1277
|
+
"head node is not parent._firstChild — a node was spliced while flagged live but not in the chain (see #3543)"
|
|
1278
|
+
);
|
|
1279
|
+
if (prev !== null) prev._nextSibling = next;
|
|
1280
|
+
else node._parent._firstChild = next;
|
|
1281
|
+
if (next !== null) next._prevSibling = prev;
|
|
1282
|
+
node._prevSibling = null;
|
|
1283
|
+
}
|
|
1284
|
+
runDisposal(node, zombie);
|
|
1285
|
+
// Final effect-returned cleanup fires at true disposal, after `_disposal`
|
|
1286
|
+
// to mirror rerun ordering (compute-phase teardown first, cleanup last).
|
|
1287
|
+
if (self && node._cleanup) {
|
|
1288
|
+
const effectCleanup = node._cleanup;
|
|
1289
|
+
node._cleanup = undefined;
|
|
1290
|
+
enterDisposal();
|
|
1291
|
+
runCleanup(effectCleanup);
|
|
1292
|
+
exitDisposal();
|
|
1279
1293
|
}
|
|
1280
|
-
return false;
|
|
1281
|
-
}
|
|
1282
|
-
/** A frame reader whose say counts for `t`'s hold — `t` the transaction
|
|
1283
|
-
* being judged: not a zombie whose removal `t` stages (A15 #3463: a re-ask
|
|
1284
|
-
* in the unmount frame lands its pending on the zombie it unmounts — the
|
|
1285
|
-
* compiled <Show> of #3372; a zombie blocks every other judgment while it
|
|
1286
|
-
* is visible, a lane's reveal included: only the commit that disposes it
|
|
1287
|
-
* makes its say moot), and not behind a fallback (A33: a boundary showing its fallback is the display of
|
|
1288
|
-
* everything under it, so a reader there is not on screen and holds
|
|
1289
|
-
* nothing — boundaries.ts). */
|
|
1290
|
-
function onScreen(r, t) {
|
|
1291
|
-
return (
|
|
1292
|
-
!(r._flags & REACTIVE_ZOMBIE && removalStagedBy(r, t)) &&
|
|
1293
|
-
!(GlobalQueue._hidden && GlobalQueue._hidden(r))
|
|
1294
|
-
);
|
|
1295
|
-
}
|
|
1296
|
-
/** A15 (#3463): a reader whose removal is staged — a zombie, parked by a
|
|
1297
|
-
* pass awaiting its commit — is still on screen and live for every hold,
|
|
1298
|
-
* except the transaction staging its removal: for that one its say is moot
|
|
1299
|
-
* (done, and the commit disposes it; not done, and it stays parked). The
|
|
1300
|
-
* staging transaction is the one holding the pass that parked the frame:
|
|
1301
|
-
* the nearest non-zombie ancestor. */
|
|
1302
|
-
function removalStagedBy(r, t) {
|
|
1303
|
-
let o = r;
|
|
1304
|
-
while (o !== null && o._flags & REACTIVE_ZOMBIE) o = o._parent;
|
|
1305
|
-
return (
|
|
1306
|
-
o !== null &&
|
|
1307
|
-
(o._config & CONFIG_HELD) !== 0 &&
|
|
1308
|
-
o._x?._transaction != null &&
|
|
1309
|
-
txOf(o) === resolveTx(t)
|
|
1310
|
-
);
|
|
1311
|
-
}
|
|
1312
|
-
/** Unchanged passes this flush whose stale dependency tail awaits the
|
|
1313
|
-
* flush's verdict (A30, #3469): trimmed when the flush commits, kept when
|
|
1314
|
-
* it parks — the committed frame still derives from the previous pass's
|
|
1315
|
-
* dependencies, and a write to one of them must reach the node until a
|
|
1316
|
-
* committing pass replaces it. */
|
|
1317
|
-
const heldTrims = [];
|
|
1318
|
-
function heldTrim(el) {
|
|
1319
|
-
heldTrims.push(el);
|
|
1320
|
-
}
|
|
1321
|
-
/** Zombies popped from the dirty heap this flush (`deferZombie`). The seam
|
|
1322
|
-
* decides their fate: a park cancels them (the writes that dirtied them are
|
|
1323
|
-
* held with it); a commit runs the survivors — the ones whose owner is still
|
|
1324
|
-
* awaiting its commit — in height order, after the commits that would have
|
|
1325
|
-
* disposed them (#3546). No second heap: the list is the deferral. */
|
|
1326
|
-
const deferredZombies = [];
|
|
1327
|
-
function deferZombie(el) {
|
|
1328
|
-
deleteFromHeap(el, dirtyQueue);
|
|
1329
|
-
el._flags &= -4;
|
|
1330
|
-
deferredZombies.push(el);
|
|
1331
|
-
}
|
|
1332
|
-
/** Put a staged node in a transaction: flagged, stamped, on its landing
|
|
1333
|
-
* list. */
|
|
1334
|
-
function holdNode(n, t) {
|
|
1335
|
-
n._config |= CONFIG_HELD;
|
|
1336
|
-
list(n, t);
|
|
1337
1294
|
}
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
x._transaction = t;
|
|
1345
|
-
t._nodes.push(n);
|
|
1295
|
+
function linkChild(parent, node) {
|
|
1296
|
+
const head = parent._firstChild;
|
|
1297
|
+
node._prevSibling = null;
|
|
1298
|
+
node._nextSibling = head;
|
|
1299
|
+
if (head !== null) head._prevSibling = node;
|
|
1300
|
+
parent._firstChild = node;
|
|
1346
1301
|
}
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1302
|
+
function runDisposal(node, zombie) {
|
|
1303
|
+
// Detach the list BEFORE running it (#3601), as `_cleanup` is (#2813). A
|
|
1304
|
+
// cleanup that disposes an ancestor re-enters this node through the death
|
|
1305
|
+
// walk while the loop is still running; with the list still attached, that
|
|
1306
|
+
// walk ran every entry a second time. Detached, the re-entrant drain finds
|
|
1307
|
+
// nothing. The same shape latched a throwing cleanup: the list survived the
|
|
1308
|
+
// throw and every later drain re-ran and re-threw it.
|
|
1309
|
+
const disposal = zombie ? node._x._pendingDisposal : node._disposal;
|
|
1310
|
+
if (!disposal) return;
|
|
1311
|
+
if (zombie) node._x._pendingDisposal = null;
|
|
1312
|
+
else node._disposal = null;
|
|
1313
|
+
// Each cleanup is guarded (`runCleanup`), so the depth is always lowered.
|
|
1314
|
+
enterDisposal();
|
|
1315
|
+
if (Array.isArray(disposal)) {
|
|
1316
|
+
// Unwind order (#3572, restores 1.x #1562): later registrations run
|
|
1317
|
+
// before earlier ones. Children have already been disposed by the caller,
|
|
1318
|
+
// so with LIFO a body that registers cleanup before creating its children
|
|
1319
|
+
// tears down after them — the same order a per-component owner gives.
|
|
1320
|
+
for (let i = disposal.length - 1; i >= 0; i--) {
|
|
1321
|
+
runCleanup(disposal[i]);
|
|
1366
1322
|
}
|
|
1367
|
-
|
|
1323
|
+
} else {
|
|
1324
|
+
runCleanup(disposal);
|
|
1368
1325
|
}
|
|
1326
|
+
exitDisposal();
|
|
1369
1327
|
}
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
let clock = 0;
|
|
1377
|
-
let scheduled = false;
|
|
1378
|
-
let halted = false;
|
|
1379
|
-
let haltNotified = false;
|
|
1380
|
-
let syncDepth = 0;
|
|
1381
|
-
let inTrackedQueueCallback = false;
|
|
1382
|
-
let _enforceLoadingBoundary = false;
|
|
1383
|
-
let _hitUnhandledAsync = false;
|
|
1384
|
-
// Once per enforcement window: the ASYNC_OUTSIDE_LOADING_BOUNDARY finding is a
|
|
1385
|
-
// fact about the MOUNT ("the root mount will be deferred"), not about each
|
|
1386
|
-
// pending render effect — N async siblings at mount used to produce N copies.
|
|
1387
|
-
let _reportedUnhandledAsync = false;
|
|
1388
|
-
/**
|
|
1389
|
-
* Consume the unhandled-async hit. Returns whether this is the first report
|
|
1390
|
-
* of the current enforcement window — the caller warns only then.
|
|
1391
|
-
*/
|
|
1392
|
-
function resetUnhandledAsync() {
|
|
1393
|
-
_hitUnhandledAsync = false;
|
|
1394
|
-
if (_reportedUnhandledAsync) return false;
|
|
1395
|
-
_reportedUnhandledAsync = true;
|
|
1396
|
-
return true;
|
|
1328
|
+
function childId(owner, consume) {
|
|
1329
|
+
let counter = owner;
|
|
1330
|
+
while (counter._config & CONFIG_TRANSPARENT && counter._parent) counter = counter._parent;
|
|
1331
|
+
if (counter.id != null)
|
|
1332
|
+
return formatId(counter.id, consume ? counter._childCount++ : counter._childCount);
|
|
1333
|
+
throw new Error("Cannot get child id from owner without an id");
|
|
1397
1334
|
}
|
|
1398
1335
|
/**
|
|
1399
|
-
*
|
|
1400
|
-
*
|
|
1401
|
-
* top-level uncaught async read surfaces the diagnostic. Not part of the
|
|
1402
|
-
* user-facing API.
|
|
1336
|
+
* Allocates and returns the next stable child id for `owner`. Used by
|
|
1337
|
+
* hydration plumbing and `createUniqueId`. Not part of the user-facing API.
|
|
1403
1338
|
*
|
|
1404
1339
|
* @internal
|
|
1405
1340
|
*/
|
|
1406
|
-
function
|
|
1407
|
-
|
|
1408
|
-
if (enabled) _reportedUnhandledAsync = false;
|
|
1409
|
-
}
|
|
1410
|
-
function setTrackedQueueCallback(value) {
|
|
1411
|
-
inTrackedQueueCallback = value;
|
|
1341
|
+
function getNextChildId(owner) {
|
|
1342
|
+
return childId(owner, true);
|
|
1412
1343
|
}
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
*
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1344
|
+
/**
|
|
1345
|
+
* The id a freshly-created node inherits: an explicit `options.id` wins;
|
|
1346
|
+
* transparent nodes share their parent's id; otherwise the parent's next
|
|
1347
|
+
* child id is consumed (or `undefined` outside an id-carrying tree).
|
|
1348
|
+
*/
|
|
1349
|
+
function inheritId(options, transparent, parent) {
|
|
1350
|
+
return (
|
|
1351
|
+
options?.id ??
|
|
1352
|
+
(transparent ? parent?.id : parent?.id != null ? getNextChildId(parent) : undefined)
|
|
1353
|
+
);
|
|
1421
1354
|
}
|
|
1422
|
-
|
|
1423
|
-
|
|
1355
|
+
/**
|
|
1356
|
+
* Returns the *next* child id for `owner` without consuming it. Used by
|
|
1357
|
+
* hydration plumbing to peek at the id a future child will receive.
|
|
1358
|
+
*
|
|
1359
|
+
* @internal
|
|
1360
|
+
*/
|
|
1361
|
+
function peekNextChildId(owner) {
|
|
1362
|
+
return childId(owner, false);
|
|
1424
1363
|
}
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
* started it (async.ts; mainline is always the current question). An
|
|
1430
|
-
* answer from an older question than the guess it lands on is stale: held
|
|
1431
|
-
* silently, it does not move the graph. */
|
|
1432
|
-
let question = 0;
|
|
1433
|
-
let questions = 0;
|
|
1434
|
-
function setQuestion(q) {
|
|
1435
|
-
question = q;
|
|
1364
|
+
function formatId(prefix, id) {
|
|
1365
|
+
const num = id.toString(36),
|
|
1366
|
+
len = num.length - 1;
|
|
1367
|
+
return prefix + (len ? String.fromCharCode(64 + len) : "") + num;
|
|
1436
1368
|
}
|
|
1437
|
-
|
|
1438
|
-
|
|
1369
|
+
/**
|
|
1370
|
+
* Returns the currently-tracking observer (the computation that subscribes to
|
|
1371
|
+
* reactive reads at this point), or `null` if reads here would be untracked.
|
|
1372
|
+
* Used by reactive primitives that need to know whether they're inside a
|
|
1373
|
+
* tracking scope. App code rarely needs this — see `getOwner()` for the
|
|
1374
|
+
* lifecycle owner instead.
|
|
1375
|
+
*
|
|
1376
|
+
* @example
|
|
1377
|
+
* ```ts
|
|
1378
|
+
* // Library predicate: only register a hot-path subscription when the
|
|
1379
|
+
* // caller is inside a tracking scope (memo / effect compute / JSX).
|
|
1380
|
+
* function trackIfTracked(source: () => unknown) {
|
|
1381
|
+
* if (getObserver()) source();
|
|
1382
|
+
* }
|
|
1383
|
+
* ```
|
|
1384
|
+
*/
|
|
1385
|
+
function getObserver() {
|
|
1386
|
+
return tracking ? context : null;
|
|
1439
1387
|
}
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1388
|
+
/**
|
|
1389
|
+
* Returns the current reactive **owner** — the lifecycle node that the next
|
|
1390
|
+
* `cleanup()` / `onCleanup()` / `createSignal()` etc. will be attached to.
|
|
1391
|
+
*
|
|
1392
|
+
* Returns `null` if called outside any owner. Capture the owner with
|
|
1393
|
+
* `getOwner()` and re-enter it later with `runWithOwner(owner, fn)` to attach
|
|
1394
|
+
* disposables created from a callback (event handler, async resolution, etc.)
|
|
1395
|
+
* back to a component's lifecycle.
|
|
1396
|
+
*
|
|
1397
|
+
* @example
|
|
1398
|
+
* ```ts
|
|
1399
|
+
* function defer<T>(fn: () => T) {
|
|
1400
|
+
* const owner = getOwner();
|
|
1401
|
+
* queueMicrotask(() => runWithOwner(owner, fn));
|
|
1402
|
+
* }
|
|
1403
|
+
* ```
|
|
1404
|
+
*/
|
|
1405
|
+
function getOwner() {
|
|
1406
|
+
return context;
|
|
1444
1407
|
}
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1408
|
+
/**
|
|
1409
|
+
* Low-level: registers `fn` as a disposal callback on the current owner.
|
|
1410
|
+
* Most code should use `onCleanup()` from `solid-js`, which adds dev-mode
|
|
1411
|
+
* checks. `cleanup()` is the unchecked primitive used by internals; `owner`
|
|
1412
|
+
* (internal) targets a specific owner's disposal list instead of the ambient
|
|
1413
|
+
* one.
|
|
1414
|
+
*/
|
|
1415
|
+
function cleanup(fn, owner = context) {
|
|
1416
|
+
if (!owner) return fn;
|
|
1417
|
+
if (!owner._disposal) owner._disposal = fn;
|
|
1418
|
+
else if (Array.isArray(owner._disposal)) owner._disposal.push(fn);
|
|
1419
|
+
else owner._disposal = [owner._disposal, fn];
|
|
1420
|
+
return fn;
|
|
1455
1421
|
}
|
|
1456
1422
|
/**
|
|
1457
|
-
*
|
|
1458
|
-
*
|
|
1459
|
-
*
|
|
1423
|
+
* Returns `true` if the owner has been disposed. Pair with a captured owner
|
|
1424
|
+
* to bail out of late callbacks whose surrounding component already
|
|
1425
|
+
* unmounted.
|
|
1426
|
+
*
|
|
1427
|
+
* @example
|
|
1428
|
+
* ```ts
|
|
1429
|
+
* function onSettleSafe(fn: () => void) {
|
|
1430
|
+
* const owner = getOwner();
|
|
1431
|
+
* queueMicrotask(() => {
|
|
1432
|
+
* if (owner && isDisposed(owner)) return; // component unmounted; skip
|
|
1433
|
+
* runWithOwner(owner, fn);
|
|
1434
|
+
* });
|
|
1435
|
+
* }
|
|
1436
|
+
* ```
|
|
1460
1437
|
*/
|
|
1438
|
+
function isDisposed(node) {
|
|
1439
|
+
return !!(node._flags & REACTIVE_DISPOSED);
|
|
1440
|
+
}
|
|
1441
|
+
function disposeRootSelf(self = true) {
|
|
1442
|
+
disposeChildren(this, self);
|
|
1443
|
+
// Outside a flush the dispose call rethrows; inside one, the flush does.
|
|
1444
|
+
// Kept out of `disposeChildren`, whose hot path it slows.
|
|
1445
|
+
if (cleanupError && !globalQueue._running) rethrowCleanupError();
|
|
1446
|
+
}
|
|
1461
1447
|
/**
|
|
1462
|
-
*
|
|
1463
|
-
*
|
|
1464
|
-
*
|
|
1465
|
-
*
|
|
1466
|
-
*
|
|
1448
|
+
* Creates a fresh owner attached as a child of the current owner (or as a
|
|
1449
|
+
* detached root if there is none). Used by framework internals to group
|
|
1450
|
+
* cleanups; app code should use `createRoot()` (host a reactive scope outside
|
|
1451
|
+
* a component) or `runWithOwner()` (re-enter a captured owner).
|
|
1452
|
+
*
|
|
1453
|
+
* @internal
|
|
1467
1454
|
*/
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1455
|
+
function createOwner(options) {
|
|
1456
|
+
const parent = context;
|
|
1457
|
+
const transparent = options?.transparent ?? false;
|
|
1458
|
+
// Prod and observe boilerplates (see core.ts computed()). The observe
|
|
1459
|
+
// literal carries the `_name` and `_loc` slots the rendering layer fills
|
|
1460
|
+
// with the component label and JSX site (`owner._name = "<App>"`,
|
|
1461
|
+
// `owner._loc = "src/App.tsx:3:5"`) — slots, so labelling a root is a
|
|
1462
|
+
// plain store rather than a shape fork between labelled and plain roots.
|
|
1463
|
+
const owner = {
|
|
1464
|
+
id: inheritId(options, transparent, parent),
|
|
1465
|
+
_config: transparent ? CONFIG_TRANSPARENT : 0,
|
|
1466
|
+
// Present from birth: disposeChildren marks death here on roots too, and
|
|
1467
|
+
// a first store of a missing property is a shape transition plus a
|
|
1468
|
+
// properties-store allocation per root — on a 100×4 table teardown that
|
|
1469
|
+
// was 12 KB and most of disposal's time (2026-10-17).
|
|
1470
|
+
_flags: REACTIVE_NONE,
|
|
1471
|
+
_root: true,
|
|
1472
|
+
_parentComputed: parent?._root ? parent._parentComputed : parent,
|
|
1473
|
+
_firstChild: null,
|
|
1474
|
+
_nextSibling: null,
|
|
1475
|
+
_prevSibling: null,
|
|
1476
|
+
_disposal: null,
|
|
1477
|
+
_context: parent?._context || defaultContext,
|
|
1478
|
+
_childCount: 0,
|
|
1479
|
+
_x: null,
|
|
1480
|
+
_parent: parent,
|
|
1481
|
+
dispose: disposeRootSelf,
|
|
1482
|
+
_name: undefined,
|
|
1483
|
+
_loc: undefined
|
|
1484
|
+
};
|
|
1485
|
+
if (parent && parent._config & CONFIG_CHILDREN_FORBIDDEN) {
|
|
1476
1486
|
emitDiagnostic({
|
|
1477
|
-
code: "
|
|
1478
|
-
kind: "
|
|
1487
|
+
code: "PRIMITIVE_IN_FORBIDDEN_SCOPE",
|
|
1488
|
+
kind: "lifecycle",
|
|
1479
1489
|
severity: "error",
|
|
1480
|
-
message
|
|
1490
|
+
message: PRIMITIVE_IN_FORBIDDEN_SCOPE_MESSAGE,
|
|
1491
|
+
ownerId: parent.id,
|
|
1492
|
+
ownerName: parent._name
|
|
1481
1493
|
});
|
|
1494
|
+
throw new Error(PRIMITIVE_IN_FORBIDDEN_SCOPE_MESSAGE);
|
|
1482
1495
|
}
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
// halt that only reaches console.error leaves a page that LOOKS alive with
|
|
1489
|
-
// nothing an app or its telemetry can act on (#3338 — an uncaught throw
|
|
1490
|
-
// during the hydration render). The rethrow may reach the top as well in
|
|
1491
|
-
// the non-swallowed cases; a duplicate report beats a silent one.
|
|
1492
|
-
const report = cause !== undefined && globalThis.reportError;
|
|
1493
|
-
report || cause === undefined ? console.error(message) : console.error(message, cause);
|
|
1494
|
-
report && report(cause);
|
|
1496
|
+
assertNotInEffectCallback();
|
|
1497
|
+
if (parent) linkChild(parent, owner);
|
|
1498
|
+
else registerRoot(owner);
|
|
1499
|
+
DEV.hooks.onOwner?.(owner);
|
|
1500
|
+
return owner;
|
|
1495
1501
|
}
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1502
|
+
/**
|
|
1503
|
+
* Creates a reactive root — an owner scope with its own `dispose()`. A root
|
|
1504
|
+
* created inside an existing owner is owned by it and is disposed when the
|
|
1505
|
+
* parent is disposed; call `dispose()` to tear it down earlier. To create a
|
|
1506
|
+
* root that outlives its creator, detach explicitly:
|
|
1507
|
+
* `runWithOwner(null, () => createRoot(...))`. Pass `id` to seed hydration
|
|
1508
|
+
* ids for the tree it owns.
|
|
1509
|
+
*
|
|
1510
|
+
* `dispose()` tears down every signal, memo, effect, and `onCleanup`
|
|
1511
|
+
* registered inside the root.
|
|
1512
|
+
*
|
|
1513
|
+
* Use this to host long-lived reactive scopes outside of a component (custom
|
|
1514
|
+
* controllers, app bootstrapping, tests). Inside a component, prefer
|
|
1515
|
+
* letting Solid's component lifecycle own things.
|
|
1516
|
+
*
|
|
1517
|
+
* @example
|
|
1518
|
+
* ```ts
|
|
1519
|
+
* // At module level there is no owner, so this root lives until disposed.
|
|
1520
|
+
* const dispose = createRoot(dispose => {
|
|
1521
|
+
* const [n, setN] = createSignal(0);
|
|
1522
|
+
* createEffect(() => n(), value => console.log(value));
|
|
1523
|
+
* setInterval(() => setN(x => x + 1), 1000);
|
|
1524
|
+
* return dispose;
|
|
1525
|
+
* });
|
|
1526
|
+
*
|
|
1527
|
+
* // Later, to tear everything down:
|
|
1528
|
+
* dispose();
|
|
1529
|
+
*
|
|
1530
|
+
* // Inside an owner (component, effect, another root), detach explicitly
|
|
1531
|
+
* // if the root must outlive its creator:
|
|
1532
|
+
* const detached = runWithOwner(null, () => createRoot(d => d));
|
|
1533
|
+
* ```
|
|
1534
|
+
*
|
|
1535
|
+
* @description https://v2.solidjs.com/reference/solid-js/advanced/owner-introspection/create-root
|
|
1536
|
+
*/
|
|
1537
|
+
function createRoot(init, options) {
|
|
1538
|
+
const owner = createOwner(options);
|
|
1539
|
+
// A bound function, not an arrow: an arrow would share this scope, which
|
|
1540
|
+
// holds `init`, so keeping `dispose` would keep `init` and all it closes
|
|
1541
|
+
// over alive (#4095).
|
|
1542
|
+
return runWithOwner(owner, () => init(disposeRootSelf.bind(owner, true)));
|
|
1503
1543
|
}
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1544
|
+
|
|
1545
|
+
// Optional-module hooks: module bindings, not `GlobalQueue` statics, so a
|
|
1546
|
+
// bundle that never imports the installing module never assigns them — its
|
|
1547
|
+
// minifier folds every guarded call site (and the branch behind it) away.
|
|
1548
|
+
// Each module installs its own set at evaluation (pay-for-use).
|
|
1549
|
+
//
|
|
1550
|
+
// Lanes (lanes.ts; installed when `createOptimistic` — or verdict.ts — is
|
|
1551
|
+
// imported; every call site is behind a bit or a `passLane` nothing else sets).
|
|
1552
|
+
let laneReadHook = null;
|
|
1553
|
+
let laneStageHook = null;
|
|
1554
|
+
let laneOutcomeHook = null;
|
|
1555
|
+
let laneWriteHook = null;
|
|
1556
|
+
let applyGuessesHook = null;
|
|
1557
|
+
let laneSeamsHook = null;
|
|
1558
|
+
let laneCorrectionsHook = null;
|
|
1559
|
+
let endLanesHook = null;
|
|
1560
|
+
let lanesBlockedHook = null;
|
|
1561
|
+
function installLaneHooks(
|
|
1562
|
+
read,
|
|
1563
|
+
stage,
|
|
1564
|
+
outcome,
|
|
1565
|
+
write,
|
|
1566
|
+
applyGuesses,
|
|
1567
|
+
seams,
|
|
1568
|
+
corrections,
|
|
1569
|
+
end,
|
|
1570
|
+
blocked
|
|
1571
|
+
) {
|
|
1572
|
+
laneReadHook = read;
|
|
1573
|
+
laneStageHook = stage;
|
|
1574
|
+
laneOutcomeHook = outcome;
|
|
1575
|
+
laneWriteHook = write;
|
|
1576
|
+
applyGuessesHook = applyGuesses;
|
|
1577
|
+
laneSeamsHook = seams;
|
|
1578
|
+
laneCorrectionsHook = corrections;
|
|
1579
|
+
endLanesHook = end;
|
|
1580
|
+
lanesBlockedHook = blocked;
|
|
1581
|
+
}
|
|
1582
|
+
// Boundaries (boundaries.ts): the display consumers between an observer
|
|
1583
|
+
// and the root. `catch` — status from a frame reader, nearest boundary
|
|
1584
|
+
// first (true: caught, the root never hears of it; a clear — flags 0 —
|
|
1585
|
+
// settles the reader there); `fresh` — a pass that read a hold (true: a
|
|
1586
|
+
// first pass a loading boundary that has not shown content caught,
|
|
1587
|
+
// `joinPass`; `held`: the flush's hold owns it, nothing catches it);
|
|
1588
|
+
// `hidden` — a frame reader behind a fallback (the boundary showing it)
|
|
1589
|
+
// is not on screen and holds nothing; `boundarySeam` — the seam's sweep (readers
|
|
1590
|
+
// gone or settled without a pass reveal; an `on` re-arm resolves);
|
|
1591
|
+
// `heldRun` — a queued run under a fallback-showing boundary waits for
|
|
1592
|
+
// the reveal (true: held; the boundary re-queues it by type). The
|
|
1593
|
+
// synchronous first render on creation builds the subtree, attached or
|
|
1594
|
+
// not; its updates and the user effects wait.
|
|
1595
|
+
let catchHook = null;
|
|
1596
|
+
let freshHook = null;
|
|
1597
|
+
let hiddenHook = null;
|
|
1598
|
+
let boundarySeamHook = null;
|
|
1599
|
+
let heldRunHook = null;
|
|
1600
|
+
function installBoundaryHooks(caught, fresh, hidden, seam, heldRun) {
|
|
1601
|
+
catchHook = caught;
|
|
1602
|
+
freshHook = fresh;
|
|
1603
|
+
hiddenHook = hidden;
|
|
1604
|
+
boundarySeamHook = seam;
|
|
1605
|
+
heldRunHook = heldRun;
|
|
1606
|
+
}
|
|
1607
|
+
/** Store (store/store.ts): `park` — the flush parked into `t`: the store
|
|
1608
|
+
* materializes and holds the container node of every staging that had none
|
|
1609
|
+
* (a staging needs the node as its home only when something can read the
|
|
1610
|
+
* frame through it; a hold can); `commit` — fold the pending backings whose
|
|
1611
|
+
* container nodes this flush committed (the owned-raw model's one mutation
|
|
1612
|
+
* point). */
|
|
1613
|
+
let storeParkHook = null;
|
|
1614
|
+
let storeCommitHook = null;
|
|
1615
|
+
function installStoreHooks(park, commit) {
|
|
1616
|
+
storeParkHook = park;
|
|
1617
|
+
storeCommitHook = commit;
|
|
1618
|
+
}
|
|
1619
|
+
/** `affects()` marks (affects.ts): the releases at a landing and at the seam
|
|
1620
|
+
* (ambient marks). */
|
|
1621
|
+
let releaseMarksHook = null;
|
|
1622
|
+
let releaseAmbientMarksHook = null;
|
|
1623
|
+
function installMarkHooks(release, releaseAmbient) {
|
|
1624
|
+
releaseMarksHook = release;
|
|
1625
|
+
releaseAmbientMarksHook = releaseAmbient;
|
|
1626
|
+
}
|
|
1627
|
+
/** Verdicts (verdict.ts). */
|
|
1628
|
+
let observeFlightHook = null;
|
|
1629
|
+
function installVerdictHooks(observeFlight) {
|
|
1630
|
+
observeFlightHook = observeFlight;
|
|
1631
|
+
}
|
|
1632
|
+
/** External-source bridge (wired by enableExternalSource(); undefined while
|
|
1633
|
+
* no config is active — including after _resetExternalSourceConfig()). */
|
|
1634
|
+
let wireExternalSourceHook = undefined;
|
|
1635
|
+
let externalUntrackHook = undefined;
|
|
1636
|
+
function setExternalHooks(wire, untrack) {
|
|
1637
|
+
wireExternalSourceHook = wire;
|
|
1638
|
+
externalUntrackHook = untrack;
|
|
1508
1639
|
}
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1640
|
+
/** Live transactions — opened, unmerged, not landed. Scanned at every seam
|
|
1641
|
+
* while non-empty; nothing on the plain path. */
|
|
1642
|
+
const transactions = [];
|
|
1643
|
+
/** Observe tier: this flush parked its transaction (`holdStart` fired). */
|
|
1644
|
+
let holding = false;
|
|
1645
|
+
/** A transaction (listed here) or a lane (lanes.ts lists its own). */
|
|
1646
|
+
function newTransaction(lane, parent = null) {
|
|
1647
|
+
const t = {
|
|
1648
|
+
_nodes: [],
|
|
1649
|
+
_queues: [[], []],
|
|
1650
|
+
_reruns: [],
|
|
1651
|
+
_into: null,
|
|
1652
|
+
_parent: parent,
|
|
1653
|
+
_lane: lane,
|
|
1654
|
+
_verdict: null,
|
|
1655
|
+
_open: 0,
|
|
1656
|
+
_acted: false,
|
|
1657
|
+
_links: null,
|
|
1658
|
+
_shown: false,
|
|
1659
|
+
_held: false,
|
|
1660
|
+
_marks: null
|
|
1661
|
+
};
|
|
1662
|
+
if (!lane) transactions.push(t);
|
|
1663
|
+
return t;
|
|
1664
|
+
}
|
|
1665
|
+
// Lanes (plan sec. 28, 2026-10-02; lanes.ts). A lane is "a new base of a transition":
|
|
1666
|
+
// the sub-frame an optimistic write opens in the transition of the frame it
|
|
1667
|
+
// is made in. It sees the screen plus its own guesses (plan sec. 19), breaks out of
|
|
1668
|
+
// the parent's hold — its effects run now — and holds itself if its own
|
|
1669
|
+
// derivations hit async. It ends when the parent lands: a guess reverts to
|
|
1670
|
+
// the truth it covered, or lands as the truth that superseded it. A frame
|
|
1671
|
+
// that does not park has nothing to be optimistic over: the write is void.
|
|
1672
|
+
//
|
|
1673
|
+
// The seat of a pass is its node's: a node carrying a lane's value runs as
|
|
1674
|
+
// the lane's whoever dirtied it; a derivation's tracked read of a lane's
|
|
1675
|
+
// value moves its pass into the lane; a leaf's never does (it reads the
|
|
1676
|
+
// screen). Lane work's values go to the lane slot, its runs to the lane's
|
|
1677
|
+
// queues; the rest of the frame is the flush's.
|
|
1678
|
+
/** The lane the running pass is work of; set at `recompute`'s head from the
|
|
1679
|
+
* node, moved by a derivation's lane read, saved and restored around the
|
|
1680
|
+
* pass. */
|
|
1681
|
+
let passLane = null;
|
|
1682
|
+
function setPassLane(l) {
|
|
1683
|
+
passLane = l;
|
|
1684
|
+
}
|
|
1685
|
+
/** The transaction this flush parks into: set by the first join, merged
|
|
1686
|
+
* with every further one. Consumed at the seam. A join outside a flush (a
|
|
1687
|
+
* mount's pending observer, a write to a held node, an action opening)
|
|
1688
|
+
* schedules the seam that consumes it: membership is the tick's — the same
|
|
1689
|
+
* synchronous frame — and the next tick's writes are not this one's. */
|
|
1690
|
+
let flushTransaction = null;
|
|
1691
|
+
/** The transaction the passes running OUTSIDE a flush this tick derive from
|
|
1692
|
+
* (a mainline mount, a mainline re-pass): the passes' own (A29, creation-
|
|
1693
|
+
* time form — "the entry is the pass's alone"): their results are staged
|
|
1694
|
+
* into it, and nothing else in the tick is — a write made after the mount
|
|
1695
|
+
* is a mainline write, a render effect mounted beside it a stale reader.
|
|
1696
|
+
* Inside a flush, or in a tick that already has its transaction (an
|
|
1697
|
+
* action's body), the frame joins instead (`flushTransaction`) — except a
|
|
1698
|
+
* first pass a loading boundary that has not shown content catches (`own`,
|
|
1699
|
+
* A29's boundary exemption, #3540): it is the boundary's, not the tick's,
|
|
1700
|
+
* and a flush that has joined nothing keeps it pass-scoped as outside one.
|
|
1701
|
+
* Cleared by the flush the join schedules, and at the end of a flush that
|
|
1702
|
+
* set it. */
|
|
1703
|
+
let passTx = null;
|
|
1704
|
+
function joinPassTx(t, own) {
|
|
1705
|
+
if ((globalQueue._running && !own) || flushTransaction !== null) return joinFuture(t);
|
|
1706
|
+
if (passTx === null) passTx = resolveTx(t);
|
|
1707
|
+
else merge(resolveTx(t), passTx);
|
|
1708
|
+
schedule();
|
|
1709
|
+
}
|
|
1710
|
+
/** The transaction holding a node (CONFIG_HELD set), resolved through
|
|
1711
|
+
* merges; the path is compressed. */
|
|
1712
|
+
function txOf(n) {
|
|
1713
|
+
return (n._x._transaction = resolveTx(n._x._transaction));
|
|
1714
|
+
}
|
|
1715
|
+
/** `t` resolved through merges while it is still parked or open; null once
|
|
1716
|
+
* it has landed. */
|
|
1717
|
+
function liveTx(t) {
|
|
1718
|
+
t = resolveTx(t);
|
|
1719
|
+
return transactions.indexOf(t) !== -1 ? t : null;
|
|
1720
|
+
}
|
|
1721
|
+
function resolveTx(t) {
|
|
1722
|
+
while (t._into !== null) t = t._into;
|
|
1723
|
+
return t;
|
|
1724
|
+
}
|
|
1725
|
+
/** The flush joins transaction `t` — or needs one of its own (`null`: a pass
|
|
1726
|
+
* went pending with nothing held). Joining a second one merges it into the
|
|
1727
|
+
* flush's: the work that touched both entangles them (A15). */
|
|
1728
|
+
function joinFuture(t) {
|
|
1729
|
+
const f = flushTransaction;
|
|
1730
|
+
if (f === null) {
|
|
1731
|
+
flushTransaction = t === null ? newTransaction(false) : resolveTx(t);
|
|
1732
|
+
} else if (t !== null) merge(resolveTx(t), f);
|
|
1733
|
+
if (!globalQueue._running) schedule();
|
|
1734
|
+
}
|
|
1735
|
+
/** `t` merges into `f`: the work that touched both entangles them (A15).
|
|
1736
|
+
* A lane never merges: it is a sub-frame, not a peer — its work is routed
|
|
1737
|
+
* per pass (`passLane`). */
|
|
1738
|
+
function merge(t, f) {
|
|
1739
|
+
// Both ends resolved: a stale pointer to a transaction already merged
|
|
1740
|
+
// into the other would close a cycle `resolveTx` never leaves.
|
|
1741
|
+
t = resolveTx(t);
|
|
1742
|
+
f = resolveTx(f);
|
|
1743
|
+
if (t === f || t._lane || f._lane) return;
|
|
1744
|
+
if (attrHooks !== null) attrHooks.transitionMerged(f, t);
|
|
1745
|
+
t._into = f;
|
|
1746
|
+
f._open += t._open;
|
|
1747
|
+
f._acted ||= t._acted;
|
|
1748
|
+
append(f._nodes, t._nodes);
|
|
1749
|
+
append(f._queues[0], t._queues[0]);
|
|
1750
|
+
append(f._queues[1], t._queues[1]);
|
|
1751
|
+
append(f._reruns, t._reruns);
|
|
1752
|
+
if (t._marks !== null) append((f._marks ??= []), t._marks);
|
|
1753
|
+
transactions.splice(transactions.indexOf(t), 1);
|
|
1754
|
+
}
|
|
1755
|
+
/** A15: a hold is a property of the async node — observed pending by a
|
|
1756
|
+
* render reader. A transaction is blocked while a node it holds, in flight,
|
|
1757
|
+
* has one: a pending render effect (the frame itself), or a pending held
|
|
1758
|
+
* node a render effect reads. A stale reader served the committed value
|
|
1759
|
+
* never went pending, but derives from the flight all the same (#3494) and
|
|
1760
|
+
* is still linked to it; a memo between them is pending, and held, too — so
|
|
1761
|
+
* one hop of `_subs` is the chain. Reads of the reader's LAST pass only (the
|
|
1762
|
+
* link's generation is the pass's): a tail kept for A30 — the committed
|
|
1763
|
+
* frame's dependency, awaiting the run that retires it — is not a read of
|
|
1764
|
+
* the flight, and a reader that stopped reading releases it (O3, #3494).
|
|
1765
|
+
* An errored pass (NotReady included) did not stop: it never got there, and
|
|
1766
|
+
* its full list stands (A30) — the reads it did not reach still observe. A
|
|
1767
|
+
* flight nobody renders holds nothing (A29). Only the nodes `t` still owns
|
|
1768
|
+
* count: one a lane took over since is the lane's to wait on. A guess whose
|
|
1769
|
+
* own truth is in flight blocks the lane's parent, not the lane — it stands
|
|
1770
|
+
* in for the flight (A17: "visible until its own fetch settles"). */
|
|
1771
|
+
function blocked(t) {
|
|
1772
|
+
// The transaction being judged (a lane consulted on its behalf judges for
|
|
1773
|
+
// it): a zombie whose removal it stages is moot for it alone (`onScreen`).
|
|
1774
|
+
const prev = judge;
|
|
1775
|
+
judge ??= t;
|
|
1776
|
+
// An action still running in it holds it open (action.ts). Its lanes
|
|
1777
|
+
// (lanes.ts): a lane under `t` blocks it while blocked itself.
|
|
1778
|
+
const r = t._open !== 0 || blockedBy(t._nodes, t) || !!lanesBlockedHook?.(t);
|
|
1779
|
+
judge = prev;
|
|
1780
|
+
return r;
|
|
1781
|
+
}
|
|
1782
|
+
let judge = null;
|
|
1783
|
+
/** Observe tier: the flights `t` waits on — its pending held sources a
|
|
1784
|
+
* frame reader observes (`blockedBy`'s predicate, collected; the readers
|
|
1785
|
+
* themselves are not sources). */
|
|
1786
|
+
function blockersOf(t) {
|
|
1787
|
+
const out = [];
|
|
1788
|
+
for (let i = 0; i < t._nodes.length; i++) {
|
|
1789
|
+
const n = t._nodes[i];
|
|
1790
|
+
if (
|
|
1791
|
+
!n._type &&
|
|
1792
|
+
n._statusFlags & STATUS_PENDING &&
|
|
1793
|
+
!(n._flags & REACTIVE_DISPOSED) &&
|
|
1794
|
+
(n._x._transaction === null || txOf(n) === t) &&
|
|
1795
|
+
blockedBy([n], t)
|
|
1796
|
+
)
|
|
1797
|
+
out.push(n);
|
|
1551
1798
|
}
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
// A lane's own flush (its flight landing, its reveal) parks nothing: the
|
|
1572
|
-
// lane's work reached it per pass; the frame's plain writes commit.
|
|
1573
|
-
// (Resolved: a guess over another transaction's guess merged them.)
|
|
1574
|
-
const t = joined !== null && !joined._lane ? resolveTx(joined) : null;
|
|
1575
|
-
if (t !== null) {
|
|
1576
|
-
// `t._nodes` grows under the walk (holdFrame pushes); the bound is
|
|
1577
|
-
// this flush's list, which does not.
|
|
1578
|
-
const count = pendingNodes.length;
|
|
1579
|
-
for (let i = 0; i < count; i++) {
|
|
1580
|
-
const n = pendingNodes[i];
|
|
1581
|
-
// Already held: the pass that re-staged it joined its transaction,
|
|
1582
|
-
// which is `t` (or merged into it). Lane work is the lane's (a
|
|
1583
|
-
// pending mark propagated onto it queued it here too).
|
|
1584
|
-
if (
|
|
1585
|
-
!(n._config & (CONFIG_HELD | CONFIG_OVERRIDE)) &&
|
|
1586
|
-
(n._x === null || n._x._transaction === null || !n._x._transaction._lane)
|
|
1587
|
-
) {
|
|
1588
|
-
// A loading source — pending with nothing staged and no committed
|
|
1589
|
-
// value (A16, A19 exc. 1: uninitialized is loading, not pending)
|
|
1590
|
-
// that is not an observer — is nobody's frame: not held. Its
|
|
1591
|
-
// first landing is commit #0, not a resumption of this frame, and
|
|
1592
|
-
// a write that reaches it later overlaps nothing (#2937: the
|
|
1593
|
-
// stale stamp was the bridge that entangled unrelated updates
|
|
1594
|
-
// with a never-resolving flight). Its observers are held and
|
|
1595
|
-
// wait on its flight (`blocked`).
|
|
1596
|
-
if (
|
|
1597
|
-
n._pendingValue === NOT_PENDING &&
|
|
1598
|
-
n._statusFlags & STATUS_UNINITIALIZED &&
|
|
1599
|
-
n._type !== EFFECT_RENDER
|
|
1600
|
-
)
|
|
1601
|
-
continue;
|
|
1602
|
-
// A34 (2): a tick whose writes net to the committed value made no
|
|
1603
|
-
// proposal (`setShow(false); setShow(true)` on a committed true) —
|
|
1604
|
-
// not staged, not held, pends nothing; a later write to it is
|
|
1605
|
-
// plain mainline. (A node already held stays held when written
|
|
1606
|
-
// back: A34 (1).)
|
|
1607
|
-
if (
|
|
1608
|
-
n._pendingValue !== NOT_PENDING &&
|
|
1609
|
-
!(n._statusFlags & (STATUS_PENDING | STATUS_UNINITIALIZED)) &&
|
|
1610
|
-
n._equals &&
|
|
1611
|
-
n._equals(n._value, n._pendingValue) &&
|
|
1612
|
-
n._x?._pendingFirstChild == null &&
|
|
1613
|
-
n._x?._pendingDisposal == null
|
|
1614
|
-
) {
|
|
1615
|
-
n._pendingValue = NOT_PENDING;
|
|
1616
|
-
n._config &= ~CONFIG_STAGED;
|
|
1617
|
-
continue;
|
|
1618
|
-
}
|
|
1619
|
-
holdNode(n, t);
|
|
1620
|
-
}
|
|
1621
|
-
// A held pass's frame is held with it (ruling A, O2): the children it
|
|
1622
|
-
// created are the transaction's — they compute, and a pass over them
|
|
1623
|
-
// or a write that reaches them joins; they effect at the landing.
|
|
1624
|
-
if (n._config & CONFIG_STAGED) holdFrame(n, t);
|
|
1625
|
-
}
|
|
1626
|
-
pendingNodes.length = 0;
|
|
1627
|
-
GlobalQueue._storePark?.(t);
|
|
1628
|
-
// (This flush's runs are stashed with `t` below — after the landings,
|
|
1629
|
-
// so a `t` that lands at this very seam runs them first, ahead of
|
|
1630
|
-
// the runs it held from earlier flushes.)
|
|
1631
|
-
// The batch joins: the writes that dirtied these are held with it — a
|
|
1632
|
-
// zombie never displays that world. A verdict reader among them does
|
|
1633
|
-
// (#3444: `latest()` inside a branch a held Show is removing peers
|
|
1634
|
-
// through like the same read outside): it re-runs now, as the
|
|
1635
|
-
// holder's verdict lane's work — before the lane seams, which reveal
|
|
1636
|
-
// it (nothing commits this seam that could dispose it).
|
|
1637
|
-
const zombies = deferredZombies.splice(0).sort((a, b) => a._height - b._height);
|
|
1638
|
-
for (let i = 0; i < zombies.length; i++)
|
|
1639
|
-
if (
|
|
1640
|
-
(zombies[i]._flags & (REACTIVE_DISPOSED | REACTIVE_ZOMBIE)) === REACTIVE_ZOMBIE &&
|
|
1641
|
-
zombies[i]._config & CONFIG_VERDICT
|
|
1642
|
-
)
|
|
1643
|
-
recompute(zombies[i]);
|
|
1644
|
-
// The unchanged passes' tails stay linked (A30, #3469): their inputs
|
|
1645
|
-
// are held, and the committed frame still derives from them — no trim.
|
|
1646
|
-
} else {
|
|
1647
|
-
commitPendingNodes();
|
|
1648
|
-
// The flush committed: an unchanged pass's stale tail goes now. Not
|
|
1649
|
-
// after a later pass this flush that threw (NotReady included — it
|
|
1650
|
-
// keeps its full list, `_depsTail` marking where it stopped) or that
|
|
1651
|
-
// left an effect owing a run (`runEffect` trims when the run applies):
|
|
1652
|
-
// `recompute`'s own gates.
|
|
1653
|
-
for (let i = 0; i < heldTrims.length; i++) {
|
|
1654
|
-
const t = heldTrims[i];
|
|
1655
|
-
if (t._x?._error == null && !t._modified) trimStaleDeps(t);
|
|
1656
|
-
}
|
|
1657
|
-
}
|
|
1658
|
-
// The list is the flush's either way. Reset only when there is something
|
|
1659
|
-
// to reset (`stagedReaders` below likewise): `length = 0` is a runtime
|
|
1660
|
-
// call even on an empty array, and the plain flush — no unchanged pass,
|
|
1661
|
-
// no lane work — has nothing in either.
|
|
1662
|
-
if (heldTrims.length) heldTrims.length = 0;
|
|
1663
|
-
// Lanes: a blocked one parks its frame (its own flight is up); an
|
|
1664
|
-
// unblocked one reveals this round's work. Then land every transaction
|
|
1665
|
-
// no frame is waiting on — its lanes end with it. Backwards: a landing
|
|
1666
|
-
// removes its entry.
|
|
1667
|
-
// This flush's own runs, the lanes' reveals, and the landings' held runs
|
|
1668
|
-
// are collected apart and ordered below.
|
|
1669
|
-
const own = this._queues;
|
|
1670
|
-
const lanes = (this._queues = [[], []]);
|
|
1671
|
-
// The passes that read this frame's stagings as the screen read held
|
|
1672
|
-
// writes if it parked: the lane seam re-derives them on the committed
|
|
1673
|
-
// world (a lane's runs wait this round).
|
|
1674
|
-
GlobalQueue._laneSeams?.(t !== null ? stagedReaders : null);
|
|
1675
|
-
if (stagedReaders.length) stagedReaders.length = 0;
|
|
1676
|
-
this._queues = [[], []];
|
|
1677
|
-
for (let k = transactions.length - 1; k >= 0; k--) {
|
|
1678
|
-
const u = transactions[k];
|
|
1679
|
-
if (blocked(u)) continue;
|
|
1680
|
-
transactions.splice(k, 1);
|
|
1681
|
-
GlobalQueue._endLanes?.(u);
|
|
1682
|
-
land(u);
|
|
1683
|
-
// A landing's commits can dispose a zombie a transaction judged above
|
|
1684
|
-
// was blocked on (#3463: live "until the commit that disposes it") —
|
|
1685
|
-
// judge them again.
|
|
1686
|
-
k = transactions.length;
|
|
1687
|
-
}
|
|
1688
|
-
// The store folds the pending backings whose container nodes committed
|
|
1689
|
-
// — this flush's, or a landing's (store/store.ts installs it).
|
|
1690
|
-
GlobalQueue._storeCommit?.();
|
|
1691
|
-
// Marks declared outside a transaction: the flush that carried them
|
|
1692
|
-
// parked (something async below) — the window is the frame's, the marks
|
|
1693
|
-
// are its transaction's; else they release at the seam, verdict-only,
|
|
1694
|
-
// nothing to show (affects.ts).
|
|
1695
|
-
const live = t && liveTx(t);
|
|
1696
|
-
GlobalQueue._releaseAmbientMarks?.(live);
|
|
1697
|
-
// The effect phase: lane work first (displayed ahead of the frame), then
|
|
1698
|
-
// this flush's runs, then the runs the landings held from earlier
|
|
1699
|
-
// flushes — as one pass would have queued them (#3540: a shell
|
|
1700
|
-
// re-derived at the landing before a boundary swap held since the
|
|
1701
|
-
// write; #3528: a display-ahead swap before the count it is shown
|
|
1702
|
-
// beside). A parked frame's runs wait with its transaction instead: the
|
|
1703
|
-
// effect phase applies a committed frame.
|
|
1704
|
-
const parked = live !== null;
|
|
1705
|
-
// Attribution hook: this flush found its transaction incomplete — its
|
|
1706
|
-
// writes stay staged, its runs are stashed below. Before the effect
|
|
1707
|
-
// phase (the lanes' display-ahead runs are the visible acknowledgers);
|
|
1708
|
-
// `holdEnd` fires after it (`flush`).
|
|
1709
|
-
if (parked && attrHooks !== null) {
|
|
1710
|
-
attrHooks.holdStart(live);
|
|
1711
|
-
holding = true;
|
|
1712
|
-
}
|
|
1713
|
-
for (let i = 0; i < 2; i++) {
|
|
1714
|
-
if (parked) append(live._queues[i], own[i]);
|
|
1715
|
-
// The plain flush — no reveal, no landing, not parked — has nothing to
|
|
1716
|
-
// order around its own runs: they are the queue as they stand (the
|
|
1717
|
-
// same array, not a copy; `run` takes it whole). Every other seam
|
|
1718
|
-
// builds the ordered queue above.
|
|
1719
|
-
this._queues[i] =
|
|
1720
|
-
parked || lanes[i].length || this._queues[i].length
|
|
1721
|
-
? lanes[i].concat(parked ? [] : own[i], this._queues[i])
|
|
1722
|
-
: own[i];
|
|
1723
|
-
}
|
|
1724
|
-
if (deferredZombies.length !== 0) {
|
|
1725
|
-
// Survivors are the displayed frame of a held pass: they rerun for the
|
|
1726
|
-
// live write, parents before children.
|
|
1727
|
-
const zombies = deferredZombies.splice(0).sort((a, b) => a._height - b._height);
|
|
1728
|
-
for (let i = 0; i < zombies.length; i++) {
|
|
1729
|
-
if (!(zombies[i]._flags & REACTIVE_DISPOSED)) recompute(zombies[i]);
|
|
1730
|
-
}
|
|
1731
|
-
}
|
|
1732
|
-
// Boundaries: readers that settled, landed or died this flush are
|
|
1733
|
-
// dropped; a fallback with none left reveals next round.
|
|
1734
|
-
GlobalQueue._boundarySeam?.();
|
|
1799
|
+
return out;
|
|
1800
|
+
}
|
|
1801
|
+
/** `t`'s own flights only — not its lanes': a flight the frame asked for
|
|
1802
|
+
* (a refetch, a plain load) is authoritative; a lane's derivation flight is
|
|
1803
|
+
* not (lanes.ts, the body-end corollary). A pending reader counts only if
|
|
1804
|
+
* one of the sources it waits on is `t`'s (a render effect marked pending
|
|
1805
|
+
* by a lane flight, never re-run, is the lane's wait, not the frame's). */
|
|
1806
|
+
function ownFlights(t) {
|
|
1807
|
+
return blockedBy(t._nodes, t, true);
|
|
1808
|
+
}
|
|
1809
|
+
/** A source the pending node waits on that is `t`'s (held by it, or by no
|
|
1810
|
+
* one) — not another transaction's or lane's work. Itself, when it is the
|
|
1811
|
+
* source. */
|
|
1812
|
+
function ownSource(n, t) {
|
|
1813
|
+
const sources = n._x?._pendingSources;
|
|
1814
|
+
if (sources === undefined) return true;
|
|
1815
|
+
for (const s of sources) {
|
|
1816
|
+
const u = s._x?._transaction;
|
|
1817
|
+
if (u == null || resolveTx(u) === t) return true;
|
|
1735
1818
|
}
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1819
|
+
return false;
|
|
1820
|
+
}
|
|
1821
|
+
function blockedBy(nodes, owner, own = false) {
|
|
1822
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
1823
|
+
const n = nodes[i];
|
|
1824
|
+
if (
|
|
1825
|
+
!(n._statusFlags & STATUS_PENDING) ||
|
|
1826
|
+
n._flags & REACTIVE_DISPOSED ||
|
|
1827
|
+
n._config & CONFIG_GUESS ||
|
|
1828
|
+
(n._x._transaction !== null && txOf(n) !== owner) ||
|
|
1829
|
+
(own && !ownSource(n, owner))
|
|
1830
|
+
)
|
|
1831
|
+
continue;
|
|
1832
|
+
// The frame's own pending observer (off screen: this one holds nothing;
|
|
1833
|
+
// the next listed node may).
|
|
1834
|
+
if (n._type === EFFECT_RENDER) {
|
|
1835
|
+
if (onScreen(n, judge ?? owner)) return true;
|
|
1836
|
+
continue;
|
|
1741
1837
|
}
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
* never hears of it. */
|
|
1765
|
-
notify(node, mask, flags, error) {
|
|
1766
|
-
if (GlobalQueue._catch && GlobalQueue._catch(node, flags & mask, error)) return true;
|
|
1767
|
-
if (mask & STATUS_PENDING) {
|
|
1768
|
-
if (flags & STATUS_PENDING) {
|
|
1769
|
-
if (_enforceLoadingBoundary) _hitUnhandledAsync = true;
|
|
1770
|
-
// A pass that went pending holds its frame: the async's own
|
|
1771
|
-
// synchronous frame is not ready (A15 reveal corollary: a reveal
|
|
1772
|
-
// that discovers a flight whose inputs are visible holds and joins
|
|
1773
|
-
// it; one whose inputs are held is a stale reader and never gets
|
|
1774
|
-
// here — `read` serves it the committed value). The frame's, not the
|
|
1775
|
-
// observer's: a render effect belongs to whoever dirtied its pass
|
|
1776
|
-
// (A15 shared-hole, #3407) — one held by T1 and re-run by T2's write
|
|
1777
|
-
// pends for T2's flight, and the two stay parallel; a frame that is
|
|
1778
|
-
// T1's already joined T1 through the write or the memo that made it
|
|
1779
|
-
// so. Outside a flush — root setup, a mount — the frame is the
|
|
1780
|
-
// mount's: its pending observers are one transaction, and a flight
|
|
1781
|
-
// started in it resumes it when it lands (maintainer, #3461: "part
|
|
1782
|
-
// of the same one"); what the mount published synchronously stays
|
|
1783
|
-
// published. Lane work going pending holds the lane, not the frame:
|
|
1784
|
-
// `blocked(lane)` finds it in the lane's staging ("if further async
|
|
1785
|
-
// downstream is hit it holds like its own transition").
|
|
1786
|
-
if (passLane === null) joinFuture(null);
|
|
1787
|
-
}
|
|
1788
|
-
return true;
|
|
1838
|
+
for (let s = n._subs; s !== null; s = s._nextSub) {
|
|
1839
|
+
const r = s._sub;
|
|
1840
|
+
// A frame reader: a render effect, or a stale reader of the flight
|
|
1841
|
+
// (REACTIVE_FRAME_READ — a verdict reader, `observeFlight`; lane work,
|
|
1842
|
+
// `frameRead`): served committed instead of going pending, it derives
|
|
1843
|
+
// from the flight all the same and is re-derived at the landing — the
|
|
1844
|
+
// frame's observation survives its reader becoming a lane's (a guess
|
|
1845
|
+
// over a held window, V5/A17). A probe alone does not hold. A
|
|
1846
|
+
// boundary showing content forwards the pending (its output pending,
|
|
1847
|
+
// CONFIG_REDERIVE): transparent, its readers hold through it (A33; the
|
|
1848
|
+
// lane-membership ruling, 2026-10-05). An output that never committed
|
|
1849
|
+
// shows nothing — a mount the frame has not revealed — and holds
|
|
1850
|
+
// nothing.
|
|
1851
|
+
if (
|
|
1852
|
+
((r._type === EFFECT_RENDER && r._statusFlags & STATUS_PENDING) ||
|
|
1853
|
+
r._flags & REACTIVE_FRAME_READ ||
|
|
1854
|
+
(r._config & CONFIG_REDERIVE && r._statusFlags === STATUS_PENDING)) &&
|
|
1855
|
+
(s._gen === r._depGen || r._x?._error != null) &&
|
|
1856
|
+
!(r._flags & REACTIVE_DISPOSED) &&
|
|
1857
|
+
onScreen(r, judge ?? owner)
|
|
1858
|
+
)
|
|
1859
|
+
return true;
|
|
1789
1860
|
}
|
|
1790
|
-
return false;
|
|
1791
1861
|
}
|
|
1862
|
+
return false;
|
|
1863
|
+
}
|
|
1864
|
+
/** A frame reader whose say counts for `t`'s hold — `t` the transaction
|
|
1865
|
+
* being judged: not a zombie whose removal `t` stages (A15 #3463: a re-ask
|
|
1866
|
+
* in the unmount frame lands its pending on the zombie it unmounts — the
|
|
1867
|
+
* compiled <Show> of #3372; a zombie blocks every other judgment while it
|
|
1868
|
+
* is visible, a lane's reveal included: only the commit that disposes it
|
|
1869
|
+
* makes its say moot), and not behind a fallback (A33: a boundary showing its fallback is the display of
|
|
1870
|
+
* everything under it, so a reader there is not on screen and holds
|
|
1871
|
+
* nothing — boundaries.ts). */
|
|
1872
|
+
function onScreen(r, t) {
|
|
1873
|
+
return !(r._flags & REACTIVE_ZOMBIE && removalStagedBy(r, t)) && !hiddenHook?.(r);
|
|
1874
|
+
}
|
|
1875
|
+
/** A15 (#3463): a reader whose removal is staged — a zombie, parked by a
|
|
1876
|
+
* pass awaiting its commit — is still on screen and live for every hold,
|
|
1877
|
+
* except the transaction staging its removal: for that one its say is moot
|
|
1878
|
+
* (done, and the commit disposes it; not done, and it stays parked). The
|
|
1879
|
+
* staging transaction is the one holding the pass that parked the frame:
|
|
1880
|
+
* the nearest non-zombie ancestor. */
|
|
1881
|
+
function removalStagedBy(r, t) {
|
|
1882
|
+
let o = r;
|
|
1883
|
+
while (o?._flags & REACTIVE_ZOMBIE) o = o._parent;
|
|
1884
|
+
return o?._config & CONFIG_HELD && o._x?._transaction != null && txOf(o) === resolveTx(t);
|
|
1885
|
+
}
|
|
1886
|
+
/** Unchanged passes this flush whose stale dependency tail awaits the
|
|
1887
|
+
* flush's verdict (A30, #3469): trimmed when the flush commits, kept when
|
|
1888
|
+
* it parks — the committed frame still derives from the previous pass's
|
|
1889
|
+
* dependencies, and a write to one of them must reach the node until a
|
|
1890
|
+
* committing pass replaces it. */
|
|
1891
|
+
const heldTrims = [];
|
|
1892
|
+
function heldTrim(el) {
|
|
1893
|
+
heldTrims.push(el);
|
|
1894
|
+
}
|
|
1895
|
+
/** Zombies popped from the dirty heap this flush (`deferZombie`). The seam
|
|
1896
|
+
* decides their fate: a park cancels them (the writes that dirtied them are
|
|
1897
|
+
* held with it); a commit runs the survivors — the ones whose owner is still
|
|
1898
|
+
* awaiting its commit — in height order, after the commits that would have
|
|
1899
|
+
* disposed them (#3546). No second heap: the list is the deferral. */
|
|
1900
|
+
const deferredZombies = [];
|
|
1901
|
+
function deferZombie(el) {
|
|
1902
|
+
deleteFromHeap(el, dirtyQueue);
|
|
1903
|
+
el._flags &= -4;
|
|
1904
|
+
deferredZombies.push(el);
|
|
1905
|
+
}
|
|
1906
|
+
/** Put a staged node in a transaction: flagged, stamped, on its landing
|
|
1907
|
+
* list. */
|
|
1908
|
+
function holdNode(n, t) {
|
|
1909
|
+
n._config |= CONFIG_HELD;
|
|
1910
|
+
list(n, t);
|
|
1792
1911
|
}
|
|
1793
|
-
/**
|
|
1794
|
-
*
|
|
1795
|
-
*
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1912
|
+
/** Put a node on a transaction's list, once: a node's `_transaction` is the
|
|
1913
|
+
* transaction whose `_nodes` it is on (every site that sets one does the
|
|
1914
|
+
* other), so a re-pass, a re-landing or a re-staging adds nothing. */
|
|
1915
|
+
function list(n, t) {
|
|
1916
|
+
const x = ext(n);
|
|
1917
|
+
if (x._transaction === t) return;
|
|
1918
|
+
x._transaction = t;
|
|
1919
|
+
t._nodes.push(n);
|
|
1920
|
+
}
|
|
1921
|
+
/** Hold the (uncommitted) frame under a held pass: every computed in the
|
|
1922
|
+
* live child chain, recursively. Plain owners are walked through, not held
|
|
1923
|
+
* — nothing reads or re-passes them. Once per park, over the parked frames
|
|
1924
|
+
* only; recomputes pay nothing for it. A lane's node in the frame (listed on
|
|
1925
|
+
* a lane: a verdict reader broken out of this very hold — display-ahead — or
|
|
1926
|
+
* a derivation of a guess, CONFIG_OVERRIDE; a lane pass's effect) is the
|
|
1927
|
+
* lane's, as the seam's own loop has it: "lane work never makes its node
|
|
1928
|
+
* transaction work" (A15, #3698). Re-listed here it would carry the lane's
|
|
1929
|
+
* value on the transaction's list, where the landing nulls its transaction
|
|
1930
|
+
* and `dissolveLane` no longer finds it — a lane node with no lane, which the
|
|
1931
|
+
* next read of it trips over (`laneRead` → `txOf`). Its frame is the lane's
|
|
1932
|
+
* too (ruling A: a lane pass's children are the lane's), revealed and
|
|
1933
|
+
* retired at the lane's seam. */
|
|
1934
|
+
function holdFrame(owner, t) {
|
|
1935
|
+
for (let c = owner._firstChild; c !== null; c = c._nextSibling) {
|
|
1936
|
+
if (c._x?._transaction?._lane) continue;
|
|
1937
|
+
if (c._fn !== undefined) {
|
|
1938
|
+
if (!(c._config & CONFIG_HELD)) holdNode(c, t);
|
|
1939
|
+
c._config |= CONFIG_HELD_FRAME;
|
|
1815
1940
|
}
|
|
1941
|
+
holdFrame(c, t);
|
|
1816
1942
|
}
|
|
1817
|
-
u._reruns.length = 0;
|
|
1818
1943
|
}
|
|
1819
|
-
/** The
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
if (u._marks !== null) GlobalQueue._releaseMarks(u._marks);
|
|
1826
|
-
// Old children die in the commits (cleanups first), then the stashed
|
|
1827
|
-
// effects run ahead of this flush's own. A node a lane took over since
|
|
1828
|
-
// (a guess written over the staged truth) is the lane's to land.
|
|
1829
|
-
for (let i = 0; i < u._nodes.length; i++) {
|
|
1830
|
-
const n = u._nodes[i];
|
|
1831
|
-
if (n._x._transaction === null || txOf(n) !== u) continue;
|
|
1832
|
-
n._x._transaction = null;
|
|
1833
|
-
commitPendingNode(n);
|
|
1944
|
+
/** The nodes under `owner` a transaction other than `t` holds (boundaries.ts:
|
|
1945
|
+
* what an earlier hold staged under a re-armed boundary). */
|
|
1946
|
+
function heldUnder(owner, t, fn) {
|
|
1947
|
+
for (let c = owner._firstChild; c !== null; c = c._nextSibling) {
|
|
1948
|
+
if ((c._config & (CONFIG_HELD | CONFIG_OVERRIDE)) === CONFIG_HELD && txOf(c) !== t) fn(c);
|
|
1949
|
+
heldUnder(c, t, fn);
|
|
1834
1950
|
}
|
|
1835
|
-
releaseQueues(u);
|
|
1836
1951
|
}
|
|
1837
|
-
/**
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
function releaseQueues(u) {
|
|
1842
|
-
for (let i = 0; i < 2; i++) {
|
|
1843
|
-
append(globalQueue._queues[i], u._queues[i]);
|
|
1844
|
-
u._queues[i] = [];
|
|
1845
|
-
}
|
|
1952
|
+
/** `n` leaves the hold it was staged in: its next pass publishes mainline. */
|
|
1953
|
+
function unhold(n) {
|
|
1954
|
+
n._x._transaction = null;
|
|
1955
|
+
n._config &= ~CONFIG_HELD;
|
|
1846
1956
|
}
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1957
|
+
const dirtyQueue = {
|
|
1958
|
+
_heap: new Array(2000).fill(undefined),
|
|
1959
|
+
_marked: false,
|
|
1960
|
+
_min: 0,
|
|
1961
|
+
_max: 0
|
|
1962
|
+
};
|
|
1963
|
+
let clock = 0;
|
|
1964
|
+
let scheduled = false;
|
|
1965
|
+
let halted = false;
|
|
1966
|
+
let haltNotified = false;
|
|
1967
|
+
// Dev: where the halt happened (the owner path and source lines of its
|
|
1968
|
+
// diagnostic), repeated on the "Update ignored" notice so a frozen interaction
|
|
1969
|
+
// points back at the computation that threw (#4178).
|
|
1970
|
+
let haltedAt = "";
|
|
1971
|
+
// Dev: the computation the halt in progress is located at (haltReactivityAt).
|
|
1972
|
+
let haltSubject;
|
|
1973
|
+
let syncDepth = 0;
|
|
1974
|
+
let inTrackedQueueCallback = false;
|
|
1975
|
+
let _enforceLoadingBoundary = false;
|
|
1976
|
+
let _hitUnhandledAsync = false;
|
|
1977
|
+
// Once per enforcement window: the ASYNC_OUTSIDE_LOADING_BOUNDARY finding is a
|
|
1978
|
+
// fact about the MOUNT ("the root mount will be deferred"), not about each
|
|
1979
|
+
// pending render effect — N async siblings at mount used to produce N copies.
|
|
1980
|
+
let _reportedUnhandledAsync = false;
|
|
1981
|
+
/**
|
|
1982
|
+
* Consume the unhandled-async hit. Returns whether this is the first report
|
|
1983
|
+
* of the current enforcement window — the caller warns only then.
|
|
1984
|
+
*/
|
|
1985
|
+
function resetUnhandledAsync() {
|
|
1986
|
+
_hitUnhandledAsync = false;
|
|
1987
|
+
if (_reportedUnhandledAsync) return false;
|
|
1988
|
+
_reportedUnhandledAsync = true;
|
|
1989
|
+
return true;
|
|
1850
1990
|
}
|
|
1851
|
-
/**
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
*
|
|
1855
|
-
*
|
|
1856
|
-
*
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
if (!(c._flags & REACTIVE_FRAME_READ)) {
|
|
1863
|
-
c._flags |= REACTIVE_FRAME_READ;
|
|
1864
|
-
t._reruns.push(c);
|
|
1865
|
-
}
|
|
1991
|
+
/**
|
|
1992
|
+
* Toggles the dev-mode "must be inside a `<Loading>` boundary" enforcement
|
|
1993
|
+
* window. Only `render()` calls this — wrapping the initial mount so that a
|
|
1994
|
+
* top-level uncaught async read surfaces the diagnostic. Not part of the
|
|
1995
|
+
* user-facing API.
|
|
1996
|
+
*
|
|
1997
|
+
* @internal
|
|
1998
|
+
*/
|
|
1999
|
+
function enforceLoadingBoundary(enabled) {
|
|
2000
|
+
_enforceLoadingBoundary = enabled;
|
|
2001
|
+
if (enabled) _reportedUnhandledAsync = false;
|
|
1866
2002
|
}
|
|
1867
|
-
function
|
|
1868
|
-
|
|
1869
|
-
pendingNodes.push(node);
|
|
1870
|
-
if (!globalQueue._running) markUnflushedStaged(); // A28
|
|
2003
|
+
function setTrackedQueueCallback(value) {
|
|
2004
|
+
inTrackedQueueCallback = value;
|
|
1871
2005
|
}
|
|
1872
|
-
// Dev-only
|
|
1873
|
-
//
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
let notifyEpoch = 0;
|
|
1881
|
-
function bumpNotifyEpoch() {
|
|
1882
|
-
notifyEpoch++;
|
|
2006
|
+
// Dev-only marker for the effect half of createEffect/createRenderEffect, so
|
|
2007
|
+
// flush() can report the no-op instead of failing silently (React parity).
|
|
2008
|
+
/** Action bodies on the stack (action.ts): `flush()` is refused inside one —
|
|
2009
|
+
* the body's writes are its transaction's and commit when it settles; a
|
|
2010
|
+
* drain mid-step cannot reveal them, and dev says so. */
|
|
2011
|
+
let actionDepth = 0;
|
|
2012
|
+
function enterAction() {
|
|
2013
|
+
actionDepth++;
|
|
1883
2014
|
}
|
|
1884
|
-
function
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
2015
|
+
function exitAction() {
|
|
2016
|
+
actionDepth--;
|
|
2017
|
+
}
|
|
2018
|
+
/** Provenance (A18, #3331): the question being asked — an action's
|
|
2019
|
+
* sequence while a slice of its body and the flush that carries it run, 0
|
|
2020
|
+
* otherwise. A guess is stamped with it (lanes.ts; a mainline guess takes a
|
|
2021
|
+
* fresh sequence: the newest intent), a flight with the question that
|
|
2022
|
+
* started it (async.ts; mainline is always the current question). An
|
|
2023
|
+
* answer from an older question than the guess it lands on is stale: held
|
|
2024
|
+
* silently, it does not move the graph. */
|
|
2025
|
+
let question = 0;
|
|
2026
|
+
let questions = 0;
|
|
2027
|
+
function setQuestion(q) {
|
|
2028
|
+
question = q;
|
|
2029
|
+
}
|
|
2030
|
+
function nextQuestion() {
|
|
2031
|
+
return ++questions;
|
|
2032
|
+
}
|
|
2033
|
+
const MAINLINE_QUESTION = 0x7fffffff;
|
|
2034
|
+
let inEffectCallback = false;
|
|
2035
|
+
function setEffectCallback(value) {
|
|
2036
|
+
inEffectCallback = value;
|
|
2037
|
+
}
|
|
2038
|
+
function schedule() {
|
|
2039
|
+
if (halted) {
|
|
2040
|
+
notifyHalted();
|
|
2041
|
+
return;
|
|
2042
|
+
}
|
|
2043
|
+
if (scheduled) return;
|
|
2044
|
+
scheduled = true;
|
|
2045
|
+
if (!syncDepth && !globalQueue._running) {
|
|
2046
|
+
queueMicrotask(flush);
|
|
1914
2047
|
}
|
|
1915
|
-
if (fanOut >= GRAPH_SIZE_WARN_AT) noteFanOut(node, fanOut);
|
|
1916
2048
|
}
|
|
1917
|
-
/**
|
|
1918
|
-
*
|
|
1919
|
-
*
|
|
1920
|
-
*
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
*
|
|
1924
|
-
*
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
2049
|
+
/**
|
|
2050
|
+
* Permanently halts the reactive system. Called when a user error escapes
|
|
2051
|
+
* every boundary — app state is undefined at that point, so scheduling stops
|
|
2052
|
+
* entirely rather than limping along with a half-applied update.
|
|
2053
|
+
*/
|
|
2054
|
+
/**
|
|
2055
|
+
* The key a root owner carries its client error hook under (`render`'s
|
|
2056
|
+
* `onError`) — registered, so a runtime writes it with no import of the hook
|
|
2057
|
+
* module (core/error-hooks.ts) and no property mangling in the way. Defined
|
|
2058
|
+
* HERE, not there: a runtime that only writes the key must not retain the
|
|
2059
|
+
* hook machinery (pay-for-use).
|
|
2060
|
+
*/
|
|
2061
|
+
const ROOT_ERROR_HOOK = Symbol.for("solid-js/root-error-hook");
|
|
2062
|
+
function haltReactivity(cause) {
|
|
2063
|
+
if (halted) return;
|
|
2064
|
+
halted = true;
|
|
2065
|
+
// A cleanup error collected before the halt is dropped (owner.ts).
|
|
2066
|
+
rethrowCleanupError(true);
|
|
2067
|
+
let message = "[REACTIVITY_HALTED]";
|
|
2068
|
+
{
|
|
2069
|
+
message +=
|
|
2070
|
+
" An uncaught error halted the reactive system. No further updates will be processed. Handle errors with <Errored> or treat this as a crash.";
|
|
2071
|
+
// Located at the computation the error escaped from when the call site
|
|
2072
|
+
// named it (haltReactivityAt), so the console says where it happened
|
|
2073
|
+
// (`in <App> › <Composer>`), not only that it did (#4178). Otherwise the
|
|
2074
|
+
// ambient owner, as every other diagnostic.
|
|
2075
|
+
const entry = emitDiagnostic(
|
|
2076
|
+
{
|
|
2077
|
+
code: "REACTIVITY_HALTED",
|
|
2078
|
+
kind: "error",
|
|
2079
|
+
severity: "error",
|
|
2080
|
+
message
|
|
2081
|
+
},
|
|
2082
|
+
haltSubject
|
|
2083
|
+
);
|
|
2084
|
+
haltedAt = locationLines(entry);
|
|
2085
|
+
message += haltedAt;
|
|
1929
2086
|
}
|
|
2087
|
+
// Surface the cause here too: callers rethrow it, but a creation-time throw
|
|
2088
|
+
// unwinds through ancestor recomputes that convert it to status instead of
|
|
2089
|
+
// surfacing it (#2884), so the rethrow alone cannot guarantee visibility.
|
|
2090
|
+
// Where the platform has one, hand the cause to its uncaught-error channel
|
|
2091
|
+
// (`reportError` → `error` event → window.onerror / error monitoring): a
|
|
2092
|
+
// halt that only reaches console.error leaves a page that LOOKS alive with
|
|
2093
|
+
// nothing an app or its telemetry can act on (#3338 — an uncaught throw
|
|
2094
|
+
// during the hydration render). The rethrow may reach the top as well in
|
|
2095
|
+
// the non-swallowed cases; a duplicate report beats a silent one.
|
|
2096
|
+
const report = cause !== undefined && globalThis.reportError;
|
|
2097
|
+
report || cause === undefined ? console.error(message) : console.error(message, cause);
|
|
2098
|
+
report && report(cause);
|
|
1930
2099
|
}
|
|
1931
|
-
/**
|
|
1932
|
-
|
|
1933
|
-
|
|
2100
|
+
/**
|
|
2101
|
+
* Dev: `haltReactivity`, located at `subject` — the computation the uncaught
|
|
2102
|
+
* error escaped from — for the REACTIVITY_HALTED message's owner path. Call
|
|
2103
|
+
* sites pick it in dev builds only (`haltReactivity(e)` in production), so
|
|
2104
|
+
* production keeps the one-argument halt and its bytes unchanged.
|
|
2105
|
+
*/
|
|
2106
|
+
function haltReactivityAt(cause, subject) {
|
|
2107
|
+
haltSubject = subject;
|
|
2108
|
+
haltReactivity(cause);
|
|
2109
|
+
haltSubject = undefined;
|
|
1934
2110
|
}
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
2111
|
+
// Logs on the first write after a halt so a frozen interaction is traceable.
|
|
2112
|
+
function notifyHalted() {
|
|
2113
|
+
if (haltNotified) return;
|
|
2114
|
+
haltNotified = true;
|
|
2115
|
+
console.error(
|
|
2116
|
+
"[REACTIVITY_HALTED] Update ignored: the reactive system was halted by an earlier uncaught error." +
|
|
2117
|
+
haltedAt
|
|
2118
|
+
);
|
|
2119
|
+
}
|
|
2120
|
+
/** @internal Test/dev-reload hook. Revives scheduling after a halt. */
|
|
2121
|
+
function resetErrorHalt() {
|
|
2122
|
+
halted = false;
|
|
2123
|
+
haltNotified = false;
|
|
2124
|
+
haltedAt = "";
|
|
2125
|
+
}
|
|
2126
|
+
class GlobalQueue {
|
|
2127
|
+
_queues = [[], []];
|
|
2128
|
+
_running = false;
|
|
2129
|
+
flush() {
|
|
2130
|
+
if (this._running) return;
|
|
2131
|
+
this._running = true;
|
|
2132
|
+
// A28: companions of nodes written since the last flush mirror the
|
|
2133
|
+
// flushed world — re-synced inside the running window (the write is
|
|
2134
|
+
// "flushed" from here on).
|
|
2135
|
+
resyncUnflushedCompanions();
|
|
2136
|
+
passLane = null;
|
|
2137
|
+
passTx = null;
|
|
2138
|
+
try {
|
|
2139
|
+
// Sweep before the heap: unobserved() pulls swept nodes out of it, so
|
|
2140
|
+
// a dormant memo dirtied in the same tick is reclaimed instead of
|
|
2141
|
+
// recomputed; late subscribers (an effect reading a swept memo this
|
|
2142
|
+
// flush) revive it, which is the pay-for-use contract.
|
|
2143
|
+
sweepDormant();
|
|
2144
|
+
runHeap(dirtyQueue, GlobalQueue._update);
|
|
2145
|
+
this.settle();
|
|
2146
|
+
clock++;
|
|
2147
|
+
// A write the commit sweep staged with no subscriber to dirty is work
|
|
2148
|
+
// too — the next round commits it.
|
|
2149
|
+
scheduled = dirtyQueue._max >= dirtyQueue._min || pendingNodes.length !== 0;
|
|
2150
|
+
this.run(EFFECT_RENDER);
|
|
2151
|
+
this.run(EFFECT_USER);
|
|
2152
|
+
if (true && holding) {
|
|
2153
|
+
holding = false;
|
|
2154
|
+
attrHooks.holdEnd();
|
|
2155
|
+
}
|
|
2156
|
+
if (true && !scheduled) {
|
|
2157
|
+
// Fully drained: no staged value may survive this point unqueued or
|
|
2158
|
+
// unheld.
|
|
2159
|
+
devCheckQuiescent(
|
|
2160
|
+
n => pendingNodes.includes(n) || (n._config & (CONFIG_HELD | CONFIG_OVERRIDE)) !== 0
|
|
2161
|
+
);
|
|
2162
|
+
}
|
|
2163
|
+
if (true) DEV.hooks.onUpdate?.();
|
|
2164
|
+
} finally {
|
|
2165
|
+
this._running = false;
|
|
2166
|
+
passTx = null;
|
|
1947
2167
|
}
|
|
1948
|
-
return;
|
|
1949
2168
|
}
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
//
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
// The
|
|
1964
|
-
|
|
1965
|
-
//
|
|
1966
|
-
//
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
2169
|
+
/** L2 — the seam: end of the pure phase. Commit this flush's staged nodes,
|
|
2170
|
+
* or park them with the transaction the flush joined; then land every
|
|
2171
|
+
* transaction no frame is waiting on. The landing flush is an ordinary
|
|
2172
|
+
* flush: the settle walk (async.ts) re-ran the parked readers in its pure
|
|
2173
|
+
* phase, and here no flight the transaction holds has a frame deriving
|
|
2174
|
+
* from it (`blocked`). */
|
|
2175
|
+
settle() {
|
|
2176
|
+
// Lanes: an ended action body judges its guesses now that their passes
|
|
2177
|
+
// have run (lanes.ts); a correction's re-derivations run in one more
|
|
2178
|
+
// pure round — the parent's flights before the parent is judged below.
|
|
2179
|
+
if (laneCorrectionsHook?.()) runHeap(dirtyQueue, GlobalQueue._update);
|
|
2180
|
+
const joined = flushTransaction;
|
|
2181
|
+
flushTransaction = null;
|
|
2182
|
+
// The frame's verdict is in: the guesses written since the last seam
|
|
2183
|
+
// open their lane under the frame's transaction (a lane's own flush
|
|
2184
|
+
// parents a nested lane to it), or are dropped — the frame commits, and
|
|
2185
|
+
// there is nothing to be optimistic over. Their passes run next round;
|
|
2186
|
+
// a parent landing at this very seam ends the lane before they do.
|
|
2187
|
+
applyGuessesHook?.(joined);
|
|
2188
|
+
// A lane's own flush (its flight landing, its reveal) parks nothing: the
|
|
2189
|
+
// lane's work reached it per pass; the frame's plain writes commit.
|
|
2190
|
+
// (Resolved: a guess over another transaction's guess merged them.)
|
|
2191
|
+
const t = joined !== null && !joined._lane ? resolveTx(joined) : null;
|
|
2192
|
+
if (t !== null) {
|
|
2193
|
+
// `t._nodes` grows under the walk (holdFrame pushes); the bound is
|
|
2194
|
+
// this flush's list, which does not.
|
|
2195
|
+
const count = pendingNodes.length;
|
|
2196
|
+
for (let i = 0; i < count; i++) {
|
|
2197
|
+
const n = pendingNodes[i];
|
|
2198
|
+
// Already held: the pass that re-staged it joined its transaction,
|
|
2199
|
+
// which is `t` (or merged into it). Lane work is the lane's (a
|
|
2200
|
+
// pending mark propagated onto it queued it here too).
|
|
2201
|
+
if (
|
|
2202
|
+
!(n._config & (CONFIG_HELD | CONFIG_OVERRIDE)) &&
|
|
2203
|
+
(n._x === null || n._x._transaction === null || !n._x._transaction._lane)
|
|
2204
|
+
) {
|
|
2205
|
+
// A loading source — pending with nothing staged and no committed
|
|
2206
|
+
// value (A16, A19 exc. 1: uninitialized is loading, not pending)
|
|
2207
|
+
// that is not an observer — is nobody's frame: not held. Its
|
|
2208
|
+
// first landing is commit #0, not a resumption of this frame, and
|
|
2209
|
+
// a write that reaches it later overlaps nothing (#2937: the
|
|
2210
|
+
// stale stamp was the bridge that entangled unrelated updates
|
|
2211
|
+
// with a never-resolving flight). Its observers are held and
|
|
2212
|
+
// wait on its flight (`blocked`).
|
|
2213
|
+
if (
|
|
2214
|
+
n._pendingValue === NOT_PENDING &&
|
|
2215
|
+
n._statusFlags & STATUS_UNINITIALIZED &&
|
|
2216
|
+
n._type !== EFFECT_RENDER
|
|
2217
|
+
)
|
|
2218
|
+
continue;
|
|
2219
|
+
// A34 (2): a tick whose writes net to the committed value made no
|
|
2220
|
+
// proposal (`setShow(false); setShow(true)` on a committed true) —
|
|
2221
|
+
// not staged, not held, pends nothing; a later write to it is
|
|
2222
|
+
// plain mainline. (A node already held stays held when written
|
|
2223
|
+
// back: A34 (1).)
|
|
2224
|
+
if (
|
|
2225
|
+
n._pendingValue !== NOT_PENDING &&
|
|
2226
|
+
!(n._statusFlags & (STATUS_PENDING | STATUS_UNINITIALIZED)) &&
|
|
2227
|
+
n._x?._pendingFirstChild == null &&
|
|
2228
|
+
n._x?._pendingDisposal == null &&
|
|
2229
|
+
// The comparator last: a pass that created children (an object
|
|
2230
|
+
// owning a fresh async memo, #4111) must not have it read them.
|
|
2231
|
+
// A NotReady read there is a change (`isSame`).
|
|
2232
|
+
isSame(n, n._value, n._pendingValue)
|
|
2233
|
+
) {
|
|
2234
|
+
n._pendingValue = NOT_PENDING;
|
|
2235
|
+
n._config &= ~CONFIG_STAGED;
|
|
2236
|
+
continue;
|
|
2237
|
+
}
|
|
2238
|
+
holdNode(n, t);
|
|
2239
|
+
}
|
|
2240
|
+
// A held pass's frame is held with it (ruling A, O2): the children it
|
|
2241
|
+
// created are the transaction's — they compute, and a pass over them
|
|
2242
|
+
// or a write that reaches them joins; they effect at the landing.
|
|
2243
|
+
if (n._config & CONFIG_STAGED) holdFrame(n, t);
|
|
2244
|
+
}
|
|
2245
|
+
pendingNodes.length = 0;
|
|
2246
|
+
storeParkHook?.(t);
|
|
2247
|
+
// (This flush's runs are stashed with `t` below — after the landings,
|
|
2248
|
+
// so a `t` that lands at this very seam runs them first, ahead of
|
|
2249
|
+
// the runs it held from earlier flushes.)
|
|
2250
|
+
// The batch joins: the writes that dirtied these are held with it — a
|
|
2251
|
+
// zombie never displays that world. A verdict reader among them does
|
|
2252
|
+
// (#3444: `latest()` inside a branch a held Show is removing peers
|
|
2253
|
+
// through like the same read outside): it re-runs now, as the
|
|
2254
|
+
// holder's verdict lane's work — before the lane seams, which reveal
|
|
2255
|
+
// it (nothing commits this seam that could dispose it).
|
|
2256
|
+
// (Verdict readers exist only with the verdict engine installed.)
|
|
2257
|
+
const zombies = deferredZombies.splice(0);
|
|
2258
|
+
if (observeFlightHook) {
|
|
2259
|
+
zombies.sort((a, b) => a._height - b._height);
|
|
2260
|
+
for (let i = 0; i < zombies.length; i++)
|
|
2261
|
+
if (
|
|
2262
|
+
(zombies[i]._flags & (REACTIVE_DISPOSED | REACTIVE_ZOMBIE)) === REACTIVE_ZOMBIE &&
|
|
2263
|
+
zombies[i]._config & CONFIG_VERDICT
|
|
2264
|
+
)
|
|
2265
|
+
recompute(zombies[i]);
|
|
2266
|
+
}
|
|
2267
|
+
// The unchanged passes' tails stay linked (A30, #3469): their inputs
|
|
2268
|
+
// are held, and the committed frame still derives from them — no trim.
|
|
2269
|
+
} else {
|
|
2270
|
+
commitPendingNodes();
|
|
2271
|
+
// The flush committed: an unchanged pass's stale tail goes now. Not
|
|
2272
|
+
// after a later pass this flush that threw (NotReady included — it
|
|
2273
|
+
// keeps its full list, `_depsTail` marking where it stopped) or that
|
|
2274
|
+
// left an effect owing a run (`runEffect` trims when the run applies):
|
|
2275
|
+
// `recompute`'s own gates.
|
|
2276
|
+
for (let i = 0; i < heldTrims.length; i++) {
|
|
2277
|
+
const t = heldTrims[i];
|
|
2278
|
+
if (t._x?._error == null && !t._modified) trimStaleDeps(t);
|
|
2279
|
+
}
|
|
1970
2280
|
}
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
(c._x._pendingFirstChild !== null || c._x._pendingDisposal !== null)
|
|
1991
|
-
)
|
|
1992
|
-
GlobalQueue._dispose(c, false, true);
|
|
1993
|
-
}
|
|
1994
|
-
function commitPendingNodes() {
|
|
1995
|
-
for (let i = 0; i < pendingNodes.length; i++) {
|
|
1996
|
-
const n = pendingNodes[i];
|
|
1997
|
-
// A node a lane pass re-staged this round is the lane's to reveal; one
|
|
1998
|
-
// a transaction holds (born held by a mainline pass, `passTx`) is that
|
|
1999
|
-
// transaction's to commit, at its landing.
|
|
2000
|
-
if (!(n._config & (CONFIG_OVERRIDE | CONFIG_HELD))) commitPendingNode(n);
|
|
2001
|
-
}
|
|
2002
|
-
pendingNodes.length = 0;
|
|
2003
|
-
}
|
|
2004
|
-
const globalQueue = new GlobalQueue();
|
|
2005
|
-
function flush(fn) {
|
|
2006
|
-
if (actionDepth > 0) {
|
|
2007
|
-
{
|
|
2008
|
-
throw new Error(
|
|
2009
|
-
"[FLUSH_IN_ACTION] flush() inside an action body is not allowed. An action's writes are held in its " +
|
|
2010
|
-
"transaction and commit when the action settles: flush() cannot reveal them, and draining here would " +
|
|
2011
|
-
"detach the writes that follow from the transaction. Remove the flush(); to observe the result, read " +
|
|
2012
|
-
"after the action resolves."
|
|
2013
|
-
);
|
|
2281
|
+
// The list is the flush's either way. Reset only when there is something
|
|
2282
|
+
// to reset (`stagedReaders` below likewise): `length = 0` is a runtime
|
|
2283
|
+
// call even on an empty array, and the plain flush — no unchanged pass,
|
|
2284
|
+
// no lane work — has nothing in either.
|
|
2285
|
+
if (heldTrims.length) heldTrims.length = 0;
|
|
2286
|
+
// Lanes: a blocked one parks its frame (its own flight is up); an
|
|
2287
|
+
// unblocked one reveals this round's work. Then land every transaction
|
|
2288
|
+
// no frame is waiting on — its lanes end with it. Backwards: a landing
|
|
2289
|
+
// removes its entry.
|
|
2290
|
+
// This flush's own runs, the lanes' reveals, and the landings' held runs
|
|
2291
|
+
// are collected apart and ordered below.
|
|
2292
|
+
const own = this._queues;
|
|
2293
|
+
const lanes = (this._queues = [[], []]);
|
|
2294
|
+
// The passes that read this frame's stagings as the screen read held
|
|
2295
|
+
// writes if it parked: the lane seam re-derives them on the committed
|
|
2296
|
+
// world (a lane's runs wait this round).
|
|
2297
|
+
if (laneSeamsHook) {
|
|
2298
|
+
laneSeamsHook(t !== null ? stagedReaders : null);
|
|
2299
|
+
if (stagedReaders.length) stagedReaders.length = 0;
|
|
2014
2300
|
}
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
//
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
syncDepth--;
|
|
2027
|
-
}
|
|
2301
|
+
this._queues = [[], []];
|
|
2302
|
+
for (let k = transactions.length - 1; k >= 0; k--) {
|
|
2303
|
+
const u = transactions[k];
|
|
2304
|
+
if (blocked(u)) continue;
|
|
2305
|
+
transactions.splice(k, 1);
|
|
2306
|
+
endLanesHook?.(u);
|
|
2307
|
+
land(u);
|
|
2308
|
+
// A landing's commits can dispose a zombie a transaction judged above
|
|
2309
|
+
// was blocked on (#3463: live "until the commit that disposes it") —
|
|
2310
|
+
// judge them again.
|
|
2311
|
+
k = transactions.length;
|
|
2028
2312
|
}
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2313
|
+
// The store folds the pending backings whose container nodes committed
|
|
2314
|
+
// — this flush's, or a landing's (store/store.ts installs it).
|
|
2315
|
+
storeCommitHook?.();
|
|
2316
|
+
// Marks declared outside a transaction: the flush that carried them
|
|
2317
|
+
// parked (something async below) — the window is the frame's, the marks
|
|
2318
|
+
// are its transaction's; else they release at the seam, verdict-only,
|
|
2319
|
+
// nothing to show (affects.ts).
|
|
2320
|
+
const live = t && liveTx(t);
|
|
2321
|
+
releaseAmbientMarksHook?.(live);
|
|
2322
|
+
// The effect phase: lane work first (displayed ahead of the frame), then
|
|
2323
|
+
// this flush's runs, then the runs the landings held from earlier
|
|
2324
|
+
// flushes — as one pass would have queued them (#3540: a shell
|
|
2325
|
+
// re-derived at the landing before a boundary swap held since the
|
|
2326
|
+
// write; #3528: a display-ahead swap before the count it is shown
|
|
2327
|
+
// beside). A parked frame's runs wait with its transaction instead: the
|
|
2328
|
+
// effect phase applies a committed frame.
|
|
2329
|
+
const parked = live !== null;
|
|
2330
|
+
// Attribution hook: this flush found its transaction incomplete — its
|
|
2331
|
+
// writes stay staged, its runs are stashed below. Before the effect
|
|
2332
|
+
// phase (the lanes' display-ahead runs are the visible acknowledgers);
|
|
2333
|
+
// `holdEnd` fires after it (`flush`).
|
|
2334
|
+
if (parked && attrHooks !== null) {
|
|
2335
|
+
attrHooks.holdStart(live);
|
|
2336
|
+
holding = true;
|
|
2036
2337
|
}
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
})
|
|
2048
|
-
);
|
|
2338
|
+
for (let i = 0; i < 2; i++) {
|
|
2339
|
+
if (parked) append(live._queues[i], own[i]);
|
|
2340
|
+
// The plain flush — no reveal, no landing, not parked — has nothing to
|
|
2341
|
+
// order around its own runs: they are the queue as they stand (the
|
|
2342
|
+
// same array, not a copy; `run` takes it whole). Every other seam
|
|
2343
|
+
// builds the ordered queue above.
|
|
2344
|
+
this._queues[i] =
|
|
2345
|
+
parked || lanes[i].length || this._queues[i].length
|
|
2346
|
+
? lanes[i].concat(parked ? [] : own[i], this._queues[i])
|
|
2347
|
+
: own[i];
|
|
2049
2348
|
}
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
let drained = false;
|
|
2058
|
-
// The drain's opening instant, under the loop's own condition so it fires
|
|
2059
|
-
// exactly when `flushEnd` below will. Outside every try (see the rule in
|
|
2060
|
-
// attribution-hooks.ts).
|
|
2061
|
-
if (attrHooks !== null && scheduled) attrHooks.flushStart();
|
|
2062
|
-
while (scheduled) {
|
|
2063
|
-
if (++count === 1e5) {
|
|
2064
|
-
// Attribution beats a bare guard (#3140): say what kept the loop alive.
|
|
2065
|
-
throw new Error(
|
|
2066
|
-
`Potential Infinite Loop Detected. Kept alive by scheduled work${lastStagedNodeName ? `; last staged node: ${lastStagedNodeName}` : ""}`
|
|
2067
|
-
);
|
|
2349
|
+
if (deferredZombies.length !== 0) {
|
|
2350
|
+
// Survivors are the displayed frame of a held pass: they rerun for the
|
|
2351
|
+
// live write, parents before children.
|
|
2352
|
+
const zombies = deferredZombies.splice(0).sort((a, b) => a._height - b._height);
|
|
2353
|
+
for (let i = 0; i < zombies.length; i++) {
|
|
2354
|
+
if (!(zombies[i]._flags & REACTIVE_DISPOSED)) recompute(zombies[i]);
|
|
2355
|
+
}
|
|
2068
2356
|
}
|
|
2069
|
-
|
|
2070
|
-
|
|
2357
|
+
// Boundaries: readers that settled, landed or died this flush are
|
|
2358
|
+
// dropped; a fallback with none left reveals next round.
|
|
2359
|
+
boundarySeamHook?.();
|
|
2071
2360
|
}
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
}
|
|
2078
|
-
function runQueue(queue, type) {
|
|
2079
|
-
for (let i = 0; i < queue.length; i++) queue[i](type);
|
|
2080
|
-
}
|
|
2081
|
-
|
|
2082
|
-
/**
|
|
2083
|
-
* Schedule one subscriber to re-run on the next flush: inserted into the
|
|
2084
|
-
* dirty heap with the `_min` cursor pulled down. Tracked effects ride the
|
|
2085
|
-
* heap too — the heap visit is their (empty) compute phase, which hands the
|
|
2086
|
-
* callback to the user queue once the pass has committed (see
|
|
2087
|
-
* GlobalQueue._update, #3291).
|
|
2088
|
-
*/
|
|
2089
|
-
function enqueueSub(node) {
|
|
2090
|
-
if (dirtyQueue._min > node._height) dirtyQueue._min = node._height;
|
|
2091
|
-
insertIntoHeap(node, dirtyQueue);
|
|
2092
|
-
}
|
|
2093
|
-
function actualInsertIntoHeap(n, heap) {
|
|
2094
|
-
const parentHeight =
|
|
2095
|
-
(n._parent?._root ? n._parent._parentComputed?._height : n._parent?._height) ?? -1;
|
|
2096
|
-
if (parentHeight >= n._height) n._height = parentHeight + 1;
|
|
2097
|
-
const height = n._height;
|
|
2098
|
-
const heapAtHeight = heap._heap[height];
|
|
2099
|
-
if (heapAtHeight === undefined) heap._heap[height] = n;
|
|
2100
|
-
else {
|
|
2101
|
-
const tail = heapAtHeight._prevHeap;
|
|
2102
|
-
tail._nextHeap = n;
|
|
2103
|
-
n._prevHeap = tail;
|
|
2104
|
-
heapAtHeight._prevHeap = n;
|
|
2361
|
+
run(type) {
|
|
2362
|
+
const effects = this._queues[type - 1];
|
|
2363
|
+
if (effects.length) {
|
|
2364
|
+
this._queues[type - 1] = [];
|
|
2365
|
+
runQueue(effects, type);
|
|
2366
|
+
}
|
|
2105
2367
|
}
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
if (flags & (REACTIVE_IN_HEAP | REACTIVE_RECOMPUTING_DEPS)) return;
|
|
2114
|
-
if (flags & REACTIVE_CHECK) {
|
|
2115
|
-
n._flags = (flags & -4) | REACTIVE_DIRTY | REACTIVE_IN_HEAP;
|
|
2116
|
-
} else {
|
|
2117
|
-
n._flags = flags | REACTIVE_IN_HEAP;
|
|
2118
|
-
// An unmarked node entering an already-marked heap is marked on the
|
|
2119
|
-
// spot, keeping the markHeap memo valid. `_marked` is only reset by
|
|
2120
|
-
// runHeap, so a write between two mid-tick pulls (read-time markHeap +
|
|
2121
|
-
// updateIfNecessary) would otherwise leave this node unmarked and every
|
|
2122
|
-
// downstream pull stale until the next flush (#2922: the second
|
|
2123
|
-
// `latest()` returned the first write's value). Invalidating the memo
|
|
2124
|
-
// instead re-walked the WHOLE heap on the next pull — with N effects
|
|
2125
|
-
// parked in the heap for a synchronous mount (each row writing a ref
|
|
2126
|
-
// signal its effect subscribes to), mounting N rows was O(N²) (#3350).
|
|
2127
|
-
// markNode's own guard skips an already-DIRTY node.
|
|
2128
|
-
if (heap._marked) markNode(n);
|
|
2368
|
+
enqueue(type, fn, lane = passLane) {
|
|
2369
|
+
// Lane work's runs are the lane's: they run at its reveal, not with
|
|
2370
|
+
// the frame (which may park). A lane pass that read the frame's own
|
|
2371
|
+
// staging is the frame's (`recompute` passes null): its run waits with
|
|
2372
|
+
// the frame.
|
|
2373
|
+
if (type) (lane !== null ? lane : this)._queues[type - 1].push(fn);
|
|
2374
|
+
schedule();
|
|
2129
2375
|
}
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2376
|
+
/** Status reaching the root: pending is absorbed (the root mount defers —
|
|
2377
|
+
* dev records it for the ASYNC_OUTSIDE_LOADING_BOUNDARY FYI); an error is
|
|
2378
|
+
* unhandled (`false` → the caller halts).
|
|
2379
|
+
*
|
|
2380
|
+
* L2: this is where a transaction is registered. Pending that reaches an
|
|
2381
|
+
* observer (a render effect: the frame) is a frame that cannot show — the
|
|
2382
|
+
* flush parks, and the transaction waits on the flight while a frame
|
|
2383
|
+
* derives from it (`blocked`). Pending nobody observes holds nothing: a
|
|
2384
|
+
* memo in flight that no frame reads is not a frame waiting. (Base: the
|
|
2385
|
+
* queue's `_asyncReporters`, INV-3's one registration site.)
|
|
2386
|
+
*
|
|
2387
|
+
* A boundary between the observer and the root (boundaries.ts) is asked
|
|
2388
|
+
* first: a fallback catches what is not ready under it, and the root
|
|
2389
|
+
* never hears of it. */
|
|
2390
|
+
notify(node, mask, flags, error) {
|
|
2391
|
+
if (catchHook?.(node, flags & mask, error)) return true;
|
|
2392
|
+
if (mask & STATUS_PENDING) {
|
|
2393
|
+
if (flags & STATUS_PENDING) {
|
|
2394
|
+
if (_enforceLoadingBoundary) _hitUnhandledAsync = true;
|
|
2395
|
+
// A pass that went pending holds its frame: the async's own
|
|
2396
|
+
// synchronous frame is not ready (A15 reveal corollary: a reveal
|
|
2397
|
+
// that discovers a flight whose inputs are visible holds and joins
|
|
2398
|
+
// it; one whose inputs are held is a stale reader and never gets
|
|
2399
|
+
// here — `read` serves it the committed value). The frame's, not the
|
|
2400
|
+
// observer's: a render effect belongs to whoever dirtied its pass
|
|
2401
|
+
// (A15 shared-hole, #3407) — one held by T1 and re-run by T2's write
|
|
2402
|
+
// pends for T2's flight, and the two stay parallel; a frame that is
|
|
2403
|
+
// T1's already joined T1 through the write or the memo that made it
|
|
2404
|
+
// so. Outside a flush — root setup, a mount — the frame is the
|
|
2405
|
+
// mount's: its pending observers are one transaction, and a flight
|
|
2406
|
+
// started in it resumes it when it lands (maintainer, #3461: "part
|
|
2407
|
+
// of the same one"); what the mount published synchronously stays
|
|
2408
|
+
// published. Lane work going pending holds the lane, not the frame:
|
|
2409
|
+
// `blocked(lane)` finds it in the lane's staging ("if further async
|
|
2410
|
+
// downstream is hit it holds like its own transition").
|
|
2411
|
+
if (passLane === null) joinFuture(null);
|
|
2412
|
+
}
|
|
2413
|
+
return true;
|
|
2414
|
+
}
|
|
2415
|
+
return false;
|
|
2151
2416
|
}
|
|
2152
|
-
n._prevHeap = n;
|
|
2153
|
-
n._nextHeap = undefined;
|
|
2154
2417
|
}
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2418
|
+
/** A15's stale readers: a frame whose LAST pass read a held node as
|
|
2419
|
+
* committed re-derives on the landed world — next round, after the commits.
|
|
2420
|
+
* One re-derived since (any later pass cleared the bit: in the transaction,
|
|
2421
|
+
* or without the held read) owes nothing. A run this transaction stashed
|
|
2422
|
+
* for it is void (#3322: an effect has one value slot, and the stale pass
|
|
2423
|
+
* overwrote the value that run was for) — the re-derivation's run replaces
|
|
2424
|
+
* it. */
|
|
2425
|
+
function reruns(u) {
|
|
2426
|
+
for (let i = 0; i < u._reruns.length; i++) {
|
|
2427
|
+
const r = u._reruns[i];
|
|
2428
|
+
// An errored reader's display is its error (D-39): the landing gives it
|
|
2429
|
+
// nothing to re-derive, and a re-run would be a read that retries the
|
|
2430
|
+
// errored source (#3945).
|
|
2431
|
+
if (r._flags & REACTIVE_DISPOSED || r._statusFlags & STATUS_ERROR) continue;
|
|
2432
|
+
if (r._flags & REACTIVE_FRAME_READ || r._config & CONFIG_VERDICT) {
|
|
2433
|
+
r._flags &= ~REACTIVE_FRAME_READ;
|
|
2434
|
+
r._modified = false;
|
|
2435
|
+
// Its frame is replaced by the re-derivation: a run one of its
|
|
2436
|
+
// children queued this flush (re-run by the same landing) is for the
|
|
2437
|
+
// frame being replaced — void too (#3404).
|
|
2438
|
+
for (let c = r._firstChild; c !== null; c = c._nextSibling) if (c._type) c._modified = false;
|
|
2439
|
+
enqueueSub(r);
|
|
2161
2440
|
}
|
|
2162
2441
|
}
|
|
2442
|
+
u._reruns.length = 0;
|
|
2163
2443
|
}
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2444
|
+
/** The landing: the held frame becomes the frame. */
|
|
2445
|
+
function land(u) {
|
|
2446
|
+
// Attribution hook: judged complete — its held writes commit next
|
|
2447
|
+
// (`_nodes` still lists them).
|
|
2448
|
+
if (attrHooks !== null) attrHooks.transitionSettled(u);
|
|
2449
|
+
reruns(u);
|
|
2450
|
+
if (u._marks !== null) releaseMarksHook(u._marks);
|
|
2451
|
+
// Old children die in the commits (cleanups first), then the stashed
|
|
2452
|
+
// effects run ahead of this flush's own. A node a lane took over since
|
|
2453
|
+
// (a guess written over the staged truth) is the lane's to land.
|
|
2454
|
+
for (let i = 0; i < u._nodes.length; i++) {
|
|
2455
|
+
const n = u._nodes[i];
|
|
2456
|
+
if (n._x._transaction === null || txOf(n) !== u) continue;
|
|
2457
|
+
n._x._transaction = null;
|
|
2458
|
+
commitPendingNode(n);
|
|
2170
2459
|
}
|
|
2460
|
+
// A cleanup those commits ran that wrote a node `u` still held joined
|
|
2461
|
+
// `u`, which has left `transactions`: the write is this landing's, and
|
|
2462
|
+
// the next flush must not park in it (#4089).
|
|
2463
|
+
if (flushTransaction === u) flushTransaction = null;
|
|
2464
|
+
releaseQueues(u);
|
|
2171
2465
|
}
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
}
|
|
2466
|
+
/** The held runs join this flush's effect phase after its own runs (the
|
|
2467
|
+
* landing's re-passes, in height order): the frame's effects run as one
|
|
2468
|
+
* pass would have queued them — a shell re-derived at the landing before a
|
|
2469
|
+
* boundary swap held since the write (#3540's log order). */
|
|
2470
|
+
function releaseQueues(u) {
|
|
2471
|
+
for (let i = 0; i < 2; i++) {
|
|
2472
|
+
append(globalQueue._queues[i], u._queues[i]);
|
|
2473
|
+
u._queues[i] = [];
|
|
2181
2474
|
}
|
|
2182
|
-
heap._max = 0;
|
|
2183
2475
|
}
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
let
|
|
2187
|
-
for (let d = el._deps; d; d = d._nextDep) {
|
|
2188
|
-
const dep = d._dep;
|
|
2189
|
-
if (dep._fn && dep._height >= newHeight) newHeight = dep._height + 1;
|
|
2190
|
-
}
|
|
2191
|
-
if (el._height !== newHeight) {
|
|
2192
|
-
el._height = newHeight;
|
|
2193
|
-
for (let s = el._subs; s !== null; s = s._nextSub) insertIntoHeapHeight(s._sub, heap);
|
|
2194
|
-
}
|
|
2476
|
+
/** `a.push(...b)` without the argument-count limit. */
|
|
2477
|
+
function append(a, b) {
|
|
2478
|
+
for (let i = 0; i < b.length; i++) a.push(b[i]);
|
|
2195
2479
|
}
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2480
|
+
/** Nodes staged this flush (`_pendingValue` set), committed at its end. */
|
|
2481
|
+
const pendingNodes = [];
|
|
2482
|
+
/** Passes that read a staging of this flush as the screen — lane work
|
|
2483
|
+
* (core.ts `stagedRead`), a verdict reader before the frame's verdict
|
|
2484
|
+
* (verdict.ts): if the frame parks they read a held write — re-derived next
|
|
2485
|
+
* round (lanes.ts; a lane's runs wait that round). */
|
|
2486
|
+
const stagedReaders = [];
|
|
2487
|
+
/** A15's stale reader: `c` was served the committed value of a node `t`
|
|
2488
|
+
* holds (a render effect, a verdict reader, lane work) and is re-derived
|
|
2489
|
+
* after `t`'s landing (`_reruns`). Once per pass. */
|
|
2490
|
+
function staleReader(c, t) {
|
|
2491
|
+
if (!(c._flags & REACTIVE_FRAME_READ)) {
|
|
2492
|
+
c._flags |= REACTIVE_FRAME_READ;
|
|
2493
|
+
t._reruns.push(c);
|
|
2210
2494
|
}
|
|
2211
2495
|
}
|
|
2212
|
-
function
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
// left queued would be recomputed and resurrected by the next flush (#2983)
|
|
2217
|
-
// — dep unlinking, child disposal) is exactly unobserved()'s body; only
|
|
2218
|
-
// this flag distinguishes death from dormancy.
|
|
2219
|
-
node._config &= ~CONFIG_AUTO_DISPOSE;
|
|
2220
|
-
unobserved(node);
|
|
2496
|
+
function queuePendingNode(node) {
|
|
2497
|
+
lastStagedNodeName = node._name ?? null;
|
|
2498
|
+
pendingNodes.push(node);
|
|
2499
|
+
if (!globalQueue._running) markUnflushedStaged(); // A28
|
|
2221
2500
|
}
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
// Only unobserved()'s own node keeps its dormancy — it is never in
|
|
2265
|
-
// this loop; its children are rebuilt fresh on reawaken.
|
|
2266
|
-
n._config &= ~CONFIG_AUTO_DISPOSE;
|
|
2267
|
-
// Heap removal must not be gated on `_deps`: a dependency-free
|
|
2268
|
-
// computation queued by refresh() has a null dep list but still sits in
|
|
2269
|
-
// the dirty heap, and left there the post-disposal flush recomputes it —
|
|
2270
|
-
// recompute() rewriting `_flags` clears REACTIVE_DISPOSED and the node
|
|
2271
|
-
// comes back to life (post-unmount runs, leaked cleanups, #2983).
|
|
2272
|
-
// deleteFromHeap self-guards on the in-heap flags (and tolerates plain
|
|
2273
|
-
// Owners, whose _flags is undefined), so no gate here.
|
|
2274
|
-
deleteFromHeap(n, dirtyQueue);
|
|
2275
|
-
clearDeps(n);
|
|
2276
|
-
// The chain is detached above so a node a cleanup links mid-drain lands
|
|
2277
|
-
// on the fresh head and survives. Pointing each drained child's prev at
|
|
2278
|
-
// itself routes its later splice onto the detached chain, never the head,
|
|
2279
|
-
// and keeps the dev owner-chain-head invariant honest for those children.
|
|
2280
|
-
child._prevSibling = child;
|
|
2281
|
-
disposeChildren(child, true);
|
|
2282
|
-
// Read after, not before: a sibling this disposal made dormant spliced
|
|
2283
|
-
// itself out of the detached chain, and a cleanup may then have linked
|
|
2284
|
-
// it at the fresh head, which rewrote the `_nextSibling` a pre-read
|
|
2285
|
-
// would still be holding.
|
|
2286
|
-
child = child._nextSibling;
|
|
2287
|
-
}
|
|
2288
|
-
if (!zombie) node._childCount = 0;
|
|
2289
|
-
// O(1) splice out of parent's chain on individual dispose. Skipped during
|
|
2290
|
-
// batch dispose (parent already disposed) and for a zombie (it sits on the
|
|
2291
|
-
// parent's parked frame, drained whole by the commit). We leave
|
|
2292
|
-
// node._nextSibling intact so outer walks that already advanced past us
|
|
2293
|
-
// still reach later siblings.
|
|
2294
|
-
if (
|
|
2295
|
-
self &&
|
|
2296
|
-
!zombie &&
|
|
2297
|
-
!(flags & REACTIVE_ZOMBIE) &&
|
|
2298
|
-
node._parent !== null &&
|
|
2299
|
-
!(node._parent._flags & REACTIVE_DISPOSED)
|
|
2300
|
-
) {
|
|
2301
|
-
const prev = node._prevSibling;
|
|
2302
|
-
const next = node._nextSibling;
|
|
2303
|
-
// A node with no predecessor must be the chain's head; otherwise the
|
|
2304
|
-
// write below would clobber the head with a stale `_nextSibling`,
|
|
2305
|
-
// orphaning every live child ahead of it (#3543).
|
|
2306
|
-
assertInvariant(
|
|
2307
|
-
prev !== null || node._parent._firstChild === node,
|
|
2308
|
-
"owner-chain-head",
|
|
2309
|
-
"head node is not parent._firstChild — a node was spliced while flagged live but not in the chain (see #3543)"
|
|
2310
|
-
);
|
|
2311
|
-
if (prev !== null) prev._nextSibling = next;
|
|
2312
|
-
else node._parent._firstChild = next;
|
|
2313
|
-
if (next !== null) next._prevSibling = prev;
|
|
2314
|
-
node._prevSibling = null;
|
|
2501
|
+
// Dev-only attribution for the flush loop guard (#3140): when the guard
|
|
2502
|
+
// trips, naming what the loop kept chewing on lets the app author attribute
|
|
2503
|
+
// the runaway without patching dist.
|
|
2504
|
+
let lastStagedNodeName = null;
|
|
2505
|
+
/** §12d: bumped by every recompute and every new subscriber edge. A node's
|
|
2506
|
+
* staged-rewrite skip is sound only while NOTHING recomputed or linked since
|
|
2507
|
+
* its last notify — a mid-batch pull can clean a marked subscriber, and a
|
|
2508
|
+
* skipped re-write would leave it stale. */
|
|
2509
|
+
let notifyEpoch = 0;
|
|
2510
|
+
function bumpNotifyEpoch() {
|
|
2511
|
+
notifyEpoch++;
|
|
2512
|
+
}
|
|
2513
|
+
function insertSubs(node) {
|
|
2514
|
+
// §12d: stamp before walking — setSignal's staged-rewrite fast path skips
|
|
2515
|
+
// the next walk for this node while the epoch holds (marking is idempotent).
|
|
2516
|
+
node._notifiedAt = notifyEpoch;
|
|
2517
|
+
// Presence bits gate the optional-slot probes (see constants.ts): one
|
|
2518
|
+
// masked read of the always-present _config instead of missing-property
|
|
2519
|
+
// lookups in the hottest notify loop.
|
|
2520
|
+
const cfg = node._config;
|
|
2521
|
+
const hasSnapshot = (cfg & CONFIG_HAS_SNAPSHOT) !== 0 && node._x?._snapshotValue !== undefined;
|
|
2522
|
+
// Observe-tier fan-out: this walk visits every subscriber edge anyway, so
|
|
2523
|
+
// the graph-size count is one local increment here and no field anywhere.
|
|
2524
|
+
let fanOut = 0;
|
|
2525
|
+
for (let s = node._subs; s !== null; s = s._nextSub) {
|
|
2526
|
+
const sub = s._sub;
|
|
2527
|
+
fanOut++;
|
|
2528
|
+
// Missed-wake latch (#3037): this write is landing while the subscriber
|
|
2529
|
+
// is mid-recompute (a nested pull committing beneath its reads), and the
|
|
2530
|
+
// heap refuses RECOMPUTING nodes. A gen-current link means the pass
|
|
2531
|
+
// already validated this dep — the value it read is now stale — so latch
|
|
2532
|
+
// for recompute's tail to reschedule. Untouched links need no latch (the
|
|
2533
|
+
// pass either re-reads them fresh or trims them), and neither does the
|
|
2534
|
+
// tail link: it is the read IN FLIGHT — read() links before it pulls, so
|
|
2535
|
+
// this very commit is what that read returns.
|
|
2536
|
+
if (sub._flags & REACTIVE_RECOMPUTING_DEPS && s._gen === sub._depGen && s !== sub._depsTail)
|
|
2537
|
+
sub._flags |= REACTIVE_MISSED_WAKE;
|
|
2538
|
+
if (hasSnapshot && sub._config & CONFIG_IN_SNAPSHOT_SCOPE) {
|
|
2539
|
+
sub._flags |= REACTIVE_SNAPSHOT_STALE;
|
|
2540
|
+
continue;
|
|
2541
|
+
}
|
|
2542
|
+
enqueueSub(sub);
|
|
2315
2543
|
}
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2544
|
+
if (fanOut >= GRAPH_SIZE_WARN_AT) noteFanOut(node, fanOut);
|
|
2545
|
+
}
|
|
2546
|
+
/** Lane work re-staged (lanes.ts; `recompute`): the lane's members among
|
|
2547
|
+
* its subscribers re-derive as the lane's work (REACTIVE_LANE_DIRTY), their
|
|
2548
|
+
* runs held with it — not as stale readers republishing the committed view
|
|
2549
|
+
* (#3460). After `insertSubs`. A render effect on no lane is marked too:
|
|
2550
|
+
* inert for its pass (`recompute` seats a lane only from its own `_x`), it
|
|
2551
|
+
* is a live write if the effect becomes a zombie later this flush — an
|
|
2552
|
+
* on-screen reader whose removal an adopted write stages follows the lane
|
|
2553
|
+
* (the lane-membership ruling, 2026-10-05; `_update`). */
|
|
2554
|
+
function laneDirty(node, l) {
|
|
2555
|
+
for (let s = node._subs; s !== null; s = s._nextSub) {
|
|
2556
|
+
const t = s._sub._x?._transaction;
|
|
2557
|
+
if (t?._lane ? sameLane(t, l) : s._sub._type) s._sub._flags |= REACTIVE_LANE_DIRTY;
|
|
2325
2558
|
}
|
|
2326
2559
|
}
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
node._nextSibling = head;
|
|
2331
|
-
if (head !== null) head._prevSibling = node;
|
|
2332
|
-
parent._firstChild = node;
|
|
2560
|
+
/** One lane, or two of one link group (#3335): one reveal unit. */
|
|
2561
|
+
function sameLane(a, b) {
|
|
2562
|
+
return a === b || (a._links !== null && a._links === b._links);
|
|
2333
2563
|
}
|
|
2334
|
-
function
|
|
2335
|
-
|
|
2336
|
-
//
|
|
2337
|
-
//
|
|
2338
|
-
//
|
|
2339
|
-
//
|
|
2340
|
-
//
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
// throw leaves the depth raised; dev.ts clears the stale count on the next
|
|
2347
|
-
// microtask, since teardown never spans one (core.ts).
|
|
2348
|
-
enterDisposal();
|
|
2349
|
-
if (Array.isArray(disposal)) {
|
|
2350
|
-
// Unwind order (#3572, restores 1.x #1562): later registrations run
|
|
2351
|
-
// before earlier ones. Children have already been disposed by the caller,
|
|
2352
|
-
// so with LIFO a body that registers cleanup before creating its children
|
|
2353
|
-
// tears down after them — the same order a per-component owner gives.
|
|
2354
|
-
for (let i = disposal.length - 1; i >= 0; i--) {
|
|
2355
|
-
const callable = disposal[i];
|
|
2356
|
-
callable.call(callable);
|
|
2564
|
+
function commitPendingNode(n) {
|
|
2565
|
+
const c = n;
|
|
2566
|
+
// L2: the commit is where a pass, held or not, stops being uncommitted —
|
|
2567
|
+
// unless it has no answer yet. A pass still in flight (pending, nothing
|
|
2568
|
+
// staged) stays uncommitted: a re-pass disposes its children on the spot,
|
|
2569
|
+
// and the frame it parked stays the frame until a pass of this node
|
|
2570
|
+
// commits a value.
|
|
2571
|
+
if (!c._fn) {
|
|
2572
|
+
n._config &= -385;
|
|
2573
|
+
if (n._pendingValue !== NOT_PENDING) {
|
|
2574
|
+
n._value = n._pendingValue;
|
|
2575
|
+
n._pendingValue = NOT_PENDING;
|
|
2357
2576
|
}
|
|
2358
|
-
|
|
2359
|
-
disposal.call(disposal);
|
|
2577
|
+
return;
|
|
2360
2578
|
}
|
|
2361
|
-
|
|
2579
|
+
// Computeds only from here (`_statusFlags` is not a signal field). A
|
|
2580
|
+
// commit beneath a flight publishes its inputs (A15 reveal corollary,
|
|
2581
|
+
// #3305): a reveal of it must observe, not show the pre-flight value.
|
|
2582
|
+
const inFlight = n._pendingValue === NOT_PENDING && (c._statusFlags & STATUS_PENDING) !== 0;
|
|
2583
|
+
const err = n._config & CONFIG_STAGED_ERROR;
|
|
2584
|
+
// An answer commits: the error it replaces leaves the display (D-41).
|
|
2585
|
+
if (!inFlight && c._x) c._x._shownError = undefined;
|
|
2586
|
+
n._config = inFlight ? (n._config & -1048833) | CONFIG_INPUTS_PUBLISHED : n._config & -3147137;
|
|
2587
|
+
// D-41: the staged answer is an error — published now, when a value would
|
|
2588
|
+
// show, through the ordinary error path (`stageError`). Its readers first
|
|
2589
|
+
// see it in the cycle after this seam: a tracked re-read there is not a
|
|
2590
|
+
// retry (`read`), or a boundary re-derived by it would retry it forever.
|
|
2591
|
+
if (err) {
|
|
2592
|
+
const e = n._pendingValue;
|
|
2593
|
+
n._pendingValue = NOT_PENDING;
|
|
2594
|
+
n._time = clock + 1;
|
|
2595
|
+
notifyStatus(c, STATUS_ERROR, e);
|
|
2596
|
+
} else if (n._pendingValue !== NOT_PENDING) {
|
|
2597
|
+
n._value = n._pendingValue;
|
|
2598
|
+
n._pendingValue = NOT_PENDING;
|
|
2599
|
+
// A node born staged (recompute) initializes at this commit.
|
|
2600
|
+
c._statusFlags &= ~STATUS_UNINITIALIZED;
|
|
2601
|
+
// A quiet re-ask's classification survives its landing and dies with
|
|
2602
|
+
// the landed value's commit (A19 exc. 2, #3178): verdict-quiet through
|
|
2603
|
+
// the reveal. (The sweep of a node still in flight is not a landing.)
|
|
2604
|
+
// The A28 stash goes with it.
|
|
2605
|
+
if (c._x != null) ((c._x._reask = false), (c._x._flushed = NOT_PENDING));
|
|
2606
|
+
// An effect with a staged value was born held (L2): its first run is
|
|
2607
|
+
// this commit's, not its creation's (A29) — queue it now.
|
|
2608
|
+
if (n._type && n._type !== EFFECT_TRACKED) {
|
|
2609
|
+
n._modified = true;
|
|
2610
|
+
globalQueue.enqueue(n._type, (n._boundRunEffect ??= GlobalQueue._runEffect.bind(null, c)));
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2613
|
+
// The committed value is the first observable answer for a loading-window
|
|
2614
|
+
// node — the window closes here, not at compute time (#2990). Unconditional
|
|
2615
|
+
// store to an always-present computed slot.
|
|
2616
|
+
c._loading = false;
|
|
2617
|
+
c._flags &= ~REACTIVE_MANUAL_WRITE;
|
|
2618
|
+
// The dependencies of the pass that produced the value are the frame's now:
|
|
2619
|
+
// the previous frame's tail goes (A30, #3410; `recompute` left it for a
|
|
2620
|
+
// staged pass). Only after a clean pass: `_error` is cleared by a clean
|
|
2621
|
+
// pass or by the node's own landing (whose pass was clean), so a set
|
|
2622
|
+
// `_error` means the last pass threw, kept its full list, and `_depsTail`
|
|
2623
|
+
// marks where it stopped.
|
|
2624
|
+
if (c._x?._error == null) trimStaleDeps(c);
|
|
2625
|
+
if (!(c._statusFlags & STATUS_PENDING)) c._statusFlags &= ~STATUS_UNINITIALIZED;
|
|
2626
|
+
// L2: the children this commit publishes are the frame's now — the frame
|
|
2627
|
+
// they replace, parked by the pass (`recompute`), goes.
|
|
2628
|
+
if (
|
|
2629
|
+
!inFlight &&
|
|
2630
|
+
c._x != null &&
|
|
2631
|
+
(c._x._pendingFirstChild !== null || c._x._pendingDisposal !== null)
|
|
2632
|
+
)
|
|
2633
|
+
GlobalQueue._dispose(c, false, true);
|
|
2362
2634
|
}
|
|
2363
|
-
function
|
|
2364
|
-
let
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2635
|
+
function commitPendingNodes() {
|
|
2636
|
+
for (let i = 0; i < pendingNodes.length; i++) {
|
|
2637
|
+
const n = pendingNodes[i];
|
|
2638
|
+
// A node a lane pass re-staged this round is the lane's to reveal; one
|
|
2639
|
+
// a transaction holds (born held by a mainline pass, `passTx`) is that
|
|
2640
|
+
// transaction's to commit, at its landing.
|
|
2641
|
+
if (!(n._config & (CONFIG_OVERRIDE | CONFIG_HELD))) commitPendingNode(n);
|
|
2642
|
+
}
|
|
2643
|
+
pendingNodes.length = 0;
|
|
2369
2644
|
}
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2645
|
+
const globalQueue = new GlobalQueue();
|
|
2646
|
+
function flush(fn) {
|
|
2647
|
+
if (actionDepth > 0) {
|
|
2648
|
+
{
|
|
2649
|
+
throw new Error(
|
|
2650
|
+
"[FLUSH_IN_ACTION] flush() inside an action body is not allowed. An action's writes are held in its " +
|
|
2651
|
+
"transaction and commit when the action settles: flush() cannot reveal them, and draining here would " +
|
|
2652
|
+
"detach the writes that follow from the transaction. Remove the flush(); to observe the result, read " +
|
|
2653
|
+
"after the action resolves."
|
|
2654
|
+
);
|
|
2655
|
+
}
|
|
2656
|
+
}
|
|
2657
|
+
if (fn) {
|
|
2658
|
+
syncDepth++;
|
|
2659
|
+
try {
|
|
2660
|
+
return fn();
|
|
2661
|
+
} finally {
|
|
2662
|
+
// Decrement even if the drain throws (a throwing effect): a leaked
|
|
2663
|
+
// syncDepth would stop `schedule()` from ever queuing a microtask again.
|
|
2664
|
+
try {
|
|
2665
|
+
flush();
|
|
2666
|
+
} finally {
|
|
2667
|
+
syncDepth--;
|
|
2668
|
+
}
|
|
2669
|
+
}
|
|
2670
|
+
}
|
|
2671
|
+
if (globalQueue._running) {
|
|
2672
|
+
if (inTrackedQueueCallback) {
|
|
2673
|
+
throw new Error(
|
|
2674
|
+
"Cannot call flush() from inside onSettled or createTrackedEffect. flush() is not reentrant there. " +
|
|
2675
|
+
"Writes made here are processed in the same flush's continuation; to force a drain afterwards, defer it: queueMicrotask(() => flush())."
|
|
2676
|
+
);
|
|
2677
|
+
}
|
|
2678
|
+
if (inEffectCallback) {
|
|
2679
|
+
const message =
|
|
2680
|
+
"[FLUSH_IN_EFFECT_CALLBACK] flush() called from inside an effect callback is a no-op: the flush that runs effects is already in progress. " +
|
|
2681
|
+
"Writes made here are processed in the same flush's continuation; to force a drain afterwards, defer it: queueMicrotask(() => flush()).";
|
|
2682
|
+
reportDiagnostic(
|
|
2683
|
+
emitDiagnostic({
|
|
2684
|
+
code: "FLUSH_IN_EFFECT_CALLBACK",
|
|
2685
|
+
kind: "lifecycle",
|
|
2686
|
+
severity: "warn",
|
|
2687
|
+
message
|
|
2688
|
+
})
|
|
2689
|
+
);
|
|
2690
|
+
}
|
|
2691
|
+
return;
|
|
2692
|
+
}
|
|
2693
|
+
if (halted) return;
|
|
2694
|
+
let count = 0;
|
|
2695
|
+
// Attribution: whether this call drained anything, so `flushEnd` fires once
|
|
2696
|
+
// per real drain and never for a no-op call. The declaration is dead in
|
|
2697
|
+
// prod (its only write is behind true) and rollup drops it.
|
|
2698
|
+
let drained = false;
|
|
2699
|
+
// The drain's opening instant, under the loop's own condition so it fires
|
|
2700
|
+
// exactly when `flushEnd` below will. Outside every try (see the rule in
|
|
2701
|
+
// attribution-hooks.ts).
|
|
2702
|
+
if (attrHooks !== null && scheduled) attrHooks.flushStart();
|
|
2703
|
+
while (scheduled) {
|
|
2704
|
+
if (++count === 1e5) {
|
|
2705
|
+
rethrowCleanupError(true);
|
|
2706
|
+
// Attribution beats a bare guard (#3140): say what kept the loop alive.
|
|
2707
|
+
throw new Error(
|
|
2708
|
+
`Potential Infinite Loop Detected. Kept alive by scheduled work${lastStagedNodeName ? `; last staged node: ${lastStagedNodeName}` : ""}`
|
|
2709
|
+
);
|
|
2710
|
+
}
|
|
2711
|
+
globalQueue.flush();
|
|
2712
|
+
drained = true;
|
|
2713
|
+
}
|
|
2714
|
+
// Outside every try in this function (see the rule in attribution-hooks.ts):
|
|
2715
|
+
// the drain loop above is the one place all scheduled work funnels through,
|
|
2716
|
+
// so this is the "committed and effects ran" instant for everything the
|
|
2717
|
+
// loop processed.
|
|
2718
|
+
if (drained && attrHooks !== null) attrHooks.flushEnd();
|
|
2719
|
+
// A cleanup that threw during the drain, now that the drain is complete
|
|
2720
|
+
// (owner.ts).
|
|
2721
|
+
rethrowCleanupError();
|
|
2378
2722
|
}
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
* transparent nodes share their parent's id; otherwise the parent's next
|
|
2382
|
-
* child id is consumed (or `undefined` outside an id-carrying tree).
|
|
2383
|
-
*/
|
|
2384
|
-
function inheritId(options, transparent, parent) {
|
|
2385
|
-
return (
|
|
2386
|
-
options?.id ??
|
|
2387
|
-
(transparent ? parent?.id : parent?.id != null ? getNextChildId(parent) : undefined)
|
|
2388
|
-
);
|
|
2723
|
+
function runQueue(queue, type) {
|
|
2724
|
+
for (let i = 0; i < queue.length; i++) queue[i](type);
|
|
2389
2725
|
}
|
|
2726
|
+
|
|
2390
2727
|
/**
|
|
2391
|
-
*
|
|
2392
|
-
*
|
|
2393
|
-
*
|
|
2394
|
-
*
|
|
2728
|
+
* Schedule one subscriber to re-run on the next flush: inserted into the
|
|
2729
|
+
* dirty heap with the `_min` cursor pulled down. Tracked effects ride the
|
|
2730
|
+
* heap too — the heap visit is their (empty) compute phase, which hands the
|
|
2731
|
+
* callback to the user queue once the pass has committed (see
|
|
2732
|
+
* GlobalQueue._update, #3291).
|
|
2395
2733
|
*/
|
|
2396
|
-
function
|
|
2397
|
-
|
|
2734
|
+
function enqueueSub(node) {
|
|
2735
|
+
if (dirtyQueue._min > node._height) dirtyQueue._min = node._height;
|
|
2736
|
+
insertIntoHeap(node, dirtyQueue);
|
|
2398
2737
|
}
|
|
2399
|
-
function
|
|
2400
|
-
const
|
|
2401
|
-
|
|
2402
|
-
|
|
2738
|
+
function actualInsertIntoHeap(n, heap) {
|
|
2739
|
+
const parentHeight =
|
|
2740
|
+
(n._parent?._root ? n._parent._parentComputed?._height : n._parent?._height) ?? -1;
|
|
2741
|
+
if (parentHeight >= n._height) n._height = parentHeight + 1;
|
|
2742
|
+
const height = n._height;
|
|
2743
|
+
const heapAtHeight = heap._heap[height];
|
|
2744
|
+
if (heapAtHeight === undefined) heap._heap[height] = n;
|
|
2745
|
+
else {
|
|
2746
|
+
const tail = heapAtHeight._prevHeap;
|
|
2747
|
+
tail._nextHeap = n;
|
|
2748
|
+
n._prevHeap = tail;
|
|
2749
|
+
heapAtHeight._prevHeap = n;
|
|
2750
|
+
}
|
|
2751
|
+
if (height > heap._max) heap._max = height;
|
|
2403
2752
|
}
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2753
|
+
function insertIntoHeap(n, heap) {
|
|
2754
|
+
let flags = n._flags;
|
|
2755
|
+
// RECOMPUTING refusals are not always losses: a genuinely missed wake (a
|
|
2756
|
+
// write to a link this pass already validated) is latched link-side in
|
|
2757
|
+
// insertSubs as REACTIVE_MISSED_WAKE for recompute's tail (#3037).
|
|
2758
|
+
if (flags & (REACTIVE_IN_HEAP | REACTIVE_RECOMPUTING_DEPS)) return;
|
|
2759
|
+
if (flags & REACTIVE_CHECK) {
|
|
2760
|
+
n._flags = (flags & -4) | REACTIVE_DIRTY | REACTIVE_IN_HEAP;
|
|
2761
|
+
} else {
|
|
2762
|
+
n._flags = flags | REACTIVE_IN_HEAP;
|
|
2763
|
+
// An unmarked node entering an already-marked heap is marked on the
|
|
2764
|
+
// spot, keeping the markHeap memo valid. `_marked` is only reset by
|
|
2765
|
+
// runHeap, so a write between two mid-tick pulls (read-time markHeap +
|
|
2766
|
+
// updateIfNecessary) would otherwise leave this node unmarked and every
|
|
2767
|
+
// downstream pull stale until the next flush (#2922: the second
|
|
2768
|
+
// `latest()` returned the first write's value). Invalidating the memo
|
|
2769
|
+
// instead re-walked the WHOLE heap on the next pull — with N effects
|
|
2770
|
+
// parked in the heap for a synchronous mount (each row writing a ref
|
|
2771
|
+
// signal its effect subscribes to), mounting N rows was O(N²) (#3350).
|
|
2772
|
+
// markNode's own guard skips an already-DIRTY node.
|
|
2773
|
+
if (heap._marked) markNode(n);
|
|
2774
|
+
}
|
|
2775
|
+
if (!(flags & REACTIVE_IN_HEAP_HEIGHT)) actualInsertIntoHeap(n, heap);
|
|
2422
2776
|
}
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
* `getOwner()` and re-enter it later with `runWithOwner(owner, fn)` to attach
|
|
2429
|
-
* disposables created from a callback (event handler, async resolution, etc.)
|
|
2430
|
-
* back to a component's lifecycle.
|
|
2431
|
-
*
|
|
2432
|
-
* @example
|
|
2433
|
-
* ```ts
|
|
2434
|
-
* function defer<T>(fn: () => T) {
|
|
2435
|
-
* const owner = getOwner();
|
|
2436
|
-
* queueMicrotask(() => runWithOwner(owner, fn));
|
|
2437
|
-
* }
|
|
2438
|
-
* ```
|
|
2439
|
-
*/
|
|
2440
|
-
function getOwner() {
|
|
2441
|
-
return context;
|
|
2777
|
+
function insertIntoHeapHeight(n, heap) {
|
|
2778
|
+
let flags = n._flags;
|
|
2779
|
+
if (flags & (REACTIVE_IN_HEAP | REACTIVE_RECOMPUTING_DEPS | REACTIVE_IN_HEAP_HEIGHT)) return;
|
|
2780
|
+
n._flags = flags | REACTIVE_IN_HEAP_HEIGHT;
|
|
2781
|
+
actualInsertIntoHeap(n, heap);
|
|
2442
2782
|
}
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2783
|
+
function deleteFromHeap(n, heap) {
|
|
2784
|
+
const flags = n._flags;
|
|
2785
|
+
if (!(flags & (REACTIVE_IN_HEAP | REACTIVE_IN_HEAP_HEIGHT))) return;
|
|
2786
|
+
n._flags = flags & -25;
|
|
2787
|
+
const height = n._height;
|
|
2788
|
+
if (n._prevHeap === n) heap._heap[height] = undefined;
|
|
2789
|
+
else {
|
|
2790
|
+
const next = n._nextHeap;
|
|
2791
|
+
const dhh = heap._heap[height];
|
|
2792
|
+
const end = next ?? dhh;
|
|
2793
|
+
if (n === dhh) heap._heap[height] = next;
|
|
2794
|
+
else n._prevHeap._nextHeap = next;
|
|
2795
|
+
end._prevHeap = n._prevHeap;
|
|
2796
|
+
}
|
|
2797
|
+
n._prevHeap = n;
|
|
2798
|
+
n._nextHeap = undefined;
|
|
2454
2799
|
}
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
* const owner = getOwner();
|
|
2464
|
-
* queueMicrotask(() => {
|
|
2465
|
-
* if (owner && isDisposed(owner)) return; // component unmounted; skip
|
|
2466
|
-
* runWithOwner(owner, fn);
|
|
2467
|
-
* });
|
|
2468
|
-
* }
|
|
2469
|
-
* ```
|
|
2470
|
-
*/
|
|
2471
|
-
function isDisposed(node) {
|
|
2472
|
-
return !!(node._flags & REACTIVE_DISPOSED);
|
|
2800
|
+
function markHeap(heap) {
|
|
2801
|
+
if (heap._marked) return;
|
|
2802
|
+
heap._marked = true;
|
|
2803
|
+
for (let i = 0; i <= heap._max; i++) {
|
|
2804
|
+
for (let el = heap._heap[i]; el !== undefined; el = el._nextHeap) {
|
|
2805
|
+
if (el._flags & REACTIVE_IN_HEAP) markNode(el);
|
|
2806
|
+
}
|
|
2807
|
+
}
|
|
2473
2808
|
}
|
|
2474
|
-
function
|
|
2475
|
-
|
|
2809
|
+
function markNode(el, newState = REACTIVE_DIRTY) {
|
|
2810
|
+
const flags = el._flags;
|
|
2811
|
+
if ((flags & (REACTIVE_CHECK | REACTIVE_DIRTY)) >= newState) return;
|
|
2812
|
+
el._flags = (flags & -4) | newState;
|
|
2813
|
+
for (let link = el._subs; link !== null; link = link._nextSub) {
|
|
2814
|
+
markNode(link._sub, REACTIVE_CHECK);
|
|
2815
|
+
}
|
|
2476
2816
|
}
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
const parent = context;
|
|
2487
|
-
const transparent = options?.transparent ?? false;
|
|
2488
|
-
// Prod and observe boilerplates (see core.ts computed()). The observe
|
|
2489
|
-
// literal carries the `_name` slot the rendering layer fills with the
|
|
2490
|
-
// component label (`owner._name = "<App>"`) — a slot, so labelling a root
|
|
2491
|
-
// is a plain store rather than a shape fork between labelled and plain roots.
|
|
2492
|
-
const owner = {
|
|
2493
|
-
id: inheritId(options, transparent, parent),
|
|
2494
|
-
_config: transparent ? CONFIG_TRANSPARENT : 0,
|
|
2495
|
-
_root: true,
|
|
2496
|
-
_parentComputed: parent?._root ? parent._parentComputed : parent,
|
|
2497
|
-
_firstChild: null,
|
|
2498
|
-
_nextSibling: null,
|
|
2499
|
-
_prevSibling: null,
|
|
2500
|
-
_disposal: null,
|
|
2501
|
-
_context: parent?._context || defaultContext,
|
|
2502
|
-
_childCount: 0,
|
|
2503
|
-
_x: null,
|
|
2504
|
-
_parent: parent,
|
|
2505
|
-
dispose: disposeRootSelf,
|
|
2506
|
-
_name: undefined
|
|
2507
|
-
};
|
|
2508
|
-
if (parent && parent._config & CONFIG_CHILDREN_FORBIDDEN) {
|
|
2509
|
-
emitDiagnostic({
|
|
2510
|
-
code: "PRIMITIVE_IN_FORBIDDEN_SCOPE",
|
|
2511
|
-
kind: "lifecycle",
|
|
2512
|
-
severity: "error",
|
|
2513
|
-
message: PRIMITIVE_IN_FORBIDDEN_SCOPE_MESSAGE,
|
|
2514
|
-
ownerId: parent.id,
|
|
2515
|
-
ownerName: parent._name
|
|
2516
|
-
});
|
|
2517
|
-
throw new Error(PRIMITIVE_IN_FORBIDDEN_SCOPE_MESSAGE);
|
|
2817
|
+
function runHeap(heap, recompute) {
|
|
2818
|
+
heap._marked = false;
|
|
2819
|
+
for (heap._min = 0; heap._min <= heap._max; heap._min++) {
|
|
2820
|
+
let el = heap._heap[heap._min];
|
|
2821
|
+
while (el !== undefined) {
|
|
2822
|
+
if (el._flags & REACTIVE_IN_HEAP) recompute(el);
|
|
2823
|
+
else adjustHeight(el, heap);
|
|
2824
|
+
el = heap._heap[heap._min];
|
|
2825
|
+
}
|
|
2518
2826
|
}
|
|
2519
|
-
|
|
2520
|
-
if (parent) linkChild(parent, owner);
|
|
2521
|
-
else registerRoot(owner);
|
|
2522
|
-
DEV.hooks.onOwner?.(owner);
|
|
2523
|
-
return owner;
|
|
2827
|
+
heap._max = 0;
|
|
2524
2828
|
}
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
* Use this to host long-lived reactive scopes outside of a component (custom
|
|
2537
|
-
* controllers, app bootstrapping, tests). Inside a component, prefer
|
|
2538
|
-
* letting Solid's component lifecycle own things.
|
|
2539
|
-
*
|
|
2540
|
-
* @example
|
|
2541
|
-
* ```ts
|
|
2542
|
-
* // At module level there is no owner, so this root lives until disposed.
|
|
2543
|
-
* const dispose = createRoot(dispose => {
|
|
2544
|
-
* const [n, setN] = createSignal(0);
|
|
2545
|
-
* createEffect(() => n(), value => console.log(value));
|
|
2546
|
-
* setInterval(() => setN(x => x + 1), 1000);
|
|
2547
|
-
* return dispose;
|
|
2548
|
-
* });
|
|
2549
|
-
*
|
|
2550
|
-
* // Later, to tear everything down:
|
|
2551
|
-
* dispose();
|
|
2552
|
-
*
|
|
2553
|
-
* // Inside an owner (component, effect, another root), detach explicitly
|
|
2554
|
-
* // if the root must outlive its creator:
|
|
2555
|
-
* const detached = runWithOwner(null, () => createRoot(d => d));
|
|
2556
|
-
* ```
|
|
2557
|
-
*
|
|
2558
|
-
* @description https://docs.solidjs.com/reference/reactive-utilities/create-root
|
|
2559
|
-
*/
|
|
2560
|
-
function createRoot(init, options) {
|
|
2561
|
-
const owner = createOwner(options);
|
|
2562
|
-
return runWithOwner(owner, () => init(() => owner.dispose()));
|
|
2829
|
+
function adjustHeight(el, heap) {
|
|
2830
|
+
deleteFromHeap(el, heap);
|
|
2831
|
+
let newHeight = el._height;
|
|
2832
|
+
for (let d = el._deps; d; d = d._nextDep) {
|
|
2833
|
+
const dep = d._dep;
|
|
2834
|
+
if (dep._fn && dep._height >= newHeight) newHeight = dep._height + 1;
|
|
2835
|
+
}
|
|
2836
|
+
if (el._height !== newHeight) {
|
|
2837
|
+
el._height = newHeight;
|
|
2838
|
+
for (let s = el._subs; s !== null; s = s._nextSub) insertIntoHeapHeight(s._sub, heap);
|
|
2839
|
+
}
|
|
2563
2840
|
}
|
|
2564
2841
|
|
|
2565
2842
|
/** The store's shared slot-node release handler (store/store.ts installs it
|
|
@@ -2584,20 +2861,24 @@ function unlinkSubs(link) {
|
|
|
2584
2861
|
if (nextSub === null) {
|
|
2585
2862
|
// Store slot nodes dispatch to the ONE shared hook — no per-node
|
|
2586
2863
|
// unobserved closure, no NodeExtension to hold it.
|
|
2864
|
+
// A user hook that throws is collected like a cleanup (owner.ts): the
|
|
2865
|
+
// unlink and the teardown around it carry on.
|
|
2587
2866
|
if (dep._config & CONFIG_SLOT_NODE) slotUnobservedHook(dep);
|
|
2588
|
-
else dep._x?._unobserved
|
|
2867
|
+
else if (dep._x?._unobserved) runCleanup(dep._x._unobserved);
|
|
2589
2868
|
// No more subscribers; only tear down if CONFIG_AUTO_DISPOSE is set.
|
|
2590
2869
|
// A pending node is exempt: its in-flight async work is an observer —
|
|
2591
2870
|
// tearing down would orphan the work and re-execute it on the next
|
|
2592
2871
|
// read. The settle path runs this same last-one-out check when that
|
|
2593
2872
|
// observer releases (the untracked-read dormancy sweep guards on
|
|
2594
2873
|
// pending identically).
|
|
2874
|
+
// During a pass's teardown of its unfinished frame, the release waits
|
|
2875
|
+
// for the pass to end (`deferredReleases`, #4142).
|
|
2595
2876
|
const c = dep;
|
|
2596
2877
|
c._fn &&
|
|
2597
2878
|
c._config & CONFIG_AUTO_DISPOSE &&
|
|
2598
2879
|
!(c._flags & REACTIVE_ZOMBIE) &&
|
|
2599
2880
|
!(c._statusFlags & STATUS_PENDING) &&
|
|
2600
|
-
unobserved(c);
|
|
2881
|
+
(deferredReleases ? deferredReleases.push(c) : unobserved(c));
|
|
2601
2882
|
}
|
|
2602
2883
|
}
|
|
2603
2884
|
return nextDep;
|
|
@@ -2626,6 +2907,17 @@ function clearDeps(el) {
|
|
|
2626
2907
|
el._deps = null;
|
|
2627
2908
|
el._depsTail = null;
|
|
2628
2909
|
}
|
|
2910
|
+
/**
|
|
2911
|
+
* Last-one-out releases held back while a pass tears down the frame of an
|
|
2912
|
+
* earlier pass that never committed (#4142). The pass is about to rebuild
|
|
2913
|
+
* the same children, which read the same lazy nodes again. Released on the
|
|
2914
|
+
* spot, a lazy async memo whose only reader was the old frame's child memo
|
|
2915
|
+
* was torn down, re-ran on the new child's read, went pending, and the cycle
|
|
2916
|
+
* repeated forever. `recompute` collects them around that teardown and
|
|
2917
|
+
* releases what is still unobserved when the pass ends.
|
|
2918
|
+
*/
|
|
2919
|
+
let deferredReleases = null;
|
|
2920
|
+
const deferReleases = list => (deferredReleases = list);
|
|
2629
2921
|
function unobserved(el) {
|
|
2630
2922
|
deleteFromHeap(el, dirtyQueue);
|
|
2631
2923
|
clearDeps(el);
|
|
@@ -2827,6 +3119,45 @@ function releaseSettledDependents(el) {
|
|
|
2827
3119
|
// (releaseSettledDependents shape): identity holders can sit below an
|
|
2828
3120
|
// intermediate whose own error state has since been scrubbed or replaced
|
|
2829
3121
|
// (e.g. an error boundary's tree node).
|
|
3122
|
+
/** D-41: an error a pass of a held node found is an answer of
|
|
3123
|
+
* that frame — it ends the wait like a value and shows when a value would.
|
|
3124
|
+
* Staged like one: `_pendingValue` carries it (CONFIG_STAGED_ERROR), readers
|
|
3125
|
+
* in the hold derive from it (`serve` throws it), and the commit publishes
|
|
3126
|
+
* it through the ordinary error path (`commitPendingNode` → `notifyStatus`),
|
|
3127
|
+
* dependents included. Not for a lane pass, nor a node with no shown value
|
|
3128
|
+
* (born held — its error has no frame to wait behind): those publish now,
|
|
3129
|
+
* as before. Returns whether the error was staged. */
|
|
3130
|
+
function stageError(el, error, inPass) {
|
|
3131
|
+
// Held, or — for a pass's own throw — derived from a staging of a flush
|
|
3132
|
+
// (REACTIVE_STAGED_READ is this pass's; a landing after the pass cannot
|
|
3133
|
+
// read it, the flush it read a staging of has committed or parked since).
|
|
3134
|
+
// Staged whether or not the flush has joined a hold yet: a pass that runs
|
|
3135
|
+
// before its async sibling joins is the seam's to decide, like a value
|
|
3136
|
+
// (#4109). A flush that parks nothing publishes it at its commit (S-38).
|
|
3137
|
+
// Not a projection's derive: its readers learn its error through the
|
|
3138
|
+
// family, which the commit's publish never reaches (#4189).
|
|
3139
|
+
if (
|
|
3140
|
+
!(
|
|
3141
|
+
el._config & CONFIG_HELD ||
|
|
3142
|
+
(inPass && el._flags & REACTIVE_STAGED_READ && globalQueue._running && !el.fam)
|
|
3143
|
+
) ||
|
|
3144
|
+
passLane
|
|
3145
|
+
)
|
|
3146
|
+
return false;
|
|
3147
|
+
// The node answered (an error ends the wait — no longer pending, nor
|
|
3148
|
+
// blocked); a shown error stays its display until the commit.
|
|
3149
|
+
clearPendingSources(el);
|
|
3150
|
+
if (el._x) el._x._blocked = false;
|
|
3151
|
+
if (!((el._statusFlags &= STATUS_ERROR | STATUS_UNINITIALIZED) & STATUS_ERROR))
|
|
3152
|
+
setPendingError(el);
|
|
3153
|
+
el._pendingValue = error instanceof StatusError ? error : new StatusError(el, error);
|
|
3154
|
+
el._config |= CONFIG_STAGED | CONFIG_STAGED_ERROR;
|
|
3155
|
+
// Readers parked on the flight re-run and derive from the staging; the
|
|
3156
|
+
// verdict readers the walk wakes need a flush (#4106).
|
|
3157
|
+
settlePendingSource(el);
|
|
3158
|
+
schedule();
|
|
3159
|
+
return true;
|
|
3160
|
+
}
|
|
2830
3161
|
function settleErroredDependents(el, error) {
|
|
2831
3162
|
let scheduled = false;
|
|
2832
3163
|
const visited = new Set();
|
|
@@ -3073,7 +3404,11 @@ function handleAsync(
|
|
|
3073
3404
|
el._time = clock;
|
|
3074
3405
|
return;
|
|
3075
3406
|
}
|
|
3076
|
-
|
|
3407
|
+
// D-41: a held node's error is staged (`stageError`). A derived store's
|
|
3408
|
+
// readers learn its error through the family, not the derive's
|
|
3409
|
+
// dependents: it publishes now.
|
|
3410
|
+
if (stillPending || setter || !stageError(el, error))
|
|
3411
|
+
notifyStatus(el, stillPending ? STATUS_PENDING : STATUS_ERROR, error);
|
|
3077
3412
|
// A NotReady rejection is a landing into another pending source. The
|
|
3078
3413
|
// rejected flight will never settle its self entry, so transfer ownership
|
|
3079
3414
|
// after notifyStatus has propagated the replacement source.
|
|
@@ -3091,7 +3426,7 @@ function handleAsync(
|
|
|
3091
3426
|
// skip this stale async result — the upcoming flush will recompute the node
|
|
3092
3427
|
// with the new value, creating a fresh Promise that supersedes this one.
|
|
3093
3428
|
if (el._flags & REACTIVE_DIRTY) return;
|
|
3094
|
-
const wasUninitialized =
|
|
3429
|
+
const wasUninitialized = el._statusFlags & STATUS_UNINITIALIZED;
|
|
3095
3430
|
landStatus(el);
|
|
3096
3431
|
// Attribution hook: lets the engine snapshot state before the landing
|
|
3097
3432
|
// branches, so it can tell whether the plain path's setSignal committed a
|
|
@@ -3104,6 +3439,9 @@ function handleAsync(
|
|
|
3104
3439
|
handleError(error);
|
|
3105
3440
|
return;
|
|
3106
3441
|
}
|
|
3442
|
+
// A store derive's landing commits through its setter: unheld, the
|
|
3443
|
+
// shown error leaves now; held, at the derive's commit.
|
|
3444
|
+
if (!(el._config & CONFIG_HELD)) el._x._shownError = undefined;
|
|
3107
3445
|
if (wasUninitialized) landStatus(el, true);
|
|
3108
3446
|
// Attribution hook: this path landed through the setter (derived
|
|
3109
3447
|
// stores, projections), which does not go through setSignal, so the
|
|
@@ -3119,14 +3457,9 @@ function handleAsync(
|
|
|
3119
3457
|
// CARVE 2: the override-covered landing (hold + A18 supersession) and the
|
|
3120
3458
|
// lane-routed landing (derived override, lane effect queue) went with
|
|
3121
3459
|
// the optimistic engine; every landing is the plain setSignal. A node
|
|
3122
|
-
// already held stages its answer into that hold (`setSignal`).
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
} catch (e) {
|
|
3126
|
-
// Same containment as above: setSignal's comparator throw is the only
|
|
3127
|
-
// pre-commit failure here, and there is no user callsite to throw to.
|
|
3128
|
-
notifyStatus(el, STATUS_ERROR, e);
|
|
3129
|
-
}
|
|
3460
|
+
// already held stages its answer into that hold (`setSignal`). Its
|
|
3461
|
+
// comparator throw is contained there (`isSame`, A3).
|
|
3462
|
+
setSignal(el, () => value);
|
|
3130
3463
|
// Attribution hook: this path landed through setSignal, whose write
|
|
3131
3464
|
// hook already saw any committed change — direct=false lets the engine
|
|
3132
3465
|
// reclassify that write as an async landing iff it actually committed.
|
|
@@ -3171,15 +3504,14 @@ function handleAsync(
|
|
|
3171
3504
|
// close registration uses the ambient owner; and DEFERRED (the flattening
|
|
3172
3505
|
// path — a thenable resolved to an iterable in a later microtask), where
|
|
3173
3506
|
// there is no caller to serve and no ambient owner: sync-settled steps
|
|
3174
|
-
// write through asyncWrite, and close registration
|
|
3175
|
-
//
|
|
3176
|
-
//
|
|
3177
|
-
|
|
3178
|
-
const consumeIterator = (source, registerClose) => {
|
|
3507
|
+
// write through asyncWrite, and close registration targets `owner` (el
|
|
3508
|
+
// itself) directly. Returns whether a sync answer landed (first yield or
|
|
3509
|
+
// empty completion) — meaningful only in the live posture.
|
|
3510
|
+
const consumeIterator = (source, owner) => {
|
|
3179
3511
|
const it = source[Symbol.asyncIterator]();
|
|
3180
3512
|
let hadValue = false;
|
|
3181
3513
|
let completed = false;
|
|
3182
|
-
let initialRead = !
|
|
3514
|
+
let initialRead = !owner;
|
|
3183
3515
|
const close = () => {
|
|
3184
3516
|
if (completed) return;
|
|
3185
3517
|
completed = true;
|
|
@@ -3188,7 +3520,7 @@ function handleAsync(
|
|
|
3188
3520
|
if (isThenable(returned)) returned.then(undefined, () => {});
|
|
3189
3521
|
} catch {}
|
|
3190
3522
|
};
|
|
3191
|
-
|
|
3523
|
+
cleanup(close, owner);
|
|
3192
3524
|
// Flight-identity cancellation (#3122): the registration above is the
|
|
3193
3525
|
// owner-death backstop; the teardown slot fires at the _inFlight release
|
|
3194
3526
|
// sites so supersede stops this stream immediately.
|
|
@@ -3196,8 +3528,15 @@ function handleAsync(
|
|
|
3196
3528
|
// Release check before each next pull: an unobserved lazy node must tear
|
|
3197
3529
|
// down (its close above runs via disposal, closing the iterator) instead
|
|
3198
3530
|
// of pumping the stream forever with zero subscribers (#2935).
|
|
3531
|
+
// Later pulls run from an async continuation with no compute catch above
|
|
3532
|
+
// them: a throw invoking the protocol (next(), the step's then) settles
|
|
3533
|
+
// as this flight's error instead of escaping as a rejection (#4112).
|
|
3199
3534
|
const iterateOrRelease = () => {
|
|
3200
|
-
|
|
3535
|
+
try {
|
|
3536
|
+
if (!settleAutodispose()) iterate();
|
|
3537
|
+
} catch (e) {
|
|
3538
|
+
handleError(e);
|
|
3539
|
+
}
|
|
3201
3540
|
};
|
|
3202
3541
|
const iterate = () => {
|
|
3203
3542
|
let syncResult,
|
|
@@ -3282,7 +3621,7 @@ function handleAsync(
|
|
|
3282
3621
|
// stream itself rather than settling on the iterable object. One level
|
|
3283
3622
|
// only: A+ `then` already collapses nested thenables, so the resolved
|
|
3284
3623
|
// value is never itself a thenable.
|
|
3285
|
-
const flattenIfIterable = (value,
|
|
3624
|
+
const flattenIfIterable = (value, owner) => {
|
|
3286
3625
|
let innerIterator = false;
|
|
3287
3626
|
if (typeof value === "object" && value !== null) {
|
|
3288
3627
|
untrack(() => {
|
|
@@ -3290,8 +3629,9 @@ function handleAsync(
|
|
|
3290
3629
|
});
|
|
3291
3630
|
}
|
|
3292
3631
|
if (!innerIterator) return false;
|
|
3293
|
-
|
|
3294
|
-
|
|
3632
|
+
syncValue = undefined;
|
|
3633
|
+
const landed = consumeIterator(value, owner);
|
|
3634
|
+
if (!owner) liveLanded = landed;
|
|
3295
3635
|
return true;
|
|
3296
3636
|
};
|
|
3297
3637
|
if (thenable) {
|
|
@@ -3299,29 +3639,32 @@ function handleAsync(
|
|
|
3299
3639
|
rejected = false,
|
|
3300
3640
|
syncError,
|
|
3301
3641
|
isSync = true;
|
|
3302
|
-
// Close registration for the flattening path. Consumption starts in a
|
|
3303
|
-
// microtask where the ambient owner is gone (or worse, someone else's),
|
|
3304
|
-
// so `cleanup()` can't be used — the close targets el's disposal list
|
|
3305
|
-
// directly, exactly where a live cleanup() during this recompute would
|
|
3306
|
-
// have put it. Only a flight that actually flattens becomes
|
|
3307
|
-
// disposal-bearing.
|
|
3308
|
-
const registerDeferredClose = fn => {
|
|
3309
|
-
if (!el._disposal) el._disposal = fn;
|
|
3310
|
-
else if (Array.isArray(el._disposal)) el._disposal.push(fn);
|
|
3311
|
-
else el._disposal = [el._disposal, fn];
|
|
3312
|
-
};
|
|
3313
3642
|
watchAsyncTail(el, result).then(
|
|
3314
3643
|
v => {
|
|
3315
3644
|
if (isSync) {
|
|
3316
3645
|
syncValue = v;
|
|
3317
3646
|
resolved = true;
|
|
3318
|
-
} else
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3647
|
+
} else {
|
|
3648
|
+
// Flattening consumes the stream as the value: each yield lands
|
|
3649
|
+
// through asyncWrite under this flight's identity; the first one
|
|
3650
|
+
// clears pending exactly like a plain promise resolution would.
|
|
3651
|
+
// Disposed nodes never start a pump (their disposal list already
|
|
3652
|
+
// ran, so nothing could close the iterator). Consumption starts in
|
|
3653
|
+
// a microtask where the ambient owner is gone, so the close
|
|
3654
|
+
// registers on el's own disposal list. Iterator setup here has no
|
|
3655
|
+
// compute catch above it: a protocol throw settles as this
|
|
3656
|
+
// flight's error (#4112).
|
|
3657
|
+
try {
|
|
3658
|
+
if (
|
|
3659
|
+
el._x?._inFlight === result &&
|
|
3660
|
+
!(el._flags & REACTIVE_DISPOSED) &&
|
|
3661
|
+
flattenIfIterable(v, el)
|
|
3662
|
+
)
|
|
3663
|
+
return;
|
|
3664
|
+
asyncWrite(v);
|
|
3665
|
+
} catch (e) {
|
|
3666
|
+
handleError(e);
|
|
3667
|
+
}
|
|
3325
3668
|
settleAutodispose();
|
|
3326
3669
|
}
|
|
3327
3670
|
},
|
|
@@ -3425,6 +3768,8 @@ function notifyStatus(el, status, error, blockStatus) {
|
|
|
3425
3768
|
// (CARVE 2: the lane assignment went with the engine.)
|
|
3426
3769
|
{
|
|
3427
3770
|
if (status === STATUS_PENDING && pendingSource) {
|
|
3771
|
+
// D-41: a re-ask of a shown error keeps it the display (`_shownError`).
|
|
3772
|
+
if (el._statusFlags & STATUS_ERROR) el._x._shownError = el._x._error;
|
|
3428
3773
|
addPendingSource(el, pendingSource);
|
|
3429
3774
|
el._statusFlags = STATUS_PENDING | (el._statusFlags & STATUS_UNINITIALIZED);
|
|
3430
3775
|
// Preserve the current source on this propagation so readers park on
|
|
@@ -3504,7 +3849,8 @@ function propagateStatus(el, status, error, downstreamBlockStatus) {
|
|
|
3504
3849
|
if (
|
|
3505
3850
|
passLane === null &&
|
|
3506
3851
|
(sub._config & (CONFIG_HELD | CONFIG_OVERRIDE)) === CONFIG_HELD &&
|
|
3507
|
-
globalQueue._running
|
|
3852
|
+
globalQueue._running &&
|
|
3853
|
+
!freshHook?.(sub, pendingSource._time === clock)
|
|
3508
3854
|
)
|
|
3509
3855
|
joinFuture(txOf(sub));
|
|
3510
3856
|
return;
|
|
@@ -3519,7 +3865,7 @@ function propagateStatus(el, status, error, downstreamBlockStatus) {
|
|
|
3519
3865
|
// the dependent's pending is the lane's own flight (`laneStage`).
|
|
3520
3866
|
{
|
|
3521
3867
|
// (No answer: a pending propagation leaves a written guess a guess.)
|
|
3522
|
-
if (passLane !== null)
|
|
3868
|
+
if (laneStageHook && passLane !== null) laneStageHook(sub, passLane, true, el);
|
|
3523
3869
|
else {
|
|
3524
3870
|
queuePendingNode(sub);
|
|
3525
3871
|
// A15 (#3443): pending propagates onto a held memo without
|
|
@@ -3561,8 +3907,29 @@ GlobalQueue._update = el => {
|
|
|
3561
3907
|
// (REACTIVE_LANE_DIRTY — a lane shows ahead of any park): it runs now, as
|
|
3562
3908
|
// the lane's work, for the reveal this seam (#3463).
|
|
3563
3909
|
else if (el._flags & REACTIVE_ZOMBIE && !(el._flags & REACTIVE_LANE_DIRTY)) deferZombie(el);
|
|
3564
|
-
|
|
3910
|
+
// A lane frame's member (parked by a lane pass, #3698) follows mainline
|
|
3911
|
+
// writes only: a lane's dirty on it is cancelled — the lane's apply retires
|
|
3912
|
+
// it, and the pass that replaced it already decided what that screen shows
|
|
3913
|
+
// (#4137: a `<Show>` branch withdrawn by its own lane re-ran on a falsy
|
|
3914
|
+
// condition).
|
|
3915
|
+
// (Lanes exist only with the lane engine installed: folded away without.)
|
|
3916
|
+
else if (laneStageHook && el._flags & REACTIVE_ZOMBIE && laneFrameMember(el)) {
|
|
3917
|
+
deleteFromHeap(el, dirtyQueue);
|
|
3918
|
+
el._flags &= -65540;
|
|
3919
|
+
} else recompute(el);
|
|
3565
3920
|
};
|
|
3921
|
+
/** The zombie's frame was parked by lane work: the nearest non-zombie owner
|
|
3922
|
+
* (the one whose pass parked it) carries a lane's derived value — or, an
|
|
3923
|
+
* effect (no lane value: `laneStage`), is listed in a lane (an `insert()`
|
|
3924
|
+
* hole's render effect). */
|
|
3925
|
+
function laneFrameMember(el) {
|
|
3926
|
+
let o = el._parent;
|
|
3927
|
+
while (o._flags & REACTIVE_ZOMBIE) o = o._parent;
|
|
3928
|
+
return (
|
|
3929
|
+
(o._config & (CONFIG_OVERRIDE | CONFIG_GUESS)) === CONFIG_OVERRIDE ||
|
|
3930
|
+
(o._type && o._x?._transaction?._lane)
|
|
3931
|
+
);
|
|
3932
|
+
}
|
|
3566
3933
|
GlobalQueue._dispose = disposeChildren;
|
|
3567
3934
|
const PRIMITIVE_IN_FORBIDDEN_SCOPE_MESSAGE =
|
|
3568
3935
|
"[PRIMITIVE_IN_FORBIDDEN_SCOPE] Cannot create reactive primitives inside createTrackedEffect or owner-backed onSettled";
|
|
@@ -3597,6 +3964,10 @@ const REACTIVE_WRITE_IN_OWNED_SCOPE_REFRESH_MESSAGE =
|
|
|
3597
3964
|
let tracking = false;
|
|
3598
3965
|
let context = null;
|
|
3599
3966
|
let snapshotCaptureActive = false;
|
|
3967
|
+
/** The lane at the root of the running chain of first passes: the lane whose
|
|
3968
|
+
* pass created the chain's first node (a mount), inherited by every first
|
|
3969
|
+
* pass nested under it, whose seats are mainline (`recompute`, #4166). */
|
|
3970
|
+
let laneRoot;
|
|
3600
3971
|
let snapshotSources = null;
|
|
3601
3972
|
function ownerInSnapshotScope(owner) {
|
|
3602
3973
|
while (owner) {
|
|
@@ -3656,6 +4027,7 @@ function recompute(el, create = false) {
|
|
|
3656
4027
|
// §12d: any recompute can clean a marked subscriber — invalidate skips.
|
|
3657
4028
|
bumpNotifyEpoch();
|
|
3658
4029
|
const isEffect = el._type;
|
|
4030
|
+
let released;
|
|
3659
4031
|
// Lanes (plan sec. 28): the seat of a pass is its node's. A node carrying a lane's
|
|
3660
4032
|
// derived value runs as the lane's whoever dirtied it — a sync write, a
|
|
3661
4033
|
// boundary reset, a frame rerun — so its children and its result are the
|
|
@@ -3671,18 +4043,27 @@ function recompute(el, create = false) {
|
|
|
3671
4043
|
// derivation's pass into the lane (`read`); a leaf's never moves.
|
|
3672
4044
|
// Restored at the end, after this pass's staging and runs have been
|
|
3673
4045
|
// routed.
|
|
4046
|
+
// Lanes exist only with the lane engine installed (`laneStageHook`): a
|
|
4047
|
+
// bundle without it seats every pass on mainline, and folds the seat away.
|
|
3674
4048
|
const prevLane = passLane;
|
|
4049
|
+
// The mount a first pass belongs to: its creator's lane, or the one its
|
|
4050
|
+
// creator's own mount came from — a render effect a verdict lane's mount's
|
|
4051
|
+
// content creates is that mount's too (#4166). Lanes only, like the seat.
|
|
4052
|
+
const prevMount = laneRoot;
|
|
4053
|
+
const mount = laneStageHook && (laneRoot = create && (prevLane || prevMount));
|
|
3675
4054
|
setPassLane(
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
el.
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
4055
|
+
!laneStageHook
|
|
4056
|
+
? null
|
|
4057
|
+
: (el._flags & REACTIVE_LANE_DIRTY ||
|
|
4058
|
+
(el._config & (CONFIG_OVERRIDE | CONFIG_GUESS)) === CONFIG_OVERRIDE) &&
|
|
4059
|
+
el._x?._transaction?._lane
|
|
4060
|
+
? el._x._transaction
|
|
4061
|
+
: // Loose `!=`: false with no creator lane (undefined == null).
|
|
4062
|
+
create &&
|
|
4063
|
+
prevLane?._parent._verdict != prevLane &&
|
|
4064
|
+
creatorPass(context)?._flags & REACTIVE_RECOMPUTING_DEPS
|
|
4065
|
+
? prevLane
|
|
4066
|
+
: null
|
|
3686
4067
|
);
|
|
3687
4068
|
// Attribution hook: fired before this run touches the dep list — `_deps`
|
|
3688
4069
|
// still holds the previous run's links (the subscriptions that could have
|
|
@@ -3724,8 +4105,11 @@ function recompute(el, create = false) {
|
|
|
3724
4105
|
// Held by a blocked lane: the frame joins nothing — a lane never holds
|
|
3725
4106
|
// a sync write (#3460). The pass is the lane's if it reads the lane's
|
|
3726
4107
|
// world (`read`), and has left it otherwise.
|
|
3727
|
-
|
|
3728
|
-
|
|
4108
|
+
// An answered deferred node's re-pass is a lagging question (D-40):
|
|
4109
|
+
// it joins nothing, so the write that reached it commits mainline.
|
|
4110
|
+
if (tx._lane || el._config & CONFIG_LAGS);
|
|
4111
|
+
else if (isEffect !== EFFECT_RENDER && !(el._config & CONFIG_VERDICT))
|
|
4112
|
+
joinPassTx(tx, freshHook?.(el, el._statusFlags & STATUS_UNINITIALIZED));
|
|
3729
4113
|
else if (
|
|
3730
4114
|
tx !== flushTransaction &&
|
|
3731
4115
|
!(el._statusFlags & STATUS_UNINITIALIZED || el._config & CONFIG_HELD_FRAME)
|
|
@@ -3745,8 +4129,22 @@ function recompute(el, create = false) {
|
|
|
3745
4129
|
// frame parked before it stays parked. Otherwise they ARE the frame:
|
|
3746
4130
|
// park them until this pass commits. Tracked effects run after the seam
|
|
3747
4131
|
// and own nothing to park.
|
|
3748
|
-
if (isEffect === EFFECT_TRACKED || el._config & CONFIG_STAGED)
|
|
3749
|
-
|
|
4132
|
+
if (isEffect === EFFECT_TRACKED || el._config & CONFIG_STAGED) {
|
|
4133
|
+
// A lazy node only those children read is released when this pass
|
|
4134
|
+
// ends, if the pass doesn't read it again (#4142). A cleanup or
|
|
4135
|
+
// `unobserved` hook that throws here neither stops the pass nor skips
|
|
4136
|
+
// that drain: its error is rethrown when the flush ends (owner.ts).
|
|
4137
|
+
const prev = deferredReleases;
|
|
4138
|
+
deferReleases((released = []));
|
|
4139
|
+
disposeChildren(el);
|
|
4140
|
+
deferReleases(prev);
|
|
4141
|
+
} else if (el._firstChild !== null || el._disposal !== null) parkChildren(el);
|
|
4142
|
+
// An error a pass staged (D-41) is replaced by this pass's answer: a
|
|
4143
|
+
// value, a flight, or an error of its own (#4109, #4145).
|
|
4144
|
+
if (el._config & CONFIG_STAGED_ERROR) {
|
|
4145
|
+
el._pendingValue = NOT_PENDING;
|
|
4146
|
+
el._config ^= CONFIG_STAGED_ERROR;
|
|
4147
|
+
}
|
|
3750
4148
|
clearSignals(el);
|
|
3751
4149
|
}
|
|
3752
4150
|
const wasUninitialized = !!(el._statusFlags & STATUS_UNINITIALIZED);
|
|
@@ -3763,6 +4161,8 @@ function recompute(el, create = false) {
|
|
|
3763
4161
|
// settlePendingSource walk never runs for a landing that was preempted
|
|
3764
4162
|
// (#3181).
|
|
3765
4163
|
const wasPendingSource = el._x?._pendingSources?.has(el);
|
|
4164
|
+
// D-41: this pass's error is staged, not published (`stageError`).
|
|
4165
|
+
let stagedErr = false;
|
|
3766
4166
|
// A19 exc. 2: a `refresh()` re-asks the question already answered — quiet
|
|
3767
4167
|
// for the verdict — unless the question in flight is a new one, which the
|
|
3768
4168
|
// re-ask does not launder (a poll during a quiet confirm stays quiet).
|
|
@@ -3777,7 +4177,7 @@ function recompute(el, create = false) {
|
|
|
3777
4177
|
// frame, and the splice/reawaken paths key off it. A zombie reruns for
|
|
3778
4178
|
// live writes until the commit that disposes it, so every per-pass wipe
|
|
3779
4179
|
// — here, the finally below, updateIfNecessary — carries it (#3543).
|
|
3780
|
-
el._flags = REACTIVE_RECOMPUTING_DEPS | (el._flags & REACTIVE_ZOMBIE);
|
|
4180
|
+
el._flags = REACTIVE_RECOMPUTING_DEPS | (el._flags & (REACTIVE_ZOMBIE | REACTIVE_REASK));
|
|
3781
4181
|
el._time = clock;
|
|
3782
4182
|
// The pass's previous value: its staging, else a lane node's lane value
|
|
3783
4183
|
// when it has one (lanes.ts), else the committed one.
|
|
@@ -3789,7 +4189,8 @@ function recompute(el, create = false) {
|
|
|
3789
4189
|
: el._value;
|
|
3790
4190
|
let oldHeight = el._height;
|
|
3791
4191
|
let missedWake = false;
|
|
3792
|
-
// L2: did this pass read the future (REACTIVE_JOINED, set by read)
|
|
4192
|
+
// L2: did this pass read the future (REACTIVE_JOINED, set by read) — or,
|
|
4193
|
+
// a lane's mount, a staging of this flush (#3851, #4166)?
|
|
3793
4194
|
let joined = false;
|
|
3794
4195
|
let prevTracking = tracking;
|
|
3795
4196
|
let prevStrictRead = false;
|
|
@@ -3834,11 +4235,15 @@ function recompute(el, create = false) {
|
|
|
3834
4235
|
// retry can actually run.
|
|
3835
4236
|
parkLoadingWindow(el, e);
|
|
3836
4237
|
} else {
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
4238
|
+
// D-41: a pass of a held node, or one that read a staging of this
|
|
4239
|
+
// flush, stages its error (`stageError`).
|
|
4240
|
+
if (notReady || !(stagedErr = stageError(el, e, true))) {
|
|
4241
|
+
if (notReady) ext(el)._blocked = true;
|
|
4242
|
+
notifyStatus(el, notReady ? STATUS_PENDING : STATUS_ERROR, e);
|
|
4243
|
+
// The replacement source is fully propagated now. If no new flight
|
|
4244
|
+
// re-owned self, retire the superseded flight and its dependent copies.
|
|
4245
|
+
if (notReady && wasPendingSource && !el._x?._inFlight) settlePendingSource(el);
|
|
4246
|
+
}
|
|
3842
4247
|
// A re-park drops what the earlier pass carried (#3456): a source this
|
|
3843
4248
|
// pass no longer reaches — its branch switched, or a fresh flight
|
|
3844
4249
|
// replaced the inputs' pending with its own — stays copied onto
|
|
@@ -3846,14 +4251,16 @@ function recompute(el, create = false) {
|
|
|
3846
4251
|
// stops at this node (nothing left to retire) before it finds them. A
|
|
3847
4252
|
// dependent then waits forever on a flight it has no path to. The
|
|
3848
4253
|
// re-park twin of the unchanged-value recovery sweep below; dependents
|
|
3849
|
-
// with another path keep the source (retryReaches).
|
|
3850
|
-
|
|
4254
|
+
// with another path keep the source (retryReaches). A staged error
|
|
4255
|
+
// answered what the pass carried: the same.
|
|
4256
|
+
if ((notReady || stagedErr) && outgoingPendingSources)
|
|
3851
4257
|
for (const source of outgoingPendingSources)
|
|
3852
4258
|
if (source !== el && !el._x?._pendingSources?.has(source))
|
|
3853
4259
|
settlePendingSource(el, source);
|
|
3854
4260
|
}
|
|
3855
4261
|
} finally {
|
|
3856
4262
|
tracking = prevTracking;
|
|
4263
|
+
if (laneStageHook) laneRoot = prevMount;
|
|
3857
4264
|
strictRead = prevStrictRead;
|
|
3858
4265
|
// Consume the missed-wake latch (#3037, set by insertSubs): a dep write
|
|
3859
4266
|
// landed beneath this pass on a link it had already validated. The wipe
|
|
@@ -3861,10 +4268,19 @@ function recompute(el, create = false) {
|
|
|
3861
4268
|
// (markNode(c) in read()) marks the running node as part of ordinary
|
|
3862
4269
|
// bookkeeping, and those marks are correctly discarded here.
|
|
3863
4270
|
missedWake = (el._flags & REACTIVE_MISSED_WAKE) !== 0;
|
|
3864
|
-
|
|
3865
|
-
//
|
|
3866
|
-
// no
|
|
3867
|
-
|
|
4271
|
+
// A mount's staged read joins it — except a nested derivation left
|
|
4272
|
+
// pending: no render effect reaches it yet, so it holds nothing and
|
|
4273
|
+
// reaches no boundary (Reader, #4203). A reader that does is caught.
|
|
4274
|
+
joined = !!(
|
|
4275
|
+
el._flags &
|
|
4276
|
+
(mount && (prevLane || isEffect || !(el._statusFlags & STATUS_PENDING))
|
|
4277
|
+
? REACTIVE_JOINED | REACTIVE_STAGED_READ
|
|
4278
|
+
: REACTIVE_JOINED)
|
|
4279
|
+
);
|
|
4280
|
+
// A verdict reader is one for as long as it probes, or reads a memo
|
|
4281
|
+
// that does (`read`): a pass that did neither (REACTIVE_PROBED, the same
|
|
4282
|
+
// bit) is an ordinary derivation again.
|
|
4283
|
+
el._config = (el._config & ~CONFIG_VERDICT) | (el._flags & REACTIVE_PROBED);
|
|
3868
4284
|
// REACTIVE_DISPOSED survives too (#3621): the pass may have disposed its
|
|
3869
4285
|
// own owner (a memo calling its root's `dispose()`, a cleanup doing so
|
|
3870
4286
|
// #3601/#3606), and `disposeChildren` set the flag on this node
|
|
@@ -3883,6 +4299,7 @@ function recompute(el, create = false) {
|
|
|
3883
4299
|
REACTIVE_PROBE_UNANSWERED)) |
|
|
3884
4300
|
(create ? el._flags & REACTIVE_SNAPSHOT_STALE : 0);
|
|
3885
4301
|
context = oldcontext;
|
|
4302
|
+
if (released) for (const node of released) releaseIfSettledUnobserved(node);
|
|
3886
4303
|
// A19 exc. 2: a pass that went pending on a `refresh()` re-asks the
|
|
3887
4304
|
// question already answered — quiet for the verdict; any other pending
|
|
3888
4305
|
// pass is a new question. Settled, there is nothing to classify.
|
|
@@ -3911,15 +4328,15 @@ function recompute(el, create = false) {
|
|
|
3911
4328
|
// seat that read none of the lane's world has left it: its result is the
|
|
3912
4329
|
// frame's (a derivation whose branch no longer reaches the guess). A
|
|
3913
4330
|
// guess is written, not derived — it never leaves this way.
|
|
3914
|
-
let lane = passLane;
|
|
4331
|
+
let lane = !laneStageHook ? null : passLane;
|
|
3915
4332
|
// Listed before its staging, a pending pass included (the lane's own
|
|
3916
4333
|
// flight is the lane's); false: the pass left the lane (lanes.ts).
|
|
3917
|
-
const errored = !!el._x?._error;
|
|
3918
|
-
if (lane !== null) {
|
|
3919
|
-
if (
|
|
4334
|
+
const errored = !!el._x?._error || stagedErr;
|
|
4335
|
+
if (laneStageHook && lane !== null) {
|
|
4336
|
+
if (laneStageHook(el, lane, create, errored)) setPassLane(lane);
|
|
3920
4337
|
else lane = null;
|
|
3921
4338
|
}
|
|
3922
|
-
if (!
|
|
4339
|
+
if (!errored) {
|
|
3923
4340
|
// Observe-tier fan-in (HUGE_FAN_IN): the validated prefix [_deps.._depsTail]
|
|
3924
4341
|
// IS this pass's distinct sources — count it here rather than per link. A
|
|
3925
4342
|
// begin/end bracket around the pass plus a per-link increment measured
|
|
@@ -3946,17 +4363,9 @@ function recompute(el, create = false) {
|
|
|
3946
4363
|
: el._config & CONFIG_OVERRIDE && el._x._lane !== NOT_PENDING
|
|
3947
4364
|
? el._x._lane
|
|
3948
4365
|
: el._value;
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
(!isEffect && wasUninitialized) || !el._equals || !el._equals(compareValue, value);
|
|
3953
|
-
} catch (e) {
|
|
3954
|
-
// A throwing user comparator is an error of this node's computation.
|
|
3955
|
-
// Route it through the same status path as a compute-phase throw so
|
|
3956
|
-
// error boundaries contain it; otherwise it unwinds the scheduler
|
|
3957
|
-
// flush, bypassing every boundary and wedging the queue (#2837).
|
|
3958
|
-
notifyStatus(el, STATUS_ERROR, e);
|
|
3959
|
-
}
|
|
4366
|
+
// A throwing comparator is contained inside `isSame` (A3, #2837; #4111).
|
|
4367
|
+
const valueChanged =
|
|
4368
|
+
(!isEffect && wasUninitialized) || !!el._x?._shownError || !isSame(el, compareValue, value);
|
|
3960
4369
|
// A committed derived change becomes a cause for this node's subscribers,
|
|
3961
4370
|
// chaining their attribution through this node to the root write.
|
|
3962
4371
|
if (attrHooks !== null) {
|
|
@@ -4000,7 +4409,7 @@ function recompute(el, create = false) {
|
|
|
4000
4409
|
!create &&
|
|
4001
4410
|
lane === null &&
|
|
4002
4411
|
el._config & CONFIG_GUESS &&
|
|
4003
|
-
|
|
4412
|
+
laneOutcomeHook(el, value, errored)
|
|
4004
4413
|
);
|
|
4005
4414
|
else if (errored);
|
|
4006
4415
|
else if (valueChanged) {
|
|
@@ -4034,9 +4443,7 @@ function recompute(el, create = false) {
|
|
|
4034
4443
|
create
|
|
4035
4444
|
? !(
|
|
4036
4445
|
(flushTransaction !== null || passTx !== null) &&
|
|
4037
|
-
(joined ||
|
|
4038
|
-
(prevLane && el._flags & REACTIVE_STAGED_READ) ||
|
|
4039
|
-
(creatorPass(oldcontext)?._flags ?? 0) & REACTIVE_JOINED)
|
|
4446
|
+
(joined || creatorPass(oldcontext)?._flags & REACTIVE_JOINED)
|
|
4040
4447
|
)
|
|
4041
4448
|
: isEffect && el._pendingValue === NOT_PENDING
|
|
4042
4449
|
) {
|
|
@@ -4065,8 +4472,12 @@ function recompute(el, create = false) {
|
|
|
4065
4472
|
el._statusFlags |= STATUS_UNINITIALIZED;
|
|
4066
4473
|
// A boundary that has not shown content catches it: its fallback
|
|
4067
4474
|
// waits for the commit (#3540). (The pass's own status clear above
|
|
4068
|
-
// dropped what the read recorded.)
|
|
4069
|
-
|
|
4475
|
+
// dropped what the read recorded.) Not when the flush has joined
|
|
4476
|
+
// the hold and the pass is its own (not a verdict lane's mount,
|
|
4477
|
+
// #3851): the mount is part of the hold, and the boundary appears
|
|
4478
|
+
// at its commit, its fallback never seen (D-13, U-3) — a `<Show>`
|
|
4479
|
+
// mounting it over a landing included.
|
|
4480
|
+
freshHook?.(el, !mount && flushTransaction);
|
|
4070
4481
|
}
|
|
4071
4482
|
}
|
|
4072
4483
|
// insertSubs only walks _subs (no scheduling of its own), so a
|
|
@@ -4101,16 +4512,14 @@ function recompute(el, create = false) {
|
|
|
4101
4512
|
// A first pass that went pending where the born-held arm would have held
|
|
4102
4513
|
// its value (it read a held node, or a verdict lane's mount read a staging,
|
|
4103
4514
|
// #3851) is held like it: its answer is the hold's, and the hold waits
|
|
4104
|
-
// for it (A29, #3800)
|
|
4105
|
-
//
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
el._statusFlags & STATUS_PENDING
|
|
4111
|
-
) {
|
|
4515
|
+
// for it (A29, #3800). A boundary that has not shown content catches it
|
|
4516
|
+
// and it is held all the same — its answer reveals no earlier than the
|
|
4517
|
+
// hold's commit (D-12) — but the hold does not wait for it: its only
|
|
4518
|
+
// readers are behind the fallback (`_hidden`; F-4, #3540). A boundary
|
|
4519
|
+
// re-armed by `on` owns what it collects outright (D-14): not held.
|
|
4520
|
+
if (create && lane === null && joined && el._statusFlags & STATUS_PENDING) {
|
|
4112
4521
|
const t = flushTransaction ?? passTx;
|
|
4113
|
-
if (t
|
|
4522
|
+
if (t && !t._lane && freshHook?.(el) !== CAUGHT_REARMED) holdNode(el, t);
|
|
4114
4523
|
}
|
|
4115
4524
|
// Dependencies are the committed frame's until it is replaced (A30, #3410):
|
|
4116
4525
|
// a pass that staged its value leaves the previous pass's tail linked for
|
|
@@ -4222,12 +4631,12 @@ function computed(fn, options) {
|
|
|
4222
4631
|
const loading = options !== null && typeof options === "object" && "loadingValue" in options;
|
|
4223
4632
|
// Two literals, one per tier, selected at build time (the observe flag is
|
|
4224
4633
|
// a literal after replacement; the untaken branch is dead code). The observe
|
|
4225
|
-
// literal is the prod literal plus its `_name`
|
|
4226
|
-
// boilerplate, because a post-construction `self._name = …` forces a
|
|
4634
|
+
// literal is the prod literal plus its `_name` and `_loc` slots — slots in
|
|
4635
|
+
// the boilerplate, because a post-construction `self._name = …` forces a
|
|
4227
4636
|
// hidden-class transition and an out-of-object property store on EVERY node
|
|
4228
4637
|
// (measured: the whole of the observe tier's creation overhead). Keep the
|
|
4229
4638
|
// two in sync — the dist artifact test pins observe's key set to prod's
|
|
4230
|
-
// plus
|
|
4639
|
+
// plus the slots.
|
|
4231
4640
|
const self = {
|
|
4232
4641
|
id: inheritId(options, transparent, context),
|
|
4233
4642
|
_config:
|
|
@@ -4269,6 +4678,7 @@ function computed(fn, options) {
|
|
|
4269
4678
|
// The slot is always present (hidden class); plumbing leaves it
|
|
4270
4679
|
// unset, which `ownerPath` skips.
|
|
4271
4680
|
_name: options?._plumbing ? undefined : (options?.name ?? "computed"),
|
|
4681
|
+
_loc: options?.loc,
|
|
4272
4682
|
// Observe: the verdict windows this pass entered (verdict.ts) — a
|
|
4273
4683
|
// slot, so the stamp is never a write after construction.
|
|
4274
4684
|
_devWindows: 0
|
|
@@ -4300,7 +4710,8 @@ function ext(el) {
|
|
|
4300
4710
|
_flushedAt: -1,
|
|
4301
4711
|
_q: 0,
|
|
4302
4712
|
_lane: NOT_PENDING,
|
|
4303
|
-
_marks: 0
|
|
4713
|
+
_marks: 0,
|
|
4714
|
+
_shownError: undefined
|
|
4304
4715
|
});
|
|
4305
4716
|
}
|
|
4306
4717
|
/** A tracked read of a held node (the reader derives from its transaction's
|
|
@@ -4322,10 +4733,13 @@ function joinPass(c, el) {
|
|
|
4322
4733
|
// the reader's position, not where the memo was created). Its catch, if
|
|
4323
4734
|
// any, is at its staging.
|
|
4324
4735
|
if (el._statusFlags & STATUS_UNINITIALIZED || c._type !== EFFECT_RENDER)
|
|
4325
|
-
joinPassTx(
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
4736
|
+
joinPassTx(txOf(el), (!c._type && c._statusFlags & STATUS_UNINITIALIZED) || freshHook?.(c));
|
|
4737
|
+
}
|
|
4738
|
+
/** `joinPass` into the hold `t` (a folded guess's: store/optimistic.ts
|
|
4739
|
+
* `foldedRead`, D-34) — a reader is never a render effect there. */
|
|
4740
|
+
function joinPassAt(c, t) {
|
|
4741
|
+
c._flags |= REACTIVE_JOINED;
|
|
4742
|
+
joinPassTx(t, (!c._type && c._statusFlags & STATUS_UNINITIALIZED) || freshHook?.(c));
|
|
4329
4743
|
}
|
|
4330
4744
|
/** A15's stale reader (shared-hole and reveal corollaries): a render effect
|
|
4331
4745
|
* reading a node held by a transaction that is not the flush's. It is served
|
|
@@ -4362,10 +4776,11 @@ function frameRead(c, el) {
|
|
|
4362
4776
|
// read the screen. Lane work with no committed value yet reads a flight
|
|
4363
4777
|
// as a mount's memo does: it enters, and the boundary it mounts catches
|
|
4364
4778
|
// the pending (#3540).
|
|
4365
|
-
|
|
4779
|
+
// (Verdict readers and lanes exist only with their engines installed.)
|
|
4780
|
+
const verdict = observeFlightHook && c._config & CONFIG_VERDICT;
|
|
4781
|
+
const lane = laneStageHook && passLane !== null;
|
|
4366
4782
|
if (
|
|
4367
|
-
(
|
|
4368
|
-
(c._statusFlags & STATUS_UNINITIALIZED && el._statusFlags & STATUS_PENDING)) &&
|
|
4783
|
+
(!lane || (c._statusFlags & STATUS_UNINITIALIZED && el._statusFlags & STATUS_PENDING)) &&
|
|
4369
4784
|
!verdict &&
|
|
4370
4785
|
(c._type !== EFFECT_RENDER || el._config & CONFIG_INPUTS_PUBLISHED)
|
|
4371
4786
|
)
|
|
@@ -4374,7 +4789,7 @@ function frameRead(c, el) {
|
|
|
4374
4789
|
// Lane work and verdict readers are never the transaction's pass, whichever
|
|
4375
4790
|
// flush runs it: a node T holds re-enters T when it re-runs (`recompute`'s
|
|
4376
4791
|
// head), and the pass may then read a guess and become the lane's.
|
|
4377
|
-
if ((t === flushTransaction &&
|
|
4792
|
+
if ((t === flushTransaction && !lane && !verdict) || bornInto(c, t)) return false;
|
|
4378
4793
|
staleReader(c, t);
|
|
4379
4794
|
return true;
|
|
4380
4795
|
}
|
|
@@ -4461,6 +4876,7 @@ function createEffectNode(fn, effectFn, errorFn, type, options) {
|
|
|
4461
4876
|
_type: type,
|
|
4462
4877
|
_x: null,
|
|
4463
4878
|
_name: options?.name ?? "effect",
|
|
4879
|
+
_loc: options?.loc,
|
|
4464
4880
|
_devWindows: 0
|
|
4465
4881
|
};
|
|
4466
4882
|
// Effects dispatch status through the SHARED notifier (statusNotifierOf,
|
|
@@ -4512,7 +4928,7 @@ function setupComputedNode(self, options) {
|
|
|
4512
4928
|
if (context) linkChild(context, self);
|
|
4513
4929
|
DEV.hooks.onOwner?.(self);
|
|
4514
4930
|
if (parent) self._height = parent._height + 1;
|
|
4515
|
-
|
|
4931
|
+
wireExternalSourceHook?.(self);
|
|
4516
4932
|
!options?.lazy && recompute(self, true);
|
|
4517
4933
|
if (snapshotCaptureActive && !options?.lazy) {
|
|
4518
4934
|
if (!(self._statusFlags & STATUS_PENDING) && !(self._config & CONFIG_NO_SNAPSHOT)) {
|
|
@@ -4524,7 +4940,7 @@ function setupComputedNode(self, options) {
|
|
|
4524
4940
|
}
|
|
4525
4941
|
function signal(v, options) {
|
|
4526
4942
|
// Prod and observe boilerplates — see computed(). The observe literal adds
|
|
4527
|
-
// `_name` and `_owner` (the creating owner, stamped by registerGraph for
|
|
4943
|
+
// `_name`, `_loc` and `_owner` (the creating owner, stamped by registerGraph for
|
|
4528
4944
|
// createSignal nodes so ownerPath can locate signal subjects; null here,
|
|
4529
4945
|
// and staying null on internal signals — one shape either way).
|
|
4530
4946
|
const s = {
|
|
@@ -4540,6 +4956,7 @@ function signal(v, options) {
|
|
|
4540
4956
|
_notifiedAt: -1,
|
|
4541
4957
|
_x: null,
|
|
4542
4958
|
_name: options?.name ?? "signal",
|
|
4959
|
+
_loc: options?.loc,
|
|
4543
4960
|
_owner: null
|
|
4544
4961
|
};
|
|
4545
4962
|
s._internal = false;
|
|
@@ -4563,7 +4980,8 @@ function isEqual(a, b) {
|
|
|
4563
4980
|
* object, no NodeExtension, no post-construction expandos. `ownedWrite` is
|
|
4564
4981
|
* baked in: the setter carries the owned-scope guard; node-level writes are
|
|
4565
4982
|
* the store's notification machinery. The observe literal adds the label
|
|
4566
|
-
*
|
|
4983
|
+
* and location slots (the store relabels `store.<key>` and stamps the
|
|
4984
|
+
* store's `loc` when the engine is installed). */
|
|
4567
4985
|
function slotSignal(v, equals, host, key, acc) {
|
|
4568
4986
|
const s = {
|
|
4569
4987
|
_equals: equals,
|
|
@@ -4581,6 +4999,7 @@ function slotSignal(v, equals, host, key, acc) {
|
|
|
4581
4999
|
px: undefined,
|
|
4582
5000
|
pxv: undefined,
|
|
4583
5001
|
_name: "signal",
|
|
5002
|
+
_loc: undefined,
|
|
4584
5003
|
_owner: null
|
|
4585
5004
|
};
|
|
4586
5005
|
s._internal = false;
|
|
@@ -4617,10 +5036,15 @@ function exitCallback() {
|
|
|
4617
5036
|
}
|
|
4618
5037
|
/**
|
|
4619
5038
|
* Dev-only: > 0 while owner teardown runs cleanups (`_disposal` entries and
|
|
4620
|
-
* effect-returned cleanups — owner.ts), for the same reason.
|
|
5039
|
+
* effect-returned cleanups — owner.ts), for the same reason. The owned-scope
|
|
5040
|
+
* write guard (setSignal, markRefresh, the store setter) stands down here
|
|
5041
|
+
* too: a cleanup is teardown, not part of whichever computation is disposing
|
|
5042
|
+
* its owner (a list pass removing a row, a branch switching away), so its
|
|
5043
|
+
* writes are imperative like an effect callback's (#4165). Kept apart from
|
|
4621
5044
|
* `callbackDepth` because its sites cannot use try/finally (the frame would
|
|
4622
|
-
* survive into prod)
|
|
4623
|
-
*
|
|
5045
|
+
* survive into prod). A throwing cleanup is caught (owner.ts `runCleanup`);
|
|
5046
|
+
* anything else escaping a teardown leaves it raised, and dev.ts resets it on
|
|
5047
|
+
* the next microtask, which teardown — synchronous — never spans.
|
|
4624
5048
|
*/
|
|
4625
5049
|
let disposalDepth = 0;
|
|
4626
5050
|
function enterDisposal() {
|
|
@@ -4644,6 +5068,16 @@ function setStrictRead(v) {
|
|
|
4644
5068
|
* Use `untrack` inside a memo or effect when you need to read a signal once
|
|
4645
5069
|
* without making the surrounding computation depend on its future changes.
|
|
4646
5070
|
*
|
|
5071
|
+
* `untrack` stops dependency tracking, not async settlement. Reading an
|
|
5072
|
+
* uninitialized async source still suspends the owning computation; it runs
|
|
5073
|
+
* again when that source first resolves. Afterward, the untracked read is a
|
|
5074
|
+
* snapshot and does not subscribe the computation to later changes.
|
|
5075
|
+
*
|
|
5076
|
+
* If the first read must return immediately, give the async source a
|
|
5077
|
+
* `loadingValue` before reading it with `untrack`. `isPending` and `latest`
|
|
5078
|
+
* inspect updates to an existing value; neither supplies a missing initial
|
|
5079
|
+
* value.
|
|
5080
|
+
*
|
|
4647
5081
|
* Pass a `strictReadLabel` string to enable a dev-mode warning: any reactive
|
|
4648
5082
|
* read inside `fn` that isn't inside a nested tracking scope will log a
|
|
4649
5083
|
* warning naming the label.
|
|
@@ -4657,17 +5091,12 @@ function setStrictRead(v) {
|
|
|
4657
5091
|
* log(snapshot);
|
|
4658
5092
|
* }
|
|
4659
5093
|
* );
|
|
5094
|
+
*
|
|
5095
|
+
* const color = createMemo<string>(() => fetchColor(), { loadingValue: "gray" });
|
|
5096
|
+
* const initialColor = untrack(color); // "gray" while the fetch is unresolved
|
|
4660
5097
|
* ```
|
|
4661
5098
|
*/
|
|
4662
5099
|
function untrack(fn, strictReadLabel) {
|
|
4663
|
-
if (
|
|
4664
|
-
GlobalQueue._externalUntrack === null &&
|
|
4665
|
-
!tracking &&
|
|
4666
|
-
!strictRead &&
|
|
4667
|
-
!strictReadLabel &&
|
|
4668
|
-
asyncTailFlights === 0
|
|
4669
|
-
)
|
|
4670
|
-
return fn();
|
|
4671
5100
|
const prevTracking = tracking;
|
|
4672
5101
|
const prevStrictRead = strictRead;
|
|
4673
5102
|
tracking = false;
|
|
@@ -4676,7 +5105,7 @@ function untrack(fn, strictReadLabel) {
|
|
|
4676
5105
|
untrackDepth++;
|
|
4677
5106
|
}
|
|
4678
5107
|
try {
|
|
4679
|
-
if (
|
|
5108
|
+
if (externalUntrackHook) return externalUntrackHook(fn);
|
|
4680
5109
|
return fn();
|
|
4681
5110
|
} finally {
|
|
4682
5111
|
tracking = prevTracking;
|
|
@@ -4748,7 +5177,7 @@ function prepareComputed(comp, refresh) {
|
|
|
4748
5177
|
* False means the reader derives from the staged value.
|
|
4749
5178
|
*/
|
|
4750
5179
|
function readerSeesCommitted(el, c) {
|
|
4751
|
-
return
|
|
5180
|
+
return !c || el._pendingValue === NOT_PENDING || c._config & CONFIG_CHILDREN_FORBIDDEN;
|
|
4752
5181
|
}
|
|
4753
5182
|
/** A28 — set when a node is staged (queuePendingNode) OUTSIDE a flush;
|
|
4754
5183
|
* cleared when the next flush begins. The read sites test this one module
|
|
@@ -4827,6 +5256,7 @@ function pullComputed(owner, c) {
|
|
|
4827
5256
|
if (height >= c._height && owner._parent !== c) c._height = height + 1;
|
|
4828
5257
|
}
|
|
4829
5258
|
function read(el) {
|
|
5259
|
+
DEV.hooks.onRead?.(el);
|
|
4830
5260
|
if (asyncTailFlights !== 0 && !tracking && untrackDepth === 0)
|
|
4831
5261
|
checkPostAwaitRead(
|
|
4832
5262
|
el,
|
|
@@ -4877,7 +5307,13 @@ function read(el) {
|
|
|
4877
5307
|
if (!c || el._pendingValue === NOT_PENDING || c._config & CONFIG_CHILDREN_FORBIDDEN)
|
|
4878
5308
|
return el._value;
|
|
4879
5309
|
// (A plain signal is never uninitialized — the carve-out is `serve`'s.)
|
|
4880
|
-
|
|
5310
|
+
// Lane work over a folded lane (D-34), once the frame has a
|
|
5311
|
+
// transaction, reads the screen too.
|
|
5312
|
+
if (
|
|
5313
|
+
((observeFlightHook && c._config & CONFIG_VERDICT) || (laneStageHook && passLane?.folded)) &&
|
|
5314
|
+
stagedScreen(c)
|
|
5315
|
+
)
|
|
5316
|
+
return el._value;
|
|
4881
5317
|
stagedRead(c);
|
|
4882
5318
|
return el._pendingValue;
|
|
4883
5319
|
}
|
|
@@ -4893,7 +5329,15 @@ function read(el) {
|
|
|
4893
5329
|
if (c) {
|
|
4894
5330
|
if (tracking) {
|
|
4895
5331
|
link(el, c);
|
|
4896
|
-
if (owner._fn)
|
|
5332
|
+
if (owner._fn) {
|
|
5333
|
+
pullComputed(owner, c);
|
|
5334
|
+
// F-6 (#4050): a memo over a verdict reader's value derives from a
|
|
5335
|
+
// display-ahead read — a verdict reader from this pass's end, as if
|
|
5336
|
+
// it probed inline: it reads held values as the screen and shows the
|
|
5337
|
+
// verdict now. Not an effect: a render effect is a frame reader
|
|
5338
|
+
// already, and a user effect's posture is its own reads'.
|
|
5339
|
+
if (!c._type) c._flags |= owner._config & CONFIG_VERDICT;
|
|
5340
|
+
}
|
|
4897
5341
|
}
|
|
4898
5342
|
// L2, as in the fast block above (tracked or not). A node born into the
|
|
4899
5343
|
// future (uninitialized) has no committed value: every reader derives
|
|
@@ -4910,8 +5354,9 @@ function read(el) {
|
|
|
4910
5354
|
}
|
|
4911
5355
|
// Lanes: a lane's node (after the pull — the node is current). NOT_PENDING
|
|
4912
5356
|
// falls through: lane work's read of a pending member throws like any.
|
|
4913
|
-
|
|
4914
|
-
|
|
5357
|
+
// (CONFIG_OVERRIDE is the lane engine's: no engine, no lane's node.)
|
|
5358
|
+
if (laneReadHook && el._config & CONFIG_OVERRIDE) {
|
|
5359
|
+
const v = laneReadHook(c, el);
|
|
4915
5360
|
if (v !== NOT_PENDING) return v;
|
|
4916
5361
|
}
|
|
4917
5362
|
// A stale reader of a held flight (rule 3, A15's reveal corollary) is served
|
|
@@ -4936,9 +5381,11 @@ function read(el) {
|
|
|
4936
5381
|
) {
|
|
4937
5382
|
// A reader with no committed value yet sees the flight pending: a
|
|
4938
5383
|
// boundary a lane mounts shows its fallback (A29's boundary exemption).
|
|
4939
|
-
|
|
4940
|
-
|
|
4941
|
-
committed =
|
|
5384
|
+
// (A lane, and a verdict reader, exist only with their engines installed.)
|
|
5385
|
+
if (laneStageHook && passLane !== null && !(c?._statusFlags & STATUS_UNINITIALIZED))
|
|
5386
|
+
committed = true;
|
|
5387
|
+
else if (observeFlightHook && c !== null && c._config & CONFIG_VERDICT)
|
|
5388
|
+
committed = observeFlightHook(c, owner);
|
|
4942
5389
|
}
|
|
4943
5390
|
if (owner._statusFlags & STATUS_PENDING && !committed) {
|
|
4944
5391
|
// A reader landing on a pending node throws; an untracked read of an
|
|
@@ -5007,6 +5454,9 @@ function read(el) {
|
|
|
5007
5454
|
}
|
|
5008
5455
|
}
|
|
5009
5456
|
if (committed) return el._value;
|
|
5457
|
+
// D-41: an untracked read reads the screen, and a shown error is on it
|
|
5458
|
+
// until the answer that replaces it commits.
|
|
5459
|
+
if (!c && el._x?._shownError) throw el._x._shownError;
|
|
5010
5460
|
const value = serve(el, c);
|
|
5011
5461
|
if (
|
|
5012
5462
|
!c &&
|
|
@@ -5053,14 +5503,24 @@ function serve(el, c) {
|
|
|
5053
5503
|
return u;
|
|
5054
5504
|
}
|
|
5055
5505
|
if (readerSeesCommitted(el, c)) return el._value;
|
|
5056
|
-
if (
|
|
5506
|
+
if (
|
|
5507
|
+
observeFlightHook &&
|
|
5508
|
+
c._config & CONFIG_VERDICT &&
|
|
5509
|
+
!(el._statusFlags & STATUS_UNINITIALIZED) &&
|
|
5510
|
+
stagedScreen(c)
|
|
5511
|
+
)
|
|
5057
5512
|
return el._value;
|
|
5058
5513
|
stagedRead(c, el);
|
|
5514
|
+
// D-41: the staged answer is an error (`stageError`).
|
|
5515
|
+
if (el._config & CONFIG_STAGED_ERROR) throw el._pendingValue;
|
|
5059
5516
|
return el._pendingValue;
|
|
5060
5517
|
}
|
|
5061
5518
|
/** A pass read a staging of this flush: it derives from what the flush may
|
|
5062
5519
|
* yet hold (REACTIVE_STAGED_READ — the seam decides). Lane work too (plan sec. 28, a
|
|
5063
|
-
* lane sees the screen plus its own guesses)
|
|
5520
|
+
* lane sees the screen plus its own guesses) — a folded lane's only before
|
|
5521
|
+
* the frame has a transaction (after, it reads the screen like a verdict
|
|
5522
|
+
* reader, `stagedScreen` — a list pass never pairs a held length with the
|
|
5523
|
+
* guess's rows, D-34): the staging is the screen if
|
|
5064
5524
|
* the frame commits — one pass, the common case — and a held write if it
|
|
5065
5525
|
* parks, which the seam repairs (`stagedReaders`): the pass re-derives on
|
|
5066
5526
|
* the committed world and its lane's runs wait that round, so the held
|
|
@@ -5073,7 +5533,7 @@ function serve(el, c) {
|
|
|
5073
5533
|
function stagedRead(c, el) {
|
|
5074
5534
|
if (!(el?._statusFlags & STATUS_UNINITIALIZED)) {
|
|
5075
5535
|
c._flags |= REACTIVE_STAGED_READ;
|
|
5076
|
-
if (passLane !== null) stagedReaders.push(c);
|
|
5536
|
+
if (laneSeamsHook && passLane !== null) stagedReaders.push(c);
|
|
5077
5537
|
}
|
|
5078
5538
|
}
|
|
5079
5539
|
/** A10 for a staged node: a verdict reader (the pass entered a window) that
|
|
@@ -5123,11 +5583,46 @@ function captureWriteSnapshot(el, current) {
|
|
|
5123
5583
|
el._config |= CONFIG_HAS_SNAPSHOT;
|
|
5124
5584
|
snapshotSources.add(el);
|
|
5125
5585
|
}
|
|
5586
|
+
/**
|
|
5587
|
+
* A callback run before every signal or store write lands, while set, with
|
|
5588
|
+
* the written signal or the store proxy whose setter runs. The
|
|
5589
|
+
* server-components client sets it while slot occurrences are left
|
|
5590
|
+
* unmounted after hydration (deferred hydration): a write may be one an
|
|
5591
|
+
* unmounted component would have observed, so the client mounts what is
|
|
5592
|
+
* left before the write applies, and clears the hook. `null` otherwise.
|
|
5593
|
+
*/
|
|
5594
|
+
let writeHook = null;
|
|
5595
|
+
/** @internal */
|
|
5596
|
+
function setWriteHook(hook) {
|
|
5597
|
+
writeHook = hook;
|
|
5598
|
+
}
|
|
5599
|
+
/**
|
|
5600
|
+
* `el`'s comparator verdict (`_equals`), the one guarded call for recompute,
|
|
5601
|
+
* setSignal (user setters and async landings) and the seam's
|
|
5602
|
+
* staged-equals-committed check.
|
|
5603
|
+
* - A comparator that read an unresolved async source has not shown the
|
|
5604
|
+
* values equal (#4111): falsy, a change, whose readers suspend in their own
|
|
5605
|
+
* tracked context.
|
|
5606
|
+
* - Any other throw is an error of `el`'s computation (A3, #2837): routed
|
|
5607
|
+
* through the status path so error boundaries contain it, instead of
|
|
5608
|
+
* unwinding a flush or a setter; answered "same" so nothing commits.
|
|
5609
|
+
* @internal
|
|
5610
|
+
*/
|
|
5611
|
+
function isSame(el, a, b) {
|
|
5612
|
+
if (el._equals === isEqual) return a === b;
|
|
5613
|
+
try {
|
|
5614
|
+
return el._equals && el._equals(a, b);
|
|
5615
|
+
} catch (e) {
|
|
5616
|
+
if (!(e instanceof NotReadyError)) return (notifyStatus(el, STATUS_ERROR, e), true);
|
|
5617
|
+
}
|
|
5618
|
+
}
|
|
5126
5619
|
function setSignal(el, v) {
|
|
5620
|
+
if (writeHook !== null) writeHook(el);
|
|
5127
5621
|
if (
|
|
5128
5622
|
!(el._config & CONFIG_OWNED_WRITE) &&
|
|
5129
5623
|
!(context && context._config & CONFIG_CHILDREN_FORBIDDEN) &&
|
|
5130
|
-
context
|
|
5624
|
+
context &&
|
|
5625
|
+
!disposalDepth
|
|
5131
5626
|
) {
|
|
5132
5627
|
emitDiagnostic({
|
|
5133
5628
|
code: "REACTIVE_WRITE_IN_OWNED_SCOPE",
|
|
@@ -5146,7 +5641,7 @@ function setSignal(el, v) {
|
|
|
5146
5641
|
// Lanes: a write landing on a guess is its truth (A18 — an async guess's
|
|
5147
5642
|
// own landing), judged against the guess; one landing on a lane's
|
|
5148
5643
|
// derivation (its flight) is the lane's staging (lanes.ts).
|
|
5149
|
-
if (el._config & CONFIG_OVERRIDE) return
|
|
5644
|
+
if (laneWriteHook && el._config & CONFIG_OVERRIDE) return laneWriteHook(el, v);
|
|
5150
5645
|
// L2 (T4) / A34 (1): a write to a node whose staged state is held is a
|
|
5151
5646
|
// second proposal for the slot — the same value again or another — and
|
|
5152
5647
|
// the two cannot finish at different times: the writer's tick joins the
|
|
@@ -5154,8 +5649,12 @@ function setSignal(el, v) {
|
|
|
5154
5649
|
if (el._config & CONFIG_HELD) joinFuture(txOf(el));
|
|
5155
5650
|
// Uninitialized check first: the first commit has no previous value, so the
|
|
5156
5651
|
// user comparator must not run against `undefined` (matches recompute).
|
|
5652
|
+
// D-41: a write over a shown error is a change whatever the last good
|
|
5653
|
+
// value was — the error leaves at its commit (`commitPendingNode`).
|
|
5157
5654
|
const valueChanged =
|
|
5158
|
-
!!(el._statusFlags & STATUS_UNINITIALIZED) ||
|
|
5655
|
+
!!(el._statusFlags & STATUS_UNINITIALIZED) ||
|
|
5656
|
+
!!el._x?._shownError ||
|
|
5657
|
+
!isSame(el, currentValue, v);
|
|
5159
5658
|
if (!valueChanged) return v;
|
|
5160
5659
|
// Attribution hook: this committed write is where a re-run chain begins.
|
|
5161
5660
|
if (attrHooks !== null) attrHooks.write(el, currentValue, v);
|
|
@@ -5213,8 +5712,21 @@ function setMemo(el, v) {
|
|
|
5213
5712
|
!(el._flags & REACTIVE_MANUAL_WRITE) &&
|
|
5214
5713
|
txOf(el) !== flushTransaction;
|
|
5215
5714
|
// The writer read the committed frame; its updater composes on it.
|
|
5216
|
-
if (held && typeof v === "function")
|
|
5217
|
-
|
|
5715
|
+
if (held && typeof v === "function") {
|
|
5716
|
+
const value = v(el._value);
|
|
5717
|
+
v = () => value;
|
|
5718
|
+
}
|
|
5719
|
+
// #4110: the write replaces a failed derivation's outcome. It lands without
|
|
5720
|
+
// re-running `_fn`, and nothing else clears STATUS_ERROR, so without this
|
|
5721
|
+
// every later read keeps throwing the error the derivation left behind.
|
|
5722
|
+
// The error stays the display until the write commits (D-41).
|
|
5723
|
+
if (el._statusFlags & STATUS_ERROR) {
|
|
5724
|
+
el._x._shownError = el._x._error;
|
|
5725
|
+
notifyStatus(el, STATUS_NONE, undefined);
|
|
5726
|
+
}
|
|
5727
|
+
v = setSignal(el, v);
|
|
5728
|
+
// A write replaces an error a pass staged (D-41): it is a value again.
|
|
5729
|
+
el._config &= ~CONFIG_STAGED_ERROR;
|
|
5218
5730
|
if (held) {
|
|
5219
5731
|
// The one write that re-runs: the staging it replaced was derived from
|
|
5220
5732
|
// inputs the writer never saw, so the hold re-derives over the write.
|
|
@@ -5222,7 +5734,7 @@ function setMemo(el, v) {
|
|
|
5222
5734
|
insertIntoHeap(el, dirtyQueue);
|
|
5223
5735
|
schedule();
|
|
5224
5736
|
} else if (el._pendingValue !== NOT_PENDING) el._flags |= REACTIVE_MANUAL_WRITE;
|
|
5225
|
-
return
|
|
5737
|
+
return v;
|
|
5226
5738
|
}
|
|
5227
5739
|
/**
|
|
5228
5740
|
* Executes `fn` with the given `owner` set as the current owner. Any reactive
|
|
@@ -5289,7 +5801,8 @@ function markRefresh(node) {
|
|
|
5289
5801
|
if (
|
|
5290
5802
|
context &&
|
|
5291
5803
|
!((node._config ?? 0) & CONFIG_OWNED_WRITE) &&
|
|
5292
|
-
!(context._config & CONFIG_CHILDREN_FORBIDDEN)
|
|
5804
|
+
!(context._config & CONFIG_CHILDREN_FORBIDDEN) &&
|
|
5805
|
+
!disposalDepth
|
|
5293
5806
|
) {
|
|
5294
5807
|
emitDiagnostic({
|
|
5295
5808
|
code: "REACTIVE_WRITE_IN_OWNED_SCOPE",
|
|
@@ -5323,175 +5836,196 @@ function markRefresh(node) {
|
|
|
5323
5836
|
}
|
|
5324
5837
|
|
|
5325
5838
|
export {
|
|
5326
|
-
|
|
5327
|
-
|
|
5328
|
-
|
|
5839
|
+
setPassLane as $,
|
|
5840
|
+
tracking as A,
|
|
5841
|
+
CONFIG_CHILDREN_FORBIDDEN as B,
|
|
5329
5842
|
CONFIG_GUESS as C,
|
|
5330
|
-
|
|
5331
|
-
|
|
5332
|
-
|
|
5843
|
+
passLane as D,
|
|
5844
|
+
STATUS_PENDING as E,
|
|
5845
|
+
EFFECT_RENDER as F,
|
|
5333
5846
|
GlobalQueue as G,
|
|
5334
|
-
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
5847
|
+
sameLane as H,
|
|
5848
|
+
flushTransaction as I,
|
|
5849
|
+
REACTIVE_SCREEN_READ as J,
|
|
5850
|
+
REACTIVE_FRAME_READ as K,
|
|
5851
|
+
attrHooks as L,
|
|
5852
|
+
commitPendingNode as M,
|
|
5340
5853
|
NOT_PENDING as N,
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5854
|
+
REACTIVE_IN_HEAP as O,
|
|
5855
|
+
REACTIVE_DIRTY as P,
|
|
5856
|
+
REACTIVE_CHECK as Q,
|
|
5344
5857
|
REACTIVE_DISPOSED as R,
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
5858
|
+
STATUS_UNINITIALIZED as S,
|
|
5859
|
+
CONFIG_INPUTS_PUBLISHED as T,
|
|
5860
|
+
releaseQueues as U,
|
|
5861
|
+
CONFIG_SLOT_NODE as V,
|
|
5349
5862
|
holdNode as W,
|
|
5350
5863
|
joinFuture as X,
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
|
|
5355
|
-
|
|
5356
|
-
|
|
5357
|
-
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
|
|
5361
|
-
|
|
5362
|
-
|
|
5864
|
+
reruns as Y,
|
|
5865
|
+
CONFIG_STAGED as Z,
|
|
5866
|
+
merge as _,
|
|
5867
|
+
NotReadyError as a,
|
|
5868
|
+
setMemo as a$,
|
|
5869
|
+
REACTIVE_JOINED as a0,
|
|
5870
|
+
REACTIVE_STAGED_READ as a1,
|
|
5871
|
+
stagedRead as a2,
|
|
5872
|
+
setVerdict as a3,
|
|
5873
|
+
context as a4,
|
|
5874
|
+
installVerdictHooks as a5,
|
|
5875
|
+
REACTIVE_PROBED as a6,
|
|
5363
5876
|
verdict as a7,
|
|
5364
5877
|
link as a8,
|
|
5365
5878
|
pullComputed as a9,
|
|
5366
|
-
|
|
5367
|
-
|
|
5368
|
-
|
|
5369
|
-
|
|
5370
|
-
|
|
5371
|
-
|
|
5372
|
-
|
|
5373
|
-
|
|
5374
|
-
|
|
5375
|
-
|
|
5376
|
-
|
|
5377
|
-
|
|
5378
|
-
|
|
5379
|
-
|
|
5380
|
-
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
|
|
5384
|
-
|
|
5385
|
-
|
|
5386
|
-
|
|
5387
|
-
|
|
5388
|
-
|
|
5389
|
-
|
|
5390
|
-
|
|
5391
|
-
|
|
5392
|
-
|
|
5879
|
+
cleanup as aA,
|
|
5880
|
+
read as aB,
|
|
5881
|
+
DEV as aC,
|
|
5882
|
+
CONFIG_TRANSPARENT as aD,
|
|
5883
|
+
NoOwnerError as aE,
|
|
5884
|
+
ContextNotFoundError as aF,
|
|
5885
|
+
createEffectNode as aG,
|
|
5886
|
+
recompute as aH,
|
|
5887
|
+
EFFECT_USER as aI,
|
|
5888
|
+
reportDiagnostic as aJ,
|
|
5889
|
+
computed as aK,
|
|
5890
|
+
staleValues as aL,
|
|
5891
|
+
CONFIG_AUTO_DISPOSE as aM,
|
|
5892
|
+
EFFECT_TRACKED as aN,
|
|
5893
|
+
heldRunHook as aO,
|
|
5894
|
+
unwrapStatusError as aP,
|
|
5895
|
+
haltReactivityAt as aQ,
|
|
5896
|
+
setStrictRead as aR,
|
|
5897
|
+
StatusError as aS,
|
|
5898
|
+
trimStaleDeps as aT,
|
|
5899
|
+
setTrackedQueueCallback as aU,
|
|
5900
|
+
setEffectStatusNotify as aV,
|
|
5901
|
+
_hitUnhandledAsync as aW,
|
|
5902
|
+
resetUnhandledAsync as aX,
|
|
5903
|
+
ROOT_ERROR_HOOK as aY,
|
|
5904
|
+
registerGraph as aZ,
|
|
5905
|
+
runWithOwner as a_,
|
|
5393
5906
|
strictRead as aa,
|
|
5394
5907
|
warnStrictReadUntracked as ab,
|
|
5395
|
-
|
|
5396
|
-
|
|
5397
|
-
|
|
5398
|
-
|
|
5399
|
-
|
|
5400
|
-
|
|
5401
|
-
|
|
5402
|
-
|
|
5403
|
-
|
|
5404
|
-
|
|
5405
|
-
|
|
5406
|
-
|
|
5407
|
-
|
|
5408
|
-
|
|
5409
|
-
|
|
5410
|
-
|
|
5411
|
-
|
|
5412
|
-
|
|
5413
|
-
|
|
5414
|
-
|
|
5415
|
-
|
|
5416
|
-
|
|
5417
|
-
|
|
5418
|
-
|
|
5419
|
-
|
|
5420
|
-
|
|
5421
|
-
|
|
5422
|
-
|
|
5423
|
-
|
|
5424
|
-
|
|
5425
|
-
|
|
5426
|
-
|
|
5427
|
-
|
|
5428
|
-
|
|
5429
|
-
|
|
5430
|
-
|
|
5431
|
-
|
|
5432
|
-
|
|
5433
|
-
|
|
5434
|
-
|
|
5435
|
-
|
|
5436
|
-
|
|
5437
|
-
|
|
5438
|
-
|
|
5439
|
-
|
|
5440
|
-
|
|
5441
|
-
|
|
5442
|
-
|
|
5443
|
-
|
|
5444
|
-
|
|
5445
|
-
|
|
5446
|
-
|
|
5447
|
-
|
|
5448
|
-
|
|
5449
|
-
|
|
5450
|
-
|
|
5451
|
-
|
|
5452
|
-
|
|
5453
|
-
|
|
5454
|
-
|
|
5455
|
-
|
|
5456
|
-
|
|
5457
|
-
|
|
5458
|
-
|
|
5459
|
-
|
|
5460
|
-
|
|
5461
|
-
|
|
5462
|
-
|
|
5463
|
-
|
|
5464
|
-
|
|
5465
|
-
|
|
5466
|
-
|
|
5467
|
-
|
|
5468
|
-
|
|
5469
|
-
|
|
5470
|
-
|
|
5471
|
-
|
|
5472
|
-
|
|
5473
|
-
|
|
5474
|
-
|
|
5475
|
-
|
|
5476
|
-
|
|
5477
|
-
|
|
5478
|
-
|
|
5479
|
-
|
|
5480
|
-
|
|
5908
|
+
staleReader as ac,
|
|
5909
|
+
globalQueue as ad,
|
|
5910
|
+
joinPass as ae,
|
|
5911
|
+
markLateLinker as af,
|
|
5912
|
+
spectating as ag,
|
|
5913
|
+
unflushedValue as ah,
|
|
5914
|
+
CONFIG_STAGED_ERROR as ai,
|
|
5915
|
+
stagedReaders as aj,
|
|
5916
|
+
bornInto as ak,
|
|
5917
|
+
enterAction as al,
|
|
5918
|
+
setEffectCallback as am,
|
|
5919
|
+
exitAction as an,
|
|
5920
|
+
isThenable as ao,
|
|
5921
|
+
actionDepth as ap,
|
|
5922
|
+
flush as aq,
|
|
5923
|
+
setQuestion as ar,
|
|
5924
|
+
emitDiagnostic as as,
|
|
5925
|
+
enterCallback as at,
|
|
5926
|
+
exitCallback as au,
|
|
5927
|
+
getOwner as av,
|
|
5928
|
+
inEffectCallback as aw,
|
|
5929
|
+
setExternalHooks as ax,
|
|
5930
|
+
signal as ay,
|
|
5931
|
+
setSignal as az,
|
|
5932
|
+
CONFIG_AUTHORITATIVE as b,
|
|
5933
|
+
liveRootOwners as b$,
|
|
5934
|
+
$REFRESH as b0,
|
|
5935
|
+
markRefresh as b1,
|
|
5936
|
+
createRoot as b2,
|
|
5937
|
+
getObserver as b3,
|
|
5938
|
+
dispose as b4,
|
|
5939
|
+
TimeoutError as b5,
|
|
5940
|
+
untrack as b6,
|
|
5941
|
+
dirtyQueue as b7,
|
|
5942
|
+
CONFIG_REDERIVE as b8,
|
|
5943
|
+
passTx as b9,
|
|
5944
|
+
settlePendingSource as bA,
|
|
5945
|
+
joinPassAt as bB,
|
|
5946
|
+
notifyEpoch as bC,
|
|
5947
|
+
SUPPORTS_PROXY as bD,
|
|
5948
|
+
releaseMarksHook as bE,
|
|
5949
|
+
installMarkHooks as bF,
|
|
5950
|
+
CONFIG_LAGS as bG,
|
|
5951
|
+
REACTIVE_REASK as bH,
|
|
5952
|
+
haltReactivity as bI,
|
|
5953
|
+
heldUnder as bJ,
|
|
5954
|
+
unhold as bK,
|
|
5955
|
+
laneStageHook as bL,
|
|
5956
|
+
REACTIVE_ZOMBIE as bM,
|
|
5957
|
+
installBoundaryHooks as bN,
|
|
5958
|
+
CAUGHT_REARMED as bO,
|
|
5959
|
+
OBSERVE as bP,
|
|
5960
|
+
clearSnapshots as bQ,
|
|
5961
|
+
enforceLoadingBoundary as bR,
|
|
5962
|
+
getNextChildId as bS,
|
|
5963
|
+
markSnapshotScope as bT,
|
|
5964
|
+
peekNextChildId as bU,
|
|
5965
|
+
releaseSnapshotScope as bV,
|
|
5966
|
+
resetErrorHalt as bW,
|
|
5967
|
+
setConsoleFooter as bX,
|
|
5968
|
+
setSnapshotCapture as bY,
|
|
5969
|
+
setWriteHook as bZ,
|
|
5970
|
+
setAttributionHooks as b_,
|
|
5971
|
+
CONFIG_FRESH_READ as ba,
|
|
5972
|
+
createOwner as bb,
|
|
5973
|
+
writeHook as bc,
|
|
5974
|
+
REACTIVE_WRITE_IN_OWNED_SCOPE_SIGNAL_MESSAGE as bd,
|
|
5975
|
+
ownedScopeWriteMessage as be,
|
|
5976
|
+
isEqual as bf,
|
|
5977
|
+
CONFIG_MANUAL_WRITE as bg,
|
|
5978
|
+
snapshotCaptureActive as bh,
|
|
5979
|
+
captureWriteSnapshot as bi,
|
|
5980
|
+
notePromotedWrite as bj,
|
|
5981
|
+
slotSignal as bk,
|
|
5982
|
+
stagedScreen as bl,
|
|
5983
|
+
CONFIG_PROMOTED as bm,
|
|
5984
|
+
setSlotUnobserved as bn,
|
|
5985
|
+
installStoreHooks as bo,
|
|
5986
|
+
joinPassTx as bp,
|
|
5987
|
+
asyncTailFlights as bq,
|
|
5988
|
+
untrackDepth as br,
|
|
5989
|
+
checkPostAwaitRead as bs,
|
|
5990
|
+
disposalDepth as bt,
|
|
5991
|
+
insertIntoHeap as bu,
|
|
5992
|
+
isDisposed as bv,
|
|
5993
|
+
handleAsync as bw,
|
|
5994
|
+
addPendingSource as bx,
|
|
5995
|
+
propagateStatus as by,
|
|
5996
|
+
forEachDependent as bz,
|
|
5997
|
+
CONFIG_OVERRIDE as c,
|
|
5998
|
+
ownerPath as c0,
|
|
5999
|
+
isSuppressed as c1,
|
|
6000
|
+
blockersOf as c2,
|
|
6001
|
+
CONFIG_PLUMBING as c3,
|
|
6002
|
+
anyExcluded as c4,
|
|
6003
|
+
isExcluded as c5,
|
|
6004
|
+
CONFIG_WIDE as c6,
|
|
6005
|
+
GRAPH_SIZE_WARN_AT as c7,
|
|
6006
|
+
noteFanOut as c8,
|
|
6007
|
+
CONFIG_SUPERSEDES as c9,
|
|
6008
|
+
newTransaction as d,
|
|
6009
|
+
enqueueSub as e,
|
|
6010
|
+
CONFIG_VERDICT as f,
|
|
6011
|
+
REACTIVE_PROBE_UNANSWERED as g,
|
|
6012
|
+
blocked as h,
|
|
6013
|
+
installLaneHooks as i,
|
|
6014
|
+
REACTIVE_LANE_DIRTY as j,
|
|
5481
6015
|
CONFIG_HELD as k,
|
|
5482
|
-
|
|
5483
|
-
|
|
6016
|
+
clock as l,
|
|
6017
|
+
insertSubs as m,
|
|
5484
6018
|
nextQuestion as n,
|
|
5485
|
-
|
|
5486
|
-
|
|
6019
|
+
ownFlights as o,
|
|
6020
|
+
laneDirty as p,
|
|
5487
6021
|
question as q,
|
|
5488
6022
|
resolveTx as r,
|
|
5489
6023
|
schedule as s,
|
|
5490
6024
|
txOf as t,
|
|
5491
|
-
|
|
5492
|
-
|
|
5493
|
-
|
|
5494
|
-
|
|
5495
|
-
|
|
5496
|
-
|
|
6025
|
+
REACTIVE_LANE_READ as u,
|
|
6026
|
+
ext as v,
|
|
6027
|
+
REACTIVE_RECOMPUTING_DEPS as w,
|
|
6028
|
+
queuePendingNode as x,
|
|
6029
|
+
list as y,
|
|
6030
|
+
STATUS_ERROR as z
|
|
5497
6031
|
};
|