143 lines
4.0 KiB
TypeScript
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,
|
|
};
|
|
}
|