From 883c280b59af6d114b4f49b391ff079484f38092 Mon Sep 17 00:00:00 2001 From: Sergii Medvid Date: Sun, 22 Feb 2026 11:08:28 +0200 Subject: [PATCH 1/3] fixed ttl inconsistency between usage of ioredis and node-redis clients --- .../cache-handler/src/data-cache/redis.test.ts | 17 +++++++++-------- packages/cache-handler/src/data-cache/redis.ts | 7 +++---- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/cache-handler/src/data-cache/redis.test.ts b/packages/cache-handler/src/data-cache/redis.test.ts index ef0c56e..5afd2eb 100644 --- a/packages/cache-handler/src/data-cache/redis.test.ts +++ b/packages/cache-handler/src/data-cache/redis.test.ts @@ -27,9 +27,10 @@ class FakeRedis { this.setCalls.push({ key, args }); let expireAt: number | undefined; - // ioredis style: set(key, value, "EX", seconds) - if (args[0] === "EX" && typeof args[1] === "number") { - expireAt = Date.now() + args[1] * 1000; + // node-redis style: set(key, value, { EX: seconds }) + const opts = args[0] as { EX?: number } | undefined; + if (opts && typeof opts === "object" && typeof opts.EX === "number") { + expireAt = Date.now() + opts.EX * 1000; } this.store.set(key, { value, expireAt }); @@ -147,7 +148,7 @@ describe("RedisDataCacheHandler", () => { expect(redis.setCalls).toHaveLength(1); expect(redis.setCalls[0]).toMatchObject({ key: "nextjs:data-cache:cache-key", - args: ["EX", 120], + args: [{ EX: 120 }], }); const result = await handler.get("cache-key", []); @@ -231,7 +232,7 @@ describe("RedisDataCacheHandler", () => { expect(redis.delCalls).toContainEqual(["nextjs:data-cache:invalidate-key"]); }); - test("sets TTL correctly with ioredis style args (fixes #16)", async () => { + test("sets TTL correctly with node-redis style options (fixes #16)", async () => { vi.useFakeTimers(); vi.setSystemTime(BASE_TIME); @@ -241,11 +242,11 @@ describe("RedisDataCacheHandler", () => { const entry = createEntry("ttl-test", { expire: 60, revalidate: 30 }); await handler.set("ttl-key", Promise.resolve(entry)); - // Verify the set call used ioredis style: "EX", seconds + // Verify the set call used node-redis style: { EX: seconds } expect(redis.setCalls).toHaveLength(1); const setCall = redis.setCalls[0]; expect(setCall.key).toBe("nextjs:data-cache:ttl-key"); - expect(setCall.args).toEqual(["EX", 60]); + expect(setCall.args).toEqual([{ EX: 60 }]); }); test("TTL causes entry to expire after specified time", async () => { @@ -286,6 +287,6 @@ describe("RedisDataCacheHandler", () => { await handler.set("default-ttl-key", Promise.resolve(entry)); expect(redis.setCalls).toHaveLength(1); - expect(redis.setCalls[0].args).toEqual(["EX", 3600]); + expect(redis.setCalls[0].args).toEqual([{ EX: 3600 }]); }); }); diff --git a/packages/cache-handler/src/data-cache/redis.ts b/packages/cache-handler/src/data-cache/redis.ts index ea8531e..f50e4c9 100644 --- a/packages/cache-handler/src/data-cache/redis.ts +++ b/packages/cache-handler/src/data-cache/redis.ts @@ -10,7 +10,7 @@ import type { DataCacheEntry, DataCacheHandler } from "./types.js"; export interface RedisDataCacheHandlerOptions { /** - * Redis client instance (ioredis compatible) + * Redis client instance (node-redis) */ redis: RedisClient; @@ -41,8 +41,7 @@ export interface RedisDataCacheHandlerOptions { } /** - * Redis client interface (ioredis compatible) - * Uses ioredis-style SET with "EX" positional args: set(key, value, "EX", seconds) + * Redis client interface (node-redis) */ export interface RedisClient { get(key: string): Promise; @@ -275,7 +274,7 @@ export function createRedisDataCacheHandler( const ttl = entry.expire < 4294967294 ? entry.expire : defaultTTL; // Store in Redis with TTL - await redis.set(key, JSON.stringify(serialized), "EX", Math.ceil(ttl)); + await redis.set(key, JSON.stringify(serialized), { EX: Math.ceil(ttl) }); log?.("set", cacheKey, "done", { ttl }); } catch (error) { From ad841cee2f15383ea13235302dbc1990eb2af7b5 Mon Sep 17 00:00:00 2001 From: Jason Roy Date: Tue, 24 Feb 2026 11:20:05 -0800 Subject: [PATCH 2/3] fix: update factory adapter to translate node-redis style TTL to ioredis The RedisClient interface now uses node-redis style SET options ({ EX: seconds }). The factory adapter must translate these to ioredis's positional format ("EX", seconds). Co-Authored-By: Claude Opus 4.6 --- packages/cache-handler/src/data-cache/factory.ts | 11 +++++++---- packages/cache-handler/src/data-cache/redis.ts | 3 ++- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/cache-handler/src/data-cache/factory.ts b/packages/cache-handler/src/data-cache/factory.ts index 991354f..9627a01 100644 --- a/packages/cache-handler/src/data-cache/factory.ts +++ b/packages/cache-handler/src/data-cache/factory.ts @@ -24,14 +24,17 @@ function loadIoredis(type: string): typeof import("ioredis").default { } /** - * Create adapter for ioredis (lowercase methods) to match RedisClient interface (camelCase) + * Create adapter for ioredis (lowercase methods) to match RedisClient interface (camelCase). + * Translates node-redis-style SET options `{ EX: seconds }` to ioredis positional args. */ function createRedisAdapter(redis: import("ioredis").default): RedisClient { return { get: (key) => redis.get(key), - set: (key, value, exFlag?, ttl?) => { - if (exFlag === "EX" && typeof ttl === "number") { - return redis.set(key, value, "EX", ttl) as Promise; + set: (key, value, ...args) => { + // node-redis style: set(key, value, { EX: seconds }) + const opts = args[0] as Record | undefined; + if (opts && typeof opts === "object" && typeof opts.EX === "number") { + return redis.set(key, value, "EX", opts.EX) as Promise; } return redis.set(key, value) as Promise; }, diff --git a/packages/cache-handler/src/data-cache/redis.ts b/packages/cache-handler/src/data-cache/redis.ts index f50e4c9..099a8a8 100644 --- a/packages/cache-handler/src/data-cache/redis.ts +++ b/packages/cache-handler/src/data-cache/redis.ts @@ -41,7 +41,8 @@ export interface RedisDataCacheHandlerOptions { } /** - * Redis client interface (node-redis) + * Redis client interface (node-redis compatible) + * Uses node-redis-style SET with options object: set(key, value, { EX: seconds }) */ export interface RedisClient { get(key: string): Promise; From e070140e7b1c007d1e449f0c160fca1215a138ba Mon Sep 17 00:00:00 2001 From: Brendan Holly Date: Mon, 9 Mar 2026 17:20:04 -0400 Subject: [PATCH 3/3] docs: Add setup:e2e script for installing playwright chromium and update contributing documentation for clarity --- .github/workflows/ci.yml | 4 ++-- .github/workflows/nextjs-canary-test.yml | 2 +- CONTRIBUTING.md | 14 +++++++++----- package.json | 3 ++- 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d731d50..8ff56e8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -107,7 +107,7 @@ jobs: run: pnpm build - name: Install Playwright browsers - run: pnpm --filter e2e-test-app exec playwright install --with-deps chromium + run: pnpm setup:e2e - name: Build e2e app env: @@ -169,7 +169,7 @@ jobs: run: pnpm build - name: Install Playwright browsers - run: pnpm --filter e2e-test-app exec playwright install --with-deps chromium + run: pnpm setup:e2e - name: Build e2e app with ElastiCache handler env: diff --git a/.github/workflows/nextjs-canary-test.yml b/.github/workflows/nextjs-canary-test.yml index b35639c..1808c6b 100644 --- a/.github/workflows/nextjs-canary-test.yml +++ b/.github/workflows/nextjs-canary-test.yml @@ -71,7 +71,7 @@ jobs: - name: Install Playwright browsers if: steps.build.outcome == 'success' - run: pnpm --filter e2e-test-app exec playwright install --with-deps chromium + run: pnpm setup:e2e - name: Build e2e app id: build_e2e diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9bd6903..ede5763 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,10 +20,14 @@ Thank you for your interest in contributing! This document provides guidelines a ```bash pnpm install ``` -4. **Create a branch** for your work: - ```bash - git checkout -b feature/my-new-feature - ``` +4. **Setup e2e testing**: +```bash + pnpm setup:e2e +``` +5. **Create a branch** for your work: +```bash + git checkout -b feature/my-new-feature +``` ## Development Workflow @@ -55,7 +59,7 @@ pnpm test pnpm test:e2e # Or run everything at once -pnpm lint && pnpm typecheck && pnpm test +pnpm lint && pnpm typecheck && pnpm test && pnpm test:e2e ``` ### Commit Messages diff --git a/package.json b/package.json index 7fa90a4..8a96676 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,8 @@ "docker:up": "docker compose up -d", "docker:down": "docker compose down", "docker:logs": "docker compose logs -f redis", - "clean": "turbo clean && rm -rf node_modules" + "clean": "turbo clean && rm -rf node_modules", + "setup:e2e": "pnpm --filter e2e-test-app exec playwright install --with-deps chromium" }, "devDependencies": { "@biomejs/biome": "^1.9.4",