jskelet 0.1.1 → 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.
Files changed (66) hide show
  1. package/AGENTS.md +5 -0
  2. package/CHANGELOG.md +129 -2
  3. package/README.md +21 -7
  4. package/bin/jskelet.mjs +6 -6
  5. package/docs/03-routing.md +48 -9
  6. package/docs/04-render-ve-sablonlar.md +2 -2
  7. package/docs/05-islands.md +59 -6
  8. package/docs/06-cache.md +240 -26
  9. package/docs/07-yapilandirma.md +108 -7
  10. package/docs/08-build.md +4 -4
  11. package/docs/09-dev-araclari.md +5 -0
  12. package/docs/12-panel-ve-oturum.md +384 -0
  13. package/docs/README.md +25 -2
  14. package/docs/en/01-getting-started.md +292 -0
  15. package/docs/en/02-architecture.md +305 -0
  16. package/docs/en/03-routing.md +493 -0
  17. package/docs/en/04-rendering.md +504 -0
  18. package/docs/en/05-islands.md +492 -0
  19. package/docs/en/06-caching.md +640 -0
  20. package/docs/en/07-configuration.md +789 -0
  21. package/docs/en/08-build.md +383 -0
  22. package/docs/en/09-dev-tools.md +314 -0
  23. package/docs/en/10-deployment.md +332 -0
  24. package/docs/en/11-migration.md +360 -0
  25. package/docs/en/12-dashboards-and-sessions.md +392 -0
  26. package/docs/en/README.md +112 -0
  27. package/package.json +4 -2
  28. package/src/build/build.mjs +1 -1
  29. package/src/build/tasks/client.mjs +2 -2
  30. package/src/build/tasks/fonts.mjs +3 -3
  31. package/src/build/tasks/icons.mjs +1 -1
  32. package/src/build/tasks/images.mjs +2 -2
  33. package/src/client/devtools/overlay.js +196 -164
  34. package/src/client/devtools/report.js +96 -96
  35. package/src/client/form.js +192 -0
  36. package/src/client/index.js +10 -1
  37. package/src/client/registry.js +78 -4
  38. package/src/client/swap.js +188 -0
  39. package/src/config/defaults.js +83 -0
  40. package/src/config/index.js +129 -18
  41. package/src/config/pattern.js +1 -1
  42. package/src/dev-server.mjs +1 -1
  43. package/src/http/control-flow.js +16 -1
  44. package/src/http/cookies.js +257 -0
  45. package/src/http/request-context.js +162 -0
  46. package/src/index.js +26 -2
  47. package/src/init.mjs +32 -31
  48. package/src/log.mjs +8 -2
  49. package/src/logo.png +0 -0
  50. package/src/runtime/alias-hooks.mjs +1 -1
  51. package/src/server/assets.js +1 -1
  52. package/src/server/create-app.js +12 -4
  53. package/src/server/data-cache.js +244 -0
  54. package/src/server/dev/devtools.js +6 -2
  55. package/src/server/dev/report.js +8 -1
  56. package/src/server/dev/version-check.mjs +139 -0
  57. package/src/server/head-hints.js +1 -1
  58. package/src/server/html-cache.js +32 -6
  59. package/src/server/middleware/csrf.js +134 -0
  60. package/src/server/prewarm.js +164 -19
  61. package/src/server/render.js +256 -20
  62. package/src/server/router.js +14 -7
  63. package/src/server/status-page.js +1 -1
  64. package/src/version.mjs +9 -4
  65. package/src/views/components/loader.js +1 -1
  66. package/src/views/helpers/tags.js +53 -1
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,51 @@ 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
+
43
+ ## Public ve kişiye özel ayrımı
44
+
45
+ Bu belgedeki her şey **herkese aynı gidebilen** HTML için geçerli. Cache
46
+ anahtarında kimlik yok (yalnızca yol + query), yani önbellekteki bir sayfa onu
47
+ ilk isteyen kişinin değil, o yolun cevabıdır.
48
+
49
+ Kullanıcıya bağlı bir sayfa bu yüzden ayrı bir yoldan geçer:
50
+
51
+ ```js
52
+ app.get("/panel", route(async ({ req }) => { … }, { private: true }));
53
+ ```
54
+
55
+ `private: true` üç şeyi birden yapar: önbellek devre dışı kalır, config'in
56
+ `cache.html` deseni bu kararı **ezemez** ve yanıt `private, no-store`,
57
+ `Vary: Cookie` ile, ETag'siz gider. Ayrıntılar ve oturum/CSRF tarafı
58
+ [12-panel-ve-oturum.md](./12-panel-ve-oturum.md)'de.
59
+
60
+ Bayrağı unutursanız framework sessiz kalmaz: controller `Cookie`,
61
+ `Authorization` ya da `req.session`/`req.user` okuduğu anda render işaretlenir
62
+ ve önbelleğe **yazılmaz**. Dev'de istek bir hatayla düşer, üretimde `no-store`
63
+ ile servis edilip loglanır. Koruma bir mazeret değil son savunma — doğru yer
64
+ `private: true`.
65
+
26
66
  ## `revalidate` — TTL nereden gelir
