fix: stream the body progressively past an open last node, and reject instead of hanging on stream errors - #23
Merged
Conversation
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lker If the input stream errored (aborted fetch, dead network), the reader rejection escaped as an unhandled rejection, streamInProgress stayed true forever and the walker kept waiting on requestAnimationFrame: diff() never resolved nor rejected. Now the error is captured, the stream is marked as finished on both paths, and diff() rejects with the original error. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e body When the streamed content hangs off the LAST child of a still-open container (the common <main> after <nav> layout), the nextSibling hop waited unconditionally until the stream closed, so nothing below <body> was applied progressively. Now the walker descends into an open node as soon as it has children (same idea the firstChild hop already used), null hops from the parser frontier wait for more nodes instead of ending the level early (deferring the pruning of trailing old nodes until the level is closed), and deep clones settle first so they cannot snapshot a half-parsed subtree. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
aralroca
added a commit
to aralroca/Janux
that referenced
this pull request
Jul 28, 2026
The two streaming e2e tests describe behavior that lands in diff-dom-streaming 0.6.8 (brisa-build/diff-dom-streaming#23): the walker applying children of an open last node instead of stalling the whole body, and diff() rejecting on an erroring stream instead of hanging. npm still serves 0.6.7, so CI ran without the fixes and those two tests were red. The vendored tarball is byte-identical to what `npm publish` will ship (same shasum as the pack dry-run). TEMPORARY: swap the file: spec for ^0.6.8 as soon as the release is published — before any janux release, since a file: dependency must never reach the npm registry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Two walker fixes plus a test hardening pass for arbitrary chunk boundaries. Version bumped to 0.6.8 (`deno.json` was stale at 0.6.6 and is now synced).
Fix 1 — the body was never applied progressively when content hangs off the last child
In the `nextSibling` hop, `isLastNodeOfChunk` fell through to `return streamInProgress` — an unconditional wait — even when the open node already had complete children to diff. Since real pages hang their content off the last child of a container (`
` after ``), the walk stalled before entering it and nothing of the body was applied until the stream closed; only the `` (which precedes it) streamed. Measured on a real docs site: during a 3s mid-stream pause, 43 mutations — all in ``, zero in the body.The fix descends into an open last node once it has children (same condition the `firstChild` hop already used), and defers the pruning phase of `setChildNodes` for a level whose parent is still open — trailing nodes may yet arrive in a later chunk, so removing them early would corrupt the page. Deep clones settle before cloning so a half-parsed subtree is never photographed.
Fix 2 — an erroring stream hung `diff()` forever
`processStream()` only cleared `streamInProgress` on the `done` path. A stream that errors (an aborted fetch, a dropped connection) left it `true`, so the walker waited on `requestAnimationFrame` indefinitely: `diff()` neither resolved nor rejected, plus unhandled rejections from the fire-and-forget reader and `pipeTo`. For a SPA router this meant one aborted navigation froze all navigation until a hard reload.
Now the error is captured, `streamInProgress` clears in `finally`, the duplicate `pipeTo` rejection is silenced, and `diff()` rejects with the stream's error in bounded time.
Tests
TDD throughout — every fix landed with a failing test first. New coverage (ran on Chromium, Firefox and WebKit; 137 pass):
Harness additions: `newHTMLByteChunks` (raw byte chunks), `errorStreamMessage`, `midStreamEval` (DOM snapshot during the pause before the last chunk).
Why now
The Janux framework moved to streamed SSR (flushing many small chunks with boundaries anywhere) and its SPA navigation aborts superseded page streams — both fixes are prerequisites for that to work, and the chunk-boundary tests pin the contract it relies on.
🤖 Generated with Claude Code