what-isr 0.12.3 → 0.12.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.d.ts +91 -22
- package/package.json +2 -2
package/index.d.ts
CHANGED
|
@@ -20,12 +20,30 @@ export interface PageCacheConfig {
|
|
|
20
20
|
*/
|
|
21
21
|
vary?: string[] | string;
|
|
22
22
|
fallback?: 'blocking' | boolean;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
pollInterval?: number;
|
|
23
|
+
/** 'stale-if-error' serves an expired entry rather than failing when a cold render throws. */
|
|
24
|
+
onMiss?: 'stale-if-error' | 'blocking' | string;
|
|
26
25
|
}
|
|
26
|
+
// `pollInterval` was declared on the config above and read by nothing, here or
|
|
27
|
+
// in what-server: setting it produced no error and no regeneration. Poll
|
|
28
|
+
// regeneration itself is real, but it is driven explicitly, because it only
|
|
29
|
+
// means anything in a long-lived process. Build a scheduler, register the routes
|
|
30
|
+
// worth keeping warm, and hand it to the Node adapter, which starts and stops
|
|
31
|
+
// it with the server:
|
|
32
|
+
//
|
|
33
|
+
// const scheduler = createScheduler(engine);
|
|
34
|
+
// scheduler.register({ path: '/', query: {}, params: {}, config: page, route }, { intervalMs: 300_000 });
|
|
35
|
+
// createServer({ routes, cache: engine, scheduler });
|
|
36
|
+
//
|
|
37
|
+
// Declaring the key again is only honest once something resolves it into that
|
|
38
|
+
// register() call for every route that sets it.
|
|
27
39
|
|
|
28
40
|
// --- Stores ---
|
|
41
|
+
// The time fields are the ones makeEntry() writes and isFresh() /
|
|
42
|
+
// isServableStale() / the Redis TTL read. They were declared as
|
|
43
|
+
// `createdAt` + `revalidate` + `swr`, none of which exist on a real entry: a
|
|
44
|
+
// hand-built entry following that shape has no `expiresAt`, so isFresh()
|
|
45
|
+
// compares against undefined, every read is a miss, and the cache silently
|
|
46
|
+
// never serves. Build entries with makeEntry() rather than by hand.
|
|
29
47
|
export interface CacheEntry {
|
|
30
48
|
html: string;
|
|
31
49
|
head?: string;
|
|
@@ -33,21 +51,28 @@ export interface CacheEntry {
|
|
|
33
51
|
status?: number;
|
|
34
52
|
tags?: string[];
|
|
35
53
|
path?: string;
|
|
54
|
+
/** A partial render (skeleton/streamed shell). */
|
|
55
|
+
partial?: boolean;
|
|
36
56
|
/** Per-user render: never stored, never served from a shared cache. */
|
|
37
57
|
private?: boolean;
|
|
38
|
-
/** Epoch ms when the entry was
|
|
39
|
-
|
|
40
|
-
/** Seconds
|
|
41
|
-
|
|
42
|
-
|
|
58
|
+
/** Epoch ms when the entry was rendered. */
|
|
59
|
+
renderedAt: number;
|
|
60
|
+
/** Seconds until stale. 0 means "always revalidate" when `revalidate: 0` was declared. */
|
|
61
|
+
maxAge: number;
|
|
62
|
+
/** Extra seconds the entry may be served stale while it regenerates. */
|
|
63
|
+
swrWindow: number;
|
|
64
|
+
/** Epoch ms the entry goes stale, or Infinity for a durable static entry. */
|
|
65
|
+
expiresAt: number;
|
|
43
66
|
}
|
|
44
67
|
|
|
45
68
|
export interface CacheStore {
|
|
46
69
|
get(key: string): Promise<CacheEntry | undefined>;
|
|
47
70
|
set(key: string, entry: CacheEntry): Promise<void>;
|
|
48
71
|
delete(key: string): Promise<boolean>;
|
|
72
|
+
/** Both delete-by helpers return the keys they removed. */
|
|
49
73
|
deleteByTag(tag: string): Promise<string[]>;
|
|
50
|
-
|
|
74
|
+
/** Not optional: revalidatePath() calls this unconditionally, so a store without it throws on the first purge. */
|
|
75
|
+
deleteByPath(path: string): Promise<string[]>;
|
|
51
76
|
clear(): Promise<void>;
|
|
52
77
|
keys(): Promise<string[]>;
|
|
53
78
|
}
|
|
@@ -56,7 +81,18 @@ export function createMemoryStore(options?: { max?: number }): CacheStore;
|
|
|
56
81
|
export function createFilesystemStore(options: { dir: string; shards?: number }): CacheStore;
|
|
57
82
|
export function createRedisStore(options: { client: unknown; prefix?: string }): CacheStore;
|
|
58
83
|
|
|
59
|
-
|
|
84
|
+
/** Fill an entry's time fields from a render result plus the route config. */
|
|
85
|
+
export function makeEntry(
|
|
86
|
+
out: Partial<RenderResult> & {
|
|
87
|
+
path?: string;
|
|
88
|
+
partial?: boolean;
|
|
89
|
+
private?: boolean;
|
|
90
|
+
/** A render that read request headers is per-user, so it stores as `private`. */
|
|
91
|
+
usedRequestHeaders?: boolean;
|
|
92
|
+
},
|
|
93
|
+
config?: PageCacheConfig,
|
|
94
|
+
now?: number,
|
|
95
|
+
): CacheEntry;
|
|
60
96
|
export function isFresh(entry: CacheEntry, now?: number): boolean;
|
|
61
97
|
export function isServableStale(entry: CacheEntry, now?: number): boolean;
|
|
62
98
|
|
|
@@ -95,10 +131,20 @@ export function createFastlyCDN(options: { serviceId: string; apiToken: string;
|
|
|
95
131
|
export function createVercelCDN(options: { projectId: string; token: string; teamId?: string }): CDNAdapter;
|
|
96
132
|
|
|
97
133
|
// --- Headers ---
|
|
98
|
-
export
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
134
|
+
export type CacheStatus = 'HIT' | 'STALE' | 'MISS' | 'BYPASS';
|
|
135
|
+
/**
|
|
136
|
+
* Build the Cache-Control / X-What-Cache / Cache-Tag headers for a response.
|
|
137
|
+
* The second argument is the ROUTE CONFIG, not the cache status: the previous
|
|
138
|
+
* declaration ((entry, status, { cdn })) described a function that does not
|
|
139
|
+
* exist, so a caller who followed it passed the status where the config goes.
|
|
140
|
+
*/
|
|
141
|
+
export function buildCacheHeaders(
|
|
142
|
+
entry?: Partial<CacheEntry>,
|
|
143
|
+
config?: PageCacheConfig,
|
|
144
|
+
cacheStatus?: CacheStatus,
|
|
145
|
+
/** The declared vary the cache key used. Falls back to `config.vary`. */
|
|
146
|
+
vary?: string[],
|
|
147
|
+
): Record<string, string>;
|
|
102
148
|
|
|
103
149
|
// --- Static paths ---
|
|
104
150
|
export interface StaticPathEntry {
|
|
@@ -132,14 +178,17 @@ export interface RenderResult {
|
|
|
132
178
|
status?: number;
|
|
133
179
|
tags?: string[];
|
|
134
180
|
}
|
|
135
|
-
|
|
181
|
+
/** The engine calls render(routeMatch, ctx); `ctx` is reserved and currently `{}`. */
|
|
182
|
+
export type RenderFn = (routeMatch: RouteMatch, ctx?: Record<string, unknown>) => RenderResult | Promise<RenderResult>;
|
|
136
183
|
|
|
137
184
|
// --- ISR engine ---
|
|
138
185
|
export interface ServeResult {
|
|
139
186
|
html: string;
|
|
187
|
+
head?: string;
|
|
188
|
+
state?: unknown;
|
|
140
189
|
status: number;
|
|
141
190
|
headers: Record<string, string>;
|
|
142
|
-
cacheStatus:
|
|
191
|
+
cacheStatus: CacheStatus;
|
|
143
192
|
}
|
|
144
193
|
export interface RevalidateOptions {
|
|
145
194
|
regenerate?: boolean;
|
|
@@ -147,8 +196,13 @@ export interface RevalidateOptions {
|
|
|
147
196
|
}
|
|
148
197
|
export interface CacheEngine {
|
|
149
198
|
handle(routeMatch: RouteMatch, renderOverride?: RenderFn): Promise<ServeResult>;
|
|
150
|
-
|
|
151
|
-
|
|
199
|
+
/** Render and re-store one route now. What the poll scheduler calls per tick. */
|
|
200
|
+
regenerate(routeMatch: RouteMatch): Promise<CacheEntry>;
|
|
201
|
+
/** Both purges resolve to the cache keys they deleted, not to void. */
|
|
202
|
+
revalidatePath(path: string, options?: RevalidateOptions): Promise<string[]>;
|
|
203
|
+
revalidateTag(tag: string, options?: RevalidateOptions): Promise<string[]>;
|
|
204
|
+
/** The cache key for a route match. Throws when the route declares `vary` and no headers were supplied. */
|
|
205
|
+
keyFor(routeMatch: RouteMatch): string;
|
|
152
206
|
store: CacheStore;
|
|
153
207
|
}
|
|
154
208
|
export function createCacheEngine(options?: {
|
|
@@ -174,9 +228,24 @@ export function createRevalidateWebhook(
|
|
|
174
228
|
): (req: WebhookRequest) => Promise<WebhookResponse>;
|
|
175
229
|
|
|
176
230
|
// --- Poll scheduler ---
|
|
231
|
+
// Register the routes to keep warm, then start it (or hand it to what-server's
|
|
232
|
+
// createServer, which starts it and stops it on SIGTERM/SIGINT). Every method
|
|
233
|
+
// returns the scheduler so registration can chain.
|
|
177
234
|
export interface Scheduler {
|
|
178
|
-
register(route: RouteMatch, options: { intervalMs: number }):
|
|
179
|
-
start():
|
|
180
|
-
stop():
|
|
235
|
+
register(route: RouteMatch, options: { intervalMs: number }): Scheduler;
|
|
236
|
+
start(): Scheduler;
|
|
237
|
+
stop(): Scheduler;
|
|
238
|
+
}
|
|
239
|
+
// The runtime reads maxConcurrent/random/setTimer/clearTimer/logger. The
|
|
240
|
+
// `concurrency` and `jitter` declared before were read by nothing, so a caller
|
|
241
|
+
// capping concurrency got the default 4 and no warning.
|
|
242
|
+
export interface SchedulerOptions {
|
|
243
|
+
/** Regenerations allowed in flight at once. Default 4. */
|
|
244
|
+
maxConcurrent?: number;
|
|
245
|
+
/** Jitter source (0..1), injectable for deterministic tests. Intervals are spread by up to +10%. */
|
|
246
|
+
random?: () => number;
|
|
247
|
+
setTimer?: (fn: () => void, ms: number) => any;
|
|
248
|
+
clearTimer?: (timer: any) => void;
|
|
249
|
+
logger?: Pick<Console, 'error' | 'warn' | 'log'>;
|
|
181
250
|
}
|
|
182
|
-
export function createScheduler(engine: CacheEngine, options?:
|
|
251
|
+
export function createScheduler(engine: CacheEngine, options?: SchedulerOptions): Scheduler;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "what-isr",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.4",
|
|
4
4
|
"description": "What Framework - Origin-first ISR cache engine: stale-while-revalidate, on-demand & poll regeneration, no CDN required",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"author": "ZVN DEV (https://zvndev.com)",
|
|
31
31
|
"license": "MIT",
|
|
32
32
|
"peerDependencies": {
|
|
33
|
-
"what-server": "^0.12.
|
|
33
|
+
"what-server": "^0.12.4"
|
|
34
34
|
},
|
|
35
35
|
"peerDependenciesMeta": {
|
|
36
36
|
"what-server": {
|