UNPKG

@fortedigital/nextjs-cache-handler

Version:
246 lines (242 loc) 9.01 kB
// src/constants.ts var REVALIDATED_TAGS_KEY = "__revalidated_tags__"; // src/helpers/const.ts var NEXT_CACHE_IMPLICIT_TAG_ID = "_N_T_"; var MAX_INT32 = 2 ** 31 - 1; // src/helpers/isImplicitTag.ts function isImplicitTag(tag) { return tag.startsWith(NEXT_CACHE_IMPLICIT_TAG_ID); } // src/handlers/redis-strings.ts function createHandler({ client, keyPrefix = "", sharedTagsKey = "__sharedTags__", sharedTagsTtlKey = "__sharedTagsTtl__", timeoutMs = 5e3, keyExpirationStrategy = "EXPIREAT", revalidateTagQuerySize = 1e4 }) { function assertClientIsReady() { if (!client.withAbortSignal(AbortSignal.timeout(timeoutMs)).isReady) { throw new Error( "Redis client is not ready yet or connection is lost. Keep trying..." ); } } async function revalidateTag(tag) { assertClientIsReady(); if (isImplicitTag(tag)) { await client.withAbortSignal(AbortSignal.timeout(timeoutMs)).hSet(revalidatedTagsKey, tag, Date.now()); } const tagsMap = /* @__PURE__ */ new Map(); let cursor = "0"; const hScanOptions = { COUNT: revalidateTagQuerySize }; do { const remoteTagsPortion = await client.hScan( keyPrefix + sharedTagsKey, cursor, hScanOptions ); for (const { field, value } of remoteTagsPortion.entries) { tagsMap.set(field, JSON.parse(value)); } cursor = remoteTagsPortion.cursor; } while (cursor !== "0"); const keysToDelete = []; const tagsToDelete = []; for (const [key, tags] of tagsMap) { if (tags.includes(tag)) { keysToDelete.push(keyPrefix + key); tagsToDelete.push(key); } } if (keysToDelete.length === 0) { return; } const deleteKeysOperation = client.withAbortSignal(AbortSignal.timeout(timeoutMs)).unlink(keysToDelete); const updateTagsOperation = client.withAbortSignal(AbortSignal.timeout(timeoutMs)).hDel(keyPrefix + sharedTagsKey, tagsToDelete); const updateTtlOperation = client.withAbortSignal(AbortSignal.timeout(timeoutMs)).hDel(keyPrefix + sharedTagsTtlKey, tagsToDelete); await Promise.all([ deleteKeysOperation, updateTtlOperation, updateTagsOperation ]); } async function revalidateSharedKeys() { assertClientIsReady(); const ttlMap = /* @__PURE__ */ new Map(); let cursor = "0"; const hScanOptions = { COUNT: revalidateTagQuerySize }; do { const remoteTagsPortion = await client.withAbortSignal(AbortSignal.timeout(timeoutMs)).hScan(keyPrefix + sharedTagsTtlKey, cursor, hScanOptions); for (const { field, value } of remoteTagsPortion.entries) { ttlMap.set(field, Number(value)); } cursor = remoteTagsPortion.cursor; } while (cursor !== "0"); const tagsAndTtlToDelete = []; const keysToDelete = []; for (const [key, ttlInSeconds] of ttlMap) { if ((/* @__PURE__ */ new Date()).getTime() > ttlInSeconds * 1e3) { tagsAndTtlToDelete.push(key); keysToDelete.push(keyPrefix + key); } } if (tagsAndTtlToDelete.length === 0) { return; } const deleteKeysOperation = client.withAbortSignal(AbortSignal.timeout(timeoutMs)).unlink(keysToDelete); const updateTtlOperation = client.withAbortSignal(AbortSignal.timeout(timeoutMs)).hDel(keyPrefix + sharedTagsTtlKey, tagsAndTtlToDelete); const updateTagsOperation = client.withAbortSignal(AbortSignal.timeout(timeoutMs)).hDel(keyPrefix + sharedTagsKey, tagsAndTtlToDelete); await Promise.all([ deleteKeysOperation, updateTagsOperation, updateTtlOperation ]); } const revalidatedTagsKey = keyPrefix + REVALIDATED_TAGS_KEY; return { name: "redis-strings", async get(key, { implicitTags }) { assertClientIsReady(); const result = await client.withAbortSignal(AbortSignal.timeout(timeoutMs)).get(keyPrefix + key); if (!result) { return null; } const cacheValue = JSON.parse(result); if (!cacheValue) { return null; } convertStringsToBuffers(cacheValue); const sharedTagKeyExists = await client.withAbortSignal(AbortSignal.timeout(timeoutMs)).hExists(keyPrefix + sharedTagsKey, key); if (!sharedTagKeyExists) { await client.withAbortSignal(AbortSignal.timeout(timeoutMs)).unlink(keyPrefix + key); return null; } const combinedTags = /* @__PURE__ */ new Set([...cacheValue.tags, ...implicitTags]); if (combinedTags.size === 0) { return cacheValue; } const revalidationTimes = await client.withAbortSignal(AbortSignal.timeout(timeoutMs)).hmGet(revalidatedTagsKey, Array.from(combinedTags)); for (const timeString of revalidationTimes) { if (timeString && Number.parseInt(timeString, 10) > cacheValue.lastModified) { await client.withAbortSignal(AbortSignal.timeout(timeoutMs)).unlink(keyPrefix + key); return null; } } return cacheValue; }, async set(key, cacheHandlerValue) { assertClientIsReady(); let setOperation; let expireOperation; const lifespan = cacheHandlerValue.lifespan; if (cacheHandlerValue?.value) { parseBuffersToStrings(cacheHandlerValue); } const setTagsOperation = client.withAbortSignal(AbortSignal.timeout(timeoutMs)).hSet( keyPrefix + sharedTagsKey, key, JSON.stringify(cacheHandlerValue.tags ?? []) ); const setSharedTtlOperation = lifespan ? client.withAbortSignal(AbortSignal.timeout(timeoutMs)).hSet(keyPrefix + sharedTagsTtlKey, key, lifespan.expireAt) : void 0; await Promise.all([setTagsOperation, setSharedTtlOperation]); switch (keyExpirationStrategy) { case "EXAT": { setOperation = client.withAbortSignal(AbortSignal.timeout(timeoutMs)).set( keyPrefix + key, JSON.stringify(cacheHandlerValue), typeof lifespan?.expireAt === "number" ? { EXAT: lifespan.expireAt } : void 0 ); break; } case "EXPIREAT": { setOperation = client.withAbortSignal(AbortSignal.timeout(timeoutMs)).set(keyPrefix + key, JSON.stringify(cacheHandlerValue)); expireOperation = lifespan ? client.withAbortSignal(AbortSignal.timeout(timeoutMs)).expireAt(keyPrefix + key, lifespan.expireAt) : void 0; break; } default: { throw new Error( `Invalid keyExpirationStrategy: ${keyExpirationStrategy}` ); } } await Promise.all([setOperation, expireOperation]); }, async revalidateTag(tag) { assertClientIsReady(); if (isImplicitTag(tag)) { await client.withAbortSignal(AbortSignal.timeout(timeoutMs)).hSet(revalidatedTagsKey, tag, Date.now()); } await Promise.all([revalidateTag(tag), revalidateSharedKeys()]); }, async delete(key) { await client.withAbortSignal(AbortSignal.timeout(timeoutMs)).unlink(keyPrefix + key); await Promise.all([ client.withAbortSignal(AbortSignal.timeout(timeoutMs)).hDel(keyPrefix + sharedTagsKey, key), client.withAbortSignal(AbortSignal.timeout(timeoutMs)).hDel(keyPrefix + sharedTagsTtlKey, key) ]); } }; function parseBuffersToStrings(cacheHandlerValue) { if (!cacheHandlerValue?.value) { return; } const value = { ...cacheHandlerValue.value }; const kind = value?.kind; if (kind === "APP_ROUTE") { const appRouteData = value; const appRouteValue = value; if (appRouteValue?.body) { appRouteData.body = appRouteValue.body.toString(); } } else if (kind === "APP_PAGE") { const appPageData = value; const appPageValue = value; if (appPageValue?.rscData) { appPageData.rscData = appPageValue.rscData.toString(); } if (appPageValue?.segmentData) { appPageData.segmentData = Object.fromEntries( Array.from(appPageValue.segmentData.entries()).map(([key, value2]) => [ key, value2.toString() ]) ); } } } function convertStringsToBuffers(cacheValue) { const value = cacheValue.value; const kind = value?.kind; if (kind === "APP_ROUTE") { const appRouteData = value; if (appRouteData?.body) { const appRouteValue = value; appRouteValue.body = Buffer.from(appRouteData.body, "utf-8"); } } else if (kind === "APP_PAGE") { const appPageData = value; const appPageValue = value; if (appPageData.rscData) { appPageValue.rscData = Buffer.from(appPageData.rscData, "utf-8"); } if (appPageData.segmentData) { appPageValue.segmentData = new Map( Object.entries(appPageData.segmentData).map(([key, value2]) => [ key, Buffer.from(value2, "utf-8") ]) ); } } } } export { createHandler as default };