jskelet 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -10,6 +10,49 @@ one is listed under a **Breaking** heading.
10
10
 
11
11
  ### Added
12
12
 
13
+ - An upstream data cache: `withDataCache(key, ttlSeconds, producer)` and the
14
+ `dataCache(fn, { key, revalidate })` wrapper, with `clearDataCache(prefix?)`,
15
+ `getDataCacheSize()` and `getDataCacheEntries()` alongside them. It keeps JSON
16
+ rather than HTML, so its default limit is 10,000 entries: a long-tail page that
17
+ was never prewarmed still renders without touching the API. Concurrent reads of
18
+ the same key collapse into one upstream request, an expired entry is served
19
+ immediately while it refreshes in the background, a failing producer falls back
20
+ to the stale value, and empty answers (`null`/`undefined`) are not stored
21
+ unless `storeEmpty: true` is passed.
22
+ - `cache().prewarm.priority` decides the warm-up order and accepts both the
23
+ config pattern syntax (`/news/:slug`) and plain regular expressions. Matching
24
+ paths are warmed on every pass.
25
+ - Drip prewarming for large sites: `cache().prewarm.rps` (also `PREWARM_RPS`)
26
+ caps requests per second regardless of parallelism, and `rotate` (on by
27
+ default) makes periodic passes continue through the queue where the previous
28
+ one stopped instead of re-warming the same first slice. A pass is skipped while
29
+ the previous one is still running.
30
+ - `cache().prewarm.retryDelayMs` (also `PREWARM_RETRY_DELAY_MS`) waits before the
31
+ retry pass, since rate limit windows are measured in seconds.
32
+ - `cache().maxEntries` configures the HTML cache limit, which used to be a fixed
33
+ 500.
34
+ - The dev report now includes the data cache entry count under `cache.data`.
35
+
36
+ ### Changed
37
+
38
+ - `notFound()` is no longer served as a 404 when a transient upstream failure
39
+ (`429`, `5xx`, network error) was reported during the same render. Those pages
40
+ now respond with an uncached `503` and `Retry-After`, so a temporary rate limit
41
+ is not frozen into "this page does not exist" for the whole TTL.
42
+ - Responses produced with missing data are no longer offered to shared caches:
43
+ a `degraded` render is sent with `private, no-store` instead of
44
+ `public, s-maxage=…`. The `X-JSkelet-Cache` diagnostic header is still written.
45
+ - The prewarm summary distinguishes paths left for the next pass
46
+ (`700 deferred to the next pass`) from paths dropped entirely
47
+ (`700 over the limit`).
48
+ - The changelog page of the marketing example is generated from the installed
49
+ package's `CHANGELOG.md` instead of a hand-written list, and shows the version
50
+ published on npm next to the installed one.
51
+
52
+ ## [0.1.2] - 2026-08-30
53
+
54
+ ### Added
55
+
13
56
  - `route(fn, { private: true })` for pages that depend on the visitor. The HTML
14
57
  cache is bypassed, `cache.html` patterns can no longer turn caching on for
15
58
  that route, and the response is sent with `private, no-store`, `Vary: Cookie`
@@ -42,8 +85,6 @@ one is listed under a **Breaking** heading.
42
85
  a private page, a paginated table fragment, a CSRF-protected mutation and an
43
86
  island with cleanup, covered by its own `smoke.mjs`.
44
87
  - An npm version badge in the `README`, linking to the package page.
45
- - English `README`, plus `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`, `SECURITY.md`,
46
- a `LICENSE` file, issue and pull request templates, and a CI workflow.
47
88
  - An English edition of the documentation under `docs/en/`, mirroring every
48
89
  chapter of the Turkish `docs/`.
49
90
  - The dev overlay now compares the installed version against the `latest` tag on
@@ -76,7 +117,28 @@ one is listed under a **Breaking** heading.
76
117
  either; a stored "you need to sign in" redirect used to follow the visitor
77
118
  even after signing in.
78
119
 
