@trieb.work/nextjs-turbo-redis-cache
Version:
Designed for speed, scalability, and optimized performance, nextjs-turbo-redis-cache is your custom cache handler for demanding production environments.
254 lines (246 loc) • 10.9 kB
text/typescript
import { RedisClientOptions } from 'redis';
/**
* Pluggable wire-format codec for Redis string values used by `RedisStringsHandler`.
*
* The serializer is the single point at which an in-memory `CacheEntry` becomes the
* string written to Redis (and back). Plugging in a custom serializer lets you add
* compression (gzip/brotli), encryption, or any other custom encoding without forking
* this package or losing the existing dedup / batch / keyspace features.
*
* Both `serialize` and `deserialize` may return either a value directly or a `Promise`,
* which enables non-blocking async codecs such as stream-based compression
* (`zlib.brotliCompress`) or encryption (`crypto.subtle`). Synchronous implementations
* continue to work unchanged - awaiting a plain value is a no-op.
*
* The default export {@link jsonCacheValueSerializer} is `JSON.stringify` /
* `JSON.parse` paired with {@link bufferAndMapReplacer} / {@link bufferAndMapReviver},
* so native `Buffer` and `Map` values inside a `CacheEntry` round-trip transparently
* (this is required for Next.js RSC payloads).
*
* Operational note: changing the serializer (or any of its parameters such as a
* compression level or encryption key) makes existing Redis keys unreadable, because
* the deserializer will fail or return `null` for entries written by the previous
* format. Either flush the affected keys, bump `keyPrefix`, or migrate values
* out-of-band before deploying a new serializer.
*/
type CacheValueSerializer = {
/**
* Encode an in-memory `CacheEntry` into the string written to Redis.
* May return a `Promise` for async codecs (e.g. compression, encryption).
*/
serialize(value: CacheEntry): string | Promise<string>;
/**
* Decode a string read from Redis back into a `CacheEntry`.
* Returning `null` (or a `Promise<null>`) signals "treat as cache miss" -
* the handler will return `null` from `get()` without surfacing an error.
* May return a `Promise` for async codecs.
*/
deserialize(stored: string): CacheEntry | null | Promise<CacheEntry | null>;
};
/**
* Default serializer used by `RedisStringsHandler` when no `valueSerializer` is
* configured. Wraps `JSON.stringify` / `JSON.parse` with this package's
* {@link bufferAndMapReplacer} / {@link bufferAndMapReviver} so native `Buffer`
* and `Map` values inside a `CacheEntry` survive the round-trip.
*
* Exported as a singleton so consumers can compare against the default by
* reference (e.g. to detect that no custom serializer was configured).
*/
declare const jsonCacheValueSerializer: CacheValueSerializer;
type CacheEntry = {
value: unknown;
lastModified: number;
tags: string[];
};
type CreateRedisStringsHandlerOptions = {
/** Redis redisUrl to use.
* @default process.env.REDIS_URL? process.env.REDIS_URL : process.env.REDISHOST
? `redis://${process.env.REDISHOST}:${process.env.REDISPORT}`
: 'redis://localhost:6379'
*/
redisUrl?: string;
/** Redis database number to use. Uses DB 0 for production, DB 1 otherwise
* @default process.env.VERCEL_ENV === 'production' ? 0 : 1
*/
database?: number;
/** Prefix added to all Redis keys
* @default process.env.KEY_PREFIX || process.env.VERCEL_URL || 'UNDEFINED_URL_'
*/
keyPrefix?: string;
/** Timeout in milliseconds for time critical Redis operations (during cache get, which blocks site rendering).
* If redis get is not fulfilled within this time, the cache handler will return null so site rendering will
* not be blocked further and site can fallback to re-render/re-fetch the content.
* @default 500
*/
getTimeoutMs?: number;
/** Number of entries to query in one batch during full sync of shared tags hash map
* @default 250
*/
revalidateTagQuerySize?: number;
/** Key used to store shared tags hash map in Redis
* @default '__sharedTags__'
*/
sharedTagsKey?: string;
/** Average interval in milliseconds between tag map full re-syncs
* @default 3600000 (1 hour)
*/
avgResyncIntervalMs?: number;
/** Enable deduplication of Redis get requests via internal in-memory cache
* @default true
*/
redisGetDeduplication?: boolean;
/** Time in milliseconds to cache Redis get results in memory. Set this to 0 to disable in-memory caching completely
* @default 10000
*/
inMemoryCachingTime?: number;
/** Default stale age in seconds for cached items
* @default 1209600 (14 days)
*/
defaultStaleAge?: number;
/** Function to calculate expire age (redis TTL value) from stale age
* @default Production: staleAge * 2, Other: staleAge * 1.2
*/
estimateExpireAge?: (staleAge: number) => number;
/** Kill container on Redis client error if error threshold is reached
* @default 0 (0 means no error threshold)
*/
killContainerOnErrorThreshold?: number;
/** Additional Redis client socket options
* @example { tls: true, rejectUnauthorized: false }
*/
socketOptions?: RedisClientOptions['socket'];
/** Additional Redis client options to be passed directly to createClient
* @example { username: 'user', password: 'pass' }
*/
clientOptions?: Omit<RedisClientOptions, 'url' | 'database' | 'socket'>;
/** Pluggable wire-format codec for Redis string values. Lets you plug in
* compression (gzip/brotli), encryption, or any other custom encoding without
* forking this package or losing the existing dedup / batch / keyspace features.
*
* Both `serialize` and `deserialize` may return a `Promise`, enabling
* non-blocking async codecs (e.g. `zlib.brotliCompress`) that don't block the
* Node.js event loop. Synchronous implementations continue to work unchanged.
*
* Only the main cache-entry storage path is routed through the serializer.
* The shared-tags map and the revalidated-tags map are not. The in-memory
* deduplication cache stores the wire-format string verbatim - its contents
* change with the serializer, but the cache itself is not re-encoded.
*
* Operational note: changing the serializer (or any of its parameters such as
* a compression level or encryption key) makes existing Redis keys unreadable.
* Either flush the affected keys or bump `keyPrefix` before deploying.
*
* @default jsonCacheValueSerializer (JSON.stringify with bufferAndMapReplacer
* so native Buffer and Map values inside a CacheEntry round-trip transparently)
*/
valueSerializer?: CacheValueSerializer;
};
declare class RedisStringsHandler {
private client;
private sharedTagsMap;
private revalidatedTagsMap;
private inMemoryDeduplicationCache;
private getTimeoutMs;
private redisGet;
private redisDeduplicationHandler;
private deduplicatedRedisGet;
private keyPrefix;
private redisGetDeduplication;
private inMemoryCachingTime;
private defaultStaleAge;
private estimateExpireAge;
private killContainerOnErrorThreshold;
private valueSerializer;
constructor({ redisUrl, database, keyPrefix, sharedTagsKey, getTimeoutMs, revalidateTagQuerySize, avgResyncIntervalMs, redisGetDeduplication, inMemoryCachingTime, defaultStaleAge, estimateExpireAge, killContainerOnErrorThreshold, socketOptions, clientOptions, valueSerializer, }: CreateRedisStringsHandlerOptions);
resetRequestCache(): void;
private clientReadyCalls;
private assertClientIsReady;
get(key: string, ctx: {
kind: 'APP_ROUTE' | 'APP_PAGE';
isRoutePPREnabled: boolean;
isFallback: boolean;
} | {
kind: 'FETCH';
revalidate: number;
fetchUrl: string;
fetchIdx: number;
tags: string[];
softTags: string[];
isFallback: boolean;
}): Promise<CacheEntry | null>;
set(key: string, data: {
kind: 'APP_PAGE';
status?: number;
headers: {
'x-nextjs-stale-time': string;
'x-next-cache-tags': string;
};
html: string;
rscData: Buffer;
segmentData: unknown;
postboned: unknown;
} | {
kind: 'APP_ROUTE';
status: number;
headers: {
'cache-control'?: string;
'x-nextjs-stale-time': string;
'x-next-cache-tags': string;
};
body: Buffer;
} | {
kind: 'FETCH';
data: {
headers: Record<string, string>;
body: string;
status: number;
url: string;
};
revalidate: number | false;
}, ctx: {
isRoutePPREnabled: boolean;
isFallback: boolean;
tags?: string[];
revalidate?: number | false;
cacheControl?: {
revalidate: 5;
expire: undefined;
};
}): Promise<void>;
revalidateTag(tagOrTags: string | string[], ...rest: any[]): Promise<void>;
}
type NextCacheHandlerOptions = CreateRedisStringsHandlerOptions & {
serverDistDir?: string;
};
declare class CachedHandler {
constructor(options: NextCacheHandlerOptions);
get(...args: Parameters<RedisStringsHandler['get']>): ReturnType<RedisStringsHandler['get']>;
set(...args: Parameters<RedisStringsHandler['set']>): ReturnType<RedisStringsHandler['set']>;
revalidateTag(...args: Parameters<RedisStringsHandler['revalidateTag']>): ReturnType<RedisStringsHandler['revalidateTag']>;
resetRequestCache(...args: Parameters<RedisStringsHandler['resetRequestCache']>): ReturnType<RedisStringsHandler['resetRequestCache']>;
}
declare function bufferAndMapReviver(_: string, value: any): any;
declare function bufferAndMapReplacer(_: string, value: any): any;
interface CacheComponentsEntry {
value: ReadableStream<Uint8Array>;
tags: string[];
stale: number;
timestamp: number;
expire: number;
revalidate: number;
}
interface CacheComponentsHandler {
get(cacheKey: string, softTags: string[]): Promise<CacheComponentsEntry | undefined>;
set(cacheKey: string, pendingEntry: Promise<CacheComponentsEntry>): Promise<void>;
refreshTags(): Promise<void>;
getExpiration(tags: string[]): Promise<number>;
updateTags(tags: string[], durations?: {
expire?: number;
}): Promise<void>;
}
type CreateCacheComponentsHandlerOptions = CreateRedisStringsHandlerOptions & {
serverDistDir?: string;
};
declare function getRedisCacheComponentsHandler(options?: CreateCacheComponentsHandlerOptions): CacheComponentsHandler;
declare const redisCacheHandler: CacheComponentsHandler;
export { type CacheValueSerializer, type CreateRedisStringsHandlerOptions, RedisStringsHandler, bufferAndMapReplacer, bufferAndMapReviver, CachedHandler as default, getRedisCacheComponentsHandler, jsonCacheValueSerializer, redisCacheHandler };