Something went wrong. Try again.
Monorepo for the nibling.run platform
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108/** The slice of the Workers Cache API the gateway uses. The worker passes `caches.default`; tests pass * `memoryCache()`. The Cache API is per data center and purges do not reach the others, so nothing * here purges: every entry ages out on its TTL. */export interface ResponseCache { match(key: string): Promise<Response | undefined>; put(key: string, response: Response): Promise<void>;}
/** How long an answer is served from cache (`fresh`), then served while a background refresh runs * (`stale`), in seconds, and the Cache-Control a client gets with it. */export interface CachePolicy { fresh: number; stale: number; client: string;}
export const POLICY = { /** A blob, or a record pinned by CID: the bytes can never change. */ immutable: { fresh: 31_536_000, stale: 0, client: 'public, max-age=31536000, immutable' }, /** The current version of a record or a listing. Clients revalidate so a save shows up on reload. */ repo: { fresh: 15, stale: 60, client: 'no-cache' }, identity: { fresh: 300, stale: 0, client: 'public, max-age=300' }, backlinks: { fresh: 15, stale: 0, client: 'public, max-age=15' },} as const satisfies Record<string, CachePolicy>;
export type CacheStatus = 'hit' | 'stale' | 'miss' | 'bypass';
export interface CacheContext { cache: ResponseCache; waitUntil(promise: Promise<unknown>): void; now(): number;}
const STORED_AT = 'x-nib-stored-at';/** Set by a loader on a 200 that must not be kept, and stripped before the client sees it. */export const NO_STORE = 'x-nib-no-store';
/** The copy that goes into the cache: the body, its type, when it was stored, and a max-age that * covers the stale window so the Cache API keeps it that long. */function forStorage(response: Response, body: ReadableStream<Uint8Array> | null, policy: CachePolicy, now: number): Response { const headers = new Headers(response.headers); headers.set(STORED_AT, String(now)); headers.set('cache-control', `public, max-age=${policy.fresh + policy.stale}`); return new Response(body, { status: response.status, headers });}
function forClient(response: Response, body: ReadableStream<Uint8Array> | null, policy: CachePolicy, status: CacheStatus): Response { const headers = new Headers(response.headers); headers.delete(STORED_AT); const keep = !headers.has(NO_STORE); headers.delete(NO_STORE); headers.set('cache-control', keep ? policy.client : 'no-store'); headers.set('x-nib-cache', status); return new Response(body, { status: response.status, headers });}
/** Stores a 200 in the background and hands back a response the client can read at the same time. * Anything else (an error, a record that is not there) passes through unstored. */function store(ctx: CacheContext, key: string, response: Response, policy: CachePolicy, status: CacheStatus): Response { if (response.status !== 200 || response.headers.has(NO_STORE) || !response.body) return forClient(response, response.body, policy, status); const [client, copy] = response.body.tee(); ctx.waitUntil(ctx.cache.put(key, forStorage(response, copy, policy, ctx.now())).catch(() => undefined)); return forClient(response, client, policy, status);}
/** * Serves `key` from the cache under `policy`, loading it with `load` on a miss. `bypass` (the request's * `x-nib-fresh: 1`) skips the read but still stores what it loads, so the next reader sees the write. * Stale-while-revalidate is done here because the Cache API ignores that directive. */export async function cached(ctx: CacheContext, key: string, policy: CachePolicy, bypass: boolean, load: () => Promise<Response>): Promise<Response> { if (bypass) return store(ctx, key, await load(), policy, 'bypass'); const hit = await ctx.cache.match(key).catch(() => undefined); const storedAt = Number(hit?.headers.get(STORED_AT)); if (hit && Number.isFinite(storedAt)) { const age = ctx.now() - storedAt; if (age < policy.fresh * 1000) return forClient(hit, hit.body, policy, 'hit'); if (age < (policy.fresh + policy.stale) * 1000) { ctx.waitUntil( load() .then((fresh) => (fresh.status === 200 && !fresh.headers.has(NO_STORE) ? ctx.cache.put(key, forStorage(fresh, fresh.body, policy, ctx.now())) : fresh.body?.cancel())) .catch(() => undefined), ); return forClient(hit, hit.body, policy, 'stale'); } } await hit?.body?.cancel(); return store(ctx, key, await load(), policy, 'miss');}
/** An in-memory `ResponseCache` that honors each entry's `max-age` against `now`, as the Cache API does. */export function memoryCache(now: () => number): ResponseCache & { keys(): string[] } { const entries = new Map<string, { bytes: Uint8Array; status: number; headers: [string, string][]; expires: number }>(); return { async match(key) { const entry = entries.get(key); if (!entry || entry.expires <= now()) return undefined; return new Response(entry.bytes.slice(), { status: entry.status, headers: entry.headers }); }, async put(key, response) { const maxAge = Number(/max-age=(\d+)/.exec(response.headers.get('cache-control') ?? '')?.[1] ?? 0); const bytes = new Uint8Array(await response.arrayBuffer()); entries.set(key, { bytes, status: response.status, headers: [...response.headers], expires: now() + maxAge * 1000 }); }, keys: () => [...entries.keys()], };}