Files
kolaytercih/src/lib/istek-cache.ts
bilalgursen 668568b5b5 perf: istemci istek cache'i (dedup+TTL) ve profil cache (Faz 5)
- lib/istek-cache: modül seviyesi in-flight dedup + 5dk TTL cache (SWR
  bağımlılığı olmadan); caller iptali paylaşılan fetch'i düşürmez
- /api/facetler'in 5 çağrı yeri (hero, cta, kapı, sihirbaz adımları, demo)
  ve /api/katalog-ara cache'e bağlandı — sihirbazda 2↔3 adım geçişi ve
  tekrarlanan aramalar artık ağa çıkmıyor; /api/soru GET dedup-only (ttl 0)
- katalog-arama: elle yukleniyor state'i kalktı, "cevaplanmamış sorgu"
  türetmesi + startTransition; rehber puanlaması Schwartzian; ⌘K listener'ı
  useEffectEvent ile tek sefer bağlanıyor
- sihirbaz-adimlar: il sort/filter useMemo (çip başına 81 eleman kopyalanıp
  sıralanıyordu, harita dizisi referansı sabitlendi)
- sohbet-client: stream chunk güncellemeleri startTransition'da — cevap
  akarken input yazımı bloklanmıyor
- sihirbaz-profil: modül cache + event/storage geçersizlemesi (tek "+"
  tıklamasında 3 localStorage okuma + JSON.parse vardı)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 14:11:05 +03:00

63 lines
2.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// İ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<string, Kayit>();
const ucusta = new Map<string, Promise<unknown>>();
const MAX_KAYIT = 50;
const VARSAYILAN_TTL_MS = 5 * 60_000;
export async function onbellekliJsonGetir<T>(
url: string,
{
ttlMs = VARSAYILAN_TTL_MS,
signal,
}: { ttlMs?: number; signal?: AbortSignal } = {},
): Promise<T> {
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<T>;
return new Promise<T>((cozul, reddet) => {
const iptalEt = () => reddet(new DOMException("Aborted", "AbortError"));
if (signal.aborted) return iptalEt();
signal.addEventListener("abort", iptalEt, { once: true });
(soz as Promise<T>).then(cozul, reddet).finally(() => {
signal.removeEventListener("abort", iptalEt);
});
});
}