// İstemci tarafı GET istekleri için modül seviyesi in-flight dedup + TTL // cache. SWR bağımlılığı yerine minimal el yapımı katman: /api/facetler beş // farklı bileşenden aynı parametrelerle çağrılıyor, katalog araması aynı // sorguyu tekrar tekrar atıyordu. // // - Aynı URL uçuştayken ikinci çağrı aynı promise'i bekler (dedup). // - Başarılı yanıt ttlMs boyunca bellekte kalır; ttlMs: 0 = yalnız dedup // (kullanıcıya özel, mutasyon sonrası tazelenmesi gereken uçlar için). // - Paylaşılan fetch'e caller signal'ı GEÇİLMEZ: bir abonenin iptali // diğerlerinin isteğini düşürmesin. İptal yalnızca o abonenin bekleyişini // AbortError ile sonlandırır (katalog aramasının debounce/abort akışı // davranış değiştirmeden buna bağlanır). type Kayit = { zaman: number; veri: unknown }; const bellek = new Map(); const ucusta = new Map>(); const MAX_KAYIT = 50; const VARSAYILAN_TTL_MS = 5 * 60_000; export async function onbellekliJsonGetir( url: string, { ttlMs = VARSAYILAN_TTL_MS, signal, }: { ttlMs?: number; signal?: AbortSignal } = {}, ): Promise { const kayit = bellek.get(url); if (kayit && ttlMs > 0 && Date.now() - kayit.zaman < ttlMs) { return kayit.veri as T; } let soz = ucusta.get(url); if (!soz) { soz = fetch(url) .then(async (res) => { if (!res.ok) throw new Error(`istek-cache: ${res.status} (${url})`); const veri: unknown = await res.json(); if (ttlMs > 0) { bellek.set(url, { zaman: Date.now(), veri }); // Basit LRU kırpması: en eski kayıt düşer (insertion order) if (bellek.size > MAX_KAYIT) { const enEski = bellek.keys().next().value; if (enEski !== undefined) bellek.delete(enEski); } } return veri; }) .finally(() => ucusta.delete(url)); ucusta.set(url, soz); } if (!signal) return soz as Promise; return new Promise((cozul, reddet) => { const iptalEt = () => reddet(new DOMException("Aborted", "AbortError")); if (signal.aborted) return iptalEt(); signal.addEventListener("abort", iptalEt, { once: true }); (soz as Promise).then(cozul, reddet).finally(() => { signal.removeEventListener("abort", iptalEt); }); }); }