nitropack
Version:
Build and Deploy Universal JavaScript Servers
249 lines (248 loc) • 7.68 kB
JavaScript
import { hash } from "ohash";
import {
handleCacheHeaders,
defineEventHandler,
createEvent
} from "h3";
import { parseURL } from "ufo";
import { useStorage } from "./storage.mjs";
const defaultCacheOptions = {
name: "_",
base: "/cache",
swr: true,
maxAge: 1
};
export function defineCachedFunction(fn, opts = {}) {
opts = { ...defaultCacheOptions, ...opts };
const pending = {};
const group = opts.group || "nitro/functions";
const name = opts.name || fn.name || "_";
const integrity = hash([opts.integrity, fn, opts]);
const validate = opts.validate || (() => true);
async function get(key, resolver, shouldInvalidateCache) {
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 = shouldInvalidateCache || entry.integrity !== integrity || ttl && Date.now() - (entry.mtime || 0) > ttl || !validate(entry);
const _resolve = async () => {
const isPending = pending[key];
if (!isPending) {
if (entry.value !== void 0 && (opts.staleMaxAge || 0) >= 0 && opts.swr === false) {
entry.value = void 0;
entry.integrity = void 0;
entry.mtime = void 0;
entry.expires = void 0;
}
pending[key] = Promise.resolve(resolver());
}
try {
entry.value = await pending[key];
} catch (error) {
if (!isPending) {
delete pending[key];
}
throw error;
}
if (!isPending) {
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 entry;
}
return _resolvePromise.then(() => entry);
}
return async (...args) => {
const shouldBypassCache = opts.shouldBypassCache?.(...args);
if (shouldBypassCache) {
return fn(...args);
}
const key = await (opts.getKey || getKey)(...args);
const shouldInvalidateCache = opts.shouldInvalidateCache?.(...args);
const entry = await get(key, () => fn(...args), shouldInvalidateCache);
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 > 0 ? hash(args, {}) : "";
}
function escapeKey(key) {
return key.replace(/[^\dA-Za-z]/g, "");
}
export function defineCachedEventHandler(handler, opts = defaultCacheOptions) {
const _opts = {
...opts,
getKey: async (event) => {
const key = await opts.getKey?.(event);
if (key) {
return escapeKey(key);
}
const url = event.node.req.originalUrl || event.node.req.url;
const friendlyName = escapeKey(decodeURI(parseURL(url).pathname)).slice(
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.node.req, { headers: {} });
const resHeaders = {};
let _resSendBody;
const resProxy = cloneWithProxy(incomingEvent.node.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.node.res.getHeaders();
headers.etag = headers.Etag || headers.etag || `W/"${hash(body)}"`;
headers["last-modified"] = headers["Last-Modified"] || headers["last-modified"] || (/* @__PURE__ */ 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 > 0) {
headers["cache-control"] = cacheControl.join(", ");
}
const cacheEntry = {
code: event.node.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.node.res.headersSent || event.node.res.writableEnded) {
return response.body;
}
if (handleCacheHeaders(event, {
modifiedTime: new Date(response.headers["last-modified"]),
etag: response.headers.etag,
maxAge: opts.maxAge
})) {
return;
}
event.node.res.statusCode = response.code;
for (const name in response.headers) {
event.node.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;