UNPKG

@rb2bv/cache-handler

Version:
258 lines (254 loc) 9.56 kB
// src/functions/nesh-cache.ts import assert from "node:assert/strict"; import { staticGenerationAsyncStorage } from "next/dist/client/components/static-generation-async-storage.external.js"; // src/constants.ts var MAX_INT32 = 2 ** 31 - 1; var TIME_ONE_YEAR = 31536e3; // src/functions/nesh-cache.ts var NEXT_CACHE_IMPLICIT_TAG_ID = "_N_T_"; function getDerivedTags(pathname) { const derivedTags = ["/layout"]; if (!pathname.startsWith("/")) { return derivedTags; } const pathnameParts = pathname.split("/"); for (let i = 1; i < pathnameParts.length + 1; i++) { let curPathname = pathnameParts.slice(0, i).join("/"); if (curPathname) { if (!(curPathname.endsWith("/page") || curPathname.endsWith("/route"))) { curPathname = `${curPathname}${curPathname.endsWith("/") ? "" : "/"}layout`; } derivedTags.push(curPathname); } } return derivedTags; } function addImplicitTags(staticGenerationStore) { const newTags = []; const { pagePath, urlPathname } = staticGenerationStore; if (!Array.isArray(staticGenerationStore.tags)) { staticGenerationStore.tags = []; } if (pagePath) { const derivedTags = getDerivedTags(pagePath); for (let tag of derivedTags) { tag = `${NEXT_CACHE_IMPLICIT_TAG_ID}${tag}`; if (!staticGenerationStore.tags?.includes(tag)) { staticGenerationStore.tags.push(tag); } newTags.push(tag); } } if (urlPathname) { const parsedPathname = new URL(urlPathname, "http://n").pathname; const tag = `${NEXT_CACHE_IMPLICIT_TAG_ID}${parsedPathname}`; if (!staticGenerationStore.tags?.includes(tag)) { staticGenerationStore.tags.push(tag); } newTags.push(tag); } return newTags; } 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 neshCache(callback, commonOptions) { if (commonOptions?.resultSerializer && !commonOptions?.resultDeserializer) { throw new Error("neshCache: if you provide a resultSerializer, you must provide a resultDeserializer."); } if (commonOptions?.resultDeserializer && !commonOptions?.resultSerializer) { throw new Error("neshCache: if you provide a resultDeserializer, you must provide a resultSerializer."); } const commonTags = commonOptions?.tags ?? []; 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 = staticGenerationAsyncStorage.getStore(); assert(store?.incrementalCache, "neshCache must be used in a Next.js app directory."); const { tags = [], revalidate = commonRevalidate, cacheKey, argumentsSerializer = commonArgumentsSerializer, resultDeserializer = commonResultDeserializer, resultSerializer = commonResultSerializer } = options ?? {}; assert( revalidate === false || revalidate > 0 && Number.isInteger(revalidate), "neshCache: revalidate must be a positive integer or false." ); if (store.fetchCache === "force-no-store" || store.isDraftMode || store.incrementalCache.dev) { return await callback(...args); } const uniqueTags = new Set(store.tags); const combinedTags = [...tags, ...commonTags]; for (const tag of combinedTags) { if (typeof tag === "string") { uniqueTags.add(tag); } else { console.warn(`neshCache: Invalid tag: ${tag}. Skipping it. Expected a string.`); } } const allTags = Array.from(uniqueTags); store.tags = allTags; store.revalidate = revalidate; const fetchIdx = store.nextFetchId ?? 1; store.nextFetchId = fetchIdx + 1; const key = await store.incrementalCache.fetchCacheKey(`nesh-cache-${cacheKey ?? argumentsSerializer(args)}`); const handleUnlock = await store.incrementalCache.lock(key); let cacheData = null; try { cacheData = await store.incrementalCache.get(key, { revalidate, tags: allTags, softTags: addImplicitTags(store), kindHint: "fetch", fetchIdx, fetchUrl: "neshCache" }); } catch (error) { await handleUnlock(); throw error; } if (cacheData?.value?.kind === "FETCH" && cacheData.isStale === false) { await handleUnlock(); return resultDeserializer(cacheData.value.data.body); } let data; try { data = await staticGenerationAsyncStorage.run( { ...store, // force any nested fetches to bypass cache so they revalidate // when the unstable_cache call is revalidated fetchCache: "force-no-store" }, callback, ...args ); } catch (error) { throw error; } finally { await handleUnlock(); } store.incrementalCache.set( key, { kind: "FETCH", data: { body: resultSerializer(data), headers: {}, url: "neshCache" }, revalidate: revalidate || TIME_ONE_YEAR }, { revalidate, tags, fetchCache: true, fetchIdx, fetchUrl: "neshCache" } ); return data; } return cachedCallback; } // src/functions/nesh-classic-cache.ts import assert2 from "node:assert/strict"; import { createHash } from "node:crypto"; import { staticGenerationAsyncStorage as staticGenerationAsyncStorage2 } from "next/dist/client/components/static-generation-async-storage.external.js"; 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 serializeArguments2(object) { return JSON.stringify(object); } function serializeResult2(object) { return Buffer.from(JSON.stringify(object), "utf-8").toString("base64"); } function deserializeResult2(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 ?? serializeArguments2; const commonResultSerializer = commonOptions?.resultSerializer ?? serializeResult2; const commonResultDeserializer = commonOptions?.resultDeserializer ?? deserializeResult2; async function cachedCallback(options, ...args) { const store = staticGenerationAsyncStorage2.getStore(); assert2(!store?.incrementalCache, "neshClassicCache must be used in a Next.js Pages directory."); const cacheHandler = globalThis?.__incrementalCache?.cacheHandler; assert2(cacheHandler, "neshClassicCache must be used in a Next.js Pages directory."); const { responseContext, tags = [], revalidate = commonRevalidate, cacheKey, argumentsSerializer = commonArgumentsSerializer, resultDeserializer = commonResultDeserializer, resultSerializer = commonResultSerializer } = options ?? {}; assert2( 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, // @ts-expect-error kindHint: "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, { // @ts-expect-error kind: "FETCH", data: { body: resultSerializer(data), headers: {}, url: "neshClassicCache" }, revalidate: revalidate || TIME_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 { neshCache, neshClassicCache };