---
title: 'Uygulama Mimarisi: Yönlendirme, Durum ve Veri'
source: 'https://academia.sh/tr/kurslar/frontend-mimarisi'
language: tr
updated: '2026-08-17T04:40:56+00:00'
license: 'CC BY-SA 4.0'
lessons: 22
outcomes:
    - 'İstemci tarafı yönlendirmeyi iç içe düzenlerle kurar'
    - 'Sunucu durumu ile istemci durumunu ayırarak yönetir'
    - 'Form doğrulama ve kimlik doğrulama akışlarını güvenli tasarlar'
---

# Uygulama Mimarisi: Yönlendirme, Durum ve Veri

Bileşenlerin üstündeki katman: hangi ekranın görüneceğini belirleyen yönlendirme, durumun türüne göre yerleştirilmesi, sunucu verisinin alınması ve form ile kimlik akışlarının tasarımı.

## Yönlendirme

Adresin görünüme çevrilmesi: rota eşleyicinin kuruluşu, iç içe rota ağacı ve düzen zinciri, parametre ile sorgu durumu, yetkiye bağlı erişim ve rota bazlı paket bölme.

### İstemci Tarafı Yönlendirme

Source: https://academia.sh/tr/kurslar/frontend-mimarisi/istemci-tarafi-yonlendirme

Adresi görünüme çeviren katman; rota tablosu, yol kalıplarının çözümlenmesi, özgüllüğe göre sıralama, parametre çıkarımı, bağlantı kesme ve gezinme döngüsünün sorumlulukları.

Önceki kurs bileşeni ve bileşimi kurdu: girdisi olan, kendi yerel durumunu tutan, başka
bileşenlerin içine yerleşen bir birim. Bileşen ağacı bir kez kurulduğunda hangi verinin
nereye aktığı bellidir. Belirsiz kalan soru bir üst katmandadır: **ağacın kökünde hangi
bileşen duracak?** Kuzey Yamaç Ölçüm İstasyonu uygulaması artık tek ekran değil. İstasyon
listesi, istasyon ayrıntısı, ölçüm geçmişi ve oturum açma ekranı var; bunlardan hangisinin
görüneceğine bir yerde karar verilmelidir.

Bu kararın kaynağı adrestir. Tarayıcı ve Web Platformu kursundaki Geçmiş ve Konum dersinde
adresin parçalara ayrılabildiği, geçmiş yığıtının program tarafından değiştirilebildiği ve
gezinmenin sayfa yeniden yüklenmeden yapılabildiği kurulmuştu. Yönlendirme, o düzeneğin
üzerine oturan karar katmanıdır: adresi alır, hangi görünümün kurulacağını söyler.

#### Yönlendirmenin Tanımı

Yönlendirme (routing), bir adresi bir görünüme eşleyen işlemdir. Girdisi adresin yol
bölümü, çıktısı iki parçadır: görüntülenecek bileşen ve o bileşene verilecek parametreler.

Sunucu tarafında bu iş zaten yapılır; her istek bir yola bakılarak yanıtlanır. İstemci
tarafı yönlendirmenin farkı, kararın **belge yeniden yüklenmeden** verilmesidir. Kullanıcı
istasyon listesindeki bir satıra tıkladığında yeni bir belge istenmez; adres değişir,
eşleyici yeni yolu çözümler, ağacın bir bölümü yeni bileşenle değiştirilir.

Bunun kazancı gezinme maliyetinin düşmesi değildir yalnızca. Belge yerinde kaldığı için
bellekteki durum korunur: yarım kalan bir form, açık bir panel, indirilmiş bir veri kümesi
gezinmeden sonra da durur. Bedeli, tarayıcının kendiliğinden yaptığı işlerin sayfanın
sorumluluğuna geçmesidir — kaydırma konumu, odak yönetimi, isteklerin iptali, sekme
başlığı. Bunların hepsi bu konu boyunca tek tek ele alınır.

#### Rota Tablosu

Yönlendirmenin verisi bir **rota tablosudur**: yol kalıbı ile görünüm eşlemelerinin
listesi. Yol kalıbı sabit dilimlerden, parametre dilimlerinden ve jokerden oluşur.

`/istasyon` sabit bir dilimdir; yalnızca kendisiyle eşleşir. `/:kimlik` bir
parametre dilimidir; herhangi bir dilimle eşleşir ve eşleştiği değeri bir ada bağlar.
`/*` jokerdir; kalan dilimlerin tümünü tüketir ve genellikle "bulunamadı" görünümüne
bağlanır.

Tablonun tek başına yeterli olmadığı bir nokta vardır. `/istasyon/yeni` yolu iki kalıba
birden uyar: hem `/istasyon/yeni` sabitine, hem `/istasyon/:kimlik` parametresine. Hangisi
kazanır? Yanıt yazım sırası olamaz — o zaman tablodaki satırların yeri anlam taşır ve iki
geliştirici sıralamayı bozduğunda hata sessizce ortaya çıkar. Yanıt **özgüllüktür**: daha
az yolu kabul eden kalıp önce denenir.

```js
// rota-eslestirici.mjs — yol kalibinin cozumlenmesi, ozgullukle siralama, parametre cikarimi
const AGIRLIK = { sabit: 3, parametre: 2, joker: 1 };

function kalibiCozumle(kalip) {
  const parcalar = kalip.split("/").filter(Boolean).map((p) =>
    p === "*" ? { tur: "joker" }
      : p.startsWith(":") ? { tur: "parametre", ad: p.slice(1) }
        : { tur: "sabit", deger: p });
  return {
    kalip, parcalar,
    skor: parcalar.map((p) => AGIRLIK[p.tur]),
    joker: parcalar.some((p) => p.tur === "joker") ? 1 : 0,
  };
}

function ozgullukKarsilastir(a, b) {
  if (a.joker !== b.joker) return a.joker - b.joker;      // jokerli kalip en sona
  const ortak = Math.min(a.skor.length, b.skor.length);
  for (let i = 0; i < ortak; i++)                         // ilk farkli dilim karari verir
    if (a.skor[i] !== b.skor[i]) return b.skor[i] - a.skor[i];
  return b.skor.length - a.skor.length;                   // uzun kalip once
}

function eslestir(rota, yol) {
  const dilim = yol.split("/").filter(Boolean);
  const parametre = {};
  for (let i = 0; i < rota.parcalar.length; i++) {
    const p = rota.parcalar[i];
    if (p.tur === "joker") { parametre.kalan = dilim.slice(i).join("/"); return parametre; }
    if (i >= dilim.length) return null;
    if (p.tur === "sabit") { if (p.deger !== dilim[i]) return null; }
    else parametre[p.ad] = decodeURIComponent(dilim[i]);
  }
  return dilim.length === rota.parcalar.length ? parametre : null;
}

// Yazim sirasi bilerek bozuk: ":kimlik" daha genel oldugu halde "yeni"den once yazildi.
const yazimSirasi = [
  "/istasyon/:kimlik",
  "/istasyon/yeni",
  "/*",
  "/istasyon/:kimlik/olcum/:zaman",
  "/",
  "/istasyon",
  "/istasyon/:kimlik/olcum",
];
const tablo = yazimSirasi.map(kalibiCozumle).sort(ozgullukKarsilastir);

console.log("-- ozgulluge gore siralanmis tablo --");
for (const r of tablo) console.log((r.skor.join(",") || "-").padEnd(9), r.kalip);

console.log("-- eslesme --");
for (const yol of ["/", "/istasyon", "/istasyon/yeni", "/istasyon/kuzey-yamac",
  "/istasyon/kuzey-yamac/olcum", "/istasyon/kuzey-yamac/olcum/2024-02-11T06:00",
  "/istasyon/kuzey%20yamac", "/ayarlar/gizlilik"]) {
  let bulunan = null, parametre = null;
  for (const r of tablo) {
    const p = eslestir(r, yol);
    if (p) { bulunan = r.kalip; parametre = p; break; }
  }
  console.log(yol.padEnd(44), bulunan.padEnd(30), JSON.stringify(parametre));
}
```

```
-- ozgulluge gore siralanmis tablo --
3,3       /istasyon/yeni
3,2,3,2   /istasyon/:kimlik/olcum/:zaman
3,2,3     /istasyon/:kimlik/olcum
3,2       /istasyon/:kimlik
3         /istasyon
-         /
1         /*
-- eslesme --
/                                            /                              {}
/istasyon                                    /istasyon                      {}
/istasyon/yeni                               /istasyon/yeni                 {}
/istasyon/kuzey-yamac                        /istasyon/:kimlik              {"kimlik":"kuzey-yamac"}
/istasyon/kuzey-yamac/olcum                  /istasyon/:kimlik/olcum        {"kimlik":"kuzey-yamac"}
/istasyon/kuzey-yamac/olcum/2024-02-11T06:00 /istasyon/:kimlik/olcum/:zaman {"kimlik":"kuzey-yamac","zaman":"2024-02-11T06:00"}
/istasyon/kuzey%20yamac                      /istasyon/:kimlik              {"kimlik":"kuzey yamac"}
/ayarlar/gizlilik                            /*                             {"kalan":"ayarlar/gizlilik"}
```

Sıralama, yazım sırasından bağımsız bir düzen üretmiştir. `/istasyon/yeni` en başa
çıkmıştır; `/istasyon/kuzey-yamac` yolu ona uymadığı için parametreli kalıba düşer ve
kimlik doğru çıkarılır. Joker en sona alındığı için hiçbir gerçek rotayı gölgelemez.

Eşleşme tablosunda üç ayrıntı daha okunur. Kök yolun kalıbı hiç dilim içermez ve yalnız
kendisiyle eşleşir. Parametre değeri, adresteki **kaçırma çözülerek** verilir: `kuzey%20yamac`
dilimi `kuzey yamac` olur; bunu yapmayan bir eşleyici, boşluk veya Türkçe karakter içeren
kimlikleri bozuk teslim eder. Tanımsız yol jokere düşer ve `kalan` alanında ne istendiğini
taşır; bulunamadı görünümü bu bilgiyi kullanıcıya gösterebilir.

Bir kural da örtük olarak kuruludur: parametre dilimi **bir** dilim tüketir. `/istasyon/a/b`
yolu `/istasyon/:kimlik` kalıbına uymaz, çünkü dilim sayıları tutmaz. Eğik çizgi içeren bir
değer taşınacaksa ya joker kullanılır ya da değer kaçırılarak tek dilime sığdırılır.

#### Gezinme Döngüsü

Eşleyici bir işlevdir; yönlendirme ise sürekli çalışan bir döngüdür. Döngünün dört adımı
vardır ve her adımın ayrı bir sorumluluğu bulunur.

**Adres değişir.** Bunun üç kaynağı vardır: kullanıcının bir bağlantıya tıklaması,
programın gezinme çağırması, kullanıcının geri veya ileri düğmesine basması. İlk ikisinde
adresi kod değiştirir; üçüncüsünde tarayıcı değiştirir ve koda haber verir.

**Eşleyici çalışır.** Yeni yol rota tablosunda aranır, kazanan kalıp ve parametreler
çıkarılır.

**Görünüm kurulur.** Kazanan rotanın bileşeni ağaca yerleştirilir, parametreler ona
verilir.

**Kenar sorumluluklar yerine getirilir.** Kaydırma konumu ayarlanır, odak yeni içeriğe
taşınır, sekme başlığı güncellenir, önceki görünümün istekleri iptal edilir.

Döngünün en çok atlanan yeri dördüncü adımdır. Gerçek gezinmede tarayıcı bunları
kendiliğinden yapar; istemci tarafı gezinmede belge değişmediği için hiçbiri olmaz. Odağın
taşınmaması özellikle sessiz bir kusurdur: klavye veya ekran okuyucu kullanan biri
bağlantıya bastığında sayfanın değiştiğini fark etmez, odak eski konumunda kalır.

#### Bağlantıyı Kesmek

İstemci tarafı gezinmenin başlangıcı, bağlantı tıklamasının **varsayılan eylemini iptal
etmektir**. Belge ve Olaylar konusundaki Varsayılan Davranış ve İptal dersinde bu düzenek
kurulmuştu: olay iptal edilebilir olduğunda tarayıcının kendi davranışı durdurulabilir.

Her tıklama kesilmez. Kesilmemesi gereken durumlar bellidir ve bir koşul dizisiyle
sınanır. Farklı bir kaynağa giden bağlantı kesilmez; uygulama o adresi zaten
görüntüleyemez. İndirme başlatan bağlantı, yeni sekmede açılması istenen bağlantı ve
kullanıcının değiştirici tuşla açtığı bağlantı da kesilmez. Sağ tıkla açılan menü
üzerinden yapılan işlemler ise olayı hiç üretmez.

Bu koşullar unutulduğunda ortaya çıkan kusur her zaman aynıdır: kullanıcı bilinen bir
tarayıcı davranışını kaybeder. Bağlantıyı yeni sekmede açamamak, adresi kopyalayamamak
veya indirme bağlantısının çalışmaması, hepsi bu tek eksikten doğar.

İptal edilen tıklamanın ardından yapılacak iş, geçmişe yeni bir kayıt eklemek ve döngüyü
başlatmaktır. Geri düğmesi de aynı döngüyü, kayıt eklemeden çalıştırır: tarayıcı gezinme
olayını üretir, kod olayı dinler, eşleyiciyi yeni yolla çağırır.

#### Adresin Tek Doğru Kaynağı Olması

Yönlendirme kurulduğunda uygulamada iki ayrı "hangi ekrandayız" bilgisi bulunabilir: adres
ve bir yerel değişken. İkisi ayrı tutulduğunda kaçınılmaz olarak ayrışırlar. Geri
düğmesine basıldığında adres değişir ama değişken eski değerinde kalır; ya da tersi olur.

Kural şudur: **etkin görünüm adresten türetilir, bellekte ayrıca tutulmaz.** Bir ekrana
gitmek, bir değişkeni ayarlamak değil, adresi değiştirmektir. Görünüm o değişikliğin
sonucudur.

Bu kural bir sınama ölçütü verir. Uygulamanın herhangi bir ekranındayken adres çubuğundaki
adres kopyalanıp yeni bir sekmede açıldığında aynı ekran gelmelidir. Gelmiyorsa, o ekranın
bir bölümü adrese yazılmamış demektir.

Kuralın karşılığı sunucu tarafındadır ve Geçmiş ve Konum dersinde kurulmuştu: istemcinin
ürettiği her adres doğrudan istendiğinde geçerli bir yanıt almalıdır. Tanımlı yol
örüntülerini tek belgeye eşleyen bir sunucu bunu sağlar; gerçekten bulunmayan adreslerin
yine bulunamadı yanıtı vermesi de aynı sözleşmenin parçasıdır.

#### Özet

- Yönlendirme, adresin yol bölümünü bir görünüme ve parametre kümesine eşleyen karar
  katmanıdır; istemci tarafında bu karar belge yeniden yüklenmeden verilir.
- Rota tablosu sabit, parametreli ve joker dilimlerden kurulur; eşleşme belirsizliği yazım
  sırasıyla değil özgüllük sıralamasıyla çözülür, joker her zaman en sona alınır.
- Parametre dilimi tek dilim tüketir ve değeri kaçırması çözülerek verilir.
- Gezinme bir döngüdür: adres değişir, eşleyici çalışır, görünüm kurulur, kaydırma–odak–
  başlık–iptal gibi kenar sorumluluklar yerine getirilir.
- Bağlantı tıklamasının varsayılan eylemi yalnızca uygulamanın gerçekten üstlenebileceği
  durumlarda iptal edilir; dış adres, indirme ve yeni sekme istekleri tarayıcıya bırakılır.
- Etkin görünüm adresten türetilir; aynı bilgiyi ayrıca bellekte tutmak geri düğmesiyle
  ayrışan iki doğru kaynak üretir.

#### Sonraki Adım

Bu dersin eşleyicisi düz bir tablo üzerinde çalıştı: her yol tek bir görünüme bağlandı.
Uygulamanın ekranları ise ortak bir kabuk paylaşıyor. İstasyon ayrıntısı ile ölçüm geçmişi
aynı başlığı, aynı yan gezinmeyi ve aynı istasyon künyesini gösteriyor; yalnızca ortadaki
bölge değişiyor. Düz tabloyla bu ortak kabuk her rotada yeniden kurulur, yeniden çizilir ve
içindeki durum her gezinmede sıfırlanır. Sonraki ders rotaları ağaç biçiminde iç içe
tanımlamayı, eşleşen dallardan bir düzen zinciri üretmeyi ve gezinmede kabuğun neden
korunduğunu ele alır.

### İç İçe Rotalar ve Düzenler

Source: https://academia.sh/tr/kurslar/frontend-mimarisi/ic-ice-rotalar-ve-duzenler

Rotaların ağaç biçiminde tanımlanması; düzen bileşenleri ve alt görünüm yuvası, dizin rotası, eşleşmeden üretilen etkin düzen zinciri, gezinmede korunan kabuk ve parametre değişiminin ayrı ele alınması.

Önceki dersin eşleyicisi düz bir tablo üzerinde çalıştı: her yol tek bir görünüme bağlandı.
Kuzey Yamaç Ölçüm İstasyonu uygulamasının ekranları ise ortak bir kabuk paylaşıyor.
İstasyon ayrıntısı ile ölçüm geçmişi aynı üst başlığı, aynı yan gezinmeyi ve aynı istasyon
künyesini gösteriyor; yalnızca ortadaki bölge değişiyor.

Düz tabloyla bu ortak kabuk her rotanın bileşeninde yeniden kurulmak zorundadır. Sonucu üç
ayrı kusurdur: aynı işaretleme birden çok yerde yinelenir, gezinmede kabuk yeniden çizilir,
kabuğun içindeki durum — açık bir menü, kaydırılmış bir liste — her gezinmede sıfırlanır.
Bu ders rotaları ağaç biçiminde tanımlayarak üçünü birden çözer.

#### Rota Ağacı

Adreslerin yolu zaten hiyerarşiktir. `/istasyon/kuzey-yamac/olcum` üç dilimden oluşur ve
her dilim bir öncekinin içini daraltır. Rota tanımını da aynı biçimde iç içe yazmak
mümkündür: her düğüm bir dilim üstlenir ve kendi altındaki düğümlere kalan dilimleri
devreder.

Ağaçtaki düğümler iki işe yarar. **Düzen bileşeni** olan düğüm, kendi altındaki her
görünümün çevresini kurar: başlık, gezinme, çerçeve. Yaprak düğüm ise ekranın değişen
bölümünü doldurur. Düzen bileşeni, alt görünümün nereye yerleşeceğini bir **alt görünüm
yuvası** ile bildirir. Bu, Web Bileşenleri konusundaki Şablonlar ve Yuvalar dersinde
tanımlanan yuva kavramının yönlendirmedeki karşılığıdır: içerik dışarıdan gelir, kap onu
nereye koyacağını söyler.

Bir düğümün kendi yolu **göreli** yazılır. `:kimlik` düğümü `istasyon` düğümünün altında
durduğu için tam yolu `/istasyon/:kimlik` olur; tanımda bu ön ek yinelenmez. Böylece bir
alt ağaç tümüyle başka bir yere taşındığında içindeki tanımlar değişmez.

Üçüncü bir düğüm türü gerekir. `/istasyon` yolu istendiğinde `IstasyonKabugu` düzeni
eşleşir, ama yuvaya konacak bir alt görünüm yoktur. Bu boşluğu **dizin rotası** doldurur:
yol tüketildiğinde eşleşen, kendi dilimi olmayan çocuk. Dizin rotası düzenin varsayılan
içeriğidir.

#### Eşleşmeden Zincire

Ağaç üzerinde eşleşme, kökten yaprağa doğru dilim tüketerek ilerler. Sonuç tek bir bileşen
değil, kökten yaprağa uzanan bir **düzen zinciridir**. Ekranda bu zincir içten dışa
yerleşir: her bileşen bir sonrakini kendi yuvasında barındırır.

```js
// rota-agaci.mjs — ic ice rota agacindan etkin duzen zincirinin uretilmesi
const agac = {
  yol: "", bilesen: "Kabuk", cocuklar: [
    { dizin: true, bilesen: "Karsilama" },
    { yol: "oturum", bilesen: "OturumAc" },
    {
      yol: "istasyon", bilesen: "IstasyonKabugu", cocuklar: [
        { dizin: true, bilesen: "IstasyonListesi" },
        { yol: "yeni", bilesen: "IstasyonFormu" },
        {
          yol: ":kimlik", bilesen: "IstasyonKunyesi", cocuklar: [
            { dizin: true, bilesen: "IstasyonOzeti" },
            { yol: "olcum", bilesen: "OlcumGecmisi" },
            { yol: "ayar", bilesen: "IstasyonAyarlari" },
          ],
        },
      ],
    },
    { yol: "*", bilesen: "Bulunamadi" },
  ],
};

const SIRA = (d) => d.dizin ? 0 : d.yol === "*" ? 3 : d.yol.startsWith(":") ? 2 : 1;

function coz(dugum, kalan, zincir, parametre) {
  if (dugum.dizin) return kalan.length === 0
    ? { zincir: [...zincir, dugum.bilesen], parametre } : null;

  let tuketilen = 0, p = parametre;
  if (dugum.yol === "*") { p = { ...p, kalan: kalan.join("/") }; tuketilen = kalan.length; }
  else if (dugum.yol !== "") {
    if (kalan.length === 0) return null;
    if (dugum.yol.startsWith(":")) p = { ...p, [dugum.yol.slice(1)]: decodeURIComponent(kalan[0]) };
    else if (dugum.yol !== kalan[0]) return null;
    tuketilen = 1;
  }

  const yeniZincir = [...zincir, dugum.bilesen];
  const yeniKalan = kalan.slice(tuketilen);
  for (const cocuk of [...(dugum.cocuklar ?? [])].sort((a, b) => SIRA(a) - SIRA(b))) {
    const sonuc = coz(cocuk, yeniKalan, yeniZincir, p);
    if (sonuc) return sonuc;
  }
  return (dugum.cocuklar ?? []).length === 0 && yeniKalan.length === 0
    ? { zincir: yeniZincir, parametre: p } : null;
}

const eslestir = (yol) => coz(agac, yol.split("/").filter(Boolean), [], {});

const yollar = ["/", "/oturum", "/istasyon", "/istasyon/yeni", "/istasyon/kuzey-yamac",
  "/istasyon/kuzey-yamac/olcum", "/istasyon/kuzey-yamac/ayar", "/rapor/2024"];

console.log("-- etkin duzen zinciri --");
for (const yol of yollar) {
  const { zincir, parametre } = eslestir(yol);
  console.log(yol.padEnd(30), zincir.join(" > ").padEnd(52), JSON.stringify(parametre));
}

console.log("-- gezinmede korunan ve degisen --");
const gecisler = [
  ["/istasyon", "/istasyon/kuzey-yamac"],
  ["/istasyon/kuzey-yamac", "/istasyon/kuzey-yamac/olcum"],
  ["/istasyon/kuzey-yamac/olcum", "/istasyon/kuzey-yamac/ayar"],
  ["/istasyon/kuzey-yamac/olcum", "/istasyon/dogu-sirt/olcum"],
  ["/istasyon/kuzey-yamac/olcum", "/oturum"],
];
for (const [onceki, sonraki] of gecisler) {
  const a = eslestir(onceki), b = eslestir(sonraki);
  let ortak = 0;
  while (ortak < a.zincir.length && ortak < b.zincir.length &&
    a.zincir[ortak] === b.zincir[ortak]) ortak += 1;
  const parametreAyni = JSON.stringify(a.parametre) === JSON.stringify(b.parametre);
  console.log(
    `${onceki} -> ${sonraki}`.padEnd(58),
    "korunan:", String(ortak),
    "| degisen:", (b.zincir.slice(ortak).join(" > ") || "-").padEnd(24),
    "| parametre ayni:", parametreAyni,
  );
}
```

```
-- etkin duzen zinciri --
/                              Kabuk > Karsilama                                    {}
/oturum                        Kabuk > OturumAc                                     {}
/istasyon                      Kabuk > IstasyonKabugu > IstasyonListesi             {}
/istasyon/yeni                 Kabuk > IstasyonKabugu > IstasyonFormu               {}
/istasyon/kuzey-yamac          Kabuk > IstasyonKabugu > IstasyonKunyesi > IstasyonOzeti {"kimlik":"kuzey-yamac"}
/istasyon/kuzey-yamac/olcum    Kabuk > IstasyonKabugu > IstasyonKunyesi > OlcumGecmisi {"kimlik":"kuzey-yamac"}
/istasyon/kuzey-yamac/ayar     Kabuk > IstasyonKabugu > IstasyonKunyesi > IstasyonAyarlari {"kimlik":"kuzey-yamac"}
/rapor/2024                    Kabuk > Bulunamadi                                   {"kalan":"rapor/2024"}
-- gezinmede korunan ve degisen --
/istasyon -> /istasyon/kuzey-yamac                         korunan: 2 | degisen: IstasyonKunyesi > IstasyonOzeti | parametre ayni: false
/istasyon/kuzey-yamac -> /istasyon/kuzey-yamac/olcum       korunan: 3 | degisen: OlcumGecmisi             | parametre ayni: true
/istasyon/kuzey-yamac/olcum -> /istasyon/kuzey-yamac/ayar  korunan: 3 | degisen: IstasyonAyarlari         | parametre ayni: true
/istasyon/kuzey-yamac/olcum -> /istasyon/dogu-sirt/olcum   korunan: 4 | degisen: -                        | parametre ayni: false
/istasyon/kuzey-yamac/olcum -> /oturum                     korunan: 1 | degisen: OturumAc                 | parametre ayni: false
```

Kök düğümün kendi yolu boştur; hiçbir dilim tüketmez ama zincirin başında durur. Bu, tüm
uygulamayı saran kabuğun tanımıdır. Parametre ağacın tek bir düğümünde çıkarılır ve o
düğümün altındaki her görünüme aynı değerle ulaşır: ölçüm geçmişi de, istasyon ayarları da
`kimlik` değerini yeniden çözümlemez.

#### Gezinmede Ne Korunur

Çıktının ikinci bölümü iç içe rotanın asıl kazancını sayıyla gösterir. İki zincirin ortak
ön eki, gezinmede **yerinde kalan** bileşenlerdir; ondan sonrası değişir.

Ölçüm geçmişinden istasyon ayarlarına geçişte üç bileşen korunur. Kabuk, istasyon kabuğu
ve istasyon künyesi ağaçta yerinde durur; yalnızca en içteki yuva yeni içerikle dolar.
Yeniden çizilmedikleri için içlerindeki durum da yaşamaya devam eder: açık yan gezinme
açık kalır, künyedeki veri yeniden istenmez.

Oturum açma ekranına geçişte korunan bileşen sayısı bire düşer. Zincirin geri kalanı
sökülür; sökülen bileşenlerin yerel durumu kaybolur. Bu, iç içe rota tanımının bir yan
etkisi değil, doğrudan ifade ettiği karardır: hangi bölümün hangi gezinmelerde yaşayacağı
ağacın biçimiyle belirlenir.

Dördüncü satır tek başına bir tuzağı gösterir. `kuzey-yamac` istasyonundan `dogu-sirt`
istasyonuna geçildiğinde düzen zinciri **birebir aynıdır** — dört bileşenin dördü de
korunur — ama parametre değişmiştir. Bileşenler yerinde kaldığı için yeniden kurulmazlar;
ekranda hâlâ önceki istasyonun verisi durur. Doğru davranış, parametreye bağlı verinin
parametre değişimini de dinlemesidir. Bu, Bileşen Tabanlı Arayüz Geliştirme kursundaki yan
etki dersinin bağımlılık kuralının yönlendirmedeki karşılığıdır: veri isteğinin bağımlılığı
bileşenin varlığı değil, parametrenin değeridir.

#### Bulunamadı Rotasının Yeri

Ağaçta joker düğümü hangi düzeyde durursa, bulunamadı görünümü o düzeyin kabuğu içinde
görünür. Örnekteki joker kökün çocuğudur; `/rapor/2024` isteğinde zincir kabuk ile
bulunamadı görünümünden oluşur, yani kullanıcı üst başlığı ve gezinmeyi görmeye devam eder.

Joker `istasyon` düğümünün altına konsaydı `/istasyon/kuzey-yamac/bilinmeyen` yolu istasyon
kabuğunun içinde bir hata gösterirdi; kullanıcı istasyon bağlamını kaybetmezdi. İkisi de
geçerli tasarımlardır ve seçim kullanıcının nereye dönebilmesi gerektiğine bağlıdır. Yanlış
olan tek şey, jokeri hiç tanımlamamaktır: o durumda tanımsız adres boş bir ekran üretir.

Sunucu tarafındaki karşılığı unutulmamalıdır. Bulunamadı görünümünü istemcinin çizmesi,
sunucunun o adrese başarılı yanıt vermesi anlamına gelmez; Geçmiş ve Konum dersinde
kurulan kural gereği gerçekten bulunmayan adres yine bulunamadı durum kodunu almalıdır.

#### Yolsuz Düzen Düğümleri

Ağaçtaki her düzen bir dilim tüketmek zorunda değildir. Kendi yolu boş olan bir düğüm,
adresi değiştirmeden yalnızca bir çerçeve ekler. Bunun iki kullanımı vardır.

Birincisi görsel gruplamadır: birkaç rota aynı dar sütun düzenini paylaşıyorsa, o düzen
adres hiyerarşisine yeni bir dilim eklemeden ortak bir üst düğüme çıkarılır. Kullanıcının
gördüğü adres sadeleşmiş kalır.

İkincisi davranış paylaşımıdır: aynı erişim koşulunu ya da aynı veri yüklemesini paylaşan
rotalar yolsuz bir düğümün altında toplanır. Erişim koşulunun bu düğümlerde nasıl
işletileceği sonraki iki dersin konusudur.

Karşı yönde bir seçenek daha vardır: bir dilim tüketen ama kendi görünümü olmayan düğüm.
Bu düğüm adres hiyerarşisini derinleştirir, ekrana bir şey eklemez; alt görünüm doğrudan
üst yuvaya yerleşir.

#### Özet

- Rota ağacı, adresin hiyerarşisini rota tanımının hiyerarşisine çevirir; her düğüm bir
  dilim üstlenir ve alt yolları çocuklarına devreder.
- Düzen bileşeni alt görünümü bir yuvada barındırır; dizin rotası, yol tüketildiğinde
  düzenin varsayılan içeriğini sağlar.
- Eşleşmenin çıktısı tek bileşen değil, kökten yaprağa uzanan bir düzen zinciridir;
  parametre ağaçta bir kez çıkarılır ve altındaki tüm görünümlere ulaşır.
- Gezinmede iki zincirin ortak ön eki yerinde kalır ve içindeki durum korunur; ayrılan
  bölüm sökülüp yeniden kurulur.
- Zincir aynı kalıp yalnız parametre değiştiğinde bileşenler yeniden kurulmaz; parametreye
  bağlı verinin bu değişimi kendi bağımlılığı olarak izlemesi gerekir.
- Joker düğümünün ağaçtaki düzeyi, bulunamadı görünümünün hangi kabuğun içinde
  görüneceğini belirler.

#### Sonraki Adım

Ağaç, yolun taşıdığı durumu — hangi istasyon, hangi bölüm — bileşenlere ulaştırdı. Ekranın
geri kalan durumu ise henüz adreste değil. Ölçüm geçmişi sayfasında bir tarih aralığı
seçili, bir ölçüm türü süzülmüş, liste ikinci sayfada ve tarihe göre azalan sıralanmış
olabilir. Bunlar bileşenin yerel değişkenlerinde tutulduğunda kullanıcı o görünümü
paylaşamaz, yer imine ekleyemez ve geri düğmesiyle önceki süzgece dönemez. Sonraki ders
hangi durumun yola, hangisinin sorguya yazılacağını, sorgu durumunun nasıl serileştirilip
geri çözümleneceğini ve varsayılan değerlerin adresi nasıl kirletmeden bırakılacağını ele
alır.

### Rota Parametreleri ve Sorgu

Source: https://academia.sh/tr/kurslar/frontend-mimarisi/rota-parametreleri-ve-sorgu

Durumun adreste taşınması; yol parametresi ile sorgu durumunun ayrılması, şemayla okuma ve doğrulama, varsayılanların yazılmaması, kanonik biçim, bağımlı parametrelerin sıfırlanması ve geçmiş kaydı kararı.

Rota ağacı yolun taşıdığı durumu bileşenlere ulaştırdı: hangi istasyon, hangi bölüm.
Ölçüm geçmişi ekranının geri kalan durumu ise henüz adreste değil. Bir tarih aralığı
seçili, bir ölçüm türü süzülmüş, liste ikinci sayfada ve tarihe göre azalan sıralanmış
olabilir.

Bunlar bileşenin yerel değişkenlerinde tutulduğunda kullanıcı o görünümü paylaşamaz, yer
imine ekleyemez, geri düğmesiyle önceki süzgece dönemez. Adrese yazıldığında ise adres
uzar, kirlenir ve her değişiklik geçmişe bir kayıt eklemeye adaydır. Bu ders iki soruyu
ayırır: hangi durum adrese girer ve giren durum nasıl yazılır.

#### Yol mu, Sorgu mu, Bellek mi

Geçmiş ve Konum dersinde adresin parçaları taşıdıkları duruma göre ayrılmıştı: yol kaynağın
kimliğini, sorgu aynı kaynağın hangi görünümünün istendiğini, parça belge içindeki konumu
taşır. Yönlendirme bu ayrımı işletilebilir bir kurala çevirir.

**Yol parametresi** bir kaynağı seçer. İstasyon kimliği yola girer, çünkü değiştiğinde
görüntülenen şey başka bir nesnedir. Yol parametresinin bir varsayılanı olmaz: kaynak ya
belirtilir ya belirtilmez.

**Sorgu parametresi** aynı kaynağın sunumunu ayarlar. Süzgeç, sıralama, sayfa numarası,
görünüm biçimi buraya girer. Her birinin bir varsayılanı vardır ve kullanıcı hiçbirini
seçmediğinde ekran yine anlamlıdır.

**Bellek** ise adreste yeri olmayan durumu tutar. Ölçütü Geçmiş ve Konum dersi vermişti:
kullanıcı bu adresi yeni bir sekmede açtığında aynı ekranı görmeyi bekliyor mu. Açık bir
ipucu balonu, sürüklenen bir kaydırıcının anlık değeri, bir animasyonun ilerlemesi bu
sınavı geçemez.

Sınırda duran bir durum vardır: yalnız bu oturumda anlamlı olan ama gezinme boyunca
korunması gereken bilgi — listede en son görüntülenen satır gibi. Bunun yeri geçmiş
kaydının durum nesnesidir; adrese yazılmaz ama geri gidildiğinde geri gelir.

#### Yol Parametresi Bir Dış Girdidir

Yol parametresi kullanıcının yazabildiği bir alandır. Adres çubuğuna elle girilebilir,
eski bir yer iminden gelebilir, bir bağlantıda yanlış üretilmiş olabilir. Eşleyicinin onu
çıkarmış olması geçerli olduğu anlamına gelmez.

Bu, güvenlik tarafında da geçerlidir. `/istasyon/kuzey-yamac` adresini açabilen bir
kullanıcı `/istasyon/dogu-sirt` adresini de açabilir. İstemcinin parametreyi doğrulaması
biçim hatalarını yakalar; yetkiyi ise yalnızca sunucu doğrulayabilir. Bu ayrım sonraki
dersin konusudur.

Doğrulamanın üç sonucu birbirinden ayrılır. Biçimi bozuk parametre — sayı beklenen yerde
harf — bulunamadı görünümüne düşer. Biçimi doğru ama karşılığı olmayan parametre sunucudan
gelen yanıtla anlaşılır ve yine bulunamadı üretir. Geçerli parametre ise görünüme aktarılır.

#### Sorgu Durumunun Şeması

Sorgu dizgisi bir dizgi haritasıdır: her değer metindir, her ad birden çok kez
görünebilir ve içeriği dışarıdan gelir. Bu üç özellik, sorguyu doğrudan durum olarak
kullanmayı olanaksız kılar. Araya bir **şema** girer: her parametrenin tipini, varsayılanını
ve geçerli değer kümesini bildiren tanım.

```js
// sorgu-durumu.mjs — sorgu dizgisinin semayla okunmasi ve kanonik bicimde yazilmasi
const SEMA = {
  olcum: { tur: "secim", varsayilan: "hepsi", secenek: ["hepsi", "sicaklik", "nem", "ruzgar"] },
  esik: { tur: "sayi", varsayilan: null },
  sayfa: { tur: "tamsayi", varsayilan: 1, enAz: 1 },
  sira: { tur: "secim", varsayilan: "azalan", secenek: ["artan", "azalan"] },
  etiket: { tur: "coklu", varsayilan: [] },
};

// Tek dogrulama noktasi: hem okuma hem yazma bundan gecer.
function dogrula(kural, ham) {
  if (kural.tur === "secim")
    return kural.secenek.includes(ham) ? { deger: ham } : { hata: true };
  const s = kural.tur === "tamsayi" ? Number.parseInt(ham, 10) : Number(ham);
  if (!Number.isFinite(s) || (kural.enAz !== undefined && s < kural.enAz)) return { hata: true };
  return { deger: s };
}

function oku(sorgu) {
  const p = new URLSearchParams(sorgu);
  const durum = {}, uyari = [];
  for (const [ad, kural] of Object.entries(SEMA)) {
    if (kural.tur === "coklu") {
      const hepsi = p.getAll(ad);
      durum[ad] = hepsi.length ? [...new Set(hepsi)].sort() : kural.varsayilan;
      continue;
    }
    const ham = p.get(ad);
    if (ham === null) { durum[ad] = kural.varsayilan; continue; }
    const sonuc = dogrula(kural, ham);
    if (sonuc.hata) { durum[ad] = kural.varsayilan; uyari.push(`${ad}=${ham} gecersiz`); }
    else durum[ad] = sonuc.deger;
  }
  return { durum, uyari };
}

function yaz(durum) {
  const p = new URLSearchParams();
  for (const [ad, kural] of Object.entries(SEMA)) {     // sema sirasi kanonik sirayi verir
    const deger = durum[ad];
    if (kural.tur === "coklu") {
      for (const d of [...new Set(deger ?? [])].sort()) p.append(ad, d);
      continue;
    }
    if (deger === null || deger === undefined || deger === kural.varsayilan) continue;
    if (dogrula(kural, String(deger)).hata) continue;   // gecersiz deger adrese yazilmaz
    p.set(ad, String(deger));
  }
  const s = p.toString();
  return s ? `?${s}` : "";
}

console.log("-- okuma --");
for (const sorgu of [
  "",
  "?olcum=sicaklik&sayfa=3",
  "?sayfa=abc&olcum=kar&esik=-3.5&etiket=gece&etiket=kar&etiket=gece",
  "?sayfa=0&sira=artan&olcum=bagil+nem",
]) {
  const { durum, uyari } = oku(sorgu);
  console.log((sorgu || "(bos)").padEnd(52), JSON.stringify(durum));
  if (uyari.length) console.log(" ".repeat(52), "uyari:", uyari.join("; "));
}

console.log("-- yazma --");
for (const durum of [
  { olcum: "hepsi", esik: null, sayfa: 1, sira: "azalan", etiket: [] },
  { olcum: "sicaklik", esik: -5, sayfa: 1, sira: "azalan", etiket: [] },
  { olcum: "bagil nem", esik: null, sayfa: 4, sira: "artan", etiket: ["kar", "gece"] },
]) console.log(JSON.stringify(durum).padEnd(78), yaz(durum) || "(bos)");

console.log("-- gidis donus --");
const baslangic = { olcum: "ruzgar", esik: 12.5, sayfa: 2, sira: "artan", etiket: ["gece"] };
const dizgi = yaz(baslangic);
console.log("dizgi   ", dizgi);
console.log("ayni mi ", JSON.stringify(oku(dizgi).durum) === JSON.stringify(baslangic));
console.log("kararli ", yaz(oku(dizgi).durum) === dizgi);

console.log("-- adres uzerinde --");
const adres = new URL("https://ornek.gecerli/istasyon/kuzey-yamac/olcum?sayfa=9&olcum=nem");
const guncel = { ...oku(adres.search).durum, olcum: "sicaklik", sayfa: 1 };
console.log(adres.pathname + yaz(guncel));
```

```
-- okuma --
(bos)                                                {"olcum":"hepsi","esik":null,"sayfa":1,"sira":"azalan","etiket":[]}
?olcum=sicaklik&sayfa=3                              {"olcum":"sicaklik","esik":null,"sayfa":3,"sira":"azalan","etiket":[]}
?sayfa=abc&olcum=kar&esik=-3.5&etiket=gece&etiket=kar&etiket=gece {"olcum":"hepsi","esik":-3.5,"sayfa":1,"sira":"azalan","etiket":["gece","kar"]}
                                                     uyari: olcum=kar gecersiz; sayfa=abc gecersiz
?sayfa=0&sira=artan&olcum=bagil+nem                  {"olcum":"hepsi","esik":null,"sayfa":1,"sira":"artan","etiket":[]}
                                                     uyari: olcum=bagil nem gecersiz; sayfa=0 gecersiz
-- yazma --
{"olcum":"hepsi","esik":null,"sayfa":1,"sira":"azalan","etiket":[]}            (bos)
{"olcum":"sicaklik","esik":-5,"sayfa":1,"sira":"azalan","etiket":[]}           ?olcum=sicaklik&esik=-5
{"olcum":"bagil nem","esik":null,"sayfa":4,"sira":"artan","etiket":["kar","gece"]} ?sayfa=4&sira=artan&etiket=gece&etiket=kar
-- gidis donus --
dizgi    ?olcum=ruzgar&esik=12.5&sayfa=2&sira=artan&etiket=gece
ayni mi  true
kararli  true
-- adres uzerinde --
/istasyon/kuzey-yamac/olcum?olcum=sicaklik
```

Okuma tarafı hiçbir girdide çuvallamaz. Eksik parametre varsayılana düşer, bozuk sayı
varsayılana düşer ve uyarı üretir, tanımsız seçenek reddedilir. Bu davranışın adı
**sağlamlıktır**: kullanıcının elinde kalmış eski bir adres, uygulamayı hata ekranına
düşürmek yerine anlamlı bir görünüm açar.

Yazma tarafında iki karar görünür. Varsayılana eşit değer **adrese yazılmaz**; ilk satırda
tüm alanlar varsayılan olduğu için sorgu boştur. Bu, adresin kısa kalmasından öte bir şey
sağlar: aynı ekran her zaman tek bir adresle anılır. Geçersiz değer de yazılmaz; üçüncü
satırda tanımsız ölçüm türü düşürülmüş, geri kalan alanlar yazılmıştır.

#### Kanonik Biçim

Aynı durumun tek bir dizgi karşılığı olmasına **kanonik biçim** denir. Örnekteki yazıcı
bunu iki kuralla sağlar: parametreler şemadaki sırayla yazılır, çoklu değerler kendi
içinde sıralanır. Etiket kümesi `kar, gece` verilmişken çıktıda `gece, kar` sırasıyla
görünmesinin nedeni budur.

Gidiş dönüş bölümü iki özelliği sınar. Okuma yazmanın tersidir: yazılan durum geri
okunduğunda aynı nesneyi verir. Yazma kararlıdır: geri okunan durum yeniden yazıldığında
aynı dizgiyi verir. İkisi birlikte, adresin durumun güvenilir bir taşıyıcısı olduğunu
söyler.

Kanonik biçim üç yerde işe yarar. Sunucu durumu önbelleği adresi anahtar olarak
kullanabilir; aynı görünüm iki farklı dizgiyle anılmadığı için iki ayrı önbellek girdisi
oluşmaz. İki adresin aynı ekranı gösterip göstermediği dizgi karşılaştırmasıyla anlaşılır.
Ve kullanıcıların paylaştığı adresler birbirinin aynısı olur.

#### Bağımlı Parametreler

Son bölüm bir karar örneğidir. Adreste dokuzuncu sayfa ve nem süzgeci varken ölçüm türü
sıcaklığa çevrilmiştir; çıktıda sayfa numarası yoktur, çünkü açıkça bire çekilmiştir.

Süzgeç değiştiğinde eski sayfa numarası anlamını yitirir: yeni süzgeçte dokuzuncu sayfa
bulunmayabilir ve kullanıcı boş bir liste görür. Aynı bağımlılık seçili satır kimliği,
sonsuz listenin yüklenmiş uzunluğu ve sıralama içinde konum için de geçerlidir. Bu
sıfırlamalar kendiliğinden olmaz; hangi parametrenin hangisini geçersiz kıldığı açıkça
yazılır.

Ters yön de önemlidir: **ilgisiz parametre korunur**. Ölçüm türü değişirken sıralama ve
eşik yerinde kalmalıdır. Sorgu dizgisini elle birleştiren kod bu korumayı kaybeder; durumu
bir bütün olarak okuyup üzerine yazan kod korur.

#### Geçmişe Ekleme mi, Değiştirme mi

Sorgu durumunun her değişimi adresi değiştirir; ancak her değişim geçmişe bir kayıt
eklemek zorunda değildir. Geçmiş ve Konum dersinde kurulan ayrım burada karara dönüşür.

**Eklenir:** kullanıcının geri düğmesiyle dönmek isteyeceği değişimler. Sayfa geçişi, ölçüm
türünün değiştirilmesi, sıralamanın tersine çevrilmesi.

**Değiştirilir:** ara durumlar ve kullanıcının geri dönmeyi beklemediği düzeltmeler. Metin
kutusuna yazılan her karakter için ekleme yapan bir arama alanı, geri düğmesini harf harf
geri alan bir düzeneğe çevirir. Bu tür alanlarda yazma sırasında değiştirme yapılır;
Form Olayları dersindeki gecikmeli tetikleme ile birlikte kullanıldığında adres yazma
durduktan sonra bir kez güncellenir.

Uygulamanın açılışında yapılan düzeltme de değiştirmeyle yapılır: adreste geçersiz bir
değer varsa kanonik biçime çekilir, ama bu düzeltme geçmişe kayıt bırakmaz. Bıraksaydı geri
düğmesi kullanıcıyı geçersiz adrese geri götürür ve döngü kurulurdu.

#### Özet

- Yol parametresi kaynağı seçer ve varsayılanı yoktur; sorgu parametresi aynı kaynağın
  sunumunu ayarlar ve her birinin bir varsayılanı vardır; geri kalan durum bellekte veya
  geçmiş kaydının durum nesnesinde kalır.
- Yol parametresi dış girdidir; biçim doğrulaması istemcide yapılır, yetki doğrulaması
  istemciye bırakılamaz. Sorgu durumu da bir şemayla okunur: eksik, bozuk ve tanımsız
  değerler varsayılana düşer, uygulama hata ekranına gitmez.
- Varsayılana eşit ve geçersiz değerler adrese yazılmaz; parametre sırası ve çoklu değer
  sırası sabitlenerek kanonik biçim elde edilir.
- Kanonik biçim, önbellek anahtarı üretmeyi, iki görünümü karşılaştırmayı ve paylaşılan
  adreslerin aynı olmasını sağlar.
- Bir parametre başka bir parametreyi geçersiz kılıyorsa sıfırlama açıkça yazılır; ilgisiz
  parametreler korunur.
- Geri dönülmesi beklenen değişimler geçmişe eklenir, ara durumlar ve açılıştaki
  düzeltmeler o anki kaydı değiştirir.

#### Sonraki Adım

Adres artık hangi kaynağın hangi görünümünün istendiğini eksiksiz taşıyor. Taşımadığı tek
şey, kullanıcının o kaynağı görmeye **hakkı olup olmadığı**. İstasyon ayarları ekranı yalnız
yetkili kullanıcılara açık; ölçüm girişi yalnız oturum açmış kullanıcılara. Adres çubuğuna
elle yazılan bir yol bu ekranları da açmaya çalışır. Sonraki ders rotaya bağlı erişim
koşullarını, koşulun karara dönüşmesini, oturum açtıktan sonra istenen adrese dönüşü ve
istemci tarafı denetimin neden güvenlik sınırı olmadığını ele alır.

### Rota Koruma

Source: https://academia.sh/tr/kurslar/frontend-mimarisi/rota-koruma

Yetkiye bağlı erişim; yol koruyucularının rota ağacında kalıtılması, izin–yönlendir–beklet kararları, bilinmeyen kimlik durumunun ayrı ele alınması, dönüş adresinin doğrulanması ve istemci denetiminin sınırı.

Adres artık hangi kaynağın hangi görünümünün istendiğini eksiksiz taşıyor. Taşımadığı tek
şey, kullanıcının o kaynağı görmeye hakkı olup olmadığı. Kuzey Yamaç Ölçüm İstasyonu
uygulamasında istasyon ayarları ekranı yalnız yetkili kullanıcılara, ölçüm girişi yalnız
oturum açmış kullanıcılara açık.

Bağlantıyı gizlemek çözüm değildir: adres çubuğuna elle yazılan bir yol da eşleyiciye
ulaşır. Karar, eşleşme ile görünümün kurulması arasında verilmelidir. Bu ders o karar
noktasını, kararın türlerini ve istemci tarafında verilen kararın neyi sağlayıp neyi
sağlamadığını ele alır.

#### Yol Koruyucu

**Yol koruyucu**, bir rotanın görünümü kurulmadan önce çalışan ve üç yanıttan birini veren
işlevdir. Girdisi eşleşen rota ile o anki kimlik bilgisidir; çıktısı bir karardır.

**İzin**, görünümün kurulmasına engel olmaz. **Yönlendir**, gezinmeyi başka bir adrese
çevirir. **Beklet**, kararın henüz verilemediğini söyler; kimlik bilgisi gelene kadar ne
hedef görünüm ne de yönlendirme kurulur.

Koruyucular rota ağacında **kalıtılır**. Bir düğüme konan koruyucu, o düğümün altındaki
tüm rotalar için geçerlidir. İç İçe Rotalar ve Düzenler dersindeki yolsuz düzen
düğümlerinin ikinci kullanımı buydu: aynı erişim koşulunu paylaşan rotalar tek bir üst
düğümün altında toplanır ve koşul bir kez yazılır.

Bir rotanın koruyucuları kökten yaprağa doğru sıralanmış bir zincir oluşturur ve sırayla
çalıştırılır. **İlk reddeden kararı verir**; kalan koruyucular çalışmaz. Sıra rastgele
değildir: oturum koşulu rol koşulundan önce gelir, çünkü rolü sorabilmek için önce
kullanıcının kim olduğunun bilinmesi gerekir.

```js
// yol-koruyucu.mjs — kalitilan koruyucu zinciri, karar turleri ve donus adresinin dogrulanmasi
const KORUYUCU = {
  oturum: (k) => k.durum === "bilinmiyor" ? { karar: "beklet" }
    : k.durum === "acik" ? { karar: "izin" }
      : { karar: "yonlendir", hedef: "/oturum" },
  yonetici: (k) => k.rol === "yonetici" ? { karar: "izin" }
    : { karar: "yonlendir", hedef: "/yetkisiz", kayitDegistir: true },
  anonim: (k) => k.durum === "acik" ? { karar: "yonlendir", hedef: "/istasyon", kayitDegistir: true }
    : { karar: "izin" },
};

// Koruyucular rota agacinda kalitilir: ustteki dugumunki alt agacin tumune uygulanir.
const AGAC = [
  { yol: "/", koruyucu: [] },
  { yol: "/oturum", koruyucu: ["anonim"] },
  { yol: "/istasyon", koruyucu: ["oturum"] },
  { yol: "/istasyon/:kimlik/olcum", koruyucu: ["oturum"] },
  { yol: "/istasyon/:kimlik/ayar", koruyucu: ["oturum", "yonetici"] },
];

const TABAN = "https://ornek.gecerli";

function degerlendir(rota, kullanici, istenenAdres) {
  for (const ad of rota.koruyucu) {
    const sonuc = KORUYUCU[ad](kullanici);
    if (sonuc.karar === "izin") continue;
    if (sonuc.karar === "beklet") return { karar: "beklet", koruyucu: ad };
    const hedef = sonuc.hedef === "/oturum"
      ? `/oturum?donus=${encodeURIComponent(istenenAdres)}`
      : sonuc.hedef;
    return { karar: "yonlendir", koruyucu: ad, hedef, kayitDegistir: sonuc.kayitDegistir ?? true };
  }
  return { karar: "izin" };
}

const kullanicilar = [
  { ad: "yukleniyor", durum: "bilinmiyor", rol: null },
  { ad: "konuk", durum: "kapali", rol: null },
  { ad: "gozlemci", durum: "acik", rol: "gozlemci" },
  { ad: "yonetici", durum: "acik", rol: "yonetici" },
];

console.log("-- karar tablosu --");
for (const k of kullanicilar) {
  for (const rota of AGAC) {
    const istenen = rota.yol.replace(":kimlik", "kuzey-yamac");
    const s = degerlendir(rota, k, istenen);
    console.log([
      k.ad.padEnd(11), istenen.padEnd(30), s.karar.padEnd(10),
      (s.hedef ?? "-").padEnd(48), s.koruyucu ? `(${s.koruyucu})` : "",
    ].join(" ").trimEnd());
  }
}

// Donus adresi kullanicidan gelir; ac yonlendirmeyi onlemek icin dogrulanir.
function donusuCoz(ham) {
  if (ham === null) return "/istasyon";
  let coz;
  try { coz = new URL(ham, TABAN); } catch { return "/istasyon"; }
  if (coz.origin !== TABAN) return "/istasyon";
  if (!ham.startsWith("/") || ham.startsWith("//")) return "/istasyon";
  return coz.pathname + coz.search;
}

console.log("-- donus adresinin dogrulanmasi --");
for (const ham of [
  "/istasyon/kuzey-yamac/ayar?olcum=nem",
  "https://kotu.ornek/topla",
  "//kotu.ornek/topla",
  "istasyon/kuzey-yamac",
  "/istasyon/../../etc",
  null,
]) console.log(JSON.stringify(ham).padEnd(40), "->", donusuCoz(ham));
```

```
-- karar tablosu --
yukleniyor  /                              izin       -
yukleniyor  /oturum                        izin       -
yukleniyor  /istasyon                      beklet     -                                                (oturum)
yukleniyor  /istasyon/kuzey-yamac/olcum    beklet     -                                                (oturum)
yukleniyor  /istasyon/kuzey-yamac/ayar     beklet     -                                                (oturum)
konuk       /                              izin       -
konuk       /oturum                        izin       -
konuk       /istasyon                      yonlendir  /oturum?donus=%2Fistasyon                        (oturum)
konuk       /istasyon/kuzey-yamac/olcum    yonlendir  /oturum?donus=%2Fistasyon%2Fkuzey-yamac%2Folcum  (oturum)
konuk       /istasyon/kuzey-yamac/ayar     yonlendir  /oturum?donus=%2Fistasyon%2Fkuzey-yamac%2Fayar   (oturum)
gozlemci    /                              izin       -
gozlemci    /oturum                        yonlendir  /istasyon                                        (anonim)
gozlemci    /istasyon                      izin       -
gozlemci    /istasyon/kuzey-yamac/olcum    izin       -
gozlemci    /istasyon/kuzey-yamac/ayar     yonlendir  /yetkisiz                                        (yonetici)
yonetici    /                              izin       -
yonetici    /oturum                        yonlendir  /istasyon                                        (anonim)
yonetici    /istasyon                      izin       -
yonetici    /istasyon/kuzey-yamac/olcum    izin       -
yonetici    /istasyon/kuzey-yamac/ayar     izin       -
-- donus adresinin dogrulanmasi --
"/istasyon/kuzey-yamac/ayar?olcum=nem"   -> /istasyon/kuzey-yamac/ayar?olcum=nem
"https://kotu.ornek/topla"               -> /istasyon
"//kotu.ornek/topla"                     -> /istasyon
"istasyon/kuzey-yamac"                   -> /istasyon
"/istasyon/../../etc"                    -> /etc
null                                     -> /istasyon
```

Tablonun her satırı bir çift karardır: kim, nereye. Gözlemci ölçüm geçmişini görür ama
ayarlara giremez; yönetici ikisine de girer. Kök yol ve oturum ekranı koruyucusuz ya da
ters koruyuculudur.

#### Ters Koruyucu

`anonim` koruyucusu diğerlerinin tersini yapar: oturumu **açık olan** kullanıcıyı oturum
ekranından uzaklaştırır. Bunu yapmayan bir uygulamada, oturum açmış kullanıcı geri
düğmesiyle oturum ekranına döner ve orada ne yapacağını bilmez; yeniden oturum açmaya
kalkarsa kimlik akışı iki kez çalışır.

Aynı ters mantık kayıt, parola sıfırlama ve karşılama akışları için de geçerlidir.

#### Bilinmeyen Kimlik

Üçüncü karar türü, çıktının ilk beş satırında görülür. Uygulama yeni yüklendiğinde oturumun
açık olup olmadığı henüz bilinmez: bu bilgi ya bir istekle sunucudan gelir ya da yerel bir
kayıttan okunur. Her ikisi de zaman alır.

Bu aralıkta yapılan en yaygın hata, bilinmeyeni **yetkisiz saymaktır**. Sonucu tanıdıktır:
kullanıcı korunan bir sayfayı yeniler, bir an oturum ekranını görür, kimlik bilgisi
gelince asıl sayfaya döner. Ekranın bu şekilde iki kez değişmesi yalnız rahatsız edici
değildir; adres de iki kez değişmiş, geçmişe gereksiz kayıt girmiş olabilir.

Doğru davranış, kararı ertelemektir. Beklet yanıtı alan yönlendirme, kimlik bilgisi gelene
kadar ne hedef görünümü ne de oturum ekranını kurar; yerinde bir yükleniyor göstergesi
bırakır. Kimlik geldiğinde koruyucu yeniden çalıştırılır ve gerçek karar verilir.

Bekletmenin bir sınırı olmalıdır. Kimlik isteği başarısız olursa karar sonsuza kadar
ertelenemez; başarısızlık yetkisizlikle aynı sonuca bağlanır ve kullanıcıya nedeni
söylenir.

#### Dönüş Adresi

Konuk kullanıcı korunan bir adrese gittiğinde oturum ekranına gönderilir. Oturum açtıktan
sonra karşılama ekranına düşmesi, gitmek istediği yeri unutmak demektir. Bu nedenle
istenen adres yönlendirmenin sorgusuna yazılır ve oturum açıldıktan sonra oradan okunur.

Bu parametre kullanıcının denetimindedir. Bir saldırgan `donus` değerine kendi adresini
koyup bağlantıyı paylaşabilir; kullanıcı tanıdığı bir sitede oturum açar ve hemen ardından
başka bir yere gönderilir. Bu kusurun adı **açık yönlendirmedir** ve önlemi tek cümledir:
dönüş adresi kullanılmadan önce uygulamanın kendi kaynağına ait olduğu doğrulanır.

Çıktının son bölümü doğrulamanın ne yakaladığını gösterir. Tam nitelikli dış adres
reddedilir. Eğik çizgiyle başlayan protokolsüz adres — tarayıcının aynı protokolle dış bir
konağa gitmesini sağlayan biçim — reddedilir; bu, yalnız ilk karakteri sınayan bir denetimin
kaçırdığı durumdur. Göreli adres de reddedilir, çünkü hangi tabana göre çözüleceği
belirsizdir. Geriye yalnız uygulama içi yollar kalır ve bunlar çözülürken normalleştirilir.

Aynı sınama, adresi kullanıcıdan alan her yerde geçerlidir: sorguda taşınan hedef adresler,
bildirim bağlantıları, hata sayfasındaki dönüş düğmeleri.

#### Yönlendirme Geçmişe Nasıl Yazılır

Koruyucunun ürettiği yönlendirme, kullanıcının **istediği** bir gezinme değildir. Bu
nedenle geçmişe yeni bir kayıt eklemez, o anki kaydın yerine geçer. Aksi hâlde kullanıcı
oturum ekranından geri düğmesine bastığında korunan adrese döner, koruyucu yeniden çalışır
ve onu tekrar oturum ekranına gönderir — geri düğmesi işlemez hâle gelir.

Örnekteki kararlar bu nedenle `kayitDegistir` bayrağı taşır. Geçmiş ve Konum dersinde
kurulan ayrım burada doğrudan uygulanır: kullanıcının geri dönmek isteyeceği durumlar
eklenir, ara adresler değiştirilir.

#### İstemci Denetiminin Sınırı

Bu dersin kurduğu her şey **arayüz kararıdır**, güvenlik sınırı değildir. Üç gerekçe bunu
gösterir.

Koruyucunun kodu kullanıcının makinesinde çalışır ve okunabilir; rol adları, koşullar,
gizli rotaların yolları paketin içindedir. Kullanıcı koruyucuyu çalıştırmayabilir: adresi
doğrudan istemek, isteği elle üretmek ya da yanıtı değiştirmek elindedir. Korunan görünümün
kullandığı veri de yine bir istekle alınır; görünümü gizlemek veriyi gizlemez.

Bu nedenle **her korunan işlem sunucuda yeniden yetkilendirilir**. İstemci tarafı koruma,
yetkisiz bir kullanıcının boş bir ekranla ya da bir hata yığınıyla karşılaşmasını önler;
verinin korunmasını sağlamaz.

Aynı ayrım gizli bilgi için de geçerlidir. Rol bilgisine göre bir menü ögesini gizlemek
kabul edilebilir; ancak yalnız yöneticinin görmesi gereken bir veri, gizlenmiş bir bileşenin
içine konup istemciye gönderilemez. Gönderilen her şey görülebilir.

#### Özet

- Yol koruyucu, eşleşme ile görünümün kurulması arasında çalışır ve izin, yönlendir ya da
  beklet kararlarından birini verir; koruyucular rota ağacında kalıtılır, zincir hâlinde
  sırayla çalışır ve ilk reddeden karar verir.
- Kimliğin henüz bilinmediği durum yetkisizlikten ayrılır; karar ertelenir, ama erteleme
  başarısızlık durumunda sonlandırılır.
- Ters koruyucu, oturumu açık kullanıcıyı oturum ve kayıt ekranlarından uzaklaştırır.
- Dönüş adresi kullanıcı girdisidir; uygulamanın kendi kaynağına ait olduğu doğrulanmadan
  kullanılırsa açık yönlendirme kusuru doğar.
- Koruyucunun ürettiği yönlendirme geçmişe kayıt eklemez, o anki kaydı değiştirir.
- İstemci tarafı koruma bir arayüz kararıdır; her korunan işlem ve veri sunucuda yeniden
  yetkilendirilir.

#### Sonraki Adım

Yönlendirme artık hangi ekranın kime açık olduğunu biliyor. Bilmediği şey, o ekranın
kodunun ne zaman indirileceği. Şu anda uygulamanın bütün ekranları — istasyon listesi,
ayarlar formu, ölçüm grafiği, oturum akışı — tek bir pakette birlikte iniyor. Kullanıcı
yalnız oturum açmak için gelse bile yönetici ayar ekranının kodunu indiriyor. Rota ağacı
bu bölünmeyi yapmak için gereken bilgiyi zaten taşıyor: hangi bileşenin hangi yolda
kullanıldığı tanımın içinde yazılı. Sonraki ders paketi rota sınırlarından bölmeyi, ortak
parçaların yinelenmesini önlemeyi ve bölünmenin getirdiği bekleme ile hata durumlarını ele
alır.

### Kod Bölme ve Tembel Yükleme

Source: https://academia.sh/tr/kurslar/frontend-mimarisi/kod-bolme-ve-tembel-yukleme

Paketin rota sınırlarından bölünmesi; bağımlılık kapanışı, kullanım imzasına göre parça oluşturma, ilk yükleme maliyetinin ölçülmesi, gezinme başına ek indirme, önceden getirme ve bölmenin getirdiği bekleme ile hata durumları.

Yönlendirme artık hangi ekranın kime açık olduğunu biliyor. Bilmediği şey, o ekranın
kodunun ne zaman indirileceği. Kuzey Yamaç Ölçüm İstasyonu uygulamasının bütün ekranları
tek bir pakette birlikte iniyor: yalnız oturum açmak için gelen bir kullanıcı, ölçüm
grafiğinin çizim kütüphanesini ve istasyon haritasını da indiriyor.

Rota tanımı bu bölünmeyi yapmak için gereken bilgiyi zaten taşıyor. Hangi bileşenin hangi
yolda kullanıldığı ağacın içinde yazılı; kullanıcının hangi yolda olduğu da çalışma
zamanında bilinen tek şey. **Kod bölme**, bu iki bilgiyi birleştirip paketi rota
sınırlarından ayırma işidir.

#### Bağımlılık Kapanışı

Bir rotanın gerçekte neye ihtiyaç duyduğu, doğrudan kullandığı modüllerle sınırlı değildir.
Modüller birbirine bağlıdır: ölçüm grafiği çizim modülünü, o da tarih biçimlendirmeyi
kullanır. Rotanın maliyeti, bağımlılık çizgesinde ondan erişilebilen tüm modüllerin
toplamıdır. Bu kümeye **bağımlılık kapanışı** denir ve Veri Yapıları kursundaki çizge
dolaşımıyla hesaplanır.

Kapanışlar çıkarıldıktan sonra ikinci soru gelir: bir modül birden çok rotada
kullanılıyorsa hangi parçaya girecek? Her rotanın parçasına kopyalanırsa aynı kod birden
çok kez iner. Tek bir ortak parçaya konursa, o parçayı istemeyen rotalar da onu indirmek
zorunda kalır.

Çözüm modülleri **kullanım imzasına** göre gruplamaktır: bir modülün imzası, onu isteyen
rotaların kümesidir. Aynı imzaya sahip modüller birlikte iner, çünkü hep birlikte
gerekirler.

```js
// kod-bolme.mjs — rota bagimlilik kapanisi, kullanim imzasina gore parcalama ve maliyet
const MODUL = {                       // boyut: aktarilan kB, bagimlilik: dogrudan cagrilanlar
  "cekirdek": { boyut: 38, bagimlilik: ["tasarim-sistemi"] },
  "tasarim-sistemi": { boyut: 24, bagimlilik: [] },
  "tablo": { boyut: 18, bagimlilik: ["tasarim-sistemi"] },
  "grafik": { boyut: 96, bagimlilik: ["tasarim-sistemi", "tarih-bicimlendirme"] },
  "harita": { boyut: 120, bagimlilik: ["tasarim-sistemi"] },
  "form-dogrulama": { boyut: 15, bagimlilik: ["tasarim-sistemi"] },
  "tarih-bicimlendirme": { boyut: 12, bagimlilik: [] },
  "disa-aktarma": { boyut: 30, bagimlilik: [] },
};

// Her rota: kendi duzen zinciri boyunca gereken modullerin listesi.
const ROTA = {
  "/oturum": ["form-dogrulama"],
  "/istasyon": ["tablo", "harita"],
  "/istasyon/:kimlik": ["tablo", "tarih-bicimlendirme"],
  "/istasyon/:kimlik/olcum": ["grafik", "tablo", "disa-aktarma"],
  "/istasyon/:kimlik/ayar": ["form-dogrulama"],
};

const GIRIS = ["cekirdek"];           // her ziyarette inen giris noktasi

function kapanis(kokler) {            // bagimlilik cizgesinde erisilen tum modulller
  const gorulen = new Set(), yigit = [...kokler];
  while (yigit.length) {
    const m = yigit.pop();
    if (gorulen.has(m)) continue;
    gorulen.add(m);
    yigit.push(...MODUL[m].bagimlilik);
  }
  return gorulen;
}

const toplam = (kume) => [...kume].reduce((t, m) => t + MODUL[m].boyut, 0);

const girisKapanis = kapanis(GIRIS);
const rotaKapanis = Object.fromEntries(
  Object.entries(ROTA).map(([yol, k]) => [yol, kapanis(k)]));

// Kullanim imzasi: bir modulu isteyen rotalarin kumesi. Ayni imza = ayni parca.
const imza = new Map();
for (const [modul] of Object.entries(MODUL)) {
  if (girisKapanis.has(modul)) continue;                 // giris parcasinda zaten var
  const isteyen = Object.keys(ROTA).filter((y) => rotaKapanis[y].has(modul));
  if (isteyen.length === 0) continue;
  const anahtar = isteyen.join("+");
  if (!imza.has(anahtar)) imza.set(anahtar, []);
  imza.get(anahtar).push(modul);
}

console.log("-- parcalar --");
console.log(`giris${" ".repeat(3)}${String(toplam(girisKapanis)).padStart(4)} kB  ${[...girisKapanis].sort().join(", ")}`);
const parcaAdi = new Map();
let no = 0;
for (const [anahtar, moduller] of [...imza].sort((a, b) => b[0].split("+").length - a[0].split("+").length)) {
  const ad = `parca-${++no}`;
  parcaAdi.set(anahtar, ad);
  console.log(`${ad} ${String(moduller.reduce((t, m) => t + MODUL[m].boyut, 0)).padStart(4)} kB  ${moduller.sort().join(", ")}`);
  console.log(`${" ".repeat(12)}isteyen: ${anahtar.split("+").join("  ")}`);
}

const tekPaket = toplam(new Set([...girisKapanis, ...Object.values(rotaKapanis).flatMap((s) => [...s])]));
const bolunmus = (yol) => toplam(new Set([...girisKapanis, ...rotaKapanis[yol]]));

console.log("-- ilk yukleme maliyeti --");
console.log("rota".padEnd(26), "tek paket", "bolunmus", "kazanc");
for (const yol of Object.keys(ROTA)) {
  const b = bolunmus(yol);
  console.log(yol.padEnd(26), String(tekPaket).padStart(9), String(b).padStart(9),
    `${String(Math.round((1 - b / tekPaket) * 100)).padStart(5)}%`);
}

console.log("-- gezinmede ek indirme --");
for (const [onceki, sonraki] of [
  ["/istasyon", "/istasyon/:kimlik"],
  ["/istasyon/:kimlik", "/istasyon/:kimlik/olcum"],
  ["/istasyon/:kimlik/olcum", "/istasyon/:kimlik/ayar"],
  ["/oturum", "/istasyon"],
]) {
  const inen = new Set([...girisKapanis, ...rotaKapanis[onceki]]);
  const eksik = [...rotaKapanis[sonraki]].filter((m) => !inen.has(m));
  console.log(`${onceki} -> ${sonraki}`.padEnd(52),
    String(eksik.reduce((t, m) => t + MODUL[m].boyut, 0)).padStart(4), "kB ",
    eksik.sort().join(", ") || "-");
}
```

```
-- parcalar --
giris     62 kB  cekirdek, tasarim-sistemi
parca-1   18 kB  tablo
            isteyen: /istasyon  /istasyon/:kimlik  /istasyon/:kimlik/olcum
parca-2   15 kB  form-dogrulama
            isteyen: /oturum  /istasyon/:kimlik/ayar
parca-3   12 kB  tarih-bicimlendirme
            isteyen: /istasyon/:kimlik  /istasyon/:kimlik/olcum
parca-4  126 kB  disa-aktarma, grafik
            isteyen: /istasyon/:kimlik/olcum
parca-5  120 kB  harita
            isteyen: /istasyon
-- ilk yukleme maliyeti --
rota                       tek paket bolunmus kazanc
/oturum                          353        77    78%
/istasyon                        353       200    43%
/istasyon/:kimlik                353        92    74%
/istasyon/:kimlik/olcum          353       218    38%
/istasyon/:kimlik/ayar           353        77    78%
-- gezinmede ek indirme --
/istasyon -> /istasyon/:kimlik                         12 kB  tarih-bicimlendirme
/istasyon/:kimlik -> /istasyon/:kimlik/olcum          126 kB  disa-aktarma, grafik
/istasyon/:kimlik/olcum -> /istasyon/:kimlik/ayar      15 kB  form-dogrulama
/oturum -> /istasyon                                  138 kB  harita, tablo
```

Parça listesi imza gruplamasının sonucudur. Tasarım sistemi her rotada kullanıldığı için
giriş parçasına düşmüştür. Tablo modülü üç rotanın ortağıdır ve tek bir parçada durur;
üç rotaya kopyalanmadığı gibi, onu istemeyen oturum ekranına da yüklenmez. Dışa aktarma ile
grafik modülleri aynı parçada birleşmiştir, çünkü ikisini de yalnızca ölçüm geçmişi ister —
ayrı parçalara konmaları fazladan bir istek doğururdu, hiçbir rotaya kazanç getirmezdi.

#### Ölçülen Kazanç

İkinci tablo bölmenin neden yapıldığını sayıyla söyler. Oturum ekranıyla gelen bir
kullanıcı 353 kB yerine 77 kB indirir. Kazanç rotaya göre değişir: haritayı ya da grafiği
isteyen rotalarda daha küçüktür, çünkü asıl ağırlık oradadır.

Buradan iki karar çıkar. Ağır modülü bölmek, hafif modülü bölmekten çok daha fazla kazanç
sağlar; bölme çabası boyuta bakılarak yöneltilir. Ve kazanç, girişin büyüklüğüyle
sınırlıdır: her ziyarette inen 62 kB hiçbir bölmeyle azalmaz. Giriş parçasına gereksiz
modül koymak, bölmeyle kazanılanı geri verir.

Bölmenin bir maliyeti de vardır ve tabloda görünmez: her parça ayrı bir istektir. Modüller,
Araçlar ve Ekosistem kursundaki paketleme dersinde tanımlanan bu maliyet, çok küçük
parçalarda indirilen bayttan büyük olabilir. Ölçüt şudur: bir parça, kaçınılan indirme
kadar kazanç sağlamıyorsa ayrılmaz.

#### Gezinme Başına Maliyet

Üçüncü tablo bölmenin ilk yüklemeden sonraki etkisini gösterir. İstasyon ayrıntısından
ölçüm geçmişine geçiş 126 kB'lik yeni bir indirme gerektirir. Kullanıcı bağlantıya
bastığı anda bu indirme başlar; bitene kadar ekranda içerik yoktur.

Tek paketle bu bekleme yoktu — kod zaten inmişti. Bölme, ilk yükleme süresini gezinme
süresine dağıtır. Bu takas her zaman kârlı değildir: kullanıcıların büyük çoğunluğunun
girdiği bir ekranın kodunu geciktirmek, ilk yüklemeden kazanılanı gezinmede fazlasıyla
geri verir.

Takası ortadan kaldıran düzenek **önceden getirmedir**. Bir parça, gerekmeden önce, boşta
kalan zamanda indirilir. Yönlendirmenin bunun için iki iyi işareti vardır: ekranda görünür
hâle gelen bağlantılar ve kullanıcının üzerine geldiği bağlantı. Tarayıcı ve Web Platformu
kursunda tanıtılan kesişim gözlemcisi birincisini, işaretçi olayları ikincisini verir.

Önceden getirme, gerçek isteğin önüne geçmemelidir. Düşük öncelikli yapılır ve boşta
çalışma penceresinde tetiklenir; ölçülü ağ bağlantılarında ise hiç yapılmaz. Aksi hâlde
kullanıcının o an ihtiyaç duyduğu veriyle yarışır.

#### Bekleme ve Hata

Bölünmüş bir parçanın yüklenmesi eşzamansız bir işlemdir; dolayısıyla iki yeni durum doğar
ve ikisi de arayüzde karşılanmalıdır.

**Bekleme** durumu, parça inerken ne görüneceğini belirler. Boş bir ekran bırakmak,
kullanıcının gezinmenin çalışmadığını düşünmesine yol açar. Doğru davranış, düzen zincirinin
korunan bölümünü ekranda tutup yalnız değişen yuvaya bir gösterge koymaktır: kabuk yerinde
kaldığı için ekran sarsılmaz. Çok kısa beklemelerde göstergenin görünüp hemen kaybolması da
rahatsız edicidir; gösterge küçük bir gecikmeden sonra çıkarılır.

**Hata** durumu daha kolay unutulur. Ağ kesilmiş olabilir; parça sunucudan silinmiş
olabilir. İkinci durum sanıldığından sıradandır: kullanıcı sayfayı açıkken yeni bir sürüm
yayımlanırsa, eski sayfanın istediği parça adları artık bulunmaz. Bu, Eşzamansız JavaScript
ve Çalışma Zamanı kursundaki Hata ve Yeniden Deneme Kalıpları dersinin doğrudan
uygulamasıdır: yükleme başarısız olduğunda bir kez yeniden denenir, yine başarısız olursa
kullanıcıya sayfayı yenilemesini öneren bir hata görünümü gösterilir.

Üçüncü bir tuzak koruyucularla kesişir. Yetkisiz bir kullanıcının hiç göremeyeceği bir
ekranın parçası indirilmemelidir; koruyucu kararı, parça isteğinden **önce** verilir. Aksi
hâlde bölme, hem gereksiz bayt indirir hem de yetkisiz kullanıcıya o ekranın var olduğunu
bildirir.

#### Özet

- Bir rotanın maliyeti, bağımlılık çizgesinde ondan erişilen tüm modüllerin kapanışıdır.
- Modüller kullanım imzasına — kendilerini isteyen rotalar kümesine — göre gruplanır; aynı
  imzalı modüller aynı parçaya girer, böylece ne kopyalanır ne de gereksiz indirilir.
- Bölmenin kazancı girişin büyüklüğüyle sınırlıdır ve ağır modüllerde yoğunlaşır; her parça
  bir istek maliyeti getirdiği için kazanç sağlamayan bölme yapılmaz.
- Bölme, ilk yükleme süresini gezinme süresine dağıtır; boşta zamanda ve düşük öncelikle
  yapılan önceden getirme bu takası hafifletir.
- Parça yüklemesi eşzamansızdır: bekleme durumunda korunan kabuk ekranda tutulur, hata
  durumunda yeniden deneme ve açık bir hata görünümü gerekir.
- Koruyucu kararı parça isteğinden önce verilir; görülemeyecek bir ekranın kodu indirilmez.

#### Sonraki Adım

Yönlendirme tamamlandı: adres hangi ekranın görüneceğini, kime açık olduğunu ve hangi kodun
ineceğini belirliyor. Adresin taşıdığı bir durum türü de tanımlandı — süzgeç, sıralama,
sayfa. Uygulamanın geri kalan durumu ise hâlâ dağınık: oturum bilgisi koruyucularda,
istasyon listesi bir yerde, açık paneller bileşenlerin içinde, sunucudan gelen ölçümler bir
başka yerde. Bunların hepsine "durum" demek, aralarındaki en önemli farkı gizler: bazıları
uygulamanın kendi ürettiği, bazıları uzaktaki bir kaynağın kopyası. Sonraki konu durumu
türlerine ayırmakla başlar ve her türün nereye konacağını belirleyen ölçütleri kurar.

## Durum Yönetimi

Durumun türüne göre yerleştirilmesi; merkezî depo ve indirgeyiciler, atom tabanlı ince taneli tepkisellik, sunucu durumu önbelleği, iyimser güncelleme ve kalıcılaştırma.

### Durum Türleri

Source: https://academia.sh/tr/kurslar/frontend-mimarisi/durum-turleri

Durumun türlerine ayrılması; yerel, paylaşılan, URL ve sunucu durumu ile türetilmiş değer, her türü yerleştiren karar yordamı, tek doğru kaynak ilkesi ve yanlış yerleştirmenin maliyeti.

Yönlendirme tamamlandı: adres hangi ekranın görüneceğini, kime açık olduğunu ve hangi kodun
ineceğini belirliyor. Adresin taşıdığı bir durum türü de tanımlandı — süzgeç, sıralama,
sayfa. Kuzey Yamaç Ölçüm İstasyonu uygulamasının geri kalan durumu ise dağınık: oturum
bilgisi koruyucularda, istasyon listesi bir yerde, açık paneller bileşenlerin içinde,
sunucudan gelen ölçümler bir başka yerde.

Bunların hepsine "durum" demek, aralarındaki en önemli farkı gizler. Bazıları uygulamanın
kendi ürettiği ve tek sahibi olduğu değerlerdir; bazıları uzaktaki bir kaynağın
kopyasıdır. Bu ders durumu türlerine ayırır, her türün nereye konacağını belirleyen ölçütü
verir ve yanlış yerleştirmenin maliyetini gösterir.

#### Beş Tür

**Yerel durum** tek bir bileşenin okuduğu ve yazdığı değerdir: açılır listenin açık olup
olmadığı, bir metin kutusunun o anki içeriği. Bileşenle birlikte doğar, onunla birlikte
ölür.

**Paylaşılan durum** birden çok bileşenin okuduğu, uygulamanın kendi ürettiği değerdir:
arayüz teması, yan gezinmenin açıklığı, seçili birim sistemi. Sahibi uygulamadır; başka
kimse değiştirmez.

**URL durumu** adreste taşınan durumdur. Bir önceki konu bunu tanımladı: paylaşıldığında,
yer imine eklendiğinde ve geri düğmesiyle dönüldüğünde geri gelmesi beklenen durum.

**Sunucu durumu** uzaktaki bir kaynağın istemcideki kopyasıdır: istasyon listesi, ölçüm
kayıtları, oturum açmış kullanıcının bilgileri. Diğer dördünden ayıran özelliği sahipliğin
istemcide olmamasıdır.

**Türetilmiş değer** ise aslında durum değildir. Başka durumlardan hesaplanabilen her şey
bu sınıfa girer: süzülmüş listenin uzunluğu, seçili ögelerin toplamı, bir isteğin devam
edip etmediğini gösteren bayrak.

Bu beşli, bir karar yordamına çevrilebilir.

```js
// durum-turleri.mjs — durumu yerlestiren karar yordami ve turetilmis degerin saklanma maliyeti
function sinifla(k) {
  if (k.turetilebilir) return ["turetilmis", `${k.turetilebilir} degerinden hesaplanir`];
  if (k.kaynak === "sunucu") return ["sunucu", "sahibi uzakta, kopyasi tutulur"];
  if (k.adreste) return ["URL", "paylasildiginda geri gelmeli"];
  if (k.tuketici > 1) return ["paylasilan", `${k.tuketici} ayri tuketici okuyor`];
  return ["yerel", "tek bilesen okuyor"];
}

const OGELER = [
  { ad: "istasyon listesi", kaynak: "sunucu", tuketici: 2 },
  { ad: "olcum gecmisi sayfa no", adreste: true, tuketici: 2 },
  { ad: "secili olcum turu suzgeci", adreste: true, tuketici: 3 },
  { ad: "oturum acmis kullanici", kaynak: "sunucu", tuketici: 6 },
  { ad: "yan gezinme acik mi", tuketici: 2 },
  { ad: "arayuz temasi", tuketici: 5 },
  { ad: "form alaninin o anki degeri", tuketici: 1 },
  { ad: "suruklenen kaydiricinin anlik degeri", tuketici: 1 },
  { ad: "suzulmus olcum sayisi", turetilebilir: "olcumler + suzgec" },
  { ad: "yukleniyor gostergesi", turetilebilir: "sunucu durumu" },
];

console.log("-- yerlestirme --");
for (const o of OGELER) {
  const [sinif, gerekce] = sinifla(o);
  console.log(o.ad.padEnd(38), sinif.padEnd(12), gerekce);
}

// Turetilmis degeri saklamak ile her okumada hesaplamak.
const suz = (olcumler, tur) => olcumler.filter((o) => tur === "hepsi" || o.tur === tur);

let durum = {
  olcumler: [{ tur: "sicaklik", d: -4.2 }, { tur: "nem", d: 71 }, { tur: "sicaklik", d: -3.8 }],
  suzgec: "sicaklik",
  saklananSayi: 2,                       // el ile guncellenen turetilmis deger
};
const bas = (etiket) => console.log(
  etiket.padEnd(30),
  "saklanan:", String(durum.saklananSayi).padEnd(3),
  "hesaplanan:", suz(durum.olcumler, durum.suzgec).length);

console.log("-- turetilmis deger --");
bas("baslangic");

durum = { ...durum, olcumler: [...durum.olcumler, { tur: "sicaklik", d: -5.1 }] };
bas("yeni sicaklik olcumu eklendi");

durum = { ...durum, suzgec: "nem" };
bas("suzgec neme cevrildi");

durum = { ...durum, saklananSayi: suz(durum.olcumler, durum.suzgec).length };
bas("saklanan el ile duzeltildi");

durum = { ...durum, olcumler: durum.olcumler.filter((o) => o.tur !== "nem") };
bas("nem olcumleri silindi");
```

```
-- yerlestirme --
istasyon listesi                       sunucu       sahibi uzakta, kopyasi tutulur
olcum gecmisi sayfa no                 URL          paylasildiginda geri gelmeli
secili olcum turu suzgeci              URL          paylasildiginda geri gelmeli
oturum acmis kullanici                 sunucu       sahibi uzakta, kopyasi tutulur
yan gezinme acik mi                    paylasilan   2 ayri tuketici okuyor
arayuz temasi                          paylasilan   5 ayri tuketici okuyor
form alaninin o anki degeri            yerel        tek bilesen okuyor
suruklenen kaydiricinin anlik degeri   yerel        tek bilesen okuyor
suzulmus olcum sayisi                  turetilmis   olcumler + suzgec degerinden hesaplanir
yukleniyor gostergesi                  turetilmis   sunucu durumu degerinden hesaplanir
-- turetilmis deger --
baslangic                      saklanan: 2   hesaplanan: 2
yeni sicaklik olcumu eklendi   saklanan: 2   hesaplanan: 3
suzgec neme cevrildi           saklanan: 2   hesaplanan: 1
saklanan el ile duzeltildi     saklanan: 1   hesaplanan: 1
nem olcumleri silindi          saklanan: 1   hesaplanan: 0
```

Yordamın sıralaması rastgele değildir. Türetilebilirlik en başta sınanır, çünkü türetilmiş
bir değer hiçbir yere **konmaz**. Sunucu kaynağı ikinci sıradadır: sahiplik sorusu, kaç
bileşenin okuduğu sorusundan önce gelir. Adres sınaması paylaşımdan önce gelir, çünkü
adrese yazılan durum zaten uygulama genelinde okunabilir hâle gelir.

#### Türetilmiş Değeri Saklamanın Maliyeti

Çıktının ikinci bölümü tek bir kusuru adım adım gösterir. Süzülmüş ölçüm sayısı bir durum
alanında saklanıyor ve elle güncelleniyor.

Yeni ölçüm eklendiğinde saklanan değer ikide kalır, hesaplanan değer ise üçe çıkar. Süzgeç neme
çevrildiğinde ikisi arasındaki fark yön değiştirir. Saklanan değer elle düzeltildiğinde
ikisi bir an için uyuşur — ama bir sonraki değişiklikte yeniden ayrışır. Her yeni yazma
noktası, güncellenmesi unutulabilecek yeni bir yerdir.

Bu, tek bir kuralın örneğidir: **aynı bilgi iki yerde tutulmaz**. Türetilmiş değer
saklandığında iki doğru kaynak ortaya çıkar ve hangisinin doğru olduğu belirsizleşir.
Hesaplama maliyetinden endişe ediliyorsa çözüm saklamak değil, hesabı bellemektir; bu
Bileşen Tabanlı Arayüz Geliştirme kursundaki türetilmiş değerler dersinin konusudur ve
sonuç yine tek doğru kaynaktır.

Yükleniyor göstergesinin türetilmiş sayılması aynı gerekçeye dayanır. Ayrı bir bayrak
tutulduğunda istek bitip bayrağın kapatılmadığı durum kaçınılmazdır; hata yolunda,
iptalde, eşzamanlı ikinci istekte. Göstergenin doğru kaynağı isteğin kendi durumudur.

#### Sunucu Durumunun Ayrılığı

Diğer dördü uygulamanın kendi ürettiği değerlerdir: yazılan değer doğrudur, çünkü yazan
uygulamadır. Sunucu durumu için bu geçerli değildir. İstemcideki istasyon listesi, uzaktaki
listenin geçmiş bir andaki kopyasıdır. Başka bir kullanıcı yeni istasyon eklemiş, biri bir
ölçümü düzeltmiş olabilir.

Bu farktan dört soru doğar ve dördü de diğer durum türlerinde sorulmaz. Kopya ne kadar süre
güncel sayılır? Eskidiğinde ne zaman yenilenir? Aynı veriyi iki bileşen isterse kaç istek
yapılır? Yerel bir değişiklik sunucuya yazılırken ekranda ne görünür?

Sunucu durumunu paylaşılan durumla aynı yere koymak bu dört soruyu kaybettirir. Uygulama
kopyayı gerçek sanır: veri bir kez alınır, bellekte tutulur ve hiç tazelenmez. Kullanıcı
başka bir sekmede eklediği istasyonu listede göremez. Bu nedenle sunucu durumu ayrı bir
katmanda, kendi tazelik kurallarıyla tutulur; o katman bu konunun dördüncü dersinin
konusudur.

#### Yerleştirme İlkesi

Durumun nereye konacağını belirleyen ilke, olabildiğince **aşağıda** tutmaktır: yalnız tek
bir bileşenin ilgilendiği değer yukarı çıkarılmaz.

Bunun gerekçesi Bileşen Tabanlı Arayüz Geliştirme kursundaki tek yönlü akışın doğal
sonucudur. Yukarı çıkarılan her değer, aradaki bileşenlerin görmek zorunda olduğu bir
bağımlılık yaratır; ayrıca değişimi ondan etkilenmeyen bileşenlere de duyurur. Aşağıda
tutulan durum yalnız kendi bileşenini ilgilendirir.

Karşı yönde bir tuzak vardır: durumu gerekenden aşağıda tutmak. İki kardeş bileşen aynı
değeri kendi içlerinde tuttuğunda iki kopya oluşur ve ayrışırlar. Ölçüt sabittir: değeri
okuyan bileşenlerin **en yakın ortak atası**. Ondan aşağıda tutulamaz, ondan yukarıda
tutulması ise gereksizdir.

Bu ilke iki türde uygulanmaz. URL durumunun yeri zaten bellidir; sunucu durumu ise
bileşen ağacının hiçbir düğümüne ait değildir, çünkü aynı veriyi ağacın farklı yerlerindeki
bileşenler ister ve kopyanın tek olması gerekir.

#### Özet

- Durum beş sınıfa ayrılır: yerel, paylaşılan, URL, sunucu ve türetilmiş; ilk dördü
  saklanır, beşincisi hesaplanır.
- Karar yordamı önce türetilebilirliği, sonra sahipliği, sonra adresi, en son tüketici
  sayısını sorar.
- Türetilmiş değer saklandığında iki doğru kaynak oluşur ve her yazma noktasında yeniden
  ayrışma riski doğar; çözüm saklamak değil, gerektiğinde hesabı bellemektir.
- Sunucu durumunun sahibi istemci değildir; tazelik, yenileme, istek tekrarı ve yazma
  sırasında görünen durum soruları yalnız bu türde sorulur.
- İstemci durumu, onu okuyan bileşenlerin en yakın ortak atasında tutulur — daha aşağıda
  tutmak kopya üretir, daha yukarıda tutmak gereksiz bağımlılık yaratır.

#### Sonraki Adım

Türler ayrıldı ama paylaşılan durumun **nasıl** tutulacağı henüz açık değil. Ortak atada
tutulan bir değer, ağaç derinleştikçe onu kullanmayan onlarca bileşenin arasından geçirilir;
üstelik bu değeri kimin, hangi olayda değiştirdiği kodun içine dağılır. İstasyon listesinde
bir satırın seçilmesi, bir ölçümün silinmesi, oturumun kapatılması — üçü de aynı durumu
farklı yerlerden değiştirir ve bir hata çıktığında değişikliği kimin yaptığı izlenemez.
Sonraki ders durumu tek bir yerde toplayan, değişimi yalnız adlandırılmış eylemlerle kabul
eden ve geçişleri saf bir işlevle tanımlayan merkezî depo kalıbını ele alır.

### Merkezî Depo Kalıbı

Source: https://academia.sh/tr/kurslar/frontend-mimarisi/merkezi-depo-kalibi

Durumu tek yerde toplayan kalıp; tek yönlü veri akışı, saf indirgeyici, eylem sözleşmesi, seçicilerle gereksiz bildirimin süzülmesi, ara katmanların yeri ve kalıbın dolaylılık bedeli.

Durum türleri ayrıldı ama paylaşılan durumun nasıl tutulacağı açık değil. Ortak atada
tutulan bir değer, ağaç derinleştikçe onu kullanmayan onlarca bileşenin arasından
geçirilir. Daha ağır sorun, değişimin dağınıklığıdır: Kuzey Yamaç Ölçüm İstasyonu
uygulamasında bir satırın seçilmesi, bir ölçümün silinmesi ve oturumun kapatılması aynı
durumu üç ayrı yerden değiştirir. Ekranda yanlış bir değer göründüğünde onu kimin yazdığı
izlenemez.

**Merkezî depo** kalıbı bu iki soruna tek bir kısıtla yanıt verir: durum tek bir yerde
tutulur ve yalnız adlandırılmış eylemlerle değişir.

#### Tek Yönlü Akış

Kalıbın omurgası dört durakta kapanan bir çevrimdir. Görünüm bir **eylem** gönderir; eylem
**indirgeyiciye** verilir; indirgeyici yeni durumu üretir; yeni durum görünüme akar.

Bu çevrimin dışında durumu değiştirmenin yolu yoktur. Kısıt, kalıbın bütün kazançlarının
kaynağıdır: değişimlerin tümü tek bir noktadan geçtiği için sıralanabilir, kaydedilebilir
ve yeniden oynatılabilir.

Üç parça ayrı ayrı tanımlanır. **Eylem**, olmuş bir olayı bildiren veri nesnesidir; bir
türü ve gerektiği kadar alanı vardır. **İndirgeyici**, o anki durumla eylemi alıp yeni
durumu döndüren saf bir işlevdir. **Depo**, o anki durumu tutan, eylem göndermeyi ve
değişimi izlemeyi sağlayan kaptır.

#### İndirgeyici

İndirgeyici saf bir işlevdir: aynı girdi her zaman aynı çıktıyı verir ve dışarıda hiçbir şey
değiştirmez. Ağ isteği yapmaz, rastgele sayı üretmez, o anki zamanı okumaz.

Durumu **değiştirmez**, yenisini üretir. Bu, Programlama Temelleri kursundaki
değiştirilemezlik kuralının doğrudan uygulanmasıdır ve pratik bir sonucu vardır: bir
dilimin referansı değişmediyse o dilim değişmemiştir. Bu ucuz sınama, görünümün ne zaman
yeniden çizileceğini belirler.

```js
// merkezi-depo.mjs — indirgeyici, eylem dizisi, ara katman, secici ve yeniden oynatma
const BASLANGIC = { istasyonlar: [], secili: null, suzgec: "hepsi" };

function indirgeyici(durum, eylem) {
  switch (eylem.tur) {
    case "istasyonlar/yuklendi":
      return { ...durum, istasyonlar: eylem.veri };
    case "istasyon/secildi":
      return durum.istasyonlar.some((i) => i.kimlik === eylem.kimlik)
        ? { ...durum, secili: eylem.kimlik }
        : durum;                                   // gecersiz secim durumu degistirmez
    case "istasyon/eklendi":
      return { ...durum, istasyonlar: [...durum.istasyonlar, eylem.istasyon] };
    case "istasyon/silindi":
      return {
        ...durum,
        istasyonlar: durum.istasyonlar.filter((i) => i.kimlik !== eylem.kimlik),
        secili: durum.secili === eylem.kimlik ? null : durum.secili,
      };
    case "suzgec/degisti":
      return { ...durum, suzgec: eylem.deger };
    default:
      return durum;                                // bilinmeyen eylem: ayni referans
  }
}

function depoKur(indirgeyici, baslangic, araKatmanlar = []) {
  let durum = baslangic;
  const dinleyiciler = new Set();
  const temel = (eylem) => {
    durum = indirgeyici(durum, eylem);
    for (const d of dinleyiciler) d(durum);
  };
  const gonder = araKatmanlar.reduceRight(
    (sonraki, katman) => katman({ oku: () => durum })(sonraki), temel);
  return { oku: () => durum, gonder, izle: (f) => dinleyiciler.add(f) };
}

const gunluk = [];
const kutukKatmani = ({ oku }) => (sonraki) => (eylem) => {
  const onceki = oku();
  sonraki(eylem);
  gunluk.push({ eylem: eylem.tur, degisti: oku() !== onceki, durum: oku() });
};

const EYLEMLER = [
  { tur: "istasyonlar/yuklendi", veri: [{ kimlik: "kuzey-yamac" }, { kimlik: "dogu-sirt" }] },
  { tur: "istasyon/secildi", kimlik: "kuzey-yamac" },
  { tur: "suzgec/degisti", deger: "sicaklik" },
  { tur: "istasyon/secildi", kimlik: "bati-vadi" },     // listede yok
  { tur: "istasyon/eklendi", istasyon: { kimlik: "bati-vadi" } },
  { tur: "istasyon/secildi", kimlik: "bati-vadi" },
  { tur: "istasyon/silindi", kimlik: "bati-vadi" },
  { tur: "tema/degisti", deger: "koyu" },               // indirgeyici tanimiyor
];

const depo = depoKur(indirgeyici, BASLANGIC, [kutukKatmani]);

// Secici: bilesenin ilgilendigi dilim. Referans degismediyse yeniden cizim gerekmez.
const seciciSayaci = { bildirim: 0, listeDegisti: 0 };
let oncekiListe = depo.oku().istasyonlar;
depo.izle((d) => {
  seciciSayaci.bildirim += 1;
  if (d.istasyonlar !== oncekiListe) { seciciSayaci.listeDegisti += 1; oncekiListe = d.istasyonlar; }
});

for (const e of EYLEMLER) depo.gonder(e);

console.log("-- eylem gunlugu --");
console.log("#".padEnd(3), "eylem".padEnd(24), "degisti".padEnd(8), "istasyon", "secili");
gunluk.forEach((g, i) => console.log(
  String(i + 1).padEnd(3), g.eylem.padEnd(24), String(g.degisti).padEnd(8),
  String(g.durum.istasyonlar.length).padEnd(8), String(g.durum.secili)));

console.log("-- dinleyici --");
console.log("gonderilen eylem:", EYLEMLER.length,
  "| bildirim:", seciciSayaci.bildirim,
  "| liste dilimi degisti:", seciciSayaci.listeDegisti);

// Indirgeyici saf oldugu icin ayni dizi ayni sonucu verir.
console.log("-- yeniden oynatma --");
const yeniden = EYLEMLER.reduce(indirgeyici, BASLANGIC);
console.log("son durum ayni mi:", JSON.stringify(yeniden) === JSON.stringify(depo.oku()));
console.log("3. eylemden sonrasi:", JSON.stringify(EYLEMLER.slice(0, 3).reduce(indirgeyici, BASLANGIC)));
console.log("son durum          :", JSON.stringify(depo.oku()));
```

```
-- eylem gunlugu --
#   eylem                    degisti  istasyon secili
1   istasyonlar/yuklendi     true     2        null
2   istasyon/secildi         true     2        kuzey-yamac
3   suzgec/degisti           true     2        kuzey-yamac
4   istasyon/secildi         false    2        kuzey-yamac
5   istasyon/eklendi         true     3        kuzey-yamac
6   istasyon/secildi         true     3        bati-vadi
7   istasyon/silindi         true     2        null
8   tema/degisti             false    2        null
-- dinleyici --
gonderilen eylem: 8 | bildirim: 8 | liste dilimi degisti: 3
-- yeniden oynatma --
son durum ayni mi: true
3. eylemden sonrasi: {"istasyonlar":[{"kimlik":"kuzey-yamac"},{"kimlik":"dogu-sirt"}],"secili":"kuzey-yamac","suzgec":"sicaklik"}
son durum          : {"istasyonlar":[{"kimlik":"kuzey-yamac"},{"kimlik":"dogu-sirt"}],"secili":null,"suzgec":"sicaklik"}
```

Günlüğün dördüncü ve sekizinci satırları aynı davranışı iki nedenle gösterir. Listede
bulunmayan bir istasyonun seçilmesi reddedilir; indirgeyici o anki durumu **aynı referansla**
geri döndürür. Tanınmayan eylem türü de aynı biçimde ele alınır. Her iki durumda da hiçbir
görünümün yeniden çizilmesi gerekmez ve bunu anlamak için tek bir referans karşılaştırması
yeter.

Yedinci satır tek eylemin birden çok dilimi etkileyebildiğini gösterir. Seçili istasyon
silindiğinde hem liste kısalır hem de seçim boşaltılır; bu iki değişiklik ayrı eylemlere
bölünmez, çünkü aralarında ekranın tutarsız kalacağı bir an oluşmamalıdır. Değişmez şudur:
seçili kimlik ya boştur ya da listede bulunur.

#### Yeniden Oynatma

Son bölüm saflığın karşılığını gösterir. Eylem dizisi baştan indirgendiğinde depoda oluşan
durumun aynısı elde edilir. Depoya ihtiyaç bile yoktur; durum, başlangıç değeri ile eylem
dizisinin bir fonksiyonudur.

Bunun üç kullanımı vardır. Kullanıcıdan gelen bir hata bildirimi eylem günlüğüyle birlikte
alındığında sorun yerel olarak yeniden üretilebilir. Herhangi bir ana dönmek, o ana kadarki
eylemleri yeniden indirgemekle olur — çıktıda üçüncü eylemden sonraki durum böyle
üretilmiştir. Ve geri alma işlevi, silinen eylemi çıkarıp diziyi yeniden indirgeyerek
kurulur.

Bu olanaklar indirgeyicinin saflığına bağlıdır. İçine o anki zamanı okuyan tek bir satır
konduğunda yeniden oynatma farklı sonuç verir ve üçü birden kaybolur.

#### Seçici

Dinleyici bölümü kalıbın en sık atlanan maliyetini sayıyla verir: sekiz eylem gönderilmiş,
sekiz bildirim üretilmiş, ama istasyon listesi yalnız üç kez değişmiştir.

Depo, durumun hangi bölümünün değiştiğini bilmez; her gönderimde tüm dinleyicilere haber
verir. Bileşenlerin bu bildirimleri süzmesi gerekir. **Seçici**, durumdan bileşenin
ilgilendiği dilimi çıkaran işlevdir; dilimin referansı değişmediyse yeniden çizim yapılmaz.

Seçicinin bir tuzağı vardır. Her çağrıda yeni bir nesne ya da dizi üreten seçici — süzme,
eşleme veya nesne toplama yapan bir seçici — referans karşılaştırmasını her zaman
başarısız kılar ve süzgeç hiç çalışmaz. Bu tür seçiciler girdileri değişmediği sürece aynı
sonucu döndürecek biçimde bellenir.

#### Ara Katman

İndirgeyici saf olmak zorundaysa, ağ isteği, kütüğe yazma ve depolamaya kaydetme nereye
konur? **Ara katman**, eylem gönderme yolunun üzerine yerleşen ve eylemi indirgeyiciye
ulaşmadan önce ya da ulaştıktan sonra işleyen sarmalayıcıdır.

Örnekteki kütük katmanı eylemi geçirir, sonra öncesi ve sonrasındaki durumu karşılaştırıp
kaydeder. Aynı yerde başka sorumluluklar da durur: bir eylemi eşzamansız bir işleme
çevirmek, belirli eylemlerden sonra durumu depolamaya yazmak, gelen eylemi doğrulamak.

Ara katmanların sırası anlamlıdır: dıştaki katman içtekini sarar. Kütük katmanı en dışta
durursa eşzamansız katmanın ürettiği eylemleri de görür; en içte durursa yalnız
indirgeyiciye ulaşanları.

#### Kalıbın Bedeli

Merkezî depo bedelsiz değildir. Bir alanı değiştirmek için eylem türü tanımlanır,
indirgeyiciye dal eklenir, gönderme yazılır; üç dosyaya dokunulur. Bu dolaylılık tek bir
şey karşılığında alınır: değişimin izlenebilirliği. İzlenebilirliğin değeri olmadığı yerde
bedel de gereksizdir.

Buradan iki sınır çıkar. Yerel durum depoya konmaz; bir açılır listenin açıklığı için eylem
tanımlamak, kazanç getirmeyen bir dolaylılıktır. Sunucu durumu da konmaz — kopyalanabilir
ama tazelik, yeniden getirme ve istek tekrarı soruları indirgeyicinin çözemeyeceği
sorulardır; o katman bu konunun dördüncü dersinin konusudur.

Depoya giren şey, uygulamanın kendi ürettiği ve birden çok yerden değiştirilen paylaşılan
durumdur.

#### Özet

- Merkezî depo kalıbı durumu tek yerde toplar ve değişimi yalnız adlandırılmış eylemlerle
  kabul eder; akış görünüm, eylem, indirgeyici ve durum arasında tek yönde kapanır.
- İndirgeyici saf bir işlevdir ve durumu değiştirmeden yenisini üretir; değişmeyen dilimin
  referansı korunur, bu da yeniden çizim kararını tek bir karşılaştırmaya indirir.
- Saflık, eylem dizisinden durumu yeniden üretmeyi sağlar: hata yeniden oynatılabilir,
  geçmiş bir ana dönülebilir, geri alma kurulabilir.
- Depo her gönderimde tüm dinleyicilere haber verir; gereksiz çizimi seçiciler süzer ve
  yeni nesne üreten seçiciler bellenmezse süzgeç çalışmaz.
- Yan etkiler ara katmanlarda durur; katman sırası hangi eylemlerin görüleceğini belirler.
- Kalıbın bedeli dolaylılıktır; yerel durum ve sunucu durumu depoya konmaz.

#### Sonraki Adım

Merkezî depo tek bir doğru kaynak kurdu, ama bunun bir bedeli çıktı: her değişiklik tek bir
durum nesnesini yeniler ve tüm dinleyicilere duyurulur. Gereksiz çizim, doğru yazılmış
seçicilerle önlenir — yani doğruluk, her tüketicinin kendi süzgecini kusursuz kurmasına
bağlıdır. Bu sorumluluğun ters çevrildiği bir yaklaşım daha vardır: durumu tek bir büyük
nesne olarak değil, birbirine bağlı küçük birimler olarak tanımlamak. O zaman kimin neyi
okuduğu bağımlılıktan kendiliğinden çıkar ve yalnız etkilenen tüketiciler haberdar edilir.
Sonraki ders bu ince taneli modeli, atomları ve türetilmiş atomları ele alır.

### Atom Tabanlı Yaklaşımlar

Source: https://academia.sh/tr/kurslar/frontend-mimarisi/atom-tabanli-yaklasimlar

İnce taneli tepkisel durum; atom ve türetilmiş atom, bağımlılık çizgesi, kirletme yayılımı ile tembel hesaplama, yeniden hesap ve uyarı sayısının merkezî depoyla karşılaştırılması, çizge modelinin bedeli.

Merkezî depo tek bir doğru kaynak kurdu, ama bir bedeli çıktı: her değişiklik tek bir durum
nesnesini yeniler ve tüm dinleyicilere duyurulur. Gereksiz çizimi seçiciler önler — yani
doğruluk, her tüketicinin kendi süzgecini kusursuz kurmasına bağlıdır. Bir seçici
unutulduğunda ya da her çağrıda yeni nesne ürettiğinde kusur sessizdir; ekran doğru
görünür, yalnız gereğinden çok çizilir.

Sorumluluğu ters çeviren bir model daha vardır. Durum tek bir büyük nesne olarak değil,
birbirine bağlı küçük birimler olarak tanımlanır. O zaman kimin neyi okuduğu bağımlılıktan
kendiliğinden çıkar ve süzme işi tüketiciden çizgeye geçer.

#### Atom ve Türetilmiş Atom

**Atom**, bağımsız olarak okunup yazılabilen en küçük durum birimidir. Kuzey Yamaç Ölçüm
İstasyonu uygulamasında ölçüm listesi bir atom, süzgeç ölçütü bir atom, sıralama yönü bir
atom, arayüz teması bir atomdur. Aralarında kapsayan bir nesne yoktur; her biri kendi
başına durur.

**Türetilmiş atom** ise yazılamaz; girdilerinden hesaplanır. Süzülmüş liste, sıralanmış
liste ve özet birer türetilmiş atomdur. Durum Türleri dersindeki kural burada bir birim
olarak somutlaşır: türetilmiş değer saklanmaz, tanımlanır.

İkisi birlikte bir **bağımlılık çizgesi** kurar. Kenarlar okuma yönünde çizilir: süzülmüş
liste, ölçüm listesini ve süzgeci okur. Görünümler de bu çizgenin yaprakları gibi davranır;
okudukları düğümlere bağlanırlar.

Çizgenin işleyişi iki adımlıdır. Bir atom yazıldığında ondan erişilen tüm türetilmiş
düğümler **kirletilir** — değerleri artık geçerli sayılmaz. Kirlenmiş bir düğümün değeri
**okunduğunda** yeniden hesaplanır. Yazma anında hesap yapılmaz.

```js
// atom-cizgesi.mjs — atomlar, turetilmis atomlar, kirletme yayilimi ve yeniden hesap sayimi
const hesaplananlar = [];

function atom(ad, deger) {
  return { ad, tur: "atom", deger, bagimli: new Set() };
}

function turetilmis(ad, girdiler, hesap) {
  const d = { ad, tur: "turetilmis", girdiler, hesap, kirli: true, deger: undefined, bagimli: new Set() };
  for (const g of girdiler) g.bagimli.add(d);
  return d;
}

function tuketici(ad, girdiler) {
  const t = { ad, tur: "tuketici", girdiler, bagimli: new Set() };
  for (const g of girdiler) g.bagimli.add(t);
  return t;
}

function oku(d) {
  if (d.tur === "atom") return d.deger;
  if (d.kirli) {
    d.deger = d.hesap(...d.girdiler.map(oku));
    d.kirli = false;
    hesaplananlar.push(d.ad);
  }
  return d.deger;
}

function yaz(a, deger, etkin) {
  hesaplananlar.length = 0;
  a.deger = deger;
  const uyarilan = new Set();
  const kirlet = (d) => {
    for (const b of d.bagimli) {
      if (b.tur === "tuketici") { uyarilan.add(b.ad); continue; }
      if (b.kirli) continue;                      // zaten kirli, yayilim durur
      b.kirli = true;
      kirlet(b);
    }
  };
  kirlet(a);
  const cizilen = etkin.filter((t) => uyarilan.has(t.ad));   // ekranda olmayan uyarilmaz
  for (const t of cizilen) t.girdiler.forEach(oku);
  return { uyarilan: cizilen.map((t) => t.ad), hesaplanan: [...hesaplananlar] };
}

const olcumler = atom("olcumler", [
  { tur: "sicaklik", d: -4.2 }, { tur: "nem", d: 71 }, { tur: "sicaklik", d: -3.8 }]);
const suzgec = atom("suzgec", "hepsi");
const sira = atom("sira", "azalan");
const tema = atom("tema", "acik");

const suzulmus = turetilmis("suzulmus", [olcumler, suzgec],
  (o, s) => s === "hepsi" ? o : o.filter((x) => x.tur === s));
const siralanmis = turetilmis("siralanmis", [suzulmus, sira],
  (o, s) => [...o].sort((a, b) => s === "artan" ? a.d - b.d : b.d - a.d));
const ozet = turetilmis("ozet", [suzulmus],
  (o) => ({ sayi: o.length, ortalama: o.length ? o.reduce((t, x) => t + x.d, 0) / o.length : null }));
const basliklar = turetilmis("basliklar", [tema], (t) => ({ arkaPlan: t === "koyu" ? "#111" : "#fff" }));

const Liste = tuketici("Liste", [siralanmis]);
const OzetKarti = tuketici("OzetKarti", [ozet]);
const Baslik = tuketici("Baslik", [basliklar]);

let etkin = [Liste, OzetKarti, Baslik];
etkin.forEach((t) => t.girdiler.forEach(oku));          // ilk cizim

console.log("-- yazma basina uyarilan tuketici ve yeniden hesap --");
const adimlar = [
  ["tema = koyu", () => yaz(tema, "koyu", etkin)],
  ["sira = artan", () => yaz(sira, "artan", etkin)],
  ["suzgec = sicaklik", () => yaz(suzgec, "sicaklik", etkin)],
  ["olcumler += kayit", () => yaz(olcumler, [...olcumler.deger, { tur: "sicaklik", d: -5.1 }], etkin)],
];
let toplamUyari = 0, toplamHesap = 0;
for (const [etiket, calistir] of adimlar) {
  const { uyarilan, hesaplanan } = calistir();
  toplamUyari += uyarilan.length; toplamHesap += hesaplanan.length;
  console.log(etiket.padEnd(20), "uyarilan:", (uyarilan.join(", ") || "-").padEnd(26),
    "hesaplanan:", hesaplanan.join(", ") || "-");
}

console.log("-- OzetKarti ekrandan kaldirildi --");
etkin = [Liste, Baslik];
const s = yaz(suzgec, "nem", etkin);
toplamUyari += s.uyarilan.length; toplamHesap += s.hesaplanan.length;
console.log("suzgec = nem".padEnd(20), "uyarilan:", (s.uyarilan.join(", ") || "-").padEnd(26),
  "hesaplanan:", s.hesaplanan.join(", ") || "-");
console.log("ozet kirli mi:", ozet.kirli);

console.log("-- toplam --");
console.log("ince taneli  : uyari", toplamUyari, "| yeniden hesap", toplamHesap);
console.log("tek nesne    : uyari", 5 * 3, "| (her yazmada her tuketici)");
```

```
-- yazma basina uyarilan tuketici ve yeniden hesap --
tema = koyu          uyarilan: Baslik                     hesaplanan: basliklar
sira = artan         uyarilan: Liste                      hesaplanan: siralanmis
suzgec = sicaklik    uyarilan: Liste, OzetKarti           hesaplanan: suzulmus, siralanmis, ozet
olcumler += kayit    uyarilan: Liste, OzetKarti           hesaplanan: suzulmus, siralanmis, ozet
-- OzetKarti ekrandan kaldirildi --
suzgec = nem         uyarilan: Liste                      hesaplanan: suzulmus, siralanmis
ozet kirli mi: true
-- toplam --
ince taneli  : uyari 7 | yeniden hesap 10
tek nesne    : uyari 15 | (her yazmada her tuketici)
```

Her satır tek bir yazmanın etkisini gösterir. Tema değiştiğinde yalnız başlık düğümü yeniden
hesaplanır; ölçüm listesine dokunulmaz. Sıralama yönü değiştiğinde süzülmüş liste yerinde kalır
ve yalnız sıralama yeniden yapılır — süzme işi tekrarlanmaz. Süzgeç değiştiğinde ise üç düğüm
birden kirlenir, çünkü ikisi süzülmüş listeye bağlıdır.

Yayılımın bir ayrıntısı koda yazılıdır: zaten kirli olan düğümde yayılım durur. Aynı yazma
sırasında bir düğüme iki yoldan ulaşılsa bile bir kez işaretlenir ve bir kez hesaplanır.

#### Tembel Hesaplama

Özet kartı ekrandan kaldırıldıktan sonra yapılan yazma modelin ikinci kazancını gösterir.
Süzülmüş liste ve sıralanmış liste yeniden hesaplanır, çünkü liste hâlâ ekrandadır. Özet
düğümü ise yalnız **kirletilmiş**, hesaplanmamıştır; çıktının son satırı bunu doğrular.

Bunun nedeni hesabın yazmada değil okumada yapılmasıdır. Kimsenin okumadığı bir türetilmiş
değer hiç hesaplanmaz. Özet kartı yeniden ekrana geldiğinde okuma gerçekleşir ve değer o
anda üretilir.

Bu davranış Programlama Temelleri kursundaki tembel değerlendirmenin tepkisel karşılığıdır
ve pahalı türetmelerde tek başına belirleyici olabilir: bin satırlık bir tabloyu gruplayan
bir hesap, o tablo görünmediği sürece hiç çalışmaz.

#### Kaba Taneli ile Karşılaştırma

Son iki satır iki modeli aynı senaryoda karşılaştırır. Beş yazmada ince taneli çizge yedi
tüketici uyarısı üretmiştir; tek nesneli depo, seçici kullanılmadığında on beş üretirdi.

Fark, seçicilerle kapatılabilir bir farktır — ama kapatılması gereken bir farktır. Atom
modelinde süzgeç, bağımlılığın kendisinden çıkar: bir tüketici neyi okuduysa ona bağlıdır.
Merkezî depoda ise süzgeç elle yazılır ve yanlış yazıldığında kimse uyarmaz.

Karşılığında kaybedilen şey de vardır. Merkezî depoda tüm değişimler tek bir eylem
akışından geçer; sıralanabilir, kaydedilebilir, yeniden oynatılabilir. Atom çizgesinde
yazma doğrudan atoma yapılır; "bu değeri kim değiştirdi" sorusunun tek bir yanıt noktası
yoktur. Eylem günlüğü ve yeniden oynatma bu modelde kendiliğinden gelmez.

#### Çizgenin Bedeli

İnce tanelilik üç yeni sorun getirir.

**Dağınıklık.** Durum tanımı tek bir yerde toplanmaz; atomlar kullanıldıkları yerlerin
yanına dağılır. Küçük uygulamada bu bir kazançtır. Büyüdükçe, bir atomun kim tarafından
yazıldığını bulmak dosya taramaya dönüşür. Karşı önlem, yazma işlemlerini adlandırılmış
işlevlerin arkasına almak ve atomu doğrudan yazılabilir bırakmamaktır.

**Bağımlılık döngüsü.** Bir türetilmiş atomun dolaylı olarak kendini okuması, kirletme
yayılımını sonsuz döngüye sokar. Örnekteki kirletme yayılımı buna karşı kısmen korunur —
zaten kirli düğümde durur — ama okuma tarafı sonsuz özyinelemeye girer. Çizge, kurulurken
çevrimsizlik açısından denetlenmelidir; Veri Yapıları kursundaki yönlü çevrimsiz çizge
kavramı burada bir kısıttır.

**Kimlik.** Liste ögesi başına atom gerektiğinde — her ölçüm satırının kendi seçili durumu
gibi — atomlar bir aile oluşturur ve anahtarla üretilir. Bu ailenin temizlenmesi elle
yapılır; ekrandan kalkan ögenin atomu bırakılmazsa bellek sızıntısı oluşur. Eşzamansız
JavaScript ve Çalışma Zamanı kursundaki bellek sızıntısı tanısı bu durumda doğrudan
uygulanır.

#### Hangi Model Nerede

İki model birbirinin alternatifi değil, farklı ölçütlere göre seçilen araçlardır.

Merkezî depo, değişimin **izlenebilirliği** birinci öncelikse tercih edilir: karmaşık iş
kuralları, geri alma gereksinimi, çok adımlı akışlar, hata bildirimiyle birlikte alınan
eylem günlüğü.

Atom modeli, **granülerlik** birinci öncelikse tercih edilir: birbirinden bağımsız çok
sayıda küçük durum, pahalı türetmeler, sık güncellenen ve az sayıda tüketicisi olan
değerler.

İkisi aynı uygulamada birlikte de bulunabilir. Ölçüt kalıbın kendisi değil, o durumun
hangi soruyu doğurduğudur.

#### Özet

- Atom bağımsız okunup yazılabilen en küçük durum birimidir; türetilmiş atom yazılamaz,
  girdilerinden hesaplanır ve saklanmaz.
- Yazma, çizgede erişilen düğümleri kirletir; hesap yazmada değil okumada yapılır, böylece
  kimsenin okumadığı türetilmiş değer hiç hesaplanmaz.
- Kirletme yayılımı zaten kirli düğümde durduğu için bir yazmada her düğüm en çok bir kez
  hesaplanır.
- İnce taneli çizgede süzme işi bağımlılığın kendisinden çıkar; merkezî depoda aynı sonuç
  elle yazılan seçicilerle elde edilir ve yanlış yazıldığında sessizce kaybolur.
- Atom modelinde tek bir eylem akışı olmadığı için eylem günlüğü, yeniden oynatma ve geri
  alma kendiliğinden gelmez.
- Model seçimi ölçüte bağlıdır: izlenebilirlik merkezî depoyu, granülerlik atom çizgesini
  öne çıkarır; ikisi bir arada da kullanılabilir.

#### Sonraki Adım

Bu iki modelin ortak bir varsayımı var: durumun sahibi uygulamadır, yazılan değer doğrudur.
Kuzey Yamaç Ölçüm İstasyonu uygulamasının en büyük durum kütlesi ise bu varsayımı
karşılamıyor. İstasyon listesi ve ölçüm kayıtları uzaktaki bir kaynağın kopyası; başka bir
kullanıcı yeni istasyon eklemiş, biri bir ölçümü düzeltmiş olabilir. Bu durumun sorusu
"nerede tutulacağı" değil, "ne kadar süre güncel sayılacağı". Sonraki ders sunucu durumunu
kendi katmanında ele alır: önbellek anahtarı, tazelik penceresi, aynı veriyi isteyen iki
bileşenin tek isteğe indirgenmesi, geçersizleştirme ve yeniden getirme.

### Sunucu Durumu Yönetimi

Source: https://academia.sh/tr/kurslar/frontend-mimarisi/sunucu-durumu-yonetimi

Uzak verinin istemcideki kopyası; önbellek anahtarı, tazelik penceresi, uçuşta birleşme, bayat sunup arkada yenileme, yazma sonrası geçersizleştirme, saklama süresi ve yeniden getirme tetikleyicileri.

Merkezî depo ve atom çizgesi aynı varsayımı paylaşıyor: durumun sahibi uygulamadır, yazılan
değer doğrudur. Kuzey Yamaç Ölçüm İstasyonu uygulamasının en büyük durum kütlesi bu
varsayımı karşılamıyor. İstasyon listesi ve ölçüm kayıtları uzaktaki bir kaynağın
kopyası; başka bir kullanıcı yeni istasyon eklemiş, biri bir ölçümü düzeltmiş olabilir.

Durum Türleri dersinde bu türün dört ayrı soru doğurduğu söylenmişti: kopya ne kadar süre
güncel sayılır, eskiyince ne zaman yenilenir, aynı veriyi iki bileşen isterse kaç istek
yapılır, yerel değişiklik yazılırken ekranda ne görünür. İlk üçü bu dersin, dördüncüsü bir
sonrakinin konusudur.

#### Anahtar

Sunucu durumu katmanının birinci kararı, bir kopyanın neyle anılacağıdır. **Önbellek
anahtarı**, isteği tümüyle belirleyen değerlerden üretilir: kaynağın yolu ve isteği
farklılaştıran her parametre.

Anahtarın eksik olması iki kopyayı birbirine karıştırır: süzgeci hesaba katmayan bir
anahtar, sıcaklık ölçümleriyle nem ölçümlerini aynı girdide tutar. Anahtarın gereğinden
geniş olması ise paylaşımı engeller; isteği etkilemeyen bir değer anahtara girerse her
görünüm kendi kopyasını indirir.

Rota Parametreleri ve Sorgu dersindeki kanonik biçim burada karşılığını bulur. Aynı görünüm
tek bir adresle anıldığı için, adresten türetilen anahtar da tektir; parametre sırası
değiştiğinde ikinci bir önbellek girdisi oluşmaz.

#### Tazelik ve Saklama

İkinci karar, kopyanın ne kadar süre sorgusuz kullanılacağıdır. İki ayrı süre gerekir ve
karıştırılmaları sık görülen bir kusurdur.

**Tazelik penceresi**, kopyanın yeniden istenmeden kullanılabileceği süredir. Pencere
içindeki bir okuma ağa hiç çıkmaz. **Saklama süresi** ise kopyanın bellekte tutulacağı
süredir; bu süre dolduğunda kayıt düşürülür.

İkisinin arasında kalan aralıkta kopya **bayattır**: kullanıcıya hemen gösterilir, aynı anda
arka planda yenilenir. Ekran boş kalmaz, veri de eskimiş kalmaz.

```js
// sunucu-durumu.mjs — tazelik penceresi, ucusta birlesme, gecersizlestirme, saklama suresi
const TAZELIK = 30;      // saniye: bu sure icinde kopya taze sayilir
const SAKLAMA = 120;     // saniye: bu sureden sonra kayit dusurulur

const onbellek = new Map();   // anahtar -> { veri, alindi, gecersiz }
const ucusta = new Map();     // anahtar -> istek numarasi
let istekNo = 0;

function iste(anahtar, saat) {                 // saat: disaridan verilen sanal zaman
  const kayit = onbellek.get(anahtar);
  const yas = kayit ? saat - kayit.alindi : null;
  if (kayit && yas > SAKLAMA) { onbellek.delete(anahtar); }
  const gecerliKayit = onbellek.get(anahtar);

  if (gecerliKayit && !gecerliKayit.gecersiz && saat - gecerliKayit.alindi <= TAZELIK)
    return { sonuc: "onbellek isabeti", yas: saat - gecerliKayit.alindi, istek: "-" };

  if (ucusta.has(anahtar))
    return { sonuc: "ucusta birlestirildi", yas, istek: `#${ucusta.get(anahtar)}` };

  ucusta.set(anahtar, ++istekNo);
  const sonuc = gecerliKayit
    ? "bayat sunuldu + yenileniyor"
    : (kayit ? "kayit dusuruldu, bekleme" : "veri yok, bekleme");
  return { sonuc, yas, istek: `#${istekNo}` };
}

function tamamla(anahtar, saat, veri) {
  onbellek.set(anahtar, { veri, alindi: saat, gecersiz: false });
  const n = ucusta.get(anahtar);
  ucusta.delete(anahtar);
  return { sonuc: "yanit yazildi", yas: 0, istek: `#${n}` };
}

function gecersizlestir(onEk) {                // yazma isleminden sonra cagrilir
  let sayi = 0;
  for (const [anahtar, kayit] of onbellek)
    if (anahtar.startsWith(onEk)) { kayit.gecersiz = true; sayi += 1; }
  return { sonuc: `${sayi} kayit gecersizlestirildi`, yas: null, istek: "-" };
}

const LISTE = ["kuzey-yamac", "dogu-sirt"];
const ADIMLAR = [
  [0, "Liste acildi", "istasyon/liste", (t, a) => iste(a, t)],
  [1, "OzetKarti ayni veriyi istedi", "istasyon/liste", (t, a) => iste(a, t)],
  [3, "yanit geldi", "istasyon/liste", (t, a) => tamamla(a, t, LISTE)],
  [5, "sekmeye geri donuldu", "istasyon/liste", (t, a) => iste(a, t)],
  [40, "sekmeye geri donuldu", "istasyon/liste", (t, a) => iste(a, t)],
  [42, "yanit geldi", "istasyon/liste", (t, a) => tamamla(a, t, LISTE)],
  [45, "istasyon ayrintisi acildi", "istasyon/kuzey-yamac", (t, a) => iste(a, t)],
  [47, "yanit geldi", "istasyon/kuzey-yamac", (t, a) => tamamla(a, t, { ad: "Kuzey Yamac" })],
  [50, "yeni istasyon kaydedildi", "istasyon/ (on ek)", (t) => gecersizlestir("istasyon/", t)],
  [51, "listeye donuldu", "istasyon/liste", (t, a) => iste(a, t)],
  [53, "yanit geldi", "istasyon/liste", (t, a) => tamamla(a, t, [...LISTE, "bati-vadi"])],
  [200, "ayrintiya geri donuldu", "istasyon/kuzey-yamac", (t, a) => iste(a, t)],
];

console.log("t".padStart(4), "olay".padEnd(30), "anahtar".padEnd(22), "sonuc".padEnd(28), "yas", "istek");
for (const [t, olay, anahtar, calistir] of ADIMLAR) {
  const r = calistir(t, anahtar);
  console.log(String(t).padStart(4), olay.padEnd(30), anahtar.padEnd(22),
    r.sonuc.padEnd(28), String(r.yas ?? "-").padStart(3), r.istek);
}

console.log("-- toplam ag istegi --", istekNo);
```

```
   t olay                           anahtar                sonuc                        yas istek
   0 Liste acildi                   istasyon/liste         veri yok, bekleme              - #1
   1 OzetKarti ayni veriyi istedi   istasyon/liste         ucusta birlestirildi           - #1
   3 yanit geldi                    istasyon/liste         yanit yazildi                  0 #1
   5 sekmeye geri donuldu           istasyon/liste         onbellek isabeti               2 -
  40 sekmeye geri donuldu           istasyon/liste         bayat sunuldu + yenileniyor   37 #2
  42 yanit geldi                    istasyon/liste         yanit yazildi                  0 #2
  45 istasyon ayrintisi acildi      istasyon/kuzey-yamac   veri yok, bekleme              - #3
  47 yanit geldi                    istasyon/kuzey-yamac   yanit yazildi                  0 #3
  50 yeni istasyon kaydedildi       istasyon/ (on ek)      2 kayit gecersizlestirildi     - -
  51 listeye donuldu                istasyon/liste         bayat sunuldu + yenileniyor    9 #4
  53 yanit geldi                    istasyon/liste         yanit yazildi                  0 #4
 200 ayrintiya geri donuldu         istasyon/kuzey-yamac   kayit dusuruldu, bekleme     153 #5
-- toplam ag istegi -- 5
```

Zaman dışarıdan verildiği için çıktı belirlenimcidir; aynı betik her çalıştırmada aynı
satırları üretir. Gerçek uygulamada bu değerin kaynağı sistem saatidir, ama katmanın kendisi
saati bir bağımlılık olarak alırsa sınanabilir kalır.

#### Uçuşta Birleşme

İkinci satır, katmanın en görünmez kazancını gösterir. Liste ile özet kartı aynı veriyi bir
saniye arayla ister; ağa yalnız bir istek çıkar.

Bu davranış olmadan aynı ekrandaki her bileşen kendi isteğini yapar. Ağ Panelinde Tanı
dersinde tanımlanan şelale görünümünde bu, aynı adrese giden yan yana isteklerle görünür.
Çözüm bileşenleri değiştirmek — veriyi yukarı taşımak, tek bir yerden dağıtmak — değil, aynı
anahtara giden istekleri katmanda birleştirmektir. Bileşenler birbirinden habersiz kalır ve
yine tek istek yapılır.

Birleşmenin işleyebilmesi için anahtarın **dizgi eşitliğiyle** karşılaştırılabilir olması
gerekir. Anahtar üretimi kanonik değilse, aynı isteği tanımlayan iki anahtar farklı görünür
ve birleşme kaçırılır.

#### Geçersizleştirme

Ellinci saniyedeki adım, tazelik penceresinin çözemediği durumu gösterir. Kullanıcı yeni bir
istasyon kaydetmiştir; listenin kopyası artık yanlıştır, ama yaşı henüz dokuz saniyedir.
Süre bu bilgiyi veremez.

Verebilecek olan yazma işleminin kendisidir. Bir yazma başarıyla tamamlandığında, ondan
etkilenen anahtarlar **geçersizleştirilir**: yaşları ne olursa olsun bayat sayılırlar.
Çıktıda geçersizleştirme ön ekle yapılmış ve iki kaydı birden etkilemiştir.

Etkilenen anahtarların belirlenmesi bir tasarım kararıdır. Dar bir geçersizleştirme —
yalnız değişen kaydın anahtarı — hızlıdır ama listeyi eski bırakır. Geniş bir
geçersizleştirme — tüm ön ek — güvenlidir ama gereksiz istek üretir. Ölçüt, yazmanın hangi
görünümleri gerçekten etkilediğidir; yeni bir istasyon listeyi de özetleri de değiştirir.

Geçersizleştirmenin bir kardeşi vardır: yanıtın **doğrudan yazılması**. Sunucu, yazma
isteğine güncel kaydı geri döndürüyorsa, o kayıt önbelleğe konur ve ek bir istek hiç
yapılmaz. Bu, sözleşmenin sunucu tarafını da ilgilendirir ve Veri Erişimi konusunda ele
alınır.

#### Saklama Süresi

Son satır iki yüzüncü saniyede yaşanır. İstasyon ayrıntısının kopyası saklama süresini
aştığı için düşürülmüştür; kullanıcı geri döndüğünde bayat veri bile gösterilemez ve
bekleme görünür.

Saklama süresinin varlık nedeni bellektir. Kullanıcının bir kez açıp bir daha dönmediği
her kayıt bellekte kalırsa, uzun süre açık duran bir sekme büyümeye devam eder.
Eşzamansız JavaScript ve Çalışma Zamanı kursundaki bellek sızıntısı tanısında görülen
tabloyu üreten nedenlerden biri budur.

İki süre birlikte ayarlanır. Tazelik penceresi verinin ne sıklıkta değiştiğine göre
seçilir: nadiren değişen bir istasyon künyesi uzun, sürekli akan bir ölçüm listesi kısa
pencere ister. Saklama süresi ise kullanıcının o görünüme dönme olasılığına göre seçilir.

#### Yeniden Getirme Tetikleyicileri

Bayat bir kaydın ne zaman yenileneceği yalnız okumaya bağlı değildir. Katman birkaç dış
işareti de dinler.

Sekmeye geri dönülmesi bunların en etkilisidir: kullanıcı başka bir uygulamada dakikalarca
kalıp geri döndüğünde ekrandaki her şey eskimiştir. Ağ bağlantısının yeniden kurulması
ikinci işarettir. Belirli aralıklarla yenileme ise yalnız gerçekten değişen veriler için
kullanılır ve sekme görünmediğinde durdurulur.

Her tetikleyici bir maliyet üretir. Ölçüt, kullanıcının eski veri görmesinin maliyetiyle
ek isteğin maliyetini karşılaştırmaktır; ölçüm listesinde eskilik pahalıdır, istasyon
künyesinde değildir.

#### Özet

- Sunucu durumu kendi katmanında tutulur ve her kopya, isteği tümüyle belirleyen kanonik
  bir anahtarla anılır.
- Tazelik penceresi kopyanın sorgusuz kullanılacağı süredir; saklama süresi bellekte
  kalacağı süredir. Arada kalan kopya bayattır: gösterilir ve arkada yenilenir.
- Aynı anahtara giden eşzamanlı istekler uçuşta birleştirilir; bileşenler birbirinden
  habersizken bile tek istek yapılır.
- Süre tabanlı tazelik, yerel bir yazmanın kopyayı yanlışladığını bilemez; yazma sonrası
  geçersizleştirme etkilenen anahtarları yaşlarına bakmadan bayatlatır.
- Saklama süresi dolan kayıt düşürülür ve dönüşte bayat veri bile gösterilemez; bu süre
  belleği sınırlar.
- Yeniden getirme okuma dışında sekmeye dönüş, bağlantının kurulması ve aralıklı yenileme
  ile de tetiklenir; her tetikleyici eskilik maliyetiyle istek maliyeti karşılaştırılarak
  seçilir.

#### Sonraki Adım

Bu katman okumayı çözdü. Yazma tarafında ise ekranda görünen şey hâlâ ağın hızına bağlı:
kullanıcı bir ölçümü siler, düğme beklemeye geçer, satır ancak sunucu yanıt verince
kaybolur. Oysa isteğin başarılı olacağı çoğu durumda bellidir ve sonuç zaten önceden
bilinmektedir. Sonraki ders değişikliği yanıtı beklemeden ekrana yansıtmayı, başarısızlıkta
önceki hâle dönmeyi ve bu dönüşün arada gelen başka değişikliklerle nasıl çakışabileceğini
ele alır.

### İyimser Güncelleme

Source: https://academia.sh/tr/kurslar/frontend-mimarisi/iyimser-guncelleme

Değişikliğin yanıt beklenmeden ekrana yansıtılması; anlık görüntüyle geri almanın eşzamanlı değişikliklerde bozulması, bekleyen değişiklik katmanı, sunucu yanıtıyla uzlaşma ve iyimserliğin uygulanmayacağı durumlar.

Sunucu durumu katmanı okumayı çözdü: kopya anahtarla anılıyor, tazelik penceresiyle
kullanılıyor, geçersizleştirmeyle yenileniyor. Yazma tarafında ise ekranda görünen şey hâlâ
ağın hızına bağlı. Kullanıcı Kuzey Yamaç Ölçüm İstasyonu uygulamasında bir ölçümü siliyor;
düğme beklemeye geçiyor ve satır ancak sunucu yanıt verince kayboluyor.

Oysa isteğin sonucu çoğu durumda önceden bellidir. Silme işlemi başarılı olacaksa satırın
kaybolacağı biliniyor. **İyimser güncelleme**, beklenen sonucu yanıttan önce ekrana
yansıtmak ve yanıt geldiğinde uzlaşmaktır.

#### Neyi Kazandırır

Kazanç ağ süresinin kısalması değildir; istek yine aynı sürede tamamlanır. Kazanç
kullanıcının bekleme algısındadır. Yüz milisaniyelik bir gecikme bile art arda yapılan
işlemlerde birikir: on ölçümü tek tek silen bir kullanıcı, her silmede ekranın donmasını
bekler.

İyimserlik ayrıca arayüzü basitleştirir. Her düğmenin bir bekleme durumu, her satırın bir
"siliniyor" görünümü olmak zorunda kalmaz; ekran doğrudan sonucu gösterir.

Karşılığında bir söz verilmiş olur: ekranda görünen şey henüz doğrulanmamıştır. Söz
tutulmadığında — istek başarısız olduğunda — ekran geri alınmalıdır. Bu dersin asıl konusu
o geri almadır, çünkü ilk akla gelen yöntem belirli bir durumda sessizce yanlış çalışır.

#### Geri Almanın İki Yöntemi

İlk yöntem doğrudandır: değişiklik başlarken ekranın **anlık görüntüsü** alınır, istek
başarısız olursa o görüntü geri yüklenir.

İkinci yöntem araya bir katman koyar. Sunucudan gelen veri **taban** olarak ayrı tutulur;
tamamlanmamış değişiklikler bir **bekleyen değişiklik** listesinde durur; ekranda görünen
şey, tabanın üzerine bekleyenlerin uygulanmasıyla her seferinde yeniden hesaplanır.
Başarısızlık, o değişikliği listeden çıkarmaktır.

İkisi tek bir değişiklik varken aynı sonucu verir. Fark, iki değişiklik üst üste
bindiğinde ortaya çıkar.

```js
// iyimser-guncelleme.mjs — anlik goruntuyle geri alma ile bekleyen katman yonteminin karsilastirmasi
const TABAN = ["T-01", "T-02", "T-03"];

const DEGISIKLIK = {
  A: { etiket: "sil T-02", uygula: (l) => l.filter((x) => x !== "T-02") },
  B: { etiket: "ekle T-04", uygula: (l) => [...l, "T-04"] },
};

// 1. Yontem: degisiklik baslarken ekranin anlik goruntusu alinir, hatada geri yuklenir.
function anlikGoruntuYontemi(olaylar) {
  let ekran = [...TABAN];
  const goruntu = new Map();
  const satirlar = [];
  for (const [ad, tur] of olaylar) {
    if (tur === "basla") { goruntu.set(ad, [...ekran]); ekran = DEGISIKLIK[ad].uygula(ekran); }
    else if (tur === "basarili") { goruntu.delete(ad); }
    else { ekran = goruntu.get(ad); goruntu.delete(ad); }
    satirlar.push([`${ad} ${tur}`, [...ekran]]);
  }
  return satirlar;
}

// 2. Yontem: sunucu tabani ayri tutulur, bekleyen degisiklikler her cizimde uzerine uygulanir.
function bekleyenKatmanYontemi(olaylar) {
  let taban = [...TABAN];
  const bekleyen = [];
  const ciz = () => bekleyen.reduce((l, ad) => DEGISIKLIK[ad].uygula(l), taban);
  const satirlar = [];
  for (const [ad, tur] of olaylar) {
    if (tur === "basla") bekleyen.push(ad);
    else {
      bekleyen.splice(bekleyen.indexOf(ad), 1);
      if (tur === "basarili") taban = DEGISIKLIK[ad].uygula(taban);   // sunucu onayi tabana islenir
    }
    satirlar.push([`${ad} ${tur}`, ciz()]);
  }
  return satirlar;
}

function karsilastir(baslik, olaylar) {
  console.log(`-- ${baslik} --`);
  const a = anlikGoruntuYontemi(olaylar);
  const b = bekleyenKatmanYontemi(olaylar);
  console.log("olay".padEnd(14), "anlik goruntu".padEnd(30), "bekleyen katman");
  for (let i = 0; i < a.length; i++)
    console.log(a[i][0].padEnd(14), a[i][1].join(",").padEnd(30), b[i][1].join(","));
}

karsilastir("tek degisiklik, basarisiz", [["A", "basla"], ["A", "basarisiz"]]);
karsilastir("iki degisiklik, ilki basarisiz", [
  ["A", "basla"], ["B", "basla"], ["B", "basarili"], ["A", "basarisiz"]]);
karsilastir("iki degisiklik, ikisi de basarili", [
  ["A", "basla"], ["B", "basla"], ["B", "basarili"], ["A", "basarili"]]);
```

```
-- tek degisiklik, basarisiz --
olay           anlik goruntu                  bekleyen katman
A basla        T-01,T-03                      T-01,T-03
A basarisiz    T-01,T-02,T-03                 T-01,T-02,T-03
-- iki degisiklik, ilki basarisiz --
olay           anlik goruntu                  bekleyen katman
A basla        T-01,T-03                      T-01,T-03
B basla        T-01,T-03,T-04                 T-01,T-03,T-04
B basarili     T-01,T-03,T-04                 T-01,T-03,T-04
A basarisiz    T-01,T-02,T-03                 T-01,T-02,T-03,T-04
-- iki degisiklik, ikisi de basarili --
olay           anlik goruntu                  bekleyen katman
A basla        T-01,T-03                      T-01,T-03
B basla        T-01,T-03,T-04                 T-01,T-03,T-04
B basarili     T-01,T-03,T-04                 T-01,T-03,T-04
A basarili     T-01,T-03,T-04                 T-01,T-03,T-04
```

İlk senaryoda iki yöntem aynı sonucu verir. Silinen satır geri gelir ve ekran istek öncesi
hâline döner.

İkinci senaryonun son satırı ise ayrışır. Silme başarısız olduğunda anlık görüntü yöntemi
ekranı, silmenin **başladığı andaki** hâle döndürür — o an henüz eklenmemiş olan `T-04`
kaybolur. Kullanıcı hiç hata yapmadığı bir işlemin sonucunun ekrandan silindiğini görür ve
bunun nedenini anlayamaz.

Bekleyen katman yöntemi aynı satırda doğru sonucu verir. Başarısız değişiklik listeden
çıkarılır, taban ise başarılı olan değişikliğin sonucunu zaten almıştır; ekran yeniden
hesaplandığında hem silinen satır geri gelir hem de eklenen satır yerinde kalır.

Fark yöntemin kendisinden gelir. Anlık görüntü, ekranın bir andaki **sonucunu** saklar ve o
sonuç başka değişikliklerin izini de taşır. Bekleyen katman ise yalnız değişikliğin
**kendisini** saklar; geri alma, tek bir değişikliği kaldırıp yeniden hesaplamaktır.

#### Sunucu Yanıtıyla Uzlaşma

Başarılı yanıt geldiğinde bekleyen değişiklik listeden çıkarılır, ama ekranın bozulmaması
için tabanın da güncellenmesi gerekir. İki yol vardır.

Sunucu yanıtında güncel kaydı döndürüyorsa taban doğrudan güncellenir; ek istek yapılmaz.
Döndürmüyorsa Sunucu Durumu Yönetimi dersindeki geçersizleştirme kullanılır: etkilenen
anahtarlar bayatlatılır ve bir sonraki okumada yenilenir. İkinci yolda kısa bir aralık
oluşur — bekleyen çıkarılmış ama taze veri henüz gelmemiştir — ve bu aralıkta ekran bir an eski
veriyi gösterebilir. Bu nedenle bekleyen değişiklik, yenileme tamamlanana kadar listede
bırakılabilir.

İyimser sonucun sunucu sonucundan **ayrılabildiği** durumlar da vardır. Yeni bir kayıt
eklenirken kimliği sunucu üretir; istemcinin koyduğu geçici kimlik yanıt gelince gerçek
kimlikle değiştirilir. Zaman damgası, sıra numarası ve sunucuda hesaplanan toplamlar da
aynı gruptadır. Kural şudur: iyimser değer bir **tahmindir** ve doğru kaynak yanıttır;
tahminle yanıt çeliştiğinde yanıt kazanır.

#### İyimserliğin Uygulanmadığı Yerler

İyimser güncelleme her işlem için uygun değildir. Üç ölçüt onu dışarıda bırakır.

**Sonuç öngörülemiyorsa** uygulanmaz. Sunucunun çalıştırdığı bir doğrulama, bir kota
denetimi ya da bir çakışma çözümü varsa istemci sonucu bilemez; tahmin sık tutmayacaktır.

**Başarısızlık olağansa** uygulanmaz. Geri alma seyrek olduğunda kabul edilebilir; sık
olduğunda ekran sürekli kendini düzeltiyor görünür ve kullanıcı hiçbir şeye güvenemez.

**İşlem geri alınamıyorsa** uygulanmaz. Bir ödeme, bir dış bildirim gönderimi ya da kalıcı
bir silme işleminde ekranda başarı göstermek, henüz olmamış bir şeyi olmuş gibi
sunmaktır. Bu işlemlerde bekleme göstergesi doğru davranıştır.

Aynı uygulamada iki yaklaşım bir arada bulunur: ölçüm silme ve etiket değiştirme iyimser
yapılır, istasyon kaydı ve yetki değişikliği yanıt beklenerek yapılır.

#### Başarısızlığın Görünürlüğü

Geri alma sessiz yapılamaz. Ekranda kaybolan bir satırın geri gelmesi, kullanıcı için
açıklanmamış bir olaydır; işlemin başarısız olduğunu söylemeyen bir arayüz, kullanıcıyı
işlemin yapıldığına inandırmış olur.

Başarısızlık bildirimi üç şeyi taşır: hangi işlemin başarısız olduğu, nedeni ve yeniden
deneme yolu. Bildirimin ekran okuyucuya da ulaşması gerekir; Belge ve Olaylar konusunda
kurulan odak ve duyuru kuralları burada geçerlidir.

Yeniden deneme kendiliğinden yapılacaksa, Eşzamansız JavaScript ve Çalışma Zamanı
kursundaki geri çekilme kuralları uygulanır ve deneme sırasında değişiklik bekleyen
listesinde tutulmaya devam eder. Kullanıcıya ancak son deneme de başarısız olduğunda hata
gösterilir.

#### Özet

- İyimser güncelleme, beklenen sonucu yanıttan önce ekrana yansıtır; kazancı ağ süresinde
  değil, bekleme algısında ve arayüzün sadeleşmesindedir.
- Anlık görüntüyle geri alma, ekranın bir andaki sonucunu sakladığı için araya giren başka
  bir değişikliğin sonucunu da siler.
- Bekleyen değişiklik katmanı sunucu tabanını ayrı tutar ve ekranı her seferinde yeniden
  hesaplar; geri alma tek bir değişikliği listeden çıkarmaktır.
- Başarılı yanıt tabana işlenir ya da ilgili anahtarlar geçersizleştirilir; iyimser değer
  bir tahmindir, sunucu yanıtıyla çeliştiğinde yanıt kazanır.
- Sonucu öngörülemeyen, sık başarısız olan ve geri alınamayan işlemlerde iyimserlik
  uygulanmaz; bekleme göstergesi doğru davranıştır.
- Geri alma sessiz yapılmaz: hangi işlemin neden başarısız olduğu ve nasıl yeniden
  deneneceği kullanıcıya bildirilir.

#### Sonraki Adım

Durumun nereye konacağı ve nasıl güncelleneceği çözüldü, ama tümü bellekte duruyor.
Kullanıcı sekmeyi kapattığında arayüz teması, seçili birim sistemi ve yarım kalan ölçüm
formu kayboluyor; sayfa yenilendiğinde uygulama baştan kuruluyor. Tarayıcı ve Web Platformu
kursundaki depolama katmanları bunu saklayacak yeri sağlıyor, ama hangi durumun
saklanacağı, saklanan kaydın kod değiştiğinde nasıl okunacağı ve iki sekmenin aynı depoya
yazmasının ne üreteceği ayrı sorular. Sonraki ders durumu kalıcılaştırmayı ele alır.

### Kalıcı Durum

Source: https://academia.sh/tr/kurslar/frontend-mimarisi/kalici-durum

Durumun depolamaya yazılması; hangi dilimin kalıcılaştırılacağı, dilim başına sürümleme ve göç zinciri, varsayılanlarla birleştirme, yazma maliyetinin seyreltilmesi, iki sekmenin çakışması ve başlangıç durumunun kurulması.

Durumun nereye konacağı ve nasıl güncelleneceği çözüldü, ama tümü bellekte duruyor. Kuzey
Yamaç Ölçüm İstasyonu uygulamasında kullanıcı sekmeyi kapattığında arayüz teması, seçili
birim sistemi ve son görüntülenen istasyonlar kayboluyor.

Tarayıcı ve Web Platformu kursundaki Depolama API'leri dersi saklama yerini kurdu:
katmanların ömürleri, dizgi deposunun tip kayıpları, kota ve şema göçü. Bu ders o yerin
üzerine durum katmanını oturtur ve dört ayrı soruyu yanıtlar: hangi dilim yazılır, kayıt
kod değişince nasıl okunur, ne zaman yazılır, iki sekme aynı anda yazarsa ne olur.

#### Hangi Dilim Kalıcılaştırılır

Ölçüt iki koşulun birleşimidir: değer **yeniden üretilemiyorsa** ve kullanıcı kaybını
**fark edecekse** kalıcılaştırılır. Arayüz teması ikisini de karşılar; kullanıcı onu bir kez
seçmiştir ve her açılışta yeniden seçmek istemez.

Dört tür dışarıda kalır. **Sunucu durumu** kalıcılaştırılmaz — kaynağı uzaktadır ve
kopyanın tazeliği zaten ayrı bir katmanın işidir; bir sonraki açılışta gösterilecek eski
veri, tazelik penceresi olmadan yanlış veridir. **URL durumu** kalıcılaştırılmaz; zaten
adreste taşınır ve iki yere yazılırsa hangisinin kazanacağı belirsizleşir. **Türetilmiş
değer** kalıcılaştırılmaz; girdileri saklanır. **Kimlik belirteci ve kişisel veri** de
kalıcılaştırılmaz; Depolama API'leri dersinde kurulan kural gereği sayfada çalışan her kod
o depoyu okuyabilir.

Geriye paylaşılan istemci durumunun bir bölümü ile kullanıcının yarım bıraktığı girişler
kalır.

#### Dilim Başına Sürüm

Depolanan veri onu yazan koddan uzun yaşar. Depolama API'leri dersi bunun çözümünü kurmuştu:
kayıt bir sürüm alanı taşır ve okuma yolu eski sürümü güncele taşıyan bir göç zincirinden
geçer.

Durum katmanında bir ekleme gerekir. Tek bir sürüm numarası tüm kaydı kapsarsa, tek bir
dilimin biçimi değiştiğinde bütün kaydın sürümü artar ve her dilim için bir göç adımı
yazılmak zorunda kalınır. Dilimler birbirinden bağımsız geliştiği için sürüm de **dilim
başına** tutulur.

```js
// kalici-durum.mjs — dilim basina surumleme, goc zinciri ve sekmeler arasi birlestirme
const SEMA = {
  tercihler: {
    surum: 3,
    varsayilan: { tema: "acik", birim: "metrik", satirYuksekligi: "normal" },
    goc: {
      1: (d) => ({ ...d, birim: "metrik" }),                       // 1 -> 2: birim eklendi
      2: ({ koyuTema, ...d }) => ({ ...d, tema: koyuTema ? "koyu" : "acik" }),  // 2 -> 3
    },
  },
  sonGorulenler: { surum: 1, varsayilan: [], goc: {} },
};

function kalicilastir(durum) {                    // yalniz semada yazili dilimler yazilir
  const kayit = {};
  for (const [ad, k] of Object.entries(SEMA)) kayit[ad] = { surum: k.surum, veri: durum[ad] };
  return JSON.stringify(kayit);
}

function geriYukle(ham) {
  const not = [];
  let kayit;
  try { kayit = JSON.parse(ham); } catch { return { durum: varsayilanlar(), not: ["cozumlenemedi"] }; }

  const durum = {};
  for (const [ad, k] of Object.entries(SEMA)) {
    const dilim = kayit[ad];
    if (!dilim || typeof dilim.surum !== "number") { durum[ad] = k.varsayilan; not.push(`${ad}: yok`); continue; }
    if (dilim.surum > k.surum) { durum[ad] = k.varsayilan; not.push(`${ad}: gelecek surum`); continue; }
    let veri = dilim.veri, s = dilim.surum;
    while (s < k.surum) { veri = k.goc[s](veri); s += 1; }
    if (s !== dilim.surum) not.push(`${ad}: ${dilim.surum} -> ${k.surum}`);
    durum[ad] = Array.isArray(k.varsayilan) ? veri : { ...k.varsayilan, ...veri };
  }
  for (const ad of Object.keys(kayit))
    if (!(ad in SEMA)) not.push(`${ad}: semada yok, atildi`);
  return { durum, not };
}

const varsayilanlar = () =>
  Object.fromEntries(Object.entries(SEMA).map(([ad, k]) => [ad, k.varsayilan]));

console.log("-- geri yukleme --");
const ORNEKLER = [
  ['{"tercihler":{"surum":3,"veri":{"tema":"koyu","birim":"metrik","satirYuksekligi":"sik"}},"sonGorulenler":{"surum":1,"veri":["kuzey-yamac"]}}', "guncel kayit"],
  ['{"tercihler":{"surum":1,"veri":{"koyuTema":true}},"sonGorulenler":{"surum":1,"veri":[]}}', "iki surum eski"],
  ['{"tercihler":{"surum":3,"veri":{"tema":"koyu"}}}', "eksik dilim + eksik alan"],
  ['{"tercihler":{"surum":9,"veri":{}},"sonGorulenler":{"surum":1,"veri":["dogu-sirt"]}}', "gelecek surum"],
  ['{"tercihler":{"surum":3,"veri":{}},"oturum":{"surum":1,"veri":{"belirtec":"abc"}}}', "artik yazilmayan dilim"],
  ['{tercihler:}', "bozuk icerik"],
];
for (const [ham, etiket] of ORNEKLER) {
  const { durum, not } = geriYukle(ham);
  console.log(etiket.padEnd(26), JSON.stringify(durum));
  console.log(" ".repeat(26), "not:", not.join("; ") || "-");
}

console.log("-- iki sekme --");
const A = { tercihler: { tema: "koyu", birim: "metrik", satirYuksekligi: "normal" }, sonGorulenler: ["kuzey-yamac"] };
const B = { tercihler: { tema: "acik", birim: "metrik", satirYuksekligi: "normal" }, sonGorulenler: ["dogu-sirt", "bati-vadi"] };

// Tumunu yazma: son yazan digerinin dilimini de ezer.
console.log("A yazdi           ", kalicilastir(A));
console.log("B yazdi (tumu)    ", kalicilastir(B));

// Dilim basina yazma: her sekme yalniz degistirdigi dilimi yazar.
function dilimYaz(ham, ad, veri) {
  const kayit = JSON.parse(ham);
  kayit[ad] = { surum: SEMA[ad].surum, veri };
  return JSON.stringify(kayit);
}
const sonrasi = dilimYaz(kalicilastir(A), "sonGorulenler", B.sonGorulenler);
console.log("B yazdi (dilim)   ", sonrasi);
console.log("A'nin temasi durdu:", geriYukle(sonrasi).durum.tercihler.tema);
```

```
-- geri yukleme --
guncel kayit               {"tercihler":{"tema":"koyu","birim":"metrik","satirYuksekligi":"sik"},"sonGorulenler":["kuzey-yamac"]}
                           not: -
iki surum eski             {"tercihler":{"tema":"koyu","birim":"metrik","satirYuksekligi":"normal"},"sonGorulenler":[]}
                           not: tercihler: 1 -> 3
eksik dilim + eksik alan   {"tercihler":{"tema":"koyu","birim":"metrik","satirYuksekligi":"normal"},"sonGorulenler":[]}
                           not: sonGorulenler: yok
gelecek surum              {"tercihler":{"tema":"acik","birim":"metrik","satirYuksekligi":"normal"},"sonGorulenler":["dogu-sirt"]}
                           not: tercihler: gelecek surum
artik yazilmayan dilim     {"tercihler":{"tema":"acik","birim":"metrik","satirYuksekligi":"normal"},"sonGorulenler":[]}
                           not: sonGorulenler: yok; oturum: semada yok, atildi
bozuk icerik               {"tercihler":{"tema":"acik","birim":"metrik","satirYuksekligi":"normal"},"sonGorulenler":[]}
                           not: cozumlenemedi
-- iki sekme --
A yazdi            {"tercihler":{"surum":3,"veri":{"tema":"koyu","birim":"metrik","satirYuksekligi":"normal"}},"sonGorulenler":{"surum":1,"veri":["kuzey-yamac"]}}
B yazdi (tumu)     {"tercihler":{"surum":3,"veri":{"tema":"acik","birim":"metrik","satirYuksekligi":"normal"}},"sonGorulenler":{"surum":1,"veri":["dogu-sirt","bati-vadi"]}}
B yazdi (dilim)    {"tercihler":{"surum":3,"veri":{"tema":"koyu","birim":"metrik","satirYuksekligi":"normal"}},"sonGorulenler":{"surum":1,"veri":["dogu-sirt","bati-vadi"]}}
A'nin temasi durdu: koyu
```

Göç zinciri ikinci satırda iki adım çalışmıştır. En eski biçimde tema bir mantıksal alanla
tutuluyordu; iki geçişte önce birim eklenmiş, sonra alan yeniden adlandırılıp değeri
dönüştürülmüştür. Dilimin sürümü kendi başına ilerlediği için diğer dilime hiç
dokunulmamıştır.

#### Okuma Sınırı

Kalan satırlar okuma tarafının savunmasını gösterir ve hepsi aynı ilkeden çıkar: depodan
gelen her şey dış girdidir.

Eksik dilim varsayılanına düşer. **Eksik alan** da varsayılanla tamamlanır; üçüncü satırda
yalnız tema yazılmış olmasına karşın birim ve satır yüksekliği varsayılan değerlerini
almıştır. Bu birleştirme, yeni bir alan eklendiğinde eski kayıtların geçersiz sayılmasını
önler; alan eklemek göç adımı gerektirmez.

Gelecek sürümlü dilim geriye taşınamayacağı için varsayılana düşer — kullanıcı başka bir
cihazda daha yeni bir sürüm kullanmış olabilir. Çözümlenemeyen içerikte tüm kayıt
bırakılır. Şemada bulunmayan dilim ise atılır; bu, artık kalıcılaştırılmayan eski bir
alanın — örnekte bir oturum belirtecinin — depoda kalmaya devam etmesini önler.

Son durum ayrıca bir temizlik görevidir. Bir dilim kalıcılaştırma listesinden çıkarıldığında
depodaki eski kaydın da silinmesi gerekir; yalnız yazmayı durdurmak, orada duran veriyi
ortadan kaldırmaz.

#### Yazma Zamanı

Depolama API'leri dersi ilk iki katmanın erişiminin eşzamanlı olduğunu ve büyük kayıtların
ana iş parçacığını bekletebileceğini kurmuştu. Durum katmanı her değişimde yazmaya adaydır;
bu, her tuş vuruşunda depoya yazan bir kod anlamına gelebilir.

Üç kural maliyeti sınırlar. Yazma **seyreltilir**: değişimden sonra kısa bir sessizlik
beklenir ve tek bir yazma yapılır. Yalnız **değişen dilim** yazılır; tercihler değişmediyse
o dilime dokunulmaz. Ve kayıt **küçük tutulur**; büyüyen veri eşzamansız arayüzlü
yapılandırılmış depolamaya taşınır.

Sekme kapanırken son bir yazma yapılmak istenebilir. Sayfanın kapanış anında güvenilir
biçimde çalışan tek yol eşzamanlı bir yazmadır ve o anda uzun bir işlem tamamlanmayabilir;
bu nedenle kapanışa güvenmek yerine değişim anında yazmak tercih edilir.

#### İki Sekme

Son bölüm bir kayıp senaryosunu gösterir. Aynı kaynağın iki sekmesi aynı depoyu paylaşır.
Birinci sekmede tema koyuya çevrilmiş; ikinci sekmede son görüntülenenler değişmiştir.

İkinci sekme **tüm kaydı** yazdığında birinci sekmenin teması geri açığa döner: ikinci
sekmenin bellekteki kopyası, tema değişikliğinden önceki hâldir. Yalnız **değiştirdiği
dilimi** yazdığında ise iki değişiklik de korunur; çıktının son satırı bunu doğrular.

Dilim başına yazma çakışmayı daraltır, ortadan kaldırmaz: aynı dilimi iki sekme birden
değiştirdiğinde son yazan kazanır. Bunun kabul edilebilir olduğu yerlerde başka bir şey
yapılmaz. Kabul edilemediği yerlerde, Depolama API'leri dersinde tanıtılan depolama olayı
dinlenir ve diğer sekmedeki değişiklik belleğe alınır; böylece bir sonraki yazma güncel
kayıt üzerine yapılır.

#### Başlangıç Durumunun Kurulması

Uygulama açılırken depodan okunan durum, bellekteki durumun başlangıç değeri olur. Sıra
önemlidir: okuma **ilk çizimden önce** yapılırsa ekran doğru temayla açılır. Sonra
yapılırsa uygulama önce varsayılan temayla çizilir, ardından kayıtlı temaya geçer ve
kullanıcı bir sıçrama görür.

Eşzamansız bir depolama katmanı kullanıldığında bu sıçrama kaçınılmaz görünür. İki çözüm
vardır: sıçramaya yol açan küçük tercihleri eşzamanlı katmanda tutmak, ya da ilk çizimi
kayıt okunana kadar geciktirmek. İkinci çözüm boş ekran süresini uzattığı için genellikle
yalnız ilk çözümün yetmediği yerlerde seçilir.

#### Özet

- Yeniden üretilemeyen ve kaybı kullanıcı tarafından fark edilecek istemci durumu
  kalıcılaştırılır; sunucu durumu, URL durumu, türetilmiş değer ve kimlik belirteci
  kalıcılaştırılmaz.
- Sürüm dilim başına tutulur; bir dilimin biçim değişikliği diğer dilimler için göç adımı
  yazmayı gerektirmez.
- Okuma tarafı savunmalıdır: eksik dilim ve eksik alan varsayılanla tamamlanır, gelecek
  sürümlü ve çözümlenemeyen içerik yok sayılır, şemada bulunmayan dilim atılır ve silinir.
- Alan eklemek göç adımı gerektirmez, çünkü okuma varsayılanlarla birleştirir; alan
  yeniden adlandırmak veya tipini değiştirmek gerektirir.
- Yazma seyreltilir, yalnız değişen dilim yazılır ve kayıt küçük tutulur; kapanış anındaki
  yazmaya güvenilmez.
- İki sekme tüm kaydı yazdığında birbirinin dilimini ezer; dilim başına yazma bunu önler,
  aynı dilimdeki çakışma için depolama olayı dinlenir.

#### Sonraki Adım

Durumun nerede tutulacağı, nasıl güncelleneceği ve nasıl kalıcılaştığı çözüldü. Bir
katmanın içi ise hâlâ boş: sunucu durumu önbelleği "isteği yap, yanıtı yaz" diyor ama
isteğin nasıl yapıldığını, yanıtın hangi biçimde beklendiğini ve hatanın nasıl temsil
edileceğini söylemiyor. Bir istek başarısız olduğunda katmanın eline ne geçer — bir durum
kodu mu, bir hata gövdesi mi, bir ağ kesintisi mi? Bu üçü aynı biçimde ele alınamaz.
Sonraki konu veri erişimini ele alır ve ilk dersi olan REST İstemcisi, istek katmanının
tasarımını ve uygulamanın her yerinde geçerli olacak hata sözleşmesini kurar.

## Veri Erişimi

Uygulamanın sunucuyla konuşma katmanı: istek katmanının hata sözleşmesi, sorgu tabanlı erişimde önbellek normalizasyonu, akış hâlinde gelen veri, görünüm düzeyinde durum makinesi ve ağ kararsızlığına karşı yeniden deneme düzeni.

### REST İstemcisi

Source: https://academia.sh/tr/kurslar/frontend-mimarisi/rest-istemcisi

Uygulamanın sunucuyla konuşan tek kapısı olarak istek katmanı; hata sözleşmesinin dört sınıfı, durum kodundan hataya eşleme, iptal ve zaman aşımı ile kaynak adreslerinin tasarımı.

Durum yönetimi konusu, uygulamanın hangi bilgiyi nerede tuttuğunu ve bu bilginin sayfa
kapandıktan sonra nasıl hayatta kaldığını kurdu. Kap hazır: yerel durum bileşenin içinde,
paylaşılan durum ortak bir depoda, adreste taşınan durum rotada, kalıcı olması gerekenler
depolamada duruyor. Kabın en büyük bölümü ise henüz boş — sunucu durumu.

Kuzey Yamaç Ölçüm İstasyonu arayüzünde istasyon listesi, her istasyonun ölçüm geçmişi ve
yeni ölçüm kaydı sunucuda tutulur. Bu ders o veriyi almanın **hangi sözleşmeyle**
yapıldığını ele alır: isteği kim kurar, yanıt nasıl çözülür, işler yolunda gitmediğinde
görünüme ne bildirilir. Sorunun cevabı bir işlev değil, bir kattır.

#### İstek Katmanı Neden Ayrı Bir Kat

Veriyi çeken kod doğrudan bileşenin içine yazıldığında dört şey her çağrı noktasında
tekrarlanır: taban adresin birleştirilmesi, ortak başlıkların eklenmesi, yanıtın
çözümlenmesi ve hatanın yorumlanması. Tekrar yalnız yazım maliyeti değildir; her tekrar
ayrı bir davranış üretir. Bir yerde 404 boş liste sayılır, başka yerde hata sayılır; bir
yerde zaman aşımı vardır, başka yerde yoktur.

**İstek katmanı**, bu dört sorumluluğu tek yere toplayan ince bir aradır. Görünüme iki şey
söz verir: her çağrı ya veri döndürür ya da **hata sözleşmesine** uyan bir hata nesnesi
döndürür; üçüncü bir olasılık yoktur. Katmanın altında HTTP vardır, üstünde uygulama
kavramları. İstasyon listesini isteyen kod durum kodu bilmez, yalnız "bulunamadı" ya da
"yetki yok" görür.

Katmanın sınırı da bellidir. İstek katmanı **önbellek tutmaz**, yeniden denemez, yükleme
göstergesi yönetmez. Bunlar sırasıyla sunucu durumu yönetiminin, dayanıklılık düzeninin ve
görünüm durum makinesinin işidir; sonraki dersler bunları ayrı ayrı kurar. Katmanın tek
işi, ağın belirsizliğini kapalı ve sayılabilir bir sonuç kümesine çevirmektir.

#### Hata Sözleşmesi

Bir istek dört ayrı yerde başarısız olabilir ve bu dördü aynı şey değildir.

- **Ağ hatası.** İstek karşı tarafa hiç ulaşmadı: ad çözümlenemedi, bağlantı kurulamadı,
  bağlantı koptu. Sunucu bu isteği görmemiş olabilir — ama görmüş de olabilir; yanıtın
  yolda kaybolması da aynı hataya benzer. Bu belirsizlik, yeniden deneme kararında
  belirleyici olacak.
- **HTTP hatası.** İstek ulaştı, sunucu yanıt verdi, yanıt başarısızlık bildiriyor.
  Sunucunun ne düşündüğü durum kodunda yazılıdır ve bu bilgi güvenilirdir.
- **Gövde hatası.** Yanıt geldi, durum kodu başarı diyor, ama gövde beklenen biçimde
  değil: bozuk JSON, eksik alan, farklı içerik türü. Araya giren bir vekil sunucunun HTML
  hata sayfası döndürmesi bu sınıfa girer.
- **İptal.** İstek, sonucu artık kimse beklemediği için durduruldu. Kullanıcı sayfadan
  ayrıldı, arama kutusuna yeni bir harf yazdı ya da zaman aşımı doldu. Bu bir başarısızlık
  değil, bir karardır; kullanıcıya hata gösterilmez.

Dördünün ortak bir gösterimi olmalı, yoksa her çağrı noktası kendi ayrımını yeniden
uydurur. Sözleşme şu alanlardan oluşur: `tur` (dört sınıftan biri), `kod` (uygulamanın
tanıdığı sabit ad), `durum` (varsa HTTP durum kodu), `yenidenDenenebilir` (mantıksal) ve
`ayrinti` (sunucunun gönderdiği yapılandırılmış açıklama). İleti metni sözleşmede tutulmaz
— hangi dilde ve hangi tonda yazılacağı görünümün kararıdır; bu ayrımı Uluslararasılaştırma
dersi kapatır.

#### Durum Kodundan Hataya Eşleme

Eşleme bir tablo işidir ve tablo tek yerde durur. İnternet Nasıl Çalışır kursundaki HTTP
İstek ve Yanıtı dersinde tanıtılan durum kodu sınıfları burada uygulama kararına çevrilir:
`4xx` isteğin kendisinde bir sorun olduğunu söyler, aynı isteği tekrarlamak sonucu
değiştirmez; `5xx` sunucunun o an tamamlayamadığını söyler, tekrarlamak işe yarayabilir.

```js
// durum-eslemesi.mjs — HTTP durum kodundan hata sozlesmesine gecis
const ESLEME = [
  [400, "istek_bicimi",   false, "İstek gövdesi okunamadı"],
  [401, "kimlik_yok",     false, "Oturum gerekiyor"],
  [403, "yetki_yok",      false, "Bu kayda erişim yok"],
  [404, "bulunamadi",     false, "Kayıt yok"],
  [409, "cakisma",        false, "Kayıt bu arada değişti"],
  [422, "dogrulama",      false, "Alan değerleri kabul edilmedi"],
  [429, "hiz_siniri",     true,  "İstek hızı aşıldı"],
  [500, "sunucu",         true,  "Sunucu isteği tamamlayamadı"],
  [503, "erisilemez",     true,  "Hizmet geçici olarak kapalı"],
];

function httpHatasi(durum, ayrinti = null) {
  const satir = ESLEME.find(([d]) => d === durum);
  if (satir) {
    const [, kod, yenidenDenenebilir, ileti] = satir;
    return { tur: "http", durum, kod, ileti, yenidenDenenebilir, ayrinti };
  }
  // Bilinmeyen kod: sinifina gore karar verilir, uydurma kod uretilmez.
  const sunucuSinifi = durum >= 500;
  return {
    tur: "http",
    durum,
    kod: sunucuSinifi ? "sunucu" : "istek",
    ileti: sunucuSinifi ? "Sunucu isteği tamamlayamadı" : "İstek kabul edilmedi",
    yenidenDenenebilir: sunucuSinifi,
    ayrinti,
  };
}

for (const durum of [401, 404, 422, 429, 500, 418, 504]) {
  const h = httpHatasi(durum);
  console.log(
    String(h.durum).padEnd(4),
    h.kod.padEnd(14),
    "yeniden:", String(h.yenidenDenenebilir).padEnd(5),
    h.ileti,
  );
}
```

```
401  kimlik_yok     yeniden: false Oturum gerekiyor
404  bulunamadi     yeniden: false Kayıt yok
422  dogrulama      yeniden: false Alan değerleri kabul edilmedi
429  hiz_siniri     yeniden: true  İstek hızı aşıldı
500  sunucu         yeniden: true  Sunucu isteği tamamlayamadı
418  istek          yeniden: false İstek kabul edilmedi
504  sunucu         yeniden: true  Sunucu isteği tamamlayamadı
```

Son iki satır tablonun asıl değerini gösterir. Tabloda olmayan kodlar da bir karara
bağlanır ve karar sınıfın anlamından türer; katman "bilinmeyen kod" diye çöküp görünümü
tanımsız bir duruma bırakmaz. 429 ilginç bir istisnadır: 4xx sınıfındadır ama yeniden
denenebilir, çünkü sorun isteğin içeriğinde değil zamanlamasındadır.

#### Katmanın Uçtan Uca Çalışması

Aşağıdaki dosya hem yerel bir sunucu kurar hem de istek katmanını o sunucuya karşı
çalıştırır; kendi başına çalışır ve sonunda sunucuyu kapatır. Bağlantı noktası sıfır
verildiği için işletim sistemi boş bir kapı seçer — o sayı çalıştıran makineye göre değişir
ve çıktıda görünmez.

```js
// istek-katmani.mjs — yerel sunucu + istek katmani, tek dosyada calisir
import http from "node:http";

// --- Sunucu: istasyon listesi, olcum gecmisi ve hata yollari ----------------
const ISTASYONLAR = [
  { kod: "KY-01", ad: "Kuzey Yamaç", yukseklik: 1840 },
  { kod: "KY-02", ad: "Kuzey Yamaç Sırt", yukseklik: 2110 },
];

const sunucu = http.createServer((istek, yanit) => {
  const adres = new URL(istek.url, "http://127.0.0.1");
  const gonder = (durum, govde, tur = "application/json") => {
    yanit.writeHead(durum, { "content-type": tur });
    yanit.end(typeof govde === "string" ? govde : JSON.stringify(govde));
  };
  if (adres.pathname === "/istasyonlar") return gonder(200, ISTASYONLAR);
  if (adres.pathname === "/istasyonlar/KY-99")
    return gonder(404, { kod: "istasyon_bulunamadi", ileti: "KY-99 kayıtlı değil" });
  if (adres.pathname === "/olcumler" && istek.method === "POST")
    return gonder(422, { kod: "dogrulama", alanlar: { deger: "aralik_disi" } });
  if (adres.pathname === "/rapor")
    return gonder(500, "<html>sunucu hatası</html>", "text/html");
  if (adres.pathname === "/yavas")
    return setTimeout(() => gonder(200, { hazir: true }), 300);
  return gonder(404, { kod: "yol_yok", ileti: "Bilinmeyen yol" });
});
await new Promise((coz) => sunucu.listen(0, "127.0.0.1", coz));
const TABAN = `http://127.0.0.1:${sunucu.address().port}`;

// --- Hata sozlesmesi --------------------------------------------------------
const ESLEME = new Map([
  [401, ["kimlik_yok", false]], [403, ["yetki_yok", false]],
  [404, ["bulunamadi", false]], [422, ["dogrulama", false]],
  [429, ["hiz_siniri", true]],  [500, ["sunucu", true]],
  [503, ["erisilemez", true]],
]);

const hata = (tur, kod, yenidenDenenebilir, durum = null, ayrinti = null) =>
  ({ tur, kod, yenidenDenenebilir, durum, ayrinti });

// --- Istek katmani ----------------------------------------------------------
async function istek(yol, secenekler = {}) {
  const { yontem = "GET", govde = null, zamanAsimi = 1000, taban = TABAN } = secenekler;
  const denetleyici = new AbortController();
  const sayac = setTimeout(() => denetleyici.abort("zaman_asimi"), zamanAsimi);

  let yanit;
  try {
    yanit = await fetch(taban + yol, {
      method: yontem,
      headers: govde ? { "content-type": "application/json" } : {},
      body: govde ? JSON.stringify(govde) : undefined,
      signal: denetleyici.signal,
    });
  } catch {
    // Alt katmanin iletisi ortama gore degisir; disariya kendi kodumuz cikar.
    return { ok: false, hata: denetleyici.signal.aborted
      ? hata("iptal", String(denetleyici.signal.reason), false)
      : hata("ag", "baglanti", true) };
  } finally {
    clearTimeout(sayac);
  }

  const tur = yanit.headers.get("content-type") ?? "";
  let cozulen = null;
  if (tur.startsWith("application/json")) {
    try { cozulen = await yanit.json(); }
    catch { return { ok: false, hata: hata("govde", "cozumlenemedi", false, yanit.status) }; }
  } else {
    await yanit.text();
  }

  if (!yanit.ok) {
    const [kod, yeniden] = ESLEME.get(yanit.status) ?? [
      yanit.status >= 500 ? "sunucu" : "istek", yanit.status >= 500];
    return { ok: false, hata: hata("http", kod, yeniden, yanit.status, cozulen) };
  }
  return { ok: true, veri: cozulen };
}

// --- Cagri noktalari --------------------------------------------------------
const yaz = (etiket, s) => console.log(
  etiket.padEnd(22),
  s.ok ? `ok    ${JSON.stringify(s.veri)}`
       : `hata  tur=${s.hata.tur} kod=${s.hata.kod} durum=${s.hata.durum} ` +
         `yeniden=${s.hata.yenidenDenenebilir}`);

yaz("istasyon listesi", await istek("/istasyonlar"));
yaz("bilinmeyen istasyon", await istek("/istasyonlar/KY-99"));
yaz("ölçüm gönderimi", await istek("/olcumler", { yontem: "POST", govde: { deger: 900 } }));
yaz("rapor (JSON değil)", await istek("/rapor"));
yaz("zaman aşımı", await istek("/yavas", { zamanAsimi: 50 }));
yaz("aynı yol, bol süre", await istek("/yavas", { zamanAsimi: 1000 }));

// Kapali bir baglanti noktasi: ag hatasi yolu.
const bos = http.createServer();
await new Promise((coz) => bos.listen(0, "127.0.0.1", coz));
const kapaliTaban = `http://127.0.0.1:${bos.address().port}`;
await new Promise((coz) => bos.close(coz));
yaz("sunucu yanıt vermiyor", await istek("/istasyonlar", { taban: kapaliTaban }));

sunucu.close();
```

```
istasyon listesi       ok    [{"kod":"KY-01","ad":"Kuzey Yamaç","yukseklik":1840},{"kod":"KY-02","ad":"Kuzey Yamaç Sırt","yukseklik":2110}]
bilinmeyen istasyon    hata  tur=http kod=bulunamadi durum=404 yeniden=false
ölçüm gönderimi        hata  tur=http kod=dogrulama durum=422 yeniden=false
rapor (JSON değil)     hata  tur=http kod=sunucu durum=500 yeniden=true
zaman aşımı            hata  tur=iptal kod=zaman_asimi durum=null yeniden=false
aynı yol, bol süre     ok    {"hazir":true}
sunucu yanıt vermiyor  hata  tur=ag kod=baglanti durum=null yeniden=true
```

Yedi satır, dört hata sınıfından üçünü gösteriyor. Dikkat edilecek üç nokta var.

Birincisi, **yanıtın gövdesi her durumda tüketilir**. Başarısız yanıtın gövdesi
okunmadan bırakılırsa bağlantı serbest kalmaz; içerik türü JSON değilse bile gövde metin
olarak okunup atılır. Beşinci satırdaki 500 yanıtı HTML döndürüyor — katman bunu çözmeye
çalışmıyor, durum kodundan karar veriyor.

İkincisi, 422 yanıtının gövdesi `ayrinti` alanına taşınır. Alan bazlı doğrulama hatalarını
forma geri bağlayacak olan Doğrulama Şemaları dersi bu alanı kullanacak; istek katmanı
içeriği yorumlamaz, yalnızca taşır.

Üçüncüsü, iptal ile ağ hatası aynı `catch` bloğuna düşer ama ayrı sınıflandırılır. Ayrımı
yapan şey yakalanan değer değil, **iptal işaretinin durumu**dur. İptal nedeni işaretin
üzerinde taşındığı için "kim iptal etti" sorusu da yanıtlanabilir; burada neden
`zaman_asimi` dizgisidir.

#### İptal, Zaman Aşımı ve Yarış

Eşzamansız JavaScript ve Çalışma Zamanı kursunda tanıtılan **iptal işareti (abort
signal)** bu katmanın belkemiğidir. İki ayrı ihtiyaca aynı düzenek hizmet eder: bir süre
sonra vazgeçmek ve sonucu artık istemediğimizde vazgeçmek.

Zaman aşımı, sunucunun yanıt vermemesi durumunda isteği sonsuza kadar açık tutmamayı
sağlar. Süresiz bir istek, kullanıcının bakış açısından hiç bitmeyen bir yükleme
göstergesidir. Sürenin ne olacağı işin niteliğine bağlıdır; istasyon listesi için kısa,
büyük bir rapor için uzun olur, bu yüzden katman süreyi çağrı başına alır.

İkinci ihtiyaç daha incedir. İstasyon listesinde bir süzgeç alanı varsa kullanıcı yazdıkça
istek üretilir. Üçüncü isteğin yanıtı ikincisinden önce gelirse ekranda eski sonuç kalır.
Çözümün bir yarısı, yeni istek başlarken öncekini iptal etmektir; katmanın dışarıdan bir
iptal işareti kabul etmesi bunu mümkün kılar. Diğer yarısı — geç gelen yanıtın yok
sayılması — görünüm tarafındadır ve Yükleme ve Hata Durumları dersinde ele alınır.

İptal edilen istek görünüme hata olarak yansımaz. Katmanın `iptal` sınıfını ayrı tutmasının
tek sebebi budur: çağrı noktası bu sonucu görünce hiçbir şey yapmaz, çünkü ekranı zaten
yeni bir istek dolduracaktır.

#### Kaynak Adresleri ve Sözleşmenin Sınırı

REST'in katman açısından anlamı sadedir: her kaynağın bir adresi vardır ve HTTP yöntemi ne
yapılacağını söyler. Koleksiyon `/istasyonlar`, tekil kayıt `/istasyonlar/KY-01`, bağlı
koleksiyon `/istasyonlar/KY-01/olcumler` olur. Süzme, sıralama ve sayfalama ölçütleri
adresin sorgu bölümünde taşınır; bunlar kaynağın kimliğini değil görünümünü belirler.

Bu düzenin istemci tarafında iki doğrudan sonucu vardır. Birincisi, **adres bir önbellek
anahtarıdır**: aynı adrese yapılan iki istek aynı kaydı ister ve sonucu paylaşabilir.
İkincisi, **yöntem yeniden denenebilirliği belirler**. İnternet Nasıl Çalışır kursunda
tanımlanan güvenli ve etkisiz (idempotent) yöntem ayrımı burada iş görür: bir okuma isteğini
tekrarlamak zararsızdır, bir ölçüm kaydını iki kez göndermek iki kayıt üretebilir.

Sözleşmenin sınırı da bu noktada belirir. İstemci, sunucunun kaynakları hangi
ayrıntı düzeyinde döndürdüğüne uymak zorundadır. Ölçüm geçmişi ekranı istasyon adını,
son ölçümü ve ölçüm listesini birlikte gösteriyorsa üç ayrı istek gerekebilir; tek bir
ekran için üç gidiş dönüş, gecikmenin doğrudan çarpanıdır. Sunucu bunu önlemek için
ekrana göre birleşik yanıtlar tasarlarsa bu kez kaynak modeli ekrana bağlanır ve her yeni
ekran yeni bir uç nokta ister.

#### Özet

- İstek katmanı taban adres, ortak başlıklar, gövde çözümleme ve hata yorumlama
  sorumluluklarını görünümden alıp tek yere toplar; önbellek, yeniden deneme ve yükleme
  göstergesi onun işi değildir.
- Hata sözleşmesi dört sınıf tanır: ağ, HTTP, gövde ve iptal. Ağ hatasında isteğin sunucuya
  ulaşıp ulaşmadığı belirsizdir; HTTP hatasında sunucunun kararı bilinir.
- Durum kodu eşlemesi tek bir tabloda tutulur ve tabloda olmayan kodlar sınıflarının
  anlamına göre karara bağlanır; 429, 4xx sınıfında olmasına rağmen yeniden denenebilirdir.
- Başarısız yanıtın gövdesi de tüketilir; sunucunun gönderdiği yapılandırılmış açıklama
  yorumlanmadan `ayrinti` alanında taşınır.
- İptal işareti hem zaman aşımını hem de vazgeçmeyi karşılar; iptal edilen istek görünüme
  hata olarak yansımaz.
- Kaynak adresi aynı zamanda önbellek anahtarıdır; HTTP yönteminin güvenli ve etkisiz
  olması yeniden denenebilirliği belirler.

#### Sonraki Adım

Bu dersin son bölümü bir gerilim bıraktı: ekranın istediği veri ile kaynağın döndürdüğü
veri aynı biçimde değil. Ölçüm geçmişi ekranı için üç istek atmak da, ekrana özel bir uç
nokta açmak da bedelli. Sorunun kökü, **ne isteneceğine sunucunun karar vermesi**. Sonraki
ders bu kararı istemciye veren bir sorgu diline bakar: istemci hangi alanları istediğini
yazar, tek istekte alır. Bunun karşılığında iki yeni sorun doğar — gelen iç içe yanıtın
önbellekte nasıl saklanacağı ve aynı alan kümesinin ekranlar arasında nasıl paylaşılacağı.

### GraphQL İstemcisi

Source: https://academia.sh/tr/kurslar/frontend-mimarisi/graphql-istemcisi

Alan seçiminin istemciye geçmesi, tekrar eden alan kümelerinin parçalarla adlandırılması, iç içe yanıtın kimlik tabanlı bir depoya normalleştirilmesi ve kısmi başarının hata sözleşmesine katılması.

Önceki ders bir gerilimle bitti: kaynağın döndürdüğü alanlarla ekranın istediği alanlar
çakışmıyor. Kuzey Yamaç istasyon kartı istasyonun adını, son ölçümü ve son iki ölçüm
satırını birlikte gösteriyor; REST kaynak modelinde bu üç ayrı adres demek. Alternatif —
ekrana özel bir uç nokta — sunucuyu arayüzün biçimine bağlar.

Bu ders, kararı yer değiştiren bir yaklaşımı ele alır. GraphQL'de sunucu bir **şema**
yayımlar, istemci o şemadan hangi alanları istediğini yazar. Tek uç nokta, tek istek, tam
olarak istenen alanlar. Kazanç açık; bedel, istemcinin şimdi kendi başına çözmesi gereken
iki yeni problem: aynı alan kümesinin ekranlar arasında paylaşılması ve gelen iç içe
yanıtın saklanması.

#### Alan Seçiminin İstemciye Geçmesi

Bir sorgu belgesi, şemadaki alanların bir alt ağacını seçer. `istasyon(kod: "KY-01")`
alanının altında `ad` ve `yukseklik` istenirse yanıt yalnız o ikisini taşır; `sonOlcum`
istenirse aynı istekte o da gelir. İç içe alanlar tek gidiş dönüşte toplanır, bu yüzden
gecikmenin ekran başına çarpanı ortadan kalkar.

Üç yapısal fark istek katmanını etkiler. Birincisi, **adres artık ayırt edici değildir**:
tüm sorgular aynı uç noktaya gider ve genellikle POST ile gönderilir. Önceki dersin
"adres bir önbellek anahtarıdır" kuralı burada geçersizdir; anahtar, sorgu belgesi ile
değişkenlerinin birleşiminden üretilir.

İkincisi, **başarı ve başarısızlık aynı yanıtta bulunabilir**. Sorgudaki bir alan
çözümlenemezse HTTP durumu 200 kalır, gövde hem kısmi `data` hem de `errors` listesi
taşır. Bu, önceki dersteki dört sınıflı hata sözleşmesinin kapsamadığı bir durumdur ve
ders sonunda ele alınacaktır.

Üçüncüsü, **sorgunun maliyeti istemcinin elindedir**. Derin bir iç içe seçim, sunucuda
kayıt başına ek okuma üretebilir; şemada bir liste alanının altında yine bir liste alanı
seçmek maliyeti çarpar. Sunucu bunu derinlik ve karmaşıklık sınırlarıyla dizginler;
istemci tarafında bunun karşılığı, ekranın gerçekten gösterdiği alanları istemektir.

#### Parça: Alan Kümesinin Adı

Aynı alan kümesi birden çok ekranda görünür. Ölçüm satırı hem istasyon kartında hem
ölçüm geçmişi listesinde aynı beş alanı ister. Bu kümeyi her sorguya elle yazmak, bir alan
eklendiğinde bazı ekranların geride kalması demektir.

**Sorgu parçası (fragment)**, bir tür üzerinde adlandırılmış bir alan kümesidir. Sorgu
parçayı adıyla çağırır, belge kurulurken tanımı yanına eklenir. Aşağıdaki dosya bu
birleştirmeyi saf dizgi işlemi olarak yapar: parça çağrılarını bulur, bağımlılıkları
özyinelemeli izler ve her tanımı belgeye bir kez koyar.

```js
// parca-birlestirme.mjs — sorgu parcalarindan belge kurma
const PARCALAR = {
  IstasyonOzeti: {
    tur: "Istasyon",
    govde: ["__typename", "kod", "ad", "yukseklik"],
  },
  OlcumSatiri: {
    tur: "Olcum",
    govde: ["__typename", "kimlik", "zaman", "deger", "birim"],
  },
  IstasyonKarti: {
    tur: "Istasyon",
    govde: ["...IstasyonOzeti", "sonOlcum { ...OlcumSatiri }"],
  },
};

// Bir govdedeki parca cagrilarini bulur: "...Ad" bicimindeki her sozcuk.
const cagrilar = (satirlar) =>
  satirlar.flatMap((s) => [...s.matchAll(/\.\.\.(\w+)/g)].map((e) => e[1]));

function gerekliParcalar(kokler, toplanan = new Set()) {
  for (const ad of kokler) {
    if (toplanan.has(ad)) continue;          // ayni parca iki kez yazilmaz
    const parca = PARCALAR[ad];
    if (!parca) throw new Error(`tanimsiz parça: ${ad}`);
    toplanan.add(ad);
    gerekliParcalar(cagrilar(parca.govde), toplanan); // ic ice bagimliliklar
  }
  return toplanan;
}

function belgeKur(ad, govde) {
  const adlar = gerekliParcalar(cagrilar(govde));
  const tanimlar = [...adlar].map((a) => {
    const p = PARCALAR[a];
    return `fragment ${a} on ${p.tur} {\n  ${p.govde.join("\n  ")}\n}`;
  });
  return [`query ${ad} {\n  ${govde.join("\n  ")}\n}`, ...tanimlar].join("\n\n");
}

const belge = belgeKur("IstasyonSayfasi", [
  "istasyon(kod: \"KY-01\") {",
  "  ...IstasyonKarti",
  "  olcumler(son: 2) { ...OlcumSatiri }",
  "}",
]);

console.log(belge);
console.log("---");
console.log("belgeye giren parça sayısı:", (belge.match(/^fragment /gm) ?? []).length);
```

```
query IstasyonSayfasi {
  istasyon(kod: "KY-01") {
    ...IstasyonKarti
    olcumler(son: 2) { ...OlcumSatiri }
  }
}

fragment IstasyonKarti on Istasyon {
  ...IstasyonOzeti
  sonOlcum { ...OlcumSatiri }
}

fragment IstasyonOzeti on Istasyon {
  __typename
  kod
  ad
  yukseklik
}

fragment OlcumSatiri on Olcum {
  __typename
  kimlik
  zaman
  deger
  birim
}
---
belgeye giren parça sayısı: 3
```

`OlcumSatiri` belgede iki yerden çağrılıyor — biri doğrudan sorgudan, biri
`IstasyonKarti` içinden — ama tanımı bir kez yazılıyor. Bu, tekilleştirmenin gerçek
faydasıdır: alan kümesi tek yerde durur.

Parçanın asıl tasarım değeri, **alan ihtiyacının onu kullanan parçanın yanında
bildirilmesidir**. Ölçüm satırını çizen kod hangi alanlara ihtiyaç duyduğunu kendi
parçasında yazar; sayfa sorgusu bunu bilmez, yalnız çağırır. Ekrana yeni bir alan
eklendiğinde tek bir yer değişir ve o parçayı kullanan her sorgu alanı almaya başlar.

Her parçaya `__typename` alanının konması rastgele değildir. Bir sonraki bölümdeki depo,
kayıtları türle kimliğin birleşiminden anahtarlar; tür bilgisi yanıtta yoksa anahtar
üretilemez.

#### Önbellek Normalizasyonu

Yanıt iç içe bir ağaçtır. Aynı ölçüm kaydı hem `sonOlcum` altında hem `olcumler`
listesinde görünür; iki kopya olarak saklanırsa biri güncellenip diğeri eskiyebilir.
**Normalizasyon**, ağacı kimlik anahtarlı düz bir depoya çevirir: her kayıt bir kez durur,
ağaçtaki yerinde ise ona bir başvuru kalır.

```js
// normalizasyon.mjs — ic ice yaniti duz depoya cevirme, birlestirme ve geri okuma
const KIMLIK_ALANI = { Istasyon: "kod", Olcum: "kimlik" };

const anahtar = (nesne) => {
  const alan = KIMLIK_ALANI[nesne?.__typename];
  return alan && nesne[alan] != null ? `${nesne.__typename}:${nesne[alan]}` : null;
};

function normalize(deger, depo) {
  if (Array.isArray(deger)) return deger.map((e) => normalize(e, depo));
  if (deger === null || typeof deger !== "object") return deger;

  const alanlar = {};
  for (const [ad, alt] of Object.entries(deger)) alanlar[ad] = normalize(alt, depo);

  const kimlik = anahtar(deger);
  if (!kimlik) return alanlar;                            // kimliksiz nesne yerinde kalir
  depo[kimlik] = { ...(depo[kimlik] ?? {}), ...alanlar };  // alan bazinda birlestirme
  return { __ref: kimlik };
}

// Okuma her zaman bir secim kumesiyle yapilir; depo cevrimli oldugu icin
// secimsiz dolasma biter bir sey degildir.
function oku(secim, deger, depo) {
  if (Array.isArray(deger)) return deger.map((e) => oku(secim, e, depo));
  if (deger === null || typeof deger !== "object") return deger;
  const kaynak = deger.__ref ? depo[deger.__ref] : deger;
  if (!kaynak) return { __eksik: deger.__ref };            // depoda yok
  const cikti = {};
  for (const [ad, alt] of Object.entries(secim)) {
    if (!(ad in kaynak)) { cikti[ad] = { __eksik: ad }; continue; }
    cikti[ad] = alt === true ? kaynak[ad] : oku(alt, kaynak[ad], depo);
  }
  return cikti;
}

const depo = {};
const kokler = {};

// --- Birinci sorgu: istasyon karti + son iki olcum --------------------------
const yanit1 = {
  istasyon: {
    __typename: "Istasyon", kod: "KY-01", ad: "Kuzey Yamaç", yukseklik: 1840,
    sonOlcum: {
      __typename: "Olcum", kimlik: "o-114", zaman: "2026-01-14T06:00Z",
      deger: -4.2, birim: "C",
      istasyon: { __typename: "Istasyon", kod: "KY-01", ad: "Kuzey Yamaç" },
    },
    olcumler: [
      { __typename: "Olcum", kimlik: "o-114", zaman: "2026-01-14T06:00Z", deger: -4.2, birim: "C" },
      { __typename: "Olcum", kimlik: "o-113", zaman: "2026-01-13T06:00Z", deger: -1.8, birim: "C" },
    ],
  },
};
kokler['istasyon(kod:"KY-01")'] = normalize(yanit1.istasyon, depo);

console.log("depo anahtarları:", Object.keys(depo).join("  "));
console.log("KY-01           :", JSON.stringify(depo["Istasyon:KY-01"]));
console.log("o-114           :", JSON.stringify(depo["Olcum:o-114"]));

// --- Ikinci sorgu: baska bir ekran, ayni olcumu farkli alanlarla ister ------
const yanit2 = { olcum: { __typename: "Olcum", kimlik: "o-114", deger: -4.4, kaydeden: "gözlemci-3" } };
kokler['olcum(kimlik:"o-114")'] = normalize(yanit2.olcum, depo);

console.log("--- ikinci yanıttan sonra ---");
console.log("o-114           :", JSON.stringify(depo["Olcum:o-114"]));
console.log("kayıt sayısı    :", Object.keys(depo).length);

// --- Birinci ekran, kendi secim kumesiyle depodan yeniden okunur -----------
const KART = {
  kod: true, ad: true,
  sonOlcum: { kimlik: true, deger: true, birim: true },
  olcumler: { kimlik: true, deger: true },
};
console.log("--- birinci ekranın yeni görüntüsü ---");
console.log(JSON.stringify(oku(KART, kokler['istasyon(kod:"KY-01")'], depo)));

// --- Deponun tasimadigi alan istenirse ------------------------------------
const RAPOR = { kod: true, sonOlcum: { kimlik: true, sapma: true } };
console.log("--- eksik alan isteyen ekran ---");
console.log(JSON.stringify(oku(RAPOR, kokler['istasyon(kod:"KY-01")'], depo)));
```

```
depo anahtarları: Istasyon:KY-01  Olcum:o-114  Olcum:o-113
KY-01           : {"__typename":"Istasyon","kod":"KY-01","ad":"Kuzey Yamaç","yukseklik":1840,"sonOlcum":{"__ref":"Olcum:o-114"},"olcumler":[{"__ref":"Olcum:o-114"},{"__ref":"Olcum:o-113"}]}
o-114           : {"__typename":"Olcum","kimlik":"o-114","zaman":"2026-01-14T06:00Z","deger":-4.2,"birim":"C","istasyon":{"__ref":"Istasyon:KY-01"}}
--- ikinci yanıttan sonra ---
o-114           : {"__typename":"Olcum","kimlik":"o-114","zaman":"2026-01-14T06:00Z","deger":-4.4,"birim":"C","istasyon":{"__ref":"Istasyon:KY-01"},"kaydeden":"gözlemci-3"}
kayıt sayısı    : 3
--- birinci ekranın yeni görüntüsü ---
{"kod":"KY-01","ad":"Kuzey Yamaç","sonOlcum":{"kimlik":"o-114","deger":-4.4,"birim":"C"},"olcumler":[{"kimlik":"o-114","deger":-4.4},{"kimlik":"o-113","deger":-1.8}]}
--- eksik alan isteyen ekran ---
{"kod":"KY-01","sonOlcum":{"kimlik":"o-114","sapma":{"__eksik":"sapma"}}}
```

Çıktı dört şey söylüyor.

**Depo düzdür ve tekrar barındırmaz.** İlk yanıtta ölçüm `o-114` iki yerde görünüyordu;
depoda tek kayıt var, diğer yerlerde `__ref` başvurusu duruyor. İstasyon da aynı biçimde
tekilleşti: ölçümün içindeki gömülü istasyon nesnesi ayrı bir kayıt üretmedi, var olanla
birleşti.

**Birleştirme alan bazındadır.** İkinci sorgu ölçümün yalnız iki alanını istedi ama depo
kaydı beş alanını korudu ve yeni gelen `kaydeden` alanını ekledi. Yeni değer `-4.4`,
eskisinin üzerine yazıldı. Kısmi bir yanıt, saklanan kaydı budamaz.

**Bir ekranın verisi başka bir ekranın isteğiyle tazelenir.** İstasyon kartı yeniden
istek atmadı; depo değiştiği için kartın okuduğu değer `-4.4` oldu. Normalize edilmemiş
bir önbellekte bu güncelleme yalnız ikinci ekrana yansırdı.

**Okuma bir seçim kümesi gerektirir.** Depo çevrimlidir: istasyon ölçüme, ölçüm istasyona
başvurur. Seçimsiz bir dolaşma sonlanmaz. Okuyucu her düzeyde hangi alanların istendiğini
bilmek zorundadır — yani sorgu belgesi yalnız istek anında değil, okuma anında da gerekir.
Son satır bunun devamını gösterir: depoda olmayan bir alan istendiğinde okuma **eksik**
olduğunu bildirir; bu durumda sunucuya gitmek gerekir.

#### Normalizasyonun Sınırları

Depo yalnız kimliği olan kayıtları tekilleştirir. Kimliksiz nesneler — bir toplam, bir
sayfa bilgisi, hesaplanmış bir özet — ait oldukları kaydın içinde kalır. Şemada bir tür
için kimlik alanı yoksa ya da alan adı türden türe değişiyorsa, eşleme tablosunun (burada
`KIMLIK_ALANI`) elle bakımı gerekir.

İkinci sınır listelerdir. `olcumler(son: 2)` ile `olcumler(son: 20)` aynı alanın iki farklı
sonucudur; ikisi tek bir alan değeri altında saklanamaz. Bu yüzden liste alanları
**argümanlarıyla birlikte** anahtarlanır. Sayfalama söz konusuysa bir de birleştirme
kuralı gerekir: yeni sayfa öncekinin üstüne mi yazılacak, sonuna mı eklenecek. Bu karar
alan bazında verilir ve depo bunu bilemez, uygulamanın söylemesi gerekir.

Üçüncü sınır silmedir. Bir ölçüm silindiğinde depo kaydını atmak yetmez; o kayda başvuran
listelerin de temizlenmesi gerekir. Aksi hâlde okuma `__eksik` döndürür ve ekranda boşluk
belirir. Değişiklik işlemlerinden sonra hangi kök alanların geçersizleştirileceği,
Durum Yönetimi konusundaki geçersizleştirme kuralının bu bağlamdaki karşılığıdır.

#### Kısmi Başarı ve Hata Sözleşmesi

Önceki dersin sözleşmesi dört sınıf tanıyordu: ağ, HTTP, gövde, iptal. Sorgu tabanlı
erişim beşinci bir durum getirir: **taşıma başarılı, alan başarısız**. Yanıt 200 döner,
`data` içinde istenen alanların bir kısmı `null` olur ve `errors` listesi hangi yolda ne
olduğunu söyler.

Bu yanıtı tek bir "hata" ya da tek bir "başarı" saymak ikisi de yanlıştır. İstek katmanı
üç bilgiyi ayrı taşımalıdır: elde edilen kısmi veri, başarısız alan yolları ve bu
başarısızlığın kullanıcıya gösterilip gösterilmeyeceği. İstasyon adı geldi ama son ölçüm
alanı boş döndüyse kart çizilebilir; ölçüm bölümünün yerine hata bildirimi konur. Tüm
ekranı hata durumuna düşürmek, elde olan veriyi çöpe atar.

Ayrım şu soruyla yapılır: **ekranın anlamlı olması için hangi alanlar zorunlu?** Bu soru
sunucu tarafından yanıtlanamaz; hangi alanın vazgeçilebilir olduğunu ekranı çizen kod
bilir. Bu yüzden kısmi başarı, istek katmanının otomatik olarak karara bağlayabileceği bir
şey değildir — taşınır, karar çağrı noktasında verilir.

Alan düzeyindeki hataların yanında protokol düzeyindeki hatalar da vardır: geçersiz sorgu
belgesi, şemada olmayan alan, tip uyumsuzluğu. Bunlar sorgu hiç çalıştırılmadan bildirilir
ve bir kullanıcı hatası değil, bir program hatasıdır; kullanıcıya gösterilecek iletisi
yoktur, geliştirme sırasında yakalanması gerekir.

#### Özet

- Sorgu tabanlı erişimde alan seçimi istemciye geçer; tek uç nokta ve tek istek, ekran
  başına gidiş dönüş sayısını ekranın karmaşıklığından bağımsızlaştırır.
- Adres artık ayırt edici değildir; önbellek anahtarı sorgu belgesi ile değişkenlerin
  birleşiminden üretilir.
- Sorgu parçası, bir tür üzerinde adlandırılmış alan kümesidir; alan ihtiyacı onu kullanan
  kodun yanında bildirilir ve belgeye bir kez yazılır.
- Normalizasyon iç içe yanıtı tür ile kimliğin birleştiği anahtarlarla düz bir depoya
  çevirir; birleştirme alan bazındadır ve bir ekranın isteği diğerinin verisini tazeler.
- Normalleştirilmiş depo çevrimlidir; okuma her zaman bir seçim kümesiyle yapılır ve
  depoda bulunmayan alan eksik olarak bildirilir.
- Kısmi başarı beşinci bir hata durumudur: kısmi veri, başarısız alan yolları ve
  zorunluluk kararı ayrı taşınır; hangi alanın vazgeçilebilir olduğunu ekran bilir.

#### Sonraki Adım

Buraya kadar veri her zaman istemcinin sorduğu anda geldi: sayfa açıldı, istek atıldı,
yanıt işlendi. Kuzey Yamaç istasyonu ise ölçümü kimse sormadan üretir. Ölçüm geçmişi
ekranı açıkken yeni bir kayıt oluşursa, kullanıcının bunu görmesi için sayfayı yenilemesi
gerekmemeli. Bu, veri akışının yönünü tersine çevirir: bağlantıyı istemci kurar ama
zamanlamayı sunucu belirler. Sonraki ders bu tersine akışın iki taşıyıcısını — tek yönlü
olay akışını ve çift yönlü soketi — ve gelen olayların depoya nasıl işleneceğini ele alır.

### Gerçek Zamanlı Veri

Source: https://academia.sh/tr/kurslar/frontend-mimarisi/gercek-zamanli-veri

Sunucunun kendiliğinden ürettiği verinin istemciye taşınması; yoklama, olay akışı ve soket arasındaki seçim, akış biçiminin çözümlenmesi, kopan bağlantının kimlikle devam ettirilmesi ve olayların duruma işlenmesi.

Buraya kadar veri hep istemcinin sorduğu anda geldi. Kuzey Yamaç istasyonu ise ölçümü
kimse sormadan üretir: sensör on dakikada bir okuma yapar, sunucu kaydı işler. Ölçüm
geçmişi ekranı açıkken üretilen bir kaydın görünmesi için kullanıcının sayfayı yenilemesi
gerekiyorsa, ekran gerçeği geriden izler.

Bu ders akışın yönünü tersine çeviren taşımaları ele alır. Ortak nokta şudur: bağlantıyı
her zaman istemci kurar — tarayıcıya dışarıdan bağlantı açılamaz — ama veri gönderme
zamanını sunucu belirler. Farkları, bağlantının ne kadar sürdüğünde ve kaç yöne
işlediğindedir.

#### Üç Taşıma Yolu

| Yol | Yön | Bağlantı | Yeniden bağlanma | Uygun olduğu iş |
|---|---|---|---|---|
| Yoklama | istemci sorar | her seferinde yeni | gerekmez | seyrek ve gecikmeye toleranslı veri |
| Uzun yoklama | istemci sorar | yanıt gelene dek açık | her yanıttan sonra | olay seyrek, gecikme önemli |
| Olay akışı | sunucu iter | tek uzun HTTP yanıtı | yerleşik | tek yönlü bildirim akışı |
| Soket | iki yönlü | tek uzun bağlantı | elle kurulur | karşılıklı ve sık iletişim |

**Yoklama (polling)**, belirli aralıklarla istek atmaktır. Kurulumu en ucuz yoldur ve
önceki iki dersin istek katmanı olduğu gibi kullanılır. Bedeli iki yerdedir: veri
değişmemişken de istek atılır ve en kötü durumda gecikme aralık kadardır. Otuz saniyede
bir yoklanan bir ekran, olayı en geç otuz saniye sonra görür. Koşullu istek başlıklarıyla
— Tarayıcı ve Web Platformu kursunda tanıtılan doğrulayıcılarla — gövde aktarımı
azaltılabilir, ama istek sayısı azalmaz.

**Uzun yoklama (long polling)**, sunucunun yanıtı veri oluşana kadar bekletmesidir. İstek
sayısı olay sayısına iner, gecikme kaybolur. Karşılığında her olaydan sonra yeni bir istek
kurulur ve sunucu tarafında açık bekleyen isteklerin tutulması gerekir.

**Olay akışı (Server-Sent Events)**, tek bir HTTP yanıtının kapanmadan sürmesidir. Sunucu
aynı yanıt gövdesine metin parçaları yazmaya devam eder, istemci geldikçe okur. Sıradan
bir HTTP yanıtı olduğu için ara katmanlarla — vekil sunucular, sıkıştırma, kimlik
doğrulama başlıkları — uyumludur. Yalnız tek yönlüdür: istemci bu kanaldan bir şey
gönderemez, göndermek istediğinde ayrı bir istek atar.

**Soket (WebSocket)**, HTTP üzerinden başlayıp protokol değiştirerek kurulan çift yönlü
bir kanaldır. İki taraf da istediği an ileti gönderir. Karşılıklı etkileşim gerektiren
işlerde — birlikte düzenleme, canlı imleç, oyun — tek seçenektir.

Seçim kuralı sadedir: veri tek yönde akıyorsa olay akışı yeterlidir ve daha az parça
gerektirir. İstemcinin de aynı kanaldan sık ileti göndermesi gerekiyorsa soket kurulur.

#### Olay Akışının Biçimi

Akış, satır tabanlı bir metin biçimidir. Her satır `alan: değer` yazar, boş satır olayı
tamamlar. Dört alan tanımlıdır: `event` olayın adını, `data` gövdesini, `id` sıra
kimliğini, `retry` yeniden bağlanma aralığını verir. İki nokta ile başlayan satır
yorumdur ve yok sayılır — bağlantıyı canlı tutan kalp atışı böyle gönderilir.

Aşağıdaki dosya hem akış üreten bir sunucu kurar hem de çözümleyiciyi ona karşı
çalıştırır. Bağlantı noktasını işletim sistemi seçer; sayı çalıştıran makineye göre
değişir ve çıktıda görünmez.

```js
// olay-akisi.mjs — text/event-stream ureten yerel sunucu + akis cozumleyici
import http from "node:http";

// --- Sunucu: son kimlikten sonrasini yayimlar -------------------------------
const OLCUMLER = [
  { kimlik: 1, kod: "KY-01", deger: -4.2 },
  { kimlik: 2, kod: "KY-02", deger: -6.1 },
  { kimlik: 3, kod: "KY-01", deger: -4.4 },
  { kimlik: 4, kod: "KY-02", deger: -5.9 },
];

const sunucu = http.createServer((istek, yanit) => {
  const sonKimlik = Number(istek.headers["last-event-id"] ?? 0);
  yanit.writeHead(200, {
    "content-type": "text/event-stream",
    "cache-control": "no-store",
    connection: "keep-alive",
  });
  yanit.write("retry: 3000\n\n");            // yeniden baglanma araligi onerisi
  yanit.write(": akış açık\n\n");            // yorum satiri: kalp atisi
  for (const o of OLCUMLER.filter((o) => o.kimlik > sonKimlik)) {
    yanit.write(`id: ${o.kimlik}\n`);
    yanit.write("event: olcum\n");
    yanit.write(`data: ${JSON.stringify(o)}\n\n`);
  }
  yanit.end();
});
await new Promise((coz) => sunucu.listen(0, "127.0.0.1", coz));
const TABAN = `http://127.0.0.1:${sunucu.address().port}`;

// --- Akis cozumleyici: alan satirlarindan olay uretir -----------------------
function cozumleyici(uygula) {
  let tampon = "";
  let olay = { ad: "message", veri: [], kimlik: null, yenidenDeneme: null };
  const sifirla = () => { olay = { ad: "message", veri: [], kimlik: null, yenidenDeneme: null }; };

  return function besle(parca) {
    tampon += parca;
    let kesme;
    while ((kesme = tampon.indexOf("\n")) !== -1) {
      const satir = tampon.slice(0, kesme);
      tampon = tampon.slice(kesme + 1);

      if (satir === "") {                        // bos satir: olayi gonder
        if (olay.veri.length > 0) uygula({ ...olay, veri: olay.veri.join("\n") });
        else if (olay.yenidenDeneme !== null) uygula({ ...olay, veri: null });
        sifirla();
        continue;
      }
      if (satir.startsWith(":")) continue;       // yorum satiri yok sayilir

      const ikiNokta = satir.indexOf(":");
      const alan = ikiNokta === -1 ? satir : satir.slice(0, ikiNokta);
      let deger = ikiNokta === -1 ? "" : satir.slice(ikiNokta + 1);
      if (deger.startsWith(" ")) deger = deger.slice(1);

      if (alan === "event") olay.ad = deger;
      else if (alan === "data") olay.veri.push(deger);
      else if (alan === "id" && !deger.includes("\0")) olay.kimlik = deger;
      else if (alan === "retry" && /^\d+$/.test(deger)) olay.yenidenDeneme = Number(deger);
    }
  };
}

// --- Tuketici ---------------------------------------------------------------
async function akisiTuket(sonKimlik, enCok) {
  const alinan = [];
  const denetleyici = new AbortController();
  let dur = false;
  const besle = cozumleyici((olay) => {
    if (dur) return;                       // yeterince alindi: kalani cozumleme
    alinan.push(olay);
    if (alinan.filter((o) => o.veri !== null).length >= enCok) {
      dur = true;
      denetleyici.abort("yeter");
    }
  });

  const yanit = await fetch(`${TABAN}/akis`, {
    headers: sonKimlik ? { "last-event-id": String(sonKimlik) } : {},
    signal: denetleyici.signal,
  });
  const cozucu = new TextDecoder();
  try {
    for await (const yigin of yanit.body) besle(cozucu.decode(yigin, { stream: true }));
  } catch {
    if (!denetleyici.signal.aborted) throw new Error("akış koptu");
  }
  return alinan;
}

const yaz = (o) => console.log(
  `  ad=${o.ad.padEnd(7)} kimlik=${String(o.kimlik).padEnd(4)}` +
  ` yenidenDeneme=${String(o.yenidenDeneme).padEnd(4)} veri=${o.veri}`);

console.log("ilk bağlantı (ilk iki olaydan sonra iptal):");
const birinci = await akisiTuket(0, 2);
birinci.forEach(yaz);

const sonAlinan = birinci.filter((o) => o.kimlik !== null).at(-1).kimlik;
console.log(`son alınan kimlik: ${sonAlinan}`);

console.log("yeniden bağlantı (Last-Event-ID ile):");
const ikinci = await akisiTuket(sonAlinan, 99);
ikinci.forEach(yaz);

// --- Olaylarin depoya islenmesi: tekrar eden kimlik yok sayilir -------------
const depo = new Map();
const gorulen = new Set();
let yoksayilan = 0;
for (const olay of [...birinci, ...ikinci, ...birinci]) {  // ilk parti bilerek tekrar
  if (olay.veri === null || olay.ad !== "olcum") continue;
  if (gorulen.has(olay.kimlik)) { yoksayilan++; continue; }
  gorulen.add(olay.kimlik);
  const o = JSON.parse(olay.veri);
  depo.set(o.kod, o);
}
console.log("işlenen olay:", gorulen.size, "| yok sayılan tekrar:", yoksayilan);
console.log("son durum   :", JSON.stringify([...depo.values()]));

sunucu.close();
```

```
ilk bağlantı (ilk iki olaydan sonra iptal):
  ad=message kimlik=null yenidenDeneme=3000 veri=null
  ad=olcum   kimlik=1    yenidenDeneme=null veri={"kimlik":1,"kod":"KY-01","deger":-4.2}
  ad=olcum   kimlik=2    yenidenDeneme=null veri={"kimlik":2,"kod":"KY-02","deger":-6.1}
son alınan kimlik: 2
yeniden bağlantı (Last-Event-ID ile):
  ad=message kimlik=null yenidenDeneme=3000 veri=null
  ad=olcum   kimlik=3    yenidenDeneme=null veri={"kimlik":3,"kod":"KY-01","deger":-4.4}
  ad=olcum   kimlik=4    yenidenDeneme=null veri={"kimlik":4,"kod":"KY-02","deger":-5.9}
işlenen olay: 4 | yok sayılan tekrar: 2
son durum   : [{"kimlik":3,"kod":"KY-01","deger":-4.4},{"kimlik":4,"kod":"KY-02","deger":-5.9}]
```

#### Kopmayı Kabul Etmek

Uzun süren bağlantı kopar. Kablosuz ağ değişir, aradaki vekil sunucu boşta kalan bağlantıyı
kapatır, dizüstü bilgisayar uyanır. Bu bir hata değil, akış tabanlı çalışmanın normal
durumudur; tasarım kopmayı önlemeye değil, kopmadan sonra doğru yerden devam etmeye
yönelir.

Devamı sağlayan alan `id`'dir. İstemci aldığı son kimliği saklar, yeniden bağlanırken
`Last-Event-ID` başlığıyla gönderir, sunucu o kimlikten sonrasını yayımlar. Çıktıda bu
akış görünüyor: ilk bağlantı iki olaydan sonra kesildi, ikinci bağlantı üç ve dörtten
başladı. Bu düzenek çalışsın diye sunucunun **yayımladığı olayları bir süre saklaması**
gerekir; saklamıyorsa kopma aralığındaki olaylar kaybolur ve istemcinin yeniden bağlandıktan
sonra tam durumu bir kez daha çekmesi gerekir.

`retry` alanı yeniden bağlanma aralığını milisaniye olarak önerir. Kendi çözümleyicisini
yazan kod bu değeri dikkate almalı ve sabit bir aralık yerine artan bir bekleme
uygulamalıdır; sunucu çöktüğünde bütün istemcilerin aynı aralıkla dönmesi, ayağa kalkmaya
çalışan sunucuyu yeniden düşürür. Bu hesabın kendisi bir sonraki dersin konusudur.

Son iki satır üçüncü kuralı gösteriyor: **aynı olay iki kez gelebilir**. Yeniden bağlanma
sırasında sunucu son kimliği kapsayıcı yorumlayabilir, ya da istemci kaydettiği kimliği
yazamadan kopabilir. İşleme kodu bu yüzden **etkisiz (idempotent)** olmalıdır: aynı olayı iki kez
uygulamak, bir kez uygulamakla aynı sonucu vermelidir. Örnekte bu, görülen kimliklerin
kümede tutulmasıyla sağlanıyor; ilk partinin tekrarı yok sayıldı ve son durum değişmedi.

Değişmezliğin ikinci yolu, olayı bir komut değil bir **duyuru** olarak yazmaktır. "Ölçüm
sayacını bir artır" iki kez uygulandığında bozulur; "KY-01 istasyonunun son ölçümü
`o-114`, değeri −4.4" iki kez uygulandığında aynı sonucu verir. Akış üzerinden taşınan
olaylar mümkün olduğunca ikinci biçimde tasarlanır.

#### Çift Yönlü Soketin Farkları

Soket, sıradan bir HTTP isteğiyle başlar ve sunucu kabul ederse bağlantı protokol
değiştirir. Bu andan sonra HTTP semantiği — yöntem, durum kodu, başlıklar — geçerli
değildir; kanal üzerinde metin ya da ikili iletiler taşınır. Uygulamanın kendi ileti
biçimini tanımlaması gerekir; hangi alanın ileti türünü söylediği, sıra numarasının nerede
durduğu istemci ile sunucunun anlaşmasına kalmıştır.

Dört noktada olay akışından ayrılır. **Yeniden bağlanma yerleşik değildir**; kopmayı
saptamak ve yeniden kurmak uygulamanın işidir. **Kopma her zaman fark edilmez**; iletim
sessizce kesildiğinde bağlantı açık görünmeye devam edebilir, bu yüzden iki taraf
düzenli aralıklarla yoklama iletisi gönderip yanıt bekler ve yanıt gelmezse bağlantıyı
ölü sayar. **Sıraya alma sorumluluğu istemcidedir**: bağlantı kopukken gönderilmek istenen
iletiler bir kuyrukta bekletilir, bağlantı kurulunca boşaltılır — ya da bilinçli olarak
atılır. **Kimlik doğrulaması ilk el sıkışmada yapılır**; kanal açıldıktan sonra
başlık gönderilemez, bu yüzden oturumun kanal ömrü boyunca geçerliliği ayrı bir sorundur
ve Oturum Yenileme dersinde ele alınır.

#### Akışın Duruma İşlenmesi

Gelen olayın tek başına bir anlamı yoktur; anlamı, üzerine uygulandığı duruma göre oluşur.
Üç sorun her uygulamada karşımıza çıkar.

**Başlangıç durumu ile akışın buluşması.** Ekran açıldığında önce tam liste çekilir, sonra
akış dinlenir. İki işlem arasında geçen sürede üretilen olaylar kaçar. Doğru sıra
tersidir: önce akış açılır ve gelen olaylar tampona alınır, sonra tam liste çekilir,
listenin damgasından sonraki tampon olayları uygulanır, tampon boşaltılır.

**Sıra dışı gelme.** Olaylar farklı yollardan gelirse — biri akıştan, biri kullanıcının
kendi işleminin yanıtından — daha eski bir değer daha yeninin üstüne yazılabilir. Çözüm,
her kaydın bir sürüm ya da zaman damgası taşıması ve işleme kodunun **yalnız daha yeni
damgayı** kabul etmesidir.

**Görünmeyen sekme.** Arka plandaki bir sekmede akışı açık tutmak, hem sunucuda bağlantı
tüketir hem istemcide iş üretir. Görünürlük değiştiğinde akış kapatılır, sekme öne
geldiğinde son kimlikten devam edilerek yeniden açılır. Bu, aynı zamanda kaçırılan
olayların telafi yolunu kullanmanın doğal bir örneğidir.

#### Özet

- Bağlantıyı her zaman istemci kurar; taşımaların farkı bağlantının süresinde ve yön
  sayısındadır. Yoklama en ucuz kurulumdur, gecikmesi aralığı kadardır.
- Olay akışı tek yönlü ve sıradan bir HTTP yanıtıdır; soket çift yönlüdür ve protokol
  değiştirerek kurulur, karşılığında yeniden bağlanma ile canlılık denetimini uygulamaya
  bırakır.
- Akış biçimi satır tabanlıdır: `event`, `data`, `id` ve `retry` alanları, boş satırla
  tamamlanan olaylar, yorum satırıyla gönderilen kalp atışı.
- Kopma normaldir; son olay kimliği saklanır ve yeniden bağlanmada `Last-Event-ID` ile
  bildirilir. Sunucu yayımladığı olayları bir süre saklamıyorsa devam mümkün olmaz.
- Aynı olay iki kez gelebilir; işleme kodu etkisiz olmalı, olaylar artırım komutu değil
  son durum duyurusu biçiminde tasarlanmalıdır.
- Başlangıç listesi ile akış buluşturulurken önce akış açılıp olaylar tamponlanır; sıra
  dışı gelen olaylar sürüm damgasıyla elenir.

#### Sonraki Adım

Son üç ders veriyi almanın yollarını kurdu; hepsinin ortak noktası, verinin bir süre
**henüz gelmemiş** olmasıdır. Ekran o sırada bir şey göstermek zorunda: boş bir çerçeve,
bir yükleme göstergesi, eski veri, ya da hata. Bu durumları ayrı ayrı mantıksal
bayraklarla tutmak — yükleniyor, hata var, veri var — kısa sürede birbiriyle çelişen
kombinasyonlar üretir. Sonraki ders görünümün hangi durumlarda bulunabileceğini sayılı bir
kümeye indirger ve geçişleri bir tabloyla sınar.

### Yükleme ve Hata Durumları

Source: https://academia.sh/tr/kurslar/frontend-mimarisi/yukleme-ve-hata-durumlari

Görünümün veri beklerken bulunabileceği durumların sayılı bir kümeye indirgenmesi; mantıksal bayrakların ürettiği çelişkiler, geçiş tablosu, yarış koşullarının damgayla elenmesi ve durumların ekrana karşılığı.

Önceki üç ders veriyi almanın yollarını kurdu. Hepsinin ortak yanı, verinin bir süre henüz
gelmemiş olmasıdır. Ölçüm geçmişi ekranı o sırada boş duramaz: bir şey göstermek zorunda.
Gösterilecek şey, ekranın **o an hangi durumda olduğuna** bağlıdır — ilk kez mi yükleniyor,
elde eski veri var mı, istek başarısız mı oldu, sonuç boş mu döndü.

Bu ders o durumları sayılı bir kümeye indirger. İndirgeme bir düzen kaygısı değil: aşağıda
görüleceği gibi, durumları ayrı ayrı mantıksal bayraklarla tutmak birbiriyle çelişen
bileşimler üretir ve bu bileşimler ekranda gerçek kusur olarak görünür.

#### Mantıksal Bayrakların Ürettiği Çelişki

En sık başlanan yol üç bayraktır: `yukleniyor`, `hata`, `veri`. Üç mantıksal değerin
sekiz bileşimi vardır. Bunların yalnız dördü anlamlıdır.

- Yükleniyor doğru, veri boş, hata yok — ilk yükleme. Anlamlı.
- Yükleniyor yanlış, veri dolu, hata yok — hazır. Anlamlı.
- Yükleniyor yanlış, veri boş, hata var — başarısız. Anlamlı.
- Yükleniyor doğru, veri dolu, hata yok — tazeleme. Anlamlı.
- Yükleniyor doğru, hata var — istek sürüyor ama hata gösteriliyor. Ekranda dönen bir
  yükleme göstergesiyle bir hata kutusu yan yana durur.
- Yükleniyor yanlış, veri boş, hata yok — hiçbir şey söylemez. Ekran boştur; kullanıcı
  isteğin başlamadığını mı, bittiğini ve sonuç bulunmadığını mı bilemez.
- Veri dolu, hata var — hangisi gösterilecek? Bu karar her bileşende yeniden verilir ve
  farklı verilir.

Bayrakların sorunu, **birbirinden bağımsız değişebilmeleridir**. Yeni bir istek başlarken
eski hatayı temizlemeyi unutan tek bir satır, beşinci bileşimi üretir. Kusur derleme
zamanında görünmez, yalnız belirli bir tıklama sırasında ortaya çıkar.

Çözüm, üç bayrağı tek bir alanla değiştirmektir: görünüm aynı anda tek bir durumdadır ve
durumlar arasında yalnız tanımlı geçişler vardır. Geçersiz bileşim böylece
temsil edilemez hâle gelir.

#### Yedi Durum ve Geçiş Tablosu

Veri çeken bir ekranın ihtiyaç duyduğu durumlar şunlardır: **boş** (henüz istenmedi),
**yükleniyor** (elde veri yokken bekleniyor), **hazır** (veri var), **boş sonuç** (istek
başarılı, sonuç kümesi boş), **yenileniyor** (elde veri varken tazeleniyor), **hata**
(elde veri yok, istek başarısız) ve **bayat** (elde eski veri var, tazeleme başarısız).

Son iki durumun ayrılması bu tasarımın özüdür. Elde gösterilecek veri varken tazeleme
başarısız olduğunda ekranı hata kutusuna çevirmek, kullanıcının elindeki bilgiyi almak
demektir. Bayat durumu eski veriyi ekranda tutar ve tazelemenin başarısız olduğunu ayrı
bir bildirimle söyler.

```js
// gorunum-durumu.mjs — gorunum durum makinesi ve gecis tablosu
const DURUMLAR = ["bos", "yukleniyor", "hazir", "bosSonuc", "yenileniyor", "hata", "bayat"];
const OLAYLAR = ["iste", "doluGeldi", "bosGeldi", "basarisiz", "iptal"];

// Veriye bagli hedefler islev olarak yazilir.
const doluysaHazir = (b) => (b.veri.length > 0 ? "hazir" : "bosSonuc");

const TABLO = {
  bos:         { iste: "yukleniyor" },
  yukleniyor:  { doluGeldi: "hazir", bosGeldi: "bosSonuc", basarisiz: "hata", iptal: "bos" },
  hazir:       { iste: "yenileniyor" },
  bosSonuc:    { iste: "yenileniyor" },
  yenileniyor: { doluGeldi: "hazir", bosGeldi: "bosSonuc", basarisiz: "bayat", iptal: doluysaHazir },
  hata:        { iste: "yukleniyor" },
  bayat:       { iste: "yenileniyor" },
};

function gecis(birim, olay, yuk = null) {
  const hedef = TABLO[birim.durum]?.[olay];
  if (hedef === undefined) return { ...birim, yoksayilan: birim.yoksayilan + 1 };
  const yeni = { ...birim };
  if (olay === "doluGeldi") yeni.veri = yuk;
  if (olay === "bosGeldi") yeni.veri = [];
  if (olay === "basarisiz") yeni.hata = yuk;
  if (olay === "doluGeldi" || olay === "bosGeldi") yeni.hata = null;
  yeni.durum = typeof hedef === "function" ? hedef(yeni) : hedef;
  return yeni;
}

// --- Gecis tablosunun yazdirilmasi -----------------------------------------
const hucre = (d, o) => {
  const h = TABLO[d]?.[o];
  return h === undefined ? "·" : typeof h === "function" ? "veriye bağlı" : h;
};
console.log("durum".padEnd(12) + OLAYLAR.map((o) => o.padEnd(13)).join(""));
for (const d of DURUMLAR) {
  console.log(d.padEnd(12) + OLAYLAR.map((o) => hucre(d, o).padEnd(13)).join(""));
}

// --- Dizilerin sinanmasi ----------------------------------------------------
const OLCUM = [{ kimlik: "o-114", deger: -4.2 }];
const DIZILER = [
  ["ilk yükleme başarılı", [["iste"], ["doluGeldi", OLCUM]]],
  ["ilk yükleme boş",      [["iste"], ["bosGeldi"]]],
  ["ilk yükleme hatalı",   [["iste"], ["basarisiz", "ag"]]],
  ["hatadan sonra tekrar", [["iste"], ["basarisiz", "ag"], ["iste"], ["doluGeldi", OLCUM]]],
  ["tazeleme hatası",      [["iste"], ["doluGeldi", OLCUM], ["iste"], ["basarisiz", "sunucu"]]],
  ["tazeleme iptali",      [["iste"], ["doluGeldi", OLCUM], ["iste"], ["iptal"]]],
  ["geçersiz sıra",        [["doluGeldi", OLCUM], ["iptal"], ["iste"]]],
];

console.log("");
for (const [ad, olaylar] of DIZILER) {
  let birim = { durum: "bos", veri: [], hata: null, yoksayilan: 0 };
  const iz = ["bos"];
  for (const [olay, yuk] of olaylar) {
    birim = gecis(birim, olay, yuk);
    iz.push(birim.durum);
  }
  console.log(
    ad.padEnd(22), iz.join(" → ").padEnd(46),
    `veri=${birim.veri.length} hata=${birim.hata} yoksayılan=${birim.yoksayilan}`);
}

// --- Yaris korumasi: gec gelen yanit yok sayilir ---------------------------
console.log("");
let birim = { durum: "bos", veri: [], hata: null, yoksayilan: 0 };
let damga = 0;
const istek = () => { damga += 1; birim = gecis(birim, "iste"); return damga; };
const yanit = (kendiDamgasi, olay, yuk) => {
  if (kendiDamgasi !== damga) { console.log(`  damga ${kendiDamgasi}: eskimiş, yok sayıldı`); return; }
  birim = gecis(birim, olay, yuk);
  console.log(`  damga ${kendiDamgasi}: uygulandı → ${birim.durum}`);
};

const d1 = istek();                       // "kuz" icin arama
const d2 = istek();                       // "kuzey" icin arama
yanit(d2, "doluGeldi", [{ kimlik: "o-114" }, { kimlik: "o-113" }]);
yanit(d1, "doluGeldi", [{ kimlik: "o-001" }]);   // gec gelen eski yanit
console.log("son durum:", birim.durum, "| kayıt:", birim.veri.length);
```

```
durum       iste         doluGeldi    bosGeldi     basarisiz    iptal        
bos         yukleniyor   ·            ·            ·            ·            
yukleniyor  ·            hazir        bosSonuc     hata         bos          
hazir       yenileniyor  ·            ·            ·            ·            
bosSonuc    yenileniyor  ·            ·            ·            ·            
yenileniyor ·            hazir        bosSonuc     bayat        veriye bağlı 
hata        yukleniyor   ·            ·            ·            ·            
bayat       yenileniyor  ·            ·            ·            ·            

ilk yükleme başarılı   bos → yukleniyor → hazir                       veri=1 hata=null yoksayılan=0
ilk yükleme boş        bos → yukleniyor → bosSonuc                    veri=0 hata=null yoksayılan=0
ilk yükleme hatalı     bos → yukleniyor → hata                        veri=0 hata=ag yoksayılan=0
hatadan sonra tekrar   bos → yukleniyor → hata → yukleniyor → hazir   veri=1 hata=null yoksayılan=0
tazeleme hatası        bos → yukleniyor → hazir → yenileniyor → bayat veri=1 hata=sunucu yoksayılan=0
tazeleme iptali        bos → yukleniyor → hazir → yenileniyor → hazir veri=1 hata=null yoksayılan=0
geçersiz sıra          bos → bos → bos → yukleniyor                   veri=0 hata=null yoksayılan=2

  damga 2: uygulandı → hazir
  damga 1: eskimiş, yok sayıldı
son durum: hazir | kayıt: 2
```

#### Tablodan Okunanlar

Tablodaki nokta işaretleri **tanımsız geçişleri** gösterir ve asıl bilgi oradadır.
`hazir` durumundayken `basarisiz` olayı gelemez, çünkü hazır durumdayken uçuşta bir istek
yoktur. Bu, kodun bir yerinde açıkça yasaklanmış bir kural değil; tablonun boş bıraktığı
bir hücre. Yedinci dizi bunu sınıyor: istek atılmadan gelen yanıt ve iptal olayları yok
sayıldı, sayaç ikiyi gösterdi. Bayrak tabanlı bir gerçekleştirimde aynı iki olay durumu
sessizce bozardı.

Beşinci ve altıncı diziler `yenileniyor` durumunun iki farklı çıkışını gösteriyor.
Başarısızlıkta `bayat`a geçildi ve `veri=1` korundu — eski ölçüm listesi ekranda kaldı.
İptalde `hazir`a dönüldü ve hata alanı boş kaldı; önceki dersin kuralı burada işliyor,
iptal bir hata değildir.

Dördüncü dizi hata sonrası kurtarmayı gösteriyor. `hata` durumundan yalnız `iste` çıkar;
"tekrar dene" düğmesinin karşılığı budur. Yeni istek başlarken hata alanının temizlenmesi
geçiş işlevinin içinde tek bir yerde yapılır, her çağrı noktasında değil.

Durum makinesinin yan faydası sınanabilirliktir. Yukarıdaki yedi dizi bir birim testi
kümesidir; ekran hiç çizilmeden görünümün davranışı doğrulanabilir. Bileşenin içine
serpiştirilmiş bayraklarda bu ayrıştırma mümkün değildir.

#### Yarış Koşulu ve Damga

Son bölüm, REST İstemcisi dersinde bırakılan sorunun görünüm tarafındaki yarısını
kapatıyor. Kullanıcı istasyon süzgecine "kuz" yazdı, sonra "kuzey" yazdı; iki istek yola
çıktı. İkinci istek daha önce yanıtlandı, birincisi geç geldi. Geç gelen yanıt uygulanırsa
ekranda "kuz" sonuçları görünür ama süzgeç kutusunda "kuzey" yazar.

Çözüm bir **istek damgasıdır**: her yeni istek bir sayaç artırır ve kendi damgasını taşır.
Yanıt geldiğinde damga, o an geçerli olan damgayla karşılaştırılır; eşleşmiyorsa yanıt
uygulanmaz. Çıktının son üç satırı bunu gösteriyor: ikinci istek uygulandı, birincisi
eskimiş sayıldı, ekranda iki kayıt kaldı.

Damga, isteğin iptal edilmesinin yerine geçmez; ikisi birlikte kullanılır. İptal ağ
kaynağını serbest bırakır ama iptal edilemeyen bir isteğin — örneğin yanıtı zaten
yola çıkmış bir isteğin — geç gelmesini engellemez. Damga, o son savunmadır.

Aynı düzenek anahtar bazında da kurulabilir: sayaç yerine "hangi süzgeç için" bilgisi
saklanır ve gelen yanıt kendi anahtarıyla eşleşmiyorsa atılır. Sunucu durumu yönetiminde
önbellek anahtarı zaten bu işi görür; damga, önbellek kullanmayan ekranlar için aynı
korumayı sağlar.

#### Durumların Ekrana Karşılığı

Durum kümesi belirlendikten sonra her durumun görsel karşılığı ayrı bir karardır. Arayüz
Tasarımının Temelleri kursundaki Yükleme ve Boş Durumlar ile Hata ve Uyarı Durumları
dersleri bu kararların ölçütlerini kurar; burada yalnız durum makinesine bağlanan yanları
anılır.

**Yükleniyor** durumunda gösterilecek şey, beklenen içeriğin biçimine bağlıdır. Yapısı
bilinen bir liste için yer tutucu bir iskelet, gelen içeriğin kaplayacağı alanı önceden
ayırır ve içerik geldiğinde kayma oluşmaz. Yapısı bilinmeyen bir sonuç için nötr bir
gösterge yeterlidir.

**Yenileniyor** durumu, `yukleniyor` ile aynı görünmemelidir. Elde veri varken ekranı
iskelete çevirmek, kullanıcının okuduğu içeriği ondan almaktır. Doğru sunum, mevcut
içeriği yerinde bırakıp tazelemeyi küçük ve kenarda bir göstergeyle bildirmektir.

**Boş sonuç** ile **hata** ayrı iletiler ister. Boş sonuç sistemin doğru çalıştığını ve
ölçütü karşılayan kayıt bulunmadığını söyler; kurtarma yolu ölçütü değiştirmektir. Hata,
sistemin yanıt veremediğini söyler; kurtarma yolu tekrar denemektir. İkisini aynı kutuda
sunmak, kullanıcıyı yanlış eyleme yönlendirir.

**Bayat** durumu iki bilgiyi aynı anda taşır: ekrandaki veri geçerlidir ama güncel
olmayabilir. Sunumu, veriyi yerinde bırakıp üstünde tazelemenin başarısız olduğunu ve son
başarılı güncellemenin ne zaman yapıldığını bildiren bir şeritle yapılır. "Tekrar dene"
eylemi bu şeritte durur.

Bir de sürenin kendisi bir karardır. Yanıt çok hızlı geldiğinde yükleme göstergesini
göstermek, ekranda anlık bir titreme üretir; gösterge küçük bir gecikmeden sonra
görünecek biçimde ertelenir. Gösterge bir kez göründüyse de en az kısa bir süre ekranda
kalır, yoksa aynı titreme çıkışta yaşanır. Bu iki eşik durum makinesinin dışındadır:
makine `yukleniyor` der, göstergeyi ne zaman çizeceğine sunum katmanı karar verir.

#### Özet

- Yükleme, hata ve veri bilgisini ayrı mantıksal bayraklarla tutmak sekiz bileşim üretir;
  yarısı çelişkilidir ve bu çelişkiler ekranda gerçek kusur olarak görünür.
- Tek alanlı bir durum makinesi geçersiz bileşimleri temsil edilemez kılar; tablodaki boş
  hücreler tanımsız geçişleri gösterir ve beklenmeyen olay sessizce yok sayılır.
- Elde veri varken tazelemenin başarısız olması ayrı bir durumdur; ekranı hata kutusuna
  çevirmek yerine eski veri korunur ve tazeleme başarısızlığı ayrıca bildirilir.
- Boş sonuç ile hata farklı kurtarma yolları önerir: biri ölçütü değiştirmeyi, diğeri
  tekrar denemeyi.
- İstek damgası geç gelen yanıtı eler; iptalin yerine geçmez, onunla birlikte çalışır.
- Yükleme göstergesinin görünme ve en az kalma eşikleri sunum katmanının kararıdır; durum
  makinesi yalnız hangi durumda olunduğunu söyler.

#### Sonraki Adım

Durum makinesi `basarisiz` olayını tek bir kapı olarak ele aldı: hata geldi, duruma
geçildi, kullanıcıya "tekrar dene" düğmesi sunuldu. Oysa hataların bir kısmı kullanıcıya
hiç gösterilmemeli. Zayıf bir bağlantıda kesilen bir istek, iki yüz milisaniye sonra
yeniden denendiğinde başarılı olabilir; kullanıcı arada hiçbir şey görmez. Hangi hatanın
kendiliğinden yeniden deneneceği, denemeler arasında ne kadar bekleneceği ve bu beklemenin
neden rastgeleleştirilmesi gerektiği sonraki dersin konusudur.

### Yeniden Deneme ve Geri Çekilme

Source: https://academia.sh/tr/kurslar/frontend-mimarisi/yeniden-deneme-ve-geri-cekilme

Hangi hatanın kendiliğinden yeniden deneneceği, üstel geri çekilmenin ve sarsıntının hesabı, sunucunun bekleme önerisi, deneme bütçesi ve üstüste başarısızlıkta isteği hiç göndermeyen devre kesici.

Önceki ders `basarisiz` olayını tek bir kapı olarak ele aldı: hata geldi, görünüm hata
durumuna geçti, kullanıcıya "tekrar dene" düğmesi sunuldu. Oysa bazı hatalar kullanıcıya
hiç gösterilmemelidir. Kuzey Yamaç istasyonuna dağdan bağlanan bir gözlemcinin bağlantısı
saniyeler içinde kopup düzelir; iki yüz milisaniye sonra yeniden denenen istek başarılı
olur ve kullanıcı arada hiçbir şey görmez.

Bu dersin sorusu şudur: hangi hata kendiliğinden yeniden denenir, ne kadar beklenir, kaç
kez denenir ve ne zaman denemekten vazgeçilir. Dördü de yanlış cevaplandığında ortaya
çıkan sonuç aynıdır — zaten zorlanan bir sunucunun istemciler tarafından daha da
zorlanması.

#### Hangi Hata Yeniden Denenir

Karar iki bilgiye bakar: hatanın sınıfı ve isteğin yöntemi.

Hata sınıfı REST İstemcisi dersinde kuruldu. **HTTP hatalarında** sunucunun kararı
bilinir: 4xx isteğin kendisiyle ilgilidir, aynı isteği tekrarlamak aynı yanıtı üretir.
İki istisna vardır — 429 hız sınırını, 5xx sunucunun o anki durumunu bildirir; ikisi de
zamanla değişebilir.

**Ağ hatalarında** sunucunun isteği alıp almadığı bilinmez. Yanıt yolda kaybolmuş olabilir;
bu durumda sunucu isteği işlemiştir. Bu yüzden ağ hatasında karar yönteme geçer. İnternet
Nasıl Çalışır kursunda tanımlanan **güvenli** yöntemler durum değiştirmez, tekrarlanması
zararsızdır. Ölçüm kaydı gönderen bir istek ise iki kez işlenirse iki kayıt üretebilir;
otomatik olarak yeniden denenemez.

Bu kısıtın aşılma yolu vardır: isteğe istemcinin ürettiği bir **tekillik anahtarı**
eklenir, sunucu aynı anahtarla gelen ikinci isteği yeni bir kayıt saymaz. Anahtar
gönderiliyorsa durum değiştiren istek de yeniden denenebilir hâle gelir. Bu, istemci ile
sunucunun birlikte kararlaştırması gereken bir sözleşmedir; tek taraflı kurulamaz.

**Gövde hatası** ve **iptal** hiçbir koşulda yeniden denenmez. Birincisi programın kendi
hatasıdır, ikincisi zaten bir vazgeçme kararıdır.

#### Bekleme Süresinin Hesabı

Hemen tekrar denemek işe yaramaz: sorunun ortadan kalkması için zaman gerekir. Sabit
aralıkla denemek de yetmez; sunucu ayağa kalkamamışsa aynı yükü aynı sıklıkla almaya devam
eder. **Üstel geri çekilme (exponential backoff)**, her denemede bekleme süresini bir
çarpanla büyütür; sorun kısa sürerse hızlı toparlanır, uzun sürerse istemci geri çekilir.

Üstel büyüme tek başına sınırsızdır, bu yüzden bir **tavan** konur. Asıl eksik parça ise
başkadır. Bir sunucu düştüğünde ona bağlı bütün istemciler aynı anda hata alır; hepsi aynı
formülü uygularsa aynı anda yeniden dener. Sunucu ayağa kalktığı anda eşzamanlı bir dalga
karşılar ve yeniden düşer. **Sarsıntı (jitter)**, bekleme süresine rastgelelik katarak bu
dalgayı dağıtır.

```js
// geri-cekilme.mjs — bekleme sureleri ve sarsintinin dagilima etkisi
// Belirlenimci sozde rastgele uretec: ayni tohum ayni diziyi verir.
function uretec(tohum) {
  let a = tohum >>> 0;
  return () => {
    a = (a + 0x6d2b79f5) >>> 0;
    let t = Math.imul(a ^ (a >>> 15), 1 | a);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}

const TABAN = 200, CARPAN = 2, TAVAN = 5000;
const ustel = (n) => Math.min(TAVAN, TABAN * CARPAN ** n);

const rastgele = uretec(20260114);
const tamSarsinti = (n) => Math.round(rastgele() * ustel(n));

// Iliskisiz sarsinti: bir sonraki bekleme oncekine bagli araliktan secilir.
let onceki = TABAN;
const iliskisizSarsinti = () => {
  onceki = Math.min(TAVAN, Math.round(TABAN + rastgele() * (onceki * 3 - TABAN)));
  return onceki;
};

console.log("deneme  sabit   üstel   üstel+tavan  tam sarsıntı  ilişkisiz");
for (let n = 0; n < 6; n++) {
  console.log(
    String(n + 1).padEnd(8) +
    String(TABAN).padEnd(8) +
    String(TABAN * CARPAN ** n).padEnd(8) +
    String(ustel(n)).padEnd(13) +
    String(tamSarsinti(n)).padEnd(14) +
    String(iliskisizSarsinti()));
}

// --- Ayni anda dusen istemcilerin uyanma dagilimi --------------------------
const ISTEMCI = 24, DENEME = 3;   // ucuncu denemenin beklemesi: ustel(2) = 800 ms
const kova = (ms) => Math.floor(ms / 200) * 200;

const say = (liste) => {
  const m = new Map();
  for (const v of liste) m.set(v, (m.get(v) ?? 0) + 1);
  return [...m.entries()].sort((a, b) => a[0] - b[0])
    .map(([k, s]) => `${String(k).padStart(4)}ms:${"#".repeat(s)}`);
};

const sarsintisiz = Array.from({ length: ISTEMCI }, () => kova(ustel(DENEME - 1)));
const r2 = uretec(20260114);
const sarsintili = Array.from({ length: ISTEMCI },
  () => kova(Math.round(r2() * ustel(DENEME - 1))));

console.log("\nsarsıntısız (24 istemci, 3. deneme):");
say(sarsintisiz).forEach((s) => console.log("  " + s));
console.log("sarsıntılı  (24 istemci, 3. deneme):");
say(sarsintili).forEach((s) => console.log("  " + s));
```

```
deneme  sabit   üstel   üstel+tavan  tam sarsıntı  ilişkisiz
1       200     200     200          70            384
2       200     400     400          296           1034
3       200     800     800          780           2121
4       200     1600    1600         303           1933
5       200     3200    3200         480           3986
6       200     6400    5000         957           5000

sarsıntısız (24 istemci, 3. deneme):
   800ms:########################
sarsıntılı  (24 istemci, 3. deneme):
     0ms:######
   200ms:#####
   400ms:#######
   600ms:######
```

İki histogram farkı açıkça gösteriyor. Sarsıntısız hesapta yirmi dört istemcinin tamamı
tek bir ana yığılıyor; sarsıntılı hesapta aynı istemciler dört ayrı aralığa dağılıyor.
Sunucunun gördüğü tepe yük dörtte birine iniyor.

Tablodaki iki sarsıntı biçimi farklı ödünleşim yapar. **Tam sarsıntı**, beklemeyi sıfır ile
üstel değer arasından seçer; dağılımı en geniş olan yoldur ama bazı denemeler çok erken
olur. **İlişkisiz sarsıntı**, bir sonraki beklemeyi bir öncekine bağlı bir aralıktan
seçer; büyüme daha yumuşak ve daha az sıçramalıdır, tabloda altıncı denemede tavana
ulaştığı görülüyor. Seçim, sunucunun toparlanma süresine bağlıdır: hızlı toparlanan bir
hizmet için tam sarsıntı, yavaş toparlanan için ilişkisiz sarsıntı daha uygundur.

Rastgeleliğin belirlenimci bir üreteçle kurulduğuna dikkat edin. Uygulamada üreteç
tohumsuzdur; burada tohum sabit tutulduğu için çıktı her çalıştırmada aynıdır ve tablo
doğrulanabilir.

#### Döngünün Kurulması

Hesap tek başına yetmez; döngünün ne zaman durduğu ve sunucunun söylediklerine ne kadar
uyulduğu da tanımlanmalıdır.

```js
// yeniden-deneme.mjs — yerel sunucuya karsi calisan yeniden deneme dongusu
import http from "node:http";

let olcumIstegi = 0;
const sunucu = http.createServer((istek, yanit) => {
  const yol = new URL(istek.url, "http://127.0.0.1").pathname;
  if (yol === "/olcumler" && istek.method === "GET") {
    olcumIstegi += 1;
    if (olcumIstegi <= 2) {                       // ilk iki istek basarisiz
      yanit.writeHead(503, { "content-type": "application/json" });
      return yanit.end(JSON.stringify({ kod: "erisilemez" }));
    }
    yanit.writeHead(200, { "content-type": "application/json" });
    return yanit.end(JSON.stringify([{ kimlik: "o-114", deger: -4.2 }]));
  }
  if (yol === "/olcumler" && istek.method === "POST") {
    yanit.writeHead(422, { "content-type": "application/json" });
    return yanit.end(JSON.stringify({ kod: "dogrulama" }));
  }
  if (yol === "/rapor") {
    yanit.writeHead(429, { "content-type": "application/json", "retry-after": "2" });
    return yanit.end(JSON.stringify({ kod: "hiz_siniri" }));
  }
  yanit.writeHead(404, { "content-type": "application/json" });
  yanit.end(JSON.stringify({ kod: "yol_yok" }));
});
await new Promise((coz) => sunucu.listen(0, "127.0.0.1", coz));
const TABAN = `http://127.0.0.1:${sunucu.address().port}`;

// --- Bekleme hesabi (belirlenimci) -----------------------------------------
function uretec(tohum) {
  let a = tohum >>> 0;
  return () => {
    a = (a + 0x6d2b79f5) >>> 0;
    let t = Math.imul(a ^ (a >>> 15), 1 | a);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}
const rastgele = uretec(20260114);
const bekleme = (n) => Math.round(rastgele() * Math.min(5000, 200 * 2 ** n));

// --- Ilke: hangi sonuc yeniden denenir -------------------------------------
const GUVENLI_YONTEM = new Set(["GET", "HEAD", "OPTIONS"]);
function denenirMi(sonuc, yontem) {
  if (sonuc.tur === "ag") return GUVENLI_YONTEM.has(yontem);  // ulasti mi bilinmiyor
  if (sonuc.tur === "http") return sonuc.durum === 429 || sonuc.durum >= 500;
  return false;                                                // gövde hatasi, iptal
}

// --- Dongu ------------------------------------------------------------------
async function yenidenDene(yol, { yontem = "GET", enCokDeneme = 4, uyu }) {
  const beklemeler = [];
  for (let deneme = 0; deneme < enCokDeneme; deneme++) {
    let sonuc;
    try {
      const yanit = await fetch(TABAN + yol, { method: yontem });
      const govde = await yanit.json();
      sonuc = yanit.ok
        ? { tur: "basari", veri: govde }
        : { tur: "http", durum: yanit.status,
            sunucuOnerisi: Number(yanit.headers.get("retry-after") ?? 0) * 1000 };
    } catch {
      sonuc = { tur: "ag" };
    }
    if (sonuc.tur === "basari") return { sonuc, deneme: deneme + 1, beklemeler };
    if (!denenirMi(sonuc, yontem)) return { sonuc, deneme: deneme + 1, beklemeler };
    if (deneme === enCokDeneme - 1) return { sonuc, deneme: deneme + 1, beklemeler };

    // Sunucunun onerisi hesaplanan beklemeden uzunsa oneri kazanir.
    const sure = Math.max(bekleme(deneme), sonuc.sunucuOnerisi ?? 0);
    beklemeler.push(sure);
    await uyu(sure);
  }
}

// Sinamada gercek beklemek yerine sureler kaydedilir; uygulamada burada
// gercek bir zamanlayici bulunur.
const kayitli = [];
const sahteUyku = (ms) => { kayitli.push(ms); return Promise.resolve(); };

const yaz = (ad, r) => console.log(
  ad.padEnd(24),
  `deneme=${r.deneme}`.padEnd(10),
  `sonuç=${r.sonuc.tur}${r.sonuc.durum ? "/" + r.sonuc.durum : ""}`.padEnd(16),
  `beklemeler=[${r.beklemeler.join(", ")}]`);

yaz("503, sonra başarılı", await yenidenDene("/olcumler", { uyu: sahteUyku }));
yaz("422 (denenmez)", await yenidenDene("/olcumler", { yontem: "POST", uyu: sahteUyku }));
yaz("429 (Retry-After: 2)", await yenidenDene("/rapor", { enCokDeneme: 3, uyu: sahteUyku }));
yaz("404 (denenmez)", await yenidenDene("/yok", { uyu: sahteUyku }));

console.log("sunucuya ulaşan ölçüm isteği:", olcumIstegi);
console.log("kaydedilen tüm beklemeler   :", kayitli.join(", "));

sunucu.close();
```

```
503, sonra başarılı      deneme=3   sonuç=basari     beklemeler=[70, 184]
422 (denenmez)           deneme=1   sonuç=http/422   beklemeler=[]
429 (Retry-After: 2)     deneme=3   sonuç=http/429   beklemeler=[2000, 2000]
404 (denenmez)           deneme=1   sonuç=http/404   beklemeler=[]
sunucuya ulaşan ölçüm isteği: 3
kaydedilen tüm beklemeler   : 70, 184, 2000, 2000
```

İlk satır beklenen davranışı gösteriyor: sunucu iki kez 503 döndü, döngü iki kez bekledi,
üçüncü denemede veri geldi. Kullanıcı bunların hiçbirini görmedi; görünüm hâlâ
`yukleniyor` durumundaydı.

İkinci ve dördüncü satırlar ilkenin ayırt ediciliğini gösteriyor. Doğrulama hatası ve
bulunamadı yanıtı tek denemede bitti; tekrarlamak sonucu değiştirmeyeceği için beklenmedi
bile. Yeniden denemenin en sık yapılan hatası, her hatayı denemeye sokmaktır: kullanıcı
yanlış bir değer girdiğinde hatayı öğrenmesi üç kat gecikir.

Üçüncü satır **sunucunun önerisinin kazandığını** gösteriyor. Hesaplanan bekleme birkaç yüz
milisaniyeydi; sunucu iki saniye söylediği için iki saniye beklendi. Sunucu kendi
toparlanma süresini istemciden iyi bilir, bu yüzden önerisi taban değil tavan olarak değil,
**alt sınır** olarak kullanılır. Öneri makul olmayacak kadar uzunsa denemeden vazgeçmek —
kullanıcıya durumu bildirmek — beklemekten iyidir.

Döngünün üç sınırı vardır. **Deneme sayısı** üst sınırı sonsuz döngüyü engeller.
**Toplam bütçe**, denemelerin toplam süresini sınırlar; kullanıcının bir düğmeye basıp
sekiz saniye beklemesi, hata görmesinden kötüdür. Üçüncüsü, **kullanıcı tarafından
tetiklenen** ile **arka planda çalışan** isteklerin ayrı ilkeleri olmasıdır: kullanıcı
bekliyorsa deneme sayısı az ve süre kısa tutulur, arka plan tazelemesi için tersi geçerlidir.

#### Devre Kesici

Yeniden deneme tek bir isteğin sorunudur. Sunucu tümüyle erişilemez durumdaysa her istek
kendi denemelerini yapar ve uygulama, hiçbiri başarılı olmayacak isteklerle hem kullanıcıyı
bekletir hem sunucuyu yükler. **Devre kesici (circuit breaker)** bu durumu bir üst düzeyde
tanır: ardışık başarısızlıklar bir eşiği aştığında isteği **hiç göndermez**.

```js
// devre-kesici.mjs — ustuste basarisizlikta istegin hic gonderilmemesi
const ESIK = 3, SOGUMA = 5000;   // 3 ardisik hata; 5000 ms sonra deneme izni

function devreKesici() {
  let durum = "kapali", ardisikHata = 0, acilma = 0;

  return {
    izinVer(simdi) {
      if (durum === "acik" && simdi - acilma >= SOGUMA) durum = "yariAcik";
      return { izin: durum !== "acik", durum };
    },
    bildir(basarili, simdi) {
      if (basarili) { durum = "kapali"; ardisikHata = 0; return durum; }
      ardisikHata += 1;
      if (durum === "yariAcik" || ardisikHata >= ESIK) {
        durum = "acik"; acilma = simdi; ardisikHata = 0;
      }
      return durum;
    },
  };
}

// Mantiksal saat: gercek zaman yerine adim adim ilerler.
const kesici = devreKesici();
const OLAYLAR = [
  [    0, false], [  200, false], [  600, false],   // ucuncu hata devreyi acar
  [  900, false], [ 1500, false],                   // sogumada: istek gitmez
  [ 6000, false],                                   // yari acik deneme, basarisiz
  [ 8000, false],                                   // yeniden sogumada
  [11500, true ],                                   // yari acik deneme, basarili
  [11800, true ],
];

let gonderilen = 0, engellenen = 0;
console.log("saat    izin   önceki durum  sonuç      yeni durum");
for (const [simdi, basarili] of OLAYLAR) {
  const { izin, durum } = kesici.izinVer(simdi);
  if (!izin) {
    engellenen += 1;
    console.log(String(simdi).padEnd(8) + "hayır  " + durum.padEnd(14) +
                "gönderilmedi".padEnd(13) + durum);
    continue;
  }
  gonderilen += 1;
  const yeni = kesici.bildir(basarili, simdi);
  console.log(String(simdi).padEnd(8) + "evet   " + durum.padEnd(14) +
              (basarili ? "başarılı" : "başarısız").padEnd(13) + yeni);
}
console.log(`gönderilen: ${gonderilen}  engellenen: ${engellenen}`);
```

```
saat    izin   önceki durum  sonuç      yeni durum
0       evet   kapali        başarısız    kapali
200     evet   kapali        başarısız    kapali
600     evet   kapali        başarısız    acik
900     hayır  acik          gönderilmedi acik
1500    hayır  acik          gönderilmedi acik
6000    evet   yariAcik      başarısız    acik
8000    hayır  acik          gönderilmedi acik
11500   evet   yariAcik      başarılı     kapali
11800   evet   kapali        başarılı     kapali
gönderilen: 6  engellenen: 3
```

Üç durum ve geçişleri çıktıdan okunur. **Kapalı** durumda istekler geçer; ardışık üçüncü
hata devreyi **açık** duruma alır. Açık durumda istek gönderilmez, çağrı hemen ve ağa
dokunmadan başarısız döner — dokuz olaydan üçü hiç sunucuya gitmedi. Soğuma süresi
dolduğunda devre **yarı açık** olur ve tek bir deneme isteğine izin verir. Bu denemenin
sonucu belirleyicidir: başarısız olursa devre yeniden açılır ve soğuma baştan başlar
(6000. anda görülen davranış), başarılı olursa kapanır ve sayaç sıfırlanır.

Devre açıkken çağrı noktası bir yanıt almaz ama bir hata alır. Bu hata, ağ hatasından
ayrılmalıdır: kullanıcıya "hizmet şu anda yanıt vermiyor" denir, "tekrar dene" düğmesi de
soğuma süresi dolana kadar bir şey yapmayacağı için o süreyi bildirmek anlamlıdır.

Devre kesicinin kapsamı da bir tasarım kararıdır. Tek bir kesici tüm uygulamayı kapsarsa,
bir uç noktanın arızası bütün ekranları durdurur. Uygulamada kesici genellikle **hizmet ya
da uç nokta grubu başına** kurulur; ölçüm yazma yolu arızalıyken istasyon listesi
okunabilir kalır.

#### Özet

- Yeniden deneme kararı hata sınıfına ve yönteme bakar: 429 ile 5xx her zaman, ağ hatası
  yalnız güvenli yöntemlerde denenir; doğrulama hataları, gövde hataları ve iptaller
  hiç denenmez.
- Durum değiştiren bir istek, ancak istemcinin ürettiği bir tekillik anahtarı sunucuyla
  kararlaştırılmışsa yeniden denenebilir.
- Üstel geri çekilme beklemeyi her denemede büyütür ve bir tavanla sınırlanır; sarsıntı,
  aynı anda hata alan istemcilerin aynı anda dönmesini engeller.
- Tam sarsıntı dağılımı en geniş yoldur, ilişkisiz sarsıntı büyümeyi yumuşatır; seçim
  hizmetin toparlanma süresine bağlıdır.
- Sunucunun bildirdiği bekleme önerisi hesaplanan süreyle karşılaştırılır ve büyük olan
  uygulanır; deneme sayısının yanında toplam süre bütçesi de sınır koyar.
- Devre kesici ardışık başarısızlıkta isteği hiç göndermez; açık, yarı açık ve kapalı
  durumları arasında geçer ve uç nokta grubu başına kurulur.

#### Sonraki Adım

Veri erişimi konusu burada tamamlanıyor: veri alınıyor, saklanıyor, akış hâlinde
güncelleniyor, beklenmedik durumlar sayılı bir kümeye indirgeniyor ve ağ kararsızlığı
kullanıcıya yansımadan soğuruluyor. Bütün bu akış tek yönlüydü — sunucudan ekrana.
Sonraki konu ters yönü kurar: kullanıcının verdiği veri. Ölçüm giriş formu, alanların
değerini tutmak, ne zaman doğrulamak, hatayı nasıl bildirmek ve gönderim sırasında ne
göstermek sorularını aynı anda sorar. İlk ders bu sorulardan ilkiyle başlıyor: alanın
değeri nerede durur — belgede mi, uygulamanın durumunda mı?

## Formlar ve Kimlik

Kullanıcıdan veri alan ve kullanıcıyı tanıyan akışlar: form durumunun tutulması, iki tarafta çalışan doğrulama şemaları, hatanın erişilebilir sunumu, belirteç saklama seçenekleri, oturum yenileme ve uygulamanın dile göre uyarlanması.

### Form Durum Yönetimi

Source: https://academia.sh/tr/kurslar/frontend-mimarisi/form-durum-yonetimi

Alan değerinin belgede mi uygulamanın durumunda mı tutulacağı, iki yaklaşımın bildirim maliyeti, form durumunun değerlerden başka taşıdığı bilgiler ve başlangıç değeri ile kirliliğin gönderim akışındaki rolü.

Veri erişimi konusu akışı tek yönde kurdu: sunucudan ekrana. Kuzey Yamaç arayüzünün ikinci
yarısı ters yönde çalışır. Gözlemci ölçüm giriş formunu açar, istasyonu seçer, değeri
yazar, notu ekler ve gönderir. Bu akışın ilk sorusu teknik olarak küçük ama sonuçları
büyüktür: **alanın o anki değeri nerede durur?**

İki cevap vardır. Değer belgenin kendisinde durabilir — alan yazılanı tutar ve uygulama
gerektiğinde okur. Ya da değer uygulamanın durumunda durur — alan yalnızca durumu gösterir, her
tuş vuruşu durumu günceller ve alan güncellenmiş durumdan yeniden çizilir. Bu ders iki cevabın
ne kazandırıp ne götürdüğünü ve form durumunun değerlerden başka neler taşıdığını ele alır.

#### İki Doğruluk Kaynağı

**Denetimsiz alan (uncontrolled input)**, değerin belgede tutulmasıdır. Alan kendi
durumunu yönetir; uygulama başlangıç değerini verir, sonra araya girmez. Değer gönderim
anında ya da gerektiğinde okunur. Web'in Temelleri ve HTML kursunda tanımlanan giriş
listesi bu yaklaşımın doğal karşılığıdır: tarayıcı alanları zaten toplar.

**Denetimli alan (controlled input)**, değerin uygulamanın durumunda tutulmasıdır. Alan
gösterilen değeri durumdan alır ve her değişiklikte durumu güncelleyen bir olay bildirir.
Uygulama, kullanıcının gördüğü değeri her an bilir ve değiştirebilir.

Ayrımın kaynağı Tarayıcı ve Web Platformu kursundaki öznitelik–özellik ayrımıdır: alanın
başlangıç değeri işaretlemede bildirilen özniteliktir, o anki değeri ise ögenin
özelliğidir. Denetimsiz alan ikisinin ayrı yaşamasına izin verir; denetimli alan her
değişiklikten sonra ikisini eşitler.

Denetimlilik neyi mümkün kılar? Değerin **yazılırken dönüştürülmesini** — büyük harfe
çevirme, biçim maskesi uygulama, izin verilmeyen karakteri düşürme. Alanlar arası
**bağımlılığı** — istasyon değişince birim listesinin daralması. **Anlık doğrulamayı** ve
değere bağlı arayüz kararlarını. Bir de kaydedilmemiş taslağın saklanmasını: değer zaten
uygulamanın elindedir.

Neyi pahalılaştırır? Her tuş vuruşu bir durum güncellemesi ve bir yeniden çizim üretir.
Otuz alanlı bir formda bu maliyet ölçülebilir hâle gelir.

#### Bildirim Maliyeti

Maliyetin büyüklüğü **abonelik tanesine** bağlıdır: durum değiştiğinde kim haberdar
ediliyor? Aşağıdaki model iki uç yaklaşımı sayıyor — form durumunun tamamına abone olan
alanlar ile yalnız kendi değerine abone olan alanlar.

```js
// abonelik-tanesi.mjs — form duzeyinde ve alan duzeyinde bildirim maliyeti
const ALANLAR = ["istasyon", "tarih", "deger", "birim", "not",
                 "gozlemci", "cihaz", "ruzgar", "nem", "basinc", "kar", "aciklama"];

function depo(taneler) {                 // taneler: "form" | "alan"
  const aboneler = new Map();            // yol -> islev listesi
  const cizim = new Map();               // alan -> cagrilma sayisi
  return {
    abone(yol, alan) {
      const anahtar = taneler === "form" ? "*" : yol;
      if (!aboneler.has(anahtar)) aboneler.set(anahtar, []);
      aboneler.get(anahtar).push(alan);
      cizim.set(alan, 0);
    },
    yaz(yol) {
      const anahtar = taneler === "form" ? "*" : yol;
      for (const alan of aboneler.get(anahtar) ?? []) {
        cizim.set(alan, cizim.get(alan) + 1);
      }
    },
    toplam: () => [...cizim.values()].reduce((t, s) => t + s, 0),
  };
}

// Kullanicinin tus dizisi: uc alana toplam 30 degisiklik.
const TUSLAR = [
  ...Array(14).fill("aciklama"),
  ...Array(10).fill("deger"),
  ...Array(6).fill("not"),
];

for (const taneler of ["form", "alan"]) {
  const d = depo(taneler);
  for (const a of ALANLAR) d.abone(a, a);
  for (const yol of TUSLAR) d.yaz(yol);
  console.log(
    `${taneler.padEnd(5)} tanesi:`,
    `${TUSLAR.length} değişiklik →`,
    `${String(d.toplam()).padStart(4)} alan çizimi`);
}

// Denetimsiz alan: deger belgede durur, gonderimde bir kez okunur.
const d = depo("alan");
for (const a of ALANLAR) d.abone(a, a);
console.log("denetimsiz  :", `${TUSLAR.length} değişiklik →`,
            `${String(d.toplam()).padStart(4)} alan çizimi`);
```

```
form  tanesi: 30 değişiklik →  360 alan çizimi
alan  tanesi: 30 değişiklik →   30 alan çizimi
denetimsiz  : 30 değişiklik →    0 alan çizimi
```

Sayılar oranı gösteriyor: form tanesinde her değişiklik on iki alanı çizdiriyor, alan
tanesinde yalnız değişeni. On iki katlık fark, alan sayısıyla doğru orantılı büyür.

Buradan çıkan kural, "denetimli alan pahalıdır" değil, **"denetimli alanı form tanesinde
kurmak pahalıdır"**dır. Değer uygulamanın durumunda tutulsa bile abonelik alan bazında
kurulursa maliyet tek alanla sınırlı kalır. Çoğu durumda bu, form durumunu tek bir nesne
olarak paylaşmak yerine alan başına ayrı bir okuma yolu sunmakla sağlanır.

Üçüncü satır bir sınır durumudur: değer belgede kaldığında hiçbir çizim olmaz. Bunun
bedeli, uygulamanın kullanıcının yazdığını gönderim anına kadar bilmemesidir. Karma bir
düzen de mümkündür ve yaygındır: alanların çoğu denetimsiz, yalnız birbirine bağımlı olan
ya da yazılırken dönüştürülen alanlar denetimli.

#### Form Durumu Değerlerden İbaret Değildir

Değerlerin nerede durduğu kararı verildikten sonra da form, ekranın ihtiyaç duyduğu birkaç
bilgi daha taşır. Hata iletisinin ne zaman gösterileceği, gönder düğmesinin ne zaman
kapatılacağı, sayfadan ayrılırken uyarı çıkıp çıkmayacağı bu bilgilere bağlıdır.

```js
// form-durumu.mjs — olcum giris formunun durum indirgeyicisi
const BASLANGIC = {
  istasyon: "KY-01", tarih: "2026-01-14", deger: "", birim: "C", not: "",
};

const yeniDurum = () => ({
  degerler: { ...BASLANGIC },
  dokunulan: new Set(),
  gonderiliyor: false,
  gonderimSayisi: 0,
});

const kirliAlanlar = (d) =>
  Object.keys(BASLANGIC).filter((a) => d.degerler[a] !== BASLANGIC[a]);

function indirge(d, [olay, alan, deger]) {
  switch (olay) {
    case "degisti":
      return { ...d, degerler: { ...d.degerler, [alan]: deger } };
    case "dokunuldu":
      return { ...d, dokunulan: new Set([...d.dokunulan, alan]) };
    case "gonderimBasladi":
      return { ...d, gonderiliyor: true, gonderimSayisi: d.gonderimSayisi + 1,
               dokunulan: new Set(Object.keys(BASLANGIC)) };
    case "gonderimBitti":
      return { ...d, gonderiliyor: false };
    case "kabulEdildi":                       // sunucu kaydi aldi: yeni taban
      return { ...yeniDurum(), gonderimSayisi: d.gonderimSayisi };
    case "sifirla":
      return { ...yeniDurum(), gonderimSayisi: d.gonderimSayisi };
    default:
      throw new Error(`bilinmeyen olay: ${olay}`);
  }
}

const OLAYLAR = [
  ["degisti", "deger", "-"],
  ["degisti", "deger", "-4"],
  ["degisti", "deger", "-4.2"],
  ["dokunuldu", "deger"],
  ["degisti", "not", "sabah okuması"],
  ["gonderimBasladi"],
  ["gonderimBitti"],
  ["kabulEdildi"],
];

let d = yeniDurum();
const yaz = (etiket) => console.log(
  etiket.padEnd(26),
  `deger=${JSON.stringify(d.degerler.deger).padEnd(7)}`,
  `kirli=[${kirliAlanlar(d).join(",")}]`.padEnd(18),
  `dokunulan=${d.dokunulan.size}`.padEnd(13),
  `gönderiliyor=${d.gonderiliyor}`.padEnd(19),
  `gönderim=${d.gonderimSayisi}`);

yaz("başlangıç");
for (const olay of OLAYLAR) {
  d = indirge(d, olay);
  yaz(olay.filter(Boolean).join(" "));
}

// --- Ayrilmadan uyari karari ------------------------------------------------
console.log("");
for (const senaryo of [
  ["boş form", yeniDurum()],
  ["yarım doldurulmuş", indirge(yeniDurum(), ["degisti", "deger", "-4.2"])],
  ["gönderim sonrası", d],
]) {
  const [ad, durum] = senaryo;
  const kirli = kirliAlanlar(durum);
  console.log(`${ad.padEnd(20)} kirli=${kirli.length} → ayrılma uyarısı: ` +
              (kirli.length > 0 ? "gösterilir" : "gösterilmez"));
}
```

```
başlangıç                  deger=""      kirli=[]           dokunulan=0   gönderiliyor=false  gönderim=0
degisti deger -            deger="-"     kirli=[deger]      dokunulan=0   gönderiliyor=false  gönderim=0
degisti deger -4           deger="-4"    kirli=[deger]      dokunulan=0   gönderiliyor=false  gönderim=0
degisti deger -4.2         deger="-4.2"  kirli=[deger]      dokunulan=0   gönderiliyor=false  gönderim=0
dokunuldu deger            deger="-4.2"  kirli=[deger]      dokunulan=1   gönderiliyor=false  gönderim=0
degisti not sabah okuması  deger="-4.2"  kirli=[deger,not]  dokunulan=1   gönderiliyor=false  gönderim=0
gonderimBasladi            deger="-4.2"  kirli=[deger,not]  dokunulan=5   gönderiliyor=true   gönderim=1
gonderimBitti              deger="-4.2"  kirli=[deger,not]  dokunulan=5   gönderiliyor=false  gönderim=1
kabulEdildi                deger=""      kirli=[]           dokunulan=0   gönderiliyor=false  gönderim=1

boş form             kirli=0 → ayrılma uyarısı: gösterilmez
yarım doldurulmuş    kirli=1 → ayrılma uyarısı: gösterilir
gönderim sonrası     kirli=0 → ayrılma uyarısı: gösterilmez
```

Dört ayrı bilgi görünüyor ve dördü farklı soruyu yanıtlıyor.

**Değer**, alanın içeriğidir. **Dokunulma**, kullanıcının o alanı terk edip etmediğidir;
Form Olayları dersindeki hata gösterme anı kuralı bu bilgiye dayanır — ilk üç satırda
değer değişti ama dokunulan sayısı sıfır kaldı, yani kullanıcı hâlâ yazıyor ve ona hata
göstermek erken olur.

**Kirlilik**, değerin başlangıçtan farklı olmasıdır. Bu, ayrı bir bayrak olarak tutulmaz;
başlangıç değerleriyle karşılaştırılarak **hesaplanır**. Bayrak olarak tutulsaydı,
kullanıcı bir alanı değiştirip geri eski değerine döndürdüğünde form yanlışlıkla kirli
kalırdı.

**Gönderiliyor** ve **gönderim sayısı** ayrı bilgilerdir. Birincisi düğmenin kapalı
tutulmasını sağlar; ikincisi, kullanıcının bir kez gönderim denediğini kalıcı olarak
işaretler ve bundan sonra hataların dokunulma beklenmeden gösterilmesini sağlar. Altıncı
satırda gönderim başlarken bütün alanların dokunulmuş sayıldığı görülüyor — bu tek satır,
"gönder'e basınca tüm hatalar görünsün" davranışını kurar.

#### Başlangıç, Kirlilik ve Sıfırlama

Başlangıç değerleri form durumunun bir parçasıdır, kodun içine gömülü bir sabit değil.
Bunun iki nedeni vardır.

Birincisi kirliliğin hesaplanabilmesi. Karşılaştırma taban değeri yoksa kirlilik
bilinemez. Çıktının son bloğu bu hesabın nereye bağlandığını gösteriyor: kirli alanı olan
bir formdan ayrılmaya çalışan kullanıcıya uyarı gösterilir, temiz formda gösterilmez.
Uyarıyı her koşulda göstermek — kullanıcı hiçbir şey yazmamışken bile — arayüzü rahatsız
edici kılar ve zamanla göz ardı edilir.

İkincisi, başlangıç değerlerinin **değişebilmesi**. Var olan bir ölçümü düzenleyen form,
başlangıç değerlerini sunucudan alır. Kayıt yüklendiğinde form durumunun yeni tabana
göre kurulması gerekir; aksi hâlde yüklenen değerler kirli görünür ve boş bir formda
"kaydedilmemiş değişiklik" uyarısı çıkar.

Üç ayrı işlem sıklıkla karıştırılır. **Sıfırlama**, değerleri başlangıca döndürür.
**Temizleme**, değerleri boşaltır — bu başlangıca dönmek değildir; düzenleme formunda
ikisi çok farklı sonuç verir. **Yeni taban belirleme**, o anki değerleri yeni başlangıç
kabul eder; sunucu kaydı kabul ettikten sonra yapılan işlem budur. Çıktıda `kabulEdildi`
olayı bunu yapıyor: form temizlendi ve kirlilik sıfırlandı, ama gönderim sayacı korundu.

#### Gönderim Anı

Gönderim, form durumunun en kırılgan olduğu andır ve üç kural gerektirir.

**Çift gönderim engellenir.** Gönderiliyor bayrağı doğruyken yeni bir gönderim başlatılmaz.
Düğmeyi görsel olarak devre dışı bırakmak yeterli değildir; klavyeyle tetiklenen gönderim
ya da hızlı çift tıklama düğmenin durumundan önce gelebilir, bu yüzden engel durumda
kurulur. Bu, Yeniden Deneme ve Geri Çekilme dersindeki tekillik anahtarının form
tarafındaki karşılığıdır — engel istemcide, güvence sunucudadır.

**Sunucudan gelen alan hataları forma bağlanır.** REST İstemcisi dersinde 422 yanıtının
gövdesi `ayrinti` alanında taşınıyordu; o gövdedeki alan adları form alanlarıyla eşleşir
ve hatalar ilgili alanların yanına yerleşir. Eşleşmeyen bir alan adı gelirse hata form
düzeyinde gösterilir, yutulmaz.

**Başarısız gönderimde değerler korunur.** Kullanıcının doldurduğu formu sunucu reddetti
diye boşaltmak, işi baştan yaptırmaktır. Değerler yerinde kalır; yalnız hatalar eklenir.

Gönderimin kullanıcı algısındaki süresi ayrı bir konudur. Kaydın sunucuda kabul
edileceğini varsayıp ekranı hemen güncellemek — ve hata gelirse geri almak — Durum
Yönetimi konusundaki iyimser güncellemenin form tarafındaki uygulamasıdır. Yarım kalan
formun taslak olarak saklanması ise Kalıcı Durum dersindeki depolama kararına dayanır;
saklanan taslağın hangi başlangıç değerleriyle karşılaştırılacağı, yukarıdaki taban
kuralıyla belirlenir.

#### Özet

- Alan değeri ya belgede tutulur (denetimsiz) ya uygulamanın durumunda (denetimli);
  ayrımın kaynağı öznitelik ile özelliğin ayrı yaşayıp yaşamamasıdır.
- Denetimlilik yazılırken dönüştürme, alanlar arası bağımlılık ve anlık doğrulamayı mümkün
  kılar; bedeli her değişiklikte üretilen bildirimdir.
- Bildirim maliyetini belirleyen şey denetimlilik değil abonelik tanesidir; alan bazında
  abonelik on iki alanlı bir formda çizim sayısını on ikide bire indirir.
- Form durumu değerlerin yanında dokunulma, kirlilik, gönderiliyor bayrağı ve gönderim
  sayısını taşır; her biri farklı bir arayüz kararını besler.
- Kirlilik bayrak olarak tutulmaz, başlangıç değerleriyle karşılaştırılarak hesaplanır;
  sıfırlama, temizleme ve yeni taban belirleme üç ayrı işlemdir.
- Gönderimde çift gönderim durum düzeyinde engellenir, sunucudan gelen alan hataları
  ilgili alanlara bağlanır ve başarısızlıkta kullanıcının girdiği değerler korunur.

#### Sonraki Adım

Bu ders formun değerlerini tuttu ama değerlerin **kabul edilebilir olup olmadığına** hiç
bakmadı. Ölçüm değeri sayı olmalı, sıcaklık için makul bir aralıkta durmalı, tarih
gelecekte olmamalı. Bu kuralların iki yerde bilinmesi gerekir: kullanıcıya anında geri
bildirim vermek için istemcide, isteğin sayfa hiç kullanılmadan da kurulabildiğini bilerek
sunucuda. Aynı kuralı iki yere ayrı ayrı yazmak, ikisinin zamanla ayrışması demektir.
Sonraki ders kuralları tek bir bildirimde toplar ve aynı bildirimin iki tarafta aynı sonucu
verdiğini gösterir.

### Doğrulama Şemaları

Source: https://academia.sh/tr/kurslar/frontend-mimarisi/dogrulama-semalari

Kabul kurallarının tek bir bildirimde toplanması, ham girdinin çözülmüş değere dönüştürülmesi, aynı şemanın istemcide ve sunucuda aynı sonucu vermesi ve hata kodunun iletiden ayrı tutulması.

Önceki ders formun değerlerini tuttu ama değerlerin kabul edilebilir olup olmadığına
bakmadı. Ölçüm değeri sayı olmalı, birimine göre makul bir aralıkta durmalı, tarih
gelecekte olmamalı, istasyon kodu tanımlı biçime uymalı.

Bu kuralların iki yerde bilinmesi gerekir. İstemcide, kullanıcıya sunucuya gitmeden geri
bildirim vermek için. Sunucuda, çünkü Form Olayları dersinde söylendiği gibi istek sayfa
hiç kullanılmadan da kurulabilir; istemci tarafı doğrulama bir kolaylıktır, güvence
değildir. Aynı kuralı iki yere ayrı ayrı yazmak, ikisinin zamanla ayrışması demektir:
istemci kabul eder, sunucu reddeder — ya da tersi, ki bu daha kötüdür.

#### Şema Bir Bildirimdir

Doğrulamayı iç içe koşullarla yazmak çalışır ama üç şeyi imkânsızlaştırır. Kuralları
listelemek, kuralları başka bir ortamda çalıştırmak ve kuralları arayüze bildirmek. Bir
alanın zorunlu olup olmadığını öğrenmek için kod okumak gerekiyorsa, o bilgi ekrandaki
"zorunlu" işaretine bağlanamaz.

**Doğrulama şeması**, kuralların veri olarak yazılmasıdır: hangi alanın hangi kurallara
tabi olduğu bir yapıda durur, kuralları uygulayan işlev ayrıdır. Bunun üç doğrudan sonucu
vardır. Şema **taşınabilir** — aynı yapı başka bir çalışma ortamında da yorumlanabilir.
Şema **sorgulanabilir** — arayüz hangi alanın zorunlu olduğunu şemadan okur. Şema
**birleştirilebilir** — ortak alan kuralları tek yerde tanımlanıp birden çok formda
kullanılır.

Bu, Web'in Temelleri ve HTML kursundaki **kısıt doğrulamasının** yerini almaz; onun
üstüne biner. İşaretlemede bildirilen kısıtlar tarayıcının kendi düzeneğini çalıştırır ve
sayfa betiği hiç yüklenmese bile iş görür. Şemanın kapsadığı alan daha geniştir:
işaretlemede bildirilemeyen çapraz alan kuralları, birime göre değişen aralıklar,
sunucuyla paylaşılan kod tabanı.

#### Doğrulamak Değil, Çözümlemek

Doğrulama işlevi çoğunlukla "geçerli mi" sorusuna doğru–yanlış döndürür. Bu, çağıran kodu
aynı dönüşümü ikinci kez yapmaya zorlar: değer geçerliyse metni sayıya çevir, tarihi
ayrıştır, boşlukları kırp. İki çevrim arasında ufak bir fark, doğrulanan değerle
kullanılan değerin ayrışmasına yol açar.

Daha sağlam yol, doğrulamanın **çözülmüş değeri** döndürmesidir. Kural zinciri ham girdiyi
alır, her adımda dönüştürür ve sonunda kullanıma hazır tiplenmiş bir değer üretir. Alan
geçersizse değer üretilmez; geçerliyse üretilen değer artık kuralların garantisini taşır.
Bu, iki tarafın aynı sonuca varmasını da mümkün kılar: istemci `"-4,2"` dizgisini,
sunucu `-4.2` sayısını verse bile zincirin çıkışı aynıdır.

#### Aynı Şema İki Tarafta

```js
// dogrulama-semasi.mjs — ayni sema hem istemcide hem sunucuda
const SIMDI = Date.parse("2026-01-14T12:00:00Z");   // sinama icin sabitlendi

// --- Kural kitapligi: her kural ham degeri alir, cozulmus deger ya da hata dondurur
const KURALLAR = {
  zorunlu: (ham) =>
    ham === undefined || ham === null || String(ham).trim() === ""
      ? { hata: "zorunlu" } : { deger: ham },
  kod: (ham, desen) =>
    new RegExp(desen).test(String(ham)) ? { deger: String(ham) } : { hata: "biçim" },
  sayi: (ham) => {
    const s = typeof ham === "number" ? ham : Number(String(ham).replace(",", "."));
    return Number.isFinite(s) ? { deger: s } : { hata: "sayı_değil" };
  },
  secenek: (ham, izinli) =>
    izinli.includes(String(ham)) ? { deger: String(ham) } : { hata: "seçenek_dışı" },
  tarih: (ham) => {
    const t = Date.parse(String(ham));
    return Number.isNaN(t) ? { hata: "tarih_değil" } : { deger: t };
  },
  enCokUzunluk: (ham, n) =>
    String(ham ?? "").length <= n ? { deger: String(ham ?? "") } : { hata: "çok_uzun" },
};

const SEMA = {
  alanlar: {
    istasyon: [["zorunlu"], ["kod", "^KY-\\d{2}$"]],
    tarih:    [["zorunlu"], ["tarih"]],
    deger:    [["zorunlu"], ["sayi"]],
    birim:    [["zorunlu"], ["secenek", ["C", "F"]]],
    not:      [["enCokUzunluk", 120]],
  },
  caprazlar: [
    {
      ad: "aralik", gerekli: ["deger", "birim"],
      sina: (v) => {
        const [enAz, enCok] = v.birim === "F" ? [-76, 140] : [-60, 60];
        return v.deger < enAz || v.deger > enCok
          ? { yol: "deger", kod: "aralık_dışı", param: { enAz, enCok } } : null;
      },
    },
    {
      ad: "gelecekDegil", gerekli: ["tarih"],
      sina: (v) => (v.tarih > SIMDI ? { yol: "tarih", kod: "gelecekte" } : null),
    },
  ],
};

function dogrula(sema, girdi) {
  const deger = {}, hatalar = [];
  for (const [alan, kurallar] of Object.entries(sema.alanlar)) {
    let ham = girdi[alan];
    let bozuk = false;
    for (const [ad, param] of kurallar) {
      const sonuc = KURALLAR[ad](ham, param);
      if (sonuc.hata) { hatalar.push({ yol: alan, kod: sonuc.hata, param }); bozuk = true; break; }
      ham = sonuc.deger;                       // cozulen deger sonraki kurala girer
    }
    if (!bozuk) deger[alan] = ham;
  }
  for (const capraz of sema.caprazlar) {
    if (!capraz.gerekli.every((a) => a in deger)) continue;   // girdi eksikse atlanir
    const hata = capraz.sina(deger);
    if (hata) hatalar.push(hata);
  }
  return { gecerli: hatalar.length === 0, deger, hatalar };
}

const yaz = (ad, s) => {
  console.log(`${ad}`);
  console.log(`  geçerli: ${s.gecerli}`);
  console.log(`  değer  : ${JSON.stringify(s.deger)}`);
  console.log(`  hatalar: ${JSON.stringify(s.hatalar)}`);
};

// --- Ayni kayit, iki farkli bicimde ----------------------------------------
// Istemci: butun degerler alanlardan gelen dizgilerdir.
const istemciGirdisi = {
  istasyon: "KY-01", tarih: "2026-01-14", deger: "-4,2", birim: "C", not: "sabah okuması",
};
// Sunucu: JSON govdesinde sayi sayi, dizgi dizgi gelir.
const sunucuGovdesi = {
  istasyon: "KY-01", tarih: "2026-01-14", deger: -4.2, birim: "C", not: "sabah okuması",
};

yaz("istemci girdisi (dizgiler)", dogrula(SEMA, istemciGirdisi));
yaz("sunucu gövdesi (JSON)", dogrula(SEMA, sunucuGovdesi));

const a = JSON.stringify(dogrula(SEMA, istemciGirdisi));
const b = JSON.stringify(dogrula(SEMA, sunucuGovdesi));
console.log("iki taraf aynı sonucu verdi mi:", a === b);

// --- Formu hic kullanmayan bir istek ---------------------------------------
yaz("\nsayfa atlanarak kurulan istek", dogrula(SEMA, {
  istasyon: "XX-9", tarih: "2027-03-01", deger: 900, birim: "K", not: "x".repeat(200),
}));

// --- Hata listesinden 422 govdesi ------------------------------------------
const s = dogrula(SEMA, { istasyon: "KY-01", tarih: "2026-01-14", deger: "88", birim: "C" });
const govde422 = {
  kod: "dogrulama",
  alanlar: s.hatalar.reduce((t, h) => ({ ...t, [h.yol]: { kod: h.kod, param: h.param } }), {}),
};
console.log("\n422 gövdesi:", JSON.stringify(govde422));
```

```
istemci girdisi (dizgiler)
  geçerli: true
  değer  : {"istasyon":"KY-01","tarih":1768348800000,"deger":-4.2,"birim":"C","not":"sabah okuması"}
  hatalar: []
sunucu gövdesi (JSON)
  geçerli: true
  değer  : {"istasyon":"KY-01","tarih":1768348800000,"deger":-4.2,"birim":"C","not":"sabah okuması"}
  hatalar: []
iki taraf aynı sonucu verdi mi: true

sayfa atlanarak kurulan istek
  geçerli: false
  değer  : {"tarih":1803859200000,"deger":900}
  hatalar: [{"yol":"istasyon","kod":"biçim","param":"^KY-\\d{2}$"},{"yol":"birim","kod":"seçenek_dışı","param":["C","F"]},{"yol":"not","kod":"çok_uzun","param":120},{"yol":"tarih","kod":"gelecekte"}]

422 gövdesi: {"kod":"dogrulama","alanlar":{"deger":{"kod":"aralık_dışı","param":{"enAz":-60,"enCok":60}}}}
```

#### Çıktının Okunması

İlk iki blok aynı çözülmüş değeri üretti ve karşılaştırma doğru döndü. Girdiler farklıydı:
istemci ondalık ayırıcı olarak virgül kullanan bir dizgi verdi, sunucu bir sayı. Zincir
ikisini de aynı sayıya çevirdi. Tarih de her iki tarafta aynı zaman damgasına çözüldü.
Bu eşitlik, şemanın tek kaynak olmasının pratik karşılığıdır: sunucunun reddettiği bir
değeri istemcinin kabul etmesi ancak şema ayrışırsa mümkün olur.

Üçüncü blok formu atlayan bir isteği gösteriyor ve dört hata birden bildiriliyor. Kural
zincirinin **ilk hatada durduğuna** dikkat edin: `istasyon` alanı biçim kuralında
takıldığı için sonraki kurallar çalıştırılmadı ve alan çözülmüş değer üretmedi. Bunun
nedeni, sonraki kuralın önceki kuralın çıktısına güvenmesidir; ham bir değeri sayı sanan
bir aralık kuralı anlamsız hata üretir.

Aynı blokta bir eksik var ve bilinçli: `deger` 900 olmasına rağmen aralık hatası
bildirilmedi. Çünkü çapraz kural `birim` alanının çözülmüş olmasını gerektiriyor, `birim`
ise "K" değeriyle geçersiz kaldı. **Çapraz alan kuralları girdileri eksikse atlanır.**
Alternatif — eksik girdiyle varsayılan bir değer kullanmak — yanıltıcı hatalar üretir:
kullanıcı birimi düzelttiğinde bildirilen aralık değişebilir. İstek zaten reddediliyor;
kullanıcı birimi düzeltip yeniden gönderdiğinde aralık hatası o zaman bildirilir.

Son satır döngüyü kapatıyor. Sunucu tarafında aynı şemadan çıkan hata listesi, doğrudan
422 yanıtının gövdesine dönüşüyor. REST İstemcisi dersinde bu gövde `ayrinti` alanında
taşınıyordu; Form Durum Yönetimi dersinde alan adlarıyla eşleştirilip forma bağlanıyordu.
Alan adlarının iki tarafta aynı olması bu zincirin ön koşuludur.

#### Her Kural İki Tarafta Çalışamaz

Şemanın paylaşılması, bütün kuralların paylaşılabildiği anlamına gelmez. Üç sınıf ayırmak
gerekir.

**İki tarafta da çalışan kurallar** girdinin kendisine bakar: biçim, tür, uzunluk, aralık,
alanlar arası tutarlılık. Şemanın gövdesini bunlar oluşturur.

**Yalnız sunucuda çalışabilen kurallar** sistemin durumuna bakar: istasyon kodunun kayıtlı
olması, aynı tarih ve istasyon için ölçümün zaten girilmiş olması, kullanıcının o istasyona
yazma yetkisi. İstemci bunları bilmez ve bilmemelidir. Bu kuralların istemcideki karşılığı,
sunucudan gelen alan hatasını göstermektir.

**Yalnız istemcide anlamlı kurallar** girdinin sunuma ilişkin yanına bakar: alanın uzunluk
sayacı, henüz tamamlanmamış bir girdinin uyarısı. Bunlar gönderimi engellemez.

Bir de sunucuya soru soran ara bir sınıf vardır: kullanıcı yazarken istasyon kodunun
varlığını sorgulamak gibi. Bu, doğrulamanın eşzamansız hâle gelmesidir ve üç kural
gerektirir. Sorgu **gecikmeli tetiklemeyle** seyreltilir. Geç gelen yanıt, Yükleme ve Hata
Durumları dersindeki damga kuralıyla elenir. Gönderim, cevabı bekleyen bir sorgu varken
beklemek yerine gönderilir ve sunucunun kararına güvenilir — eşzamansız doğrulama bir
kolaylıktır, kapı değildir.

#### Hata Kodu, İleti Değil

Şemanın ürettiği hatalar kod ve parametre taşıyor, metin taşımıyor. Bu ayrım üç nedenden
zorunludur.

Metin **dile bağlıdır**. Aynı hata kodunun Türkçe ve İngilizce karşılığı farklıdır; kodu
üreten yerin dili bilmesi gerekmez. Uluslararasılaştırma dersi bu eşlemeyi kurar.

Metin **bağlama bağlıdır**. Aynı `zorunlu` kodu, alanın yanında "Bu alan gerekli", hata
özetinde "Ölçüm değeri girilmedi" biçiminde yazılır. Tek bir metni iki yerde kullanmak
ikisinden birini bozar.

Metin **parametreye bağlıdır**. Aralık hatası, sınırları parametre olarak taşır; ileti
bunları yerine koyarak yazılır. Sınır değiştiğinde metin değil şema değişir.

Kodların adlandırılması da bir sözleşmedir. İstemci ile sunucu aynı kod kümesini
kullanmalıdır; sunucunun tanımadığı bir kod gönderdiği durumda istemci genel bir ileti
göstermeli, kodu ekrana yazmamalıdır.

#### Özet

- Doğrulama şeması kuralları veri olarak yazar; bu sayede kurallar listelenebilir,
  sorgulanabilir ve başka bir ortamda çalıştırılabilir hâle gelir.
- Şema, işaretlemede bildirilen kısıt doğrulamasının yerini almaz, kapsamadığı çapraz alan
  ve duruma bağlı kuralları ekler.
- Kural zinciri doğrulamakla kalmaz, ham girdiyi çözülmüş değere dönüştürür; böylece
  istemcinin dizgisi ile sunucunun sayısı aynı sonuca varır.
- Zincir ilk hatada durur, çünkü sonraki kural önceki kuralın çıktısına güvenir; çapraz
  alan kuralları gerekli girdileri çözülmemişse atlanır.
- Kurallar üç sınıftır: iki tarafta çalışanlar, yalnız sunucuda çalışabilenler ve yalnız
  istemcide anlamlı olanlar. Eşzamansız doğrulama bir kolaylıktır, gönderimi durdurmaz.
- Hata kodu ve parametre taşınır, ileti taşınmaz; metin dile, bağlama ve parametreye
  bağlıdır ve sunum katmanında üretilir.

#### Sonraki Adım

Elde artık her alan için bir hata kodu ve parametresi var. Sıradaki soru bunların ekranda
nasıl görüneceği değil — nasıl **duyulacağı**. Ekrana bakmayan bir kullanıcı, kırmızı
çerçeveyi görmez; hata iletisi alanın altına yazıldığında ekran okuyucunun onu alanla
birlikte okuması kendiliğinden olmaz. Gönderim reddedildiğinde odağın nereye gideceği,
kaç hatanın olduğunun nasıl bildirileceği ve iletinin alanla nasıl ilişkilendirileceği
ayrı kararlar ister. Sonraki ders bu ilişkilendirmeyi kurar.

### Erişilebilir Hata Sunumu

Source: https://academia.sh/tr/kurslar/frontend-mimarisi/erisilebilir-hata-sunumu

Hata iletisinin alanla programlı olarak ilişkilendirilmesi, geçersizlik bildiriminin verilmesi, belge sırasına göre kurulan hata özeti, odağın taşınması ve iletinin hangi anda yazılacağı.

Önceki ders her alan için bir hata kodu ve parametresi üretti. Sıradaki soru bunların
ekranda nasıl görüneceği değil: bir hata iletisi, yalnız görüldüğünde iş görüyorsa yarım
kalmıştır. Ekran okuyucuyla gezen bir gözlemci ölçüm giriş formunu gönderdiğinde, kırmızı
çerçeveyi görmez; alanın altına yazılan metnin o alanla birlikte okunması kendiliğinden
olmaz.

Bu ders hatanın **algılanabilir** hâle getirilmesini ele alır: ileti hangi ögeye bağlanır,
alanın geçersiz olduğu nasıl bildirilir, gönderim reddedildiğinde odak nereye gider ve
ileti hangi anda yazılır.

#### Görmeyen Kullanıcı İçin Hata Nedir

Görsel sunum üç ipucu kullanır: renk, konum ve simge. Üçü de tek başına yetersizdir.

**Renk tek gösterge olamaz.** WCAG 1.4.1 ölçütü bunu doğrudan söyler: bilgi yalnız renkle
aktarılamaz. Kırmızı çerçeve renk ayrımı olmayan bir kullanıcı için sıradan bir çerçevedir.
Rengin yanında metin gerekir.

**Konum programlı bir bağ değildir.** İletinin alanın hemen altında durması gözle kurulan
bir ilişkidir. Erişilebilirlik ağacında iki öge arasında bir bağ yoksa, alana odaklanan
kullanıcı iletiyi hiç duymaz. Bağ açıkça kurulmalıdır.

**Hata bildirimi ile hatanın tanımı ayrı ölçütlerdir.** WCAG 3.3.1 hatanın **saptandığının**
metinle bildirilmesini, 3.3.3 ise mümkünse **düzeltme önerisinin** sunulmasını ister.
"Geçersiz değer" birincisini karşılar, ikincisini karşılamaz; "−60 ile 60 arasında bir
değer girin" ikisini birden karşılar. Bu yüzden önceki dersteki hata parametreleri
iletinin içine yazılır.

#### İletiyi Alana Bağlamak

Bağ, alanın **ek açıklama** listesiyle kurulur. Web'in Temelleri ve HTML kursunda
tanımlanan bu düzenek, alana odaklanıldığında etiketten sonra okunacak metinleri belirler.
Alanın hem bir ipucu hem bir hatası varsa ikisi de listeye girer ve listedeki **sıra
okunma sırasıdır**.

```js
// hata-isaretlemesi.mjs — alan, ipucu, hata ve ozetin iliskilendirilmesi
const ILETILER = {
  zorunlu:      () => "Bu alan zorunludur.",
  "sayı_değil": () => "Sayı girin; ondalık ayırıcı olarak virgül kullanabilirsiniz.",
  "aralık_dışı": (p) => `${p.enAz} ile ${p.enCok} arasında bir değer girin.`,
  gelecekte:    () => "Ölçüm tarihi gelecekte olamaz.",
};

// Alanlar belgedeki siralarina gore tanimlanir; sira odak kararini belirler.
const ALANLAR = [
  { ad: "istasyon", etiket: "İstasyon", ipucu: null },
  { ad: "tarih",    etiket: "Ölçüm tarihi", ipucu: "Gün-ay-yıl biçiminde." },
  { ad: "deger",    etiket: "Ölçüm değeri", ipucu: "Santigrat derece." },
  { ad: "not",      etiket: "Not", ipucu: null },
];

function alanIsaretlemesi(alan, hata) {
  const kimlik = `alan-${alan.ad}`;
  const ipucuKimligi = alan.ipucu ? `${kimlik}-ipucu` : null;
  const hataKimligi = hata ? `${kimlik}-hata` : null;
  // Sira onemlidir: once ipucu, sonra hata okunur.
  const acikla = [ipucuKimligi, hataKimligi].filter(Boolean).join(" ");

  const satirlar = [`<label for="${kimlik}">${alan.etiket}</label>`];
  satirlar.push(
    `<input id="${kimlik}" name="${alan.ad}"` +
    (acikla ? ` aria-describedby="${acikla}"` : "") +
    (hata ? ` aria-invalid="true"` : "") + ">");
  if (alan.ipucu) satirlar.push(`<p id="${ipucuKimligi}">${alan.ipucu}</p>`);
  if (hata) satirlar.push(`<p id="${hataKimligi}">${ILETILER[hata.kod](hata.param)}</p>`);
  return satirlar.join("\n");
}

function ozet(hatalar) {
  // Ozet, hatalarin saptanma sirasina gore degil belgedeki alan sirasina gore dizilir.
  const sirali = ALANLAR
    .map((a) => [a, hatalar.find((h) => h.yol === a.ad)])
    .filter(([, h]) => h);
  const maddeler = sirali.map(([a, h]) =>
    `  <li><a href="#alan-${a.ad}">${a.etiket}: ${ILETILER[h.kod](h.param)}</a></li>`);
  return [
    `<div role="alert" tabindex="-1" id="hata-ozeti">`,
    `  <h2>${sirali.length} alan düzeltilmeli</h2>`,
    "  <ul>", ...maddeler, "  </ul>", "</div>",
  ].join("\n");
}

// Semadan gelen hata listesi (saptanma sirasi belge sirasindan farkli).
const HATALAR = [
  { yol: "deger", kod: "aralık_dışı", param: { enAz: -60, enCok: 60 } },
  { yol: "tarih", kod: "gelecekte" },
];

console.log(ozet(HATALAR));
console.log("");
for (const alan of ALANLAR) {
  console.log(alanIsaretlemesi(alan, HATALAR.find((h) => h.yol === alan.ad)));
}

// --- Odak hedefi -----------------------------------------------------------
const ilkGecersiz = ALANLAR.find((a) => HATALAR.some((h) => h.yol === a.ad));
console.log("\nhata sayısı :", HATALAR.length);
console.log("odak hedefi :", HATALAR.length > 1 ? "#hata-ozeti" : `#alan-${ilkGecersiz.ad}`);
console.log("tek hata olsaydı:", `#alan-${ilkGecersiz.ad}`);
```

```
<div role="alert" tabindex="-1" id="hata-ozeti">
  <h2>2 alan düzeltilmeli</h2>
  <ul>
  <li><a href="#alan-tarih">Ölçüm tarihi: Ölçüm tarihi gelecekte olamaz.</a></li>
  <li><a href="#alan-deger">Ölçüm değeri: -60 ile 60 arasında bir değer girin.</a></li>
  </ul>
</div>

<label for="alan-istasyon">İstasyon</label>
<input id="alan-istasyon" name="istasyon">
<label for="alan-tarih">Ölçüm tarihi</label>
<input id="alan-tarih" name="tarih" aria-describedby="alan-tarih-ipucu alan-tarih-hata" aria-invalid="true">
<p id="alan-tarih-ipucu">Gün-ay-yıl biçiminde.</p>
<p id="alan-tarih-hata">Ölçüm tarihi gelecekte olamaz.</p>
<label for="alan-deger">Ölçüm değeri</label>
<input id="alan-deger" name="deger" aria-describedby="alan-deger-ipucu alan-deger-hata" aria-invalid="true">
<p id="alan-deger-ipucu">Santigrat derece.</p>
<p id="alan-deger-hata">-60 ile 60 arasında bir değer girin.</p>
<label for="alan-not">Not</label>
<input id="alan-not" name="not">

hata sayısı : 2
odak hedefi : #hata-ozeti
tek hata olsaydı: #alan-tarih
```

Üretilen işaretlemede dört karar görünür.

**Ek açıklama listesi hem ipucunu hem hatayı taşır.** Hata çıktığında ipucunun listeden
düşürülmesi sık yapılan bir yanlıştır; kullanıcı hatayı duyar ama beklenen biçimi
öğrenemez. İkisi birlikte kalır ve ipucu önce okunur.

**Geçersizlik ayrıca bildirilir.** İletinin okunması, alanın geçersiz olduğunun
bildirilmesiyle aynı şey değildir. `aria-invalid="true"` alanın durumunu erişilebilirlik
ağacına yazar; ileti metniyle birlikte kullanılır, onun yerine geçmez. Aynı işi yapan bir
başka öznitelik daha vardır — hata iletisini doğrudan gösteren `aria-errormessage` — ancak
o yalnız geçersizlik bildirimi verildiğinde anlam taşır; ek açıklama bağı ise her durumda
işler. Sağlam yol, ek açıklama bağını temel almaktır.

**Hatası olmayan alanda hiçbir öznitelik bulunmaz.** İlk ve son alanda `aria-invalid`
yazılmamış; alan geçerliyken `aria-invalid="false"` yazmak da doğrudur ama gereksizdir.
Önemli olan, hata düzeldiğinde özniteliğin **kaldırılmasıdır**; kalırsa kullanıcı düzelmiş
bir alanı geçersiz duyar.

**Özet belge sırasına göre dizilir.** Şemadan gelen hata listesi `deger` ile başlıyordu,
özette `tarih` önce geldi. Kullanıcı özetteki maddelere yukarıdan aşağıya baktığında
formda da yukarıdan aşağıya ilerlemelidir; saptanma sırası bir uygulama ayrıntısıdır ve
ekrana sızmamalıdır.

#### Özet Kutusu ve Odağın Taşınması

Gönderim reddedildiğinde kullanıcı iki şeyi bilmelidir: gönderimin gerçekleşmediğini ve
neyin düzeltileceğini. Sayfanın hiçbir yeri değişmemiş gibi görünüyorsa — özellikle uzun
bir formda, hatalar ekranın dışında kaldıysa — kullanıcı düğmenin çalışmadığını sanır.

**Hata özeti** formun başında durur, hata sayısını söyler ve her maddesi ilgili alana
bağlantı verir. Bağlantı olması önemlidir: klavyeyle gezen kullanıcı maddeyi seçip doğrudan
alana gider. Madde metni, alanın etiketini ve iletiyi birlikte taşır; yalnız ileti
yazıldığında hangi alandan söz edildiği anlaşılmaz.

**Odak** özet kutusuna taşınır. Kutunun odaklanabilir olması için sekme sırasına
girmeyen bir odak hedefi olarak işaretlenmesi gerekir — çıktıdaki `tabindex="-1"` bunu
yapar; öge programla odaklanabilir ama sekme turuna katılmaz.

Hata sayısına göre iki davranış ayrılır. Tek hata varsa odağı doğrudan o alana taşımak
kullanıcıyı bir adım öne alır. Birden çok hata varsa özet kutusuna taşımak, kullanıcıya
önce durumun tamamını verir. Çıktının son üç satırı bu kararı hesaplıyor.

Odak taşımanın bir sınırı vardır: kullanıcının odağını beklemediği bir yere almak da bir
kesintidir. Kural, odağın **yalnız kullanıcının kendi eylemine yanıt olarak** taşınmasıdır.
Gönderim düğmesine basmak böyle bir eylemdir; arka planda tamamlanan bir istek değildir.

#### Bildirimin Anı

İşaretleme doğru olsa bile iletinin **ne zaman** göründüğü ayrı bir karardır. Kullanıcı
ölçüm değerini yazarken her tuşta "geçersiz" demek, henüz bitmemiş bir girdiyi hatalı ilan
eder ve yazmayı zorlaştırır.

```js
// gosterim-ani.mjs — hata iletisinin ne zaman yazilacagi
function gosterilirMi({ hataVar, dokunuldu, gonderimDenendi, hataGosterilmisti }) {
  if (!hataVar) return false;                 // hata yoksa gosterilecek sey yok
  if (gonderimDenendi) return true;           // gonderimden sonra hepsi gorunur
  if (dokunuldu) return true;                 // alan terk edildi
  return hataGosterilmisti;                   // bir kez gosterildiyse izlemeye devam
}

// Hata yokken cevap her zaman "hayir"; tablo yalniz hatali alani sayiyor.
const DURUMLAR = [];
for (const dokunuldu of [false, true])
  for (const gonderimDenendi of [false, true])
    for (const hataGosterilmisti of [false, true])
      DURUMLAR.push({ hataVar: true, dokunuldu, gonderimDenendi, hataGosterilmisti });

const im = (b) => (b ? "evet" : "hayır");
console.log("dokunuldu  gönderim  önceden  →  gösterilir");
for (const d of DURUMLAR) {
  console.log(
    im(d.dokunuldu).padEnd(11) + im(d.gonderimDenendi).padEnd(10) +
    im(d.hataGosterilmisti).padEnd(9) + "   " + im(gosterilirMi(d)));
}

// Kullanicinin gercek yolculugu: yazarken hata yok, terk edince var, duzeltince kayboluyor.
console.log("\nadım adım bir alanın yaşamı");
const YOL = [
  ["boş alana '-' yazıldı",   { hataVar: true,  dokunuldu: false, gonderimDenendi: false }],
  ["'-4' yazıldı",            { hataVar: false, dokunuldu: false, gonderimDenendi: false }],
  ["alan silindi",            { hataVar: true,  dokunuldu: false, gonderimDenendi: false }],
  ["alan terk edildi",        { hataVar: true,  dokunuldu: true,  gonderimDenendi: false }],
  ["geri dönülüp '9' yazıldı",{ hataVar: false, dokunuldu: true,  gonderimDenendi: false }],
  ["'900' yapıldı",           { hataVar: true,  dokunuldu: true,  gonderimDenendi: false }],
];
let onceden = false;
for (const [ad, kismi] of YOL) {
  const g = gosterilirMi({ ...kismi, hataGosterilmisti: onceden });
  onceden = g;
  console.log(`  ${ad.padEnd(27)} hata=${im(kismi.hataVar).padEnd(6)} → ${im(g)}`);
}
```

```
dokunuldu  gönderim  önceden  →  gösterilir
hayır      hayır     hayır       hayır
hayır      hayır     evet        evet
hayır      evet      hayır       evet
hayır      evet      evet        evet
evet       hayır     hayır       evet
evet       hayır     evet        evet
evet       evet      hayır       evet
evet       evet      evet        evet

adım adım bir alanın yaşamı
  boş alana '-' yazıldı       hata=evet   → hayır
  '-4' yazıldı                hata=hayır  → hayır
  alan silindi                hata=evet   → hayır
  alan terk edildi            hata=evet   → evet
  geri dönülüp '9' yazıldı    hata=hayır  → hayır
  '900' yapıldı               hata=evet   → evet
```

Tablonun ikinci satırı kuralın çekirdeğidir: alan henüz terk edilmemiş ve gönderim
denenmemiş olsa bile, hata **bir kez gösterildiyse** gösterilmeye devam eder. Bunun
sonucu, ikinci çıktı bloğunun son iki satırında görünüyor — alan bir kez hatalı bulunduktan
sonra kullanıcı yazdıkça ileti anında güncelleniyor; düzeltme yapılınca kayboluyor,
yeniden bozulunca dönüyor.

İlk üç satırdaki davranış da bilinçlidir. Kullanıcı boş alana eksi işareti yazdığında
değer henüz geçersizdir ama ileti çıkmaz; girdi devam ediyordur. İleti ilk kez alan terk
edildiğinde belirir.

Bu kural, önceki dersteki `dokunulan` kümesi ile `gonderimSayisi` sayacının neden ayrı
tutulduğunu açıklıyor. İkisi de "bu alanı gösterebilir miyim" sorusunun girdisidir ve
farklı anları işaretler.

#### Canlı Bölge mi, Odak mı

Sayfada beliren bir metnin ekran okuyucu tarafından okunmasının iki yolu vardır: odağı
oraya taşımak ya da metni bir **canlı bölge** içine koymak. WCAG 4.1.3 ölçütü, odak
almayan durum iletilerinin de duyurulmasını ister.

Ayrım şu soruyla yapılır: kullanıcının o an bir şey yapması gerekiyor mu?

**Odak taşıma** kullanıcıyı yönlendirir ve akışını keser. Gönderim reddedildiğinde
uygundur; kullanıcı zaten bir sonuç bekliyordur ve devam etmek için bir şey yapması
gerekir. Çıktıdaki özet kutusu `role="alert"` taşıyor; bu rol, kutunun içeriğini
belirdiği anda duyurulacak biçimde işaretler ve odak taşımayla birlikte kullanılır.

**Canlı bölge**, kullanıcının akışını kesmeden bilgi verir. Alan alan doğrulanan bir formda
her ileti için odak taşınmaz; iletiler kibar öncelikli bir bölgede birikir ve kullanıcı
yazmayı bıraktığında okunur.

Aşırı bildirim gerçek bir kusurdur. Her tuşta güncellenen bir canlı bölge, ekran okuyucuyu
sürekli konuşturur ve kullanıcının kendi yazdığını duymasını engeller. Bu yüzden alan alan
doğrulama sonuçları gecikmeli tetiklemeyle seyreltilir, gönderim sonucu ise tek seferlik
ve kesin bir duyuru olarak verilir.

#### Özet

- Renk tek gösterge olamaz ve konum programlı bir bağ değildir; ileti alana ek açıklama
  bağıyla açıkça iliştirilir.
- Ek açıklama listesi hem ipucunu hem hatayı sırayla taşır; geçersizlik bildirimi iletinin
  yerine geçmez, onunla birlikte verilir ve hata düzeldiğinde kaldırılır.
- Hata özeti belge sırasına göre dizilir, hata sayısını söyler ve her maddesi ilgili alana
  bağlantı verir.
- Odak, tek hata varsa doğrudan alana, birden çok hata varsa özet kutusuna taşınır; taşıma
  yalnız kullanıcının kendi eylemine yanıt olarak yapılır.
- İleti ilk kez alan terk edildiğinde ya da gönderim denendiğinde yazılır; bir kez
  yazıldıktan sonra her değişiklikte güncellenir.
- Odağı kesmeyen bildirimler canlı bölgeyle verilir; her tuşta güncellenen bir bölge aşırı
  bildirim üretir ve gecikmeli tetiklemeyle seyreltilir.

#### Sonraki Adım

Ölçüm giriş formu artık değerini tutuyor, kurallarını iki tarafta aynı biçimde uyguluyor
ve hatasını herkesin algılayabileceği biçimde sunuyor. Geriye formun sorduğu bir soru
kaldı: bu ölçümü kim giriyor? Kuzey Yamaç kayıtlarına herkes yazamaz; gözlemcinin önce
tanınması gerekir. Sonraki ders kimlik akışının istemci tarafını ele alır — sunucunun
verdiği belirtecin tarayıcıda nerede saklanacağı ve bu kararın hangi riskleri açıp hangi
riskleri kapattığı.

### Tarayıcıda Kimlik Doğrulama

Source: https://academia.sh/tr/kurslar/frontend-mimarisi/tarayicida-kimlik-dogrulama

Kimlik doğrulama ile yetkilendirmenin ayrımı, oturum çerezi ile taşıyıcı belirtecin karşılaştırılması, belirteç gövdesinin okunabilirliği, üç saklama seçeneğinin risk–fayda çözümlemesi ve çerez niteliklerinin işlevi.

Ölçüm giriş formu artık değerini tutuyor, kurallarını iki tarafta aynı biçimde uyguluyor ve
hatasını herkesin algılayabileceği biçimde sunuyor. Geriye formun sorduğu bir soru kaldı:
bu ölçümü kim giriyor? Kuzey Yamaç kayıtlarına herkes yazamaz; gözlemcinin önce tanınması
gerekir.

Bu ders kimlik akışının **istemci tarafını** ele alır: sunucu gözlemciyi tanıdıktan sonra
verdiği kimlik kanıtı tarayıcıda nerede durur, hangi istekle birlikte gider ve bu kararın
hangi riskleri açıp hangilerini kapattığı. Kapsam bilinçli olarak dardır — istemci
güvenliğinin bütünü, içerik güvenlik ilkesi ve kaynaklar arası paylaşım dahil, Frontend
Kalitesi kursunun güvenlik konusunda ele alınır. Burada yalnız kimlik akışının kendisi
vardır.

#### Kimlik Doğrulama ile Yetkilendirme Ayrı Katmanlardır

**Kimlik doğrulama (authentication)**, "sen kimsin" sorusunun yanıtıdır. Gözlemci
kullanıcı adı ve parolasını verir, sunucu bunu doğrular ve bir kimlik kanıtı üretir.

**Yetkilendirme (authorization)**, "bunu yapabilir misin" sorusunun yanıtıdır. Tanınmış bir
gözlemcinin KY-01 istasyonuna ölçüm yazma hakkı olabilir, KY-02'ye olmayabilir.

İkisinin ayrı tutulması bir söz oyunu değildir. Yetkilendirme kararı **her zaman sunucuda**
verilir; istemcinin yaptığı, kullanıcının erişemeyeceği bir düğmeyi göstermemektir. Bu
görsel eleme bir güvenlik denetimi değil, bir kullanışlılık iyileştirmesidir: düğmeyi
gizlemek, isteğin başka yollarla kurulmasını engellemez.

#### İki Taşıma Biçimi

Kimlik kanıtının istekle birlikte gitmesinin iki yolu vardır.

**Oturum çerezi**, sunucunun ürettiği rastgele bir tanımlayıcıyı taşır. Tanımlayıcının
kendisi bir anlam taşımaz; sunucu onu kendi oturum kayıtlarında arar ve karşılığındaki
kullanıcıyı bulur. Çerez, tarayıcı tarafından **kendiliğinden** gönderilir; uygulama kodu
her isteğe bir şey eklemez.

**Taşıyıcı belirteç (bearer token)**, kullanıcı bilgisini kendi içinde taşıyan imzalı bir
dizgidir. JWT bu biçimin yaygın bir örneğidir. Sunucu her istekte kayıt aramak zorunda
kalmaz, imzayı doğrulayıp gövdedeki iddialara bakar. Belirteç bir başlıkla elle gönderilir;
tarayıcı bunu kendiliğinden yapmaz.

Ayrımın iki pratik sonucu vardır. Kendiliğinden gönderilme, oturum çerezini **kaynaklar
arası istek sahteciliğine (CSRF)** açık kılar: başka bir sayfadan tetiklenen bir istek de
çerezi yanında götürür. Elle gönderilme, taşıyıcı belirteci bu riskten uzak tutar ama
belirtecin bir yerde saklanmasını zorunlu kılar — ve saklandığı yer yeni bir risk açar.
Bu ödünleşim dersin çekirdeğidir.

Kendi içinde bilgi taşımanın ikinci sonucu **geri alınamazlıktır**. Oturum kaydı sunucuda
silinince oturum o an biter. İmzalı bir belirteç ise süresi dolana kadar geçerli kalır;
iptal edilmesi için sunucunun ayrıca bir geçersizler listesi tutması gerekir — ki bu, kayıt
tutmama kazancını büyük ölçüde geri verir. Uygulamada bu, belirteçlerin **kısa ömürlü**
tutulmasıyla dengelenir.

#### Belirteç Gövdesi Gizli Değildir

Belirteç imzalıdır; bu, içeriğinin **değiştirilemeyeceği** anlamına gelir, gizli olduğu
anlamına gelmez. Aradaki fark, belirtecin içine ne konacağı ve istemcinin onunla ne
yapabileceği sorularını doğrudan belirler.

```js
// belirtec-okuma.mjs — belirtecin istemcide ne kadar okunabilir oldugu
import { createHmac, timingSafeEqual } from "node:crypto";

const b64 = (v) => Buffer.from(typeof v === "string" ? v : JSON.stringify(v))
  .toString("base64url");

// --- Sunucu tarafi: belirteci uretir ve imzalar -----------------------------
const GIZLI = "yalnız-sunucuda-duran-anahtar";
const SIMDI = 1768348800;                     // 2026-01-14T12:00:00Z, sinama icin sabit

function belirtecUret(iddialar, gizli) {
  const govde = `${b64({ alg: "HS256", typ: "JWT" })}.${b64(iddialar)}`;
  const imza = createHmac("sha256", gizli).update(govde).digest("base64url");
  return `${govde}.${imza}`;
}

const belirtec = belirtecUret({
  sub: "gozlemci-3", rol: "yazar", istasyon: "KY-01",
  iat: SIMDI, exp: SIMDI + 900,               // 15 dakika
}, GIZLI);

console.log("belirteç:", belirtec);

// --- Istemci tarafi: govdeyi imzayi dogrulamadan okur -----------------------
function govdeyiOku(belirtec) {
  const parcalar = belirtec.split(".");
  if (parcalar.length !== 3) return null;
  try { return JSON.parse(Buffer.from(parcalar[1], "base64url").toString("utf8")); }
  catch { return null; }
}

const iddialar = govdeyiOku(belirtec);
console.log("istemcinin okuduğu gövde:", JSON.stringify(iddialar));

// Sure denetimi bir kullanislilik hesabidir, bir yetki karari degildir.
const PAY = 30;                               // saat kaymasi payi (saniye)
const suresiDoldu = (i, simdi) => simdi >= i.exp - PAY;
for (const simdi of [SIMDI, SIMDI + 600, SIMDI + 880, SIMDI + 1000]) {
  console.log(
    `t=${String(simdi - SIMDI).padStart(4)}s  kalan=${String(iddialar.exp - simdi).padStart(4)}s`,
    ` yenilensin mi: ${suresiDoldu(iddialar, simdi) ? "evet" : "hayır"}`);
}

// --- Sunucu tarafi: imza dogrulamasi ---------------------------------------
function imzaGecerliMi(belirtec, gizli) {
  const [b, g, imza] = belirtec.split(".");
  const beklenen = createHmac("sha256", gizli).update(`${b}.${g}`).digest("base64url");
  const a = Buffer.from(imza), c = Buffer.from(beklenen);
  return a.length === c.length && timingSafeEqual(a, c);
}

// Govdesi degistirilmis bir belirtec: rol "yazar" yerine "yonetici".
const [bas, , eskiImza] = belirtec.split(".");
const degisik = `${bas}.${b64({ ...iddialar, rol: "yonetici" })}.${eskiImza}`;

console.log("\nözgün belirteç, sunucu doğrulaması :", imzaGecerliMi(belirtec, GIZLI));
console.log("gövdesi değiştirilmiş, sunucu       :", imzaGecerliMi(degisik, GIZLI));
console.log("gövdesi değiştirilmiş, istemci okur :",
  JSON.stringify(govdeyiOku(degisik).rol));
```

```
belirteç: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJnb3psZW1jaS0zIiwicm9sIjoieWF6YXIiLCJpc3Rhc3lvbiI6IktZLTAxIiwiaWF0IjoxNzY4MzQ4ODAwLCJleHAiOjE3NjgzNDk3MDB9.gwTdVAyLlHGq0DNaEXcRIsRnVA24TMTg5Q4ylPoN0aY
istemcinin okuduğu gövde: {"sub":"gozlemci-3","rol":"yazar","istasyon":"KY-01","iat":1768348800,"exp":1768349700}
t=   0s  kalan= 900s  yenilensin mi: hayır
t= 600s  kalan= 300s  yenilensin mi: hayır
t= 880s  kalan=  20s  yenilensin mi: evet
t=1000s  kalan=-100s  yenilensin mi: evet

özgün belirteç, sunucu doğrulaması : true
gövdesi değiştirilmiş, sunucu       : false
gövdesi değiştirilmiş, istemci okur : "yonetici"
```

Çıktı üç kural veriyor.

**Gövde herkesin okuyabileceği bir metindir.** Kodlama bir şifreleme değildir; belirteci
eline geçiren herkes içindekini okur. Bu yüzden gövdeye kullanıcının kimliği ve
yetki adları dışında bir şey — kişisel bilgi, iç sistem kimlikleri, gizli değer —
konmaz.

**İmza değişikliği yakalar ama yalnız sunucuda.** Değiştirilmiş belirteç sunucu
doğrulamasından geçmedi; imza gövdeyle uyuşmuyor. Aynı belirtecin gövdesini istemci
sorunsuz okuyor ve rolü "yonetici" görüyor. Yani **istemcinin okuduğu rol bir iddia
değildir, yalnızca bir gösterimdir**. Arayüz bu bilgiyle menü çizebilir; erişim kararı
veremez.

**Süre denetimi bir kullanışlılık hesabıdır.** İstemci `exp` alanına bakıp belirteci
süresi dolmadan yenileyebilir; böylece kullanıcı 401 yanıtıyla karşılaşmaz. Ama bu denetim
bir yetki kararı değildir: istemcinin saati yanlış olabilir, gövde değiştirilmiş olabilir.
Örnekteki otuz saniyelik pay, saat kaymasına karşı konur ve kararı erkene çeker. Kararı
veren yine sunucudur.

#### Belirteç Deposu: Üç Seçenek

Belirtecin nerede saklanacağı üç seçeneğe indirgenir ve üçü farklı riskleri açar. Tabloda
"betik çalıştırılırsa" ifadesi, sayfada uygulamaya ait olmayan bir betiğin çalıştığı
durumu — siteler arası betik çalıştırma, XSS — anlatır.

| Ölçüt | Bellekte değişken | Web Storage | `HttpOnly` çerez |
|---|---|---|---|
| Betik çalıştırılırsa okunur mu | Evet (aynı çalışma bağlamında) | Evet, doğrudan | Hayır, betikten erişilemez |
| Sahtecilik (CSRF) riski | Yok, elle gönderilir | Yok, elle gönderilir | Var; nitelikler ve belirteçle daraltılır |
| Sayfa yenilenince | Kaybolur | Kalır | Kalır |
| Sekmeler arasında | Paylaşılmaz | `localStorage`'da paylaşılır, `sessionStorage`'da paylaşılmaz | Paylaşılır |
| Alt alan adlarına sızma | Yok | Kaynak başına ayrıdır | `Domain` yazılırsa olur |
| Sunucu tarafında iptal | Ömür kısaysa kolay | Ömür kısaysa kolay | Oturum kaydıyla anında |

Tablodan çıkan tek bir "doğru seçenek" yoktur; iki yaygın ve savunulabilir düzen vardır.

**Erişim belirteci bellekte, yenileme `HttpOnly` çerezde.** Kısa ömürlü erişim belirteci
hiçbir kalıcı depoya yazılmaz; sayfa yenilendiğinde kaybolur ve çerezle yapılan bir
yenileme isteğiyle yeniden alınır. Betik çalıştırılırsa erişim belirteci okunabilir ama
ömrü dakikalarla sınırlıdır; yenileme belirtecine hiç erişilemez.

**Her şey `HttpOnly` çerezde, oturum kaydı sunucuda.** Betikten hiçbir kimlik değeri
okunamaz ve iptal anında yapılabilir. Karşılığında sahtecilik riski üstlenilir ve
niteliklerle daraltılır.

Kaçınılması gereken düzen, **yenileme belirtecinin Web Storage'da tutulmasıdır**: uzun
ömürlü bir kimlik kanıtı, betikten okunabilen bir yerde durur. Kısa ömürlü erişim
belirtecinin Web Storage'da tutulması ise bir ödünleşimdir — sayfa yenilenmesine dayanma
kazancı karşılığında okunabilirlik riski alınır.

Depolama seçiminin sınırı şudur: **betik çalıştırılabiliyorsa depo neredeyse önemsizdir.**
Belirteç `HttpOnly` çerezde olsa bile, sayfada çalışan bir betik kullanıcının adına istek
gönderebilir. Depolama kararı riski daraltır, ortadan kaldırmaz; asıl savunma, betiğin hiç
çalıştırılamamasıdır ve o savunma Frontend Kalitesi kursunun konusudur.

#### Çerez Niteliklerinin İşlevi

Çerez kullanan düzenlerde dört nitelik doğrudan güvenlik işlevi taşır ve dördü de sunucunun
yazdığı başlıkta bildirilir.

`HttpOnly`, çerezin betikten okunmasını engeller. Bu nitelik yazılmamışsa çerez, Web
Storage'dan farksız biçimde okunabilir hâle gelir.

`Secure`, çerezin yalnız şifreli bağlantıda gönderilmesini sağlar. Yerel geliştirme
adresleri dışında istisnası yoktur.

`SameSite`, çerezin başka bir siteden tetiklenen isteklerde gönderilip gönderilmeyeceğini
belirler. `Strict` hiçbir dış bağlamda göndermez ve dış bağlantıyla gelen kullanıcının
oturumsuz görünmesine yol açar; `Lax` üst düzey gezinmelerde gönderir, gömülü isteklerde
göndermez; `None` her durumda gönderir ve yalnız `Secure` ile birlikte kullanılabilir.
Sahtecilik riskini asıl daraltan nitelik budur, ama tek başına yeterli sayılmaz: durum
değiştiren istekler ayrıca bir sahtecilik belirteciyle korunur.

`Path` ve `Domain`, çerezin gönderileceği yolları ve alan adlarını belirler. `Domain`
yazmak çerezi bütün alt alan adlarına açar; bir alt alan adı ele geçtiğinde oturumun da
etkilenmesi demektir. Yazılmadığında çerez yalnız kendi ana bilgisayar adına gider ki
çoğu durumda istenen budur. Çerez adına konan `__Host-` öneki bu daraltmayı kural hâline
getirir: bu önekli bir çerez ancak `Secure` ile, `Domain` yazılmadan ve kök yolla kabul
edilir.

#### İstemcinin Kimlik Durumu

Uygulamanın "oturum açık mı" sorusuna verdiği yanıt da bir durum parçasıdır ve Durum
Yönetimi konusundaki ayrımlara uyar. Bu durum, belirtecin kendisinden değil,
**belirtecin varlığı ve sunucunun yanıtlarından** türetilir.

Uygulama açılışında kimlik durumu bilinmez. Bellekte belirteç tutan bir düzende sayfa
yenilendiğinde elde hiçbir şey yoktur; oturumun sürüp sürmediğini öğrenmenin tek yolu
sunucuya sormaktır. Bu, kimlik durumunun dördüncü bir değeri olduğu anlamına gelir:
`bilinmiyor`. Bu değer olmadan yazılan arayüzler, açılışta bir an için oturum kapalı
görünür ve kullanıcıyı giriş ekranına atar.

Rota koruma da bu duruma bağlanır. Yönlendirme konusundaki rota koruma kuralı istemci
tarafında **görünürlüğü** yönetir; korunan bir rotanın verisi yine sunucudan 401 alarak
korunur. İstemci koruması atlanabilir; sunucu koruması atlanamaz.

#### Özet

- Kimlik doğrulama "kimsin", yetkilendirme "bunu yapabilir misin" sorusudur; yetki kararı
  her zaman sunucuda verilir, istemci yalnız görünürlüğü yönetir.
- Oturum çerezi tarayıcı tarafından kendiliğinden gönderilir ve sahteciliğe açıktır;
  taşıyıcı belirteç elle gönderilir ve saklanmak zorundadır.
- Belirtecin imzalı olması içeriğinin değiştirilemez olduğunu söyler, gizli olduğunu değil;
  gövde herkesçe okunabilir ve istemcinin okuduğu rol bir yetki kanıtı değildir.
- Süre denetimi istemcide bir kullanışlılık hesabıdır; saat kayması payıyla erkene çekilir
  ama kararı sunucu verir.
- Üç saklama seçeneği farklı riskler açar; savunulabilir iki düzen, erişim belirtecini
  bellekte yenilemeyi `HttpOnly` çerezde tutmak ya da her şeyi çerezde tutup sahteciliğe
  karşı ayrıca korunmaktır.
- `HttpOnly`, `Secure`, `SameSite` ve `Domain` nitelikleri riski daraltır; sayfada betik
  çalıştırılabiliyorsa hiçbir depolama seçeneği tek başına korumaz.

#### Sonraki Adım

Bu ders belirtecin kısa ömürlü olmasını bir savunma olarak sundu ama kısa ömrün bir bedeli
var: on beş dakikada bir oturumun düşmesi. Gözlemci ölçüm formunu doldururken belirtecin
süresi dolarsa gönderim 401 ile döner ve yazdığı her şey tehlikeye girer. Sonraki ders bu
bedeli ödemeden kısa ömrü korumanın yolunu kurar: kullanıcı fark etmeden yapılan yenileme,
aynı anda düşen birden çok isteğin tek bir yenilemede toplanması ve oturumun her yerde
aynı anda kapanması.

### Oturum Yenileme

Source: https://academia.sh/tr/kurslar/frontend-mimarisi/oturum-yenileme

Kısa ömürlü belirtecin kullanıcıya yansımadan tazelenmesi, eşzamanlı yetki hatalarının tek bir yenilemede toplanması, yenileme belirtecinin dönüşümü ve yeniden kullanım saptaması, çıkışın her yerde etkili olması.

Önceki ders belirtecin kısa ömürlü olmasını bir savunma olarak sundu. Kısa ömrün bedeli
belli: on beş dakikada bir oturumun düşmesi. Gözlemci ölçüm formunu doldururken sürenin
dolması, gönderimin 401 ile dönmesi ve yazılan her şeyin tehlikeye girmesi demektir.

Bu ders bedeli ödemeden kısa ömrü koruyan düzeni kurar. Kullanıcının fark etmediği bir
tazeleme, aynı anda düşen birden çok isteğin tek bir tazelemede toplanması, tazeleme
kanalının kendisinin korunması ve oturumun her yerde aynı anda kapanması.

#### Yenilemenin Üç Tetikleyicisi

**Tepkisel yenileme**, 401 yanıtı alındığında yapılır. İstek katmanı yanıtı görür,
belirteci tazeler, isteği bir kez yineler. En basit ve en dayanıklı yoldur: sunucunun
kararına dayanır, istemcinin saatine güvenmez.

**Önden yenileme**, sürenin dolmasına az kaldığında yapılır. Önceki dersteki süre denetimi
buna hizmet eder; belirtecin `exp` alanı okunur ve bir pay bırakılarak erkenden tazelenir.
Kazancı, kullanıcının bir isteğin iki kez gitmesini beklememesidir. Tek başına yeterli
değildir — istemcinin saati yanlış olabilir, sunucu belirteci erken iptal etmiş olabilir —
bu yüzden tepkisel yenilemenin yerine geçmez, onunla birlikte çalışır.

**Açılıştaki sessiz oturum kurma**, sayfa yenilendiğinde yapılır. Erişim belirtecini
bellekte tutan bir düzende sayfa yenilendiğinde elde bir şey kalmaz; uygulama açılırken
yenileme kanalını bir kez dener. Başarılı olursa oturum kaldığı yerden sürer, başarısız
olursa oturum kapalı sayılır. Önceki derste anılan `bilinmiyor` kimlik durumu tam olarak
bu deneme süresince geçerlidir.

#### Tek Uçuş

Bir ekran açıldığında genellikle birkaç istek birden gider: istasyon listesi, ölçüm
geçmişi, özet, uyarılar. Belirtecin süresi dolduysa dördü de 401 alır. Her biri kendi
yenilemesini başlatırsa sunucuya dört yenileme isteği gider — ve belirteç dönüşümü varsa
üçü geçersiz belirteçle gelir, oturum kapanır.

Çözüm **tek uçuş (single flight)** düzenidir: yenileme başlatılırken üretilen söz saklanır,
aynı anda yenileme isteyen herkes o sözü bekler.

```js
// oturum-yenileme.mjs — sessiz yenileme, tek ucus ve yeniden kullanim saptamasi
import http from "node:http";

// --- Sunucu -----------------------------------------------------------------
let surum = 1;
let gecerliErisim = "e1", gecerliYenileme = "y1";
let oturumAcik = true, yenilemeSayaci = 0;
const kullanilmis = new Set();

const govdeyiOku = (istek) => new Promise((coz) => {
  let m = ""; istek.on("data", (p) => (m += p)); istek.on("end", () => coz(m ? JSON.parse(m) : {}));
});

const sunucu = http.createServer(async (istek, yanit) => {
  const yol = new URL(istek.url, "http://127.0.0.1").pathname;
  const gonder = (durum, govde) => {
    yanit.writeHead(durum, { "content-type": "application/json" });
    yanit.end(JSON.stringify(govde));
  };

  if (yol === "/olcumler") {
    if (!oturumAcik || istek.headers.authorization !== `Bearer ${gecerliErisim}`)
      return gonder(401, { kod: "kimlik_yok" });
    return gonder(200, { kayit: "o-114", surum });
  }

  if (yol === "/oturum/yenile") {
    const { yenileme } = await govdeyiOku(istek);
    if (kullanilmis.has(yenileme)) {          // ayni belirtec ikinci kez: aile iptal
      oturumAcik = false;
      return gonder(401, { kod: "yeniden_kullanim" });
    }
    if (!oturumAcik || yenileme !== gecerliYenileme)
      return gonder(401, { kod: "yenileme_gecersiz" });
    kullanilmis.add(yenileme);
    surum += 1; yenilemeSayaci += 1;
    gecerliErisim = `e${surum}`; gecerliYenileme = `y${surum}`;
    return gonder(200, { erisim: gecerliErisim, yenileme: gecerliYenileme });
  }

  if (yol === "/oturum/kapat") { oturumAcik = false; return gonder(204, {}); }
  gonder(404, { kod: "yol_yok" });
});
await new Promise((coz) => sunucu.listen(0, "127.0.0.1", coz));
const TABAN = `http://127.0.0.1:${sunucu.address().port}`;

// --- Istemci ----------------------------------------------------------------
const oturum = { erisim: "e0", yenileme: "y1", acik: true };   // erisim bilerek eskitildi
let ucustaki = null;

function yenile(kullanilanErisim) {
  // Baskasi zaten yeniledi: bu istek yeni belirtecle dogrudan tekrarlanir.
  if (kullanilanErisim !== oturum.erisim) return Promise.resolve(true);
  if (ucustaki) return ucustaki;

  ucustaki = (async () => {
    const yanit = await fetch(`${TABAN}/oturum/yenile`, {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ yenileme: oturum.yenileme }),
    });
    if (!yanit.ok) { oturum.acik = false; return false; }
    const govde = await yanit.json();
    oturum.erisim = govde.erisim;
    oturum.yenileme = govde.yenileme;         // donusum: eski belirtec artik gecersiz
    return true;
  })();
  ucustaki.finally(() => { ucustaki = null; });
  return ucustaki;
}

async function istek(ad) {
  if (!oturum.acik) return `${ad}: oturum kapalı`;
  const kullanilan = oturum.erisim;
  let yanit = await fetch(`${TABAN}/olcumler`,
    { headers: { authorization: `Bearer ${kullanilan}` } });
  if (yanit.status !== 401) return `${ad}: ${yanit.status}`;

  const yenilendi = await yenile(kullanilan);
  if (!yenilendi) return `${ad}: oturum düştü`;

  yanit = await fetch(`${TABAN}/olcumler`,
    { headers: { authorization: `Bearer ${oturum.erisim}` } });
  const govde = await yanit.json();
  return `${ad}: ${yanit.status} sürüm=${govde.surum ?? "-"}`;
}

// --- 1. Ayni anda dusen dort istek -----------------------------------------
const sonuclar = await Promise.all(["istasyon", "geçmiş", "özet", "uyarı"].map(istek));
sonuclar.forEach((s) => console.log("  " + s));
console.log("sunucudaki yenileme sayısı:", yenilemeSayaci);

// --- 2. Eski yenileme belirtecinin ikinci kez kullanilmasi ------------------
const eski = "y1";
const y = await fetch(`${TABAN}/oturum/yenile`, {
  method: "POST", headers: { "content-type": "application/json" },
  body: JSON.stringify({ yenileme: eski }),
});
console.log("\neski yenileme belirteci →", y.status, JSON.stringify(await y.json()));

// --- 3. Aile iptal edildikten sonra ----------------------------------------
oturum.acik = true;                            // istemci henuz bilmiyor
console.log("iptal sonrası istek        →", await istek("geçmiş"));
console.log("istemcinin oturum durumu   :", oturum.acik ? "açık" : "kapalı");

sunucu.close();
```

```
  istasyon: 200 sürüm=2
  geçmiş: 200 sürüm=2
  özet: 200 sürüm=2
  uyarı: 200 sürüm=2
sunucudaki yenileme sayısı: 1

eski yenileme belirteci → 401 {"kod":"yeniden_kullanim"}
iptal sonrası istek        → geçmiş: oturum düştü
istemcinin oturum durumu   : kapalı
```

#### Çıktının Okunması

Dört istek de başarıyla döndü ve sunucudaki yenileme sayacı **bir** gösterdi. Dört ayrı
çağrı noktası tek bir yenilemeyi paylaştı; hiçbiri bir diğerinin varlığını bilmiyor.

Toplamayı sağlayan iki koruma var ve ikisi de gereklidir. Birincisi **uçuştaki söz**: bir
yenileme sürerken gelen ikinci istek yenisini başlatmaz, sürene katılır. İkincisi
**kullanılan belirtecin karşılaştırılması**: 401'i geç fark eden bir istek, yenileme çoktan
bitmişse hiç yenileme istemez, yeni belirteçle doğrudan yinelenir. Yalnız birincisi
yazıldığında, yenileme bittikten hemen sonra gelen bir 401 gereksiz bir ikinci yenileme
başlatır.

`surum=2` değeri, dört isteğin de **yenilemeden sonraki** belirteçle yanıtlandığını
gösteriyor. Yineleme, yenileme sırasında değişmiş olan belirteci okumak zorundadır;
isteğin başında yakalanan eski değeri kullanmak yinelemeyi de 401'e düşürürdü.

#### Dönüşüm ve Yeniden Kullanım Saptaması

Yenileme belirteci uzun ömürlüdür ve bu onu oturumun en değerli parçası yapar. İki
düzenek riski daraltır.

**Dönüşüm (rotation)**: her yenilemede yalnız erişim belirteci değil, yenileme belirteci de
değişir. Kullanılmış olan bir daha kabul edilmez. Böylece bir yenileme belirtecinin
kullanılabilir ömrü, bir sonraki yenilemeye kadardır.

**Yeniden kullanım saptaması**: kullanılmış bir yenileme belirteci ikinci kez gelirse,
sunucu bunu tek bir istemcinin hatası saymaz. İki olasılık vardır — ya bir kopya
dolaşımdadır ya da yenileme yanıtı istemciye ulaşmadan bağlantı kopmuştur. İkisini ayırt
etmek mümkün olmadığı için sunucu güvenli tarafı seçer ve o oturuma bağlı bütün
belirteçleri iptal eder. Çıktının ikinci bölümü bunu gösteriyor: `y1` ikinci kez
gönderildiğinde yanıt `yeniden_kullanim` oldu.

Üçüncü bölüm iptalin sonucunu gösteriyor. İstemci oturumun kapandığını bilmiyordu ve
isteğini gönderdi; 401 aldı, yenilemeyi denedi, o da reddedildi ve istemci oturumu kapalı
işaretledi. Yani **oturumun bittiğini istemciye söyleyen şey yenilemenin başarısız
olmasıdır.**

Bu düzeneğin bir yan etkisi vardır: ağ kopması yüzünden kaçırılan bir yenileme yanıtı da
oturumu düşürür. Kabul edilen bir ödünleşimdir; alternatifi, kullanılmış bir belirtecin bir
süre daha geçerli sayılmasıdır ki bu, saptamanın değerini azaltır.

#### Döngü ve Kuyruk Tuzakları

Yenileme kodu iki tuzağa açıktır ve ikisi de üretimde görülür.

**Sonsuz döngü.** Yenileme isteğinin kendisi 401 döndürürse ve o istek de aynı yenileme
düzeneğinden geçiyorsa, döngü kapanmaz. Kural: yenileme isteği **kesinlikle** yenileme
düzeneğinden geçmez ve kendi 401'inde yeniden denenmez. Yukarıdaki kodda yenileme çağrısı
`istek` işlevini kullanmıyor, doğrudan gönderiliyor.

**Tek yineleme sınırı.** Yenilendikten sonra yinelenen istek yine 401 alırsa ikinci bir
yenileme denenmez. Bu durum, kullanıcının o kaynağa gerçekten yetkisi olmadığını gösterir;
yetki hatasını kimlik hatası sanıp yenilemeye devam etmek, oturumu boş yere yıpratır.

Bir de kuyruk kararı vardır. Yenileme sürerken gelen yeni istekler iki biçimde ele
alınabilir: eski belirteçle gönderilip 401 alarak kuyruğa katılmak, ya da yenileme bitene
kadar hiç gönderilmemek. İkincisi gereksiz istek üretmez ama uygulamanın her isteği
kimlik durumundan haberdar bir kapıdan geçirmesini gerektirir. Yukarıdaki kod birinciyi
uyguluyor; dört isteğin dördü de gitti, 401 aldı, tek yenilemede toplandı.

#### Çıkış Akışı

Çıkış üç adımdan oluşur ve üçü de gereklidir.

**Sunucuda geçersizleştirme.** Çerez tabanlı bir düzende oturum kaydı silinir ve çerez boş
bir değerle, geçmiş bir son kullanma tarihiyle yeniden yazılır. Belirteç tabanlı bir
düzende yenileme belirteci iptal edilir. Yalnız istemci tarafını temizlemek çıkış değildir;
belirtecin bir kopyası hâlâ geçerli kalır.

**İstemci durumunun temizlenmesi.** Bellekteki belirteç, kimlik durumu ve **önbellekteki
kullanıcıya özel veriler** silinir. Sonuncusu sıkça atlanır: ölçüm geçmişi önbellekte
kalırsa, aynı tarayıcıda oturum açan ikinci bir gözlemci bir önceki kullanıcının verisini
bir an için görür. Veri Erişimi konusundaki normalleştirilmiş depo bu temizliğin hedefidir.

**Sekmeler arası eşitleme.** Aynı kullanıcı uygulamayı üç sekmede açmış olabilir. Bir
sekmede yapılan çıkış diğerlerine bildirilmezse, o sekmeler oturum açıkmış gibi davranmaya
devam eder ve ilk isteklerinde beklenmedik biçimde 401 alır. Bildirim, aynı kaynağın
sekmeleri arasında ileti geçişiyle ya da paylaşılan depolamadaki bir değişikliğin
izlenmesiyle yapılır; ikisi de Tarayıcı ve Web Platformu kursunda tanıtıldı. Aynı düzenek
ters yönde de çalışır: bir sekmede açılan oturum diğerlerinde de açılmış sayılır.

Çıkışın kullanıcı tarafındaki karşılığı da düşünülmelidir. Yarım kalmış bir formun içeriği
çıkışta silinir; kullanıcıya bunun sorulması, sessizce silmekten iyidir. Zorunlu çıkışta —
oturum düştüğünde — kullanıcı giriş ekranına yönlendirilirken bulunduğu adres saklanır ve
giriş sonrası oraya dönülür.

#### Özet

- Yenilemenin üç tetikleyicisi vardır: 401 sonrası tepkisel, süre dolmadan önden ve sayfa
  açılışında sessiz oturum kurma. Önden yenileme tepkiselin yerine geçmez.
- Eşzamanlı yetki hataları tek uçuşlu bir yenilemede toplanır; uçuştaki söz ile kullanılan
  belirtecin karşılaştırılması birlikte gerekir.
- Yinelenen istek, yenileme sırasında değişmiş belirteci okumalıdır; isteğin başında
  yakalanan değer artık geçersizdir.
- Yenileme belirteci her kullanımda dönüşür; kullanılmış bir belirtecin ikinci kez gelmesi
  oturumun tümünün iptaliyle sonuçlanır.
- Yenileme isteği yenileme düzeneğinden geçmez ve yinelenen istek ikinci kez 401 alırsa
  yeniden yenilenmez.
- Çıkış sunucuda geçersizleştirme, istemci durumu ile kullanıcıya özel önbelleğin
  temizlenmesi ve sekmeler arası eşitleme adımlarının üçünü birden gerektirir.

#### Sonraki Adım

Kimlik akışı tamamlandı: gözlemci tanınıyor, oturumu kendiliğinden sürüyor, çıkışı her
yerde etkili oluyor. Ama tanınan gözlemcinin bir de dili var. Bu kurs boyunca üretilen her
metin — hata iletileri, "oturum düştü" bildirimi, boş sonuç açıklamaları, tarih ve sayı
biçimleri — tek bir dile göre yazıldı. İstasyonu Norveç'ten izleyen bir gözlemci için
"−4,2" doğru biçim değildir, "2 alan düzeltilmeli" doğru cümle değildir ve arayüzün akış
yönü bile farklı olabilir. Son ders bu uyarlamayı kurar.

### Uluslararasılaştırma

Source: https://academia.sh/tr/kurslar/frontend-mimarisi/uluslararasilastirma

Metnin koddan ayrılması, çeviri kataloğu ve anahtar tasarımı, çoğul kurallarının dile göre değişmesi, biçimlemenin platforma bırakılması, yön desteği ve yerel ayarın seçilmesi.

Kimlik akışı tamamlandı: gözlemci tanınıyor, oturumu kendiliğinden sürüyor. Ama tanınan
gözlemcinin bir de dili var. Bu kurs boyunca üretilen her metin tek bir dile göre yazıldı —
hata iletileri, "oturum düştü" bildirimi, boş sonuç açıklamaları. Sayılar ve tarihler de
öyle: ölçüm değeri "−4,2" biçiminde yazıldı, tarih gün-ay-yıl sırasıyla gösterildi.

Kuzey Yamaç istasyonunu başka bir ülkeden izleyen bir gözlemci için bunların hiçbiri doğru
değil. Bu ders arayüzü dile ve bölgeye uyarlanabilir kılmanın yapısını kurar.

#### İki Ayrı İş

**Uluslararasılaştırma (internationalization)**, uygulamayı uyarlanabilir hâle getirmektir:
metinlerin koddan ayrılması, biçimlemenin sabitlenmemesi, düzenin yön değişimine dayanması.
Bir kez yapılır ve dilden bağımsızdır.

**Yerelleştirme (localization)**, belirli bir dil ve bölge için içeriğin üretilmesidir:
çevirilerin yazılması, tarih ve para biriminin o bölgeye uygun seçilmesi. Her dil için
ayrı yapılır.

Ayrımın pratik değeri sıradadır. Uluslararasılaştırma yapılmamış bir uygulamaya çeviri
eklemek, kodun her yerine dokunmayı gerektirir. Yapılmış bir uygulamaya ikinci dili eklemek
yalnız bir katalog dosyası yazmaktır. Bu yüzden tek dilli kalacağı düşünülen uygulamalarda
bile metnin koddan ayrılması ucuz bir yatırımdır.

#### Çeviri Kataloğu ve Anahtar Tasarımı

**Çeviri kataloğu**, anahtarları iletilere eşleyen bir yapıdır. Kod anahtarı bilir, metni
bilmez.

Anahtarın kendisi bir tasarım kararıdır. Anahtar olarak Türkçe cümleyi kullanmak — "2 alan
düzeltilmeli" — kolay görünür ama metin her düzeltildiğinde bütün çeviriler kopar. Anlamı
tanımlayan yapılandırılmış bir ad — `form.ozet` — bu bağı korur. Aynı sözcüğün iki yerde
farklı çevrilmesi gerekebileceği için anahtar bağlamı da taşır: `dugme.kaydet` ile
`menu.kaydet` ayrı anahtarlardır, ikisi de Türkçede "Kaydet" olsa bile.

```js
// ceviri-katalogu.mjs — anahtar, yer tutucu ve cogul kurallari
const KATALOG = {
  tr: {
    "form.ozet": { one: "{sayi} alan düzeltilmeli", other: "{sayi} alan düzeltilmeli" },
    "olcum.aralik": "{enAz} ile {enCok} arasında bir değer girin.",
    "oturum.dustu": "Oturumunuz sona erdi.",
  },
  en: {
    "form.ozet": { one: "{sayi} field needs attention", other: "{sayi} fields need attention" },
    "olcum.aralik": "Enter a value between {enAz} and {enCok}.",
  },
};

const VARSAYILAN = "tr";

function ileti(dil, anahtar, degiskenler = {}) {
  const girdi = KATALOG[dil]?.[anahtar] ?? KATALOG[VARSAYILAN]?.[anahtar];
  if (girdi === undefined) return `!${anahtar}`;   // eksik anahtar gizlenmez
  const kalip = typeof girdi === "string"
    ? girdi
    : girdi[new Intl.PluralRules(dil).select(degiskenler.sayi)] ?? girdi.other;
  return kalip.replace(/\{(\w+)\}/g, (tam, ad) =>
    ad in degiskenler ? String(degiskenler[ad]) : tam);
}

console.log("çoğul kategorileri (Intl.PluralRules)");
console.log("sayı".padEnd(6) + ["tr", "en", "ru"].map((d) => d.padEnd(8)).join(""));
for (const sayi of [0, 1, 2, 5, 11, 21, 100]) {
  console.log(String(sayi).padEnd(6) +
    ["tr", "en", "ru"].map((d) => new Intl.PluralRules(d).select(sayi).padEnd(8)).join(""));
}

console.log("\niletiler");
for (const dil of ["tr", "en"]) {
  for (const sayi of [1, 2]) {
    console.log(`  ${dil} sayi=${sayi}: ${ileti(dil, "form.ozet", { sayi })}`);
  }
  console.log(`  ${dil} aralık : ${ileti(dil, "olcum.aralik", { enAz: -60, enCok: 60 })}`);
  console.log(`  ${dil} oturum : ${ileti(dil, "oturum.dustu")}`);
  console.log(`  ${dil} eksik  : ${ileti(dil, "olcum.bilinmeyen")}`);
}
```

```
çoğul kategorileri (Intl.PluralRules)
sayı  tr      en      ru      
0     other   other   many    
1     one     one     one     
2     other   other   few     
5     other   other   many    
11    other   other   many    
21    other   other   one     
100   other   other   many    

iletiler
  tr sayi=1: 1 alan düzeltilmeli
  tr sayi=2: 2 alan düzeltilmeli
  tr aralık : -60 ile 60 arasında bir değer girin.
  tr oturum : Oturumunuz sona erdi.
  tr eksik  : !olcum.bilinmeyen
  en sayi=1: 1 field needs attention
  en sayi=2: 2 fields need attention
  en aralık : Enter a value between -60 and 60.
  en oturum : Oturumunuz sona erdi.
  en eksik  : !olcum.bilinmeyen
```

Çoğul tablosu, elle yazılan bir çoğul kuralının neden yetmediğini gösteriyor. Türkçede ve
İngilizcede iki kategori var ve `sayi === 1` sınaması ikisinde de doğru sonucu verir.
Rusçada dört kategoriden üçü bu tabloda görünüyor ve seçim yalnız sayının kendisine değil
son basamağına da bağlı: 21 "one" kategorisine düşerken 11 "many" kategorisinde kalıyor.
Kuralı elle yazan kod, üçüncü dil eklendiğinde yeniden yazılır. `Intl.PluralRules` kuralı
dilden okur; katalog yalnız hangi kategoride ne yazılacağını söyler.

Türkçenin iki biçiminin aynı metni taşıması bir tutarsızlık değil. Katalog **yapıyı**
korur; hangi dilin kaç biçim kullandığı o dilin kararıdır. Aynı katalog Rusçaya
eklendiğinde `few` ve `many` anahtarları da yazılır, kod değişmez.

İki davranış daha çıktıda görünüyor. **Eksik anahtar gizlenmiyor**: bulunmayan bir anahtar
için ünlem işaretli bir gösterim üretiliyor. Boş dizgi döndürmek ya da anahtarı sessizce
atlamak, eksik çeviriyi görünmez kılar ve sürüm sonrası fark edilir. **Eksik çeviri
varsayılan dile düşüyor**: İngilizce katalogda bulunmayan oturum iletisi Türkçe geldi.
Bu, eksik metnin hiç görünmemesinden iyidir ama bir uyarı işaretidir; geliştirme sırasında
kaydedilmelidir.

Son bir kural: **iletiler dizgi birleştirmesiyle kurulmaz**. "Toplam " + sayi + " kayıt"
biçiminde yazılan bir metin, sözcük sırası farklı bir dile çevrilemez. Parçalar tek bir
kalıpta ve yer tutucuyla durur.

#### Biçimlemeyi Platforma Bırakmak

Metin dışındaki her şey — sayı, tarih, süre, liste, sıralama — dile ve bölgeye göre
değişir ve bu kuralların hepsi platformda hazır bulunur.

```js
// bicimleme.mjs — sayi, tarih, liste ve siralamanin yerel ayara bagliligi
const DILLER = ["tr", "en-US", "de-DE"];
const OLCUM = -4.2;
const AN = new Date("2026-01-14T06:00:00Z");     // sabit an: cikti yeniden uretilebilir

console.log("sıcaklık (birimle)");
for (const d of DILLER) {
  console.log("  " + d.padEnd(6) +
    new Intl.NumberFormat(d, { style: "unit", unit: "celsius",
      minimumFractionDigits: 1 }).format(OLCUM));
}

console.log("büyük sayı");
for (const d of DILLER) {
  console.log("  " + d.padEnd(6) + new Intl.NumberFormat(d).format(1840.5));
}

console.log("tarih ve saat (UTC)");
for (const d of DILLER) {
  console.log("  " + d.padEnd(6) + new Intl.DateTimeFormat(d, {
    dateStyle: "long", timeStyle: "short", timeZone: "UTC" }).format(AN));
}

console.log("göreli zaman (-3 saat)");
for (const d of DILLER) {
  console.log("  " + d.padEnd(6) +
    new Intl.RelativeTimeFormat(d, { numeric: "auto" }).format(-3, "hour"));
}

console.log("liste");
const istasyonlar = ["Kuzey Yamaç", "Sırt", "Vadi"];
for (const d of DILLER) {
  console.log("  " + d.padEnd(6) +
    new Intl.ListFormat(d, { style: "long", type: "conjunction" }).format(istasyonlar));
}

// --- Turkce'ye ozgu iki tuzak ----------------------------------------------
const ADLAR = ["Ordu", "Ödemiş", "Osmaniye", "Sivas", "Şile", "Isparta", "İstanbul"];
console.log("\nsıralama");
console.log("  tr    " + [...ADLAR].sort(new Intl.Collator("tr").compare).join(", "));
console.log("  en    " + [...ADLAR].sort(new Intl.Collator("en").compare).join(", "));
console.log("  ham   " + [...ADLAR].sort().join(", "));

console.log("büyük harfe çevirme");
console.log("  tr    " + "ilk ölçüm".toLocaleUpperCase("tr"));
console.log("  en    " + "ilk ölçüm".toLocaleUpperCase("en"));
```

```
sıcaklık (birimle)
  tr    -4,2°C
  en-US -4.2°C
  de-DE -4,2 °C
büyük sayı
  tr    1.840,5
  en-US 1,840.5
  de-DE 1.840,5
tarih ve saat (UTC)
  tr    14 Ocak 2026 06:00
  en-US January 14, 2026 at 6:00 AM
  de-DE 14. Januar 2026 um 06:00
göreli zaman (-3 saat)
  tr    3 saat önce
  en-US 3 hours ago
  de-DE vor 3 Stunden
liste
  tr    Kuzey Yamaç, Sırt ve Vadi
  en-US Kuzey Yamaç, Sırt, and Vadi
  de-DE Kuzey Yamaç, Sırt und Vadi

sıralama
  tr    Isparta, İstanbul, Ordu, Osmaniye, Ödemiş, Sivas, Şile
  en    Isparta, İstanbul, Ödemiş, Ordu, Osmaniye, Şile, Sivas
  ham   Isparta, Ordu, Osmaniye, Sivas, Ödemiş, İstanbul, Şile
büyük harfe çevirme
  tr    İLK ÖLÇÜM
  en    ILK ÖLÇÜM
```

Bu çıktı **ortama bağlıdır**: biçimlerin ayrıntısı çalışma zamanının içerdiği yerel ayar
verisinin sürümüne göre değişebilir. Yukarıdaki satırlar `node --version` çıktısı v24.18.0
olan bir kurulumda üretildi. Değişebilen şey biçimin ayrıntısıdır; ilkeler değişmez.

Beş gözlem çıkarılır.

**Ondalık ayırıcı ve binlik ayırıcı dile göre yer değiştirir.** Türkçe ve Almanca virgülle
ondalık yazar, İngilizce noktayla. Sayıyı elle biçimleyen kod bunu kaçırır ve "1.840,5"
değerini "1840.5" diye gösterir; daha kötüsü, kullanıcının yazdığı "1,5" değerini yanlış
okur. Form doğrulama şemasındaki virgül–nokta dönüşümünün nedeni buydu.

**Birimin metne yapışması bile dile bağlıdır.** Almanca çıktıda sayı ile derece işareti
arasında boşluk var, Türkçede yok.

**Tarih sırası ve ay adı değişir; saat biçimi de değişir.** İngilizce çıktıda on iki saatlik
gösterim, diğerlerinde yirmi dört saatlik. Zaman dilimi ayrıca verilmelidir; örnekte UTC
sabitlendiği için çıktı yeniden üretilebilir.

**Liste birleştirme dilbilgisidir.** Türkçede son ögeden önce "ve", İngilizcede virgül ve
"and", Almancada "und" gelir. Elle virgül koyup sonuncudan önce bağlaç eklemek her dilde
ayrı bir kural demektir.

**Sıralama ve harf dönüşümü Türkçede özel durumdur.** Ham sıralama kod noktası sırasını
verir ve Türkçe alfabeye hiç uymaz. Türkçe sıralamada `O` ile `Ö`, `S` ile `Ş` ayrı
harflerdir ve bu sırayla dizilir; İngilizce sıralamada aynı harfin çeşitleri sayılırlar,
bu yüzden "Ödemiş" iki listede farklı yerde durur. Harf dönüşümünde de `i` harfinin büyüğü
Türkçede `İ`, diğer dillerde `I`'dır; yerel ayar verilmeden yapılan bir dönüşüm, çalıştığı
makinenin ayarına göre değişen sonuç üretir.

#### Yön Desteği

Sağdan sola yazılan diller yalnız metnin değil düzenin de yön değiştirmesini gerektirir.
İş üç düzeyde ayrılır.

**Belge düzeyi.** Kök ögede yön bildirilir; yön bilgisi bir dil özelliği değil bir yazım
sistemi özelliğidir, bu yüzden dil bildiriminden ayrı verilir. Yön değiştiğinde metnin
hizası, listelerin madde işareti tarafı ve kaydırma yönü kendiliğinden döner.

**Düzen düzeyi.** CSS ile Görsel Sunum kursunda tanıtılan mantıksal kenar adları burada
karşılığını bulur: sol ve sağ yerine blok ve satır içi eksenin başlangıç ve bitiş
kenarları yazıldığında düzen yön değişimine kendiliğinden uyum sağlar. Fiziksel yönle
yazılmış her kural, yön desteği eklenirken tek tek gözden geçirilmelidir.

**Metin düzeyi.** Karışık yönlü metin — sağdan sola bir cümlenin içinde soldan sağa bir
istasyon kodu — özel bir sorun çıkarır: sıralama algoritması sınırdaki noktalama
işaretlerini yanlış tarafa taşıyabilir. Kullanıcıdan gelen ya da veriden gelen bir parçanın
çevresindeki metinden yalıtılması gerekir; işaretlemede bunun için ayrılmış bir öge vardır
ve bu ögenin varsayılan yön kestirimi parçanın kendi içeriğine bakar.

Simgelerin bir kısmı da yönlüdür: ileri oku, geri oku, girinti işaretleri yön değişiminde
aynalanır. Saat yönü, ses düzeyi gibi yönü olmayan simgeler aynalanmaz.

#### Yerel Ayarın Seçilmesi ve Kataloğun Yüklenmesi

Hangi dilin kullanılacağı üç kaynaktan gelir ve öncelik sırası bellidir. **Kullanıcının
açık tercihi** en üsttedir ve saklanır. Yoksa **adresin kendisi** — dil kodunu taşıyan bir
yol parçası ya da alt alan adı — kullanılır; bu, bağlantının paylaşılabilir olmasını
sağlar. İkisi de yoksa tarayıcının bildirdiği dil listesi, yani `Accept-Language` başlığı
değerlendirilir.

Tarayıcının listesi bir sıralı tercihtir, tek bir dil değildir. Eşleme yaparken tam
eşleşme aranır, bulunamazsa bölge düşürülerek denenir; hiçbiri tutmazsa varsayılan dile
düşülür. Yerel ayar bir dilden fazlasını içerir — bölge, takvim, sayı sistemi — bu yüzden
metin dili ile biçimleme ayarının farklı olabileceği durumlar vardır.

Katalog yüklemesi bir paketleme kararıdır. Bütün dilleri tek pakette göndermek, kullanıcıya
kullanmayacağı metinleri indirtir. Dil başına ayrı bir parça üretip yalnız seçilen dili
yüklemek doğru yoldur; bu, Oluşturma Stratejileri kursundaki kod bölme konusuyla doğrudan
ilişkilidir. Dil değiştiğinde yeni katalog yüklenene kadar geçen sürede arayüzün ne
göstereceği — eski dil mi, boşluk mu — Yükleme ve Hata Durumları dersindeki durum
makinesine bağlanır.

Son olarak, çeviri metinleri **yer kaplar**. Aynı cümle bir dilde diğerinden belirgin
biçimde uzun olabilir; sabit genişlikli düğmeler ve tek satıra sığdırılmış etiketler
çeviriyle taşar. Düzenin metin uzunluğuna dayanması bir uluslararasılaştırma gereğidir.

#### Özet

- Uluslararasılaştırma uygulamayı uyarlanabilir kılar ve bir kez yapılır; yerelleştirme her
  dil için ayrı üretilen içeriktir.
- Çeviri kataloğu anlamı tanımlayan yapılandırılmış anahtarlar kullanır; kaynak cümleyi
  anahtar yapmak, metin düzeltildiğinde bütün çevirileri koparır.
- Çoğul kategorileri dile göre değişir ve sayının son basamağına bağlı olabilir; kural
  platformdan okunur, katalog yalnız kategori karşılıklarını taşır.
- İletiler dizgi birleştirmesiyle değil tek bir kalıp ve yer tutucularla kurulur; eksik
  anahtar gizlenmez, eksik çeviri varsayılan dile düşer.
- Sayı, tarih, göreli zaman, liste ve sıralama platformun yerel ayar araçlarına bırakılır;
  Türkçede sıralama ve `i` harfinin dönüşümü yerel ayar verilmeden doğru sonuç vermez.
- Yön desteği belge, düzen ve metin düzeylerinde ayrı ele alınır; mantıksal özellikler
  düzeni, yalıtım ögesi karışık yönlü metni çözer.

#### Kurs Kapanışı

Bu kurs, bir uygulamanın iç mimarisini dört soruyla kurdu.

**Kullanıcı nerede?** Yönlendirme konusu adresi uygulamanın durumunun bir parçası hâline
getirdi: iç içe düzenler ortak kabuğu paylaştırdı, rota parametreleri durumu adreste
taşıdı, rota koruma erişimi görünürlük düzeyinde yönetti, kod bölme her rotanın bedelini
kendine ödetti.

**Bilgi nerede duruyor?** Durum yönetimi konusu durumu türlerine ayırdı — yerel, paylaşılan,
sunucu ve adres durumu — ve her biri için farklı bir kap gösterdi. Sunucu durumunun ayrı
ele alınması, sonraki konunun kapısını açtı.

**Bilgi nasıl geliyor?** Veri erişimi konusu istek katmanını ve hata sözleşmesini kurdu,
sorgu tabanlı erişimin önbellek normalizasyonunu gösterdi, akış hâlinde gelen veriyi
kopmaya dayanıklı biçimde tüketti, görünümün durumlarını sayılı bir kümeye indirdi ve ağ
kararsızlığını kullanıcıya yansımadan soğurdu.

**Bilgi nasıl giriliyor ve kim giriyor?** Formlar ve kimlik konusu değerin nerede
tutulacağını, kuralların iki tarafta nasıl aynı kalacağını, hatanın nasıl algılanabilir
olacağını, kimlik kanıtının nerede saklanacağını, oturumun nasıl süreceğini ve arayüzün
dile nasıl uyarlanacağını kurdu.

Dördünün ortak yanı, hepsinin **tarayıcının içinde** geçmesi. Uygulama açıldıktan sonrasını
anlattık; açılmadan önce ne olduğunu hiç sormadık. Oysa aynı uygulama üç ayrı yerde
oluşturulabilir: kullanıcının tarayıcısında, isteği karşılayan sunucuda ya da daha istek
gelmeden derleme sırasında. Seçim, ilk ekranın ne kadar sürede geldiğini, arama motorunun
ne gördüğünü, sunucunun ne kadar iş yaptığını ve içeriğin ne kadar taze olabileceğini
belirler.

Bir sonraki kurs — **Oluşturma Stratejileri ve Altyapı** — bu seçimi ödünleşimleriyle
kurar: istemcide, sunucuda ve derleme zamanında oluşturmanın maliyetleri, statik çıktının
seçici tazelenmesi, etkileşimin parça parça kazanılması, paketleme ve varlık iyileştirme
ve uygulamanın kullanıcıya ulaştığı dağıtım hattı.

