Background
_latestValueComputed (the shadow node behind latest()) is currently both recompute-driven and write-driven: it is an optimisticComputed(() => read(el)) that reacts through the graph, and every write path (setSignal, async resolution, transition-held sync recompute) force-writes it via syncCompanions().
This dual nature was the root cause of the #2829/#2831 bug cluster — the shadow recomputing under whatever ambient stale/lane posture happened to be active is why the following patches exist:
- the
_parentSource !== el exemption in read()'s value-selection ternary
- the
STATUS_UNINITIALIZED clearing in resolveOptimisticNodes
- the
NotReadyError catch in read()'s latest branch
Each is correct, but they are all compensations for the same underlying tension.
Proposal
Make the shadow purely write-driven: a plain optimistic signal fed exclusively by syncCompanions(), never recomputing on its own. The "shadow recomputed under the wrong context" bug class disappears structurally, and the read-path exemptions above should become deletable.
Costs to account for:
- The shadow's compute currently provides initialization (first
latest() read) and NotReady propagation (the shadow's own STATUS_PENDING is what isPending(() => latest(x)) keys off). Both need explicit handling in the write/status paths instead.
- The parent-child lane machinery in
lanes.ts (_parentLane, the independence rules in assignOrMergeLane) exists largely to serve the companions and should shrink as part of this.
Timing
Beta-period work, targeted after the upcoming beta release — the area just stabilized against three real-world reports, and the regression coverage in packages/solid-signals/tests/latest-isPending-consistency.test.ts and tests/latest-async.test.ts is what makes this refactor safe to attempt.
Refs #2829, #2831.
Background
_latestValueComputed(the shadow node behindlatest()) is currently both recompute-driven and write-driven: it is anoptimisticComputed(() => read(el))that reacts through the graph, and every write path (setSignal, async resolution, transition-held sync recompute) force-writes it viasyncCompanions().This dual nature was the root cause of the #2829/#2831 bug cluster — the shadow recomputing under whatever ambient stale/lane posture happened to be active is why the following patches exist:
_parentSource !== elexemption inread()'s value-selection ternarySTATUS_UNINITIALIZEDclearing inresolveOptimisticNodesNotReadyErrorcatch inread()'slatestbranchEach is correct, but they are all compensations for the same underlying tension.
Proposal
Make the shadow purely write-driven: a plain optimistic signal fed exclusively by
syncCompanions(), never recomputing on its own. The "shadow recomputed under the wrong context" bug class disappears structurally, and the read-path exemptions above should become deletable.Costs to account for:
latest()read) and NotReady propagation (the shadow's ownSTATUS_PENDINGis whatisPending(() => latest(x))keys off). Both need explicit handling in the write/status paths instead.lanes.ts(_parentLane, the independence rules inassignOrMergeLane) exists largely to serve the companions and should shrink as part of this.Timing
Beta-period work, targeted after the upcoming beta release — the area just stabilized against three real-world reports, and the regression coverage in
packages/solid-signals/tests/latest-isPending-consistency.test.tsandtests/latest-async.test.tsis what makes this refactor safe to attempt.Refs #2829, #2831.