Skip to content

fix(ssr): a memo joining a pending slot adopts the slot's answer - #3816

Merged
ryansolid merged 3 commits into
solidjs:nextfrom
brenelz:fix/hole-retry-shared-slot-3815
Oct 6, 2026
Merged

ryansolid merged 3 commits into
solidjs:nextfrom
brenelz:fix/hole-retry-shared-slot-3815

Conversation

@brenelz

@brenelz brenelz commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #3815.

When a retry pass re-creates an async memo while the slot it occupies is still loading, the new memo joins that slot and gets the slot's shared deferred as its NotReadyError source. Only the earlier memo's promise settles the deferred, and that settle writes to the earlier memo, not the new one. The new memo keeps throwing NotReadyError on a promise that has already resolved, so any memo that reads it retries every microtask. Timers never run again, the stream never ends, and the process sits at 100% CPU.

The issue's exact reproduction (a function hole that creates a component returning a pending lazy() view) hangs on rc.13 but already ends on next, because #3770 made the hole scope resume the suspended accessor instead of setting the component up again. The flaw in processResult is still on next and can be reached another way: an <Errored> retry that re-creates a pending <Loading> (the #3750 shape), where the component makes a fresh promise on each setup and has a memo that reads it.

A memo that joins a pending thenable slot now subscribes to the shared deferred and takes the slot's value or error when it settles, the same way the re-created async-iterable path already did. A served loadingValue still keeps the first-value lock. The three copies of the adopt code are now one adoptSlot helper.

How did you test this change?

  • packages/web/test/server/hole-retry-shared-slot-3815.spec.tsx covers the <Errored>/<Loading> shape with a sync and an async reader. Both tests fail on next without the fix (the reader memo hits its 10,000-run guard) and pass with it. A third test pins the issue's original hole-plus-lazy() shape.
  • I compiled the issue's script with @solidjs/compiler (generate: "ssr", hydratable: true) and ran it against the published rc.13 packages with the same change applied to solid-js/dist/server.js. It ends after 116 ms instead of spinning.
  • cd packages/solid && npx vitest run: 819 tests pass.
  • cd packages/web && npx vitest run --config vite.config.server.mjs and --config vite.config.hydrate.mjs: the only change against unpatched next is the two new tests going from failing to passing. The other server and hydration failures in my environment are identical with and without the change and come from the local native compiler build. With JSX_COMPILER=babel, the new spec and the [2.0 rc.13] <Loading> discovery never converges when a hole re-creates a component that reads its own async source during setup (router query() / liveQuery() included) #3734, retry-robustness and shared-async-sources specs all pass.

One thing is unchanged and out of scope here: if the joined memo's own promise settles later, its success handler still overwrites the slot and its value with its own answer, which can differ from the value already serialized to the client.

🤖 Generated with Claude Code

A retry pass that re-creates an async memo while its slot is in flight
hands it the shared deferred, but only the earlier memo's promise settles
that deferred. The joined memo kept its NotReadyError on the resolved
promise, so any memo reading it retried every microtask and the stream
never ended. Fixes solidjs#3815.
@changeset-bot

changeset-bot Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: ff3b466

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

This PR includes changesets to release 12 packages
Name Type
solid-js Patch
@solidjs/diagnostics Patch
@solidjs/element Patch
@solidjs/h Patch
@solidjs/html Patch
test-integration Patch
@solidjs/universal Patch
@solidjs/web Patch
todos-server-example Patch
@solidjs/babel-plugin Patch
@solidjs/compiler Patch
@solidjs/signals 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 Oct 6, 2026 •

Copy link
Copy Markdown

Merging this PR will improve performance by 10.05%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 1 improved benchmark
✅ 187 untouched benchmarks

Performance Changes

Benchmark BASE HEAD Efficiency
⚡ memo + sync render effect only (reference) 29.4 ms 26.8 ms +10.05%

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/hole-retry-shared-slot-3815 (ff3b466) with next (79df376)

Open in CodSpeed

@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.

Thanks — the fix is right and the diagnosis matches what I see. Verified locally:

  • On next (a52615c), both <Errored>/<Loading> variants hit the 10,000-run reader guard in ~90 ms (a microtask spin); with this PR all three tests pass. I also compiled the issue's script and ran it under node: on next it already ends in ~105 ms with Page set up once, so #3770 did remove the original trigger, as you said.
  • adoptSlot is a faithful merge of the old copies; the async-iterable path (#3804) and the loadingValue first-value lock are unchanged. The new joined-thenable subscription is correctly ordered: recordSlot runs before the deferred settles, and settle is registered before any reader in the pass subscribes, so the node adopts before readers retry. The client-hole case adopts the tagged NotReadyError, which is correct.
  • The #3734/#3750/retry/shared-async/slot-hydration specs all pass with the PR rebased onto current next, and the solid, web client, server and hydrate suites are green with a freshly built compiler. (Your 47 local failures were the stale prebuilt binary.)

One blocker: CI fails on @solidjs/web#test-types in the new spec:

test/server/hole-retry-shared-slot-3815.spec.tsx(54,43): error TS2322: Type '() => JSX.Element' is not assignable to type 'Element'.

{(() => <Page />) as any} on line 54 clears it (checked locally, tests still pass).

Not blocking:

  • The CodSpeed dbmon shallow full tick regression (−6.59%) is noise. That benchmark is in @solidjs/signals, which this PR doesn't touch, and #3814 shows the same drop against the same base.
  • Optional: a comp.disposed early return in the joined settle so superseded nodes aren't written. It's inert today, so your call.
  • The double write you flagged as out of scope (the joined node's own flight overwriting a settled slot with a different answer) is real and predates this PR. We'll track it separately as "first settle wins" for the slot.

— Claude via Cursor

ryansolid and others added 2 commits October 5, 2026 23:15
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@ryansolid
ryansolid merged commit 759a9b6 into solidjs:next Oct 6, 2026
6 checks passed
@ryansolid

Copy link
Copy Markdown
Member

Thanks @brenelz! To get this in tonight we merged next into your branch and pushed the one-line test-types fix ((() => <Page />) as any in the spec), then squash-merged once CI was green.

— 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