85 lines
2.5 KiB
TypeScript
85 lines
2.5 KiB
TypeScript
import type { ApiClient } from "./client.js";
|
|
import type { RequestScopedCache, ClientMemoryCache, ServerLruCache } from "./cache.js";
|
|
|
|
export interface CachedClientOptions {
|
|
client: ApiClient;
|
|
requestScoped?: RequestScopedCache | undefined;
|
|
clientMemory?: ClientMemoryCache<NonNullable<unknown>> | undefined;
|
|
serverLru?: ServerLruCache<NonNullable<unknown>> | undefined;
|
|
ttlMs?: number | undefined;
|
|
}
|
|
|
|
export class CachedApiClient {
|
|
private readonly client: ApiClient;
|
|
private readonly requestScoped: RequestScopedCache | undefined;
|
|
private readonly clientMemory: ClientMemoryCache<NonNullable<unknown>> | undefined;
|
|
private readonly serverLru: ServerLruCache<NonNullable<unknown>> | 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<T>(
|
|
path: string,
|
|
query?: Record<string, string | number | boolean>,
|
|
): Promise<T> {
|
|
const key = this.buildKey(path, query);
|
|
|
|
// (1) Check request-scoped cache (SSR dedup)
|
|
if (this.requestScoped) {
|
|
const cached = this.requestScoped.get<T>(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<T>(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<unknown>, this.ttlMs);
|
|
}
|
|
if (this.serverLru) {
|
|
this.serverLru.set(key, result as NonNullable<unknown>, this.ttlMs);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private buildKey(
|
|
path: string,
|
|
query?: Record<string, string | number | boolean>,
|
|
): string {
|
|
const q = query
|
|
? Object.keys(query)
|
|
.sort()
|
|
.map((k) => `${k}=${String(query[k])}`)
|
|
.join("&")
|
|
: "";
|
|
return `${path}?${q}`;
|
|
}
|
|
}
|