@fortedigital/nextjs-cache-handler
Version:
Next.js cache handlers
320 lines (241 loc) • 10.6 kB
Markdown

# /nextjs-cache-handler
This package was initially built on top of [`/cache-handler`](https://www.npmjs.com/package/@neshca/cache-handler) by providing additional cache handlers for specialized use cases, specifically for Redis-based caching solutions. The original `/cache-handler` stopped getting updated for Next 15 and above. This package addresses compatibility issues with Next 15 and onwards.
## Migration
### 1.x.x -> 2.x.x
Starting with 2.0.0 we require Next 15 and Redis 5.
Remove `redis` from your dependencies if you don't need anything other than `/client`. Then install `@redis/client`.
Upgrade Next and Redis in your project to:
```
"next": ">=15.2.4",
"@redis/client": ">= 5.1.1"
```
OR if you need full `redis` package
```
"next": ">=15.2.4",
"redis": ">= 5.1.1"
```
1.x.x
```
const { Next15CacheHandler } = require("@fortedigital/nextjs-cache-handler/next-15-cache-handler");
module.exports = new Next15CacheHandler();
```
2.x.x or higher
```
const { CacheHandler } = require("@fortedigital/nextjs-cache-handler");
module.exports = CacheHandler;
```
`createBufferStringHandler` was built into `redis-strings` and does not need to be used with Next 15 anymore.
### 1.2.x -> ^1.3.x
#### cache-handler
1.2.x
```
const { Next15CacheHandler } = require("@fortedigital/nextjs-cache-handler/next-15-cache-handler");
module.exports = new Next15CacheHandler();
```
^1.3.x
```
const { Next15CacheHandler } = require("@fortedigital/nextjs-cache-handler");
module.exports = Next15CacheHandler;
```
#### instrumentation
1.2.x
```
if (process.env.NEXT_RUNTIME === "nodejs") {
const { registerInitialCache } = await import('@neshca/cache-handler/instrumentation')
const CacheHandler = (await import("./cache-handler.js")).default;
await registerInitialCache(CacheHandler);
}
```
^1.3.x
```
if (process.env.NEXT_RUNTIME === "nodejs") {
const { registerInitialCache } = await import("@fortedigital/nextjs-cache-handler/instrumentation");
const CacheHandler = (await import("./cache-handler.js")).default;
await registerInitialCache(CacheHandler);
}
```
## Installation
If you come from Next 14 or below, ensure to flush Redis cache before you run the project with this library. Cache format between Next 15 and Next <14 are not compatible!
## Next 15 Support and migration from `/cache-handler`
As `/cache-handler` does not officially support Next 15+ yet, we try to keep up with Next and prepare more or less temporary workarounds. At some point we will either create a fork of `@neshca/cache-handler` to fully support Next 15 or it gets updated by the maintainers. As for now we're building a set of decorators/workarounds you can use to build cache solutions for Next 15. We might need to do a full-blown rework which will be marked with a proper major version upgrade.
### Revalidate fetch breaking change
Instead of:
```js
const { CacheHandler } = require("@neshca/cache-handler");
CacheHandler.onCreation(() => {
// your usual setup
});
module.exports = CacheHandler;
```
Use this:
```js
const { CacheHandler } = require("@fortedigital/nextjs-cache-handler");
CacheHandler.onCreation(() => {
// your usual setup
});
module.exports = CacheHandler;
```
### Instrumentation
Instead of:
```js
export async function register() {
if (process.env.NEXT_RUNTIME === "nodejs") {
const { registerInitialCache } = await import(
"@neshca/cache-handler/instrumentation"
);
const CacheHandler = (await import("../cache-handler.mjs")).default;
await registerInitialCache(CacheHandler);
}
}
```
Use this:
```js
export async function register() {
if (process.env.NEXT_RUNTIME === "nodejs") {
const { registerInitialCache } = await import(
"@fortedigital/nextjs-cache-handler/instrumentation"
);
const CacheHandler = (await import("../cache-handler.mjs")).default;
await registerInitialCache(CacheHandler);
}
}
```
## Handlers
### 1. `redis-strings`
This handler is designed to manage cache operations using Redis strings. It supports key-based and tag-based caching with flexible expiration strategies. Opposite to `/cache-handler`, `@fortedigital/nextjs-cache-handler`'s implementation does not have a memory leak caused by endlessly growing shared key hashmap, by adding another hashmap with TTL for shared keys entries. It also has more reliable revalidateTagQuerySize default value - 10_000 - preventing long tag revalidatation requests with large caches.
#### Features:
- **Key Expiration Strategy**: Choose between `EXAT` (more efficient with Redis 6.2 or newer) or `EXPIREAT` (compatible with Redis 4.0 or newer).
- **Tag-based Revalidation**: Supports cache revalidation using tags for efficient and fine-grained cache invalidation.
- **TTL Management**: Automatically manages time-to-live (TTL) for cache keys and tags.
#### Usage:
```js
const redisHandler = await createHandler({
client, // Redis client instance
keyPrefix: "myApp:",
sharedTagsKey: "myTags",
sharedTagsTtlKey: "myTagTtls",
});
```
### 2. `composite`
The composite handler allows for managing cache operations using a combination of other handlers. It provides a flexible way to route cache operations to multiple underlying handlers based on a defined strategy.
#### Features:
- **Flexible Handling**: Supports multiple handlers, enabling complex caching strategies.
- **Custom Set Strategy**: Allows you to define how cache entries are routed to different handlers.
- **On-Demand Revalidation**: Retrieves values from the first available handler, ensuring efficient data access.
#### Usage:
```js
const compositeHandler = createHandler({
handlers: [handler1, handler2], // Array of underlying handlers
setStrategy: (data) => (data?.tags.includes("handler1") ? 0 : 1),
});
```
## Full example
```js
// @neshca/cache-handler dependencies
const createLruHandler = require("@neshca/cache-handler/local-lru").default;
// Next/Redis dependencies
const { createClient } = require("redis");
const { PHASE_PRODUCTION_BUILD } = require("next/constants");
// @fortedigital/nextjs-cache-handler dependencies
const createCompositeHandler =
require("@fortedigital/nextjs-cache-handler/composite").default;
const createRedisHandler =
require("@fortedigital/nextjs-cache-handler/redis-strings").default;
const createBufferStringHandler =
require("@fortedigital/nextjs-cache-handler/buffer-string-decorator").default;
const { CacheHandler } = require("@fortedigital/nextjs-cache-handler");
// Usual onCreation from @neshca/cache-handler
CacheHandler.onCreation(() => {
// Important - It's recommended to use global scope to ensure only one Redis connection is made
// This ensures only one instance get created
if (global.cacheHandlerConfig) {
return global.cacheHandlerConfig;
}
// Important - It's recommended to use global scope to ensure only one Redis connection is made
// This ensures new instances are not created in a race condition
if (global.cacheHandlerConfigPromise) {
return global.cacheHandlerConfigPromise;
}
// You may need to ignore Redis locally, remove this block otherwise
if (process.env.NODE_ENV === "development") {
const lruCache = createLruHandler();
return { handlers: [lruCache] };
}
// Main promise initializing the handler
global.cacheHandlerConfigPromise = (async () => {
/** @type {import("redis").RedisClientType | null} */
let redisClient = null;
if (PHASE_PRODUCTION_BUILD !== process.env.NEXT_PHASE) {
const settings = {
url: process.env.REDIS_URL, // Make sure you configure this variable
pingInterval: 10000,
};
// This is optional and needed only if you use access keys
if (process.env.REDIS_ACCESS_KEY) {
settings.password = process.env.REDIS_ACCESS_KEY;
}
try {
redisClient = createClient(settings);
redisClient.on("error", (e) => {
if (typeof process.env.NEXT_PRIVATE_DEBUG_CACHE !== "undefined") {
console.warn("Redis error", e);
}
global.cacheHandlerConfig = null;
global.cacheHandlerConfigPromise = null;
});
} catch (error) {
console.warn("Failed to create Redis client:", error);
}
}
if (redisClient) {
try {
console.info("Connecting Redis client...");
await redisClient.connect();
console.info("Redis client connected.");
} catch (error) {
console.warn("Failed to connect Redis client:", error);
await redisClient
.disconnect()
.catch(() =>
console.warn(
"Failed to quit the Redis client after failing to connect."
)
);
}
}
const lruCache = createLruHandler();
if (!redisClient?.isReady) {
console.error("Failed to initialize caching layer.");
global.cacheHandlerConfigPromise = null;
global.cacheHandlerConfig = { handlers: [lruCache] };
return global.cacheHandlerConfig;
}
const redisCacheHandler = createRedisHandler({
client: redisClient,
keyPrefix: "nextjs:",
});
global.cacheHandlerConfigPromise = null;
// This example uses composite handler to switch from Redis to LRU cache if tags contains `memory-cache` tag.
// You can skip composite and use Redis or LRU only.
global.cacheHandlerConfig = {
handlers: [
createCompositeHandler({
handlers: [
lruCache,
createBufferStringHandler(redisCacheHandler), // Use `createBufferStringHandler` in Next15 and ignore it in Next14 or below
],
setStrategy: (ctx) => (ctx?.tags.includes("memory-cache") ? 0 : 1), // You can adjust strategy for deciding which cache should the composite use
}),
],
};
return global.cacheHandlerConfig;
})();
return global.cacheHandlerConfigPromise;
});
module.exports = CacheHandler;
```
## Reference to Original Package
This package builds upon the core functionality provided by [`/cache-handler`](https://www.npmjs.com/package/@neshca/cache-handler). You can find more information about the core library, including usage examples and API documentation, at the [official documentation page](https://caching-tools.github.io/next-shared-cache).
## License
This project is licensed under the [MIT License](./LICENSE), as was the original `/cache-handler` package.