Skip to content

Instantly share code, notes, and snippets.

@H01001000
Created November 19, 2025 03:50
Show Gist options
  • Select an option

  • Save H01001000/87c1d98a5e66410997c276094b1b7dc2 to your computer and use it in GitHub Desktop.

Select an option

Save H01001000/87c1d98a5e66410997c276094b1b7dc2 to your computer and use it in GitHub Desktop.
Hack script for using fortedigital/nextjs-cache-handler for Next.js's cachehandlers (for cache component / "use cache")
import type {
CacheHandlerValue,
Handler,
LifespanParameters,
} from "@fortedigital/nextjs-cache-handler/cache-handler.types";
import type {
CacheEntry,
CacheHandler as CacheHandlerType,
} from "next/dist/server/lib/cache-handlers/types";
import CacheHandler from "./cache-handler.ts";
// Based on https://github.com/vercel/next.js/blob/83ae64fe773453dbcf1583e5303494413b8449d7/packages/next/src/server/lib/cache-handlers/default.ts#L38C1-L206C2
export async function createDefaultCacheHandler(
maxSize: number,
): Promise<CacheHandlerType> {
// If the max size is 0, return a cache handler that doesn't cache anything,
// this avoids an unnecessary LRUCache instance and potential memory
// allocation.
if (maxSize === 0) {
return {
get: () => Promise.resolve(undefined),
set: () => Promise.resolve(),
refreshTags: () => Promise.resolve(),
getExpiration: () => Promise.resolve(0),
updateTags: () => Promise.resolve(),
};
}
await new CacheHandler({}).get("");
const memoryCache = CacheHandler.mergedHandler as Handler;
const debug = process.env.NEXT_PRIVATE_DEBUG_CACHE
? console.debug.bind(console, "CustomCacheHandler:")
: undefined;
function getLifespanParameters(
revalidate: number,
timestamp: number,
expire: number,
): LifespanParameters {
const lastModifiedAt = Math.floor(timestamp / 1000);
const staleAge = revalidate;
const staleAt = lastModifiedAt + staleAge;
const expireAge = expire;
const expireAt = lastModifiedAt + expireAge;
return {
expireAge,
expireAt,
lastModifiedAt,
revalidate,
staleAge,
staleAt,
};
}
return {
async get(cacheKey, softTags) {
const cachedData = await memoryCache.get(cacheKey, {
implicitTags: softTags,
});
if (!cachedData) {
debug?.("get", cacheKey, "not found");
return undefined;
}
const entry = cachedData.value as CacheEntry;
debug?.("get", cacheKey, "found", {
...cachedData,
value: {
...entry,
value: "[ReadableStream]",
},
});
return {
...entry,
value: new ReadableStream({
start(controller) {
controller.enqueue(Buffer.from(entry.value as string, "base64"));
controller.close();
},
}),
};
},
async set(cacheKey, pendingEntry) {
debug?.("set", cacheKey, "start");
const entry = await pendingEntry;
try {
const reader = entry.value.getReader();
const chunks = [];
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
}
} finally {
reader.releaseLock();
}
const data = Buffer.concat(chunks.map((chunk) => Buffer.from(chunk)));
const lifespan = getLifespanParameters(
entry.revalidate,
entry.timestamp,
entry.expire,
);
// If expireAt is in the past, do not cache
if (lifespan !== null && Date.now() > lifespan.expireAt * 1000) {
return;
}
const cacheHandlerValueTags = entry.tags;
const cacheHandlerValue: CacheHandlerValue = {
lastModified: entry.timestamp,
lifespan,
tags: Object.freeze(cacheHandlerValueTags),
value: {
...entry,
value: data.toString("base64"),
},
};
await memoryCache.set(cacheKey, cacheHandlerValue);
debug?.("set", cacheKey, cacheHandlerValue, "done");
} catch (err) {
debug?.("set", cacheKey, "failed", err);
}
},
async refreshTags() {
// Nothing to do for an in-memory cache handler.
},
async getExpiration() {
return Infinity;
},
async updateTags(tags, durations) {
await Promise.all(
tags.map(async (tag) => memoryCache.revalidateTag(tag)),
);
},
};
}
export default createDefaultCacheHandler(50 * 1024 * 1024);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment