@solidjs/signals 2.0.0-rc.8 → 2.0.0-rc.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dev-shared.js +1399 -285
- package/dist/dev.attribution.js +459 -307
- package/dist/dev.js +1864 -435
- package/dist/observe/affects.js +3 -1
- package/dist/observe/attribution.js +7 -1
- package/dist/observe/boundaries.js +209 -154
- package/dist/observe/core/action.js +18 -8
- package/dist/observe/core/async.js +220 -110
- package/dist/observe/core/attribution-costs.js +66 -0
- package/dist/observe/core/attribution-feedback.js +282 -0
- package/dist/observe/core/attribution-hooks.js +23 -1
- package/dist/observe/core/attribution-queries.js +28 -0
- package/dist/observe/core/attribution.js +262 -485
- package/dist/observe/core/constants.js +34 -1
- package/dist/observe/core/context.js +3 -3
- package/dist/observe/core/core.js +867 -367
- package/dist/observe/core/dev.js +78 -17
- package/dist/observe/core/effect.js +67 -51
- package/dist/observe/core/error-hooks.js +71 -0
- package/dist/observe/core/external.js +4 -4
- package/dist/observe/core/graph.js +37 -37
- package/dist/observe/core/heap.js +45 -45
- package/dist/observe/core/invariants.js +2 -0
- package/dist/observe/core/lanes.js +86 -49
- package/dist/observe/core/optimistic.js +242 -95
- package/dist/observe/core/owner.js +98 -84
- package/dist/observe/core/scheduler.js +543 -305
- package/dist/observe/core/verdict.js +247 -129
- package/dist/observe/index.js +7 -3
- package/dist/observe/map.js +126 -124
- package/dist/observe/signals.js +33 -17
- package/dist/observe/store/index.js +2 -0
- package/dist/observe/store/next/optimistic.js +141 -132
- package/dist/observe/store/next/projection.js +34 -21
- package/dist/observe/store/next/reconcile.js +58 -56
- package/dist/observe/store/next/store.js +260 -146
- package/dist/observe/store/store.js +5 -3
- package/dist/observe/store/utils.js +948 -135
- package/dist/prod/attribution.js +26 -17
- package/dist/prod/boundaries.js +128 -76
- package/dist/prod/core/action.js +16 -8
- package/dist/prod/core/async.js +251 -143
- package/dist/prod/core/constants.js +34 -1
- package/dist/prod/core/context.js +3 -3
- package/dist/prod/core/core.js +843 -347
- package/dist/prod/core/dev.js +17 -1
- package/dist/prod/core/effect.js +48 -34
- package/dist/prod/core/error-hooks.js +71 -0
- package/dist/prod/core/external.js +4 -4
- package/dist/prod/core/graph.js +37 -37
- package/dist/prod/core/heap.js +45 -45
- package/dist/prod/core/lanes.js +90 -53
- package/dist/prod/core/optimistic.js +247 -100
- package/dist/prod/core/owner.js +57 -45
- package/dist/prod/core/scheduler.js +543 -305
- package/dist/prod/core/verdict.js +245 -127
- package/dist/prod/index.js +7 -3
- package/dist/prod/map.js +112 -112
- package/dist/prod/signals.js +28 -12
- package/dist/prod/store/next/optimistic.js +135 -128
- package/dist/prod/store/next/projection.js +31 -20
- package/dist/prod/store/next/store.js +325 -252
- package/dist/prod/store/store.js +2 -2
- package/dist/prod/store/utils.js +946 -135
- package/dist/types/attribution.d.ts +7 -2
- package/dist/types/attribution.prod.d.ts +10 -1
- package/dist/types/boundaries.d.ts +10 -1
- package/dist/types/core/action.d.ts +12 -5
- package/dist/types/core/attribution-costs.d.ts +35 -0
- package/dist/types/core/attribution-feedback.d.ts +133 -0
- package/dist/types/core/attribution-hooks.d.ts +28 -3
- package/dist/types/core/attribution-queries.d.ts +10 -0
- package/dist/types/core/attribution.d.ts +51 -172
- package/dist/types/core/constants.d.ts +33 -0
- package/dist/types/core/core.d.ts +186 -5
- package/dist/types/core/dev.d.ts +129 -9
- package/dist/types/core/error-hooks.d.ts +71 -0
- package/dist/types/core/index.d.ts +3 -1
- package/dist/types/core/invariants.d.ts +4 -0
- package/dist/types/core/lanes.d.ts +31 -4
- package/dist/types/core/scheduler.d.ts +95 -2
- package/dist/types/core/types.d.ts +3 -0
- package/dist/types/index.d.ts +2 -2
- package/dist/types/signals.d.ts +8 -0
- package/dist/types/store/index.d.ts +2 -1
- package/dist/types/store/next/optimistic.d.ts +1 -1
- package/dist/types/store/next/store.d.ts +6 -5
- package/dist/types/store/next/target.d.ts +1 -1
- package/dist/types/store/utils.d.ts +177 -6
- package/package.json +1 -1
package/dist/dev-shared.js
CHANGED
|
@@ -200,6 +200,18 @@ const CONFIG_SLOT_NODE = 1 << 18;
|
|
|
200
200
|
* paths (asyncWrite, transition-held recompute); cleared by a fresh optimistic
|
|
201
201
|
* write (a new override re-masks) and by the revert. */
|
|
202
202
|
const CONFIG_OVERRIDE_SUPERSEDED = 1 << 19;
|
|
203
|
+
/** HELD children (#3404): this node's `_firstChild` chain (and `_disposal`
|
|
204
|
+
* list) was built by a recompute whose result has not committed — a staged
|
|
205
|
+
* value, a pending window, or a run under a held transaction. A later
|
|
206
|
+
* recompute may tear those children down immediately: nothing observable
|
|
207
|
+
* was ever built on them. Unset, the children belong to the committed frame
|
|
208
|
+
* and a recompute defers them as zombies (`_pendingFirstChild`) until this
|
|
209
|
+
* node commits — regardless of whether the recompute runs under a
|
|
210
|
+
* transaction. A parked node (status propagation stamps `_transition`
|
|
211
|
+
* without recomputing) recomputed when its source lands otherwise disposed
|
|
212
|
+
* its committed children mid-hold, running their cleanups before the
|
|
213
|
+
* transaction's atomic reveal. Cleared by `commitPendingNode`. */
|
|
214
|
+
const CONFIG_HELD_CHILDREN = 1 << 20;
|
|
203
215
|
/** In-flight async node whose inputs were PUBLISHED while it was pending: a
|
|
204
216
|
* batch or transaction committed with the node still `STATUS_PENDING` (an
|
|
205
217
|
* unobserved flight, #3305), so the inputs are on screen and the node's
|
|
@@ -211,6 +223,27 @@ const CONFIG_OVERRIDE_SUPERSEDED = 1 << 19;
|
|
|
211
223
|
* lane-revealed. Set by `commitPendingNodes`; cleared when the node next
|
|
212
224
|
* enters pending fresh (a new flight from a settled state). */
|
|
213
225
|
const CONFIG_INPUTS_PUBLISHED = 1 << 21;
|
|
226
|
+
/** A28 (4): the node was written inside a recompute that ran OUTSIDE a flush
|
|
227
|
+
* (a creation-time compute — boundary machinery, a mapArray's first run). Such
|
|
228
|
+
* a write is promoted at that recompute's end: readers in the same block see
|
|
229
|
+
* it. Cleared when the next flush begins; set only on that rare path. */
|
|
230
|
+
const CONFIG_PROMOTED = 1 << 22;
|
|
231
|
+
/** A28 for same-tick adoption: the node was staged outside a flush and then
|
|
232
|
+
* adopted by a transaction (initTransition) before any flush carried the
|
|
233
|
+
* staging — the stamp says "held", but nothing flushed is staged for it, so
|
|
234
|
+
* on no channel is the write visible yet: `latest()` answers the committed
|
|
235
|
+
* value, the verdict sees nothing pending (as the store's leaves already did
|
|
236
|
+
* through their own selection). Cleared when the carrying flush re-stamps the
|
|
237
|
+
* transaction's pending nodes (reassignPendingTransition). */
|
|
238
|
+
const CONFIG_ADOPTED_UNFLUSHED = 1 << 24;
|
|
239
|
+
/** The node's active override is a DERIVED one: a lane pass published its
|
|
240
|
+
* speculative result into the override slot instead of `_value` (lanes
|
|
241
|
+
* stage — an optimistic derivation is an override, #3479). Its truth is not
|
|
242
|
+
* `_value` but a recompute from its inputs' truth, so the body-end
|
|
243
|
+
* supersession (`endOptimism`) and the authoritative-flight blockage
|
|
244
|
+
* (`transitionBlocked`) skip it; the revert drops the override and re-derives
|
|
245
|
+
* it (`resolveOptimisticNodes`). Cleared with the override. */
|
|
246
|
+
const CONFIG_DERIVED_OVERRIDE = 1 << 23;
|
|
214
247
|
const STATUS_PENDING = 1 << 0;
|
|
215
248
|
const STATUS_ERROR = 1 << 1;
|
|
216
249
|
const STATUS_UNINITIALIZED = 1 << 2;
|
|
@@ -249,8 +282,18 @@ const defaultContext = {};
|
|
|
249
282
|
const $REFRESH = Symbol("refresh");
|
|
250
283
|
|
|
251
284
|
let attrHooks = null;
|
|
285
|
+
/**
|
|
286
|
+
* The installed engine, registered on `globalThis` as well (the records
|
|
287
|
+
* channel's reason, see `Records`): a wire layer bundled without a framework
|
|
288
|
+
* import — `@solidjs/web`'s server-function client — stamps the records it
|
|
289
|
+
* emits through the engine's `currentOrigin`, and this is its reach. The
|
|
290
|
+
* module binding stays the core's own read (one null check per hook site);
|
|
291
|
+
* the registration mirrors it.
|
|
292
|
+
*/
|
|
293
|
+
const INSTALLED = Symbol.for("@solidjs/signals/observe/attribution");
|
|
252
294
|
function setAttributionHooks(hooks) {
|
|
253
295
|
attrHooks = hooks;
|
|
296
|
+
globalThis[INSTALLED] = hooks ?? undefined;
|
|
254
297
|
}
|
|
255
298
|
/**
|
|
256
299
|
* Run `fn` as the handler of a user interaction: every root write it performs
|
|
@@ -305,6 +348,18 @@ function withOrigin(ref, fn) {
|
|
|
305
348
|
hooks.originEnd();
|
|
306
349
|
}
|
|
307
350
|
}
|
|
351
|
+
/**
|
|
352
|
+
* The provenance a root write performed now would carry, as the installed
|
|
353
|
+
* engine sees it (`AttributionHooks.currentOrigin`); `undefined` with no
|
|
354
|
+
* engine, or when nothing is in effect. Reachable as
|
|
355
|
+
* `OBSERVE.attribution.currentOrigin` — how a runtime stamps a fact of its
|
|
356
|
+
* own (a server-function call) with the interaction or navigation it ran
|
|
357
|
+
* for, so an observer joins the two by identity.
|
|
358
|
+
*/
|
|
359
|
+
function currentOrigin() {
|
|
360
|
+
const hooks = attrHooks;
|
|
361
|
+
return hooks === null ? undefined : hooks.currentOrigin();
|
|
362
|
+
}
|
|
308
363
|
|
|
309
364
|
/** First warning when a change reaches (or a pass tracks) this many edges. */
|
|
310
365
|
const GRAPH_SIZE_WARN_AT = 2000;
|
|
@@ -347,13 +402,59 @@ const attributionSlot = {
|
|
|
347
402
|
return attrHooks;
|
|
348
403
|
},
|
|
349
404
|
withInteraction,
|
|
350
|
-
withOrigin
|
|
405
|
+
withOrigin,
|
|
406
|
+
currentOrigin
|
|
351
407
|
};
|
|
408
|
+
// The records channel is process-wide (see `Records`): a host that bundles
|
|
409
|
+
// the core into its server build beside an instrumented `--import`ed copy
|
|
410
|
+
// holds two of this module, and a listener installed through one must hear
|
|
411
|
+
// the records the render emits through the other. The registered key makes
|
|
412
|
+
// every copy find the one listener set; the object is generic — a Map of
|
|
413
|
+
// type to listener set — and carries no knowledge of the records.
|
|
414
|
+
const RECORDS = Symbol.for("@solidjs/signals/observe/records");
|
|
415
|
+
function recordsChannel() {
|
|
416
|
+
const g = globalThis;
|
|
417
|
+
if (g[RECORDS]) return g[RECORDS];
|
|
418
|
+
const listeners = new Map();
|
|
419
|
+
return (g[RECORDS] = {
|
|
420
|
+
subscribe(type, listener) {
|
|
421
|
+
let set = listeners.get(type);
|
|
422
|
+
if (!set) listeners.set(type, (set = new Set()));
|
|
423
|
+
set.add(listener);
|
|
424
|
+
return () => {
|
|
425
|
+
set.delete(listener);
|
|
426
|
+
};
|
|
427
|
+
},
|
|
428
|
+
observed(type) {
|
|
429
|
+
const set = listeners.get(type);
|
|
430
|
+
return set !== undefined && set.size > 0;
|
|
431
|
+
},
|
|
432
|
+
emit(type, event, live) {
|
|
433
|
+
const set = listeners.get(type);
|
|
434
|
+
if (set === undefined || set.size === 0) return;
|
|
435
|
+
// Snapshot: a listener unsubscribing (itself or another) mid-delivery
|
|
436
|
+
// must not skip or double-call anyone this round. A throwing listener
|
|
437
|
+
// is reported; the others, and what was observed, are unaffected.
|
|
438
|
+
for (const listener of [...set]) {
|
|
439
|
+
try {
|
|
440
|
+
listener(event, live);
|
|
441
|
+
} catch (error) {
|
|
442
|
+
console.error(error);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
}
|
|
352
448
|
const OBSERVE = {
|
|
353
449
|
diagnostics,
|
|
450
|
+
records: recordsChannel(),
|
|
354
451
|
attribution: attributionSlot,
|
|
355
|
-
|
|
356
|
-
|
|
452
|
+
// Replaced by solid-js's server entry (see `ServerObserve`); on the
|
|
453
|
+
// client the slot stays this placeholder. The cast: the interface is
|
|
454
|
+
// empty HERE and gains its members by augmentation downstream.
|
|
455
|
+
server: {},
|
|
456
|
+
subjectOf(record) {
|
|
457
|
+
return eventSubjects.get(record);
|
|
357
458
|
},
|
|
358
459
|
exclude(owner) {
|
|
359
460
|
excludedOwners.add(owner);
|
|
@@ -464,13 +565,26 @@ function takeFooter(entry) {
|
|
|
464
565
|
return consoleFooter(entry);
|
|
465
566
|
}
|
|
466
567
|
/**
|
|
467
|
-
* The subject each emitted event was about
|
|
468
|
-
*
|
|
469
|
-
*
|
|
470
|
-
* the
|
|
471
|
-
*
|
|
568
|
+
* The subject each emitted event was about: events are serializable records
|
|
569
|
+
* and cannot carry the node, so the node is kept beside the record for the
|
|
570
|
+
* in-process consumers that want it — the console step, which can show what
|
|
571
|
+
* the node knows (a rendering runtime may stamp a binding effect with the DOM
|
|
572
|
+
* element it writes, `_devElement`, and a live element reference beside the
|
|
573
|
+
* message is the most addressable pointer a console can print), and devtools
|
|
574
|
+
* that go from a re-run record back to the scope that ran. Keyed by the
|
|
575
|
+
* record object, so the subject lives exactly as long as some consumer holds
|
|
576
|
+
* the record (a ring buffer, a captured artifact) — the same lifetime the
|
|
577
|
+
* node had when records carried it directly.
|
|
472
578
|
*/
|
|
473
579
|
const eventSubjects = new WeakMap();
|
|
580
|
+
/** Register `subject` as what `record` was about — see `Observe.subjectOf`. */
|
|
581
|
+
function recordSubject(record, subject) {
|
|
582
|
+
eventSubjects.set(record, subject);
|
|
583
|
+
}
|
|
584
|
+
/** The live subject `record` was about, if its emitter registered one. */
|
|
585
|
+
function subjectOf(record) {
|
|
586
|
+
return eventSubjects.get(record);
|
|
587
|
+
}
|
|
474
588
|
/**
|
|
475
589
|
* The console face of a diagnostic — ONE entry per finding: the message, the
|
|
476
590
|
* owner path (`in <App> › <TodoRow> › effect`) so a human can locate it, the
|
|
@@ -773,6 +887,47 @@ function laneHeld(lane) {
|
|
|
773
887
|
for (const node of lane._pendingAsync) if (waitingTransition(node) !== null) return true;
|
|
774
888
|
return false;
|
|
775
889
|
}
|
|
890
|
+
/**
|
|
891
|
+
* Lanes mirror transitions (#3460): a render effect OFF a HELD lane that reads
|
|
892
|
+
* a value the lane is revealing — an override, a `latest()` shadow — sees the
|
|
893
|
+
* committed value, exactly as a stale reader of a held transaction does
|
|
894
|
+
* (A15 reveal corollary): it publishes now, with the frame that is on screen
|
|
895
|
+
* (the lane defers its own readers' runs, so the committed value is what is
|
|
896
|
+
* visible), entangles nothing — a sync write is never held by a lane — and
|
|
897
|
+
* re-derives at the release. The release re-run rides the lane's own render
|
|
898
|
+
* queue, which runs when the lane reveals (runLaneEffects) or its transaction
|
|
899
|
+
* commits (cleanupCompletedLanes). A reader ON the lane computes the lane's
|
|
900
|
+
* reveal and takes the value as before.
|
|
901
|
+
*
|
|
902
|
+
* OFF the lane is provenance, not membership — the transaction mirror
|
|
903
|
+
* exactly: a stale reader of a transaction is a pass that runs outside it. A
|
|
904
|
+
* pass under the lane's own transaction is the lane's work — its write (lane
|
|
905
|
+
* posture), or the landing of its async, which re-enters the transaction
|
|
906
|
+
* (#3334) and runs a member with no ambient lane. Read as an outsider, that
|
|
907
|
+
* pass published the committed view and queued a replay that revealed the
|
|
908
|
+
* override beside its unready derivation at the release (`1:0` for an
|
|
909
|
+
* optimistic frame that never became ready; #3479 review). Membership is the
|
|
910
|
+
* wrong test the other way: a member re-run by a sibling's sync write is a
|
|
911
|
+
* mainline pass and shows the committed view.
|
|
912
|
+
*/
|
|
913
|
+
function readsHeldCommitted(owner, c) {
|
|
914
|
+
const lane = resolveLane(owner);
|
|
915
|
+
if (!lane || !laneHeld(lane)) return false;
|
|
916
|
+
if (ownsLane(lane, owner)) return false;
|
|
917
|
+
lane._effectQueues[0].push(() => c._flags & REACTIVE_DISPOSED || enqueueSub(c));
|
|
918
|
+
return true;
|
|
919
|
+
}
|
|
920
|
+
/** The ownership relation for a lane hold (core `ownsHold`, §6 ruling 2): the
|
|
921
|
+
* running pass owns `lane`'s hold if it runs under the transition that owns
|
|
922
|
+
* the lane (the node's, resolved through override ownership and merges) or
|
|
923
|
+
* inside the lane itself. */
|
|
924
|
+
function ownsLane(lane, owner) {
|
|
925
|
+
if (activeTransition !== null) {
|
|
926
|
+
const t = resolveTransition(owner);
|
|
927
|
+
if (t && ownsHold(t)) return true;
|
|
928
|
+
}
|
|
929
|
+
return currentOptimisticLane !== null && findLane(currentOptimisticLane) === lane;
|
|
930
|
+
}
|
|
776
931
|
/**
|
|
777
932
|
* Merge two lanes when their dependency graphs overlap.
|
|
778
933
|
*/
|
|
@@ -816,13 +971,6 @@ function resolveTransition(el) {
|
|
|
816
971
|
}
|
|
817
972
|
return resolveLane(el)?._transition ?? el._transition;
|
|
818
973
|
}
|
|
819
|
-
/**
|
|
820
|
-
* Check if a node has an active optimistic override.
|
|
821
|
-
*/
|
|
822
|
-
function hasActiveOverride(el) {
|
|
823
|
-
const x = el._x;
|
|
824
|
-
return x !== null && x._overrideValue !== undefined && x._overrideValue !== NOT_PENDING;
|
|
825
|
-
}
|
|
826
974
|
/**
|
|
827
975
|
* Assign or merge a lane onto a node. At convergence points (node already has
|
|
828
976
|
* a different active lane), merge unless the node has an active override.
|
|
@@ -831,17 +979,21 @@ function assignOrMergeLane(el, sourceLane) {
|
|
|
831
979
|
const sourceRoot = findLane(sourceLane);
|
|
832
980
|
const existing = el._x?._optimisticLane;
|
|
833
981
|
if (existing) {
|
|
834
|
-
//
|
|
835
|
-
//
|
|
836
|
-
// (
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
return;
|
|
841
|
-
}
|
|
982
|
+
// A merged lane is followed to its root like any other: the root is where
|
|
983
|
+
// the subscriber's affinity lives now. Replacing it with the source lane
|
|
984
|
+
// outright (as this once did) skipped the parent/child check below — an
|
|
985
|
+
// isPending reader of two async siblings had their two companion lanes
|
|
986
|
+
// merge, and the next parent-lane notification then moved it onto the
|
|
987
|
+
// held parent, where its verdict waited on the async it reports (#3409).
|
|
842
988
|
const existingRoot = findLane(existing);
|
|
843
989
|
if (activeLanes.has(existingRoot)) {
|
|
844
|
-
|
|
990
|
+
// A WRITTEN override is its own lane's source and merges nothing
|
|
991
|
+
// through it; a derived one (lanes stage, #3479) is a plain member —
|
|
992
|
+
// the shared reader that merges two writers' lanes carries one.
|
|
993
|
+
if (
|
|
994
|
+
existingRoot !== sourceRoot &&
|
|
995
|
+
(!hasActiveOverride(el) || el._config & CONFIG_DERIVED_OVERRIDE)
|
|
996
|
+
) {
|
|
845
997
|
// Parent-child lanes stay independent so isPending resolves without
|
|
846
998
|
// waiting for the parent's async. The child keeps ownership.
|
|
847
999
|
if (sourceRoot._parentLane && findLane(sourceRoot._parentLane) === existingRoot) {
|
|
@@ -872,8 +1024,16 @@ const zombieQueue = {
|
|
|
872
1024
|
};
|
|
873
1025
|
/** runHeap callback that discards a queued zombie recompute instead of running
|
|
874
1026
|
* it: unlink pure recompute entries; strip just the recompute bit from dirtied
|
|
875
|
-
* height-adjust entries so their height work still happens.
|
|
1027
|
+
* height-adjust entries so their height work still happens. A zombie dirtied
|
|
1028
|
+
* through the lane channel (OPTIMISTIC_DIRTY — an override or a `latest()`
|
|
1029
|
+
* companion) runs instead (#3444): a zombie renders mainline until the commit
|
|
1030
|
+
* disposes it, and the lane's values ARE the mainline frame — the still-visible
|
|
1031
|
+
* branch a held `Show` is removing showed the old `latest(count)` beside the
|
|
1032
|
+
* new one outside. Its pass runs under the lane and its run lands on the
|
|
1033
|
+
* lane's effect queue, so a held lane defers it exactly as it defers every
|
|
1034
|
+
* other reader's. */
|
|
876
1035
|
function cancelZombieRecompute(el) {
|
|
1036
|
+
if (el._flags & REACTIVE_OPTIMISTIC_DIRTY) return GlobalQueue._update(el);
|
|
877
1037
|
if (el._flags & REACTIVE_IN_HEAP_HEIGHT) el._flags &= -12;
|
|
878
1038
|
else {
|
|
879
1039
|
deleteFromHeap(el, zombieQueue);
|
|
@@ -903,11 +1063,19 @@ let _hitUnhandledAsync = false;
|
|
|
903
1063
|
// fact about the MOUNT ("the root mount will be deferred"), not about each
|
|
904
1064
|
// pending render effect — N async siblings at mount used to produce N copies.
|
|
905
1065
|
let _reportedUnhandledAsync = false;
|
|
906
|
-
// Store property nodes
|
|
907
|
-
//
|
|
908
|
-
//
|
|
909
|
-
//
|
|
1066
|
+
// Store property nodes whose last subscriber left while they carried state
|
|
1067
|
+
// the backing cannot reconstruct — an optimistic override (overrides live on
|
|
1068
|
+
// nodes, over a clone the setter discards) or a staged write. Releasing the
|
|
1069
|
+
// slot then would drop the override: an optimistic store key read `0` the
|
|
1070
|
+
// moment its only reader gated away while the action was live (S7). Swept
|
|
1071
|
+
// after each flush — a node still without subs whose override and staging
|
|
1072
|
+
// have resolved is released through the slot hook; one that regained a
|
|
1073
|
+
// subscriber leaves the set.
|
|
910
1074
|
const transientStoreNodes = new Set();
|
|
1075
|
+
/** Slot hook's deferral: release this node when its carried state resolves. */
|
|
1076
|
+
function deferSlotRelease(node) {
|
|
1077
|
+
transientStoreNodes.add(node);
|
|
1078
|
+
}
|
|
911
1079
|
function canUseSimpleSyncFlush(queue) {
|
|
912
1080
|
const batch = queue._batch;
|
|
913
1081
|
return (
|
|
@@ -1000,6 +1168,7 @@ function mergeTransitionState(target, outgoing) {
|
|
|
1000
1168
|
if (attrHooks !== null) attrHooks.transitionMerged(target, outgoing);
|
|
1001
1169
|
outgoing._done = target;
|
|
1002
1170
|
target._actions.push(...outgoing._actions);
|
|
1171
|
+
target._acted ||= outgoing._acted;
|
|
1003
1172
|
for (const lane of activeLanes) if (lane._transition === outgoing) lane._transition = target;
|
|
1004
1173
|
if (outgoing._optimisticNodes.length) {
|
|
1005
1174
|
// Move (don't copy): the global queue's batch may still be the outgoing
|
|
@@ -1134,11 +1303,47 @@ function schedule() {
|
|
|
1134
1303
|
scheduled = true;
|
|
1135
1304
|
if (!syncDepth && !globalQueue._running && !projectionWriteActive) queueMicrotask(flush);
|
|
1136
1305
|
}
|
|
1306
|
+
/**
|
|
1307
|
+
* Parked transactions whose reporter set changed without a write. A
|
|
1308
|
+
* transaction completes when nothing live reports a flight it waits on, but
|
|
1309
|
+
* the flush only judges the ACTIVE transaction: a parked one is re-entered by
|
|
1310
|
+
* a stamped node's landing or an action's resume. A reporter that stops
|
|
1311
|
+
* counting for another reason — its loading boundary flipped to the fallback
|
|
1312
|
+
* (#3375), or it was disposed by ambient work (#3372) — is neither: the
|
|
1313
|
+
* pruning in `reporterBlocksSource` would drop it at the next check, but no
|
|
1314
|
+
* check comes, and the writes held with it stay staged. Such sites record the
|
|
1315
|
+
* transaction here (deduped: one idle pass per transaction, however many
|
|
1316
|
+
* reporters changed); the flush re-enters it on an otherwise idle pass, so
|
|
1317
|
+
* the re-evaluation adopts no unrelated ambient work.
|
|
1318
|
+
*/
|
|
1319
|
+
const wokenTransitions = [];
|
|
1320
|
+
/** Wake every parked transaction — for a site that knows a reporter stopped
|
|
1321
|
+
* counting but not whose (a boundary reset). */
|
|
1322
|
+
function wakeParked() {
|
|
1323
|
+
for (const t of transitions) wokenTransitions.includes(t) || wokenTransitions.push(t);
|
|
1324
|
+
schedule();
|
|
1325
|
+
}
|
|
1326
|
+
/** Transactions a mainline tick has PROPOSED against (A34, #3494): a write to a
|
|
1327
|
+
* node one of them holds — the same value or another — is a second proposal
|
|
1328
|
+
* on a contested node, and the tick reveals with the hold ("both are
|
|
1329
|
+
* suggesting a value; if one finished before the other that would be odd").
|
|
1330
|
+
* Entered at the next flush's start, where the ambient batch is adopted;
|
|
1331
|
+
* never from the write itself, which left `activeTransition` set across the
|
|
1332
|
+
* caller's block and made creation after the write the transaction's (A29). */
|
|
1333
|
+
const batchJoins = [];
|
|
1137
1334
|
/**
|
|
1138
1335
|
* Permanently halts the reactive system. Called when a user error escapes
|
|
1139
1336
|
* every boundary — app state is undefined at that point, so scheduling stops
|
|
1140
1337
|
* entirely rather than limping along with a half-applied update.
|
|
1141
1338
|
*/
|
|
1339
|
+
/**
|
|
1340
|
+
* The key a root owner carries its client error hook under (`render`'s
|
|
1341
|
+
* `onError`) — registered, so a runtime writes it with no import of the hook
|
|
1342
|
+
* module (core/error-hooks.ts) and no property mangling in the way. Defined
|
|
1343
|
+
* HERE, not there: a runtime that only writes the key must not retain the
|
|
1344
|
+
* hook machinery (pay-for-use).
|
|
1345
|
+
*/
|
|
1346
|
+
const ROOT_ERROR_HOOK = Symbol.for("solid-js/root-error-hook");
|
|
1142
1347
|
function haltReactivity(cause) {
|
|
1143
1348
|
if (halted) return;
|
|
1144
1349
|
halted = true;
|
|
@@ -1353,11 +1558,29 @@ class GlobalQueue extends Queue {
|
|
|
1353
1558
|
* authoritative-observer wake for a silent confirm. Installed with the
|
|
1354
1559
|
* optimistic engine; only reachable on a node that has an override. */
|
|
1355
1560
|
static _supersedeOverride = null;
|
|
1561
|
+
/** The flush's pre-verdict step (#3427): once the transaction's action
|
|
1562
|
+
* bodies have all ended and nothing authoritative is left in flight, the
|
|
1563
|
+
* engine supersedes every override still in force with the truth it
|
|
1564
|
+
* reverts to, so the graph re-derives from it now, as the transaction's
|
|
1565
|
+
* held work, instead of after the flights the overrides fed have landed.
|
|
1566
|
+
* True when it superseded something: the caller re-runs the heap ahead of
|
|
1567
|
+
* the verdict. The engine owns every gate (acted, actions drained, has
|
|
1568
|
+
* overrides, no store edits, no authoritative flight); null without it. */
|
|
1569
|
+
static _endOptimism = null;
|
|
1356
1570
|
/** read()'s value for a TRACKED reader of a superseded node (#3331): the
|
|
1357
1571
|
* staged truth, unless the reader is a stale (render) reader of another
|
|
1358
1572
|
* transaction — then the displayed override, as it keeps a foreign
|
|
1359
1573
|
* transaction's committed value over its staged write. */
|
|
1360
|
-
|
|
1574
|
+
/** A tracked read of an active override: the lane outside-view rule
|
|
1575
|
+
* (#3460) and the A18 supersession selection (#3331) — see optimistic.ts. */
|
|
1576
|
+
static _overrideRead = null;
|
|
1577
|
+
/** A lane pass's publish for a memo (#3479, lanes stage): the speculative
|
|
1578
|
+
* result becomes a DERIVED override, `_value` stays committed — see
|
|
1579
|
+
* optimistic.ts laneOverride. Set with the engine, which a lane implies. */
|
|
1580
|
+
static _laneOverride = null;
|
|
1581
|
+
/** Verdict-layer recompute in progress (companion creation, latest()/
|
|
1582
|
+
* isPending() pulls): never born held — see core.ts enterStagedRead. */
|
|
1583
|
+
static _verdictPull = false;
|
|
1361
1584
|
/** setSignal's authoritative (projection-write) landing on an override-
|
|
1362
1585
|
* covered node (#3331 store twin): stage the truth for its transaction's
|
|
1363
1586
|
* commit whatever its relation to the committed value — a landing equal to
|
|
@@ -1369,7 +1592,12 @@ class GlobalQueue extends Queue {
|
|
|
1369
1592
|
flush() {
|
|
1370
1593
|
if (this._running) return;
|
|
1371
1594
|
this._running = true;
|
|
1595
|
+
resyncUnflushedCompanions(); // A28, see above
|
|
1372
1596
|
try {
|
|
1597
|
+
// The tick proposed against a hold (#3494): adopt its batch into it.
|
|
1598
|
+
// Inside the try: the adoption runs user comparators (the no-proposal
|
|
1599
|
+
// drop), and a throw there must not leave `_running` set.
|
|
1600
|
+
while (batchJoins.length) this.initTransition(batchJoins.pop());
|
|
1373
1601
|
if (true) devCheckFlushStart();
|
|
1374
1602
|
// Before runHeap for the same reason as the fast drain above; late
|
|
1375
1603
|
// subscribers (an effect reading a swept memo this flush) revive it,
|
|
@@ -1377,9 +1605,18 @@ class GlobalQueue extends Queue {
|
|
|
1377
1605
|
sweepDormant();
|
|
1378
1606
|
runHeap(dirtyQueue, GlobalQueue._update);
|
|
1379
1607
|
if (activeTransition) {
|
|
1608
|
+
// The action bodies are over: the overrides they leave in force
|
|
1609
|
+
// revert at this settle, and the correction is this transaction's
|
|
1610
|
+
// held work — re-derived here, under it, ahead of the verdict — not a
|
|
1611
|
+
// waterfall after the flights the overrides fed (#3427). After the
|
|
1612
|
+
// heap, not before: a synchronous body's own writes (a refresh that
|
|
1613
|
+
// puts an override node's source in flight) are judged applied.
|
|
1614
|
+
if (GlobalQueue._endOptimism?.(activeTransition)) runHeap(dirtyQueue, GlobalQueue._update);
|
|
1380
1615
|
const isComplete = transitionComplete(activeTransition);
|
|
1381
1616
|
if (!isComplete) {
|
|
1382
1617
|
const stashedTransition = activeTransition;
|
|
1618
|
+
// Parked: the unchanged passes' inputs are held; their tails stay (A30).
|
|
1619
|
+
heldTrims.length = 0;
|
|
1383
1620
|
// When the parking batch IS the transition, all of its writes commit
|
|
1384
1621
|
// only with it — every zombie recompute they queued would run against
|
|
1385
1622
|
// a world the zombie never displays (zombies render mainline until
|
|
@@ -1481,6 +1718,17 @@ class GlobalQueue extends Queue {
|
|
|
1481
1718
|
}
|
|
1482
1719
|
if (true) DEV.hooks.onUpdate?.();
|
|
1483
1720
|
} finally {
|
|
1721
|
+
// Re-enter a woken transaction (see wokenTransitions) only from an
|
|
1722
|
+
// idle pass: entering adopts the ambient batch, and staged or dirty
|
|
1723
|
+
// ambient work would be held behind flights it never read. `scheduled`
|
|
1724
|
+
// is that test here — after the park exit as well as the normal one:
|
|
1725
|
+
// it was recomputed from the heap this pass, every write since re-armed
|
|
1726
|
+
// it, and optimistic ambient nodes reverted with the finalize — so a
|
|
1727
|
+
// wake in a pass with work simply falls to the next. Entering re-arms
|
|
1728
|
+
// it itself; a dead (completed) wake is a bare return in
|
|
1729
|
+
// initTransition, and the loop moves on to the next.
|
|
1730
|
+
while (!scheduled && !activeTransition && wokenTransitions.length)
|
|
1731
|
+
this.initTransition(wokenTransitions.pop());
|
|
1484
1732
|
this._running = false;
|
|
1485
1733
|
}
|
|
1486
1734
|
}
|
|
@@ -1559,9 +1807,45 @@ class GlobalQueue extends Queue {
|
|
|
1559
1807
|
// must not entangle unrelated writes to it; the same rule holds one hop
|
|
1560
1808
|
// downstream: propagation never queues pended subscribers as pending
|
|
1561
1809
|
// nodes, see propagateAffectsMark, #2893.
|
|
1810
|
+
// Adopted outside a flush: the staging is still unflushed — the stamp
|
|
1811
|
+
// must not make it read as held-and-carried (CONFIG_ADOPTED_UNFLUSHED;
|
|
1812
|
+
// the carrying flush clears it in reassignPendingTransition).
|
|
1813
|
+
const adopted = this._running ? 0 : CONFIG_ADOPTED_UNFLUSHED;
|
|
1562
1814
|
for (let i = 0; i < batch._pendingNodes.length; i++) {
|
|
1563
1815
|
const node = batch._pendingNodes[i];
|
|
1816
|
+
// A tick that nets to the committed value proposed nothing (A34, #3494):
|
|
1817
|
+
// `setShow(false); setShow(true)` beside a write that opens a hold
|
|
1818
|
+
// left `show` staged at its own value, stamped, pending to the
|
|
1819
|
+
// verdict, and its next mainline write held by a flight it never
|
|
1820
|
+
// derived from. Unstage it here — its subscribers were walked at the
|
|
1821
|
+
// write and re-derive the same value. Writes only: a signal's staging
|
|
1822
|
+
// is always one, a computed's only under REACTIVE_MANUAL_WRITE
|
|
1823
|
+
// (`createSignal(fn)`'s setter, #3519 review) — otherwise it is its
|
|
1824
|
+
// pass's result, which may equal an uninitialized `undefined` (a
|
|
1825
|
+
// born-held first pass). `_equals: false` opts out. The unstaging is
|
|
1826
|
+
// the commit's own path (commitPendingNode with nothing staged): the
|
|
1827
|
+
// manual-write flag, companions and the rest are cleaned up as a
|
|
1828
|
+
// commit would, and the node is stamped nowhere. Unstamped
|
|
1829
|
+
// only: a node already a transaction's — arriving here as a parked
|
|
1830
|
+
// batch folds into a merge — carries a FLUSHED proposal a later
|
|
1831
|
+
// rewrite brought back to the committed value; it is held, not
|
|
1832
|
+
// proposal-free. Dropped, it kept the dead stamp, and the next write
|
|
1833
|
+
// to it queued under the merged transaction a value the commit then
|
|
1834
|
+
// skipped as another's (fuzzer latest-2 #1470, S3).
|
|
1835
|
+
if (
|
|
1836
|
+
node._transition === null &&
|
|
1837
|
+
node._pendingValue !== NOT_PENDING &&
|
|
1838
|
+
(!node._fn ||
|
|
1839
|
+
(node._flags & REACTIVE_MANUAL_WRITE && !(node._statusFlags & STATUS_UNINITIALIZED))) &&
|
|
1840
|
+
node._equals &&
|
|
1841
|
+
node._equals(node._value, node._pendingValue)
|
|
1842
|
+
) {
|
|
1843
|
+
node._pendingValue = NOT_PENDING;
|
|
1844
|
+
commitPendingNode(node);
|
|
1845
|
+
continue;
|
|
1846
|
+
}
|
|
1564
1847
|
node._transition = activeTransition;
|
|
1848
|
+
node._config |= adopted;
|
|
1565
1849
|
activeTransition._pendingNodes.push(node);
|
|
1566
1850
|
}
|
|
1567
1851
|
for (let i = 0; i < batch._optimisticNodes.length; i++) {
|
|
@@ -1596,6 +1880,7 @@ class GlobalQueue extends Queue {
|
|
|
1596
1880
|
function queuePendingNode(node) {
|
|
1597
1881
|
lastStagedNodeName = node._name ?? null;
|
|
1598
1882
|
currentBatch._pendingNodes.push(node);
|
|
1883
|
+
if (!globalQueue._running) markUnflushedStaged(); // A28
|
|
1599
1884
|
}
|
|
1600
1885
|
// Dev-only attribution for the flush loop guard (#3140): when the guard
|
|
1601
1886
|
// trips, naming what the loop kept chewing on lets the app author attribute
|
|
@@ -1698,6 +1983,8 @@ function commitPendingNode(n) {
|
|
|
1698
1983
|
if (n._pendingValue !== NOT_PENDING) {
|
|
1699
1984
|
n._value = n._pendingValue;
|
|
1700
1985
|
n._pendingValue = NOT_PENDING;
|
|
1986
|
+
// A node born held (recompute) initializes at this commit.
|
|
1987
|
+
c._statusFlags &= ~STATUS_UNINITIALIZED;
|
|
1701
1988
|
// Set _modified for effects, but not for tracked effects (they handle their own scheduling)
|
|
1702
1989
|
if (n._type && n._type !== EFFECT_TRACKED) n._modified = true;
|
|
1703
1990
|
// A quiet re-ask classification preserved through a held landing dies
|
|
@@ -1712,6 +1999,14 @@ function commitPendingNode(n) {
|
|
|
1712
1999
|
// store to an always-present computed slot.
|
|
1713
2000
|
c._loading = false;
|
|
1714
2001
|
c._flags &= ~REACTIVE_MANUAL_WRITE;
|
|
2002
|
+
// The children this commit publishes are the frame's now (#3404) — and so
|
|
2003
|
+
// are the dependencies of the pass that produced the value: the previous
|
|
2004
|
+
// frame's tail goes (A30, #3410; `recompute` left it for a staged pass). Only
|
|
2005
|
+
// after a clean pass: `_error` is cleared by a clean pass or by the node's
|
|
2006
|
+
// own landing (whose pass was clean), so a set `_error` means the last pass
|
|
2007
|
+
// threw, kept its full list, and `_depsTail` marks where it stopped.
|
|
2008
|
+
if (c._x?._error == null) trimStaleDeps(c);
|
|
2009
|
+
c._config &= ~CONFIG_HELD_CHILDREN;
|
|
1715
2010
|
if (!(c._statusFlags & STATUS_PENDING)) c._statusFlags &= ~STATUS_UNINITIALIZED;
|
|
1716
2011
|
// A flight this commit leaves in the air (unobserved, or observed only by
|
|
1717
2012
|
// a boundary) now has PUBLISHED inputs: its committed value is stale
|
|
@@ -1737,7 +2032,17 @@ function setStoreCommitHook(fn) {
|
|
|
1737
2032
|
* confirming truth is committed and the override still displays, a torn
|
|
1738
2033
|
* frame no timeline contains. */
|
|
1739
2034
|
const heldRevealed = [];
|
|
2035
|
+
/** Unchanged passes with a stale dependency tail, waiting on this flush's
|
|
2036
|
+
* verdict (A30, #3469). A pass that changed nothing replaced nothing either —
|
|
2037
|
+
* and cannot know at its own tail whether the flush that ran it will park:
|
|
2038
|
+
* parked, its inputs are held and the committed frame still derives from the
|
|
2039
|
+
* tail (`b() ? b() : a()` computed `1` from the held `b`, equal to the `1` it
|
|
2040
|
+
* had from `a` — with `a` trimmed, the mainline `a = 2` never reached it).
|
|
2041
|
+
* Trimmed when the flush commits; dropped with a park, the tail stays linked
|
|
2042
|
+
* until a committing pass trims it (one spurious recompute at most). */
|
|
2043
|
+
const heldTrims = [];
|
|
1740
2044
|
function commitPendingNodes() {
|
|
2045
|
+
while (heldTrims.length) trimStaleDeps(heldTrims.pop());
|
|
1741
2046
|
const pendingNodes = currentBatch._pendingNodes;
|
|
1742
2047
|
for (let i = 0; i < pendingNodes.length; i++) {
|
|
1743
2048
|
const node = pendingNodes[i];
|
|
@@ -1884,6 +2189,7 @@ function shiftAffectsMarks(delta) {
|
|
|
1884
2189
|
function reassignPendingTransition(pendingNodes) {
|
|
1885
2190
|
for (let i = 0; i < pendingNodes.length; i++) {
|
|
1886
2191
|
pendingNodes[i]._transition = activeTransition;
|
|
2192
|
+
pendingNodes[i]._config &= ~CONFIG_ADOPTED_UNFLUSHED; // this flush carried it
|
|
1887
2193
|
}
|
|
1888
2194
|
}
|
|
1889
2195
|
const globalQueue = new GlobalQueue();
|
|
@@ -1985,13 +2291,65 @@ function flush(fn) {
|
|
|
1985
2291
|
function runQueue(queue, type) {
|
|
1986
2292
|
for (let i = 0; i < queue.length; i++) queue[i](type);
|
|
1987
2293
|
}
|
|
1988
|
-
function reporterBlocksSource(reporter, source) {
|
|
1989
|
-
|
|
2294
|
+
function reporterBlocksSource(reporter, source, verdict) {
|
|
2295
|
+
const flags = reporter._flags;
|
|
2296
|
+
if (flags & REACTIVE_DISPOSED) return false;
|
|
2297
|
+
// A zombie renders until the commit that disposes it (#3463): while its
|
|
2298
|
+
// removal is staged in a live transaction it is still on screen, and what
|
|
2299
|
+
// it displays must stay consistent with the frame — a held `Show`'s
|
|
2300
|
+
// `Details: 0` beside the lane's `Value: 1` otherwise. Its say is moot for
|
|
2301
|
+
// the verdict of the transaction that stages the removal (`verdict`): done,
|
|
2302
|
+
// and the commit disposes it; not done, and it stays parked regardless. A
|
|
2303
|
+
// zombie whose removal commits this flush (owner's pass not held) is dead.
|
|
2304
|
+
// The owner is stamped when the flush parks; held in this flush, its
|
|
2305
|
+
// staging transaction is the active one.
|
|
2306
|
+
if (flags & REACTIVE_ZOMBIE) {
|
|
2307
|
+
let p = reporter;
|
|
2308
|
+
while (p && p._flags & REACTIVE_ZOMBIE) p = p._parent;
|
|
2309
|
+
let t = p && (p._transition || (p._config & CONFIG_HELD_CHILDREN ? activeTransition : null));
|
|
2310
|
+
if (!t || (t = currentTransition(t))._done === true || t === verdict) return false;
|
|
2311
|
+
}
|
|
2312
|
+
// Fallback-caught async holds nothing. A collecting loading boundary
|
|
2313
|
+
// consumes the notification, so a reader under a fallback never registers —
|
|
2314
|
+
// but a reader registered while its boundary showed content stays
|
|
2315
|
+
// registered when the boundary's `on` later changes and it flips to the
|
|
2316
|
+
// fallback. The reader is behind the fallback now; if nothing outside the
|
|
2317
|
+
// boundary consumes the flight, the hold is over (A33, ruled 2026-09-12, #3375).
|
|
2318
|
+
for (let q = reporter._queue; q; q = q._parent)
|
|
2319
|
+
if (q._collectionType & STATUS_PENDING && !q._initialized) return false;
|
|
2320
|
+
// "Still derives from the source" is a question about THIS pass's reads:
|
|
2321
|
+
// the deps up to `_depsTail`. Past it lie the committed frame's — kept
|
|
2322
|
+
// linked by A30 until the commit trims them (a staged pass, an errored
|
|
2323
|
+
// one). Reading them here made a reporter whose pass had stopped reading
|
|
2324
|
+
// the source (a gate closed in the same flush as the write) look live, and
|
|
2325
|
+
// the hold it kept was the commit that would have trimmed the dep that
|
|
2326
|
+
// kept it (spec O3, same-flush form; fuzzer #3446 P1 cases 21/79). A
|
|
2327
|
+
// trimmed list ends at `_depsTail`, so the bound is free there; a pass
|
|
2328
|
+
// that read nothing has a null tail and derives from nothing.
|
|
2329
|
+
// The registration is trusted: a pending mark rides only this pass's links
|
|
2330
|
+
// (notifyStatus skips the kept tail), so a registered reporter read the
|
|
2331
|
+
// source, or threw on it.
|
|
1990
2332
|
if (reporter._x?._pendingSources?.has(source)) return true;
|
|
1991
|
-
|
|
2333
|
+
const tail = reporter._depsTail;
|
|
2334
|
+
for (
|
|
2335
|
+
let dep = tail === null ? null : reporter._deps;
|
|
2336
|
+
dep;
|
|
2337
|
+
dep = dep === tail ? null : dep._nextDep
|
|
2338
|
+
) {
|
|
1992
2339
|
let current = dep._dep;
|
|
1993
2340
|
while (current) {
|
|
1994
|
-
|
|
2341
|
+
// Or through a memo pending on the flight (#3494): a stale reader
|
|
2342
|
+
// served a held memo's committed value never turned pending itself, so
|
|
2343
|
+
// its only trace of the flight is the memo between them — `copy` of
|
|
2344
|
+
// `details`. Judged dead, its transaction released `count=1` beside
|
|
2345
|
+
// the `Copy: 0` it displays. `_pendingSources` is transitive, so one
|
|
2346
|
+
// hop covers any depth.
|
|
2347
|
+
if (
|
|
2348
|
+
current === source ||
|
|
2349
|
+
current._firewall === source ||
|
|
2350
|
+
current._x?._pendingSources?.has(source)
|
|
2351
|
+
)
|
|
2352
|
+
return true;
|
|
1995
2353
|
current = current._x?._parentSource;
|
|
1996
2354
|
}
|
|
1997
2355
|
}
|
|
@@ -2001,6 +2359,30 @@ function reporterBlocksSource(reporter, source) {
|
|
|
2001
2359
|
reporter._x?._error.source === source
|
|
2002
2360
|
);
|
|
2003
2361
|
}
|
|
2362
|
+
/**
|
|
2363
|
+
* Does a live reporter of `transition` still observe `source` pending? Dead
|
|
2364
|
+
* reporters (disposed, behind a fallback, no longer reading the source) are
|
|
2365
|
+
* pruned as they are found, and the source's entry with them. Shared by the
|
|
2366
|
+
* settle verdict and the lane's hold check (`waitingTransition`): a live
|
|
2367
|
+
* action parks its transaction without a verdict, so this prune is the only
|
|
2368
|
+
* one an optimistic lane whose last async reader unmounted mid-action ever
|
|
2369
|
+
* gets — without it the lane held on the dead reporter's registration until
|
|
2370
|
+
* the flight it no longer observed landed (#3426).
|
|
2371
|
+
*/
|
|
2372
|
+
function sourceObserved(transition, source, verdict) {
|
|
2373
|
+
const reporters = transition._asyncReporters.get(source);
|
|
2374
|
+
let kept = false;
|
|
2375
|
+
for (const reporter of reporters ?? []) {
|
|
2376
|
+
if (reporterBlocksSource(reporter, source, verdict)) return true;
|
|
2377
|
+
// A zombie the verdict passes over is kept, not pruned (#3463): moot for
|
|
2378
|
+
// this verdict, it still holds a lane's reveal while the transaction
|
|
2379
|
+
// stays parked on something else.
|
|
2380
|
+
if (verdict && reporter._flags & REACTIVE_ZOMBIE) kept = true;
|
|
2381
|
+
else reporters.delete(reporter);
|
|
2382
|
+
}
|
|
2383
|
+
if (!kept) transition._asyncReporters.delete(source);
|
|
2384
|
+
return false;
|
|
2385
|
+
}
|
|
2004
2386
|
function transitionComplete(transition) {
|
|
2005
2387
|
if (transition._done) return true;
|
|
2006
2388
|
if (transition._actions.length) {
|
|
@@ -2009,17 +2391,22 @@ function transitionComplete(transition) {
|
|
|
2009
2391
|
return false;
|
|
2010
2392
|
}
|
|
2011
2393
|
let done = true;
|
|
2012
|
-
for (const
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2394
|
+
for (const source of transition._asyncReporters.keys()) {
|
|
2395
|
+
// The source blocks while its OWN flight is up — the self entry in its
|
|
2396
|
+
// pending sources (added by notifyStatus's source path, with status;
|
|
2397
|
+
// retired by the landing and the supersede sweep, so it implies
|
|
2398
|
+
// STATUS_PENDING). `_error.source` is not that test: propagation from an
|
|
2399
|
+
// input that went pending later overwrites it with the input (#3375 — a
|
|
2400
|
+
// boundary-consumed load re-asked under a held derivation), and the
|
|
2401
|
+
// still-flying source read as settled, committing the writes it was
|
|
2402
|
+
// asked with ahead of its answer.
|
|
2403
|
+
// Not the self entry alone: a source whose own flight an upstream re-ask
|
|
2404
|
+
// superseded is still pending — on that re-ask (#3462). Its reader cannot
|
|
2405
|
+
// render until the chain lands, and the landing folds this transaction in
|
|
2406
|
+
// (enterWaiting). Judged complete instead, a re-entry between the two (a
|
|
2407
|
+
// repeated write to a held signal) committed the held writes beside the
|
|
2408
|
+
// reader's stale frame.
|
|
2409
|
+
if (sourceObserved(transition, source, transition) && source._x?._pendingSources?.size) {
|
|
2023
2410
|
done = false;
|
|
2024
2411
|
break;
|
|
2025
2412
|
}
|
|
@@ -2056,12 +2443,24 @@ function currentTransition(transition) {
|
|
|
2056
2443
|
* the node, so a hold check must not assume it was recorded in the transaction
|
|
2057
2444
|
* it happens to hold — lanes merge across transactions (#2912), and a merged
|
|
2058
2445
|
* root's transaction knows nothing of the async its members' transactions
|
|
2059
|
-
* observed (#3335). Null when nobody is waiting
|
|
2446
|
+
* observed (#3335). Null when nobody is waiting — a registration whose every
|
|
2447
|
+
* reporter has since died is nobody (#3426).
|
|
2060
2448
|
*/
|
|
2061
2449
|
function waitingTransition(source) {
|
|
2062
|
-
for (const t of transitions) if (t
|
|
2450
|
+
for (const t of transitions) if (sourceObserved(t, source)) return t;
|
|
2063
2451
|
return null;
|
|
2064
2452
|
}
|
|
2453
|
+
/** A landing enters EVERY parked transaction still waiting on `source`, folding
|
|
2454
|
+
* them into the active one (A15: each reveal that discovered the flight
|
|
2455
|
+
* completes at its landing). The fold used to happen as the waiters' stamped
|
|
2456
|
+
* readers recomputed under the landing — recompute re-entering an effect's
|
|
2457
|
+
* stamp — which also folded in writes those readers merely shared a hole
|
|
2458
|
+
* with (#3407); effects no longer re-enter, so the landing folds explicitly.
|
|
2459
|
+
* Live iteration is safe: a merge deletes the outgoing (active) entry and
|
|
2460
|
+
* re-adds the visited one. */
|
|
2461
|
+
function enterWaiting(source) {
|
|
2462
|
+
for (const t of transitions) if (sourceObserved(t, source)) globalQueue.initTransition(t);
|
|
2463
|
+
}
|
|
2065
2464
|
function runInTransition(transition, fn) {
|
|
2066
2465
|
const prevTransition = activeTransition;
|
|
2067
2466
|
try {
|
|
@@ -2279,6 +2678,19 @@ function disposeChildren(node, self = false, zombie) {
|
|
|
2279
2678
|
// false, and notifies subscribers still watching the companion.
|
|
2280
2679
|
const n = node;
|
|
2281
2680
|
if (n._x?._pendingSignal || n._x?._latestValueComputed) GlobalQueue._snapCompanions(n);
|
|
2681
|
+
// A firewall's leaves have no lifecycle of their own, so a companion on
|
|
2682
|
+
// one of them outlives its source the same way (INV-9's rationale). The
|
|
2683
|
+
// firewall knows which leaves carry companions (CONFIG_CHILD_COMPANIONS,
|
|
2684
|
+
// #3038): snap them with it — the snap retires a shadow whose firewall is
|
|
2685
|
+
// disposed (spec O5).
|
|
2686
|
+
if (n._config & CONFIG_CHILD_COMPANIONS)
|
|
2687
|
+
n._x._companionChildren.forEach(GlobalQueue._snapCompanions);
|
|
2688
|
+
// A pending reader parked in a transaction may be the only thing holding
|
|
2689
|
+
// it (#3372): its death is a completion event the transaction must be
|
|
2690
|
+
// re-judged for, and nothing else re-enters a parked transaction.
|
|
2691
|
+
const t = n._transition;
|
|
2692
|
+
if (t && n._statusFlags & STATUS_PENDING && !wokenTransitions.includes(t))
|
|
2693
|
+
(wokenTransitions.push(t), schedule());
|
|
2282
2694
|
}
|
|
2283
2695
|
if (self && true) clearSignals(node);
|
|
2284
2696
|
if (self && node._fn && node._x !== null) node._x._inFlight = null;
|
|
@@ -3002,6 +3414,13 @@ function handleAsync(el, result, setter) {
|
|
|
3002
3414
|
// fired _flightTeardown. A future non-recompute registration path must
|
|
3003
3415
|
// release it here before overwriting _inFlight.
|
|
3004
3416
|
ext(el)._inFlight = result;
|
|
3417
|
+
// The run that asked this flight read every input without throwing: an
|
|
3418
|
+
// input still in flight was masked for it (an active override, A17), so
|
|
3419
|
+
// pending state those inputs propagated onto the node earlier does not
|
|
3420
|
+
// describe this answer. Drop it — the flight is the node's pending now.
|
|
3421
|
+
// The landing retires only the flight's own entry (landStatus, #3373), so
|
|
3422
|
+
// an entry that survived here would hold the node past its own answer.
|
|
3423
|
+
el._x._pendingSources = undefined;
|
|
3005
3424
|
// Provenance of the question this flight asks (#3331): the action whose
|
|
3006
3425
|
// window is registering it, or the flight whose landing is. Its landings
|
|
3007
3426
|
// propagate under it (asyncWrite) so an override downstream can tell a
|
|
@@ -3032,7 +3451,12 @@ function handleAsync(el, result, setter) {
|
|
|
3032
3451
|
// waiting on this flight into it at the landing — a reveal that
|
|
3033
3452
|
// discovered the flight (#3305) would then wait on the owner's action
|
|
3034
3453
|
// instead of on the flight (#3334). Enter the waiter: the transaction
|
|
3035
|
-
// whose blocker this landing clears.
|
|
3454
|
+
// whose blocker this landing clears. Then every transaction waiting on
|
|
3455
|
+
// the flight folds in (enterWaiting): a reveal that discovered it
|
|
3456
|
+
// completes at its landing (A15) — a stampless node's fresh batch
|
|
3457
|
+
// included (its flight started under a batch that committed beneath it,
|
|
3458
|
+
// #3305). The fold used to happen as each stamped reader recomputed,
|
|
3459
|
+
// which effects no longer do (#3407).
|
|
3036
3460
|
if (el._x?._optimisticLane) transition = waitingTransition(el) ?? transition;
|
|
3037
3461
|
if (
|
|
3038
3462
|
transition &&
|
|
@@ -3045,6 +3469,7 @@ function handleAsync(el, result, setter) {
|
|
|
3045
3469
|
return;
|
|
3046
3470
|
}
|
|
3047
3471
|
globalQueue.initTransition(transition);
|
|
3472
|
+
enterWaiting(el);
|
|
3048
3473
|
};
|
|
3049
3474
|
const handleError = error => {
|
|
3050
3475
|
if (el._x?._inFlight !== result) return;
|
|
@@ -3107,8 +3532,7 @@ function handleAsync(el, result, setter) {
|
|
|
3107
3532
|
// old value as pending, a one-frame pulse to direct observers (#3178).
|
|
3108
3533
|
// A truthy capture implies `_x` exists, so the restore writes it directly.
|
|
3109
3534
|
const wasReask = el._x?._reask;
|
|
3110
|
-
|
|
3111
|
-
clearStatus(el);
|
|
3535
|
+
landStatus(el);
|
|
3112
3536
|
if (wasReask) el._x._reask = true;
|
|
3113
3537
|
const lane = resolveLane(el);
|
|
3114
3538
|
if (lane) lane._pendingAsync.delete(el);
|
|
@@ -3123,8 +3547,15 @@ function handleAsync(el, result, setter) {
|
|
|
3123
3547
|
handleError(error);
|
|
3124
3548
|
return;
|
|
3125
3549
|
}
|
|
3126
|
-
if (wasUninitialized)
|
|
3127
|
-
} else if (
|
|
3550
|
+
if (wasUninitialized) landStatus(el, true);
|
|
3551
|
+
} else if (
|
|
3552
|
+
el._x?._overrideValue !== undefined &&
|
|
3553
|
+
!(lane && el._config & CONFIG_DERIVED_OVERRIDE)
|
|
3554
|
+
) {
|
|
3555
|
+
// A derived override's landing UNDER its lane is the lane's own work
|
|
3556
|
+
// (the branch below); demoted — its source superseded (A18) — the
|
|
3557
|
+
// landing is the truth the correction asked for, and holds and
|
|
3558
|
+
// supersedes here like the sync twin (recompute). Otherwise:
|
|
3128
3559
|
// Optimistic node — resting OR covered by an active override — holds
|
|
3129
3560
|
// through the shared pending-node path, exactly like a plain async memo,
|
|
3130
3561
|
// so the commit clears STATUS_UNINITIALIZED (#2806) and elevation to
|
|
@@ -3162,11 +3593,15 @@ function handleAsync(el, result, setter) {
|
|
|
3162
3593
|
} else if (lane) {
|
|
3163
3594
|
// Route through lane's effect queue for independent flushing
|
|
3164
3595
|
const isEffect = el._type;
|
|
3165
|
-
const prevValue = el._value;
|
|
3596
|
+
const prevValue = hasActiveOverride(el) ? unwrapOverride(el._x._overrideValue) : el._value;
|
|
3166
3597
|
const equals = el._equals;
|
|
3167
3598
|
try {
|
|
3168
3599
|
if ((!isEffect && wasUninitialized) || !equals || !equals(value, prevValue)) {
|
|
3169
|
-
|
|
3600
|
+
// Lanes stage (#3479): a memo's landing under its lane is a derived
|
|
3601
|
+
// override, as its sync pass's result is (recompute) — `_value`
|
|
3602
|
+
// stays the committed truth for readers off the lane.
|
|
3603
|
+
if (isEffect) el._value = value;
|
|
3604
|
+
else GlobalQueue._laneOverride(el, value, lane);
|
|
3170
3605
|
el._time = clock;
|
|
3171
3606
|
// The latest() shadow write gives latest() effects independent lanes; the
|
|
3172
3607
|
// _pendingSignal update is a no-op repeat of the clearStatus() call above
|
|
@@ -3212,6 +3647,15 @@ function handleAsync(el, result, setter) {
|
|
|
3212
3647
|
if (el._pendingValue === NOT_PENDING) {
|
|
3213
3648
|
el._loading = false;
|
|
3214
3649
|
if (wasReask) el._x._reask = false;
|
|
3650
|
+
// The landing published: the dependency tail the flight's pass left
|
|
3651
|
+
// linked goes now (A30, #3410). A transition-held landing has not
|
|
3652
|
+
// replaced the committed frame — the committed value still derives
|
|
3653
|
+
// from the previous pass's inputs, and a mainline write to one of them
|
|
3654
|
+
// must reach this node and join its hold (its stamp) instead of
|
|
3655
|
+
// publishing beside the stale derivation (#3461: `b() ? b() : a()`
|
|
3656
|
+
// held on `b` dropped `a` at its landing, and `A: 1` then committed
|
|
3657
|
+
// beside `Selected: 0`). `commitPendingNode` trims a held landing.
|
|
3658
|
+
trimStaleDeps(el);
|
|
3215
3659
|
}
|
|
3216
3660
|
settlePendingSource(el);
|
|
3217
3661
|
schedule();
|
|
@@ -3466,6 +3910,41 @@ function clearStatus(el, clearUninitialized = false) {
|
|
|
3466
3910
|
const notify = statusNotifierOf(el);
|
|
3467
3911
|
if (notify) notify.call(el);
|
|
3468
3912
|
}
|
|
3913
|
+
/**
|
|
3914
|
+
* Status clear for a flight LANDING (asyncWrite). A landing answers the
|
|
3915
|
+
* node's OWN question — it retires the node's self entry, not the pending
|
|
3916
|
+
* state its sources propagated onto it. An input re-asked while this flight
|
|
3917
|
+
* was up (a second write to the signal feeding `a` while `b`'s first flight
|
|
3918
|
+
* is in the air, #3373) marks `b` pending on `a` by propagation, with `b`'s
|
|
3919
|
+
* flight still current: nothing superseded it (the re-ask only changed `a`'s
|
|
3920
|
+
* status, not yet its value), so the landing arrives, and a full clear made
|
|
3921
|
+
* `b` answer with the stale value — the transaction's reporter for `a` found
|
|
3922
|
+
* nothing pending below it and committed the newer signal beside the older
|
|
3923
|
+
* derived value (`2 / 1`); `isPending(b)` read false for the gap (#3376).
|
|
3924
|
+
* With another source still pending the node stays derivatively pending on
|
|
3925
|
+
* it; the landed value is written below (the staged answer is still the
|
|
3926
|
+
* answer for the inputs it was asked with) and the input's own settle
|
|
3927
|
+
* releases it, or its value change recomputes the node into a fresh flight.
|
|
3928
|
+
* `_blocked` clears like a full clear: a landing that passed the `_inFlight`
|
|
3929
|
+
* guard was not superseded by a re-run (recompute nulls `_inFlight` first),
|
|
3930
|
+
* so the flag is the flight's own registration throw — the input settling
|
|
3931
|
+
* unchanged must not re-run the node (an extra flight for the same inputs).
|
|
3932
|
+
* The node is already STATUS_PENDING in that branch (only notifyStatus fills
|
|
3933
|
+
* the set, with status; a loading-window park cannot coexist with a live
|
|
3934
|
+
* flight since registration drops the set), so the flags only change when
|
|
3935
|
+
* the first landing retires UNINITIALIZED. `_error` must move off self: a
|
|
3936
|
+
* reader thrown NotReady(self) would park on a retired entry. Companions
|
|
3937
|
+
* keep their verdict (pending before and after; the write re-syncs them).
|
|
3938
|
+
*/
|
|
3939
|
+
function landStatus(el, clearUninitialized = false) {
|
|
3940
|
+
const sources = el._x?._pendingSources;
|
|
3941
|
+
// (The full clear below drops the set whether or not self was retired first.)
|
|
3942
|
+
if (sources && (sources.delete(el), sources.size)) {
|
|
3943
|
+
el._x._blocked = false;
|
|
3944
|
+
if (clearUninitialized) el._statusFlags = STATUS_PENDING;
|
|
3945
|
+
setPendingError(el, sources.values().next().value);
|
|
3946
|
+
} else clearStatus(el, clearUninitialized);
|
|
3947
|
+
}
|
|
3469
3948
|
function notifyStatus(el, status, error, blockStatus, lane) {
|
|
3470
3949
|
// Wrap regular errors to track source node
|
|
3471
3950
|
if (
|
|
@@ -3477,10 +3956,24 @@ function notifyStatus(el, status, error, blockStatus, lane) {
|
|
|
3477
3956
|
const pendingSource =
|
|
3478
3957
|
status === STATUS_PENDING && error instanceof NotReadyError ? error.source : undefined;
|
|
3479
3958
|
const isSource = pendingSource === el;
|
|
3959
|
+
// An optimistic node (a WRITTEN override slot) pending derivatively is a
|
|
3960
|
+
// boundary: its override is the answer, pending stops here (A17). A
|
|
3961
|
+
// derived override (#3479) is a previous speculative answer on a plain
|
|
3962
|
+
// member — pending flows through it as through any memo.
|
|
3480
3963
|
const isOptimisticBoundary =
|
|
3481
|
-
status === STATUS_PENDING &&
|
|
3964
|
+
status === STATUS_PENDING &&
|
|
3965
|
+
el._x?._overrideValue !== undefined &&
|
|
3966
|
+
!(el._config & CONFIG_DERIVED_OVERRIDE) &&
|
|
3967
|
+
!isSource;
|
|
3482
3968
|
const startsBlocking = isOptimisticBoundary && hasActiveOverride(el);
|
|
3483
3969
|
if (!blockStatus) {
|
|
3970
|
+
// Lane before companions: the companion pokes below may create the
|
|
3971
|
+
// node's pending-signal lane, whose parent is read from the node's lane
|
|
3972
|
+
// at creation. Assigned after them (as it was), a node made pending by
|
|
3973
|
+
// propagation before it rode the lane got a parentless companion lane,
|
|
3974
|
+
// and the isPending reader that also depends on the node merged it into
|
|
3975
|
+
// the held lane — the verdict then waited on the async it reports (#3379).
|
|
3976
|
+
if (lane) assignOrMergeLane(el, lane);
|
|
3484
3977
|
if (status === STATUS_PENDING && pendingSource) {
|
|
3485
3978
|
addPendingSource(el, pendingSource);
|
|
3486
3979
|
// A fresh flight from a settled state starts with its inputs unpublished
|
|
@@ -3505,9 +3998,6 @@ function notifyStatus(el, status, error, blockStatus, lane) {
|
|
|
3505
3998
|
)
|
|
3506
3999
|
GlobalQueue._updateChildCompanions(el);
|
|
3507
4000
|
}
|
|
3508
|
-
if (lane && !blockStatus) {
|
|
3509
|
-
assignOrMergeLane(el, lane);
|
|
3510
|
-
}
|
|
3511
4001
|
const downstreamBlockStatus = blockStatus || startsBlocking;
|
|
3512
4002
|
const downstreamLane = blockStatus || isOptimisticBoundary ? undefined : lane;
|
|
3513
4003
|
const elNotify = statusNotifierOf(el);
|
|
@@ -3524,6 +4014,30 @@ function notifyStatus(el, status, error, blockStatus, lane) {
|
|
|
3524
4014
|
}
|
|
3525
4015
|
forEachDependent(el, (sub, link) => {
|
|
3526
4016
|
sub._time = clock;
|
|
4017
|
+
// A pending mark on a kept-tail link re-derives the subscriber instead of
|
|
4018
|
+
// marking it (A30, #3494 review; fuzzer latest-1 #2141; #3519 review).
|
|
4019
|
+
// Past `_depsTail` lie the committed frame's deps, kept by A30 because
|
|
4020
|
+
// that frame still derives from them while the pass that dropped them is
|
|
4021
|
+
// held (staged, or unchanged and parked). A source going pending there is
|
|
4022
|
+
// a question for the node's NEXT pass, not a fact about its current one:
|
|
4023
|
+
// marked, the node was registered as the flight's reporter and its holder
|
|
4024
|
+
// entangled with the flight (the A15 arm below) on a dep the held frame
|
|
4025
|
+
// never reads — an orphaned fetch held the truth (a hide joined to a
|
|
4026
|
+
// parked action, A34), and a manual flight nobody awaited held a gated
|
|
4027
|
+
// reader hidden forever (fuzzer branches-1 #1105). Skipped, the committed
|
|
4028
|
+
// frame published stale beside its new inputs (`query=1` beside a
|
|
4029
|
+
// `selected` derived from `remote(0)`). Re-derived, the pass decides: it
|
|
4030
|
+
// reads the dep and registers through its own read, or reads a held input
|
|
4031
|
+
// and enters that transaction (A29), or reads neither and is done. Clears
|
|
4032
|
+
// and errors still ride every link. A link inside the prefix carries the
|
|
4033
|
+
// pass's generation (`link()`), so the test is O(1); mid-pass the prefix
|
|
4034
|
+
// is what the pass has read so far, and the heap refuses a recomputing
|
|
4035
|
+
// node — a dep it has yet to reach registers through its own read.
|
|
4036
|
+
if (status === STATUS_PENDING && link._gen !== sub._depGen) {
|
|
4037
|
+
enqueueSub(sub);
|
|
4038
|
+
schedule();
|
|
4039
|
+
return;
|
|
4040
|
+
}
|
|
3527
4041
|
if (
|
|
3528
4042
|
(status === STATUS_PENDING &&
|
|
3529
4043
|
pendingSource &&
|
|
@@ -3540,7 +4054,26 @@ function notifyStatus(el, status, error, blockStatus, lane) {
|
|
|
3540
4054
|
schedule();
|
|
3541
4055
|
return;
|
|
3542
4056
|
}
|
|
3543
|
-
|
|
4057
|
+
// A memo another live transaction HOLDS — pending on its work, or
|
|
4058
|
+
// staged by it — made pending by THIS flight cannot reveal before the
|
|
4059
|
+
// flight lands: the two settle as one unit (A15, a shared derivation of
|
|
4060
|
+
// both — #3443). Propagation marks the held memo without recomputing it
|
|
4061
|
+
// (its inputs' values are unchanged), so this is the one moment the
|
|
4062
|
+
// entanglement is known; the memo's stamped re-entry at its next pass
|
|
4063
|
+
// came too late — the holder's own flight landed first and revealed the
|
|
4064
|
+
// inputs beside the stale sum. The stamp alone decides nothing (#3334):
|
|
4065
|
+
// a node the transaction once queued but holds nothing of — a switch's
|
|
4066
|
+
// shared output whose first flight the second write superseded — must
|
|
4067
|
+
// not drag the older flight into the newer reveal. Effects entangle
|
|
4068
|
+
// nothing (A15 shared-hole corollary): their reader registers with the
|
|
4069
|
+
// flight's transaction at queue notification.
|
|
4070
|
+
if (!downstreamBlockStatus)
|
|
4071
|
+
sub._transition
|
|
4072
|
+
? pendingSource &&
|
|
4073
|
+
!sub._type &&
|
|
4074
|
+
(sub._statusFlags & STATUS_PENDING || sub._pendingValue !== NOT_PENDING) &&
|
|
4075
|
+
globalQueue.initTransition(sub._transition)
|
|
4076
|
+
: queuePendingNode(sub);
|
|
3544
4077
|
notifyStatus(sub, status, error, downstreamBlockStatus, downstreamLane);
|
|
3545
4078
|
}
|
|
3546
4079
|
});
|
|
@@ -3570,6 +4103,10 @@ const REACTIVE_WRITE_IN_OWNED_SCOPE_SIGNAL_MESSAGE =
|
|
|
3570
4103
|
const REACTIVE_WRITE_IN_OWNED_SCOPE_REFRESH_MESSAGE =
|
|
3571
4104
|
"[REACTIVE_WRITE_IN_OWNED_SCOPE] Calling refresh() inside an owned scope (component, computation) is not allowed. " +
|
|
3572
4105
|
"Move the invalidation outside pure computation.";
|
|
4106
|
+
const ASYNC_STORE_SETTER_MESSAGE =
|
|
4107
|
+
"[ASYNC_STORE_SETTER] Store setter callback returned a Promise. A store setter is a synchronous transaction: " +
|
|
4108
|
+
"the draft closes when the callback returns, so writes after an `await` are lost. " +
|
|
4109
|
+
"Move the await into an action() and call the setter from there.";
|
|
3573
4110
|
let tracking = false;
|
|
3574
4111
|
/** @internal verdict-module glue */
|
|
3575
4112
|
function setPendingCheckActive(v) {
|
|
@@ -3633,10 +4170,15 @@ function releaseSubtree(owner) {
|
|
|
3633
4170
|
function clearSnapshots() {
|
|
3634
4171
|
if (snapshotSources) {
|
|
3635
4172
|
for (const source of snapshotSources) {
|
|
3636
|
-
|
|
4173
|
+
// The extension is a fixed-shape object with `_snapshotValue`
|
|
4174
|
+
// pre-initialized to undefined (see ext()), and every reader tests
|
|
4175
|
+
// `!== undefined` — assign, don't `delete`: deleting a field pushes the
|
|
4176
|
+
// object to dictionary mode for every later read of every field.
|
|
4177
|
+
const x = source._x;
|
|
4178
|
+
if (x != null) x._snapshotValue = undefined;
|
|
3637
4179
|
// StoreNode targets share one pre-initialized hidden class (see
|
|
3638
|
-
// createStoreProxy) —
|
|
3639
|
-
//
|
|
4180
|
+
// createStoreProxy) — same rule, and only when present so signal-node
|
|
4181
|
+
// sources don't grow the field.
|
|
3640
4182
|
if (source[STORE_SNAPSHOT_PROPS] !== undefined) source[STORE_SNAPSHOT_PROPS] = undefined;
|
|
3641
4183
|
}
|
|
3642
4184
|
snapshotSources = null;
|
|
@@ -3653,7 +4195,11 @@ function recompute(el, create = false) {
|
|
|
3653
4195
|
let devChanged = false;
|
|
3654
4196
|
if (attrHooks !== null) attrHooks.recomputeStart(el, create);
|
|
3655
4197
|
if (!create) {
|
|
3656
|
-
|
|
4198
|
+
// A stamped memo re-enters its hold: its value is that transaction's work.
|
|
4199
|
+
// An effect's pass belongs to whatever dirtied it (A15 corollary: effects
|
|
4200
|
+
// don't entangle parallel transactions); it joins its stamp only when the
|
|
4201
|
+
// pass observes the held flight — queue notification (#3407).
|
|
4202
|
+
if (el._transition && !isEffect && activeTransition !== el._transition)
|
|
3657
4203
|
globalQueue.initTransition(el._transition);
|
|
3658
4204
|
deleteFromHeap(el, queueFor(el));
|
|
3659
4205
|
if (el._x !== null) {
|
|
@@ -3665,8 +4211,14 @@ function recompute(el, create = false) {
|
|
|
3665
4211
|
// work that replaced it. Idempotent with the cleanup-channel close.
|
|
3666
4212
|
releaseFlightTeardown(el);
|
|
3667
4213
|
}
|
|
3668
|
-
// Tracked effects run after finalizePureQueue, so dispose immediately
|
|
3669
|
-
|
|
4214
|
+
// Tracked effects run after finalizePureQueue, so dispose immediately
|
|
4215
|
+
// instead of deferring. Children built by an uncommitted recompute
|
|
4216
|
+
// (CONFIG_HELD_CHILDREN) die immediately too: no frame ever showed them.
|
|
4217
|
+
// Everything else is the committed frame's and is deferred as zombies
|
|
4218
|
+
// until this node's commit — a transaction-owned node included (#3404):
|
|
4219
|
+
// a parked node's children predate the hold, and tearing them down when
|
|
4220
|
+
// the source lands ran cleanups before the transaction's atomic reveal.
|
|
4221
|
+
if (isEffect === EFFECT_TRACKED || el._config & CONFIG_HELD_CHILDREN) disposeChildren(el);
|
|
3670
4222
|
else if (el._firstChild !== null || el._disposal !== null) {
|
|
3671
4223
|
markDisposal(el);
|
|
3672
4224
|
const x = ext(el);
|
|
@@ -3679,8 +4231,11 @@ function recompute(el, create = false) {
|
|
|
3679
4231
|
} else clearSignals(el);
|
|
3680
4232
|
}
|
|
3681
4233
|
let isOptimisticDirty = !!(el._flags & REACTIVE_OPTIMISTIC_DIRTY);
|
|
4234
|
+
// A derived override (lanes stage, #3479) is override-covered like a written
|
|
4235
|
+
// one: a plain pass over it — its source superseded (A18) — stages the truth
|
|
4236
|
+
// and supersedes the override through the sync twin below.
|
|
3682
4237
|
const hasOverride =
|
|
3683
|
-
(el._config & CONFIG_OPTIMISTIC) !== 0 &&
|
|
4238
|
+
(el._config & (CONFIG_OPTIMISTIC | CONFIG_DERIVED_OVERRIDE)) !== 0 &&
|
|
3684
4239
|
el._x?._overrideValue !== NOT_PENDING &&
|
|
3685
4240
|
el._x?._overrideValue !== undefined;
|
|
3686
4241
|
const wasUninitialized = !!(el._statusFlags & STATUS_UNINITIALIZED);
|
|
@@ -3688,8 +4243,8 @@ function recompute(el, create = false) {
|
|
|
3688
4243
|
// A conditional can drop its pending source and recover to an unchanged
|
|
3689
4244
|
// value, leaving blocked dependents outside that source’s settle walk.
|
|
3690
4245
|
const outgoingError = el._statusFlags & STATUS_ERROR ? el._x?._error : undefined;
|
|
3691
|
-
const
|
|
3692
|
-
|
|
4246
|
+
const wasPending = (el._statusFlags & STATUS_PENDING) !== 0;
|
|
4247
|
+
const outgoingPendingSources = wasPending ? el._x?._pendingSources : undefined;
|
|
3693
4248
|
// Pending SOURCE-hood, captured before the compute clears status: a node
|
|
3694
4249
|
// whose own flight parked dependents self-registers in _pendingSources
|
|
3695
4250
|
// (notifyStatus, isSource). If this recompute supersedes that flight and
|
|
@@ -3705,6 +4260,11 @@ function recompute(el, create = false) {
|
|
|
3705
4260
|
// commits (commitPendingNode) — a closed window plus a held value reads as
|
|
3706
4261
|
// a pending frame to live observers of the verdict (#2990).
|
|
3707
4262
|
const wasLoading = el._loading;
|
|
4263
|
+
// Creation-time A29 (see enterStagedRead): a pass outside a flush that is
|
|
4264
|
+
// served a live transaction's staged value records the transaction here
|
|
4265
|
+
// and is staged INTO it below — "born held" — instead of committing.
|
|
4266
|
+
const prevStagedEntry = stagedEntry;
|
|
4267
|
+
stagedEntry = null;
|
|
3708
4268
|
const oldcontext = context;
|
|
3709
4269
|
context = el;
|
|
3710
4270
|
el._depsTail = null;
|
|
@@ -3730,6 +4290,17 @@ function recompute(el, create = false) {
|
|
|
3730
4290
|
// covers creation-time computes and flushes that run inside the window.
|
|
3731
4291
|
const prevLatestRead = latestReadActive;
|
|
3732
4292
|
latestReadActive = false;
|
|
4293
|
+
// A memo computes under its OWN lane posture, never the puller's (A31,
|
|
4294
|
+
// #3442): its value is one shared slot every reader sees, so a pull from a
|
|
4295
|
+
// lane-carrying reader (a probe effect on its companion lane pulling a sync
|
|
4296
|
+
// memo) must not run it with that lane's read carve-outs — under a lane, a
|
|
4297
|
+
// pending node on no lane serves its committed value instead of throwing,
|
|
4298
|
+
// and the memo then published a stale "settled" value, dropped its
|
|
4299
|
+
// pending status, and stopped holding its transaction. The branches below
|
|
4300
|
+
// re-establish the posture the memo itself owns (OPT-dirty, or adopted
|
|
4301
|
+
// through its deps). Effects keep the ambient lane: their runs are the
|
|
4302
|
+
// lane's own view.
|
|
4303
|
+
if (!isEffect) currentOptimisticLane = null;
|
|
3733
4304
|
// Lane posture lives with the engine: OPTIMISTIC_DIRTY is only ever set by
|
|
3734
4305
|
// engine-driven paths, and _optimisticNodes is only pushed by
|
|
3735
4306
|
// _optimisticWrite, so the hook is installed whenever either gate holds.
|
|
@@ -3740,6 +4311,21 @@ function recompute(el, create = false) {
|
|
|
3740
4311
|
// latest()/isPending() pull stages instead of direct-committing (#3009).
|
|
3741
4312
|
// The predicate lives with the engine (recomputeLane).
|
|
3742
4313
|
else if (lane === false) isOptimisticDirty = false;
|
|
4314
|
+
} else if (el._config & CONFIG_DERIVED_OVERRIDE) {
|
|
4315
|
+
// Lanes stage (#3479): a pass over a live lane member carrying a derived
|
|
4316
|
+
// override is the lane's pass whatever channel dirtied it (a boundary
|
|
4317
|
+
// reset, an unrelated sync write) — its inputs serve the lane's view, so
|
|
4318
|
+
// its result is the lane's and belongs in the override slot. Run plain,
|
|
4319
|
+
// A18's sync twin below read that re-derived lane view as a differing
|
|
4320
|
+
// truth (a fresh array), superseded the override and demoted the lane;
|
|
4321
|
+
// the lane's next pass then dropped the staged "truth" and left the node
|
|
4322
|
+
// flagged superseded with nothing to serve (fuzzer latest-1 #2481). A
|
|
4323
|
+
// demoted node resolves no lane and stays plain: its pass IS the truth.
|
|
4324
|
+
const lane = GlobalQueue._recomputeLane(el, true);
|
|
4325
|
+
if (lane) {
|
|
4326
|
+
isOptimisticDirty = true;
|
|
4327
|
+
currentOptimisticLane = lane;
|
|
4328
|
+
}
|
|
3743
4329
|
} else if (activeTransition && !create && activeTransition._optimisticNodes.length) {
|
|
3744
4330
|
// Lane adoption: parent-deeper-than-owned-child can run before its OPT-dirty
|
|
3745
4331
|
// child propagates. Walk deps once and inherit the OPT lane so this node
|
|
@@ -3787,7 +4373,9 @@ function recompute(el, create = false) {
|
|
|
3787
4373
|
// its body gates on is either _statusFlags or lives in the cold
|
|
3788
4374
|
// extension — no extension, no status to clear. (_x from an unrelated
|
|
3789
4375
|
// installer just makes clearStatus a cheap re-verified no-op.)
|
|
3790
|
-
|
|
4376
|
+
// A node born held keeps STATUS_UNINITIALIZED until its transaction
|
|
4377
|
+
// commits: it has no committed value, and read() holds its readers.
|
|
4378
|
+
if (el._statusFlags !== 0 || el._x !== null) clearStatus(el, create && stagedEntry === null);
|
|
3791
4379
|
// _optimisticLane is only ever assigned by engine paths (CONFIG_HAS_LANE
|
|
3792
4380
|
// is their sticky presence mark).
|
|
3793
4381
|
if (el._config & CONFIG_HAS_LANE && el._x?._optimisticLane) GlobalQueue._laneAsyncSettled(el);
|
|
@@ -3821,6 +4409,18 @@ function recompute(el, create = false) {
|
|
|
3821
4409
|
// The replacement source is fully propagated now. If no new flight
|
|
3822
4410
|
// re-owned self, retire the superseded flight and its dependent copies.
|
|
3823
4411
|
if (notReady && wasPendingSource && !el._x?._inFlight) settlePendingSource(el);
|
|
4412
|
+
// A re-park drops what the earlier pass carried (#3456): a source this
|
|
4413
|
+
// pass no longer reaches — its branch switched, or a fresh flight
|
|
4414
|
+
// replaced the inputs' pending with its own — stays copied onto
|
|
4415
|
+
// dependents that reached it only through here, and its landing walk
|
|
4416
|
+
// stops at this node (nothing left to retire) before it finds them. A
|
|
4417
|
+
// dependent then waits forever on a flight it has no path to. The
|
|
4418
|
+
// re-park twin of the unchanged-value recovery sweep below; dependents
|
|
4419
|
+
// with another path keep the source (retryReaches).
|
|
4420
|
+
if (notReady && outgoingPendingSources)
|
|
4421
|
+
for (const source of outgoingPendingSources)
|
|
4422
|
+
if (source !== el && !el._x?._pendingSources?.has(source))
|
|
4423
|
+
settlePendingSource(el, source);
|
|
3824
4424
|
if (reaskChanged) GlobalQueue._repollVerdicts(el);
|
|
3825
4425
|
}
|
|
3826
4426
|
} finally {
|
|
@@ -3837,18 +4437,26 @@ function recompute(el, create = false) {
|
|
|
3837
4437
|
el._flags = REACTIVE_NONE | (create ? el._flags & REACTIVE_SNAPSHOT_STALE : 0);
|
|
3838
4438
|
context = oldcontext;
|
|
3839
4439
|
}
|
|
4440
|
+
// The cast re-widens: TS narrowed `stagedEntry` to `null` at the reset
|
|
4441
|
+
// above and does not invalidate that across the compute call that
|
|
4442
|
+
// `enterStagedRead` runs under. No emitted code.
|
|
4443
|
+
const bornHeld = stagedEntry;
|
|
4444
|
+
stagedEntry = prevStagedEntry;
|
|
3840
4445
|
if (!el._x?._error) {
|
|
3841
|
-
|
|
3842
|
-
//
|
|
3843
|
-
//
|
|
3844
|
-
//
|
|
3845
|
-
//
|
|
3846
|
-
//
|
|
3847
|
-
//
|
|
3848
|
-
//
|
|
4446
|
+
// Observe-tier fan-in (HUGE_FAN_IN): the validated prefix [_deps.._depsTail]
|
|
4447
|
+
// IS this pass's distinct sources — count it here rather than per link. A
|
|
4448
|
+
// begin/end bracket around the pass plus a per-link increment measured
|
|
4449
|
+
// -5.8% on createRenderEffects:create1to1 (CodSpeed, dev tier) and cost
|
|
4450
|
+
// several points of the shape wins elsewhere; this walk is a fraction of
|
|
4451
|
+
// the reads that built the list and keeps no module state, so nested
|
|
4452
|
+
// pulls need no save/restore. (The stale tail is trimmed at the end of
|
|
4453
|
+
// this pass, or at its commit — see recompute's tail.)
|
|
3849
4454
|
{
|
|
3850
4455
|
let fanIn = 0;
|
|
3851
|
-
for (let d = el._deps; d !== null; d = d._nextDep)
|
|
4456
|
+
for (let d = el._deps; d !== null; d = d._nextDep) {
|
|
4457
|
+
fanIn++;
|
|
4458
|
+
if (d === el._depsTail) break;
|
|
4459
|
+
}
|
|
3852
4460
|
if (fanIn >= GRAPH_SIZE_WARN_AT) noteFanIn(el, fanIn);
|
|
3853
4461
|
}
|
|
3854
4462
|
// INV-11 (#3330): the equality gate compares against the slot this run
|
|
@@ -3921,7 +4529,7 @@ function recompute(el, create = false) {
|
|
|
3921
4529
|
else if (valueChanged) {
|
|
3922
4530
|
const prevVisible = hasOverride ? el._x?._overrideValue : undefined;
|
|
3923
4531
|
if (
|
|
3924
|
-
create ||
|
|
4532
|
+
(create && bornHeld === null) ||
|
|
3925
4533
|
// Plain sync flush (no transition on either side) commits effect
|
|
3926
4534
|
// values directly — the pending round-trip (queuePendingNode +
|
|
3927
4535
|
// commitPendingNodes) exists to sequence transition reveals, and
|
|
@@ -3931,6 +4539,7 @@ function recompute(el, create = false) {
|
|
|
3931
4539
|
// not the stashed queues, so a staged value would hand the immediate
|
|
3932
4540
|
// apply stale state — see CONFIG_DIRECT_COMMIT.
|
|
3933
4541
|
(isEffect &&
|
|
4542
|
+
bornHeld === null &&
|
|
3934
4543
|
(activeTransition !== el._transition ||
|
|
3935
4544
|
activeTransition === null ||
|
|
3936
4545
|
el._config & CONFIG_DIRECT_COMMIT)) ||
|
|
@@ -3943,26 +4552,50 @@ function recompute(el, create = false) {
|
|
|
3943
4552
|
// round-trip is that separation; it cannot be skipped on any path a
|
|
3944
4553
|
// pull can reach.
|
|
3945
4554
|
) {
|
|
3946
|
-
|
|
3947
|
-
//
|
|
3948
|
-
//
|
|
3949
|
-
//
|
|
3950
|
-
//
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
4555
|
+
// Lanes stage (#3479): a lane pass on a memo publishes its speculative
|
|
4556
|
+
// result as an OVERRIDE — `_value` stays the committed truth, so a
|
|
4557
|
+
// reader off the lane (A17's committed view, #3460) sees a whole
|
|
4558
|
+
// committed frame: the source's shadow and its derivations together,
|
|
4559
|
+
// never a committed shadow beside a speculative memo. The lane's own
|
|
4560
|
+
// readers and untracked reads see the override (A17); the revert
|
|
4561
|
+
// drops it and re-derives (resolveOptimisticNodes). Effects keep the
|
|
4562
|
+
// direct commit — their `_value` is a run result the lane's queues
|
|
4563
|
+
// already sequence. Either way the lane pass drops any superseded
|
|
4564
|
+
// older hold so its queued commit can't clobber the fresh frame: a
|
|
4565
|
+
// hold staged on an earlier, lane-free pass of the SAME transaction
|
|
4566
|
+
// is superseded — left in place, the commit published the older
|
|
4567
|
+
// frame over the fresh one (#3377). A reversion pass (OPT-dirty with
|
|
4568
|
+
// no lane — the override dropped) commits directly: it IS the truth.
|
|
4569
|
+
if (isOptimisticDirty && !isEffect && currentOptimisticLane !== null)
|
|
4570
|
+
GlobalQueue._laneOverride(el, value, currentOptimisticLane);
|
|
4571
|
+
else el._value = value;
|
|
4572
|
+
if (isOptimisticDirty) el._pendingValue = NOT_PENDING;
|
|
3955
4573
|
} else {
|
|
3956
4574
|
el._pendingValue = value;
|
|
4575
|
+
if (bornHeld !== null) {
|
|
4576
|
+
// Born held: the pass ran from mainline and derived from this
|
|
4577
|
+
// transaction's staged world (enterStagedRead). Its value is the
|
|
4578
|
+
// transaction's — staged into it directly, stamped, and committed
|
|
4579
|
+
// with it; mainline's batch never sees it. A born-held effect
|
|
4580
|
+
// skips its synchronous first run (effect()) and is replayed by
|
|
4581
|
+
// the commit like a stale reader that showed the committed frame.
|
|
4582
|
+
el._transition = bornHeld;
|
|
4583
|
+
bornHeld._pendingNodes.push(el);
|
|
4584
|
+
if (isEffect) bornHeld._gatedSubs.add(el);
|
|
4585
|
+
}
|
|
3957
4586
|
// A window landing that gets held re-opens the window until the hold
|
|
3958
4587
|
// commits — the verdict's held-value branch is window-gated (#2990).
|
|
3959
4588
|
if (wasLoading) el._loading = true;
|
|
3960
|
-
//
|
|
4589
|
+
// A staged sync recompute is a write path like setSignal/asyncWrite,
|
|
3961
4590
|
// so sync derivations of held sources stay visible to isPending()/latest()
|
|
3962
4591
|
// (#2831). Both companion writes are transition-scoped (optimistic) and
|
|
3963
|
-
// auto-revert/re-derive at commit.
|
|
3964
|
-
//
|
|
3965
|
-
|
|
4592
|
+
// auto-revert/re-derive at commit. Not gated on an active transition:
|
|
4593
|
+
// a plain flush can still become a hold after this recompute — an
|
|
4594
|
+
// async memo downstream pends and the batch is adopted into a
|
|
4595
|
+
// transaction (scheduler.enterTransition) — and nothing re-derives
|
|
4596
|
+
// the companion at adoption, so a memo held that way read
|
|
4597
|
+
// isPending() false while its held source read true (#3413).
|
|
4598
|
+
if (el._config & CONFIG_HAS_COMPANIONS && GlobalQueue._syncCompanions !== null)
|
|
3966
4599
|
GlobalQueue._syncCompanions(el, value);
|
|
3967
4600
|
}
|
|
3968
4601
|
// insertSubs only walks _subs (no scheduling of its own), so a
|
|
@@ -4024,6 +4657,54 @@ function recompute(el, create = false) {
|
|
|
4024
4657
|
if (wasPendingSource && !(el._statusFlags & (STATUS_PENDING | STATUS_UNINITIALIZED)))
|
|
4025
4658
|
settlePendingSource(el);
|
|
4026
4659
|
}
|
|
4660
|
+
// A REPORTER whose pass stopped reading a source it reported on (a gate
|
|
4661
|
+
// closed) stops counting for the transaction waiting on it (A15 / #3426:
|
|
4662
|
+
// the hold lasts while a live reporter observes the flight). Nothing else
|
|
4663
|
+
// re-judges a parked transaction (see wokenTransitions; the disposal
|
|
4664
|
+
// (#3372) and boundary (#3375) twins of this site), so the writes it held
|
|
4665
|
+
// stayed staged for as long as the flight stayed up — forever, for one
|
|
4666
|
+
// that never lands (fuzzer #3446 P1, spec O3). The event is "this pass
|
|
4667
|
+
// dropped a dep" — deps past `_depsTail` (trimmed below, or kept by A30
|
|
4668
|
+
// for a staged pass), or a pass that read nothing — not "recovered from
|
|
4669
|
+
// pending": a reporter registered by the stale-reader carve-out
|
|
4670
|
+
// (heldFromStale, an INITIALIZED source refetching) displays the committed
|
|
4671
|
+
// value and is never pending (fuzzer case 79). Every parked transaction,
|
|
4672
|
+
// not the reporter's stamp: the transaction waiting on it registered it
|
|
4673
|
+
// without stamping it. One idle pass per parked transaction; done ones
|
|
4674
|
+
// return at re-entry. Effects only — reporters register from render-effect
|
|
4675
|
+
// notification (INV-3).
|
|
4676
|
+
// (`_depsTail` was reset at the top of the pass; TS keeps that narrowing.)
|
|
4677
|
+
const tail = el._depsTail;
|
|
4678
|
+
if (
|
|
4679
|
+
isEffect &&
|
|
4680
|
+
((wasPending && !(el._statusFlags & STATUS_PENDING)) ||
|
|
4681
|
+
(tail === null ? el._deps !== null : tail._nextDep !== null))
|
|
4682
|
+
)
|
|
4683
|
+
wakeParked();
|
|
4684
|
+
// Dependencies are the committed frame's until it is replaced (A30, #3410; the
|
|
4685
|
+
// deps twin of the held children above): a pass that staged its value
|
|
4686
|
+
// leaves the previous pass's tail linked for `commitPendingNode` to trim,
|
|
4687
|
+
// so a write to a dependency the committed value still derives from
|
|
4688
|
+
// reaches this node — and joins its hold if the stage is transaction-held
|
|
4689
|
+
// by then (a plain flush decides nothing here: the transaction that holds
|
|
4690
|
+
// the pass may open later in the same flush). A pass that published
|
|
4691
|
+
// directly (a first pass, a lane's own reveal) trims now. An errored pass
|
|
4692
|
+
// (a throw, NotReady included, or a comparator throw above) keeps its full
|
|
4693
|
+
// list as before — `_depsTail` marks where it stopped — and the commit
|
|
4694
|
+
// skips it by the same `_error`. An effect's frame is the run its value is
|
|
4695
|
+
// applied by, not the value slot (#3438): a direct-committed pass that
|
|
4696
|
+
// still owes a run (`_modified`) has not replaced what the last run
|
|
4697
|
+
// published — the same flush may stash that run into a transaction it
|
|
4698
|
+
// opens later — so its tail waits for `runEffect` to trim once the run
|
|
4699
|
+
// applies. A pass that changed nothing replaced nothing either (#3469): the
|
|
4700
|
+
// same flush may park with its inputs held, and the committed frame still
|
|
4701
|
+
// derives from the tail — its trim waits on the flush's verdict (heldTrims).
|
|
4702
|
+
// A tracked effect's pass IS its run (it bypasses the heap and runs after
|
|
4703
|
+
// the commit): the frame is replaced, trim now.
|
|
4704
|
+
if (!el._x?._error && el._pendingValue === NOT_PENDING && !(isEffect && el._modified)) {
|
|
4705
|
+
if (create || isOptimisticDirty || isEffect === EFFECT_TRACKED) trimStaleDeps(el);
|
|
4706
|
+
else if (el._depsTail?._nextDep ?? el._deps) heldTrims.push(el);
|
|
4707
|
+
}
|
|
4027
4708
|
// Attribution hook: fired before the lane restore so `currentOptimisticLane`
|
|
4028
4709
|
// still reflects THIS run's posture. The facts distinguish an overlay
|
|
4029
4710
|
// recompute (optimistic lane, transition replay, transition-held commit)
|
|
@@ -4048,14 +4729,40 @@ function recompute(el, create = false) {
|
|
|
4048
4729
|
// under the override (A17). Revert no longer commits anything, so an
|
|
4049
4730
|
// unqueued covered hold would leak (INV-7) once the revert clears
|
|
4050
4731
|
// _transition.
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4732
|
+
//
|
|
4733
|
+
// While a pass's result waits on a commit, its children (and `_disposal`)
|
|
4734
|
+
// wait with it, and a re-run may tear them down immediately
|
|
4735
|
+
// (CONFIG_HELD_CHILDREN, #3404). One exception: a transaction-owned effect
|
|
4736
|
+
// recomputed mainline (contested, #3322) published its value directly — a
|
|
4737
|
+
// `_pendingValue` left from an earlier held pass is that transaction's,
|
|
4738
|
+
// not this one's — so this pass's children are the frame's, and any
|
|
4739
|
+
// zombies deferred at the top are superseded on that same frame. Its
|
|
4740
|
+
// commit rides the transaction, not this flush: release them here rather
|
|
4741
|
+
// than let two generations render at once.
|
|
4742
|
+
let held =
|
|
4743
|
+
needsPendingCommit &&
|
|
4744
|
+
(!create || bornHeld !== null || (el._statusFlags & STATUS_PENDING) !== 0);
|
|
4745
|
+
if (held && (!el._transition || hasOverride)) queuePendingNode(el);
|
|
4746
|
+
else if (
|
|
4747
|
+
held &&
|
|
4748
|
+
activeTransition === null &&
|
|
4749
|
+
!(el._statusFlags & (STATUS_PENDING | STATUS_UNINITIALIZED))
|
|
4750
|
+
) {
|
|
4751
|
+
held = false;
|
|
4752
|
+
disposeChildren(el, false, true);
|
|
4753
|
+
}
|
|
4754
|
+
if (held) el._config |= CONFIG_HELD_CHILDREN;
|
|
4755
|
+
else el._config &= ~CONFIG_HELD_CHILDREN;
|
|
4756
|
+
// (A born-held pass IS the transaction's staged view — nothing to refresh.)
|
|
4757
|
+
if (el._transition && isEffect && activeTransition !== el._transition && bornHeld === null) {
|
|
4758
|
+
// The re-run refreshes the transaction's STAGED view (_pendingValue); the
|
|
4759
|
+
// value this pass published in _value belongs to the run that just
|
|
4760
|
+
// finished. Keep that ownership, or the effect phase parks a
|
|
4761
|
+
// mainline-computed value with the transaction (#3412).
|
|
4762
|
+
const owner = el._valueTransition;
|
|
4058
4763
|
runInTransition(el._transition, () => recompute(el));
|
|
4764
|
+
el._valueTransition = owner;
|
|
4765
|
+
}
|
|
4059
4766
|
// Missed-wake reschedule (see the finally above): values this pass read
|
|
4060
4767
|
// before the nested commit are stale, so run again now that the heap will
|
|
4061
4768
|
// accept the node. Equality gates stop same-value landings from cascading,
|
|
@@ -4159,6 +4866,7 @@ function ext(el) {
|
|
|
4159
4866
|
_overrideValue: undefined,
|
|
4160
4867
|
_overrideOwner: undefined,
|
|
4161
4868
|
_overrideTime: 0,
|
|
4869
|
+
_flushedStaged: NOT_PENDING,
|
|
4162
4870
|
_overrideStamp: 0,
|
|
4163
4871
|
_optimisticLane: undefined,
|
|
4164
4872
|
_pendingSignal: undefined,
|
|
@@ -4372,9 +5080,10 @@ function linkFirewallChild(firewall, s) {
|
|
|
4372
5080
|
/** Release a firewall child the store no longer addresses (unobserved sweep
|
|
4373
5081
|
* dropped it from its target's cache): unlink it from the chain so the
|
|
4374
5082
|
* projection stops retaining it and its last value. The node keeps its own
|
|
4375
|
-
* `_nextChild` so a walk that is mid-chain on it still terminates.
|
|
4376
|
-
* `_companionChildren`
|
|
4377
|
-
*
|
|
5083
|
+
* `_nextChild` so a walk that is mid-chain on it still terminates. It also
|
|
5084
|
+
* leaves `_companionChildren` (#3503): the companions themselves are
|
|
5085
|
+
* permanent on the node, but the node is unreachable through the store, so
|
|
5086
|
+
* the set would only retain it and its last value. */
|
|
4378
5087
|
function unlinkFirewallChild(node) {
|
|
4379
5088
|
const n = node;
|
|
4380
5089
|
const fw = n._firewall;
|
|
@@ -4385,6 +5094,11 @@ function unlinkFirewallChild(node) {
|
|
|
4385
5094
|
else if (fw._x._child === n) fw._x._child = next;
|
|
4386
5095
|
if (next !== null) next._prevChild = prev;
|
|
4387
5096
|
n._prevChild = null;
|
|
5097
|
+
// #3503: the companion set is the last projection-side reference to a
|
|
5098
|
+
// released leaf; leaving it there kept the leaf and its last value alive
|
|
5099
|
+
// for the projection's lifetime after every latest()/isPending() reader
|
|
5100
|
+
// was disposed.
|
|
5101
|
+
fw._x._companionChildren?.delete(n);
|
|
4388
5102
|
}
|
|
4389
5103
|
function slotSignal(v, equals, host, key, acc, firewall = null) {
|
|
4390
5104
|
// Prod and observe boilerplates — see computed(). The store relabels the
|
|
@@ -4481,6 +5195,31 @@ function untrack(fn, strictReadLabel) {
|
|
|
4481
5195
|
strictRead = prevStrictRead;
|
|
4482
5196
|
}
|
|
4483
5197
|
}
|
|
5198
|
+
/**
|
|
5199
|
+
* Set while runtime bookkeeping evaluates a user accessor inside another
|
|
5200
|
+
* node's pass (a loading boundary's `on` key, read from `notify` while the
|
|
5201
|
+
* node that went pending is still recomputing). `context` is that node, but
|
|
5202
|
+
* the read is nobody's: the value is compared, never derived from, so the
|
|
5203
|
+
* untracked-pending re-run link (`read`, `!tracking`) must not be recorded on
|
|
5204
|
+
* it — it made the key's source a dependency of an unrelated async memo
|
|
5205
|
+
* (#3528: `isPending(m2)` through a memo in `on()` linked the memo into `m2`,
|
|
5206
|
+
* a cycle that re-derived `m2` on every pending mark and never converged).
|
|
5207
|
+
*/
|
|
5208
|
+
let spectating = false;
|
|
5209
|
+
/**
|
|
5210
|
+
* Evaluates `fn` untracked and without recording a dependency for the
|
|
5211
|
+
* untracked-pending re-run rule on the current `context`. For bookkeeping
|
|
5212
|
+
* reads made mid-propagation on behalf of no node — see `spectating`.
|
|
5213
|
+
*/
|
|
5214
|
+
function spectate(fn) {
|
|
5215
|
+
const prev = spectating;
|
|
5216
|
+
spectating = true;
|
|
5217
|
+
try {
|
|
5218
|
+
return untrack(fn);
|
|
5219
|
+
} finally {
|
|
5220
|
+
spectating = prev;
|
|
5221
|
+
}
|
|
5222
|
+
}
|
|
4484
5223
|
/**
|
|
4485
5224
|
* Bring a computed to a readable state: lazy/disposed nodes are (re)computed;
|
|
4486
5225
|
* an isPending() probe (`refresh`) additionally pulls the node fully up to
|
|
@@ -4557,15 +5296,262 @@ function installAuthoritativeRead() {
|
|
|
4557
5296
|
* already use). An effect the transaction itself computed re-derives at its
|
|
4558
5297
|
* commit on its own (parked run, or the contested re-derive, #3322) and is
|
|
4559
5298
|
* not recorded — replaying it too would publish the frame twice.
|
|
5299
|
+
*
|
|
5300
|
+
* Flight twin (the pending-branch carve-out): the reader is served the
|
|
5301
|
+
* node's committed, pre-flight value and now observes that flight — A15:
|
|
5302
|
+
* async work observed by a reader settles as one unit with the writes that
|
|
5303
|
+
* asked it — so it joins the transaction's reporters for the node. The
|
|
5304
|
+
* reporter the transaction recorded when the flight started may be gone (a
|
|
5305
|
+
* keyed remount disposed it, #3374); a completion check that found no live
|
|
5306
|
+
* reporter committed the writes ahead of the answer, tearing the new
|
|
5307
|
+
* reader's frame (`Count: 1` beside `Details: 0`). Joins an entry the
|
|
5308
|
+
* transaction already holds; a flight nobody had observed yet has none (the
|
|
5309
|
+
* reader is its first observer — a conditional that just revealed it, #3458)
|
|
5310
|
+
* and is notified up the reader's own queue chain under that transaction,
|
|
5311
|
+
* the one sanctioned registration site (INV-3): a collecting boundary above
|
|
5312
|
+
* the reader consumes it as it would any pending, an unboundaried reader
|
|
5313
|
+
* opens the entry — and the transaction, judged complete on its other
|
|
5314
|
+
* flights, revealed the inputs beside the reader's pre-flight value
|
|
5315
|
+
* otherwise (`Count: 1 | A: 1` beside `B: 0`). A staged signal or a settled
|
|
5316
|
+
* node registers nothing. Every reporter dies with its reader
|
|
5317
|
+
* (reporterBlocksSource: the read linked it as a dep). The node's own entry
|
|
5318
|
+
* is the only one that can matter: a chain's intermediate memo is re-pulled
|
|
5319
|
+
* by the read (updateIfNecessary's retry) and enters the transaction, so the
|
|
5320
|
+
* reader holds through the normal path; a node with its own flight that is
|
|
5321
|
+
* also pending on an upstream re-ask blocks through that flight until it
|
|
5322
|
+
* lands, and its landing re-runs the reader into the normal path.
|
|
5323
|
+
*/
|
|
5324
|
+
/** The replay half of the stale-of-foreign clause (A15 / A26): a stale reader
|
|
5325
|
+
* served the committed value because `txn` holds what it read re-runs at
|
|
5326
|
+
* txn's commit, when the value it was denied becomes the frame — unless its
|
|
5327
|
+
* own last value already came from that transaction. One registration for
|
|
5328
|
+
* the node path (heldFromStale) and the store's backing paths, which have
|
|
5329
|
+
* no node to carry the hold (heldFromReader, the adoption hold view). */
|
|
5330
|
+
function recordStaleReplay(txn, c) {
|
|
5331
|
+
const vt = c._valueTransition;
|
|
5332
|
+
if (vt == null || currentTransition(vt) !== txn) txn._gatedSubs.add(c);
|
|
5333
|
+
}
|
|
5334
|
+
/**
|
|
5335
|
+
* The ownership relation (DESIGN-CONSOLIDATION §6, ruled 2026-09-17): is
|
|
5336
|
+
* `hold` part of the running pass's world? A plain reader's world is the
|
|
5337
|
+
* transaction it runs under, through merges. A lane reader's world is its
|
|
5338
|
+
* lane AND the transition that owns the lane — the one asymmetry between a
|
|
5339
|
+
* lane and a separate transaction (a lane sees what lands from its parent as
|
|
5340
|
+
* its own; a separate transaction would wait for the parent to settle) —
|
|
5341
|
+
* see `ownsLane` in lanes.ts, built on this. One relation for the
|
|
5342
|
+
* stale-of-foreign clause (heldFromStale), the lane arm (readsHeldCommitted)
|
|
5343
|
+
* and the store's backing holds (foreignHold); `serve` has no lane arm of
|
|
5344
|
+
* its own, the lane's extra visibility lives here.
|
|
4560
5345
|
*/
|
|
5346
|
+
function ownsHold(hold) {
|
|
5347
|
+
return (
|
|
5348
|
+
activeTransition !== null && currentTransition(hold) === currentTransition(activeTransition)
|
|
5349
|
+
);
|
|
5350
|
+
}
|
|
4561
5351
|
function heldFromStale(el, c) {
|
|
4562
5352
|
const t = el._transition;
|
|
4563
|
-
if (t === null || t
|
|
5353
|
+
if (t === null || ownsHold(t)) return false;
|
|
4564
5354
|
const txn = currentTransition(t);
|
|
4565
|
-
|
|
4566
|
-
|
|
5355
|
+
recordStaleReplay(txn, c);
|
|
5356
|
+
const reporters = txn._asyncReporters.get(el);
|
|
5357
|
+
if (reporters) reporters.add(c);
|
|
5358
|
+
else if (el._statusFlags & STATUS_PENDING)
|
|
5359
|
+
runInTransition(txn, () => c._queue.notify(c, STATUS_PENDING, STATUS_PENDING, el._x._error));
|
|
5360
|
+
return true;
|
|
5361
|
+
}
|
|
5362
|
+
/**
|
|
5363
|
+
* A tracked computation about to be served a live transaction's staged value
|
|
5364
|
+
* derives from that transaction's world, so it enters the transaction and
|
|
5365
|
+
* this pass's result is held with it (A29, #3408). The write side (`setSignal`)
|
|
5366
|
+
* and a stamped node's recompute already enter; a reader that only now
|
|
5367
|
+
* starts reading the held node — a conditional memo whose branch flipped —
|
|
5368
|
+
* was the gap: it published a value derived from the staged world into the
|
|
5369
|
+
* mainline frame. Not for a probe (`isPending(() => x())` observes, it does
|
|
5370
|
+
* not derive) and a no-op for the ambient batch (`_transition` null) or the
|
|
5371
|
+
* transaction already active.
|
|
5372
|
+
*/
|
|
5373
|
+
/** The transaction a pass running OUTSIDE a flush was served a staged value
|
|
5374
|
+
* from (A29, creation-time form). Inside a flush the pass enters through
|
|
5375
|
+
* initTransition; outside one — a memo or effect created from mainline code
|
|
5376
|
+
* while a hold is live — entering would leave `activeTransition` and the
|
|
5377
|
+
* batch pointed at the transaction for the rest of the synchronous block,
|
|
5378
|
+
* so an unrelated write made after the mount was held with someone else's
|
|
5379
|
+
* action. The entry is the pass's alone: recompute stages the node into the
|
|
5380
|
+
* transaction (born held) and mainline is never touched. */
|
|
5381
|
+
let stagedEntry = null;
|
|
5382
|
+
function enterStagedRead(el, t = el._transition) {
|
|
5383
|
+
if (!t || t === activeTransition || pendingCheckActive) return;
|
|
5384
|
+
// A companion (the latest() shadow, the isPending() verdict signal) is the
|
|
5385
|
+
// engine's mirror of the flushed world — reading it, or being it, is an
|
|
5386
|
+
// observation, not a derivation from the hold: latest(x) never enters x's
|
|
5387
|
+
// transaction, and the shadow's own pass never enters either (it would
|
|
5388
|
+
// flip activeTransition under the reader that pulled it). (`el` is null for
|
|
5389
|
+
// a store backing served under a hold — no node, the transaction is the
|
|
5390
|
+
// fold's.)
|
|
5391
|
+
if (el?._x?._parentSource || context?._x?._parentSource) return;
|
|
5392
|
+
// Verdict machinery (GlobalQueue._verdictPull: companion creation and the
|
|
5393
|
+
// latest()/isPending() pulls — the latest() shadow is created before it is
|
|
5394
|
+
// marked optimistic, so the bit alone cannot tell) and optimistic nodes
|
|
5395
|
+
// (own lane posture, A31) read staged truth by design; they keep the
|
|
5396
|
+
// entering path.
|
|
5397
|
+
// (`context` is non-null here: every caller selected a value for a reader.)
|
|
5398
|
+
const ctx = context;
|
|
5399
|
+
if (activeTransition === null && !globalQueue._running) {
|
|
5400
|
+
// Verdict pulls are observations, not derivations: a latest() /
|
|
5401
|
+
// isPending() call from mainline must never enter a transaction (it
|
|
5402
|
+
// would capture the rest of the caller's synchronous block).
|
|
5403
|
+
if (GlobalQueue._verdictPull) return;
|
|
5404
|
+
if (
|
|
5405
|
+
ctx._flags & REACTIVE_RECOMPUTING_DEPS &&
|
|
5406
|
+
!(ctx._config & CONFIG_OPTIMISTIC) &&
|
|
5407
|
+
(stagedEntry === null || stagedEntry === t)
|
|
5408
|
+
) {
|
|
5409
|
+
stagedEntry = t;
|
|
5410
|
+
return;
|
|
5411
|
+
}
|
|
5412
|
+
}
|
|
5413
|
+
globalQueue.initTransition(t);
|
|
5414
|
+
}
|
|
5415
|
+
/**
|
|
5416
|
+
* Rule 1 (value selection), the full arm: does this reader see a STAGED
|
|
5417
|
+
* node's COMMITTED value? One implementation of the rule the fast paths
|
|
5418
|
+
* (readNodeFast, read's fast block) carry as their trivial ternary and that
|
|
5419
|
+
* every slow site — read's tail, the store's backing selection, the lane and
|
|
5420
|
+
* verdict arms — used to restate by hand (docs/DESIGN-CONSOLIDATION.md, move 3b). In order:
|
|
5421
|
+
* - no reader at all (an untracked read) — the committed frame;
|
|
5422
|
+
* - a reader under an optimistic lane the engine says reads committed
|
|
5423
|
+
* (laneReadsCommitted: another lane's hold, #3460);
|
|
5424
|
+
* - nothing staged;
|
|
5425
|
+
* - a children-forbidden reader (createTrackedEffect / onSettled: the frame,
|
|
5426
|
+
* never the graph — A32);
|
|
5427
|
+
* - a stale reader (render effect) of a FOREIGN transaction's staged write —
|
|
5428
|
+
* committed, no entanglement (heldFromStale registers the replay; a node
|
|
5429
|
+
* born held has no committed frame to fall back to, `noCommitted`);
|
|
5430
|
+
* - A17 for HELD truth (#3164, CONFIG_HELD_TRUTH): staged confirming truth —
|
|
5431
|
+
* fold-staged onto an armed family, or entangle-stolen by an awaited
|
|
5432
|
+
* until() — is masked from ordinary readers until its transaction's
|
|
5433
|
+
* reveal, the retaining transaction's own speculative recomputes included
|
|
5434
|
+
* (partial override coverage would otherwise compose override + staged
|
|
5435
|
+
* truth into a state no timeline contains). Authoritative readers
|
|
5436
|
+
* (until()'s predicate) and latest() see the staged truth — the tunnel that
|
|
5437
|
+
* keeps the hold deadlock-free.
|
|
5438
|
+
* False means the reader derives from the staged value and enters its
|
|
5439
|
+
* transaction (enterStagedRead, A29).
|
|
5440
|
+
*/
|
|
5441
|
+
function readerSeesCommitted(el, c, owner, noCommitted) {
|
|
5442
|
+
return !!(
|
|
5443
|
+
!c ||
|
|
5444
|
+
(currentOptimisticLane !== null && GlobalQueue._laneReadsCommitted(el, owner, c)) ||
|
|
5445
|
+
el._pendingValue === NOT_PENDING ||
|
|
5446
|
+
c._config & CONFIG_CHILDREN_FORBIDDEN ||
|
|
5447
|
+
(stale && !noCommitted && heldFromStale(el, c)) ||
|
|
5448
|
+
(el._config & CONFIG_HELD_TRUTH &&
|
|
5449
|
+
!latestReadActive &&
|
|
5450
|
+
!(c._config & CONFIG_AUTHORITATIVE_READ))
|
|
5451
|
+
);
|
|
5452
|
+
}
|
|
5453
|
+
/** A28 — set when a node is staged (queuePendingNode) or a held node rewritten
|
|
5454
|
+
* (stashHeldRewrite) OUTSIDE a flush; cleared when the next flush begins. The
|
|
5455
|
+
* read sites test this one module boolean instead of `globalQueue._running`:
|
|
5456
|
+
* inside a flush it is false and the A28 arm costs nothing; outside, only a
|
|
5457
|
+
* tick with unflushed writes pays the staged-node check. */
|
|
5458
|
+
let unflushedStaged = false;
|
|
5459
|
+
function markUnflushedStaged() {
|
|
5460
|
+
unflushedStaged = true;
|
|
5461
|
+
}
|
|
5462
|
+
/** A28 — a write becomes visible at flush. Outside a flush, a node holding an
|
|
5463
|
+
* AMBIENT staged value (no transaction stamp) was written since the last
|
|
5464
|
+
* flush: ambient staging commits at flush end, so nothing else leaves a node
|
|
5465
|
+
* in this state; a stamped value is the flushed held world, which A28 says
|
|
5466
|
+
* latest() serves. Inside a flush the rule does not apply (A28 (4): promoted
|
|
5467
|
+
* within the round). Structural — no marker on the write path. */
|
|
5468
|
+
function unflushed(el) {
|
|
5469
|
+
return unflushedValue(el) !== NOT_PENDING;
|
|
5470
|
+
}
|
|
5471
|
+
/** The value an unflushed node serves — the committed value for an ambient
|
|
5472
|
+
* write, the flushed staged value for a rewrite of a held node — or
|
|
5473
|
+
* NOT_PENDING when nothing is unflushed. Exempt: owned-write nodes (A28 (4):
|
|
5474
|
+
* a write issued inside a recompute is promoted at that recompute's end —
|
|
5475
|
+
* boundary and loading machinery, until()'s internals, signals declared for
|
|
5476
|
+
* in-computation writes) and engine companions (the isPending() verdict
|
|
5477
|
+
* signal, the latest() shadow: the system's own writes, made at the source's
|
|
5478
|
+
* write to mirror it, installing eagerly — A28, A8). */
|
|
5479
|
+
function unflushedValue(el, committed = el._value) {
|
|
5480
|
+
if (
|
|
5481
|
+
globalQueue._running ||
|
|
5482
|
+
el._pendingValue === NOT_PENDING ||
|
|
5483
|
+
el._config & CONFIG_PROMOTED ||
|
|
5484
|
+
el._x?._parentSource
|
|
5485
|
+
)
|
|
5486
|
+
return NOT_PENDING;
|
|
5487
|
+
// Ambient, or adopted by a transaction before any flush carried the staging
|
|
5488
|
+
// (CONFIG_ADOPTED_UNFLUSHED): nothing flushed is staged — the committed
|
|
5489
|
+
// value answers (the caller's notion of committed: a store node's backing).
|
|
5490
|
+
// A held node: unflushed only if rewritten since the last flush (stash).
|
|
5491
|
+
if (el._transition === null || el._config & CONFIG_ADOPTED_UNFLUSHED) return committed;
|
|
5492
|
+
return el._x === null ? NOT_PENDING : el._x._flushedStaged;
|
|
5493
|
+
}
|
|
5494
|
+
/** Held nodes rewritten since the last flush (setSignal); the flush clears
|
|
5495
|
+
* their stash — from then on latest() answers with the rewrite. */
|
|
5496
|
+
const unflushedRewrites = [];
|
|
5497
|
+
/** Nodes written inside a creation-time recompute (CONFIG_PROMOTED). */
|
|
5498
|
+
const promotedWrites = [];
|
|
5499
|
+
/** A28 (5): an optimistic write becomes the ACTIVE override at the flush that
|
|
5500
|
+
* carries it. `_overrideTime` is stamped with `clock` at the write and `clock`
|
|
5501
|
+
* advances after every flush, so "this tick, outside a flush" is unflushed. */
|
|
5502
|
+
function unflushedOverride(el) {
|
|
5503
|
+
// Companions are optimistic signals written by the engine (see unflushed).
|
|
5504
|
+
return !globalQueue._running && el._x?._overrideTime === clock && !el._x?._parentSource;
|
|
5505
|
+
}
|
|
5506
|
+
/** Active optimistic override on an armed node (an armed slot idles at
|
|
5507
|
+
* NOT_PENDING; undefined = unarmed plain node). The writer's own channels —
|
|
5508
|
+
* the draft, `in`/keys inside the setter — compose on this regardless of
|
|
5509
|
+
* flush state. */
|
|
5510
|
+
function hasActiveOverride(el) {
|
|
5511
|
+
const x = el._x;
|
|
5512
|
+
return x !== null && x._overrideValue !== undefined && x._overrideValue !== NOT_PENDING;
|
|
5513
|
+
}
|
|
5514
|
+
/** The override a READER sees: installed, and carried by a flush (A28 (5) —
|
|
5515
|
+
* an optimistic write is a write; until its flush no reader sees it). One
|
|
5516
|
+
* implementation for read()'s override arm, the verdict channels
|
|
5517
|
+
* (latestRead, computePendingState) and the store's selection
|
|
5518
|
+
* (docs/DESIGN-CONSOLIDATION.md, move 3b). */
|
|
5519
|
+
function visibleOverride(el) {
|
|
5520
|
+
return hasActiveOverride(el) && !unflushedOverride(el);
|
|
5521
|
+
}
|
|
5522
|
+
/** A derivation served the committed value because of an unflushed write
|
|
5523
|
+
* (A28) must run again in the flush that carries it — the late-linker case
|
|
5524
|
+
* (#3337's reason to defer the walk): it linked after the write walked. */
|
|
5525
|
+
function markLateLinker(c) {
|
|
5526
|
+
// The pass's own tail re-enqueues on this latch (recompute's finally) —
|
|
5527
|
+
// a direct enqueue here would be wiped by the pass's flag reset.
|
|
5528
|
+
c._flags |= REACTIVE_MISSED_WAKE;
|
|
4567
5529
|
return true;
|
|
4568
5530
|
}
|
|
5531
|
+
/** Companion-bearing nodes written outside a flush (setSignal); the flush
|
|
5532
|
+
* that carries their writes re-syncs their companions (A28). */
|
|
5533
|
+
const unflushedCompanions = [];
|
|
5534
|
+
function resyncUnflushedCompanions() {
|
|
5535
|
+
unflushedStaged = false;
|
|
5536
|
+
// Length-guarded: the common flush has nothing here, and must allocate nothing.
|
|
5537
|
+
// Length-guarded: the common flush has nothing here and allocates nothing.
|
|
5538
|
+
if (unflushedRewrites.length !== 0) {
|
|
5539
|
+
for (const el of unflushedRewrites) el._x._flushedStaged = NOT_PENDING;
|
|
5540
|
+
unflushedRewrites.length = 0;
|
|
5541
|
+
}
|
|
5542
|
+
if (promotedWrites.length !== 0) {
|
|
5543
|
+
for (const el of promotedWrites) el._config &= ~CONFIG_PROMOTED;
|
|
5544
|
+
promotedWrites.length = 0;
|
|
5545
|
+
}
|
|
5546
|
+
if (unflushedCompanions.length !== 0) {
|
|
5547
|
+
for (const el of unflushedCompanions)
|
|
5548
|
+
GlobalQueue._syncCompanions(
|
|
5549
|
+
el,
|
|
5550
|
+
el._pendingValue !== NOT_PENDING ? el._pendingValue : el._value
|
|
5551
|
+
);
|
|
5552
|
+
unflushedCompanions.length = 0;
|
|
5553
|
+
}
|
|
5554
|
+
}
|
|
4569
5555
|
function readNodeFast(el) {
|
|
4570
5556
|
if (
|
|
4571
5557
|
latestReadActive ||
|
|
@@ -4577,6 +5563,9 @@ function readNodeFast(el) {
|
|
|
4577
5563
|
activeTransition !== null ||
|
|
4578
5564
|
currentOptimisticLane !== null ||
|
|
4579
5565
|
snapshotCaptureActive ||
|
|
5566
|
+
// A28: a staged node read outside a flush may serve its committed value;
|
|
5567
|
+
// that arm lives on the slow path so this body stays inlinable.
|
|
5568
|
+
(unflushedStaged && el._pendingValue !== NOT_PENDING) ||
|
|
4580
5569
|
strictRead
|
|
4581
5570
|
)
|
|
4582
5571
|
return READ_SLOW;
|
|
@@ -4598,7 +5587,7 @@ function readNodeFast(el) {
|
|
|
4598
5587
|
c._config & CONFIG_CHILDREN_FORBIDDEN ||
|
|
4599
5588
|
(stale && heldFromStale(el, c))
|
|
4600
5589
|
? el._value
|
|
4601
|
-
: el._pendingValue;
|
|
5590
|
+
: (enterStagedRead(el), el._pendingValue);
|
|
4602
5591
|
}
|
|
4603
5592
|
function read(el) {
|
|
4604
5593
|
// Handle latest() mode: read from _latestValueComputed
|
|
@@ -4627,6 +5616,7 @@ function read(el) {
|
|
|
4627
5616
|
activeTransition === null &&
|
|
4628
5617
|
currentOptimisticLane === null &&
|
|
4629
5618
|
!snapshotCaptureActive &&
|
|
5619
|
+
(!unflushedStaged || el._pendingValue === NOT_PENDING) && // A28, see readNodeFast
|
|
4630
5620
|
!strictRead
|
|
4631
5621
|
) {
|
|
4632
5622
|
if (c && tracking) link(el, c);
|
|
@@ -4637,7 +5627,7 @@ function read(el) {
|
|
|
4637
5627
|
c._config & CONFIG_CHILDREN_FORBIDDEN ||
|
|
4638
5628
|
(stale && heldFromStale(el, c))
|
|
4639
5629
|
? el._value
|
|
4640
|
-
: el._pendingValue;
|
|
5630
|
+
: (enterStagedRead(el), el._pendingValue);
|
|
4641
5631
|
}
|
|
4642
5632
|
// The dev component-body safeguard (#2897) must not fire inside an
|
|
4643
5633
|
// isPending() probe: its plain Error would be swallowed by the probe's
|
|
@@ -4724,7 +5714,10 @@ function read(el) {
|
|
|
4724
5714
|
// lane (#3276); that check rides laneSuspends so floor bundles don't
|
|
4725
5715
|
// pay for it.
|
|
4726
5716
|
if (currentOptimisticLane === null || GlobalQueue._laneSuspends(owner)) {
|
|
4727
|
-
|
|
5717
|
+
// An untracked read of a pending node still re-runs its reader when
|
|
5718
|
+
// the node settles — unless nobody is reading (`spectating`, #3528)
|
|
5719
|
+
// or the reader is the node itself.
|
|
5720
|
+
if (!tracking && !spectating && el !== c) link(el, c);
|
|
4728
5721
|
throw owner._x?._error;
|
|
4729
5722
|
}
|
|
4730
5723
|
} else if (!c && owner._statusFlags & STATUS_UNINITIALIZED) {
|
|
@@ -4760,7 +5753,48 @@ function read(el) {
|
|
|
4760
5753
|
ownerName: c?._name,
|
|
4761
5754
|
nodeName: owner?._name
|
|
4762
5755
|
});
|
|
4763
|
-
|
|
5756
|
+
const value = serve(el, c, owner, el._value);
|
|
5757
|
+
if (
|
|
5758
|
+
!c &&
|
|
5759
|
+
owner === el &&
|
|
5760
|
+
typeof computed._fn === "function" &&
|
|
5761
|
+
el._config & CONFIG_AUTO_DISPOSE &&
|
|
5762
|
+
!(owner._statusFlags & STATUS_PENDING) &&
|
|
5763
|
+
!el._subs &&
|
|
5764
|
+
// An untracked read served a visible override returned before this
|
|
5765
|
+
// sweep registration when the arm was inline; keep that.
|
|
5766
|
+
!visibleOverride(el)
|
|
5767
|
+
) {
|
|
5768
|
+
// Deferred, not inline (#3078): an inline unobserved() here made untracked
|
|
5769
|
+
// reads destructive — dispose on this read, full revival recompute on the
|
|
5770
|
+
// next — so consecutive reads could answer differently with no write in
|
|
5771
|
+
// between (the revival samples the ambient transition/lane context).
|
|
5772
|
+
// The sweep at flush finalization re-validates and reclaims; schedule()
|
|
5773
|
+
// guarantees that flush happens even if nothing else is queued.
|
|
5774
|
+
dormantNodes.add(el);
|
|
5775
|
+
schedule();
|
|
5776
|
+
}
|
|
5777
|
+
return value;
|
|
5778
|
+
}
|
|
5779
|
+
/**
|
|
5780
|
+
* Rule 1, the one slow implementation (DESIGN-CONSOLIDATION move 3b, step
|
|
5781
|
+
* 6c): the value a reader `c` (null = untracked, no pass) is served from
|
|
5782
|
+
* `el`, whose committed value is `committed` — the node's own `_value` for
|
|
5783
|
+
* a signal or memo, the BACKING for a store property node (single-home rule,
|
|
5784
|
+
* O6: committed truth lives in the backing and a node's `_value` is never
|
|
5785
|
+
* served for one). Called by read()'s slow tail and by the store's untracked
|
|
5786
|
+
* node path (nodeValue); the fast paths (readNodeFast, read's fast block)
|
|
5787
|
+
* keep their trivial ternary by design (perf, see the doc). Arms, in order:
|
|
5788
|
+
* - the override (A17), routed through the engine for a tracked reader under
|
|
5789
|
+
* a lane or a supersession (A18), an authoritative reader marked instead;
|
|
5790
|
+
* - the lane entanglement gate (committed, recorded for replay);
|
|
5791
|
+
* - a node born held has nothing for an untracked reader (A19 exception 1);
|
|
5792
|
+
* - an unflushed write serves committed and re-runs the reader in the
|
|
5793
|
+
* carrying flush (A28);
|
|
5794
|
+
* - readerSeesCommitted, else the staged value and the transaction (A29).
|
|
5795
|
+
*/
|
|
5796
|
+
function serve(el, c, owner, committed) {
|
|
5797
|
+
if (hasActiveOverride(el)) {
|
|
4764
5798
|
// A17: the override IS the value for every reader — except an authoritative
|
|
4765
5799
|
// reader (until()'s predicate carries CONFIG_AUTHORITATIVE_READ): it must
|
|
4766
5800
|
// observe independently-arriving truth, and serving it the caller's own
|
|
@@ -4771,13 +5805,18 @@ function read(el) {
|
|
|
4771
5805
|
// authoritative — optimism never lives there); the sticky mark makes the
|
|
4772
5806
|
// A17-silent "landing equals override" paths notify this node's subs so
|
|
4773
5807
|
// the reader re-runs when truth arrives.
|
|
4774
|
-
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
//
|
|
4779
|
-
//
|
|
4780
|
-
|
|
5808
|
+
// A28 (5): an optimistic write written this tick, outside a flush, is not
|
|
5809
|
+
// yet the active override — fall through to the normal selection (the
|
|
5810
|
+
// authoritative mark below still applies: until() must wake on landing).
|
|
5811
|
+
if (!(c && c._config & CONFIG_AUTHORITATIVE_READ) && !unflushedOverride(el)) {
|
|
5812
|
+
// A tracked read of an override is the engine's selection (a lane or a
|
|
5813
|
+
// supersession implies the engine): a render effect OFF the override's
|
|
5814
|
+
// held lane sees the committed value (#3460, lanes mirror transitions);
|
|
5815
|
+
// a node whose own source answered with a DIFFERENT value hands a
|
|
5816
|
+
// tracked reader the staged truth (A18 supersession, #3331). Untracked
|
|
5817
|
+
// reads display the override.
|
|
5818
|
+
if (c && el._config & (CONFIG_HAS_LANE | CONFIG_OVERRIDE_SUPERSEDED))
|
|
5819
|
+
return GlobalQueue._overrideRead(el, c);
|
|
4781
5820
|
return unwrapOverride(el._x?._overrideValue);
|
|
4782
5821
|
}
|
|
4783
5822
|
el._config |= CONFIG_AUTHORITATIVE_OBSERVED;
|
|
@@ -4795,7 +5834,7 @@ function read(el) {
|
|
|
4795
5834
|
c !== null &&
|
|
4796
5835
|
GlobalQueue._gatedRead(el, owner, c)
|
|
4797
5836
|
) {
|
|
4798
|
-
return
|
|
5837
|
+
return committed;
|
|
4799
5838
|
}
|
|
4800
5839
|
// In optimistic lane context, return _value for optimistic/lane-assigned signals
|
|
4801
5840
|
// and for regular signals in stale mode (render effects). Non-stale readers (user
|
|
@@ -4803,58 +5842,40 @@ function read(el) {
|
|
|
4803
5842
|
// (The lane-context clause lives with the engine.) Children-forbidden readers
|
|
4804
5843
|
// (createTrackedEffect / onSettled callbacks) get committed visibility — see
|
|
4805
5844
|
// readNodeFast (#3006).
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
|
|
4816
|
-
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
|
|
4822
|
-
!(c._config & CONFIG_AUTHORITATIVE_READ))
|
|
4823
|
-
? el._value
|
|
4824
|
-
: el._pendingValue;
|
|
5845
|
+
// A node born held (recompute) has a staged value and no committed one: a
|
|
5846
|
+
// stale reader cannot fall back to the committed frame, so it takes the
|
|
5847
|
+
// staged value and enters like a tracked reader (A29); an untracked reader
|
|
5848
|
+
// has nothing to serve and holds (A19 exception 1).
|
|
5849
|
+
const noCommitted =
|
|
5850
|
+
el._pendingValue !== NOT_PENDING && (el._statusFlags & STATUS_UNINITIALIZED) !== 0;
|
|
5851
|
+
if (noCommitted && !c) throw new NotReadyError(null);
|
|
5852
|
+
const u = c && unflushedStaged ? unflushedValue(el, committed) : NOT_PENDING;
|
|
5853
|
+
if (u !== NOT_PENDING) {
|
|
5854
|
+
markLateLinker(c);
|
|
5855
|
+
if (pendingCheckActive) GlobalQueue._recordFresh(el, u);
|
|
5856
|
+
return u;
|
|
5857
|
+
}
|
|
5858
|
+
const value = readerSeesCommitted(el, c, owner, noCommitted)
|
|
5859
|
+
? committed
|
|
5860
|
+
: (enterStagedRead(el), el._pendingValue);
|
|
4825
5861
|
// Record that this isPending() probe observed the fresh pending value, so
|
|
4826
5862
|
// the probe doesn't pair "pending" with the new value (#2831).
|
|
4827
5863
|
if (pendingCheckActive) GlobalQueue._recordFresh(el, value);
|
|
4828
|
-
if (
|
|
4829
|
-
!c &&
|
|
4830
|
-
owner === el &&
|
|
4831
|
-
typeof computed._fn === "function" &&
|
|
4832
|
-
el._config & CONFIG_AUTO_DISPOSE &&
|
|
4833
|
-
!(owner._statusFlags & STATUS_PENDING) &&
|
|
4834
|
-
!el._subs
|
|
4835
|
-
) {
|
|
4836
|
-
// Deferred, not inline (#3078): an inline unobserved() here made untracked
|
|
4837
|
-
// reads destructive — dispose on this read, full revival recompute on the
|
|
4838
|
-
// next — so consecutive reads could answer differently with no write in
|
|
4839
|
-
// between (the revival samples the ambient transition/lane context).
|
|
4840
|
-
// The sweep at flush finalization re-validates and reclaims; schedule()
|
|
4841
|
-
// guarantees that flush happens even if nothing else is queued.
|
|
4842
|
-
dormantNodes.add(el);
|
|
4843
|
-
schedule();
|
|
4844
|
-
}
|
|
4845
5864
|
return value;
|
|
4846
5865
|
}
|
|
4847
5866
|
/**
|
|
4848
5867
|
* Store-rewrite setter guard: the rewrite parks writes in a pending backing
|
|
4849
5868
|
* (no setSignal at write time), so the owned-scope write protection must
|
|
4850
|
-
* fire at the setter entry instead.
|
|
5869
|
+
* fire at the setter entry instead. Exactly setSignal's guard condition
|
|
4851
5870
|
* minus the node-specific exemptions (ownedWrite/firewall), which don't
|
|
4852
|
-
* apply to plain store setters.
|
|
5871
|
+
* apply to plain store setters. Roots are NOT exempt (#3500): a root body is
|
|
5872
|
+
* tree construction — every dev component body, every context Provider, the
|
|
5873
|
+
* top of `render()`, and the whole SSR pass run directly under one — and a
|
|
5874
|
+
* write there re-runs what already read the old value (or on the server,
|
|
5875
|
+
* can't). Same rule as setSignal, which never exempted roots.
|
|
4853
5876
|
*/
|
|
4854
5877
|
function devGuardStoreSetterWrite() {
|
|
4855
|
-
|
|
4856
|
-
// are legal (legacy parity; the guard targets computed/effect bodies).
|
|
4857
|
-
if (context && !context._root && !(context._config & CONFIG_CHILDREN_FORBIDDEN)) {
|
|
5878
|
+
if (context && !(context._config & CONFIG_CHILDREN_FORBIDDEN)) {
|
|
4858
5879
|
emitDiagnostic({
|
|
4859
5880
|
code: "REACTIVE_WRITE_IN_OWNED_SCOPE",
|
|
4860
5881
|
kind: "write",
|
|
@@ -4869,12 +5890,59 @@ function devGuardStoreSetterWrite() {
|
|
|
4869
5890
|
throw new Error(ownedScopeWriteMessage(context));
|
|
4870
5891
|
}
|
|
4871
5892
|
}
|
|
5893
|
+
/**
|
|
5894
|
+
* Store setter result guard: the callback's return has one meaning — a
|
|
5895
|
+
* replacement root to adopt — and a thenable can never be that. It is the
|
|
5896
|
+
* signature of `setStore(async d => …)` (or a sync arrow whose helper is
|
|
5897
|
+
* async): only the writes before the first `await` were in the transaction;
|
|
5898
|
+
* the rest land on a closed draft and vanish. Setters are synchronous
|
|
5899
|
+
* transactions; async orchestration is `action()`'s job. Store-specific —
|
|
5900
|
+
* a signal may legitimately hold a promise, so its setter has no such rule.
|
|
5901
|
+
*/
|
|
5902
|
+
function devGuardStoreSetterResult(result) {
|
|
5903
|
+
if (result == null) return;
|
|
5904
|
+
if (
|
|
5905
|
+
(typeof result === "object" || typeof result === "function") &&
|
|
5906
|
+
typeof result.then === "function"
|
|
5907
|
+
) {
|
|
5908
|
+
emitDiagnostic({
|
|
5909
|
+
code: "ASYNC_STORE_SETTER",
|
|
5910
|
+
kind: "write",
|
|
5911
|
+
severity: "error",
|
|
5912
|
+
message: ASYNC_STORE_SETTER_MESSAGE,
|
|
5913
|
+
ownerId: context?.id,
|
|
5914
|
+
ownerName: context?._name,
|
|
5915
|
+
data: { operation: "setStore" }
|
|
5916
|
+
});
|
|
5917
|
+
throw new Error(ASYNC_STORE_SETTER_MESSAGE);
|
|
5918
|
+
}
|
|
5919
|
+
}
|
|
4872
5920
|
function ownedScopeWriteMessage(owner) {
|
|
4873
5921
|
const name = owner._name;
|
|
4874
5922
|
return name
|
|
4875
5923
|
? `${REACTIVE_WRITE_IN_OWNED_SCOPE_SIGNAL_MESSAGE} (in ${name})`
|
|
4876
5924
|
: REACTIVE_WRITE_IN_OWNED_SCOPE_SIGNAL_MESSAGE;
|
|
4877
5925
|
}
|
|
5926
|
+
/** A28 — a rewrite of a HELD node outside a flush keeps the staged value the
|
|
5927
|
+
* last flush left, for latest()/verdicts, until the next flush carries it
|
|
5928
|
+
* (stash, cleared at that flush). Cold: only stamped nodes reach here. */
|
|
5929
|
+
function stashHeldRewrite(el) {
|
|
5930
|
+
if (globalQueue._running) return;
|
|
5931
|
+
const x = ext(el);
|
|
5932
|
+
if (x._flushedStaged === NOT_PENDING) {
|
|
5933
|
+
x._flushedStaged = el._pendingValue;
|
|
5934
|
+
unflushedRewrites.push(el);
|
|
5935
|
+
unflushedStaged = true;
|
|
5936
|
+
}
|
|
5937
|
+
}
|
|
5938
|
+
/** A28 (4) — written inside a recompute that runs OUTSIDE a flush (a
|
|
5939
|
+
* creation-time compute): promoted at that pass's end, visible to the rest of
|
|
5940
|
+
* the block. Cold: only contextual writes reach here. */
|
|
5941
|
+
function notePromotedWrite(el) {
|
|
5942
|
+
if (globalQueue._running || el._config & CONFIG_PROMOTED) return;
|
|
5943
|
+
el._config |= CONFIG_PROMOTED;
|
|
5944
|
+
promotedWrites.push(el);
|
|
5945
|
+
}
|
|
4878
5946
|
function setSignal(el, v) {
|
|
4879
5947
|
if (
|
|
4880
5948
|
!(el._config & CONFIG_OWNED_WRITE) &&
|
|
@@ -4894,8 +5962,22 @@ function setSignal(el, v) {
|
|
|
4894
5962
|
});
|
|
4895
5963
|
throw new Error(ownedScopeWriteMessage(context));
|
|
4896
5964
|
}
|
|
4897
|
-
|
|
4898
|
-
|
|
5965
|
+
// A write to a held node is a second proposal on a contested node (A34, #3494):
|
|
5966
|
+
// the writer's tick reveals with the hold, the same value or another. Inside
|
|
5967
|
+
// a flush the round enters now; from mainline the entry waits for the next
|
|
5968
|
+
// flush's start (batchJoins) — entering here left activeTransition set for
|
|
5969
|
+
// the rest of the caller's block, so a memo created after the write became
|
|
5970
|
+
// the transaction's instead of mainline's (A28, A29). Before the equality
|
|
5971
|
+
// gate below: repeating the held value proposes it too — and that repeat
|
|
5972
|
+
// leaves through the gate, so the join schedules its own flush here; left
|
|
5973
|
+
// for the next flush to find, it adopted an unrelated tick (#3519 review).
|
|
5974
|
+
if (el._transition && activeTransition !== el._transition) {
|
|
5975
|
+
if (globalQueue._running) globalQueue.initTransition(el._transition);
|
|
5976
|
+
else {
|
|
5977
|
+
batchJoins.push(el._transition); // dupes: a bare return in initTransition
|
|
5978
|
+
schedule();
|
|
5979
|
+
}
|
|
5980
|
+
}
|
|
4899
5981
|
// The optimistic write path lives with the engine: only optimisticSignal /
|
|
4900
5982
|
// optimisticComputed callers and optimistic store nodes carry an
|
|
4901
5983
|
// _overrideValue slot (flagged by CONFIG_OPTIMISTIC — a masked read of the
|
|
@@ -4919,15 +6001,25 @@ function setSignal(el, v) {
|
|
|
4919
6001
|
if (attrHooks !== null) attrHooks.write(el, currentValue, v);
|
|
4920
6002
|
const wasStaged = el._pendingValue !== NOT_PENDING;
|
|
4921
6003
|
if (!wasStaged) queuePendingNode(el);
|
|
6004
|
+
// A28 arms, gated on the loads the write already pays for (a plain ambient
|
|
6005
|
+
// rewrite outside a flush — the write-loop shape — costs `_transition` and
|
|
6006
|
+
// `context` here and nothing else; the arms themselves are cold helpers so
|
|
6007
|
+
// setSignal stays within every setter's inlining budget, ~300 B bytecode).
|
|
6008
|
+
else if (el._transition !== null) stashHeldRewrite(el);
|
|
4922
6009
|
el._pendingValue = v;
|
|
6010
|
+
if (context !== null) notePromotedWrite(el);
|
|
4923
6011
|
// syncCompanions only pokes _pendingSignal/_latestValueComputed — with
|
|
4924
6012
|
// neither companion present the call is a guaranteed no-op (companions are
|
|
4925
6013
|
// only ever created, never removed, and creating one installs the hook and
|
|
4926
6014
|
// sets CONFIG_HAS_COMPANIONS — one masked read replaces two optional-field
|
|
4927
6015
|
// probes on every write).
|
|
4928
|
-
el._config & CONFIG_HAS_COMPANIONS &&
|
|
4929
|
-
GlobalQueue._syncCompanions !== null &&
|
|
6016
|
+
if (el._config & CONFIG_HAS_COMPANIONS && GlobalQueue._syncCompanions !== null) {
|
|
4930
6017
|
GlobalQueue._syncCompanions(el, v);
|
|
6018
|
+
// A28 (2): the verdict computed here is the PRE-flush one (an unflushed
|
|
6019
|
+
// write is not pending); the flush that carries the write re-syncs so
|
|
6020
|
+
// the companions mirror the flushed world. Only companion nodes pay.
|
|
6021
|
+
if (!globalQueue._running) unflushedCompanions.push(el);
|
|
6022
|
+
}
|
|
4931
6023
|
// _time is a computed-only slot (§12e): writing it on a signal would fork
|
|
4932
6024
|
// the lean shape. Every read site is computed-typed.
|
|
4933
6025
|
if (el._fn !== undefined) el._time = clock;
|
|
@@ -5091,131 +6183,153 @@ function markRefresh(node) {
|
|
|
5091
6183
|
}
|
|
5092
6184
|
|
|
5093
6185
|
export {
|
|
5094
|
-
|
|
5095
|
-
|
|
5096
|
-
|
|
6186
|
+
insertIntoHeap as $,
|
|
6187
|
+
laneHeld as A,
|
|
6188
|
+
CONFIG_AUTHORITATIVE_OBSERVED as B,
|
|
5097
6189
|
ContextNotFoundError as C,
|
|
5098
|
-
|
|
6190
|
+
findLane as D,
|
|
5099
6191
|
EFFECT_RENDER as E,
|
|
5100
|
-
|
|
6192
|
+
sourceObserved as F,
|
|
5101
6193
|
GlobalQueue as G,
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
6194
|
+
resolveLane as H,
|
|
6195
|
+
stale as I,
|
|
6196
|
+
readsHeldCommitted as J,
|
|
6197
|
+
enterStagedRead as K,
|
|
6198
|
+
currentTransition as L,
|
|
6199
|
+
queuePendingNode as M,
|
|
5108
6200
|
NoOwnerError as N,
|
|
5109
6201
|
OVERRIDE_UNDEFINED as O,
|
|
5110
|
-
|
|
5111
|
-
|
|
6202
|
+
latestReadActive as P,
|
|
6203
|
+
REACTIVE_MANUAL_WRITE as Q,
|
|
5112
6204
|
REACTIVE_DIRTY as R,
|
|
5113
6205
|
STATUS_UNINITIALIZED as S,
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
|
|
6206
|
+
currentOptimisticLane as T,
|
|
6207
|
+
REACTIVE_OPTIMISTIC_DIRTY as U,
|
|
6208
|
+
assignOrMergeLane as V,
|
|
6209
|
+
LANE_RUN as W,
|
|
6210
|
+
attrHooks as X,
|
|
6211
|
+
EFFECT_USER as Y,
|
|
6212
|
+
REACTIVE_DISPOSED as Z,
|
|
6213
|
+
dispose as _,
|
|
5122
6214
|
setSignal as a,
|
|
5123
|
-
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
|
|
5127
|
-
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
|
|
5131
|
-
|
|
5132
|
-
|
|
5133
|
-
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
|
|
5137
|
-
|
|
5138
|
-
|
|
5139
|
-
|
|
5140
|
-
|
|
5141
|
-
|
|
5142
|
-
|
|
5143
|
-
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
|
|
5158
|
-
|
|
5159
|
-
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
|
|
5167
|
-
|
|
5168
|
-
|
|
5169
|
-
|
|
5170
|
-
|
|
5171
|
-
|
|
5172
|
-
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
6215
|
+
TimeoutError as a$,
|
|
6216
|
+
queueFor as a0,
|
|
6217
|
+
visibleOverride as a1,
|
|
6218
|
+
unflushedValue as a2,
|
|
6219
|
+
context as a3,
|
|
6220
|
+
REACTIVE_RECOMPUTING_DEPS as a4,
|
|
6221
|
+
markLateLinker as a5,
|
|
6222
|
+
prepareComputed as a6,
|
|
6223
|
+
tracking as a7,
|
|
6224
|
+
link as a8,
|
|
6225
|
+
enqueueSub as a9,
|
|
6226
|
+
staleValues as aA,
|
|
6227
|
+
CONFIG_AUTO_DISPOSE as aB,
|
|
6228
|
+
CONFIG_CHILDREN_FORBIDDEN as aC,
|
|
6229
|
+
EFFECT_TRACKED as aD,
|
|
6230
|
+
setEffectCallback as aE,
|
|
6231
|
+
setTrackedQueueCallback as aF,
|
|
6232
|
+
_hitUnhandledAsync as aG,
|
|
6233
|
+
resetUnhandledAsync as aH,
|
|
6234
|
+
setOrigin as aI,
|
|
6235
|
+
isThenable as aJ,
|
|
6236
|
+
actionStepDepth as aK,
|
|
6237
|
+
flush as aL,
|
|
6238
|
+
enterActionStep as aM,
|
|
6239
|
+
exitActionStep as aN,
|
|
6240
|
+
ROOT_ERROR_HOOK as aO,
|
|
6241
|
+
registerGraph as aP,
|
|
6242
|
+
runWithOwner as aQ,
|
|
6243
|
+
setMemo as aR,
|
|
6244
|
+
dirtyQueue as aS,
|
|
6245
|
+
$REFRESH as aT,
|
|
6246
|
+
installAuthoritativeRead as aU,
|
|
6247
|
+
markRefresh as aV,
|
|
6248
|
+
createRoot as aW,
|
|
6249
|
+
getObserver as aX,
|
|
6250
|
+
CONFIG_DIRECT_COMMIT as aY,
|
|
6251
|
+
CONFIG_AUTHORITATIVE_READ as aZ,
|
|
6252
|
+
entangleConfirmingTransitions as a_,
|
|
6253
|
+
setLatestReadActive as aa,
|
|
6254
|
+
setContextInternal as ab,
|
|
6255
|
+
optimisticComputed as ac,
|
|
6256
|
+
CONFIG_HAS_COMPANIONS as ad,
|
|
6257
|
+
unflushed as ae,
|
|
6258
|
+
CONFIG_ADOPTED_UNFLUSHED as af,
|
|
6259
|
+
CONFIG_CHILD_COMPANIONS as ag,
|
|
6260
|
+
runAsTransitionBatch as ah,
|
|
6261
|
+
unflushedCompanions as ai,
|
|
6262
|
+
setPendingCheckActive as aj,
|
|
6263
|
+
setStrictRead as ak,
|
|
6264
|
+
strictRead as al,
|
|
6265
|
+
optimisticSignal as am,
|
|
6266
|
+
activeAffectsMarks as an,
|
|
6267
|
+
pendingCheckActive as ao,
|
|
6268
|
+
STATUS_ERROR as ap,
|
|
6269
|
+
setEffectStatusNotify as aq,
|
|
6270
|
+
unwrapStatusError as ar,
|
|
6271
|
+
haltReactivity as as,
|
|
6272
|
+
StatusError as at,
|
|
6273
|
+
trimStaleDeps as au,
|
|
6274
|
+
createEffectNode as av,
|
|
6275
|
+
recompute as aw,
|
|
6276
|
+
reportDiagnostic as ax,
|
|
6277
|
+
emitDiagnostic as ay,
|
|
6278
|
+
computed as az,
|
|
5187
6279
|
NOT_PENDING as b,
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
|
|
5199
|
-
|
|
5200
|
-
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
6280
|
+
untrack as b0,
|
|
6281
|
+
Queue as b1,
|
|
6282
|
+
CONFIG_FRESH_READ as b2,
|
|
6283
|
+
forEachDependent as b3,
|
|
6284
|
+
statusNotifierOf as b4,
|
|
6285
|
+
shiftAffectsMarks as b5,
|
|
6286
|
+
SUPPORTS_PROXY as b6,
|
|
6287
|
+
setSlotUnobserved as b7,
|
|
6288
|
+
devGuardStoreSetterWrite as b8,
|
|
6289
|
+
devGuardStoreSetterResult as b9,
|
|
6290
|
+
clearSnapshots as bA,
|
|
6291
|
+
enforceLoadingBoundary as bB,
|
|
6292
|
+
getNextChildId as bC,
|
|
6293
|
+
isDisposed as bD,
|
|
6294
|
+
markSnapshotScope as bE,
|
|
6295
|
+
ownerPath as bF,
|
|
6296
|
+
peekNextChildId as bG,
|
|
6297
|
+
releaseSnapshotScope as bH,
|
|
6298
|
+
resetErrorHalt as bI,
|
|
6299
|
+
setSnapshotCapture as bJ,
|
|
6300
|
+
setAttributionHooks as bK,
|
|
6301
|
+
isSuppressed as bL,
|
|
6302
|
+
recordSubject as bM,
|
|
6303
|
+
anyExcluded as bN,
|
|
6304
|
+
isExcluded as bO,
|
|
6305
|
+
GRAPH_SIZE_WARN_AT as bP,
|
|
6306
|
+
subjectOf as bQ,
|
|
6307
|
+
projectionWriteActive as ba,
|
|
6308
|
+
setProjectionWriteActive as bb,
|
|
6309
|
+
DEV as bc,
|
|
6310
|
+
isEqual as bd,
|
|
6311
|
+
readNodeFast as be,
|
|
6312
|
+
READ_SLOW as bf,
|
|
6313
|
+
throwPendingUntrackedRead as bg,
|
|
6314
|
+
warnStrictReadUntracked as bh,
|
|
6315
|
+
unlinkFirewallChild as bi,
|
|
6316
|
+
CONFIG_OWNED_WRITE as bj,
|
|
6317
|
+
ownsHold as bk,
|
|
6318
|
+
deferSlotRelease as bl,
|
|
6319
|
+
slotSignal as bm,
|
|
6320
|
+
serve as bn,
|
|
6321
|
+
recordStaleReplay as bo,
|
|
6322
|
+
setStoreCommitHook as bp,
|
|
6323
|
+
suppressComputedRecompute as bq,
|
|
6324
|
+
handleAsync as br,
|
|
6325
|
+
createTransition as bs,
|
|
6326
|
+
CONFIG_HELD_TRUTH as bt,
|
|
6327
|
+
createOwner as bu,
|
|
6328
|
+
spectate as bv,
|
|
6329
|
+
transitions as bw,
|
|
6330
|
+
wakeParked as bx,
|
|
6331
|
+
REACTIVE_ZOMBIE as by,
|
|
6332
|
+
OBSERVE as bz,
|
|
5219
6333
|
cleanup as c,
|
|
5220
6334
|
REACTIVE_CHECK as d,
|
|
5221
6335
|
resolveTransition as e,
|
|
@@ -5230,14 +6344,14 @@ export {
|
|
|
5230
6344
|
schedule as n,
|
|
5231
6345
|
origin as o,
|
|
5232
6346
|
STATUS_PENDING as p,
|
|
5233
|
-
|
|
6347
|
+
CONFIG_DERIVED_OVERRIDE as q,
|
|
5234
6348
|
read as r,
|
|
5235
6349
|
signal as s,
|
|
5236
|
-
|
|
6350
|
+
CONFIG_OPTIMISTIC as t,
|
|
5237
6351
|
unwrapOverride as u,
|
|
5238
|
-
|
|
5239
|
-
|
|
5240
|
-
|
|
5241
|
-
|
|
5242
|
-
|
|
6352
|
+
CONFIG_OVERRIDE_SUPERSEDED as v,
|
|
6353
|
+
hasActiveOverride as w,
|
|
6354
|
+
NotReadyError as x,
|
|
6355
|
+
activeLanes as y,
|
|
6356
|
+
signalLanes as z
|
|
5243
6357
|
};
|