what-isr 0.12.2 → 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.
Files changed (2) hide show
  1. package/index.d.ts +91 -22
  2. 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
- onMiss?: 'blocking' | string;
24
- /** Background regeneration interval, in seconds. */
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 created. */
39
- createdAt: number;
40
- /** Seconds-to-stale snapshot from the page config. */
41
- revalidate?: number;
42
- swr?: number;
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
- deleteByPath?(path: string): Promise<string[]>;
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
- export function makeEntry(out: Partial<CacheEntry>, config?: PageCacheConfig, now?: number): CacheEntry;
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 interface CacheHeaderOptions {
99
- cdn?: boolean;
100
- }
101
- export function buildCacheHeaders(entry?: CacheEntry, status?: 'HIT' | 'STALE' | 'MISS', options?: CacheHeaderOptions): Record<string, string>;
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
- export type RenderFn = (routeMatch: RouteMatch) => RenderResult | Promise<RenderResult>;
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: 'HIT' | 'STALE' | 'MISS';
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
- revalidatePath(path: string, options?: RevalidateOptions): Promise<void>;
151
- revalidateTag(tag: string, options?: RevalidateOptions): Promise<void>;
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 }): void;
179
- start(): void;
180
- stop(): void;
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?: { concurrency?: number; jitter?: number }): Scheduler;
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.2",
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.2"
33
+ "what-server": "^0.12.4"
34
34
  },
35
35
  "peerDependenciesMeta": {
36
36
  "what-server": {