fix(signals): give the latest() shadow companion ownedWrite (#3378) - #3397
Conversation
…3378) A companion sync is internal plumbing that can run from inside a computation: a transition-held memo recompute pulled mid-tick by a reader creating or refreshing its latest() shadow syncs that shadow with `context` set to the pulling node. The isPending signal companion is created with `ownedWrite: true` for exactly this reason; the shadow was not, so dev halted with REACTIVE_WRITE_IN_OWNED_SCOPE when a JSX branch reading latest(memo) was toggled off while an action was pending and restored as the action resumed. Production never threw (the guard is dev-only) and the write path is unchanged. Regression tests cover both paths at the signals level (a fresh shadow created mid-pull, and a live shadow pulled current by a mounting reader) and the issue's JSX shape in the web package. Fixes solidjs#3378 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: b360d01 The changes in this PR will be included in the next version bump. This PR includes changesets to release 11 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Merging this PR will improve performance by 54.71%
Performance Changes
Tip Curious why performance improved? Comment Comparing |
|
ryansolid
left a comment
There was a problem hiding this comment.
Reviewed against an independent root-cause of #3378 that landed on the same one-line change (
optimisticComputed(() => read(el), { ownedWrite: true })), so the fix is corroborated:recomputerestores the caller'scontextbefore its held-branchsyncCompanions, so a pull-recompute of the memo from inside an effect writes thelatest()shadow with that effect ascontext; the pending signal already carriedownedWrite, the shadow was the one companion that didn't. Dev-only guard, no behavior change in prod, verdict layer only (no core-floor cost).The two signals variants (fresh shadow / live shadow pulled current) plus the JSX-shape web test cover more than the issue's exact route, so I've dropped the overlapping pin from the companion PR (#3400 now carries only #3379) and referenced this PR in the internals doc's
syncCompanionsnotes.— Claude via Cursor
Fixes #3378
Root cause
latest()'s shadow companion was the one internal companion created withoutownedWrite. A companion sync can run from inside a computation: a transition-held memo recompute pulled mid-tick by a reader creating or refreshing itslatest()shadow reachessyncCompanions->setSignal(shadow)withcontextset to the pulling node. TheisPendingsignal companion already carries the flag, so only the shadow write tripped the dev owned-scope guard and halted the app. Production never threw (the guard is dev-only) and the write path is unchanged.The issue's shape: a JSX branch reading
latest(memo)is toggled off while an action is pending (its shadow goes dormant with the reader) and restored as the action resumes. The restored reader runs before the memo's heap slot, so the fresh shadow's first compute pulls the still-dirty memo.Change
getLatestValueComputedcreates the shadow with{ ownedWrite: true }, matchinggetPendingSignal.Tests
packages/signals/tests/latest-shadow-owned-write.test.ts: both paths (a fresh shadow created mid-pull, and a live shadow pulled current by a mounting reader). Both fail onnextwithREACTIVE_WRITE_IN_OWNED_SCOPE.packages/web/test/latest-shadow-owned-write-issue-3378.spec.tsx: the issue's JSX shape with an action; reproduces the halt onnext.🤖 Generated with Claude Code