import type { RedisLike, RedisScanResult, } from "../../src/redis-like/redis-like.js"; interface Entry { value: string; expiresAt: number; } export interface FakeRedisCall { command: string; args: ReadonlyArray; } /** * Minimal in-memory `RedisLike` implementation used by the cache-redis test * suite. Tracks every command in `calls` so assertions can verify the exact * wire `RedisCache` issued. */ export class FakeRedis implements RedisLike { public readonly calls: FakeRedisCall[] = []; readonly #store = new Map(); #now: () => number; public constructor(now: () => number = () => Date.now()) { this.#now = now; } public setClock(now: () => number): void { this.#now = now; } #record(command: string, args: ReadonlyArray): void { this.calls.push({ command, args }); } public async set( key: string, value: string, options?: { EX?: number; PX?: number } ): Promise<"OK"> { this.#record( "SET", options === undefined ? [key, value] : [key, value, options] ); let expiresAt = Number.POSITIVE_INFINITY; if (options?.EX !== undefined) { expiresAt = this.#now() + options.EX * 1000; } else if (options?.PX !== undefined) { expiresAt = this.#now() + options.PX; } this.#store.set(key, { value, expiresAt }); return "OK"; } public async get(key: string): Promise { this.#record("GET", [key]); const entry = this.#store.get(key); if (entry === undefined) return null; if (entry.expiresAt <= this.#now()) { this.#store.delete(key); return null; } return entry.value; } public async del(...keys: string[]): Promise { this.#record("DEL", keys); let n = 0; for (const k of keys) { if (this.#store.delete(k)) n += 1; } return n; } public async exists(...keys: string[]): Promise { this.#record("EXISTS", keys); let n = 0; for (const k of keys) { const e = this.#store.get(k); if (e === undefined) continue; if (e.expiresAt > this.#now()) n += 1; } return n; } public async ttl(key: string): Promise { this.#record("TTL", [key]); const entry = this.#store.get(key); if (entry === undefined) return -2; if (entry.expiresAt === Number.POSITIVE_INFINITY) return -1; const remaining = Math.ceil((entry.expiresAt - this.#now()) / 1000); return remaining > 0 ? remaining : -2; } public async expire(key: string, seconds: number): Promise { this.#record("EXPIRE", [key, seconds]); const entry = this.#store.get(key); if (entry === undefined) return 0; entry.expiresAt = this.#now() + seconds * 1000; return 1; } public async scan( cursor: string, options?: { MATCH?: string; COUNT?: number } ): Promise { this.#record("SCAN", [cursor, options]); const match = options?.MATCH; const re = match === undefined ? null : new RegExp( `^${match .replace(/[.+^${}()|[\]\\]/g, "\\$&") .replace(/\*/g, ".*")}$` ); const keys: string[] = []; for (const k of this.#store.keys()) { if (re === null || re.test(k)) keys.push(k); } return { cursor: "0", keys }; } } /** RedisLike that fails every command with the given error. */ export function makeFailingRedis(error: Error): RedisLike { const fail = async (): Promise => { throw error; }; return { set: fail, get: fail, del: fail, exists: fail, ttl: fail, expire: fail, scan: fail, }; }