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>
This commit is contained in:
62
src/lib/istek-cache.ts
Normal file
62
src/lib/istek-cache.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
// İ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);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user