jskelet 0.1.2 → 0.1.4

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,68 @@ 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
+ - Transient upstream failures are now detected without any application code:
35
+ `globalThis.fetch` is wrapped during startup and `429`, `5xx` and network
36
+ errors raised inside a render are reported on their own, so rate limits stop
37
+ turning existing pages into 404s even when the data layer never calls
38
+ `reportUpstreamFailure()`. Requests outside a render and requests to the
39
+ server itself are ignored, deterministic answers such as `404` are not
40
+ reported, and the wrapper can be turned off with `cache().trackUpstream:
41
+ false`.
42
+ - `cache().transientRetry` (`{ attempts: 1, delayMs: 300 }` by default) retries a
43
+ page that called `notFound()` while upstream was failing. Each attempt runs in
44
+ a fresh upstream and per-request cache scope, so a page whose data arrives on
45
+ the second try is served and cached as usual instead of degrading to an error.
46
+ - The dev report now includes the data cache entry count under `cache.data`.
47
+
48
+ ### Changed
49
+
50
+ - `notFound()` is no longer served as a 404 when a transient upstream failure
51
+ (`429`, `5xx`, network error) happened during the same render. The page is
52
+ retried first and, if upstream is still failing, responds with an uncached
53
+ `503` and `Retry-After`. A temporary rate limit is no longer frozen into "this
54
+ page does not exist" for the whole TTL. A retry that gets a clean answer saying
55
+ the page is gone still returns a normal 404.
56
+ - Responses produced with missing data are no longer offered to shared caches:
57
+ a `degraded` render is sent with `private, no-store` instead of
58
+ `public, s-maxage=…`. The `X-JSkelet-Cache` diagnostic header is still written.
59
+ - The prewarm summary distinguishes paths left for the next pass
60
+ (`700 deferred to the next pass`) from paths dropped entirely
61
+ (`700 over the limit`).
62
+ - The changelog page of the marketing example is generated from the project's
63
+ `CHANGELOG.md` instead of a hand-written list, and shows the version published
64
+ on npm next to the installed one.
65
+ - The marketing example reads its markdown (documentation and changelog) from
66
+ the repository over GitHub's raw endpoint, falling back to the installed
67
+ package when the network is unavailable, so a deployment that ships without
68
+ `node_modules` can still serve the docs. In development the local file wins
69
+ and nothing is cached. The branch is overridable with `DOCS_REF`.
70
+
71
+ ## [0.1.2] - 2026-08-30
72
+
73
+ ### Added
74
+
13
75
  - `route(fn, { private: true })` for pages that depend on the visitor. The HTML
14
76
  cache is bypassed, `cache.html` patterns can no longer turn caching on for
15
77
  that route, and the response is sent with `private, no-store`, `Vary: Cookie`
@@ -42,8 +104,6 @@ one is listed under a **Breaking** heading.
42
104
  a private page, a paginated table fragment, a CSRF-protected mutation and an
43
105
  island with cleanup, covered by its own `smoke.mjs`.
44
106
  - 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
107
  - An English edition of the documentation under `docs/en/`, mirroring every
48
108
  chapter of the Turkish `docs/`.
49
109
  - The dev overlay now compares the installed version against the `latest` tag on
@@ -76,7 +136,28 @@ one is listed under a **Breaking** heading.
76
136
  either; a stored "you need to sign in" redirect used to follow the visitor
77
137
  even after signing in.
78
138
 
79
- ## [0.1.0]
139
+ ## [0.1.1] - 2026-08-30
140
+
141
+ ### Added
142
+
143
+ - English `README`, plus `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`, `SECURITY.md`,
144
+ a `LICENSE` file, issue and pull request templates, and a CI workflow.
145
+ - An English-first `examples/marketing` with a Turkish translation, serving the
146
+ package documentation under `/docs` and reading its version, dependencies and
147
+ bundle sizes from the installed package.
148
+
149
+ ### Changed
150
+
151
+ - The install instructions point at the npm package instead of the git
152
+ repository.
153
+
154
+ ### Fixed
155
+
156
+ - No more white flash between pages: the page background moved onto the root
157
+ element, so it applies before the body paints. Reduced-motion preferences now
158
+ switch off the decorative animations as well, not just page transitions.
159
+
160
+ ## [0.1.0] - 2026-08-30
80
161
 
