Files
flights_web/src/shared/api/cached-client.ts
T

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}`;
}
}