Skip to content

fix(ssr): stop a pending seed reading the registry after a failed render - #47

Merged
jarrednorrisdev merged 1 commit into
mainfrom
fix/scoped-seed-disposed-registry
Oct 10, 2026
Merged

jarrednorrisdev merged 1 commit into
mainfrom
fix/scoped-seed-disposed-registry

Conversation

@jarrednorrisdev

Copy link
Copy Markdown
Owner

Fixes the unit-test failure on main since #39 landed: an unhandled rejection, Cannot access Atom … scoped-user-a: registry is disposed, which fails the check job on every branch built on main (including #37).

Background: on the server, each serializable atom a component awaits gets a seed: its result, encoded and sent with the page so the browser doesn't fetch it again. Svelte's hydratable collects these while the page renders.

A failed server render rejects with "registry is disposed"

How it happens: #45 added a test whose server render is meant to fail. Two providers make two different atoms with one serialization key, and the second reader throws. #39 then made a RegistryProvider dispose of its registry when the render ends, failed renders included. Each passed CI on its own; together they don't.

<RegistryProvider>
  <ScopedUserProvider id="a"><ScopedUserReader /></ScopedUserProvider>
  <!-- same key, different atom: throws, and the render fails -->
  <ScopedUserProvider id="a"><ScopedUserReader /></ScopedUserProvider>
</RegistryProvider>

Bug: the first reader's seed callback waits a microtask before reading its atom. By then the second reader has thrown, the render has ended, and the provider has disposed of the registry. The read throws inside a promise that nothing awaits any more, and Vitest fails the run on the unhandled rejection. A real app would hit the same thing on any failed server render with a serializable atom still seeding: the error is logged, but the page fails either way.

Fix: the seed records when the render ends, as subscribedReader has done since #39, and stops before reading the registry once it has. A render that succeeds waits for every seed before it ends, so this only changes failed renders.

   const release = serverMount(registry, atom);
-  onRenderEnd(release);
+  let ended = false;
+  onRenderEnd(() => {
+    ended = true;
+    release();
+  });
   const encoded = hydratable(key, () => {
     claim.own = (async () => {
       await undefined;
+      if (ended) {
+        return noSeed;
+      }
       …
+      if (ended) {
+        return noSeed;
+      }
       return encodeSeed(key, encode, serverGet(registry, atom));

Test: the existing scoped.browser.test.ts › "two providers of one input make two different atoms with one key, which fails the server render" covers it: the suite fails on main and should pass here.

🤖 Generated with Claude Code

Since #39, a RegistryProvider disposes of its registry when the server render
ends, failed renders included. A seed's hydratable callback that resumed after
that read the disposed registry and rejected with nothing awaiting it, which
failed the unit suite on main (scoped.browser.test.ts, two providers of one
input). The callback now stops once the render has ended.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jarrednorrisdev
jarrednorrisdev merged commit 836e19f into main Oct 10, 2026
16 checks passed
@jarrednorrisdev
jarrednorrisdev deleted the fix/scoped-seed-disposed-registry branch October 10, 2026 14:21
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.

1 participant