27
67
 
28
68
  Bir route'un TTL'i iki kaynaktan gelebilir ve **config kazanır**:
@@ -31,6 +71,9 @@ Bir route'un TTL'i iki kaynaktan gelebilir ve **config kazanır**:
31
71
  2. `jskelet.config.mjs` → `cache().html` içindeki eşleşen desen. Varsa
32
72
  route'unkini ezer.
33
73
 
74
+ Tek istisna `private: true`: desen eşleşse bile yok sayılır. Kilit tek yönlü,
75
+ çünkü ters yönde bir hata sessiz veri sızıntısı anlamına geliyor.
76
+
34
77
  ```js
35
78
  // jskelet.config.mjs
36
79
  export default {
@@ -53,8 +96,14 @@ mümkün kılar; route dosyalarını dolaşmak gerekmez.
53
96
  yapılmaz. Hiç `cache().html` kuralı yoksa doğrudan route'un değeri kullanılır.
54
97
 
55
98
  `revalidate` verilmemişse ya da 0 ise sayfa **hiç önbelleklenmez**: her istek
56
- render edilir ve yanıta `Cache-Control` yazılmaz (yalnızca
57
- `X-JSkelet-Cache: MISS`).
99
+ render edilir ve yanıt `Cache-Control: private, no-store` ile, ETag'siz gider.
100
+ `X-JSkelet-Cache` başlığı da yazılmaz — önbellek yolu hiç çalışmadı, `MISS`
101
+ demek yanıltıcı olurdu.
102
+
103
+ Dinamik bir sayfaya `no-store` yazılması bilinçli. Hiç direktif taşımayan bir
104
+ yanıtı HTTP "sezgisel olarak önbelleklenebilir" sayıyor; araya giren bir proxy
105
+ ya da tarayıcının geri tuşu, tek bir ziyaretçi için üretilmiş HTML'i
106
+ saklayabiliyordu.
58
107
 
59
108
  Önbellek ayrıca yalnızca `GET` istekleri için devreye girer.
60
109
 
@@ -70,8 +119,8 @@ ile `/liste?sayfa=3` ayrı girdilerdir.
70
119
  Bunun pratik sonucu: query string'e bağlı olmayan bir sayfa, farklı kampanya
71
120
  parametreleriyle (`?utm_source=…`) çağrıldığında her kombinasyon için ayrı bir
72
121
  girdi üretir. Bu tür parametreleri ters proxy katmanında temizlemek ya da
73
- önbelleği kapatmak (`revalidate` vermemek) makul bir önlemdir; store en fazla
74
- 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.
75
124
 
76
125
  ## Stale-while-revalidate
77
126
 
@@ -92,7 +141,7 @@ Okuma davranışı:
92
141
 
93
142
  Stale penceresinde tazelemenin hatası isteği etkilemez: eski HTML pencere
94
143
  boyunca geçerli kalır ve hata yalnızca loglanır
95
- (`[html-cache] arka plan tazelemesi başarısız: …`).
144
+ (`[html-cache] background refresh failed: …`).
96
145
 
97
146
  Aynı anahtar için eşzamanlı tazelemeler tek bir çalışmada birleştirilir
98
147
  (`inflight` haritası): yüz eşzamanlı istek tek render'a düşer.
@@ -102,8 +151,8 @@ veri en fazla `revalidate + bir tazeleme turu` kadar geride olabilir. Bu bedel
102
151
  kabul edilebilir, çünkü fiyat gibi canlı alanlar istemcide WebSocket'ten
103
152
  güncelleniyor.
104
153
 
105
- Store LRU'dur: erişilen girdi sona taşınır, `MAX_ENTRIES = 500` aşılınca en
106
- 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.
107
156
 
108
157
  ## Ne önbelleğe yazılır
109
158
 
@@ -184,6 +233,82 @@ Ayrıntılar:
184
233
  - `withRequestCache(run)` dışa açıktır; `route()` dışında (ör. kendi yazdığınız
185
234
  bir Express handler'ında) aynı kapsamı kurmak için kullanılabilir.
186
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
+
187
312
  ## Degraded render: `reportUpstreamFailure`
188
313
 
189
314
  Render sırasında upstream düştüyse çıktı eksik veri içeriyor demektir. Böyle bir
@@ -220,14 +345,40 @@ export async function apiGet(path) {
220
345
 
221
346
  | Durum | Sayılır | Sonuç |
222
347
  | --- | --- | --- |
223
- | `0` (ağ hatası), `408`, `425`, `429`, `>= 500` | **Geçici** | Sayfa önbelleğe yazılmaz, uyarı: `[render] <yol> eksik veriyle üretildi, önbelleğe alınmıyor (…)` |
224
- | Diğerleri (`400`, `403`, `404`, …) | **Kalıcı** | Yalnızca uyarı: `[render] <yol> eksik veriyle üretildi, upstream kalıcı hata veriyor (…)`. Önbellek engellenmez. |
348
+ | `0` (ağ hatası), `408`, `425`, `429`, `>= 500` | **Geçici** | Sayfa önbelleğe yazılmaz, uyarı: `[render] <path> was produced with missing data, not caching it (…)` |
349
+ | Diğerleri (`400`, `403`, `404`, …) | **Kalıcı** | Yalnızca uyarı: `[render] <path> was produced with missing data, upstream is failing permanently (…)`. Önbellek engellenmez. |
225
350
 
226
351
  Kalıcı hataların önbelleği engellememesi bilinçli: deterministik cevaplar tekrar
227
352
  denemekle düzelmez. Onlar yüzünden önbelleği kapatmak sayfayı her ziyarette
228
353
  baştan render etmek olur — içerik yine aynı eksik hâliyle döner, ziyaretçi
229
354
  sadece render süresini öder.
230
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
+
231
382
  ## Önbelleği yönetmek
232
383
 
233
384
  `jskelet` şu fonksiyonları dışa açar:
@@ -303,23 +454,74 @@ Kurallar:
303
454
  - Yalnızca `/` ile başlayan string'ler alınır.
304
455
  - `prewarmSkip` öneklerinden biriyle başlayanlar atlanır. Varsayılan liste:
305
456
  `/api/`, `/_fragment/`, `/__jskelet/`. Oturuma bağlı sayfalar ısıtılmamalı.
306
- - Tekilleştirme **sırayı korur**: liste `PREWARM_MAX` ile budandığı için
307
- uygulamanın verdiği öncelik sırası anlamlıdır — en önemli sayfaları başa
308
- 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.
309
459
  - Bu hook tanımlı değilse ısıtma hiç kurulmaz; zamanlayıcı bile açılmaz.
310
460
 
311
461
  ### Tur mantığı
312
462
 
313
- 1. Liste toplanır, `PREWARM_MAX` (varsayılan 400) ile budanır.
314
- 2. `PREWARM_CONCURRENCY` işçi paralel olarak istek atar (prod'da 4, dev'de 2).
315
- Dev'de daha az paralellik: tarama, o an tarayıcıda açtığın sayfanın
316
- render'ıyla CPU için yarışmasın.
317
- 3. Başarısız yollar için **tek seri tekrar turu** yapılır (`concurrency: 1`).
318
- Hatalar çoğunlukla upstream rate limit'i (429): ilk tur yüzlerce sayfayı aynı
319
- anda çekerken API'yi zorluyor. Tekrar turu bu sayfaların önbelleğe girmesini
320
- sağlıyor; aksi hâlde ziyaretçi soğuk render'ı öder.
321
- 4. Özet loglanır:
322
- `[prewarm] 128/130 sayfa ısıtıldı, 2 hata, 5 sayfa tekrar turunda kurtarıldı (12.4s)`
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:
474
+ `[prewarm] warmed 128/130 pages, 2 failed, 5 recovered on the retry pass (12.4s)`
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.
323
525
 
324
526
  İstekler `user-agent: jskelet-prewarm` (`brand.prewarmUserAgent`) ve
325
527
  `accept-encoding: br, gzip` başlıklarıyla gider; ikincisi sıkıştırılmış gövdenin
@@ -353,10 +555,14 @@ tek seferlik deneyler config'i düzenlemeden yapılabilsin.
353
555
  | Ayar | Env | `cache().prewarm` | Varsayılan |
354
556
  | --- | --- | --- | --- |
355
557
  | Açık/kapalı | `PREWARM=0` kapatır, `PREWARM=1` config'i ezip açar | `enabled` | `true` |
356
- | En fazla yol | `PREWARM_MAX` | `max` | `400` |
558
+ | Tur başına en fazla yol | `PREWARM_MAX` | `max` | `400` |
357
559
  | Paralellik | `PREWARM_CONCURRENCY` | `concurrency` | prod 4, dev 2 |
560
+ | Saniyedeki istek | `PREWARM_RPS` | `rps` | `0` (sınırsız) |
358
561
  | Başlangıç gecikmesi (ms) | `PREWARM_DELAY_MS` | `delayMs` | prod 500, dev 3000 |
562
+ | Tekrar turu gecikmesi (ms) | `PREWARM_RETRY_DELAY_MS` | `retryDelayMs` | `2000` |
359
563
  | Periyot (saniye) | `PREWARM_INTERVAL_SECONDS` | `intervalSeconds` | `0` (kapalı) |
564
+ | Kuyruk rotasyonu | — | `rotate` | `true` |
565
+ | Isıtma sırası | — | `priority` | `[]` |
360
566
 
361
567
  Sayısal ayarlar yalnızca **pozitif ve sonlu** değer kabul eder; geçersiz bir
362
568
  değer sessizce bir sonraki katmana düşer.
@@ -392,7 +598,7 @@ turunun gerçekten `MISS` → önbellek doldurup doldurmadığını buradan gör
392
598
  `cache().html` içindeki desen 0 saniye veriyor. Veya sayfa `status: 200`
393
599
  dışında bir kod dönüyor.
394
600
  - **Sayfa `MISS` dönüyor ama upstream sağlam.** Geçici bir upstream hatası
395
- bildirilmiş olabilir; logda `eksik veriyle üretildi, önbelleğe alınmıyor`
601
+ bildirilmiş olabilir; logda `was produced with missing data, not caching it`
396
602
  satırını arayın.
397
603
  - **Sürekli eski veri.** `revalidate` çok yüksek; unutmayın ki gerçek gecikme en
398
604
  fazla `revalidate` + bir tazeleme turudur.
@@ -400,6 +606,14 @@ turunun gerçekten `MISS` → önbellek doldurup doldurmadığını buradan gör
400
606
  parametreleri girdi çoğaltıyor olabilir.
401
607
  - **Isıtma hiç çalışmıyor.** `hooks.prewarmPaths` tanımlı değil, `PREWARM=0`
402
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.
403
617
 
404
618
  ## Sırada ne var
405
619
 
@@ -25,7 +25,7 @@ düz değer** olabilir; fonksiyon olmaları hâlinde `async` olabilirler ve `thi
25
25
  config nesnesine bağlıdır. Bir bölüm hata verirse yalnızca o bölüm yok sayılır.
