Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions packages/cache-handler/src/handlers/redis.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@ class FakeRedis {
listenerCount(event: string): number {
return this.listeners.get(event)?.length ?? 0;
}

getStored(key: string): { value: string; ttl?: number } | undefined {
return this.store.get(key);
}
}

// Mock ioredis to return our FakeRedis
Expand Down Expand Up @@ -416,6 +420,37 @@ describe("RedisCacheHandler", () => {
});
});

describe("TTL on set", () => {
afterEach(() => {
vi.useRealTimers();
});

test("should store entry with setex when defaultTTL is configured", async () => {
Comment on lines +423 to +428

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using vi.useFakeTimers() without restoring them can leak fake timers to subsequent tests in this file or other test files, leading to unexpected behavior or flakiness. Please ensure that real timers are restored after the tests in this block run.

  describe("TTL on set", () => {
    afterEach(() => {
      vi.useRealTimers();
    });

    test("should store entry with setex when defaultTTL is configured", async () => {

vi.useFakeTimers();
vi.setSystemTime(new Date("2024-01-02T03:04:05.000Z"));

const handler = new RedisCacheHandler({ defaultTTL: 3600 });

const value: CacheValue = {
kind: "FETCH",
data: {
headers: { "content-type": "application/json" },
body: '{"ttl": true}',
status: 200,
url: "https://example.com",
},
revalidate: 3600,
};

await handler.set("ttl-key", value);

const stored = fakeRedis.getStored("nextjs:cache:ttl-key");
expect(stored).toBeDefined();
expect(stored?.ttl).toBeGreaterThan(0);
expect(stored?.ttl).toBeLessThanOrEqual(3600);
});
});

describe("existing Redis client support", () => {
// Use a separate FakeRedis instance for external-client tests
// so we can distinguish it from the mock constructor's fakeRedis
Expand Down
7 changes: 3 additions & 4 deletions packages/cache-handler/src/handlers/redis.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Redis, { type RedisOptions } from "ioredis";
import { calculateLifespan, isExpired } from "../helpers/lifespan.js";
import { calculateLifespan, isExpired, redisTtlSecondsFromLifespan } from "../helpers/lifespan.js";
import { jsonReplacer, jsonReviver } from "../helpers/serialization.js";
import type {
CacheHandler,
Expand Down Expand Up @@ -254,9 +254,8 @@ export class RedisCacheHandler implements CacheHandler {
// Determine TTL for Redis
let ttl: number | undefined;
if (lifespan?.expireAt) {
// Use expire time as TTL
ttl = Math.ceil((lifespan.expireAt - Date.now()) / 1000);
if (ttl <= 0) {
ttl = redisTtlSecondsFromLifespan(lifespan);
if (!ttl || ttl <= 0) {
this.log("SET", cacheKey, "SKIP (already expired)");
return;
}
Expand Down
18 changes: 17 additions & 1 deletion packages/cache-handler/src/helpers/lifespan.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { afterEach, describe, expect, test, vi } from "vitest";
import { calculateLifespan, isExpired } from "./lifespan.js";
import { calculateLifespan, isExpired, redisTtlSecondsFromLifespan } from "./lifespan.js";

const MOCK_TIME = new Date("2024-01-02T03:04:05.000Z");

Expand Down Expand Up @@ -71,4 +71,20 @@ describe("lifespan helpers", () => {
vi.setSystemTime(new Date(MOCK_TIME.getTime() + 2_000));
expect(isExpired(lifespan)).toBe(true);
});

test("redisTtlSecondsFromLifespan computes positive Redis TTL from lifespan", () => {
const lifespan = calculateLifespan(3600, 3600);
const ttl = redisTtlSecondsFromLifespan(lifespan);

expect(ttl).toBeGreaterThan(0);
expect(ttl).toBeLessThanOrEqual(3600);
});

test("redisTtlSecondsFromLifespan documents the broken formula always skips writes", () => {
const lifespan = calculateLifespan(3600, 3600);
expect(lifespan).not.toBeNull();

const broken = Math.ceil((lifespan.expireAt - Date.now()) / 1000);
expect(broken).toBeLessThanOrEqual(0);
});
});
11 changes: 11 additions & 0 deletions packages/cache-handler/src/helpers/lifespan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,14 @@ export function isExpired(lifespan: LifespanParameters | null): boolean {
const nowSeconds = Math.floor(Date.now() / 1000);
return nowSeconds >= lifespan.expireAt;
}

/**
* Compute remaining Redis EX TTL in seconds from lifespan parameters.
* expireAt is epoch seconds (see calculateLifespan).
*/
export function redisTtlSecondsFromLifespan(
lifespan: LifespanParameters | null,
): number | undefined {
if (!lifespan?.expireAt) return undefined;
return lifespan.expireAt - Math.floor(Date.now() / 1000);
}