import { Logger } from '@dunx/core'; import { CacheOptions, MeteredCacheStore, TieredCacheStore, } from '@dunx/infra/cache'; import { CacheL2 } from './cache-l2.js'; /** * One read, a second that never reaches the loader, ten at once that share a * single load, and an eviction that makes the next one load again. */ export class CatalogDemo { constructor( private readonly logger: Logger, private readonly options: CacheOptions, private readonly l2: CacheL2, ) {} async demonstrate(url: string): Promise { // `metrics: true` puts a MeteredCacheStore in front of what was configured, // so the tier check reads through it. An `instanceof` straight at // `options.store` answers no here and narrates the wrong store. const { store } = this.options; const metered = store instanceof MeteredCacheStore; const configured = metered ? store.inner : store; // Always two tiers now, so reachability is the probe below, not the shape. const tiers = configured instanceof TieredCacheStore ? 'L1 memory in front of a degrading L2 redis' : String(configured.constructor.name); this.logger.info( `store -> ${tiers}${metered ? ', metered' : ''}, ` + `default ttl ${this.options.ttl}ms`, ); // Asked rather than remembered: `degraded` is only what the last call set. this.logger.info( `L2 probe -> ${(await this.l2.store.probe()) ? 'answering' : 'unreachable, reads degrade to misses'}`, ); const before = await this.loads(url); const first = await this.quote(url, 'dunx'); const second = await this.quote(url, 'dunx'); this.logger.info( `GET /api/catalog/dunx twice -> ${first}, ${second} ` + `(loads ${before} -> ${await this.loads(url)})`, ); // Unique per run, because "uncached" has to be true for the count below to // mean anything: L2 is Redis, so a fixed symbol is still cached from the // previous tour against the same server and ten reads then load zero times. // `max(12)` on the route's param, so the suffix is short. const fresh = `bun-${Bun.randomUUIDv7().slice(-6)}`; const at = await this.loads(url); await Promise.all(Array.from({ length: 10 }, () => this.quote(url, fresh))); this.logger.info( `10 concurrent reads of an uncached key -> ${(await this.loads(url)) - at} ` + 'load (single flight, per process)', ); const evicted = await fetch(new URL('api/catalog/dunx', url), { method: 'DELETE', }); const after = await this.loads(url); await this.quote(url, 'dunx'); this.logger.info( `DELETE -> ${JSON.stringify(await evicted.json())}, the next read ` + `loads again (loads ${after} -> ${await this.loads(url)})`, ); } private async quote(url: string, symbol: string): Promise { const response = await fetch(new URL(`api/catalog/${symbol}`, url)); return ((await response.json()) as { price: number }).price; } private async loads(url: string): Promise { const response = await fetch(new URL('api/catalog', url)); return ((await response.json()) as { loads: number }).loads; } }