-
Notifications
You must be signed in to change notification settings - Fork 351
Expand file tree
/
Copy pathisr-decision.ts
More file actions
188 lines (173 loc) · 6.91 KB
/
Copy pathisr-decision.ts
File metadata and controls
188 lines (173 loc) · 6.91 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
/**
* Centralised ISR `Cache-Control` derivation module.
*
* `decideIsr` is the single place that maps (router kind, cache state,
* revalidate/expire metadata) → the exact `Cache-Control` string to stamp on
* an ISR response. Every ISR code path (app-page, app-route, pages,
* dev-server) routes through it.
*
* `disposition` and `scheduleRegeneration` are informational fields for
* callers that want them; all current callers only read `cacheControl`.
*/
import type { CacheControlMetadata } from "vinext/shims/cache-handler";
import {
buildCachedRevalidateCacheControl,
buildRevalidateCacheControl,
INFINITE_CACHE_SECONDS,
NEVER_CACHE_CONTROL,
NO_STORE_CACHE_CONTROL,
STATIC_CACHE_CONTROL,
} from "./cache-control.js";
type IsrDisposition = "HIT" | "STALE" | "MISS";
type IsrDecision = {
disposition: IsrDisposition;
/** True when the caller must schedule a background regeneration. */
scheduleRegeneration: boolean;
/** The `Cache-Control` string to stamp on the response. */
cacheControl: string;
};
/**
* Per-router special-case policies for `Cache-Control`.
*
* - `"app-page"` / `"pages"`: `buildCachedRevalidateCacheControl` for HIT/STALE.
* - `"app-route"`: same, but `revalidateSeconds=0` forces `NEVER_CACHE_CONTROL`
* and `revalidateSeconds=Infinity` forces `STATIC_CACHE_CONTROL`.
* - `"dev"`: like `"pages"`, but `revalidate=0`/`Infinity` guards are absent
* (dev never caches when revalidate=0 and never has Infinity entries in practice).
*/
type IsrPolicyKind = "app-page" | "app-route" | "pages" | "dev";
type DecideIsrOptions = {
/**
* The cache state. Content guards (kind-mismatch, empty body,
* query-variant-unproven) must have already passed before passing
* `"HIT"` or `"STALE"` here.
*/
cacheState: "HIT" | "STALE" | "MISS";
/** Which router is making the decision. */
kind: IsrPolicyKind;
/**
* The route's configured revalidate window in seconds. Used as the fallback
* when `cacheControlMeta` is absent.
*
* For `"dev"` call sites this is the only source of the revalidate value —
* dev never has metadata attached to a cache entry.
*/
revalidateSeconds: number;
/**
* The expire ceiling (seconds from epoch) read from the route config.
* Absent when the route pre-dates expire metadata support.
*/
expireSeconds?: number;
/**
* Optional per-entry metadata written alongside the cache value.
* When present its `revalidate`/`expire` fields override the route defaults,
* exactly as the call sites do today with `cacheControl?.revalidate ?? revalidateSeconds`.
*/
cacheControlMeta?: CacheControlMetadata;
};
/** Resolve effective revalidate/expire, preferring per-entry metadata. */
function resolveRevalidate(options: DecideIsrOptions): {
effectiveRevalidate: number;
effectiveExpire: number | undefined;
} {
const effectiveRevalidate = options.cacheControlMeta?.revalidate ?? options.revalidateSeconds;
// `expireSeconds` is the route-level config fallback. It is only meaningful
// when per-entry metadata is present — it acts as the fallback for entries
// written before expire support was added. When `cacheControlMeta` is absent
// entirely, the expire ceiling is unknown (undefined), matching the
// original per-call-site logic:
//
// const expire = options.cacheControl === undefined
// ? undefined
// : (options.cacheControl.expire ?? options.expireSeconds);
const effectiveExpire =
options.cacheControlMeta === undefined
? undefined
: (options.cacheControlMeta.expire ?? options.expireSeconds);
return { effectiveRevalidate, effectiveExpire };
}
function buildCacheControl(
disposition: "HIT" | "STALE",
kind: IsrPolicyKind,
revalidate: number,
expire: number | undefined,
): string {
if (kind === "app-route") {
if (revalidate === 0) return NEVER_CACHE_CONTROL;
if (revalidate === Infinity || revalidate === INFINITE_CACHE_SECONDS) {
return STATIC_CACHE_CONTROL;
}
}
return buildCachedRevalidateCacheControl(disposition, revalidate, expire);
}
/**
* Derive the `Cache-Control` string for an ISR response.
*
* Content guards (kind mismatch, query-variant-unproven, empty body) are the
* caller's responsibility and must happen *before* this call. `cacheState`
* must only be `"HIT"` or `"STALE"` when those guards have already passed.
*/
export function decideIsr(options: DecideIsrOptions): IsrDecision {
if (options.cacheState === "MISS") {
return { disposition: "MISS", scheduleRegeneration: false, cacheControl: "" };
}
const { effectiveRevalidate, effectiveExpire } = resolveRevalidate(options);
if (options.cacheState === "HIT") {
return {
disposition: "HIT",
scheduleRegeneration: false,
cacheControl: buildCacheControl("HIT", options.kind, effectiveRevalidate, effectiveExpire),
};
}
// STALE: serve + schedule regen.
return {
disposition: "STALE",
scheduleRegeneration: true,
cacheControl: buildCacheControl("STALE", options.kind, effectiveRevalidate, effectiveExpire),
};
}
/**
* Build the `Cache-Control` string for a fresh MISS response whose ISR policy
* is known (i.e. revalidate is set and > 0). Uses the unbounded SWR form when
* no expire ceiling is available, exactly as `buildRevalidateCacheControl` does.
*
* Separate from `decideIsr` because a MISS doesn't read a cache entry and
* therefore never has `cacheControlMeta`. `expireSeconds` here is the route
* config ceiling passed directly from the caller (not a per-entry fallback).
*/
export function buildMissIsrCacheControl(
revalidateSeconds: number,
expireSeconds?: number,
): string {
return buildRevalidateCacheControl(revalidateSeconds, expireSeconds);
}
/**
* Build the `Cache-Control` string for a fresh (MISS) app-route response.
*
* Applies the same `revalidateSeconds=0`→NEVER and `Infinity`→STATIC gates
* that `decideIsr` uses for app-route cached responses. `expireSeconds` is
* the route config ceiling passed directly (not per-entry metadata fallback).
*
* Used by `applyRouteHandlerRevalidateHeader` which operates on a fresh
* response that has no per-entry cache metadata.
*/
export function buildAppRouteMissIsrCacheControl(
revalidateSeconds: number,
expireSeconds?: number,
): string {
if (revalidateSeconds === 0) return NEVER_CACHE_CONTROL;
if (revalidateSeconds === Infinity || revalidateSeconds === INFINITE_CACHE_SECONDS) {
return STATIC_CACHE_CONTROL;
}
return buildRevalidateCacheControl(revalidateSeconds, expireSeconds);
}
/**
* The `Cache-Control` for a response that must never be cached (getServerSideProps
* default, on-demand revalidation, nonce-bearing pages). Matches `NEVER_CACHE_CONTROL`.
*/
export { NEVER_CACHE_CONTROL as ISR_NEVER_CACHE_CONTROL };
/**
* The `Cache-Control` for a nonce-bearing ISR response (the page has a
* script nonce, so it must not enter any shared cache). Matches `NO_STORE_CACHE_CONTROL`.
*/
export { NO_STORE_CACHE_CONTROL as ISR_NO_STORE_CACHE_CONTROL };