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 thread
tobiasvdorp marked this conversation as resolved.
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);
}