Files
suaveplan/packages/persistence/storage/storage-plugin-cache/tests/setup/fake-cache.ts
2026-05-27 23:47:53 +01:00

142 lines
4.0 KiB
TypeScript

import type { CacheSetOptions, CacheStats } from "@suaveplan/cache";
import { AbstractCache } from "@suaveplan/cache";
/**
* Real in-memory `AbstractCache<Uint8Array>` 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<Uint8Array> {
#data = new Map<string, Uint8Array>();
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<Uint8Array | undefined> {
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<void> {
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<boolean> {
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<void> {
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<boolean> {
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<string, Uint8Array> {
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<string> {
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<string> {
this.keysCalls += 1;
for (const k of this._data.keys()) {
yield k;
}
}
}