@fortedigital/nextjs-cache-handler
Version:
Next.js cache handlers
115 lines (114 loc) • 4 kB
JavaScript
// src/functions/nesh-classic-cache.ts
import assert from "assert/strict";
import { createHash } from "crypto";
import { workAsyncStorage } from "next/dist/server/app-render/work-async-storage.external.js";
import { CACHE_ONE_YEAR } from "next/dist/lib/constants";
function hashCacheKey(url) {
const MAIN_KEY_PREFIX = "nesh-pages-cache-v1";
const cacheString = JSON.stringify([MAIN_KEY_PREFIX, url]);
return createHash("sha256").update(cacheString).digest("hex");
}
function serializeArguments(object) {
return JSON.stringify(object);
}
function serializeResult(object) {
return Buffer.from(JSON.stringify(object), "utf-8").toString("base64");
}
function deserializeResult(string) {
return JSON.parse(Buffer.from(string, "base64").toString("utf-8"));
}
function neshClassicCache(callback, commonOptions) {
if (commonOptions?.resultSerializer && !commonOptions?.resultDeserializer) {
throw new Error(
"neshClassicCache: if you provide a resultSerializer, you must provide a resultDeserializer."
);
}
if (commonOptions?.resultDeserializer && !commonOptions?.resultSerializer) {
throw new Error(
"neshClassicCache: if you provide a resultDeserializer, you must provide a resultSerializer."
);
}
const commonRevalidate = commonOptions?.revalidate ?? false;
const commonArgumentsSerializer = commonOptions?.argumentsSerializer ?? serializeArguments;
const commonResultSerializer = commonOptions?.resultSerializer ?? serializeResult;
const commonResultDeserializer = commonOptions?.resultDeserializer ?? deserializeResult;
async function cachedCallback(options, ...args) {
const store = workAsyncStorage.getStore();
assert(
!store?.incrementalCache,
"neshClassicCache must be used in a Next.js Pages directory."
);
const cacheHandler = globalThis?.__incrementalCache?.cacheHandler;
assert(
cacheHandler,
"neshClassicCache must be used in a Next.js Pages directory."
);
const {
responseContext,
tags = [],
revalidate = commonRevalidate,
cacheKey,
argumentsSerializer = commonArgumentsSerializer,
resultDeserializer = commonResultDeserializer,
resultSerializer = commonResultSerializer
} = options ?? {};
assert(
revalidate === false || revalidate > 0 && Number.isInteger(revalidate),
"neshClassicCache: revalidate must be a positive integer or false."
);
responseContext?.setHeader(
"Cache-Control",
`public, s-maxage=${revalidate}, stale-while-revalidate`
);
const uniqueTags = /* @__PURE__ */ new Set();
for (const tag of tags) {
if (typeof tag === "string") {
uniqueTags.add(tag);
} else {
console.warn(
`neshClassicCache: Invalid tag: ${tag}. Skipping it. Expected a string.`
);
}
}
const allTags = Array.from(uniqueTags);
const key = hashCacheKey(
`nesh-classic-cache-${cacheKey ?? argumentsSerializer(args)}`
);
const cacheData = await cacheHandler.get(key, {
revalidate,
tags: allTags,
kind: "FETCH",
fetchUrl: "neshClassicCache"
});
if (cacheData?.value?.kind === "FETCH" && cacheData.lifespan && cacheData.lifespan.staleAt > Date.now() / 1e3) {
return resultDeserializer(cacheData.value.data.body);
}
const data = await callback(...args);
cacheHandler.set(
key,
{
kind: "FETCH",
data: {
body: resultSerializer(data),
headers: {},
url: "neshClassicCache"
},
revalidate: revalidate || CACHE_ONE_YEAR
},
{
revalidate,
tags,
fetchCache: true,
fetchUrl: "neshClassicCache"
}
);
if (cacheData?.value?.kind === "FETCH" && cacheData?.lifespan && cacheData.lifespan.expireAt > Date.now() / 1e3) {
return resultDeserializer(cacheData.value.data.body);
}
return data;
}
return cachedCallback;
}
export {
neshClassicCache
};