From 84510994dab7b83715981108521b0dffc1197b91 Mon Sep 17 00:00:00 2001 From: Mathias Sandberg Date: Wed, 14 Jan 2026 23:04:18 +0100 Subject: [PATCH 1/3] feat: add support for redis client Adds support for existing Redis client to ISR cache handler --- packages/cache-handler/src/handlers/redis.ts | 45 +++++++++++++++++--- 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/packages/cache-handler/src/handlers/redis.ts b/packages/cache-handler/src/handlers/redis.ts index cb80fc2..d74c279 100644 --- a/packages/cache-handler/src/handlers/redis.ts +++ b/packages/cache-handler/src/handlers/redis.ts @@ -14,9 +14,12 @@ import type { export interface RedisCacheHandlerOptions extends CacheHandlerOptions { /** * Redis connection options (ioredis) - * Can be a URL string or RedisOptions object + * Can be: + * - A Redis client instance (ioredis) + * - A URL string + * - A RedisOptions object */ - redis?: string | RedisOptions; + redis?: Redis | string | RedisOptions; /** * Key prefix for all cache entries @@ -58,6 +61,7 @@ export interface RedisCacheHandlerOptions extends CacheHandlerOptions { * // In cache-handler.mjs or data-cache-handler.mjs * import { RedisCacheHandler } from "@mrjasonroy/cache-components-cache-handler/handlers/redis"; * + * // Option 1: Pass a Redis URL or config * export default class NextCacheHandler extends RedisCacheHandler { * constructor(options) { * super({ @@ -68,6 +72,26 @@ export interface RedisCacheHandlerOptions extends CacheHandlerOptions { * }); * } * } + * + * // Option 2: Pass an existing Redis client instance + * import Redis from "ioredis"; + * + * const redisClient = new Redis({ + * host: "localhost", + * port: 6379, + * // ... other options + * }); + * + * export default class NextCacheHandler extends RedisCacheHandler { + * constructor(options) { + * super({ + * ...options, + * redis: redisClient, // Pass existing client + * keyPrefix: "nextjs:cache:", + * defaultTTL: 3600 + * }); + * } + * } * ``` */ export class RedisCacheHandler implements CacheHandler { @@ -81,9 +105,14 @@ export class RedisCacheHandler implements CacheHandler { constructor(options: RedisCacheHandlerOptions = {}) { // Initialize Redis connection - if (typeof options.redis === "string") { + if (options.redis instanceof Redis) { + // Use existing Redis client instance + this.redis = options.redis; + } else if (typeof options.redis === "string") { + // Create new client from URL string this.redis = new Redis(options.redis); } else { + // Create new client from RedisOptions or default config this.redis = new Redis(options.redis || {}); } @@ -92,10 +121,12 @@ export class RedisCacheHandler implements CacheHandler { this.defaultTTL = options.defaultTTL; this.debug = options.debug ?? false; - // Handle Redis connection errors - this.redis.on("error", (err) => { - console.error("[RedisCacheHandler] Redis connection error:", err); - }); + // Handle Redis connection errors (only add listener if we created the client) + if (!(options.redis instanceof Redis)) { + this.redis.on("error", (err) => { + console.error("[RedisCacheHandler] Redis connection error:", err); + }); + } if (this.debug) { console.log("[RedisCacheHandler] Initialized", { From 5e6597c29191bee4f9cf43114d273f59d33b521f Mon Sep 17 00:00:00 2001 From: Mathias Sandberg Date: Thu, 15 Jan 2026 08:41:54 +0100 Subject: [PATCH 2/3] fix: ensure proper handling of Redis client lifecycle in RedisCacheHandler --- packages/cache-handler/src/handlers/redis.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/cache-handler/src/handlers/redis.ts b/packages/cache-handler/src/handlers/redis.ts index d74c279..2e5295f 100644 --- a/packages/cache-handler/src/handlers/redis.ts +++ b/packages/cache-handler/src/handlers/redis.ts @@ -102,6 +102,7 @@ export class RedisCacheHandler implements CacheHandler { private readonly tagPrefix: string; private readonly defaultTTL?: number; private readonly debug: boolean; + private didCreateClient = false; constructor(options: RedisCacheHandlerOptions = {}) { // Initialize Redis connection @@ -111,9 +112,11 @@ export class RedisCacheHandler implements CacheHandler { } else if (typeof options.redis === "string") { // Create new client from URL string this.redis = new Redis(options.redis); + this.didCreateClient = true; } else { // Create new client from RedisOptions or default config this.redis = new Redis(options.redis || {}); + this.didCreateClient = true; } this.keyPrefix = options.keyPrefix ?? "nextjs:cache:"; @@ -320,11 +323,17 @@ export class RedisCacheHandler implements CacheHandler { /** * Close the Redis connection * Call this when shutting down your application + * Note: Only closes connections created by this handler, not shared clients */ async close(): Promise { try { - await this.redis.quit(); - this.log("Connection closed"); + // Only close if we created the client + if (this.didCreateClient) { + await this.redis.quit(); + this.log("Connection closed"); + } else { + this.log("Skipping close (using shared client)"); + } } catch (error) { console.error("[RedisCacheHandler] close error:", error); } From 2185376adb58b67a60104cc63ad24c1dc12662ed Mon Sep 17 00:00:00 2001 From: Jason Roy Date: Tue, 24 Feb 2026 11:21:52 -0800 Subject: [PATCH 3/3] fix: use duck typing for client detection, add tests - Replace instanceof Redis with duck typing (avoids fragility across package versions and bundler setups) - Use didCreateClient consistently for error listener check - Add 5 tests for existing client behavior (lifecycle, error listeners) Co-Authored-By: Claude Opus 4.6 --- .../cache-handler/src/handlers/redis.test.ts | 66 +++++++++++++++++++ packages/cache-handler/src/handlers/redis.ts | 23 ++++--- 2 files changed, 80 insertions(+), 9 deletions(-) diff --git a/packages/cache-handler/src/handlers/redis.test.ts b/packages/cache-handler/src/handlers/redis.test.ts index 722b73e..ea5acef 100644 --- a/packages/cache-handler/src/handlers/redis.test.ts +++ b/packages/cache-handler/src/handlers/redis.test.ts @@ -91,6 +91,13 @@ class FakeRedis { handlers.push(handler); return this; } + + public quitCalled = false; + + async quit(): Promise { + this.quitCalled = true; + return "OK"; + } } // Mock ioredis to return our FakeRedis @@ -404,4 +411,63 @@ describe("RedisCacheHandler", () => { expect(rawData).toBeNull(); }); }); + + describe("existing Redis client support", () => { + test("should accept an existing Redis client instance", async () => { + const handler = new RedisCacheHandler({ + redis: fakeRedis as unknown as import("ioredis").default, + }); + + const value: CacheValue = { + kind: "FETCH", + data: { + headers: { "content-type": "application/json" }, + body: '{"shared": true}', + status: 200, + url: "https://example.com", + }, + revalidate: 60, + }; + + await handler.set("shared-key", value, { revalidate: false }); + const result = await handler.get("shared-key"); + + expect(result).not.toBeNull(); + expect(result?.value).toEqual(value); + }); + + test("should not add error listener for existing client", async () => { + const listenersBefore = fakeRedis.listeners.get("error")?.length ?? 0; + + new RedisCacheHandler({ + redis: fakeRedis as unknown as import("ioredis").default, + }); + + const listenersAfter = fakeRedis.listeners.get("error")?.length ?? 0; + expect(listenersAfter).toBe(listenersBefore); + }); + + test("should add error listener for internally created client", async () => { + new RedisCacheHandler(); + + const errorListeners = fakeRedis.listeners.get("error")?.length ?? 0; + expect(errorListeners).toBeGreaterThan(0); + }); + + test("should not close shared client on close()", async () => { + const handler = new RedisCacheHandler({ + redis: fakeRedis as unknown as import("ioredis").default, + }); + + await handler.close(); + expect(fakeRedis.quitCalled).toBe(false); + }); + + test("should close internally created client on close()", async () => { + const handler = new RedisCacheHandler(); + + await handler.close(); + expect(fakeRedis.quitCalled).toBe(true); + }); + }); }); diff --git a/packages/cache-handler/src/handlers/redis.ts b/packages/cache-handler/src/handlers/redis.ts index 2e5295f..5b272f6 100644 --- a/packages/cache-handler/src/handlers/redis.ts +++ b/packages/cache-handler/src/handlers/redis.ts @@ -105,17 +105,22 @@ export class RedisCacheHandler implements CacheHandler { private didCreateClient = false; constructor(options: RedisCacheHandlerOptions = {}) { - // Initialize Redis connection - if (options.redis instanceof Redis) { - // Use existing Redis client instance - this.redis = options.redis; + // Initialize Redis connection - detect existing client via duck typing + // (instanceof can break across package versions or bundler setups) + if ( + options.redis && + typeof options.redis === "object" && + "get" in options.redis && + "set" in options.redis && + "del" in options.redis && + typeof (options.redis as Redis).get === "function" + ) { + this.redis = options.redis as Redis; } else if (typeof options.redis === "string") { - // Create new client from URL string this.redis = new Redis(options.redis); this.didCreateClient = true; } else { - // Create new client from RedisOptions or default config - this.redis = new Redis(options.redis || {}); + this.redis = new Redis((options.redis as RedisOptions) || {}); this.didCreateClient = true; } @@ -124,8 +129,8 @@ export class RedisCacheHandler implements CacheHandler { this.defaultTTL = options.defaultTTL; this.debug = options.debug ?? false; - // Handle Redis connection errors (only add listener if we created the client) - if (!(options.redis instanceof Redis)) { + // Only attach error listener for clients we created + if (this.didCreateClient) { this.redis.on("error", (err) => { console.error("[RedisCacheHandler] Redis connection error:", err); });