* fix(app-router): emit per-page dynamic stale time metadata
App Router pages that export unstable_dynamicStaleTime currently behave as if no page-level stale window exists. The server segment config resolver discards the export, so RSC responses cannot carry the per-page metadata that the client cache needs for independently configured pages.
The violated invariant is that route segment config resolution must preserve page-only stale-time metadata across active parallel pages before response construction. Parse the page export, reduce active page slots by shortest stale time, thread the value through generated App page dispatch, and emit it on RSC responses for the client cache to consume.
* fix(app-router): gate dynamic stale time header to dynamic-only renders
Only emit X-Vinext-Dynamic-Stale-Time when the render is truly dynamic
(not prerender or force-static), matching Next.js behavior where the
response `d` field is only set for non-static-generation renders.
The client treats the headers presence as authoritative; leaking it onto
static/prerendered RSC responses would cause the client cache to apply
dynamic stale time semantics to a statically-cached entry.
* test(app-router): add behavioral tests for dynamic stale time gating
* docs(app-page-render): add line-numbered link to Next.js source
* refactor(app-router): drop redundant Number.isFinite check
Number.isInteger already returns false for Infinity/NaN, so the
preceding Number.isFinite guard is redundant in both the dynamic
stale-time header check and resolveDynamicStaleTimeSeconds.
* fix(app-router): gate dynamic stale time on cache-capture predicate
The previous gating (isPrerender || isForceStatic) was too narrow for
production: isPrerender is only true during the build-time prerender
pass, so a production ISR render (revalidate > 0) still emitted
X-Vinext-Dynamic-Stale-Time even though that render is captured into the
ISR cache. Next.js gates on !workStore.isStaticGeneration; the runtime
analog here is shouldCaptureRscForCacheMetadata, so add it to the guard.
Adds production ISR coverage to lock down the gating.
* docs(app-router): document dynamic stale time over-gating boundary
Add a comment explaining that shouldCaptureRscForCacheMetadata is
computed from config before the stream runs, so a production
default-config route that turns dynamic via a late dynamic API loses the
advisory stale-time hint even though Next.js (!isStaticGeneration) would
emit it. The value is unknowable until after headers are built.
Adds a complementary test pinning down this boundary behavior.
---------
Co-authored-by: James <james@eli.cx>
* fix(app-router): emit x-edge-runtime marker on edge RSC route response
Closes#1531
The Next.js edge-runtime app template (edge-ssr-app.ts) sets
`x-edge-runtime: 1` on every App Router response. vinext serves App
Router responses from a Cloudflare Worker (an edge-equivalent runtime)
but was not emitting this marker, so Next.js' upstream test suite
(`test/e2e/app-dir/app/index.test.ts`) saw `null` for the header.
Add the header to both the RSC and HTML response builders so the
assertion passes for any App Router page, regardless of which payload
shape the client requests.
* fix(app-router): gate x-edge-runtime marker on configured runtime
Only emit `x-edge-runtime: 1` when the route's resolved segment config
opts into the edge runtime (`export const runtime = "edge"` or
`"experimental-edge"`). Plumb the resolved runtime from the segment
config through dispatch and the render lifecycle so both RSC and HTML
response builders see it.
Without the gate, nodejs-runtime routes also advertise the marker, which
hides the configured runtime from downstream consumers.