218 lines
6.5 KiB
JavaScript
218 lines
6.5 KiB
JavaScript
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;
|