import type { ApiClient } from "./client.js"; import type { RequestScopedCache, ClientMemoryCache, ServerLruCache } from "./cache.js"; export interface CachedClientOptions { client: ApiClient; requestScoped?: RequestScopedCache | undefined; clientMemory?: ClientMemoryCache> | undefined; serverLru?: ServerLruCache> | undefined; ttlMs?: number | undefined; } export class CachedApiClient { private readonly client: ApiClient; private readonly requestScoped: RequestScopedCache | undefined; private readonly clientMemory: ClientMemoryCache> | undefined; private readonly serverLru: ServerLruCache> | undefined; private readonly ttlMs: number | undefined; constructor(options: CachedClientOptions) { this.client = options.client; this.requestScoped = options.requestScoped; this.clientMemory = options.clientMemory; this.serverLru = options.serverLru; this.ttlMs = options.ttlMs; } async get( path: string, query?: Record, ): Promise { const key = this.buildKey(path, query); // (1) Check request-scoped cache (SSR dedup) if (this.requestScoped) { const cached = this.requestScoped.get(key); if (cached) return cached; } // (2) Check client-memory cache if (this.clientMemory) { const cached = this.clientMemory.get(key) as T | undefined; if (cached !== undefined) return cached; } // (3) Check server LRU cache if (this.serverLru) { const cached = this.serverLru.get(key) as T | undefined; if (cached !== undefined) return cached; } // Cache miss — fetch from upstream const promise = this.client.get(path, query); // Store the in-flight promise in request-scoped cache for dedup if (this.requestScoped) { this.requestScoped.set(key, promise); } const result = await promise; // Populate longer-lived caches with the resolved value if (this.clientMemory) { this.clientMemory.set(key, result as NonNullable, this.ttlMs); } if (this.serverLru) { this.serverLru.set(key, result as NonNullable, this.ttlMs); } return result; } private buildKey( path: string, query?: Record, ): string { const q = query ? Object.keys(query) .sort() .map((k) => `${k}=${String(query[k])}`) .join("&") : ""; return `${path}?${q}`; } }