Commit Graph

76 Commits

Author SHA1 Message Date
James 13cbd88e24 fix(cache): align response store HTML identity 2026-09-10 10:02:10 +01:00
James 209356333d feat(cache): lazily resolve response store tag expirations 2026-09-09 19:02:12 +01:00
James fc05d956d1 feat(cache): stream response-store cache misses 2026-09-09 17:47:29 +01:00
James d98fa29899 feat(cache): seed RSC during response-store warmup 2026-09-08 23:37:18 +01:00
James 729f0f5cb6 feat(cache): add Workers Response Store POC 2026-09-08 16:50:23 +01:00
James Anderson 86afc11051 refactor(cache): delegate response policy to CDN adapters (#3191)
* refactor(cache): delegate response policy to CDN adapters

* fix(cache): keep adapter policy fail closed
2026-09-08 10:35:02 +01:00
James Anderson bc606792e3 fix(cloudflare): restore bounded probe scheduling (#3171)
* fix(cloudflare): restore bounded probe scheduling

* fix(cloudflare): retry malformed cacheability probes

* fix(cloudflare): retry transient cacheability probes

* fix(cloudflare): remove probe scheduling barriers

* fix(cloudflare): preserve probe throughput and retry budgets

* fix(cloudflare): fill prunable probe slots

* fix(cloudflare): preserve probe no-progress timeout

* test(cloudflare): retain hung probe watchdog coverage
2026-09-07 21:48:30 +01:00
James Anderson 35af79106e fix(cloudflare): reduce staged CDN probe work (#3168)
* fix(cloudflare): reduce staged cacheability probes

* fix(cloudflare): isolate uncached response renders

* test(cloudflare): cover uncached entrypoint collisions

* fix(cloudflare): preserve i18n middleware probe scope

* fix(cloudflare): preserve purge routing in uncached renders

* fix(cloudflare): match runtime middleware probe paths

* fix(cloudflare): keep probe concurrency saturated

* fix(cloudflare): preserve encoded middleware probe paths

* fix(cloudflare): preserve websocket upgrade responses

* fix(cloudflare): preserve deferred probe route ownership

* fix(cache): preserve warmed responses on browser reload

* fix(cloudflare): preserve warmed cache on reload

* fix(cache): preserve App responses on browser reload

* test(cache): preserve full-stage coverage after reload parity
2026-09-07 21:48:30 +01:00
James Anderson 4e24e15651 docs(cloudflare): explain cached response entrypoint (#3154)
* docs(cloudflare): explain cached response entrypoints

* docs(cloudflare): distinguish private cache headers

* docs(cloudflare): clarify private gateway egress
2026-09-07 21:48:29 +01:00
James Anderson cf681a2e9b fix(cache): preserve staged cache invalidation parity (#3158)
* fix(cache): preserve response-stage invalidation parity

* test(cache): cover live tag invalidation
2026-09-07 21:48:29 +01:00
James Anderson e8eaec9f39 fix(cloudflare): prewarm routed response stages (#3153)
* fix(cloudflare): prewarm routed response stages

* test(cloudflare): own prewarm middleware fixtures

* test(cloudflare): cover config-personalized cache hits

* test(cloudflare): expose edge cache status mirrors

* test(cloudflare): cover cached Pages data isolation
2026-09-07 21:48:28 +01:00
James Anderson 9265f300cc feat(cloudflare): isolate cached response entrypoint (#3152)
* feat(cloudflare): isolate cached response entrypoint

* test(cloudflare): reuse generic stage facade coverage

* test(cloudflare): preserve composite CDN output config

* fix(cloudflare): track final shared response provenance

* fix(cloudflare): validate routed response artifacts

* test(build): follow response-stage sidecar closure

* test(build): validate response-stage manifest closure

* style(test): format stage closure assertion

* test(cache): assert uncached gateway policy for browser fetch

* fix(cloudflare): partition framework cache variants

* feat(cloudflare): expose entrypoint cache status

* fix(cloudflare): protect shared response provenance

* test(cloudflare): align local cache status mirrors

* test(cloudflare): align entrypoint gateway policy
2026-09-07 21:48:28 +01:00
James Anderson 91880b2e6d fix(build): preserve staged prerender routing (#3151)
* fix(build): preserve staged prerender routing

* fix(build): prerender the emitted application entry

* fix(build): preserve application entry across prerender workers

* fix(build): preserve nested prerender artifact roots

* fix(build): preserve nested server artifact roots

* fix(build): honor configured prerender output roots

* fix(build): keep staged prerender artifacts canonical
2026-09-07 21:48:27 +01:00
James Anderson 5219f652ab refactor(cache): isolate response-stage admission (#3144)
* refactor(cache): isolate response-stage admission

* test(cache): cover staged policy helpers

* fix(cache): preserve provider policy during admission

* refactor(cache): unify staged response admission

* fix(cache): preserve provider route handler policies

* fix(cache): normalize Pages API admission at its owner
2026-09-07 21:48:25 +01:00
James Anderson 13af970f62 feat(cloudflare): summarize CDN warmup by route (#3163)
* feat(cloudflare): summarize CDN warmup by route

* refactor(cloudflare): align CDN warm report columns
2026-09-03 11:01:26 +01:00
James Anderson a6931c0716 fix(cloudflare): wait for per-route version propagation (#3164) 2026-09-02 17:44:11 +01:00
github-actions[bot] 9cf754fcc2 chore: version packages (beta) (#3074)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-09-02 12:10:10 +00:00
James Anderson 8370369383 fix(cloudflare): finalize CDN version metadata output (#3137)
* fix(cloudflare): finalize CDN version metadata output

* test(cloudflare): validate generated version metadata deployment

* fix(cloudflare): finalize metadata from primary output
2026-09-02 12:55:40 +01:00
James Anderson 9d94dcdd5f fix(cloudflare): support explicit CDN warm targets (#3138) 2026-09-01 22:16:55 +00:00
James Anderson 6a18dbe3ff fix(cloudflare): harden post-deploy readiness checks (#3136) 2026-09-01 22:43:02 +01:00
James Anderson 04c4de7f81 fix(cloudflare): restore web CDN warmup deploy (#3135) 2026-09-01 22:00:50 +01:00
James Anderson 2a08fb7913 fix(cloudflare): classify CDN cacheability per concrete route (#3115)
* fix(cloudflare): scale cacheability probing

* fix(cloudflare): preserve cache policy probe identities

* test(cache): cover conditional response vetoes

* fix(cache): match header policy sources exactly

* perf(cache): compute probe safety per pattern

* fix(build): omit empty route ownership metadata

* fix(cloudflare): classify cacheability per concrete path

* fix(cloudflare): retain zero-path static fallbacks

* test(cloudflare): cover on-demand static fallbacks

* fix(cloudflare): admit custom vary cache variants

* fix(cloudflare): keep wildcard vary private

* fix(cloudflare): reject wildcard vary during probing

* fix(cloudflare): reject empty Cache Components params

* fix(cloudflare): scope empty params validation to pages

* fix(cloudflare): remove cacheability response size limits

* fix(cloudflare): bound completed cache admission

* fix(cloudflare): release failed admission captures

* fix(cloudflare): honor staged readiness retries

* fix(cloudflare): retain probed manifest in dist

* fix(cloudflare): probe staged readiness without rendering

* fix(cloudflare): verify staged readiness response

* fix(cloudflare): reserve staged readiness endpoint

* fix(cloudflare): relax cacheability manifest bounds

* perf(cloudflare): avoid duplicate deployment status check
2026-09-01 21:25:26 +01:00
James Anderson c8bea6bccf feat(cache): classify and warm static Route Handlers (#3113)
* feat(cache): classify and warm static Route Handlers

* fix(cache): identify Route Handler probe responses

* test(cache): assert Route Handler probe identity

* fix(cache): prove final Route Handler responses

* fix(cache): admit explicit Route Handler policies safely

* fix(cache): close Route Handler admission fallbacks

* fix(cache): honor explicit dynamic handler policies

* fix(cache): reject unsafe Route Handler probe responses

* fix(cache): preserve Route Handler CDN policy ownership
2026-09-01 21:25:26 +01:00
James Anderson 09969d951a feat(cache): probe Pages Router cacheability (#3098)
* feat(cache): probe Pages Router cacheability

* test(cache): prove Pages prewarm reuse

* fix(cache): fail closed for Pages request variants

* test(cache): assert Pages response finalization

* fix(cache): transfer hybrid Pages admission ownership

* fix(cloudflare): warm Pages Router data routes

* fix(cache): align Pages cacheability ownership

* test(cache): preserve Pages ownership order

* fix(cache): enforce Pages response admission without manifest

* fix(cache): align Pages request-time cache policy

* test(cache): expect completed Pages request-time probes

* fix(cache): preserve routing vetoes during probing

* fix(cache): preserve final routing cache policy

* fix(cache): retain middleware cache policy precedence
2026-09-01 21:25:25 +01:00
James Anderson 8956b70556 fix(cache): certify staged cache fills before promotion (#3094)
* fix(cache): certify staged cache fills before promotion

* test(cache): verify post-purge CDN admission

* test(cache): await post-purge CDN reuse

* fix(cloudflare): make cache fill certification opt-in

* fix(cloudflare): make cache certification authoritative

* style(cloudflare): format certification deploy
2026-09-01 21:25:25 +01:00
James Anderson c2377c6b23 feat(cache): deploy probed manifests in two stages (#3093)
* feat(cache): deploy probed manifests in two stages

* fix(cache): isolate App Page cacheability probes

* fix(cache): preserve hybrid route ownership in probes

* fix(cache): isolate staged probe retries

* fix(cache): fail closed for unprobed request identities

* fix(cache): bound and filter staged cacheability manifests

* fix(cloudflare): validate cacheability manifest imports

* fix(cloudflare): bound staged cacheability probes

* fix(cloudflare): isolate cacheability probe budgets

* fix(cloudflare): bound staged probe envelopes

* test(cache): import manifest limit from owner

* fix(cloudflare): fail closed during staged warming

* fix(cloudflare): guard final staged traffic

* fix(cloudflare): deploy empty cacheability manifests

* fix(cloudflare): reconcile staged deployment failures
2026-09-01 21:25:24 +01:00
James Anderson bbaed77c35 fix(cache): gate CDN admission on probed routes (#3092)
* fix(cache): gate CDN admission on probed routes

* fix(cache): admit no-manifest responses after clean EOF

* fix(cache): fail closed for contextual admission

* fix(cache): isolate conditional config headers

* test(cache): use a valid legacy revalidate fixture

* fix(cache): finalize private app page evaluations

* test(cache): cover RSC manifest admission

* fix(cache): veto late private response policy

* fix(cache): reject conditional middleware probes

* fix(cache): trust framework admission policy

* fix(cache): isolate request-conditioned CDN responses

* fix(cache): avoid cookie response variation

* fix(cache): preserve hybrid Pages admission policy

* test(cache): allow existing Pages warm fills

* fix(cache): complete cacheable route handler responses

* fix(cache): preserve final public page policy

* fix(cache): complete adapter-admitted route handlers

* fix(cache): preserve route handler opt-out headers

* fix(cache): preserve explicit route handler policy

* style(cache): format Route Handler admission
2026-09-01 21:25:23 +01:00
James Anderson 56b6be06d6 fix(cloudflare): discover prewarm paths from staged worker (#3090)
* fix(cloudflare): discover prewarm paths from staged worker

* fix(cloudflare): hide prerender capability from user code

* perf(cloudflare): skip discovery URL parsing on ordinary requests

* fix(cloudflare): bound staged discovery phases

* perf(pages): lazy-load staged discovery runtime

* fix(cloudflare): use the full staged discovery deadline

* fix(cloudflare): distinguish staged discovery failures

* test(cloudflare): preserve deadline-bounded discovery

* test(cloudflare): avoid wall-clock retry count

* test(cloudflare): stabilize discovery deadline coverage
2026-09-01 21:25:22 +01:00
James Anderson 4146d69021 fix(cloudflare): verify Worker version IDs during CDN warmup (#3072)
* fix(cloudflare): revalidate stale warmup retries

* fix(cloudflare): report CDN warmup retry progress

* test(cloudflare): retry prewarm verification on fresh Worker

* feat(cloudflare): configure CDN readiness probes

* fix(cloudflare): validate CDN warmup version overrides

* fix(cloudflare): verify CDN warmup version ids

* test(cloudflare): opt into dangerous warmup fallback

* fix(cloudflare): harden warmup version validation

* docs(cloudflare): clarify version binding environments
2026-08-24 23:03:15 +01:00
James Anderson 8759b65c86 fix(build): validate CDN warm discovery and deploy inputs (#3057)
* fix(build): validate CDN warm path params

* fix(build): exclude handlers from CDN warm paths

* fix(build): resolve all CDN warm path owners

* fix(build): preserve exact CDN warm path identity

* fix(build): preserve encoded Pages warm paths

* test(cloudflare): preserve encoded warm request URLs

* fix(build): reject unsafe Pages warm paths

* fix(router): match localized warm paths case-insensitively

* fix(cache): honor build identity adapter capability

* fix(build): reject normalized CDN warm paths

* fix(build): discover dynamic MDX warm paths

* fix(build): skip routes without static param generators

* fix(cloudflare): validate warmup promotion delay
2026-08-24 23:03:15 +01:00
James Anderson e2e2b177eb fix(cloudflare): complete warmup response and promotion contracts (#3046)
* test(cloudflare): prove deployed Pages CDN warmup

* fix(cloudflare): reject incomplete no-promote warmup

* fix(cloudflare): keep warm discovery fail-closed

* test(cloudflare): prove prewarmed App HTML reuse

* fix(cloudflare): enforce no-promote warmup contract

* fix(cloudflare): warm terminal ISR responses
2026-08-24 23:03:14 +01:00
James Anderson f1efcc1e60 fix(cloudflare): harden canonical RSC warmup end to end (#3040)
* fix(cloudflare): validate reusable warm responses

* fix(cloudflare): retry staged warmup failures safely

* fix(cloudflare): extend staged propagation retries

* fix(build): filter Pages-owned RSC warm paths

* fix(cloudflare): keep CDN warmup correct with custom adapters and i18n

* fix(cloudflare): reject unusable CDN warm responses

* fix(cloudflare): keep CDN warmup bound to the deployed build

* fix(router): reuse warmed RSC on locale-prefixed hybrid routes

* fix(cloudflare): verify exact build and variant during CDN warmup

* fix(router): keep warmed RSC ownership rewrite-safe

* test(cloudflare): prove browser reuse of warmed RSC variants

* test(cloudflare): bind RSC warm proof to each deploy attempt

* fix(rsc): keep deferred loading shells contextual

* fix(build): keep CDN warm discovery route-correct

* fix(cache): preserve valid CDN policy during RSC warmup

* fix(cloudflare): warm the deployed Worker route host

* fix(cloudflare): preserve staged warmup guarantees

* fix(build): discover localized Pages warmup paths

* fix(cloudflare): wait for staged Worker before CDN warmup

* fix(cloudflare): preserve strict warmup deploy invariants

* fix(build): fail closed during CDN warm discovery
2026-08-24 23:03:14 +01:00
James Anderson aeee350a6f feat(cloudflare): configure warmup promotion delay (#3017) 2026-08-24 23:03:13 +01:00
James Anderson 7bf69109fc feat(cloudflare): allow warmup without promotion (#3016) 2026-08-24 23:03:12 +01:00
James Anderson 843ecd1ef7 fix(cloudflare): default CDN warmup concurrency to 25 (#3015) 2026-08-24 23:03:12 +01:00
James Anderson 8bb09481d6 feat(cloudflare): prewarm canonical ISR RSC requests (#3002)
* feat(cloudflare): prewarm canonical ISR RSC requests

* fix(cloudflare): forward preview deploy options

* fix(cloudflare): harden canonical RSC prewarming

* fix(test): wait for preview alias propagation

* fix(cloudflare): guarantee exact RSC prewarm reuse

* fix(cloudflare): harden canonical RSC prewarming

* fix(cloudflare): preserve definitive RSC warm identity

* fix(cloudflare): warm immutable worker versions

* test(form): expect Next-compatible absolute navigation

* fix(cloudflare): harden RSC prewarm deployment

* test(cloudflare): stabilize deployed RSC proof

* test(cloudflare): await RSC prefetch completion

* test(cloudflare): isolate RSC readiness probes

* fix(cloudflare): warm RSC through version overrides

* fix(cloudflare): parse labeled Worker version IDs

* fix(cloudflare): warm document requests as HTML

* fix(cloudflare): retry staged version propagation

* test(cloudflare): seed prewarm proof with prior Worker

* fix(cloudflare): await version override propagation

* fix(cloudflare): serialize staged cache warming

* fix(cloudflare): verify warmed RSC build identity

* fix(cloudflare): harden staged RSC cache warming

* fix(cache): keep RSC prewarm identity build-scoped

* fix(cloudflare): gate warmup on uploaded build

* fix(cache): certify canonical RSC warmup responses

* fix(cache): harden canonical RSC warmup propagation

* fix(cache): close RSC warmup review gaps

* fix(cache): preserve explicit response cache policies

* feat(cloudflare): prewarm canonical RSC variants without prerendering

* fix(cloudflare): forward deploy config from CLI

* fix(cache): close RSC prewarm reuse gaps

* fix(cloudflare): parse labeled Worker version IDs

* fix(router): preserve canonical prefetch invalidation

* test(cloudflare): align RSC seed Worker bindings

* fix(cloudflare): scope warmup deadlines per request

* test(cloudflare): avoid caching stale warmup seed

* fix(cloudflare): fail permanent warmup errors promptly

* fix(router): keep contextual RSC requests hashed

* test(cloudflare): await promoted warmup Worker

* test(cloudflare): retry browser warmup source

* revert: restore focused warmup deadline fix

* fix(cloudflare): remove warmup propagation deadline

* fix(cloudflare): allow slower warmup responses

* fix(cloudflare): confirm warmed cache hits

* fix(cloudflare): confirm cache hits without backoff

* fix(cloudflare): avoid duplicate cache warm requests

* test(cloudflare): allow prewarm propagation

* test(cloudflare): isolate loading prewarm propagation

* test(cloudflare): await promoted prewarm worker

* fix(cloudflare): wait for warm cache propagation before promotion

* test(cloudflare): allow promoted warm cache to settle

* test(cloudflare): wait after promoted build is reachable

* test(cloudflare): poll deployed version before warm cache checks

* test(cloudflare): trace warm cache identity

* test(cloudflare): trace prewarm verification location

* test(cloudflare): compare warm cache clients

* test(cloudflare): trace bypassed response identity

* fix(cloudflare): serialize warm cache variants

* test(cloudflare): report prewarm cache miss context

* test(cloudflare): await target RSC deployment readiness

* test(cloudflare): await stable worker promotion

* test(cloudflare): isolate RSC prewarm target

* fix(cloudflare): defer CDN warmup retries

* fix(cloudflare): verify warmed RSC variants

* test(cloudflare): trace RSC warm cache paths

* fix(cloudflare): separate loading-shell warm cache key

* test(cloudflare): make RSC warm reuse proof deterministic

* test(cloudflare): tolerate stale promoted Worker routing

* fix(rsc): enforce definitive loading-shell cache key
2026-08-24 23:03:12 +01:00
James Anderson 9a876322c5 fix(web): consolidate canonical host SEO signals (#2989) 2026-08-19 00:12:05 +01:00
github-actions[bot] 365c604032 chore: version packages (beta) (#2844)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-15 09:49:29 +00:00
James Anderson c4d645b196 fix(prerender): cache use-cache metadata routes (#2848)
* fix(prerender): expose production build phase

* fix(prerender): load config in production build phase

* fix(prerender): cache use-cache metadata routes

* test(ci): serialize route collision fixture

* fix(prerender): skip uncached metadata seeds
2026-08-10 23:04:04 +01:00
James Anderson d84c4beae3 fix(cache): delegate CDN header cleanup to adapters (#2797)
* fix(cache): delegate CDN header cleanup to adapters

* fix(cache): keep provider headers in CDN adapters

* docs(cache): define adapter header ownership

* test(cache): restore adapter lifecycle coverage

* fix(cache): require Cloudflare-owned CDN adapters

* test(cache): configure adapters in Cloudflare builds

* fix(cache): use the existing Cloudflare CDN adapter

* fix(cache): keep the Cloudflare CDN adapter opt-in

* refactor(cache): keep Cloudflare header policy in CDN adapter
2026-08-10 01:03:14 +01:00
github-actions[bot] 07cf9e5c58 chore: version packages (beta) (#2726)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-07 23:15:07 +01:00
Nathan Nguyen 73f56ef3f8 fix(app-router): honor cacheLife stale on the client router (#2708)
* fix(app-router): honor cacheLife stale on the client router

`cacheLife` profiles carry three independent numbers, and `stale` is the
client-router dimension: how long the browser may reuse cached route output
without asking the server. vinext aggregated it correctly (`resolveCacheLife`
min-reduces all three; the request scope accumulates min-wins) and then
projected it away at every consumer, so the only staleness a browser ever saw
was `dynamicStaleTimeSeconds` from `experimental.staleTimes` — a build-time
constant unrelated to the cached subtrees that produced the render. A subtree
declaring `cacheLife("seconds")` (stale: 30) was held for the full 5-minute
visited-response TTL.

Carry the resolved `stale` to the client on `x-nextjs-stale-time` (matching
Next.js's `NEXT_ROUTER_STALE_TIME_HEADER`) and combine it with the config
value by taking the minimum, so neither min-wins lattice overrides the other.
The 30s prefetch floor applies to the new value too.

Two normalization rules, both deliberate:

- An absent `stale` is never synthesized from `revalidate`/`expire`. The
  `default` profile has no `stale` and an `expire` of ~136 years, so deriving
  one would license session-long reuse without a refresh.
- The three numbers are not assumed to be ordered — `seconds` is
  `{ stale: 30, revalidate: 1, expire: 60 }` — so `revalidate` never
  constrains `stale`. Only the hard `expire` ceiling does.

* fix(app-router): source the client stale time from completed renders

The previous commit derived `x-nextjs-stale-time` from `peekRequestCacheLife()`
at response-construction time. That read happens after `probeAppPageBeforeRender`
but before the RSC stream is consumed, and the probe only awaits the page
component's own async result — it does not render the returned tree. Any
`use cache` scope in a child Server Component registers later, during stream
consumption, so the header described the probe rather than the output.

Because `cacheLife` aggregation is minimum-wins, missing one late scope is
enough to make the value wrong, and "it can only shorten" did not hold: with no
`dynamicStaleTimeSeconds` present (the usual case for `use cache` pages, which
are not dynamic renders), a peeked `stale: 300` widened the prefetch window from
`PREFETCH_CACHE_TTL` (30s) to 300s — 10x longer than today, in the direction the
change set out to fix.

Carry the value on the cache entry instead. The ISR write path reads the
request-scoped accumulation via the consuming `getRequestCacheLife()` inside the
cache-write closure, which runs after the captured stream has drained, so it
observes the completed render's minimum. `CacheControlMetadata` gains `stale`,
`isrSet` persists it, and `buildAppPageCachedResponse` re-advertises it on hits.
Prerender seeds carry it through `VINEXT_PRERENDER_CACHE_LIFE_HEADER` and the
prerender manifest so a seeded entry makes the same claim a runtime render would.

Two links that would otherwise silently drop the claim are closed with it: the
`use cache` entry write now persists `stale`, and `recordRequestScopedCacheControl`
re-registers it on a data-cache hit — without both, a page's advertised freshness
would depend on data-cache temperature rather than on what it declared.

Streaming responses now advertise nothing and leave the client on its configured
`experimental.staleTimes`, which is the honest answer for a value that is not yet
known when headers are committed. Covering fresh streaming renders needs a
render-completion signal the streaming RSC path does not have today; Next.js
solves it by streaming an `AsyncIterable<number>` in the RSC payload
(`app-render.tsx` `baseResponse.s`, closed after the render settles), which is
the natural follow-up.

The client-side combination logic is unchanged: both stale signals are still
min-reduced, absent `stale` is never synthesized from `revalidate`/`expire`,
`revalidate` never clamps `stale`, and `expire` still caps it.

* fix(app-router): deliver the resolved client stale time on fresh renders

The previous round persisted the completed render's cacheLife stale onto
the ISR entry and replayed it on cache hits, which left the value missing
or drifting wherever the entry's lifetime diverged from the render's:

- Fresh streaming renders advertised nothing, so the first visitor of any
  route (and every dev render, since dev writes no ISR entry) stayed on
  the configured staleTimes despite a declared cacheLife. The done-script
  emitted by the RSC embed transform's finalize() runs only after the
  full RSC stream has drained, so it can carry the completed render's
  minimum where streaming headers cannot. Emit the peeked request-scoped
  cacheLife there and seed the hydration visited-response entry from it.
  This also makes the client's min-combination of the config and
  cacheLife signals reachable: a dynamic render with use cache subtrees
  now legitimately carries both.

- The expire clamp bounded the value but not the elapsed window: an
  entry of { stale: 30, expire: 60 } hit at age 59s replayed a 30s reuse
  window reaching 29s past expire. Age the serve-time clamp with the
  entry's lastModified so only the remaining window is advertised.

- The two client caches applied different floors: prefetch entries
  floored a cacheLife stale at 30s while cold navigations honored it
  verbatim, so the same declaration produced two behaviors keyed on
  whether a prefetch fired first. Floor the cacheLife signal once in the
  shared resolver, mirroring Next.js getStaleTimeMs, before the min so
  it can never raise the config-derived bound.

Mechanically, the app-page cache setter's six-position signature
(declared identically in four modules) collapses into one exported
AppPageCacheSetter taking an AppPageCacheWritePolicy object, with
isrSetAppPage adapting to the shared positional isrSet, which stays
unchanged for the Pages Router and route handlers.

* docs(app-router): pin the cold RSC stale contract and expire precedence

The cold RSC fetch gap is a design decision, not a pending follow-up:
Next.js's AsyncIterable stale transport only works under staged rendering
(cacheComponents), where cache scopes settle before the render task queue
drains — in vinext's lazy streaming model the iterable could never close.
Next.js's plain-mode mechanism is a blocking cold render, which #961
deliberately rejected to keep ISR page streams unblocked. Assert the
resulting no-header contract on the streaming-response test.

Also reword the write-policy expire comment: a cacheLife-declared expire
replaces the config expireTime fallback (Next.js precedence), it is not
min-merged with a route-level ceiling — no such ceiling exists outside
cacheLife.

* fix(app-router): carry stale through regen and bound cold responses

Three fixes from review round 3:

Background regeneration dropped the regenerating render's cacheLife stale:
renderAppPageCacheArtifacts returned only { revalidate, expire }, so
resolveRegeneratedAppPageCachePolicy could never receive the stale it was
built to preserve and the first regen silently widened client reuse back
to the configured fallback. The producer now carries it, with a
real-producer regression test the mocked-cacheControl tests could not
provide.

The age-aware expire clamp is removed. Composed with the client's 30s
floor it delivered neither contract (a clamped 1 re-floored to 30), and
cached HTML replayed the unclamped done-script value regardless. Next.js
stores the stale header at generation and replays it verbatim on every
hit; vinext now does the same, keeping expire a serve-side ceiling.

Cold cacheable RSC responses stream before their cacheLife resolves
(#961), which left them on the 300s client fallback — reproducing the
headline bug for the first request of every entry epoch. They now carry
X-Vinext-Stale-Time-Pending, and both client caches bound such responses
at the 30s floor: the unresolved claim, once floored, could never
license less.

* fix(app-router): bound pending-stale responses by the dynamic stale time

The pending marker meant 'capture was attempted', but the client read it
as 'a cacheLife claim exists'. Capture eligibility is decided before the
lazy stream runs, so a late request-API read can make the completed
render dynamic — the finalizer skips the ISR write and no claim ever
exists, yet the marker granted 30 seconds of reuse even under
staleTimes.dynamic: 0.

Pending responses now carry the configured dynamic stale time (including
0) and the client takes the minimum, so an unresolved response never
receives a wider window than the dynamic bound.

* fix(app-router): keep the pending cap independent of staleTimes.dynamic

Pairing the pending marker with the configured dynamic stale time broke
the segment-cache-client-params compat test: dynamic-param routes
prefetch with no minimum TTL, so the paired 0 default made every cold
prefetch entry expire instantly and navigations refetched routes that
Next.js serves entirely from a static prefetch.

A cold stream cannot distinguish a render that will resolve static from
one that turns dynamic mid-stream, and bounding both by staleTimes.dynamic
sacrifices the guaranteed-correct case for the ambiguous one. Pending
responses go back to the 30s floor cap; the late-dynamic exposure
(one epoch-cold response, at most 30s) is documented as the price of
non-blocking cold renders (#961).

* fix: carry the client stale claim through KV, the prerender index, and nested cache hits

- writePrerenderIndex now copies `stale` into vinext-prerender.json so
  seedMemoryCacheFromPrerender actually receives it
- KVCacheHandler.set() and buildPrerenderKVPairs persist cacheControl.stale
  so warm hits on the Cloudflare KV backend replay the producing render's
  claim; validateCacheEntry accepts the field
- a nested use cache HIT pushes its stored lifetime into the enclosing
  cache context's lifeConfigs, mirroring the MISS path, so the outer entry
  keeps the child's stale claim once the outer goes warm

* perf: trim shipped comment bytes in the stale-time plumbing

The added modules ride in every consumer build environment; the review-grade
rationale lives in the PR body, the code keeps one-line constraints.

* refactor(app-router): model the cached client stale claim as one state

CachedRscResponse carried staleTimePending and staleTimeSeconds as two
independent optionals, so the cached form admitted a state the wire never
produces (pending and resolved at once) and every consumer had to encode
the precedence rule. Replace both with a discriminated serverStaleTime
(pending | resolved), collapsed once at the header-parse boundary.

* refactor(isr): give isrSet a cache-metadata write policy

The generic setter had grown to six positional arguments, the last an
App-Router-only `stale` value, with isrSetAppPage as a policy-object
wrapper that unpacked straight back into it. Take { cacheControl, tags }
instead: routers construct the metadata they actually claim, the wrapper
and its type disappear, and the shared isrCacheControl builder replaces
the cache-control literal duplicated across writers.

* fix(app-router): keep the dynamic bound on captured dynamic renders

The done-script metadata reused the RSC header's rule of dropping the
config-derived dynamic stale time while the speculative ISR capture was
armed. That rule only holds on the header path, which substitutes the
pending marker and its 30s floor; the done script emits the resolved
cacheLife instead, so a production render that turned dynamic shipped the
cacheLife claim as its only bound and let the hydration-seeded entry reuse
dynamic output for its full duration. Dev never took the capture path, so
the two diverged.

Also migrates the pages-basic seed fixture, the last positional isrSet
caller, which typecheck does not cover.

* ci: re-run after unrelated dev-overlay canary flake

* fix(app-router): preserve completed client stale metadata

* fix(app-router): strip completion metadata during HMR

* fix(app-router): stream completion metadata safely

* fix(app-router): validate completed stale metadata

* docs(cache): clarify zero stale client claim

---------

Co-authored-by: James <james@eli.cx>
2026-07-28 14:41:47 +01:00
github-actions[bot] 05eee9fecc chore: version packages (beta) (#2673)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-24 16:23:30 +01:00
James Anderson 95661c2192 fix(cache): preserve prerendered page cache tags (#709)
* feat(cache): implement Next.js 16 revalidateTag two-phase stale/expired model

- Add TagRevalidationDurations interface and update CacheHandler interface
- Rewrite MemoryCacheHandler to use stale/expired TagManifestEntry model
- Rewrite KVCacheHandler with KVTagEntry JSON format (backward-compat with legacy plain-timestamp)
- Add deprecation warning to public revalidateTag() when called without profile
- SWR semantics when profile with expire>0: mark stale immediately, hard-expire after window
- Hard invalidation when no profile or expire=0: set expired=now, next get() is a miss
- Fix >= comparisons for same-millisecond set()+revalidateTag() correctness
- Add tests for deprecation warning, SWR stale return, expire=0 hard miss, JSON KV format

* refactor(cache): address bonk review comments

- Document >= vs > as deliberate divergence from Next.js with rationale
  (same-millisecond set+revalidateTag must invalidate; strict > would allow
  stale serves when both events share a timestamp)
- Export TagRevalidationDurations interface from next-shims.d.ts and use it
  in CacheHandler, MemoryCacheHandler, and revalidateTag signatures
- Remove dead ...existing spread in MemoryCacheHandler.revalidateTag — both
  branches fully overwrite the TagManifestEntry, the spread could accidentally
  preserve stale fields if the interface grows
- Tighten deprecation warning test regex to match the exact emitted message

* fix(cache): match Next.js updateTags branch logic for durations without expire

When revalidateTag is called with a truthy durations object, stale is now
always written to the tag manifest regardless of whether expire is set.
The expired field is only set when durations.expire !== undefined.

This fixes two divergences from Next.js default.ts updateTags:
- revalidateTag('tag', {}) → { stale: now } (stale-only SWR), was { expired: now }
- revalidateTag('tag', { expire: 0 }) → { stale: now, expired: now }, was { expired: now }

Applies the same fix to both MemoryCacheHandler and KVCacheHandler.
Adds tests covering the new stale-only ({}) and expire=0 shapes.

* fix(cache): reuse now variable in MemoryCacheHandler.get for time-based expiry check

* fix(cache): preserve prerendered page cache tags
2026-07-22 20:33:12 +01:00
github-actions[bot] 3d30a3ca36 chore: version packages (beta) (#2632)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-21 23:24:35 +01:00
Nathan Nguyen 060de14a0b fix(cloudflare): report custom-domain deploy URLs (#2630)
* fix(cloudflare): report custom-domain deploy URLs

Cloudflare deploys that disable workers.dev currently finish with "(URL not detected in wrangler output)" even when Wrangler confirms a custom-domain target. Wrangler emits custom domains as bare hostnames, while vinext only parses HTTPS workers.dev URLs.

Parse Wrangler custom-domain target markers, validate the hostname as an HTTPS origin, and ignore wildcard routes and disabled domains. Preserve workers.dev URL precedence and cover the deploy boundary plus Wrangler 4.110 output variants.

* ci: retry flaky unit test shard

The upstream unit-test shard failed while removing a temporary typegen directory after all test assertions passed. The affected test is unrelated to this branch and passed 20 consecutive targeted runs locally.\n\nTrigger a fresh GitHub Actions run without changing the pull request diff.

---------

Co-authored-by: James <james@eli.cx>
2026-07-21 21:18:24 +01:00
github-actions[bot] 7eccf8e898 chore: version packages (beta) (#2616)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-16 12:37:23 +00:00
Andrew 106398d494 fix(cache): guard 'use cache' key against Cloudflare KV's 512-byte limit (#2606)
* fix(cache): guard 'use cache' KV key against Cloudflare's 512-byte limit

A long dynamic-route slug flows into the 'use cache' KV key via the
serialized args. When the assembled key exceeded Cloudflare KV's 512-byte
key limit, handler.get threw a 414 *before* the wrapped render reached
notFound()/redirect(), masking those control-flow signals and surfacing a
catch-all not-found as a 200 error boundary instead of a 404 (soft-404).

- buildUseCacheKey now hashes the oversized parts (fnv1a64), mirroring the
  ISR cache's guard in isr-cache.ts (buildCacheKey); the readable
  function-scoped prefix is preserved when it fits so distinct cached
  functions never collide.
- handler.get is wrapped so any cache-store failure falls through to fresh
  execution, letting the function's own thrown digest propagate.

Regression tests added in tests/shims.test.ts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(cache): enforce Cloudflare KV key limits

* refactor(cache): keep key hashing in KV adapter

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: James <james@eli.cx>
2026-07-15 00:37:21 +01:00
James Anderson 645d1a1c74 fix(create): make create-vinext-app work with npm and npx (#2618)
* fix(create): make create-vinext-app work with npx

* fix(ci): run packed creator directly with npx

* fix(cloudflare): support vinext prerelease peers
2026-07-14 20:25:39 +01:00
github-actions[bot] 8d0a18d440 chore: version packages (beta) (#2549)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-10 08:44:13 +00:00