79
- ## [0.1.0]
120
+ ## [0.1.1] - 2026-08-30
121
+
122
+ ### Added
123
+
124
+ - English `README`, plus `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`, `SECURITY.md`,
125
+ a `LICENSE` file, issue and pull request templates, and a CI workflow.
126
+ - An English-first `examples/marketing` with a Turkish translation, serving the
127
+ package documentation under `/docs` and reading its version, dependencies and
128
+ bundle sizes from the installed package.
129
+
130
+ ### Changed
131
+
132
+ - The install instructions point at the npm package instead of the git
133
+ repository.
134
+
135
+ ### Fixed
136
+
137
+ - No more white flash between pages: the page background moved onto the root
138
+ element, so it applies before the body paints. Reduced-motion preferences now
139
+ switch off the decorative animations as well, not just page transitions.
140
+
141
+ ## [0.1.0] - 2026-08-30
80
142
 
81
143
  Initial release.
82
144
 
@@ -99,5 +161,7 @@ Initial release.
99
161
  - Documentation under `docs/` and three examples: `minimal`, `blog`,
100
162
  `marketing`.
101
163
 
102
- [Unreleased]: https://github.com/ayberkenis/jskelet/compare/v0.1.0...HEAD
164
+ [Unreleased]: https://github.com/ayberkenis/jskelet/compare/v0.1.2...HEAD
165
+ [0.1.2]: https://github.com/ayberkenis/jskelet/compare/v0.1.1...v0.1.2
166
+ [0.1.1]: https://github.com/ayberkenis/jskelet/compare/v0.1.0...v0.1.1
103
167
  [0.1.0]: https://github.com/ayberkenis/jskelet/releases/tag/v0.1.0
package/docs/06-cache.md CHANGED
@@ -4,8 +4,9 @@ Bu belge JSkelet'in ISR ikamesini bütün ayrıntılarıyla anlatır: HTML TTL
4
4
  önbelleği ve stale-while-revalidate davranışı, `revalidate`'in nereden geldiği,
5
5
  cache anahtarının nasıl kurulduğu, `X-JSkelet-Cache` başlığının değerleri,
6
6
  sıkıştırılmış gövdenin neden önbellekte durduğu, istek içi memoizasyon
7
- (`withRequestCache` / `cache()`), upstream hatalarının önbelleği nasıl
8
- etkilediği (`reportUpstreamFailure`) ve sunucu açılışındaki ısıtma turu.
7
+ (`withRequestCache` / `cache()`), veri önbelleği (`withDataCache`), upstream
8
+ hatalarının önbelleği nasıl etkilediği (`reportUpstreamFailure`) ve sunucu
9
+ açılışındaki ısıtma turu.
9
10
  Kararların arkasındaki ölçüm gerekçeleri [02-mimari.md](./02-mimari.md)'de,
10
11
  config alanlarının tam referansı [07-yapilandirma.md](./07-yapilandirma.md)'de.
11
12
 
@@ -17,12 +18,28 @@ route(controller, { revalidate })
17
18
  └─ withUpstreamTracking(...) ← eksik veri tespiti
18
19
  └─ withRequestCache(...) ← istek içi memoizasyon
19
20
  └─ produce() → controller + renderPage
