import { hash } from "ohash"; import { handleCacheHeaders, defineEventHandler, createEvent } from "h3"; import { parseURL } from "ufo"; import { useStorage } from "#internal/nitro"; const defaultCacheOptions = { name: "_", base: "/cache", swr: true, maxAge: 1 }; export function defineCachedFunction(fn, opts) { opts = { ...defaultCacheOptions, ...opts }; const pending = {}; const group = opts.group || "nitro"; const name = opts.name || fn.name || "_"; const integrity = hash([opts.integrity, fn, opts]); const validate = opts.validate || (() => true); async function get(key, resolver) { const cacheKey = [opts.base, group, name, key + ".json"].filter(Boolean).join(":").replace(/:\/$/, ":index"); const entry = await useStorage().getItem(cacheKey) || {}; const ttl = (opts.maxAge ?? opts.maxAge ?? 0) * 1e3; if (ttl) { entry.expires = Date.now() + ttl; } const expired = entry.integrity !== integrity || ttl && Date.now() - (entry.mtime || 0) > ttl || !validate(entry); const _resolve = async () => { if (!pending[key]) { entry.value = void 0; entry.integrity = void 0; entry.mtime = void 0; entry.expires = void 0; pending[key] = Promise.resolve(resolver()); } entry.value = await pending[key]; entry.mtime = Date.now(); entry.integrity = integrity; delete pending[key]; if (validate(entry)) { useStorage().setItem(cacheKey, entry).catch((error) => console.error("[nitro] [cache]", error)); } }; const _resolvePromise = expired ? _resolve() : Promise.resolve(); if (opts.swr && entry.value) { _resolvePromise.catch(console.error); return Promise.resolve(entry); } return _resolvePromise.then(() => entry); } return async (...args) => { const key = (opts.getKey || getKey)(...args); const entry = await get(key, () => fn(...args)); let value = entry.value; if (opts.transform) { value = await opts.transform(entry, ...args) || value; } return value; }; } export const cachedFunction = defineCachedFunction; function getKey(...args) { return args.length ? hash(args, {}) : ""; } export function defineCachedEventHandler(handler, opts = defaultCacheOptions) { const _opts = { ...opts, getKey: (event) => { const url = event.req.originalUrl || event.req.url; const friendlyName = decodeURI(parseURL(url).pathname).replace(/[^a-zA-Z0-9]/g, "").substring(0, 16); const urlHash = hash(url); return `${friendlyName}.${urlHash}`; }, validate: (entry) => { if (entry.value.code >= 400) { return false; } if (entry.value.body === void 0) { return false; } return true; }, group: opts.group || "nitro/handlers", integrity: [ opts.integrity, handler ] }; const _cachedHandler = cachedFunction(async (incomingEvent) => { const reqProxy = cloneWithProxy(incomingEvent.req, { headers: {} }); const resHeaders = {}; let _resSendBody; const resProxy = cloneWithProxy(incomingEvent.res, { statusCode: 200, getHeader(name) { return resHeaders[name]; }, setHeader(name, value) { resHeaders[name] = value; return this; }, getHeaderNames() { return Object.keys(resHeaders); }, hasHeader(name) { return name in resHeaders; }, removeHeader(name) { delete resHeaders[name]; }, getHeaders() { return resHeaders; }, end(chunk, arg2, arg3) { if (typeof chunk === "string") { _resSendBody = chunk; } if (typeof arg2 === "function") { arg2(); } if (typeof arg3 === "function") { arg3(); } return this; }, write(chunk, arg2, arg3) { if (typeof chunk === "string") { _resSendBody = chunk; } if (typeof arg2 === "function") { arg2(); } if (typeof arg3 === "function") { arg3(); } return this; }, writeHead(statusCode, headers2) { this.statusCode = statusCode; if (headers2) { for (const header in headers2) { this.setHeader(header, headers2[header]); } } return this; } }); const event = createEvent(reqProxy, resProxy); event.context = incomingEvent.context; const body = await handler(event) || _resSendBody; const headers = event.res.getHeaders(); headers.etag = headers.Etag || headers.etag || `W/"${hash(body)}"`; headers["last-modified"] = headers["Last-Modified"] || headers["last-modified"] || new Date().toUTCString(); const cacheControl = []; if (opts.swr) { if (opts.maxAge) { cacheControl.push(`s-maxage=${opts.maxAge}`); } if (opts.staleMaxAge) { cacheControl.push(`stale-while-revalidate=${opts.staleMaxAge}`); } else { cacheControl.push("stale-while-revalidate"); } } else if (opts.maxAge) { cacheControl.push(`max-age=${opts.maxAge}`); } if (cacheControl.length) { headers["cache-control"] = cacheControl.join(", "); } const cacheEntry = { code: event.res.statusCode, headers, body }; return cacheEntry; }, _opts); return defineEventHandler(async (event) => { if (opts.headersOnly) { if (handleCacheHeaders(event, { maxAge: opts.maxAge })) { return; } return handler(event); } const response = await _cachedHandler(event); if (event.res.headersSent || event.res.writableEnded) { return response.body; } if (handleCacheHeaders(event, { modifiedTime: new Date(response.headers["last-modified"]), etag: response.headers.etag, maxAge: opts.maxAge })) { return; } event.res.statusCode = response.code; for (const name in response.headers) { event.res.setHeader(name, response.headers[name]); } return response.body; }); } function cloneWithProxy(obj, overrides) { return new Proxy(obj, { get(target, property, receiver) { if (property in overrides) { return overrides[property]; } return Reflect.get(target, property, receiver); }, set(target, property, value, receiver) { if (property in overrides) { overrides[property] = value; return true; } return Reflect.set(target, property, value, receiver); } }); } export const cachedEventHandler = defineCachedEventHandler;