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