Files
2026-05-27 23:47:53 +01:00

143 lines
4.0 KiB
TypeScript

import type {
RedisLike,
RedisScanResult,
} from "../../src/redis-like/redis-like.js";
interface Entry {
value: string;
expiresAt: number;
}
export interface FakeRedisCall {
command: string;
args: ReadonlyArray<unknown>;
}
/**
* 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<string, Entry>();
#now: () => number;
public constructor(now: () => number = () => Date.now()) {
this.#now = now;
}
public setClock(now: () => number): void {
this.#now = now;
}
#record(command: string, args: ReadonlyArray<unknown>): 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<string | null> {
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<number> {
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<number> {
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<number> {
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<number> {
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<RedisScanResult> {
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<never> => {
throw error;
};
return {
set: fail,
get: fail,
del: fail,
exists: fail,
ttl: fail,
expire: fail,
scan: fail,
};
}