![NPM Version](https://img.shields.io/npm/v/%40fortedigital%2Fnextjs-cache-handler)

# @fortedigital/nextjs-cache-handler

This package was initially built on top of [`@neshca/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 `@neshca/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 `@redis/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 `@neshca/cache-handler`

As `@neshca/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 `@neshca/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 [`@neshca/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 `@neshca/cache-handler` package.
