Repository navigation
feat: streaming SSR, abortable navigations, and speculation rules from janux.config.ts - #32
Merged
Merged
Conversation
…tay parallel The joined chunks are byte-identical to renderToString(...).html; islands yield their open tag before loading sources, so a slow source holds back its own children, not the page. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…gue after it shellParts splits htmlDocument into a render-independent prelude (title and meta resolve from the route before the body renders) and a render-dependent epilogue (snapshots, island runtime, i18n payload). A render that fails after the first flush cannot change the status line, so it reports in-page via janux:error and closes the document; no auto-reload, a deterministic render error would just repeat. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
arrayBuffer() held every chunk until the slowest island resolved — exactly what streaming SSR exists to avoid. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n a persisted island is dropped A prefetched body has no fetch signal, so the navigation's own signal now kills the stream — without it a superseded diff kept consuming while the newest navigation waited behind it in the chain. And a persist island the destination page does not render disposes silently, which reads as 'my assistant closed itself': dev builds now name the island and the route. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rapid clicks Two of these are red until diff-dom-streaming lands its walker fixes: the body is not applied while a chunk's last node stays open (so a page whose content hangs off <main> only streams its <head>), and a cancelled stream leaves the diff hanging instead of rejecting. Both reproduce with the library alone; the tests describe the behaviour Janux needs. Also: a superseded navigation must not report success — the diff can finish cleanly on a cancelled stream having applied only part of the page. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nux.config.ts Speculation rules only apply to browser-driven navigations, so the shell emits them for every internal link and boot() rewrites them to cover only [data-native] links once it intercepts — otherwise Chrome speculates documents the SPA path never uses and hover fetches the page twice. Hover prefetch is now configurable (and skipped under data saver, or when there is no Navigation API to make it useful), and janux.config.ts finally reaches the client through a keyed shell script. A static page's one script tag is now the rules' JSON, which the browser reads as data: still zero JavaScript, and that page type is the one the API helps most. The ZERO-JS test now says exactly that. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The incoming page carries the server's document-wide rules and the diff applies them faithfully, so from the second page on Chrome went back to speculating documents the SPA path never uses — and hover fetched each page twice. Caught in Chrome, not by the unit suite. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The browser suite skips itself without a client bundle, so without this it would report green having run nothing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…es across the stream path The generator design suspended each sibling at its first yield, which assigned nested island keys in yield order instead of the client's synchronous depth-first order — two same-typed nested islands could swap state on the parent's first re-render. Ordered emitters restore the exact Promise.all scheduling the string renderer always had (keys identical to main, pinned by test) while the leftmost unfinished child streams straight through. It also drops the per-chunk generator-chain cost. Also from review: chunks coalesce on genuine renderer pauses (one flush per static run instead of one write per tag); RenderStream.cancel() stops an abandoned render instead of paying for every island of a closed connection; the shell splits into prelude/epilogue builders (nothing is built twice per request); shellI18n owns the no-render shape; navigation responses ship the rules already narrowed so the client rescope is a no-op; prefetch caps warmed pages and cancels evicted bodies; the dev adapter survives a client that disconnects with the socket full; shared script ids are constants; the persist warning is unconditional like every other developer-mistake warning. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
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>
…ball Published (same shasum as the vendored pack), so the temporary file: pin lives no longer than one commit. 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
Navigation becomes what it was always meant to feel like: the server flushes the page as it renders, the client diffs it as it arrives, rapid clicks cancel the in-flight navigation (the last one wins), and prefetching is configurable from `janux.config.ts` with Speculation Rules emitted by default. Live on https://janux.build.
Streaming SSR (the actual fix for "streaming doesn't feel like streaming")
` at 59ms where previously nothing left the server until the last byte existed.
The renderer's internal design is ordered emitters (not async generators): siblings run under the exact `Promise.all` scheduling the string renderer always had — which is what keeps nested island key assignment aligned with the client's synchronous depth-first traversal (pinned by test; a generator-based draft assigned keys in yield order and two same-typed nested islands could swap state) — while the leftmost unfinished child streams straight through.
Abortable navigations (rapid clicks)
Navigations are serialized and each one cancels the one before it: the superseded fetch dies with its Navigation API signal, and a prefetched body — which has no fetch signal of its own — is wrapped so the navigation's signal kills it too (`abortableStream`). A superseded navigation never reports `after`, never disposes an island, and never wins over the last click. Verified in real Chrome: click two sidebar entries faster than the pages arrive → the last one lands, the assistant stays open, same live instance.
This surfaced two upstream bugs in `diff-dom-streaming`, both fixed with TDD in brisa-build/diff-dom-streaming#23 (v0.6.8):
Prefetch + Speculation Rules, configurable
```ts
export default defineConfig({
navigation: {
spa: true, // default: true
prefetch: { ttl: 60_000 }, // or false
speculationRules: { eagerness: 'moderate', exclude: ['/logout'] },
},
});
```
Persisted islands
A `persist` island the destination page does not render is disposed — that is the contract — but it reads as "my assistant closed itself", so it now warns naming the island and the route. Documented: render it from a shared layout and it survives the whole session.
Tests & docs
Depends on
Follow-ups (noted, out of scope)
🤖 Generated with Claude Code