26
26
 
27
27
  Config başarıyla yüklendiğinde bir özet basılır:
28
- `[config] jskelet.config.mjs yüklendi — 3 header, 2 redirect, 1 cache kuralı`
28
+ `[config] jskelet.config.mjs loaded — 3 headers, 2 redirects, 1 cache rule`
29
29
 
30
30
  ## Tam örnek
31
31
 
@@ -62,6 +62,20 @@ export default {
62
62
  devGateBypass: ["/api/healthcheck", "/robots.txt"],
63
63
  preconnect: ["https://cdn.ornek.com"],
64
64
 
65
+ security: {
66
+ trustProxy: true,
67
+ cookieSecret: process.env.JSKELET_SECRET,
68
+ csrf: {
69
+ enabled: true,
70
+ token: false,
71
+ allowedOrigins: [],
72
+ exclude: ["/webhook/:path*"],
73
+ cookieName: "csrf_token",
74
+ fieldName: "_csrf",
75
+ headerName: "x-csrf-token",
76
+ },
77
+ },
78
+
65
79
  navigation: {
66
80
  prefetch: "moderate",
67
81
  prerender: "conservative",
@@ -101,7 +115,17 @@ export default {
101
115
  async cache() {
102
116
  return {
103
117
  html: { "/": 60, "/haber/:slug": 300 },
104
- 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
+ },
105
129
  };
106
130
  },
107
131
 
@@ -239,6 +263,37 @@ bir yapılandırmadır.
239
263
  preconnect: ["https://cdn.ornek.com", "https://api.ornek.com"]
240
264
  ```
241
265
 
266
+ ## `security`
267
+
268
+ **Tip:** `object` — **Varsayılan:**
269
+ `{ trustProxy: true, cookieSecret: null, csrf: { enabled: true, token: false, … } }`
270
+
271
+ Kişiye özel sayfaların tamamı ve gerekçeleri
272
+ [12-panel-ve-oturum.md](./12-panel-ve-oturum.md)'de; burada alanların referansı
273
+ var.
274
+
275
+ | Alan | Tip | Varsayılan | Anlamı |
276
+ | --- | --- | --- | --- |
277
+ | `trustProxy` | `boolean` | `true` | Express'in `trust proxy` ayarı. Ters proxy arkasında doğru protokol ve istemci IP'si için gerekli. |
278
+ | `cookieSecret` | `string \| null` | `null` | İmzalı cookie sırrı. Verilmezse `JSKELET_SECRET` okunur. |
279
+ | `csrf.enabled` | `boolean` | `true` | Origin/`Sec-Fetch-Site` kontrolü. |
280
+ | `csrf.token` | `boolean` | `false` | Çift gönderim token'ı katmanı. |
281
+ | `csrf.allowedOrigins` | `string[]` | `[]` | Kendi host'umuzun yanında kabul edilen origin'ler. |
282
+ | `csrf.exclude` | `string[]` | `[]` | Kontrolden muaf yollar; `source` desen sözdizimi. |
283
+ | `csrf.cookieName` | `string` | `"csrf_token"` | Token cookie'sinin adı. |
284
+ | `csrf.fieldName` | `string` | `"_csrf"` | `csrfField()`in bastığı alan adı. |
285
+ | `csrf.headerName` | `string` | `"x-csrf-token"` | Token'ın kabul edildiği başlık. |
286
+
287
+ `trustProxy` doğrudan internete açık bir sunucuda **kapatılmalı**: açıkken
288
+ istemci kendi `X-Forwarded-For` başlığını uydurabilir ve rate limit ile audit
289
+ log yanlış adresi görür.
290
+
291
+ CSRF kontrolü yalnızca çapraz site olduğu **belli** olan istekleri reddeder —
292
+ `Origin` uyuşmuyorsa ya da `Sec-Fetch-Site: cross-site` geldiyse. İkisi de yoksa
293
+ istek geçer, çünkü tarayıcılar çapraz origin bir POST'ta `Origin`'i her zaman
294
+ gönderirken webhook'lar hiç göndermez. Yine de tarayıcıdan gelmeyen uçları
295
+ `csrf.exclude` listesine yazmak niyeti okunur kılıyor.
296
+
242
297
  ## `navigation`
243
298
 
244
299
  **Tip:** `object` — **Varsayılan:**
@@ -504,8 +559,10 @@ Ayrıntı: [03-routing.md](./03-routing.md).
504
559
 
505
560
  ## `cache()`
506
561
 
507
- **Tip:** `() => { html?: Record<string, number>, prewarm?: object }` —
508
- **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 } }`
509
566
 
510
567
  ### `cache().html`
511
568
 
@@ -521,18 +578,59 @@ html: {
521
578
  }
522
579
  ```
523
580
 
581
+ Tek istisna `route(fn, { private: true })`: bu route'ta desen eşleşse bile yok
582
+ sayılır. Kilidin tek yönlü olması bilinçli — ters yönde bir hata, bir
583
+ kullanıcının HTML'inin bir başkasına servis edilmesi anlamına geliyor.
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
+
524
602
  ### `cache().prewarm`
525
603
 
526
604
  | Alan | Tip | Varsayılan | Anlamı |
527
605
  | --- | --- | --- | --- |
528
606
  | `enabled` | `boolean` | `true` | `false` ise ısıtma yapılmaz (`PREWARM=1` ile ezilebilir) |
529
- | `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 |
530
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. |
531
610
  | `delayMs` | `number` | prod 500, dev 3000 | Açılıştan sonra ilk turun gecikmesi |
611
+ | `retryDelayMs` | `number` | `2000` | Tekrar turundan önce beklenen süre |
532
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 |
533
615
 
534
- Her biri aynı adı taşıyan ortam değişkeniyle ezilebilir; env önceliklidir.
535
- 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).
536
634
 
537
635
  ## `hooks`
538
636
 
@@ -622,11 +720,14 @@ basılmaz.
622
720
  | `NODE_ENV` | her yer | `production` (start/build), `development` (dev) | Dev overlay, EJS cache, manifest yeniden okuma, route hata davranışı ve prewarm varsayılanlarını belirler. `jskelet dev` bunu kendisi ayarlar — `cross-env` gerekmez. |
623
721
  | `PORT` | `startServer` | `3000` | Dinlenecek port |
624
722
  | `HOST` | `startServer` | `0.0.0.0` | Bağlanılacak arayüz |
723
+ | `JSKELET_SECRET` | `jskelet/cookies` | — | İmzalı cookie sırrı. `security.cookieSecret` verilmediğinde buradan okunur; ikisi de yoksa imzalı cookie API'si hata verir. [12](./12-panel-ve-oturum.md) |
625
724
  | `DEV_TOKEN` | `devGate`, `prewarm` | — | Ayarlıysa token taşımayan her isteğe 404 döner. Isıtma token'ı çerez olarak taşır. [09](./09-dev-araclari.md) |
626
725
  | `PREWARM` | `startPrewarm` | — | `0` ısıtmayı kapatır; `1` config'teki `enabled: false`'u ezip açar |
627
726
  | `PREWARM_MAX` | `prewarm` | `400` | En fazla kaç yol ısıtılır |
628
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 |
629
729
  | `PREWARM_DELAY_MS` | `startPrewarm` | prod 500, dev 3000 | İlk turun gecikmesi |
730
+ | `PREWARM_RETRY_DELAY_MS` | `prewarm` | `2000` | Tekrar turundan önceki bekleme |
630
731
  | `PREWARM_INTERVAL_SECONDS` | `startPrewarm` | `0` | 0'dan büyükse periyodik tur |
631
732
  | `JSKELET_VERBOSE` | `jskelet dev` | — | `1` ise restart'ta değişen dosyaların tamamı listelenir |
632
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. |
package/docs/08-build.md CHANGED
@@ -69,7 +69,7 @@ dosya adlarını okunur tutuyor. Hash'li olmaları sayesinde bu dosyalara
69
69
  Build çalışmadıysa uygulama yine ayağa kalkar: `hasAsset()` false olur, layout
70
70
  stylesheet ve script etiketlerini hiç basmaz. `jskelet build` unutulduğunda hata
71
71
  yerine stilsiz ama çalışan bir sayfa görürsünüz. Manifest hiç yoksa bir kez uyarı
72
- basılır: `[assets] manifest yok — 'jskelet build' çalıştırın.`
72
+ basılır: ``[assets] no manifest — run `jskelet build`.``
73
73
 
74
74
  Manifest **dev'de her istekte** yeniden okunur (watch build hash'leri
75
75
  değiştirir), prod'da bir kez.
