@rb2bv/cache-handler
Version:
Next.js self-hosting simplified.
294 lines (288 loc) • 11.6 kB
JavaScript
;
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/functions/functions.ts
var functions_exports = {};
__export(functions_exports, {
neshCache: () => neshCache,
neshClassicCache: () => neshClassicCache
});
module.exports = __toCommonJS(functions_exports);
// src/functions/nesh-cache.ts
var import_strict = __toESM(require("assert/strict"), 1);
var import_static_generation_async_storage_external = require("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 = import_static_generation_async_storage_external.staticGenerationAsyncStorage.getStore();
(0, import_strict.default)(store?.incrementalCache, "neshCache must be used in a Next.js app directory.");
const {
tags = [],
revalidate = commonRevalidate,
cacheKey,
argumentsSerializer = commonArgumentsSerializer,
resultDeserializer = commonResultDeserializer,
resultSerializer = commonResultSerializer
} = options ?? {};
(0, import_strict.default)(
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 import_static_generation_async_storage_external.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
var import_strict2 = __toESM(require("assert/strict"), 1);
var import_node_crypto = require("crypto");
var import_static_generation_async_storage_external2 = require("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 (0, import_node_crypto.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 = import_static_generation_async_storage_external2.staticGenerationAsyncStorage.getStore();
(0, import_strict2.default)(!store?.incrementalCache, "neshClassicCache must be used in a Next.js Pages directory.");
const cacheHandler = globalThis?.__incrementalCache?.cacheHandler;
(0, import_strict2.default)(cacheHandler, "neshClassicCache must be used in a Next.js Pages directory.");
const {
responseContext,
tags = [],
revalidate = commonRevalidate,
cacheKey,
argumentsSerializer = commonArgumentsSerializer,
resultDeserializer = commonResultDeserializer,
resultSerializer = commonResultSerializer
} = options ?? {};
(0, import_strict2.default)(
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;
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
neshCache,
neshClassicCache
});