21
+ └─ withDataCache(...) ← upstream veri önbelleği
20
22
  ```
21
23
 
22
24
  Sıra önemli: **istek içi cache en içte** olmalı ki aynı render'daki iki çağrı
23
25
  tek upstream isteğine düşsün; **upstream takibi HTML cache'in içinde** olmalı ki
24
26
  eksik veriyle üretilen çıktı önbelleğe yazılmasın.
25
27
 
28
+ İki önbelleğin iş bölümü:
29
+
30
+ | | HTML önbelleği | Veri önbelleği |
31
+ | --- | --- | --- |
32
+ | Ne tutar | Sayfanın tamamı (+ sıkıştırılmış gövdesi) | Upstream'den gelen JSON |
33
+ | Girdi boyutu | ~100-200 kB | ~1-20 kB |
34
+ | Girdi sınırı | 500 (`cache().maxEntries`) | 10.000 (`cache().data.maxEntries`) |
35
+ | Kime yarar | Trafiği olan sayfalar: render bile edilmez | Uzun kuyruk: render edilir ama API'ye gidilmez |
36
+
37
+ Bu ayrım pratikte şuna karşılık gelir: on binlerce yollu bir sitede sayfaların
38
+ tamamını HTML olarak sıcak tutmak mümkün değil — 500 girdiyi aşan ısıtma kendi
39
+ ısıttığını siler. Uzun kuyruk için hedef "HTML hazır olsun" değil, **"sayfayı
40
+ üretecek veri API'ye gitmeden bulunsun"** olmalı. O zaman hiç ısıtılmamış bir
41
+ sayfa da ilk ziyaretçide milisaniyeler içinde üretilir ve kota harcamaz.
42
+
26
43
  ## Public ve kişiye özel ayrımı
27
44
 
28
45
  Bu belgedeki her şey **herkese aynı gidebilen** HTML için geçerli. Cache
@@ -102,8 +119,8 @@ ile `/liste?sayfa=3` ayrı girdilerdir.
102
119
  Bunun pratik sonucu: query string'e bağlı olmayan bir sayfa, farklı kampanya
103
120
  parametreleriyle (`?utm_source=…`) çağrıldığında her kombinasyon için ayrı bir
104
121
  girdi üretir. Bu tür parametreleri ters proxy katmanında temizlemek ya da
105
- önbelleği kapatmak (`revalidate` vermemek) makul bir önlemdir; store en fazla
106
- 500 girdi tutar ve LRU ile en eskiyi düşürür.
122
+ önbelleği kapatmak (`revalidate` vermemek) makul bir önlemdir; store varsayılan
123
+ olarak en fazla 500 girdi tutar ve LRU ile en eskiyi düşürür.
107
124
 
108
125
  ## Stale-while-revalidate
109
126
 
@@ -134,8 +151,8 @@ veri en fazla `revalidate + bir tazeleme turu` kadar geride olabilir. Bu bedel
134
151
  kabul edilebilir, çünkü fiyat gibi canlı alanlar istemcide WebSocket'ten
135
152
  güncelleniyor.
136
153
 
137
- Store LRU'dur: erişilen girdi sona taşınır, `MAX_ENTRIES = 500` aşılınca en
138
- eski düşürülür.
154
+ Store LRU'dur: erişilen girdi sona taşınır, sınır (`cache().maxEntries`,
155
+ varsayılan 500) aşılınca en eski düşürülür.
139
156
 
140
157
  ## Ne önbelleğe yazılır
141
158
 
@@ -216,6 +233,82 @@ Ayrıntılar:
216
233
  - `withRequestCache(run)` dışa açıktır; `route()` dışında (ör. kendi yazdığınız
217
234
  bir Express handler'ında) aynı kapsamı kurmak için kullanılabilir.
218
235
 
236
+ ## İstekler arası veri önbelleği: `withDataCache`
237
+
238
+ `cache()` yalnızca **tek bir istek** boyunca yaşar. Uzun kuyruğu API kotasından
239
+ korumak için gereken şey istekler arasında yaşayan, TTL'li ve kendi kendini
240
+ tazeleyen bir veri katmanı:
241
+
242
+ ```js
243
+ // lib/api/articles.js
244
+ import { withDataCache, reportUpstreamFailure } from "jskelet";
245
+
246
+ export async function getArticle(slug) {
247
+ return withDataCache(`haber:${slug}`, 600, async () => {
248
+ const response = await fetch(`${process.env.API_ORIGIN}/articles/${slug}`);
249
+
250
+ if (!response.ok) {
251
+ reportUpstreamFailure({ status: response.status, path: `/articles/${slug}` });
252
+ return null;
253
+ }
254
+
255
+ return response.json();
256
+ });
257
+ }
258
+ ```
259
+
260
+ Aynı kalıbın sarmalayıcı biçimi — anahtar argümanlardan üretilir:
261
+
262
+ ```js
263
+ import { dataCache } from "jskelet";
264
+
265
+ export const getArticle = dataCache(
266
+ async (slug) => apiGet(`/articles/${slug}`),
267
+ { key: "haber", revalidate: 600 },
268
+ );
269
+ ```
270
+
271
+ Davranış:
272
+
273
+ | Durum | Sonuç |
274
+ | --- | --- |
275
+ | Taze girdi | Anında döner, `producer` çalışmaz |
276
+ | TTL geçmiş, bayat pencere sürüyor | Bayat değer **anında** döner, tazeleme arkada yürür |
277
+ | Girdi yok | `producer` beklenir |
278
+ | `producer` hata verdi, bayat girdi var | Bayat değer döner, uyarı: `[data-cache] producer failed, serving stale value: …` |
279
+ | `producer` hata verdi, girdi yok | Hata çağırana gider |
280
+
281
+ Ayrıntılar:
282
+
283
+ - **Aynı anahtarı eşzamanlı isteyen çağrılar tek upstream isteğine düşer.**
284
+ Isıtma turlarında kotayı en çok kurtaran davranış bu: 50 sayfa aynı endeks
285
+ verisini istiyorsa API bir kez çağrılır.
286
+ - **`null` ve `undefined` saklanmaz.** Uygulamaların HTTP istemcisi hatada
287
+ genellikle `null` döner; bunu saklamak geçici bir 429'u TTL boyunca "veri yok"
288
+ hâline dondurmak olurdu. Boş cevabı bilinçli olarak saklamak isteyen
289
+ `{ storeEmpty: true }` verir.
290
+ - **Bayat pencere HTML'dekinden uzun**: `staleFactor` varsayılanı 10, yani girdi
291
+ TTL'inin 11 katı boyunca acil durum yedeği olarak kalır. Bayat veri, eksik
292
+ sayfadan iyidir. Anahtar başına `{ staleFactor: 0 }` ile kapatılabilir.
293
+ - Anahtar tamamen uygulamanın: dil, sürüm, sayfa numarası gibi ayrımlar anahtara
294
+ yazılır (`haber:tr:v2:${slug}`).
295
+ - TTL `0` verildiğinde önbellek devre dışı kalır ve `producer` her çağrıda
296
+ çalışır — bir ayarı geçici olarak kapatmak için yeterli.
297
+
298
+ Yönetim yüzeyi:
299
+
300
+ | Fonksiyon | Ne yapar |
301
+ | --- | --- |
302
+ | `withDataCache(key, ttlSeconds, producer, options?)` | Ana giriş noktası |
303
+ | `dataCache(fn, { key, revalidate, … })` | Fonksiyon sarmalayıcısı |
304
+ | `clearDataCache(prefix?)` | Önek eşleşen girdileri (ya da tümünü) düşürür, silinen sayısını döner |
305
+ | `getDataCacheSize()` | Girdi sayısı |
306
+ | `getDataCacheEntries()` | Döküm: `{ key, stale, expiresIn }`. Değerin kendisi dönmez. |
307
+
308
+ `clearDataCache("haber:")`, "bu içerik güncellendi" webhook'unun karşılığıdır:
309
+ tek bir bölümün verisini düşürüp HTML'in bir sonraki tazelemesinde yeni içeriği
310
+ almasını sağlar.
311
+
219
312
  ## Degraded render: `reportUpstreamFailure`
220
313
 
221
314
  Render sırasında upstream düştüyse çıktı eksik veri içeriyor demektir. Böyle bir
@@ -260,6 +353,32 @@ denemekle düzelmez. Onlar yüzünden önbelleği kapatmak sayfayı her ziyarett
260
353
  baştan render etmek olur — içerik yine aynı eksik hâliyle döner, ziyaretçi
261
354
  sadece render süresini öder.
262
355
 
356
+ Eksik veriyle üretilen çıktı **paylaşılan önbelleklere de sunulmaz**: `degraded`
357
+ bir yanıt `public, s-maxage=…` değil `private, no-store` alır. Süreç içi
358
+ önbelleğe yazmama kararını CDN'de geri almak, aynı hatayı bir katman yukarıda
359
+ tekrarlamak olurdu. Teşhis başlığı (`X-JSkelet-Cache: MISS`) yine yazılır.
360
+
361
+ ### `notFound()` geçici hataya denk gelirse
362
+
363
+ Veri gelmediği için `notFound()` çağıran bir controller, upstream rate limit'e
364
+ girdiğinde tüm siteyi 404'e çevirebilir — ve bu 404'ler önbelleğe girdiği için
365
+ geçici bir kota sorunu TTL boyunca "bu sayfa yok" cevabına dönüşür. Arama motoru
366
+ için bu kalıcı bir kayıp.
367
+
368
+ Framework bu durumu ayırır: render sırasında **geçici** bir upstream hatası
369
+ bildirilmişse `notFound()` 404 olarak servis edilmez.
370
+
371
+ | Render sırasında | `notFound()` sonucu |
372
+ | --- | --- |
373
+ | Geçici hata var (`429`, `5xx`, ağ hatası) | `503`, `Retry-After: 30`, `no-store` — önbelleğe **girmez**, sonraki istek gerçek içeriği üretir |
374
+ | Kalıcı hata var (`404`, `403`…) ya da hata yok | Normal `404` |
375
+
376
+ Log satırı:
377
+ `[render] /haber/x returned notFound() while upstream is failing (429 /api/...), serving an uncached 503 instead`
378
+
379
+ Yani upstream'in kotası dolduğunda sayfa dinamik olarak, önbelleğe yazılmadan
380
+ üretilir; hiçbir şey "yok" olarak dondurulmaz.
381
+
263
382
  ## Önbelleği yönetmek
264
383
 
265
384
  `jskelet` şu fonksiyonları dışa açar:
@@ -335,24 +454,75 @@ Kurallar:
335
454
  - Yalnızca `/` ile başlayan string'ler alınır.
336
455
  - `prewarmSkip` öneklerinden biriyle başlayanlar atlanır. Varsayılan liste:
337
456
  `/api/`, `/_fragment/`, `/__jskelet/`. Oturuma bağlı sayfalar ısıtılmamalı.
338
- - Tekilleştirme **sırayı korur**: liste `PREWARM_MAX` ile budandığı için
339
- uygulamanın verdiği öncelik sırası anlamlıdır — en önemli sayfaları başa
340
- koyun.
457
+ - Tekilleştirme **sırayı korur**: `priority` verilmediğinde uygulamanın verdiği
458
+ sıra anlamlıdır — en önemli sayfaları başa koyun.
341
459
  - Bu hook tanımlı değilse ısıtma hiç kurulmaz; zamanlayıcı bile açılmaz.
342
460
 
343
461
  ### Tur mantığı
344
462
 
345
- 1. Liste toplanır, `PREWARM_MAX` (varsayılan 400) ile budanır.
346
- 2. `PREWARM_CONCURRENCY` işçi paralel olarak istek atar (prod'da 4, dev'de 2).
347
- Dev'de daha az paralellik: tarama, o an tarayıcıda açtığın sayfanın
348
- render'ıyla CPU için yarışmasın.
349
- 3. Başarısız yollar için **tek seri tekrar turu** yapılır (`concurrency: 1`).
350
- Hatalar çoğunlukla upstream rate limit'i (429): ilk tur yüzlerce sayfayı aynı
351
- anda çekerken API'yi zorluyor. Tekrar turu bu sayfaların önbelleğe girmesini
352
- sağlıyor; aksi hâlde ziyaretçi soğuk render'ı öder.
353
- 4. Özet loglanır:
463
+ 1. Liste toplanır. `max`'tan (varsayılan 400) uzunsa bir dilim seçilir:
464
+ `priority` eşleşenler **her turda** başa alınır, kalan yerler kuyruktan
465
+ doldurulur.
466
+ 2. `concurrency` işçi paralel olarak istek atar (prod'da 4, dev'de 2). Dev'de
467
+ daha az paralellik: tarama, o an tarayıcıda açtığın sayfanın render'ıyla CPU
468
+ için yarışmasın.
469
+ 3. `rps` verilmişse tur bu hızın üstüne çıkmaz — paralellik ne olursa olsun.
470
+ 4. Başarısız yollar için, `retryDelayMs` bekledikten sonra **tek seri tekrar
471
+ turu** yapılır (`concurrency: 1`). Bekleme bilinçli: rate limit pencereleri
472
+ saniye mertebesinde, hemen tekrar denemek aynı 429'u almak demek.
473
+ 5. Özet loglanır:
354
474
  `[prewarm] warmed 128/130 pages, 2 failed, 5 recovered on the retry pass (12.4s)`
355
475
 
476
+ ### Isıtma sırası: `priority`
477
+
478
+ ```js
479
+ // jskelet.config.mjs
480
+ cache: () => ({
481
+ prewarm: {
482
+ priority: [
483
+ "/",
484
+ "/piyasalar/:path*",
485
+ /-yorumlar$/,
486
+ ],
487
+ },
488
+ }),
489
+ ```
490
+
491
+ Desen sözdizimi (`/haber/:slug`) ve doğrudan `RegExp` birlikte kullanılabilir;
492
+ ikincisi "sonu `-yorumlar` ile bitenler" gibi desen sözdiziminin karşılamadığı
493
+ kurallar için. Önce yazılan önce ısınır, hiçbirine uymayan yollar kuyruğa gider
494
+ ve kendi aralarındaki sırayı korur.
495
+
496
+ ### Damla damla ısıtma: `rotate` + `rps` + `intervalSeconds`
497
+
498
+ 10.000 yolluk bir sitede tek turda her şeyi ısıtmak ne mümkün (HTML önbelleği
499
+ 500 girdi tutar) ne de doğru (API kotası dolar). Doğru davranış listeyi zamana
500
+ yaymak:
501
+
502
+ ```js
503
+ prewarm: {
504
+ max: 300, // her turda 300 sayfa
505
+ rps: 4, // saniyede en fazla 4 istek
506
+ intervalSeconds: 300, // 5 dakikada bir tur
507
+ rotate: true, // kuyruk kaldığı yerden devam eder
508
+ priority: ["/", "/piyasalar/:path*"],
509
+ }
510
+ ```
511
+
512
+ Bu kurulumda öncelikli sayfalar her turda tazelenir, geri kalan kuyruk turlar
513
+ boyunca baştan sona dolaşılır ve upstream saniyede dört isteğin üstünü hiç
514
+ görmez. Veri önbelleği ile birlikte kullanıldığında ikinci turdan sonra ısıtma
515
+ API'ye neredeyse hiç gitmez: veri katmanından okur.
516
+
517
+ Rotasyon açıkken sınırın dışında kalan yollar kaybolmuyor, bir sonraki tura
518
+ kalıyor; log bunu ayırt eder:
519
+ `… , 700 deferred to the next pass`. `rotate: false` ile klasik davranışa
520
+ dönülür — her tur listenin aynı ilk dilimini ısıtır ve gerisi hiç ısınmaz
521
+ (`… , 700 over the limit`).
522
+
523
+ Bir tur `intervalSeconds`'tan uzun sürerse yeni tur başlatılmaz; üst üste binen
524
+ turlar upstream'e iki kat yük bindirirdi.
525
+
356
526
  İstekler `user-agent: jskelet-prewarm` (`brand.prewarmUserAgent`) ve
357
527
  `accept-encoding: br, gzip` başlıklarıyla gider; ikincisi sıkıştırılmış gövdenin
358
528
  de önbelleğe girmesi için.
@@ -385,10 +555,14 @@ tek seferlik deneyler config'i düzenlemeden yapılabilsin.
385
555
  | Ayar | Env | `cache().prewarm` | Varsayılan |
386
556
  | --- | --- | --- | --- |
387
557
  | Açık/kapalı | `PREWARM=0` kapatır, `PREWARM=1` config'i ezip açar | `enabled` | `true` |
388
- | En fazla yol | `PREWARM_MAX` | `max` | `400` |
558
+ | Tur başına en fazla yol | `PREWARM_MAX` | `max` | `400` |
389
559
  | Paralellik | `PREWARM_CONCURRENCY` | `concurrency` | prod 4, dev 2 |
560
+ | Saniyedeki istek | `PREWARM_RPS` | `rps` | `0` (sınırsız) |
390
561
  | Başlangıç gecikmesi (ms) | `PREWARM_DELAY_MS` | `delayMs` | prod 500, dev 3000 |
562
+ | Tekrar turu gecikmesi (ms) | `PREWARM_RETRY_DELAY_MS` | `retryDelayMs` | `2000` |
391
563
  | Periyot (saniye) | `PREWARM_INTERVAL_SECONDS` | `intervalSeconds` | `0` (kapalı) |
564
+ | Kuyruk rotasyonu | — | `rotate` | `true` |
565
+ | Isıtma sırası | — | `priority` | `[]` |
392
566
 
393
567
  Sayısal ayarlar yalnızca **pozitif ve sonlu** değer kabul eder; geçersiz bir
394
568
  değer sessizce bir sonraki katmana düşer.
@@ -432,6 +606,14 @@ turunun gerçekten `MISS` → önbellek doldurup doldurmadığını buradan gör
432
606
  parametreleri girdi çoğaltıyor olabilir.
433
607
  - **Isıtma hiç çalışmıyor.** `hooks.prewarmPaths` tanımlı değil, `PREWARM=0`
434
608
  ayarlı ya da `cache().prewarm.enabled === false`.
609
+ - **Isıtma turu API'yi 429'a sokuyor.** `rps` verilmemiş. `concurrency`
610
+ düşürmek yeterli değil; kotayı koruyan ayar toplam hız. Kalıcı çözüm veri
611
+ önbelleği: ikinci turdan sonra ısıtma upstream'e gitmez.
612
+ - **Isıtma listesi `max`'tan uzun ve sonu hiç ısınmıyor.** `rotate: false`
613
+ olabilir; logdaki `over the limit` ifadesi bunu gösterir.
614
+ - **Bir bölümün tamamı 404 dönüyor.** Upstream düşmüş olabilir. Artık bu durumda
615
+ 404 değil önbelleğe girmeyen 503 dönüyor; logda `returned notFound() while
616
+ upstream is failing` satırını arayın.
435
617
 
436
618
  ## Sırada ne var
437
619
 
@@ -115,7 +115,17 @@ export default {
115
115
  async cache() {
116
116
  return {
117
117
  html: { "/": 60, "/haber/:slug": 300 },
118
- prewarm: { enabled: true, max: 400, concurrency: 4, intervalSeconds: 0 },
118
+ maxEntries: 500,
119
+ data: { maxEntries: 10000, staleFactor: 10 },
120
+ prewarm: {
121
+ enabled: true,
122
+ max: 400,
123
+ concurrency: 4,
124
+ rps: 0,
125
+ intervalSeconds: 0,
126
+ rotate: true,
127
+ priority: ["/", "/haber/:slug"],
128
+ },
119
129
  };
120
130
  },
121
131
 
@@ -549,8 +559,10 @@ Ayrıntı: [03-routing.md](./03-routing.md).
549
559
 
550
560
  ## `cache()`
551
561
 
552
- **Tip:** `() => { html?: Record<string, number>, prewarm?: object }` —
553
- **Varsayılan:** `{ html: {}, prewarm: { enabled: true, max: 400, intervalSeconds: 0 } }`
562
+ **Tip:**
563
+ `() => { html?: Record<string, number>, maxEntries?: number, data?: object, prewarm?: object }` —
564
+ **Varsayılan:**
565
+ `{ html: {}, maxEntries: 500, data: { maxEntries: 10000, staleFactor: 10 }, prewarm: { enabled: true, max: 400, intervalSeconds: 0 } }`
554
566
 
555
567
  ### `cache().html`
556
568
 
@@ -570,18 +582,55 @@ Tek istisna `route(fn, { private: true })`: bu route'ta desen eşleşse bile yok
570
582
  sayılır. Kilidin tek yönlü olması bilinçli — ters yönde bir hata, bir
571
583
  kullanıcının HTML'inin bir başkasına servis edilmesi anlamına geliyor.
572
584
 
585
+ ### `cache().maxEntries`
586
+
587
+ **Tip:** `number` — **Varsayılan:** `500`
588
+
589
+ HTML önbelleğinin girdi sınırı. Girdi başına yüz kilobayt düştüğü için bu sayıyı
590
+ yükseltmek belleği hızla tüketir; on binlerce yollu bir siteyi buradan çözmeye
591
+ çalışmak yanlış katman, doğru yer `cache().data`.
592
+
593
+ ### `cache().data`
594
+
595
+ Upstream veri önbelleği (`withDataCache`). Ayrıntı: [06-cache.md](./06-cache.md).
596
+
597
+ | Alan | Tip | Varsayılan | Anlamı |
598
+ | --- | --- | --- | --- |
599
+ | `maxEntries` | `number` | `10000` | LRU girdi sınırı. JSON, HTML'e göre onlarca kat küçük olduğu için sınır yüksek. |
600
+ | `staleFactor` | `number` | `10` | TTL dolduktan sonra girdinin kaç TTL boyunca daha kullanılabileceği. `0` → bayat servis yok. |
601
+
573
602
  ### `cache().prewarm`
574
603
 
575
604
  | Alan | Tip | Varsayılan | Anlamı |
576
605
  | --- | --- | --- | --- |
577
606
  | `enabled` | `boolean` | `true` | `false` ise ısıtma yapılmaz (`PREWARM=1` ile ezilebilir) |
578
- | `max` | `number` | `400` | En fazla kaç yol ısıtılır |
607
+ | `max` | `number` | `400` | Bir turda en fazla kaç yol ısıtılır |
579
608
  | `concurrency` | `number` | prod 4, dev 2 | Paralel işçi sayısı |
609
+ | `rps` | `number` | `0` | Saniyedeki en fazla ısıtma isteği; `0` sınırsız. Upstream kotasını koruyan ayar bu. |
580
610
  | `delayMs` | `number` | prod 500, dev 3000 | Açılıştan sonra ilk turun gecikmesi |
611
+ | `retryDelayMs` | `number` | `2000` | Tekrar turundan önce beklenen süre |
581
612
  | `intervalSeconds` | `number` | `0` | 0'dan büyükse tur periyodik tekrarlanır |
613
+ | `rotate` | `boolean` | `true` | Liste `max`'tan uzunsa periyodik turlar kaldığı yerden devam eder |
614
+ | `priority` | `(string \| RegExp)[]` | `[]` | Isıtma sırası; eşleşen yollar her turda başa alınır |
582
615
 
583
- Her biri aynı adı taşıyan ortam değişkeniyle ezilebilir; env önceliklidir.
584
- Ayrıntı: [06-cache.md](./06-cache.md).
616
+ `priority` iki biçim kabul eder: config'in her yerinde geçerli olan desen
617
+ sözdizimi ve doğrudan `RegExp`. Önce yazılan önce ısınır.
618
+
619
+ ```js
620
+ prewarm: {
621
+ max: 500,
622
+ rps: 4,
623
+ intervalSeconds: 300,
624
+ priority: [
625
+ "/", // ana sayfa
626
+ "/piyasalar/:path*", // tüm piyasa bölümü
627
+ /-yorumlar$/, // desen sözdiziminin karşılamadığı kural
628
+ ],
629
+ }
630
+ ```
631
+
632
+ Sayısal alanların her biri aynı adı taşıyan ortam değişkeniyle ezilebilir; env
633
+ önceliklidir. Ayrıntı: [06-cache.md](./06-cache.md).
585
634
 
586
635
  ## `hooks`
587
636
 
@@ -676,7 +725,9 @@ basılmaz.
676
725
  | `PREWARM` | `startPrewarm` | — | `0` ısıtmayı kapatır; `1` config'teki `enabled: false`'u ezip açar |
677
726
  | `PREWARM_MAX` | `prewarm` | `400` | En fazla kaç yol ısıtılır |
678
727
  | `PREWARM_CONCURRENCY` | `prewarm` | prod 4, dev 2 | Paralel işçi sayısı |
728
+ | `PREWARM_RPS` | `prewarm` | `0` | Saniyedeki en fazla ısıtma isteği; `0` sınırsız |
679
729
  | `PREWARM_DELAY_MS` | `startPrewarm` | prod 500, dev 3000 | İlk turun gecikmesi |
730
+ | `PREWARM_RETRY_DELAY_MS` | `prewarm` | `2000` | Tekrar turundan önceki bekleme |
680
731
  | `PREWARM_INTERVAL_SECONDS` | `startPrewarm` | `0` | 0'dan büyükse periyodik tur |
681
732
  | `JSKELET_VERBOSE` | `jskelet dev` | — | `1` ise restart'ta değişen dosyaların tamamı listelenir |
682
733
  | `JSKELET_COLOR` | `jskelet/log` | — | `1` ise renk zorlanır. Alt süreçler boruya yazdığı için renk algılaması kapanır; `jskelet dev` bunu kendisi ayarlar. |