@@ -244,8 +244,8 @@ Son satır için iki güvenlik ağı var: ad taşıyan yapılandırma alanları
244
244
  sembolleri okuyup eksik olan için tek seferlik uyarı basar:
245
245
 
246
246
  ```
247
- [icon] sprite'ta yok: x-logo-regular — adı sabit yazın ya da
248
- build/tasks/icons.mjs taramasına ekleyin.
247
+ [icon] missing from sprite: x-logo-regular — write the name as a literal or add
248
+ it to the build/tasks/icons.mjs scan.
249
249
  ```
250
250
 
251
251
  Bu uyarıyı görürseniz ya adı sabit yazın, ya `icons.scan` listesine ilgili
@@ -352,7 +352,7 @@ karşılaşmaması.
352
352
  kontrol edin.
353
353
  - **Bazı Tailwind sınıfları çalışmıyor.** `@source` direktifi eksik olan bir
354
354
  dizinde yazılmışlar.
355
- - **İkon boş görünüyor.** Sprite'ta o sembol yok; dev'de `[icon] sprite'ta yok`
355
+ - **İkon boş görünüyor.** Sprite'ta o sembol yok; dev'de `[icon] missing from sprite`
356
356
  uyarısına bakın.
357
357
  - **Island'lar hiç açılmıyor.** `main.js` manifest'te yok (entry dizini boş ya
358
358
  da build atlanmış) veya bir build hatası var.
@@ -161,6 +161,11 @@ Gösterdikleri:
161
161
  - **Prewarm:** ısıtma turunun ilerlemesi; panelden elle tetiklenebilir, tek tek
162
162
  yollar tekrar denenebilir.
163
163
  - **Süreç:** pid, Node sürümü, uptime, RSS ve heap kullanımı.
164
+ - **Sürüm:** kurulu JSkelet sürümü ve npm'deki `latest` ile karşılaştırması.
165
+ Yeni bir sürüm varsa **Server** sekmesinde `update` rozeti ve yükseltme
166
+ komutunu kopyalayan bir satır çıkar. Yoklama açılıştan 1,5 saniye sonra bir
167
+ kez yapılır, sonucu 6 saat boyunca `os.tmpdir()` içinde saklanır ve ağ yoksa
168
+ sessizce atlanır. `JSKELET_VERSION_CHECK=0` ile tamamen kapatılır.
164
169
 
165
170
  Isıtma istekleri (`user-agent: jskelet-prewarm`) hem terminalden hem istek
166
171
  listesinden filtrelenir: yüzlerce istek görünümü doldurmasın. İlerleme baloncuğun