/** A cached SSR response - the bytes + metadata a {@link CacheStore} persists. */ export interface CachedResponse { /** The rendered document (UTF-8 HTML, fully buffered). */ readonly body: string; readonly status: number; /** Response headers to replay (e.g. `content-type`). */ readonly headers: Readonly>; /** When this entry was stored, via the injected clock (ms epoch). */ readonly storedAt: number; /** Freshness window (ms): `now - storedAt >= revalidate` ⇒ stale (serve it, regenerate behind it). */ readonly revalidate: number; /** Public, bounded invalidation labels. A tag purge makes matching entries miss immediately. */ readonly tags?: readonly string[]; } /** * Pluggable ISR cache backend. **Production deploys MUST use a shared/durable store** (Workers KV, * Redis, the platform Cache API) so cached pages *and* revalidation hold across instances; * {@link MemoryCacheStore} is dev / single-instance only. Implementations are async so a network store * (KV/Redis) fits the same interface. */ export interface CacheStore { /** The cached entry for `key`, or `undefined` on a miss. */ get(key: string): Promise; /** Store (or overwrite) the entry for `key`. */ set(key: string, value: CachedResponse): Promise; /** Drop `key` (on-demand revalidation / purge). A no-op if absent. */ delete(key: string): Promise; /** Drop every cached response carrying `tag`, when the backend supports tag invalidation. */ readonly invalidateTag?: (tag: string) => Promise; } /** Validate and serialize route-declared ISR tags for the internal response header. */ export declare function serializeISRTags(tags: readonly string[]): string | undefined; export interface MemoryCacheStoreOptions { /** Allow the in-memory store in production. Off by default - per-instance caching means revalidation * won't propagate across instances and each instance caches separately. */ readonly allowInProduction?: boolean; /** Hard cap on entries; the least-recently-used is evicted past it (default 500). */ readonly max?: number; } /** * In-process ISR cache. Refuses to run in production unless explicitly allowed (mirrors the * rate-limit `MemoryStore` - a per-instance cache is unsafe across instances). Bounded **LRU**: a * read or write bumps the entry, so the least-recently-used evicts past `max` (a hot, frequently-read * page survives a burst of new pages). */ export declare class MemoryCacheStore implements CacheStore { private readonly cache; private readonly max; private readonly tagEpoch; constructor(options?: MemoryCacheStoreOptions); get(key: string): Promise; set(key: string, value: CachedResponse): Promise; delete(key: string): Promise; invalidateTag(tag: string): Promise; private pruneTagEpochs; } /** * Minimal structural shape of a Cloudflare Workers **KV namespace** binding - just the three methods * {@link KVCacheStore} uses. Structural (not a dependency on `@cloudflare/workers-types`) so any * KV-like binding satisfies it and tests can pass an in-memory double. */ export interface KVNamespaceLike { /** Read a stored string value, or `null` on a miss (Cloudflare's `KVNamespace.get(key)` default). */ get(key: string): Promise; /** Write a string value, optionally with a TTL (**seconds**). */ put(key: string, value: string, options?: { readonly expirationTtl?: number; }): Promise; /** Delete a key (a no-op if absent). */ delete(key: string): Promise; } export interface KVCacheStoreOptions { /** * GC backstop (**seconds**) written as the KV entry's `expirationTtl`, so abandoned entries * eventually evict. MUST exceed your longest `revalidate` window - otherwise KV expiry turns a * stale-while-revalidate into a *blocking* miss (the entry vanishes instead of being served stale * while it regenerates) - and be ≥ 60 (Cloudflare KV's minimum). Omit ⇒ entries persist until * overwritten on regeneration or purged via `revalidateEndpoint`. */ readonly expirationTtl?: number; /** * Smallest `expirationTtl` this binding accepts (**seconds**). Defaults to 60, which is Cloudflare * KV's floor - the common case, and worth rejecting at construction because Cloudflare fails the * `put` at runtime instead, one deploy later. * * The floor belongs to the *binding*, not to this class. {@link KVNamespaceLike} is structural, so * a Redis, Deno KV, Upstash, or in-memory binding satisfies it too, and those accept far shorter * TTLs. Pass their real minimum, or `0` for a backend with none. */ readonly minExpirationTtl?: number; } /** * A {@link CacheStore} backed by a **Cloudflare Workers KV** namespace (or any {@link KVNamespaceLike} * binding) - the production-grade shared/durable store ISR wants: cached pages and on-demand purges * hold *across* worker instances (unlike the per-instance {@link MemoryCacheStore}). Entries serialize * to JSON; every read is validated before it's trusted (a malformed/version-skewed entry is treated as * a miss). Construct it in your Workers `fetch` from the binding: `new KVCacheStore(env.ISR_CACHE)`. * * Cloudflare is the default, not a requirement: {@link KVNamespaceLike} is three structural methods, * so a Redis, Deno KV, or Upstash binding satisfies it (pass `minExpirationTtl` for their TTL floor). * A backend that *can* enumerate keys deserves its own {@link CacheStore} rather than this one - the * epoch indirection in {@link KVCacheStore.invalidateTag} exists only because KV cannot list by tag. */ export declare class KVCacheStore implements CacheStore { private readonly kv; private readonly putOptions; constructor(kv: KVNamespaceLike, options?: KVCacheStoreOptions); get(key: string): Promise; set(key: string, value: CachedResponse): Promise; delete(key: string): Promise; invalidateTag(tag: string): Promise; private tagKey; } /** Minimal platform shape `withISR` needs - just `waitUntil` (edge runtimes extend the response * lifetime so background regeneration finishes). Off-edge it's absent and regen runs fire-and-forget. */ export interface ISRPlatform { readonly waitUntil?: (promise: Promise) => void; } /** The app `withISR` wraps - anything with a `fetch(req, platform?)` (a `createWebApp` result). */ export interface ISRApp { fetch(req: Request, platform?: ISRPlatform): Response | Promise; } /** Response header marking how an ISR response was served: a cache `hit` (fresh), `stale` (served + * regenerating behind it), or `miss` (rendered now + stored). Useful for debugging + tests. */ export declare const ISR_STATUS_HEADER = "x-nifra-isr"; /** Response header carrying bounded route tags into {@link withISR}. */ export declare const ISR_REVALIDATE_TAGS_HEADER = "x-nifra-isr-tags"; /** * Response header a route uses to advertise its ISR freshness (**seconds**) to a {@link withISR} * wrapper - `createWebApp` emits it from a route's `export const revalidate`. Deliberately distinct * from the action-revalidation `x-nifra-revalidate` header (a CSV path list the *client* parses to * refetch): this one is an integer TTL the *wrapper* reads, so the two channels never alias. */ export declare const ISR_REVALIDATE_HEADER = "x-nifra-isr-revalidate"; export interface ISROptions { readonly store: CacheStore; /** Default freshness window (**seconds**) for a cached page; older ⇒ stale (served, regenerated * behind). A route overrides it per-page via `export const revalidate` (surfaced as the * `x-nifra-isr-revalidate` response header). */ readonly revalidate: number; /** Monotonic clock (ms) - injected for testability; production passes `() => Date.now()`. */ readonly now: () => number; /** Cache key for a request. Default: `origin + pathname + search` so host-routed apps do not share * entries across tenants. Return `null` to bypass the cache for this request (it goes straight to the * app, uncached). */ readonly key?: (req: Request) => string | null; /** Draft/preview secret (the same one given to `createWebApp({ draftSecret })` + `enableDraft`). When * set, a request carrying a valid signed draft cookie **bypasses the cache** - editors always render * fresh, and a draft render is never written to the store (it can't poison the public cache). */ readonly draftSecret?: string; } /** * Wrap a nifra app with **Incremental Static Regeneration**: a cacheable page is served from * {@link CacheStore} when fresh, served **stale while a fresh copy regenerates in the background** * (`platform.waitUntil` on edge), or rendered + stored on a miss. Framework-agnostic (it caches the * rendered bytes). Returns a `fetch(req, platform?)` handler - hand it to `Bun.serve`/the Workers * `export default`, etc. Regeneration is single-flight per key (no stampede on a hot stale page). * * Each route's freshness comes from the `revalidate` header the app sets (per-route * `export const revalidate`), falling back to `options.revalidate`. Only full-document `text/html` * GET 200s are cached; everything else (assets, data-mode GETs, redirects, errors) passes through. */ export declare function withISR(app: ISRApp, options: ISROptions): (req: Request, platform?: ISRPlatform) => Promise; export interface RevalidateEndpointOptions { readonly store: CacheStore; /** Shared secret; the request's token must match it (constant-time). */ readonly secret: string; /** Header carrying the secret. Default `x-nifra-revalidate-token`. */ readonly tokenHeader?: string; /** Map a to-purge path → its cache key - MUST match the `withISR` `key` fn. The default uses the * revalidation request's origin plus the purged path, matching `withISR`'s default host-aware key. */ readonly key?: (path: string, req: Request) => string; } /** * An **on-demand revalidation** (purge) endpoint - a `fetch` handler that drops a path's cached entry * or invalidates every entry carrying a tag. `POST` with the secret in the token header and either * `?path=/blog/x`, `?tag=products`, or a JSON `{ "path": "/blog/x" }` / `{ "tag": "products" }` body. * The token is checked in **constant time** (wrong/missing → `401`); malformed targets → `400`; * non-POST → `405`. A store without tag support returns `501` for tag requests. Mount it on a nifra route, e.g. * `app.post("/__nifra/revalidate", (c) => handler(c.req))`. */ export declare function revalidateEndpoint(options: RevalidateEndpointOptions): (req: Request) => Promise; //# sourceMappingURL=isr.d.ts.map