81
162
  Initial release.
82
163
 
@@ -99,5 +180,7 @@ Initial release.
99
180
  - Documentation under `docs/` and three examples: `minimal`, `blog`,
100
181
  `marketing`.
101
182
 
102
- [Unreleased]: https://github.com/ayberkenis/jskelet/compare/v0.1.0...HEAD
183
+ [Unreleased]: https://github.com/ayberkenis/jskelet/compare/v0.1.2...HEAD
184
+ [0.1.2]: https://github.com/ayberkenis/jskelet/compare/v0.1.1...v0.1.2
185
+ [0.1.1]: https://github.com/ayberkenis/jskelet/compare/v0.1.0...v0.1.1
103
186
  [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 (otomatik izleme ve
9
+ `reportUpstreamFailure`) ve sunucu 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,15 +233,116 @@ 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
222
315
  HTML'i tüm TTL boyunca servis etmek yerine önbelleğe **hiç yazmamak** doğru
223
316
  davranış: sonraki istek yeniden dener.
224
317
 
318
+ Bu bilgi iki yoldan gelir.
319
+
320
+ ### Otomatik izleme (varsayılan)
321
+
322
+ `createApp()` açılışta `globalThis.fetch`i sarar ve render sırasında yapılan
323
+ çağrılardaki **geçici** hataları (`429`, `5xx`, ağ hatası) kendiliğinden
324
+ bildirir. Uygulama tarafında hiçbir satır gerekmez; `fetch` ile konuşan bir API
325
+ istemcisi varsa rate limit koruması hazırdır.
326
+
327
+ Ayrıntılar:
328
+
329
+ - Yalnızca bir render bağlamı içindeki çağrılar sayılır. Script'ten, cron'dan
330
+ ya da istek dışı bir yerden yapılan `fetch` dokunulmaz kalır.
331
+ - Kendi sunucumuza yapılan istekler (`localhost`, `127.0.0.1`) atlanır: ısıtma
332
+ turu ve sağlık kontrolü upstream değildir.
333
+ - `404`/`403` gibi deterministik cevaplar **otomatik olarak bildirilmez**. Çoğu
334
+ API'de `404` "böyle bir kayıt yok" demektir; onu eksik veri saymak her yok
335
+ sayfasında yanlış uyarı üretirdi.
336
+ - Kapatmak için `cache().trackUpstream: false`. `fetch`i kendisi saran bir
337
+ uygulama (ölçüm, retry, circuit breaker) bunu tercih edebilir.
338
+
339
+ ### Elle bildirim
340
+
341
+ `fetch` kullanmayan bir istemci (veritabanı sürücüsü, gRPC, satıcı SDK'sı) ya da
342
+ kalıcı hataları da işaretlemek isteyen bir katman için sözleşme aynı kaldı.
225
343
  Bağımlılık yönü bilinçli olarak tersine çevrilmiştir: framework veri katmanını
226
344
  tanımaz, veri katmanı framework'e haber verir. Hiç çağıran olmazsa maliyet boş
227
- bir dizidir.
345
+ bir dizidir. Aynı hata her iki yoldan bildirilirse tekilleştirilir.
228
346
 
229
347
  ```js
230
348
  // lib/api/client.js
@@ -260,6 +378,58 @@ denemekle düzelmez. Onlar yüzünden önbelleği kapatmak sayfayı her ziyarett
260
378
  baştan render etmek olur — içerik yine aynı eksik hâliyle döner, ziyaretçi
261
379
  sadece render süresini öder.
262
380
 
381
+ Eksik veriyle üretilen çıktı **paylaşılan önbelleklere de sunulmaz**: `degraded`
382
+ bir yanıt `public, s-maxage=…` değil `private, no-store` alır. Süreç içi
383
+ önbelleğe yazmama kararını CDN'de geri almak, aynı hatayı bir katman yukarıda
384
+ tekrarlamak olurdu. Teşhis başlığı (`X-JSkelet-Cache: MISS`) yine yazılır.
385
+
386
+ ### `notFound()` geçici hataya denk gelirse
387
+
388
+ Veri gelmediği için `notFound()` çağıran bir controller, upstream rate limit'e
389
+ girdiğinde tüm siteyi 404'e çevirebilir — ve bu 404'ler önbelleğe girdiği için
390
+ geçici bir kota sorunu TTL boyunca "bu sayfa yok" cevabına dönüşür. Arama motoru
391
+ için bu kalıcı bir kayıp.
392
+
393
+ Framework bu durumu ayırır: render sırasında **geçici** bir upstream hatası
394
+ varsa `notFound()` 404 olarak servis edilmez. Sırayla:
395
+
396
+ 1. Sayfa kısa bir beklemeden sonra **yeniden denenir** (varsayılan bir kez,
397
+ 300 ms sonra). Deneme kendi upstream ve istek içi cache bağlamında koşar;
398
+ ilk turun hatası da memoize edilmiş boş cevabı da ikinci turu etkilemez.
399
+ 2. İkinci tur sayfayı üretebilirse ziyaretçi **gerçek içeriği** görür ve çıktı
400
+ normal şekilde önbelleğe girer. Isıtma günlükleri bunun sık olduğunu
401
+ gösteriyor: aynı yol saniyeler sonra 200 dönüyor.
402
+ 3. Denemeler tükendiyse yanıt `503` olur — önbelleğe girmez, `Retry-After`
403
+ taşır, sonraki istek yine gerçek içeriği üretebilir.
404
+
405
+ | Render sırasında | `notFound()` sonucu |
406
+ | --- | --- |
407
+ | Geçici hata var (`429`, `5xx`, ağ hatası) | Tekrar dene → başarılıysa sayfa; hâlâ olmuyorsa `503`, `Retry-After: 30`, `no-store` |
408
+ | Tekrar denemede upstream sağlam cevap verip "yok" dedi | Normal `404` |
409
+ | Kalıcı hata var (`404`, `403`…) ya da hata yok | Normal `404`, tekrar denenmez |
410
+
411
+ Log satırları:
412
+
413
+ ```
414
+ [render] /haber/x returned notFound() while upstream is failing (429 /api/...), retrying (1/1)
415
+ [render] /haber/x could not be produced, upstream is still failing (429 /api/...), serving an uncached 503 instead of a 404
416
+ ```
417
+
418
+ Yani **var olan bir sayfa hiçbir koşulda 404'e dönüşmez**: ya gerçek içerik
419
+ gelir, ya önbelleğe girmeyen bir 503. Hiçbir şey "yok" olarak dondurulmaz.
420
+
421
+ Tekrar denemenin maliyeti upstream'e binen ikinci bir istek turudur; bu yüzden
422
+ varsayılan tek deneme. Ayar `cache().transientRetry`:
423
+
424
+ ```js
425
+ cache: {
426
+ transientRetry: { attempts: 2, delayMs: 500 },
427
+ }
428
+ ```
429
+
430
+ `transientRetry: false` (ya da `attempts: 0`) tekrarı kapatır ve doğrudan 503'e
431
+ düşer.
432
+
263
433
  ## Önbelleği yönetmek
264
434
 
265
435
  `jskelet` şu fonksiyonları dışa açar:
@@ -335,24 +505,75 @@ Kurallar:
335
505
  - Yalnızca `/` ile başlayan string'ler alınır.
336
506
  - `prewarmSkip` öneklerinden biriyle başlayanlar atlanır. Varsayılan liste:
337
507
  `/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.
508
+ - Tekilleştirme **sırayı korur**: `priority` verilmediğinde uygulamanın verdiği
509
+ sıra anlamlıdır — en önemli sayfaları başa koyun.
341
510
  - Bu hook tanımlı değilse ısıtma hiç kurulmaz; zamanlayıcı bile açılmaz.
342
511
 
343
512
  ### Tur mantığı
344
513
 
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:
514
+ 1. Liste toplanır. `max`'tan (varsayılan 400) uzunsa bir dilim seçilir:
515
+ `priority` eşleşenler **her turda** başa alınır, kalan yerler kuyruktan
516
+ doldurulur.
517
+ 2. `concurrency` işçi paralel olarak istek atar (prod'da 4, dev'de 2). Dev'de
518
+ daha az paralellik: tarama, o an tarayıcıda açtığın sayfanın render'ıyla CPU
519
+ için yarışmasın.
520
+ 3. `rps` verilmişse tur bu hızın üstüne çıkmaz — paralellik ne olursa olsun.
521
+ 4. Başarısız yollar için, `retryDelayMs` bekledikten sonra **tek seri tekrar
522
+ turu** yapılır (`concurrency: 1`). Bekleme bilinçli: rate limit pencereleri
523
+ saniye mertebesinde, hemen tekrar denemek aynı 429'u almak demek.
524
+ 5. Özet loglanır:
354
525
  `[prewarm] warmed 128/130 pages, 2 failed, 5 recovered on the retry pass (12.4s)`
355
526
 
527
+ ### Isıtma sırası: `priority`
528
+
529
+ ```js
530
+ // jskelet.config.mjs
531
+ cache: () => ({
532
+ prewarm: {
533
+ priority: [
534
+ "/",
535
+ "/piyasalar/:path*",
536
+ /-yorumlar$/,
537
+ ],
538
+ },
539
+ }),
540
+ ```
541
+
542
+ Desen sözdizimi (`/haber/:slug`) ve doğrudan `RegExp` birlikte kullanılabilir;
543
+ ikincisi "sonu `-yorumlar` ile bitenler" gibi desen sözdiziminin karşılamadığı
544
+ kurallar için. Önce yazılan önce ısınır, hiçbirine uymayan yollar kuyruğa gider
545
+ ve kendi aralarındaki sırayı korur.
546
+
547
+ ### Damla damla ısıtma: `rotate` + `rps` + `intervalSeconds`
548
+
549
+ 10.000 yolluk bir sitede tek turda her şeyi ısıtmak ne mümkün (HTML önbelleği
550
+ 500 girdi tutar) ne de doğru (API kotası dolar). Doğru davranış listeyi zamana
551
+ yaymak:
552
+
553
+ ```js
554
+ prewarm: {
555
+ max: 300, // her turda 300 sayfa
556
+ rps: 4, // saniyede en fazla 4 istek
557
+ intervalSeconds: 300, // 5 dakikada bir tur
558
+ rotate: true, // kuyruk kaldığı yerden devam eder
559
+ priority: ["/", "/piyasalar/:path*"],
560
+ }
561
+ ```
562
+
563
+ Bu kurulumda öncelikli sayfalar her turda tazelenir, geri kalan kuyruk turlar
564
+ boyunca baştan sona dolaşılır ve upstream saniyede dört isteğin üstünü hiç
565
+ görmez. Veri önbelleği ile birlikte kullanıldığında ikinci turdan sonra ısıtma
566
+ API'ye neredeyse hiç gitmez: veri katmanından okur.
567
+
568
+ Rotasyon açıkken sınırın dışında kalan yollar kaybolmuyor, bir sonraki tura
569
+ kalıyor; log bunu ayırt eder:
570
+ `… , 700 deferred to the next pass`. `rotate: false` ile klasik davranışa
571
+ dönülür — her tur listenin aynı ilk dilimini ısıtır ve gerisi hiç ısınmaz
572
+ (`… , 700 over the limit`).
573
+
574
+ Bir tur `intervalSeconds`'tan uzun sürerse yeni tur başlatılmaz; üst üste binen
575
+ turlar upstream'e iki kat yük bindirirdi.
576
+
356
577
  İstekler `user-agent: jskelet-prewarm` (`brand.prewarmUserAgent`) ve
357
578
  `accept-encoding: br, gzip` başlıklarıyla gider; ikincisi sıkıştırılmış gövdenin
358
579
  de önbelleğe girmesi için.
@@ -385,10 +606,14 @@ tek seferlik deneyler config'i düzenlemeden yapılabilsin.
385
606
  | Ayar | Env | `cache().prewarm` | Varsayılan |
386
607
  | --- | --- | --- | --- |
387
608
  | Açık/kapalı | `PREWARM=0` kapatır, `PREWARM=1` config'i ezip açar | `enabled` | `true` |
388
- | En fazla yol | `PREWARM_MAX` | `max` | `400` |
609
+ | Tur başına en fazla yol | `PREWARM_MAX` | `max` | `400` |
389
610
  | Paralellik | `PREWARM_CONCURRENCY` | `concurrency` | prod 4, dev 2 |
611
+ | Saniyedeki istek | `PREWARM_RPS` | `rps` | `0` (sınırsız) |
390
612
  | Başlangıç gecikmesi (ms) | `PREWARM_DELAY_MS` | `delayMs` | prod 500, dev 3000 |
613
+ | Tekrar turu gecikmesi (ms) | `PREWARM_RETRY_DELAY_MS` | `retryDelayMs` | `2000` |
391
614
  | Periyot (saniye) | `PREWARM_INTERVAL_SECONDS` | `intervalSeconds` | `0` (kapalı) |
615
+ | Kuyruk rotasyonu | — | `rotate` | `true` |
616
+ | Isıtma sırası | — | `priority` | `[]` |
392
617
 
393
618
  Sayısal ayarlar yalnızca **pozitif ve sonlu** değer kabul eder; geçersiz bir
394
619
  değer sessizce bir sonraki katmana düşer.
@@ -432,6 +657,16 @@ turunun gerçekten `MISS` → önbellek doldurup doldurmadığını buradan gör
432
657
  parametreleri girdi çoğaltıyor olabilir.
433
658
  - **Isıtma hiç çalışmıyor.** `hooks.prewarmPaths` tanımlı değil, `PREWARM=0`
434
659
  ayarlı ya da `cache().prewarm.enabled === false`.
660
+ - **Isıtma turu API'yi 429'a sokuyor.** `rps` verilmemiş. `concurrency`
661
+ düşürmek yeterli değil; kotayı koruyan ayar toplam hız. Kalıcı çözüm veri
662
+ önbelleği: ikinci turdan sonra ısıtma upstream'e gitmez.
663
+ - **Isıtma listesi `max`'tan uzun ve sonu hiç ısınmıyor.** `rotate: false`
664
+ olabilir; logdaki `over the limit` ifadesi bunu gösterir.
665
+ - **Bir bölümün tamamı 404 dönüyor.** Upstream düşmüş olabilir. Artık bu durumda
666
+ sayfa bir kez daha denenir, olmazsa 404 değil önbelleğe girmeyen 503 döner;
667
+ logda `returned notFound() while upstream is failing` satırını arayın. Hâlâ
668
+ 404 görüyorsanız hata `fetch` dışı bir istemciden geliyor olabilir
669
+ (`reportUpstreamFailure()` gerekir) ya da `cache().trackUpstream` kapatılmış.
435
670
 
436
671
  ## Sırada ne var
437
672
 
@@ -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, trackUpstream?: boolean, transientRetry?: object | false, prewarm?: object }` —
564
+ **Varsayılan:**
565
+ `{ html: {}, maxEntries: 500, data: { maxEntries: 10000, staleFactor: 10 }, trackUpstream: true, transientRetry: { attempts: 1, delayMs: 300 }, prewarm: { enabled: true, max: 400, intervalSeconds: 0 } }`
554
566
 
555
567
  ### `cache().html`
556
568
 
@@ -570,18 +582,73 @@ 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
+
602
+ ### `cache().trackUpstream`
603
+
604
+ **Tip:** `boolean` — **Varsayılan:** `true`
605
+
606
+ Açıkken `globalThis.fetch` sarılır ve render sırasındaki geçici upstream
607
+ hataları (`429`, `5xx`, ağ) kendiliğinden bildirilir; `reportUpstreamFailure()`
608
+ çağırmak gerekmez. `fetch`i kendisi saran bir uygulama bunu kapatabilir.
609
+
610
+ ### `cache().transientRetry`
611
+
612
+ **Tip:** `{ attempts?: number, delayMs?: number } | false` —
613
+ **Varsayılan:** `{ attempts: 1, delayMs: 300 }`
614
+
615
+ Geçici bir upstream hatası yüzünden `notFound()` çağrılan sayfa kaç kez daha
616
+ denenir. Amaç var olan bir sayfanın 404'e dönüşmemesi; denemeler tükenirse yanıt
617
+ önbelleğe girmeyen bir 503 olur. `false` ya da `attempts: 0` tekrarı kapatır.
618
+ Ayrıntı: [06-cache.md](./06-cache.md).
619
+
573
620
  ### `cache().prewarm`
574
621
 
575
622
  | Alan | Tip | Varsayılan | Anlamı |
576
623
  | --- | --- | --- | --- |
577
624
  | `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 |
625
+ | `max` | `number` | `400` | Bir turda en fazla kaç yol ısıtılır |
579
626
  | `concurrency` | `number` | prod 4, dev 2 | Paralel işçi sayısı |
627
+ | `rps` | `number` | `0` | Saniyedeki en fazla ısıtma isteği; `0` sınırsız. Upstream kotasını koruyan ayar bu. |
580
628
  | `delayMs` | `number` | prod 500, dev 3000 | Açılıştan sonra ilk turun gecikmesi |
629
+ | `retryDelayMs` | `number` | `2000` | Tekrar turundan önce beklenen süre |
581
630
  | `intervalSeconds` | `number` | `0` | 0'dan büyükse tur periyodik tekrarlanır |
631
+ | `rotate` | `boolean` | `true` | Liste `max`'tan uzunsa periyodik turlar kaldığı yerden devam eder |
632
+ | `priority` | `(string \| RegExp)[]` | `[]` | Isıtma sırası; eşleşen yollar her turda başa alınır |
582
633
 
583
- Her biri aynı adı taşıyan ortam değişkeniyle ezilebilir; env önceliklidir.
584
- Ayrıntı: [06-cache.md](./06-cache.md).
634
+ `priority` iki biçim kabul eder: config'in her yerinde geçerli olan desen
635
+ sözdizimi ve doğrudan `RegExp`. Önce yazılan önce ısınır.
636
+
637
+ ```js
638
+ prewarm: {
639
+ max: 500,
640
+ rps: 4,
641
+ intervalSeconds: 300,
642
+ priority: [
643
+ "/", // ana sayfa
644
+ "/piyasalar/:path*", // tüm piyasa bölümü
645
+ /-yorumlar$/, // desen sözdiziminin karşılamadığı kural
646
+ ],
647
+ }
648
+ ```
649
+
650
+ Sayısal alanların her biri aynı adı taşıyan ortam değişkeniyle ezilebilir; env
651
+ önceliklidir. Ayrıntı: [06-cache.md](./06-cache.md).
585
652
 
586
653
  ## `hooks`
587
654
 
@@ -676,7 +743,9 @@ basılmaz.
676
743
  | `PREWARM` | `startPrewarm` | — | `0` ısıtmayı kapatır; `1` config'teki `enabled: false`'u ezip açar |
677
744
  | `PREWARM_MAX` | `prewarm` | `400` | En fazla kaç yol ısıtılır |
678
745
  | `PREWARM_CONCURRENCY` | `prewarm` | prod 4, dev 2 | Paralel işçi sayısı |
746
+ | `PREWARM_RPS` | `prewarm` | `0` | Saniyedeki en fazla ısıtma isteği; `0` sınırsız |
679
747
  | `PREWARM_DELAY_MS` | `startPrewarm` | prod 500, dev 3000 | İlk turun gecikmesi |
748
+ | `PREWARM_RETRY_DELAY_MS` | `prewarm` | `2000` | Tekrar turundan önceki bekleme |
680
749
  | `PREWARM_INTERVAL_SECONDS` | `startPrewarm` | `0` | 0'dan büyükse periyodik tur |
681
750
  | `JSKELET_VERBOSE` | `jskelet dev` | — | `1` ise restart'ta değişen dosyaların tamamı listelenir |
682
751
  | `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. |