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", 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.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..099a8a8 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,8 @@ 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 compatible) + * Uses node-redis-style SET with options object: set(key, value, { EX: seconds }) */ export interface RedisClient { get(key: string): Promise; @@ -275,7 +275,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) {