Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
265 changes: 136 additions & 129 deletions packages/nextjs-cache-handler/src/handlers/cache-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
Handler,
OnCreationHook,
Revalidate,
CacheHandlerMeta,
CacheHandlersValue,
} from "./cache-handler.types";
import { PrerenderManifest } from "next/dist/build";
import {
Expand All @@ -37,11 +39,9 @@ const PRERENDER_MANIFEST_VERSION = 4;
*
* @returns A Promise that resolves when all handlers have finished deleting the entry.
*/
async function removeEntryFromHandlers(
handlers: Handler[],
key: string,
debug: boolean,
): Promise<void> {
async function removeEntryFromHandlers<
T extends CacheHandlerValue | CacheHandlersValue = CacheHandlerValue,
>(handlers: Handler<T>[], key: string, debug: boolean): Promise<void> {
if (debug) {
console.info(
"[CacheHandler] [method: %s] [key: %s] %s",
Expand Down Expand Up @@ -80,6 +80,136 @@ async function removeEntryFromHandlers(
});
}

export function createMergedHandler<
T extends CacheHandlerValue | CacheHandlersValue = CacheHandlerValue,
>(handlersList: Handler<T>[]): Omit<Handler<T>, "name"> {
const debug = typeof process.env.NEXT_PRIVATE_DEBUG_CACHE !== "undefined";

return {
async get(key, meta) {
for (const handler of handlersList) {
if (debug) {
console.info(
"[CacheHandler] [handler: %s] [method: %s] [key: %s] %s",
handler.name,
"get",
key,
"Started retrieving value.",
);
}

try {
let cacheHandlerValue = await handler.get(key, meta);

if (
cacheHandlerValue?.lifespan &&
cacheHandlerValue.lifespan.expireAt < Math.floor(Date.now() / 1000)
) {
if (debug) {
console.info(
"[CacheHandler] [handler: %s] [method: %s] [key: %s] %s",
handler.name,
"get",
key,
"Entry expired.",
);
}

cacheHandlerValue = null;

// remove the entry from all handlers in background
removeEntryFromHandlers(handlersList, key, debug);
}

if (cacheHandlerValue && debug) {
console.info(
"[CacheHandler] [handler: %s] [method: %s] [key: %s] %s",
handler.name,
"get",
key,
"Successfully retrieved value.",
);
}

return cacheHandlerValue;
} catch (error) {
if (debug) {
console.warn(
"[CacheHandler] [handler: %s] [method: %s] [key: %s] %s",
handler.name,
"get",
key,
`Error: ${error}`,
);
}
}
}

return null;
},
async set(key, cacheHandlerValue) {
const operationsResults = await Promise.allSettled(
handlersList.map((handler) =>
handler.set(key, { ...cacheHandlerValue }),
),
);

if (!debug) {
return;
}

operationsResults.forEach((handlerResult, index) => {
if (handlerResult.status === "rejected") {
console.warn(
"[CacheHandler] [handler: %s] [method: %s] [key: %s] %s",
handlersList[index]?.name ?? `unknown-${index}`,
"set",
key,
`Error: ${handlerResult.reason}`,
);
} else {
console.info(
"[CacheHandler] [handler: %s] [method: %s] [key: %s] %s",
handlersList[index]?.name ?? `unknown-${index}`,
"set",
key,
"Successfully set value.",
);
}
});
},
async revalidateTag(tag) {
const operationsResults = await Promise.allSettled(
handlersList.map((handler) => handler.revalidateTag(tag)),
);

if (!debug) {
return;
}

operationsResults.forEach((handlerResult, index) => {
if (handlerResult.status === "rejected") {
console.warn(
"[CacheHandler] [handler: %s] [method: %s] [tag: %s] %s",
handlersList[index]?.name ?? `unknown-${index}`,
"revalidateTag",
tag,
`Error: ${handlerResult.reason}`,
);
} else {
console.info(
"[CacheHandler] [handler: %s] [method: %s] [tag: %s] %s",
handlersList[index]?.name ?? `unknown-${index}`,
"revalidateTag",
tag,
"Successfully revalidated tag.",
);
}
});
},
};
}

export class CacheHandler implements NextCacheHandler {
/**
* Provides a descriptive name for the CacheHandler class.
Expand Down Expand Up @@ -496,130 +626,7 @@ export class CacheHandler implements NextCacheHandler {

CacheHandler.#cacheListLength = handlersList.length;

CacheHandler.#mergedHandler = {
async get(key, meta) {
for (const handler of handlersList) {
if (CacheHandler.#debug) {
console.info(
"[CacheHandler] [handler: %s] [method: %s] [key: %s] %s",
handler.name,
"get",
key,
"Started retrieving value.",
);
}

try {
let cacheHandlerValue = await handler.get(key, meta);

if (
cacheHandlerValue?.lifespan &&
cacheHandlerValue.lifespan.expireAt <
Math.floor(Date.now() / 1000)
) {
if (CacheHandler.#debug) {
console.info(
"[CacheHandler] [handler: %s] [method: %s] [key: %s] %s",
handler.name,
"get",
key,
"Entry expired.",
);
}

cacheHandlerValue = null;

// remove the entry from all handlers in background
removeEntryFromHandlers(handlersList, key, CacheHandler.#debug);
}

if (cacheHandlerValue && CacheHandler.#debug) {
console.info(
"[CacheHandler] [handler: %s] [method: %s] [key: %s] %s",
handler.name,
"get",
key,
"Successfully retrieved value.",
);
}

return cacheHandlerValue;
} catch (error) {
if (CacheHandler.#debug) {
console.warn(
"[CacheHandler] [handler: %s] [method: %s] [key: %s] %s",
handler.name,
"get",
key,
`Error: ${error}`,
);
}
}
}

return null;
},
async set(key, cacheHandlerValue) {
const operationsResults = await Promise.allSettled(
handlersList.map((handler) =>
handler.set(key, { ...cacheHandlerValue }),
),
);

if (!CacheHandler.#debug) {
return;
}

operationsResults.forEach((handlerResult, index) => {
if (handlerResult.status === "rejected") {
console.warn(
"[CacheHandler] [handler: %s] [method: %s] [key: %s] %s",
handlersList[index]?.name ?? `unknown-${index}`,
"set",
key,
`Error: ${handlerResult.reason}`,
);
} else {
console.info(
"[CacheHandler] [handler: %s] [method: %s] [key: %s] %s",
handlersList[index]?.name ?? `unknown-${index}`,
"set",
key,
"Successfully set value.",
);
}
});
},
async revalidateTag(tag) {
const operationsResults = await Promise.allSettled(
handlersList.map((handler) => handler.revalidateTag(tag)),
);

if (!CacheHandler.#debug) {
return;
}

operationsResults.forEach((handlerResult, index) => {
if (handlerResult.status === "rejected") {
console.warn(
"[CacheHandler] [handler: %s] [method: %s] [tag: %s] %s",
handlersList[index]?.name ?? `unknown-${index}`,
"revalidateTag",
tag,
`Error: ${handlerResult.reason}`,
);
} else {
console.info(
"[CacheHandler] [handler: %s] [method: %s] [tag: %s] %s",
handlersList[index]?.name ?? `unknown-${index}`,
"revalidateTag",
tag,
"Successfully revalidated tag.",
);
}
});
},
};
CacheHandler.#mergedHandler = createMergedHandler(handlersList);

if (CacheHandler.#debug) {
console.info(
Expand Down
31 changes: 21 additions & 10 deletions packages/nextjs-cache-handler/src/handlers/cache-handler.types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { CacheEntry } from "next/dist/server/lib/cache-handlers/types.js";
import type {
CacheHandler as NextCacheHandler,
CacheHandlerValue as NextCacheHandlerValue,
Expand Down Expand Up @@ -59,7 +60,9 @@ export type HandlerGetMeta = {
/**
* Represents a cache Handler.
*/
export type Handler = {
export type Handler<
T extends CacheHandlerValue | CacheHandlersValue = CacheHandlerValue,
> = {
/**
* A descriptive name for the cache Handler.
*/
Expand Down Expand Up @@ -114,16 +117,13 @@ export type Handler = {
* }
* ```
*/
get: (
key: string,
meta: HandlerGetMeta,
) => Promise<CacheHandlerValue | null | undefined>;
get: (key: string, meta: HandlerGetMeta) => Promise<T | null | undefined>;
/**
* Sets or updates a value in the cache store.
*
* @param key - The unique string identifier for the cache entry.
*
* @param value - The value to be stored in the cache. See {@link CacheHandlerValue}.
* @param value - The value to be stored in the cache. See {@link T}.
*
* @returns A Promise that resolves when the value has been successfully set in the cache.
*
Expand All @@ -137,7 +137,7 @@ export type Handler = {
*
* Use the absolute time (`expireAt`) to set and expiration time for the cache entry in your cache store to be in sync with the file system cache.
*/
set: (key: string, value: CacheHandlerValue) => Promise<void>;
set: (key: string, value: T) => Promise<void>;
/**
* Deletes all cache entries that are associated with the specified tag.
* See [fetch `options.next.tags` and `revalidateTag` ↗](https://nextjs.org/docs/app/building-your-application/caching#fetch-optionsnexttags-and-revalidatetag)
Expand Down Expand Up @@ -183,12 +183,14 @@ export type TTLParameters = {
/**
* Configuration options for the {@link CacheHandler}.
*/
export type CacheHandlerConfig = {
export type CacheHandlerConfig<
T extends CacheHandlerValue | CacheHandlersValue = CacheHandlerValue,
> = {
/**
* An array of cache instances that conform to the Handler interface.
* Multiple caches can be used to implement various caching strategies or layers.
*/
handlers: (Handler | undefined | null)[];
handlers: (Handler<T> | undefined | null)[];
/**
* Time-to-live (TTL) options for the cache entries.
*/
Expand Down Expand Up @@ -329,7 +331,7 @@ export type FileSystemCacheContext = ConstructorParameters<
typeof FileSystemCache
>[0];

export type CacheHandlerValue = NextCacheHandlerValue & {
export type CacheHandlerMeta = {
/**
* Timestamp in milliseconds when the cache entry was last modified.
*/
Expand All @@ -346,3 +348,12 @@ export type CacheHandlerValue = NextCacheHandlerValue & {
*/
lifespan: LifespanParameters | null;
};

export type CacheHandlerValue = NextCacheHandlerValue & CacheHandlerMeta;

export type BufferedCacheEntry = Omit<CacheEntry, "value"> & {
value: Buffer;
};
export type CacheHandlersValue = {
value: BufferedCacheEntry;
} & CacheHandlerMeta;
Loading