Live mode - #460
Open
freekh wants to merge 10 commits into
Open
Conversation
Live mode is an opt-in config flag that makes an app render committed patches that are not yet in the running deploy, for anonymous end users - closing the window between an editor saving and CI redeploying. The document covers the required new endpoint on the content host, the mandatory cache TTL (with stale-while-revalidate and stale-if-error), the changes to the RSC and client rendering paths, and why the git commit sha alone cannot be treated as deploy identity. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V72pRWkhoSLtU3qap7bXmW
The suspend prop on ValProvider grows from a boolean into "always" | "never" | "draft" | "live", so an app can wait for draft sources, live sources, both or neither. true keeps meaning "draft", so enabling live mode never starts showing a Suspense fallback to anonymous visitors without an explicit opt in. The live gate cannot reuse hasAllLoaded: a live response only carries the modules that changed, so a module without a live patch would suspend until the load timeout and then re-suspend on every render. It gets its own settleLive/waitForLive pair on ValExternalStore instead, settled on failure as well as success. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V72pRWkhoSLtU3qap7bXmW
🦋 Changeset detectedLatest commit: c8b7dfa The changes in this PR will be included in the next version bump. This PR includes changesets to release 6 packages
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 |
`live: { ttl, staleWhileRevalidate? }` on ValConfig is the on-switch for live
mode: rendering patches that are committed to Val but not yet deployed. `ttl`
is required (0 allowed) because live mode has to ask Val what changed on every
request unless we cache.
Added to all five places ValConfig is duplicated (core type, the two shared zod
copies, the CLI config zod and the init template), plus `resolveLiveConfig` in
ValRouter which applies the VAL_LIVE_TTL / VAL_LIVE_STALE_WHILE_REVALIDATE /
VAL_LIVE_DISABLED env overrides and validates at runtime - val.config may be
plain JS where the type is not enforced. Live mode is proxy mode only; in fs
mode it warns and no-ops.
Nothing consumes the resolved config yet.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The cache that keeps live mode from hitting Val on every render. Single entry, keyed by the caller. The key includes baseSha, not just the commit sha, because the same commit can be deployed several times with different evaluated sources - a commit sha alone does not identify a deploy. On a key change the entry is dropped outright, stale-if-error included: serving another deploy's patches is worse than serving none, since "none" just means the deployed content. ttl 0 skips the serve-without-awaiting branches entirely rather than letting staleWhileRevalidate reintroduce the staleness ttl 0 asks us not to have. Nothing uses this yet. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`ValOpsHttp.getLiveSources()` is the whole server-side of live mode: fetch the live patch set from Val, apply it, return the changed modules. Nothing new is invented to apply the patches - analyzePatches + getSources are the same functions the editor path already uses. - `analyzePatches` grows an `includeApplied` option. Live patches are all committed, which is precisely what the draft path filters out; the default is unchanged so the draft path keeps skipping them. - `getSources` already returns only the modules that had patches, which is what we want on the wire, and it already records per-module errors and skips - so a patch that no longer applies degrades to the deployed content for that module instead of failing the render. - Failures are never fatal: an http error, a network error, an unparseable response or a response for a different commit all fall back to the deployed content, via a stale entry first if there is one. `getLiveSources` is on the ValOps base class returning empty, so callers do not have to branch on the mode - ValOpsFS has no committed-but-undeployed patches. Also fixes resolveLiveConfig against an undefined config: initVal() hands back whatever it was given, so `initVal()` with no arguments leaves config undefined at runtime regardless of what the type says. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The app-side route the browser and the RSC path both read live sources from. Unauthenticated by design, and the justification is stronger than the one /files already relies on: everything it returns is committed to the repository and therefore public. It carries no draft content of any kind. Returns an empty set - rather than an error - when live mode is off or the mode is fs, so callers never have to branch on whether live mode is enabled. Also enables the immutable Cache-Control on /files for the patch_id branch, which was commented out with a TODO. A (filePath, patch_id) pair always resolves to the same bytes, because editing a file creates a new patch with a new id, and live mode makes that route much hotter: every image added by a committed-but-undeployed patch is served from there rather than from the deploy. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The highest-value surface, and shippable on its own: for an anonymous visitor with live mode configured, fetchVal now resolves committed-but-undeployed content server-side, so the HTML is already correct on a hard load. Two invariants, both covered by tests: - `disabled` stays bound to draft mode, never to live mode. Live content is public, so no data-val-path stega markers may leak into the HTML anonymous visitors get. (Verified by breaking it: the test does fail.) - The live path adds no dynamic API. cookies()/headers() opt a route out of static generation for every visitor, so they stay behind `enabled`; live mode needs nothing per-request beyond the config and the in-process server. Failures degrade to the deployed content rather than propagating - including away from the caller's catch, which re-encodes with stega enabled. fetchValRoute / fetchValRouteUrl need no change: they call fetchVal internally, so a route that only exists in a committed patch resolves for free. Such routes are not in generateStaticParams, but Next's default dynamicParams: true renders them on demand. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- README: a Live mode section covering what renders live today (the RSC surfaces, new routes from a committed patch, images) and what does not yet (client components), the ttl contract, the env overrides, and that Val is never in the critical path for correctness. - The generated val.config carries a commented-out live block, so the option is discoverable without turning it on. - examples/next enables it, which means the example's build exercises the live path. That build is also the assertion that live mode adds no dynamic API: `/` is still prerendered as static content with live mode configured. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review fixes on top of the live mode branch: - packages/init: the generated val.config dropped the closing brace without a trailing comma, so uncommenting the `live` pointer produced invalid JS. - An empty VAL_LIVE_TTL counted as an opt-in and was then rejected, so a declared-but-empty env var took down an app that never asked for live mode. Empty and whitespace-only values now count as unset. - initValRsc creates the Val server at module-eval time but only awaits it from inside fetchVal, so the new live config validation could reject with no handler attached and take the process down. Guard it the way createValApiRouter already does. - LiveCache wrote the result of an overtaken refresh: a slow fetch for an old key could clobber a newer entry, and clear() could be undone by an in-flight write. Guard both with an epoch. - getLiveSources re-derived the sources on every fetchVal, so a page with ten of them re-applied every live patch ten times per request. Memoise on the patch set. - Warn once per process about live mode in fs mode, not once per Val server. Also documents the revalidation contract: Next records a page's revalidate interval from the fetches performed while rendering it, and LiveCache suppresses that fetch on a hit, so prerendered pages need `export const revalidate = <ttl>`. README, changeset, plan risk table and the example app's layout. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…h logic and add tests for revalidation behavior
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.
Live mode is an opt-in config flag that makes an app render committed
patches that are not yet in the running deploy, for anonymous end users -
closing the window between an editor saving and CI redeploying.
The document covers the required new endpoint on the content host, the
mandatory cache TTL (with stale-while-revalidate and stale-if-error), the
changes to the RSC and client rendering paths, and why the git commit sha
alone cannot be treated as deploy identity.
Co-Authored-By: Claude Opus 5 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01V72pRWkhoSLtU3qap7bXmW