import type { CacheSetOptions, CacheStats } from "@suaveplan/cache"; import { AbstractCache } from "@suaveplan/cache"; /** * Real in-memory `AbstractCache` subclass used by the unit * tests. NOT a mock — it implements every abstract method against a * backing `Map`. The `failNext(op, err)` helper injects a deterministic * rejection for the next call to that op so the wrapper's * cache-failure-swallow paths are exercised against real throwing * behaviour. * * The class deliberately omits `keys()` by default; the * `KeysCapableFakeCache` subclass adds it. This lets the wrapper's * `clear()` feature-detection be tested against both shapes without a * mock. */ export class FakeCache extends AbstractCache { #data = new Map(); public getCalls: string[] = []; public setCalls: { key: string; value: Uint8Array }[] = []; public deleteCalls: string[] = []; public clearCalls = 0; public hasCalls: string[] = []; #failNext: Partial< Record<"get" | "set" | "delete" | "clear" | "has", Error> > = {}; public failNext( op: "get" | "set" | "delete" | "clear" | "has", err: Error ): void { this.#failNext[op] = err; } override async get(key: string): Promise { this.getCalls.push(key); const fail = this.#failNext.get; if (fail !== undefined) { this.#failNext.get = undefined; throw fail; } return this.#data.get(key); } override async set( key: string, value: Uint8Array, _options?: CacheSetOptions ): Promise { this.setCalls.push({ key, value }); const fail = this.#failNext.set; if (fail !== undefined) { this.#failNext.set = undefined; throw fail; } this.#data.set(key, value); } override async delete(key: string): Promise { this.deleteCalls.push(key); const fail = this.#failNext.delete; if (fail !== undefined) { this.#failNext.delete = undefined; throw fail; } return this.#data.delete(key); } override async clear(): Promise { this.clearCalls += 1; const fail = this.#failNext.clear; if (fail !== undefined) { this.#failNext.clear = undefined; throw fail; } this.#data.clear(); } override async has(key: string): Promise { this.hasCalls.push(key); const fail = this.#failNext.has; if (fail !== undefined) { this.#failNext.has = undefined; throw fail; } return this.#data.has(key); } override getStats(): CacheStats { return { hits: 0, misses: 0, size: this.#data.size, hitRate: 0, evictions: 0, }; } /** Test-only direct access. */ public _peek(key: string): Uint8Array | undefined { return this.#data.get(key); } /** Test-only direct seed. */ public _seed(key: string, value: Uint8Array): void { this.#data.set(key, value); } public get _data(): ReadonlyMap { return this.#data; } } /** * `FakeCache` extension that exposes a synchronous `keys()` iterator * — the exact shape `CachedStorage.clear()` feature-detects. */ export class KeysCapableFakeCache extends FakeCache { public keysCalls = 0; public *keys(): IterableIterator { this.keysCalls += 1; for (const k of this._data.keys()) yield k; } } /** * `FakeCache` extension exposing an async `keys()` iterator — * representative of remote cache backends that stream keys. */ export class AsyncKeysCapableFakeCache extends FakeCache { public keysCalls = 0; public async *keys(): AsyncIterableIterator { this.keysCalls += 1; for (const k of this._data.keys()) { yield k; } } }