Skip to content

fix(signals): give the latest() shadow companion ownedWrite (#3378) - #3397

Merged
ryansolid merged 1 commit into
solidjs:nextfrom
brenelz:fix/latest-shadow-owned-write
Sep 12, 2026
Merged

ryansolid merged 1 commit into
solidjs:nextfrom
brenelz:fix/latest-shadow-owned-write

Conversation

@brenelz

@brenelz brenelz commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

Fixes #3378

Root cause

latest()'s shadow companion was the one internal companion created without ownedWrite. A companion sync can run from inside a computation: a transition-held memo recompute pulled mid-tick by a reader creating or refreshing its latest() shadow reaches syncCompanions -> setSignal(shadow) with context set to the pulling node. The isPending signal 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

  • getLatestValueComputed creates the shadow with { ownedWrite: true }, matching getPendingSignal.

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 on next with REACTIVE_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 on next.
  • Full signals and web suites pass.

🤖 Generated with Claude Code

…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-bot

changeset-bot Bot commented Sep 12, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: b360d01

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 11 packages
Name Type
@solidjs/signals Patch
test-integration Patch
@solidjs/web Patch
@solidjs/babel-plugin Patch
@solidjs/compiler Patch
@solidjs/diagnostics Patch
@solidjs/element Patch
@solidjs/h Patch
@solidjs/html Patch
solid-js Patch
@solidjs/universal Patch

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

@codspeed

codspeed Bot commented Sep 12, 2026

Copy link
Copy Markdown

Merging this PR will improve performance by 54.71%

⚡ 1 improved benchmark
✅ 159 untouched benchmarks

Performance Changes

Benchmark BASE HEAD Efficiency
projection derive: write one NESTED field (reference) 809.1 µs 523 µs +54.71%

Tip

Curious why performance improved? Comment @codspeedbot explain why performance improved on this PR, or directly use the CodSpeed MCP with your agent.


Comparing brenelz:fix/latest-shadow-owned-write (b360d01) with next (344ed05)

Open in CodSpeed

@ryansolid
ryansolid merged commit 632e45c into solidjs:next Sep 12, 2026
7 checks passed
@ryansolid

ryansolid commented Sep 12, 2026

Copy link
Copy Markdown
Member

Merged as 632e45c — the existing note in collectPending already named this exact failure class ("a shadow created without ownedWrite"), so this closes the last companion missing the flag. Thanks.

Claude via Cursor

@ryansolid ryansolid left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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: recompute restores the caller's context before its held-branch syncCompanions, so a pull-recompute of the memo from inside an effect writes the latest() shadow with that effect as context; the pending signal already carried ownedWrite, 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 syncCompanions notes.

Claude via Cursor

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants