refactor(isr): centralise HIT/STALE/MISS + Cache-Control decision in isr-decision.ts#1883
Conversation
…isr-decision.ts Add server/isr-decision.ts as the single owner of the ISR cache policy decision across all four call sites (app-page-cache, app-route-handler, pages-page-data, dev-server). Every cache disposition, background-regen flag, and Cache-Control string now flows through decideIsr() or its MISS helpers. Also eliminate all hardcoded Cache-Control literals outside cache-control.ts (NEVER_CACHE_CONTROL, NO_STORE_CACHE_CONTROL, and raw s-maxage strings in dev-server.ts, pages-page-response.ts, pages-page-handler.ts). One deliberate behaviour change: dev STALE responses now emit s-maxage=0, stale-while-revalidate (matching prod Pages Router) instead of s-maxage=<revalidate>, stale-while-revalidate. This closes a dev/prod parity gap that was masked by the duplication. Closes cloudflare#1783
commit: |
|
/bigbonk review for issues |
There was a problem hiding this comment.
Reviewed the refactor end-to-end against cache-control.ts and all four migrated call sites. The equivalence claims hold up:
- app-page HIT/STALE: precedence
cacheControlMeta?.revalidate ?? revalidateSecondsand theexpireSecondsonly-when-meta-present rule are preserved exactly.Infinitystill routes throughbuildCachedRevalidateCacheControl's internal STATIC gate. ✅ - app-route cached + MISS: the old
buildRouteHandlerCacheControl("HIT", ...)for the MISS path reduced tobuildRevalidateCacheControl(r, e)for finite values, which is exactly what the newbuildAppRouteMissIsrCacheControlemits. The0→NEVER /Infinity→STATIC gates are carried over. ✅ - pages HIT/STALE:
cacheControl?.revalidate ?? revalidateSeconds ?? 60is preserved (meta insidedecideIsr,?? 60hoisted toeffectiveRevalidateSeconds). ✅ - dev MISS / nonce / gssp-default: byte-for-byte identical to the previous literals. ✅
- dev STALE: the one deliberate change (
s-maxage=<secs>→s-maxage=0, stale-while-revalidate) is correct and well-justified — it brings dev in line with prod Pages Router. No existing test asserted the old dev STALE header (tests/features.test.tsonly checksx-vinext-cache), so nothing breaks. ✅
Ran tests/isr-decision.test.ts (33 pass) plus app-page-cache, app-route-handler-response, pages-page-data, cache-control (87 pass). All green.
The refactor is solid and removes real copy-paste duplication. One scope/accuracy nit on the module docstring below — not blocking.
| /** | ||
| * Centralised ISR cache-decision module. | ||
| * | ||
| * The HIT/STALE/MISS disposition, the `scheduleRegeneration` flag, and the |
There was a problem hiding this comment.
The docstring claims this module is the single owner of the disposition and scheduleRegeneration flag, and that "No caller may produce these values independently." In practice that isn't what landed: every migrated call site computes isStale (and therefore the disposition) itself and passes it in, then destructures only { cacheControl } from the result. disposition and scheduleRegeneration are never read by any caller (verified across app-page-cache.ts, app-route-handler-response.ts, pages-page-data.ts, dev-server.ts).
So what this module actually centralizes is the Cache-Control string, not the disposition decision. Consider tightening the prose to match the real (still valuable) scope, otherwise the next reader will assume disposition logic is owned here when it isn't. Either drop the unused disposition/scheduleRegeneration fields, or note them as informational-only for now.
| * caller's responsibility and must happen *before* this call. `hasUsableValue` | ||
| * must only be true when those guards have already passed. | ||
| */ | ||
| export function decideIsr(options: DecideIsrOptions): IsrDecision { |
There was a problem hiding this comment.
Minor: resolveRevalidate(options) is called identically in both the HIT and STALE branches. You could hoist it above the if (!options.isStale) to compute it once and remove the duplication (the MISS early-return already short-circuits before either branch). Not a correctness issue.
|
Review submitted successfully. Here's my summary: Review of PR #1883 —
|
…alue/isStale Replace the two-field hasUsableValue+isStale decomposition with a single cacheState: "HIT" | "STALE" | "MISS" parameter that callers already have. Also remove the before/after equivalence table from the module docstring, which is only relevant during the migration window. All five call sites updated; tsc --noEmit passes with no errors.
|
/bigbonk re-review for issues |
There was a problem hiding this comment.
Re-review (/bigbonk). I re-traced every migrated call site against cache-control.ts at the current HEAD (78c5a38) and ran the relevant suites — tests/isr-decision.test.ts (33), cache-control (11), app-page-cache (30), app-route-handler-response (20), pages-page-data (26): all green. vp check on the new module is clean (format + lint + types).
Prior review comments are now resolved by later commits:
072fe61hoistedresolveRevalidateabove the HIT/STALE branches (was ask-bonk's dedup nit) and tightened the docstring sodisposition/scheduleRegenerationare honestly labelled informational.a571a1bswitched callers to passcacheStatedirectly (james-elicx's "infer from state" suggestion).
Equivalence verification (all confirmed byte-for-byte):
- app-page HIT/STALE —
cacheControlMeta?.revalidate ?? revalidateSecondsprecedence and theexpire-only-when-meta-present rule preserved;Infinitystill routes throughbuildCachedRevalidateCacheControl's STATIC gate. - app-route cached + MISS — old
buildRouteHandlerCacheControl("HIT", …)for the MISS path reduces tobuildRevalidateCacheControl(r, e)for finite values, exactly whatbuildAppRouteMissIsrCacheControlemits;0→NEVER /Infinity→STATIC gates carried over. - pages HIT/STALE — verified the
?? 60hoist preserves precedence. Original(cacheControl?.revalidate ?? revalidateSeconds) ?? 60vs newcacheControlMeta?.revalidate ?? (revalidateSeconds ?? 60)are equivalent for every input (the inner/outer??only fire on null/undefined, and0short-circuits identically in both). - dev nonce / gssp-default / MISS — identical to the previous literals.
- dev STALE — the one deliberate change (
s-maxage=<secs>→s-maxage=0, stale-while-revalidate). Confirmed no E2E or unit test asserts the old dev STALE header, so nothing breaks; this is a genuine dev/prod parity fix.
No blocking issues. One minor leftover noted inline.
| * an ISR response. Every ISR code path (app-page, app-route, pages, | ||
| * dev-server) routes through it. | ||
| * | ||
| * `disposition` and `scheduleRegeneration` are informational fields for |
There was a problem hiding this comment.
The docstring now honestly labels these as informational, which resolves the earlier concern. But disposition and scheduleRegeneration are still returned by decideIsr and read by no caller anywhere in the repo (verified with a repo-wide grep — only the test references .disposition/.scheduleRegeneration). Since YAGNI-style dead return surface tends to rot, consider either dropping them now and returning just { cacheControl }, or keeping them only if there's a concrete near-term consumer. Not blocking — the prose is accurate as-is.
|
Re-review submitted successfully. SummaryI re-reviewed PR #1883 at the current HEAD ( Key findings:
One non-blocking inline comment: the |
Closes #1783
What
Adds
server/isr-decision.tsas the single owner of the ISR cache policy decision. All four call sites that previously re-derived HIT/STALE/MISS disposition, background-regen scheduling, andCache-Controlstring selection now delegate todecideIsr()or its MISS helpers.Also eliminates all hardcoded
Cache-Controlliterals outsidecache-control.ts— raw strings like"private, no-cache, no-store, max-age=0, must-revalidate"that were copy-pasted acrossdev-server.ts,pages-page-response.ts, andpages-page-handler.ts.Call sites migrated
server/app-page-cache.ts— removed localbuildAppPageCacheControlwrapperserver/app-route-handler-response.ts— removed localbuildRouteHandlerCacheControl; cached responses usedecideIsr(kind:"app-route"), fresh MISS responses usebuildAppRouteMissIsrCacheControl(preserves therevalidate=0→NEVER andInfinity→STATIC gates)server/pages-page-data.ts—buildPagesCacheResponsedelegates todecideIsr(kind:"pages")server/dev-server.ts— all four inline literals replacedDeliberate behaviour change
Dev STALE responses now emit
s-maxage=0, stale-while-revalidate(matching prod Pages Router and the canonicalbuildCachedRevalidateCacheControlhelper) instead ofs-maxage=<revalidate>, stale-while-revalidate. The old value incorrectly told downstream caches that a stale-served payload was freshly cacheable — a dev/prod parity gap that was masked by the duplication.Equivalence
Every other migrated path emits the same header as before. The full per-call-site equivalence table is in the
isr-decision.tsmodule doc comment.Tests
tests/isr-decision.test.ts— 33 new unit tests covering all dispositions, router kinds,0/Infinityspecial cases, metadata fallback semantics, and the MISS helpersapp-page-cache,app-route-handler-cache,app-route-handler-response,pages-page-data,isr-cache,cache-control,features