jskelet 0.6.2 → 0.6.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/AGENTS.md +136 -136
- package/CHANGELOG.md +620 -596
- package/LICENSE +21 -21
- package/bin/jskelet.mjs +130 -130
- package/docs/01-baslangic.md +291 -291
- package/docs/02-mimari.md +310 -309
- package/docs/03-routing.md +515 -515
- package/docs/04-render-ve-sablonlar.md +661 -661
- package/docs/05-islands.md +486 -486
- package/docs/06-cache.md +1443 -1423
- package/docs/07-yapilandirma.md +12 -6
- package/docs/08-build.md +429 -428
- package/docs/09-dev-araclari.md +364 -364
- package/docs/10-dagitim.md +338 -338
- package/docs/12-panel-ve-oturum.md +478 -478
- package/docs/README.md +83 -83
- package/docs/en/01-getting-started.md +298 -298
- package/docs/en/02-architecture.md +329 -328
- package/docs/en/03-routing.md +531 -531
- package/docs/en/04-rendering.md +669 -669
- package/docs/en/05-islands.md +497 -497
- package/docs/en/06-caching.md +1453 -1431
- package/docs/en/07-configuration.md +1219 -1214
- package/docs/en/08-build.md +447 -446
- package/docs/en/09-dev-tools.md +373 -373
- package/docs/en/10-deployment.md +340 -340
- package/docs/en/11-migration.md +398 -398
- package/docs/en/12-dashboards-and-sessions.md +488 -488
- package/docs/en/README.md +87 -87
- package/package.json +137 -137
- package/src/build/ensure-build.mjs +19 -19
- package/src/build/paths.mjs +153 -153
- package/src/build/resolve-peer.mjs +36 -36
- package/src/build/tasks/client.mjs +349 -349
- package/src/build/tasks/css.mjs +235 -235
- package/src/build/tasks/fonts.mjs +146 -146
- package/src/build/tasks/icons.mjs +357 -357
- package/src/build/tasks/images.mjs +244 -244
- package/src/build/tasks/precompress.mjs +78 -78
- package/src/build/tasks/templates.mjs +20 -20
- package/src/client/admin/i18n.js +764 -764
- package/src/client/admin/login.html +74 -74
- package/src/client/admin/panel.css +809 -809
- package/src/client/admin/panel.html +495 -495
- package/src/client/admin/panel.js +1251 -1251
- package/src/client/devtools/report.html +185 -185
- package/src/client/devtools/report.js +745 -745
- package/src/client/devtools/seo.js +628 -628
- package/src/client/dom.js +95 -95
- package/src/client/form.js +192 -192
- package/src/client/index.js +45 -45
- package/src/client/registry.js +305 -305
- package/src/client/safe-image.js +91 -91
- package/src/client/shared-cookie.js +225 -225
- package/src/client/store.js +36 -36
- package/src/client/swap.js +188 -188
- package/src/compile/codegen.js +336 -336
- package/src/compile/compile-all.js +149 -149
- package/src/compile/errors.js +66 -66
- package/src/compile/expr.js +409 -409
- package/src/compile/index.js +17 -17
- package/src/compile/parse.js +541 -541
- package/src/compile/resolve.js +211 -211
- package/src/compile/scan-exports.js +51 -51
- package/src/config/defaults.js +17 -1
- package/src/config/index.js +13 -0
- package/src/config/pattern.js +107 -107
- package/src/generate.mjs +163 -163
- package/src/http/control-flow.js +71 -71
- package/src/http/cookies-entry.js +21 -21
- package/src/http/cookies.js +277 -277
- package/src/http/request-cache.js +46 -46
- package/src/http/request-context.js +165 -165
- package/src/http/shared-cookie.js +178 -178
- package/src/index.js +101 -101
- package/src/init.mjs +230 -230
- package/src/migrate/apply.mjs +262 -262
- package/src/migrate/babel.mjs +79 -79
- package/src/migrate/classify.mjs +155 -155
- package/src/migrate/config.mjs +126 -126
- package/src/migrate/fs-walk.mjs +191 -191
- package/src/migrate/parse.mjs +26 -26
- package/src/migrate/scan.mjs +177 -177
- package/src/migrate/transform/expr-source.mjs +168 -168
- package/src/migrate/transform/island.mjs +67 -67
- package/src/migrate/transform/jsx-to-component.mjs +302 -302
- package/src/migrate/transform/jsx-to-jsk.mjs +330 -330
- package/src/migrate/transform/page-split.mjs +435 -435
- package/src/migrate/write.mjs +81 -81
- package/src/migrate.mjs +171 -171
- package/src/runtime/alias-hooks.mjs +119 -119
- package/src/runtime/register.mjs +4 -4
- package/src/server/admin/actions.js +229 -229
- package/src/server/admin/auth.js +125 -125
- package/src/server/admin/event-log.js +151 -151
- package/src/server/admin/gate.js +209 -209
- package/src/server/admin/inventory.js +188 -188
- package/src/server/admin/mount.js +56 -56
- package/src/server/admin/router.js +216 -216
- package/src/server/admin/snapshot.js +241 -241
- package/src/server/assets.js +147 -147
- package/src/server/auth/handoff.js +309 -309
- package/src/server/cache-blob.js +70 -0
- package/src/server/cache-deps.js +42 -42
- package/src/server/cache-vary.js +113 -113
- package/src/server/cloudflare.js +607 -607
- package/src/server/create-app.js +366 -366
- package/src/server/data-cache.js +553 -462
- package/src/server/dev/report.js +485 -485
- package/src/server/dev/socket.js +170 -170
- package/src/server/dev/version-check.mjs +139 -139
- package/src/server/disk-cache.js +233 -0
- package/src/server/ejs-adapter.js +59 -59
- package/src/server/html-cache.js +1196 -1122
- package/src/server/image-optimizer.js +500 -407
- package/src/server/logs/access-middleware.js +66 -66
- package/src/server/logs/file-sink.js +193 -66
- package/src/server/logs/pipeline.js +165 -158
- package/src/server/logs/s3-put.js +214 -214
- package/src/server/logs/s3-sink.js +112 -112
- package/src/server/metadata.js +102 -102
- package/src/server/middleware/compression.js +205 -205
- package/src/server/middleware/csrf.js +134 -134
- package/src/server/middleware/dev-gate.js +75 -75
- package/src/server/middleware/headers.js +37 -37
- package/src/server/middleware/redirects.js +32 -32
- package/src/server/middleware/robots-txt.js +341 -341
- package/src/server/middleware/static-precompressed.js +121 -100
- package/src/server/middleware/trailing-slash.js +53 -53
- package/src/server/middleware/upstream-proxy.js +141 -141
- package/src/server/og-image.js +356 -356
- package/src/server/port-guard.js +255 -255
- package/src/server/prewarm.js +1082 -1058
- package/src/server/redis.js +588 -569
- package/src/server/render.js +4 -4
- package/src/server/router.js +157 -157
- package/src/server/status-page.js +265 -265
- package/src/server/upstream-limiter.js +376 -376
- package/src/server/upstream-tracking.js +166 -166
- package/src/shared/cookie-domain.js +66 -66
- package/src/start.mjs +22 -22
- package/src/templates/layout.ejs +30 -30
- package/src/templates/layout.jsk +30 -30
- package/src/version.mjs +31 -31
- package/src/views/components/loader.js +101 -101
- package/src/views/helpers/html.js +102 -102
- package/src/views/helpers/tags.js +375 -375
- package/types/config/defaults.d.ts +15 -1
- package/types/config/index.d.ts +8 -0
- package/types/server/cache-blob.d.ts +13 -0
- package/types/server/data-cache.d.ts +9 -0
- package/types/server/disk-cache.d.ts +36 -0
- package/types/server/html-cache.d.ts +26 -3
- package/types/server/logs/file-sink.d.ts +16 -5
- package/types/server/redis.d.ts +2 -1
package/docs/06-cache.md
CHANGED
|
@@ -1,1423 +1,1443 @@
|
|
|
1
|
-
# 06 — Önbellek ve prewarm
|
|
2
|
-
|
|
3
|
-
Bu belge JSkelet'in ISR ikamesini bütün ayrıntılarıyla anlatır: HTML TTL
|
|
4
|
-
önbelleği ve stale-while-revalidate davranışı, `revalidate`'in nereden geldiği,
|
|
5
|
-
cache anahtarının nasıl kurulduğu, `X-JSkelet-Cache` başlığının değerleri,
|
|
6
|
-
sıkıştırılmış gövdenin neden önbellekte durduğu, istek içi memoizasyon
|
|
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.
|
|
10
|
-
Kararların arkasındaki ölçüm gerekçeleri [02-mimari.md](./02-mimari.md)'de,
|
|
11
|
-
config alanlarının tam referansı [07-yapilandirma.md](./07-yapilandirma.md)'de.
|
|
12
|
-
|
|
13
|
-
## Genel resim
|
|
14
|
-
|
|
15
|
-
```
|
|
16
|
-
route(controller, { revalidate })
|
|
17
|
-
└─ withHtmlCache(key, ttl, producer) ← TTL + stale-while-revalidate
|
|
18
|
-
└─ withUpstreamTracking(...) ← eksik veri tespiti
|
|
19
|
-
└─ withRequestCache(...) ← istek içi memoizasyon
|
|
20
|
-
└─ produce() → controller + renderPage
|
|
21
|
-
└─ withDataCache(...) ← upstream veri önbelleği
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
Sıra önemli: **istek içi cache en içte** olmalı ki aynı render'daki iki çağrı
|
|
25
|
-
tek upstream isteğine düşsün; **upstream takibi HTML cache'in içinde** olmalı ki
|
|
26
|
-
eksik veriyle üretilen çıktı önbelleğe yazılmasın.
|
|
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 + isteğe bağlı `vary`); yani
|
|
47
|
-
önbellekteki bir sayfa onu ilk isteyen kişinin değil, o yolun (ve vary
|
|
48
|
-
parçalarının) cevabıdır.
|
|
49
|
-
|
|
50
|
-
Kullanıcıya bağlı bir sayfa bu yüzden ayrı bir yoldan geçer:
|
|
51
|
-
|
|
52
|
-
```js
|
|
53
|
-
app.get("/panel", route(async ({ req }) => { … }, { private: true }));
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
`private: true` üç şeyi birden yapar: önbellek devre dışı kalır, config'in
|
|
57
|
-
`cache.html` deseni bu kararı **ezemez** ve yanıt `private, no-store`,
|
|
58
|
-
`Vary: Cookie` ile, ETag'siz gider. Ayrıntılar ve oturum/CSRF tarafı
|
|
59
|
-
[12-panel-ve-oturum.md](./12-panel-ve-oturum.md)'de.
|
|
60
|
-
|
|
61
|
-
Bayrağı unutursanız framework sessiz kalmaz: controller `Cookie`,
|
|
62
|
-
`Authorization` ya da `req.session`/`req.user` okuduğu anda render işaretlenir
|
|
63
|
-
ve önbelleğe **yazılmaz**. Dev'de istek bir hatayla düşer, üretimde `no-store`
|
|
64
|
-
ile servis edilip loglanır. Koruma bir mazeret değil son savunma — doğru yer
|
|
65
|
-
`private: true`.
|
|
66
|
-
|
|
67
|
-
## `revalidate` — TTL nereden gelir
|
|
68
|
-
|
|
69
|
-
Bir route'un TTL'i iki kaynaktan gelebilir ve **config kazanır**:
|
|
70
|
-
|
|
71
|
-
1. `route(controller, { revalidate: 60 })` — route'un kendi süresi.
|
|
72
|
-
2. `jskelet.config.mjs` → `cache().html` içindeki eşleşen desen. Varsa
|
|
73
|
-
route'unkini ezer.
|
|
74
|
-
|
|
75
|
-
Tek istisna `private: true`: desen eşleşse bile yok sayılır. Kilit tek yönlü,
|
|
76
|
-
çünkü ters yönde bir hata sessiz veri sızıntısı anlamına geliyor.
|
|
77
|
-
|
|
78
|
-
```js
|
|
79
|
-
// jskelet.config.mjs
|
|
80
|
-
export default {
|
|
81
|
-
async cache() {
|
|
82
|
-
return {
|
|
83
|
-
html: {
|
|
84
|
-
"/": 60,
|
|
85
|
-
"/haber/:slug": 300,
|
|
86
|
-
"/etiket/:slug": 120,
|
|
87
|
-
},
|
|
88
|
-
};
|
|
89
|
-
},
|
|
90
|
-
};
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
Config ile ezme, tek bir dosyadan tüm sitenin tazelik profilini ayarlamayı
|
|
94
|
-
mümkün kılar; route dosyalarını dolaşmak gerekmez.
|
|
95
|
-
|
|
96
|
-
Çözüm sonucu **yol başına hatırlanır**, böylece her istekte desen taraması
|
|
97
|
-
yapılmaz. Hiç `cache().html` kuralı yoksa doğrudan route'un değeri kullanılır.
|
|
98
|
-
|
|
99
|
-
`revalidate` verilmemişse ya da 0 ise sayfa **hiç önbelleklenmez**: her istek
|
|
100
|
-
render edilir ve yanıt `Cache-Control: private, no-store` ile, ETag'siz gider.
|
|
101
|
-
`X-JSkelet-Cache` başlığı da yazılmaz — önbellek yolu hiç çalışmadı, `MISS`
|
|
102
|
-
demek yanıltıcı olurdu.
|
|
103
|
-
|
|
104
|
-
Dinamik bir sayfaya `no-store` yazılması bilinçli. Hiç direktif taşımayan bir
|
|
105
|
-
yanıtı HTTP "sezgisel olarak önbelleklenebilir" sayıyor; araya giren bir proxy
|
|
106
|
-
ya da tarayıcının geri tuşu, tek bir ziyaretçi için üretilmiş HTML'i
|
|
107
|
-
saklayabiliyordu.
|
|
108
|
-
|
|
109
|
-
Önbellek ayrıca yalnızca `GET` istekleri için devreye girer.
|
|
110
|
-
|
|
111
|
-
## Cache anahtarı
|
|
112
|
-
|
|
113
|
-
```
|
|
114
|
-
`${varyPrefix}${yol}?${izin verilen query parametreleri, sıralı}`
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
`varyPrefix` varsayılan olarak boştur. Query'siz bir istek için anahtar
|
|
118
|
-
`${varyPrefix}${yol}?` biçimindedir. **Query parametresi taşıyan istek
|
|
119
|
-
varsayılan olarak dinamiktir**: önbelleğe hiç girmez ve `private, no-store`
|
|
120
|
-
ile gider. Bir yolun bütün varyantlarını cache'lemek `?utm_source=…` gibi
|
|
121
|
-
sonsuz sayıda anahtar üretiyor ve 500 girdilik store'da LRU, gerçek
|
|
122
|
-
sayfaları kampanya varyantları için dışarı atıyor.
|
|
123
|
-
|
|
124
|
-
Hangi parametrenin çıktıyı gerçekten değiştirdiğini uygulama bildirir —
|
|
125
|
-
`jskelet.config.mjs` → `cache().query`:
|
|
126
|
-
|
|
127
|
-
```js
|
|
128
|
-
cache: () => ({
|
|
129
|
-
html: { "/liste": 60 },
|
|
130
|
-
query: { "/liste": ["sayfa"] },
|
|
131
|
-
}),
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
Artık `/liste?sayfa=2` ile `/liste?sayfa=3` ayrı girdiler, `/liste?sayfa=2&utm_source=x`
|
|
135
|
-
ise `?sayfa=2` kopyasını paylaşır: listede olmayan parametre anahtara girmez.
|
|
136
|
-
Bir desen `true` ile eşlenirse bütün parametreler anahtara girer (dikkat: girdi
|
|
137
|
-
sayısını sınırlayan tek şey `maxEntries` olur), `[]` ile eşlenirse query tamamen
|
|
138
|
-
yok sayılır. Ayrıntı: [07-yapilandirma.md](./07-yapilandirma.md).
|
|
139
|
-
|
|
140
|
-
### Host / locale: `cache().vary`
|
|
141
|
-
|
|
142
|
-
CDN zaten tam URL ile ayırır; asıl risk **origin L1** ve Redis HTML anahtarıdır.
|
|
143
|
-
Host'tan locale üreten sitelerde (`tr.example.com` / `en.example.com`) vary
|
|
144
|
-
olmadan ilk locale'in HTML'i diğer host'a servis edilir — Express 5'te istek
|
|
145
|
-
nesnesine locale yazmak kırılgan bir kaçış yoludur.
|
|
146
|
-
|
|
147
|
-
```js
|
|
148
|
-
cache: () => ({
|
|
149
|
-
html: { "/": 300, "/instruments/:slug": 300 },
|
|
150
|
-
vary: {
|
|
151
|
-
// true → public Host (x-forwarded-host || host), lowercase, portsuz
|
|
152
|
-
host: true,
|
|
153
|
-
// veya özel:
|
|
154
|
-
// headers: ["x-locale"],
|
|
155
|
-
// fn: (req) => req.hostname.startsWith("tr.") ? "l=tr" : "l=en",
|
|
156
|
-
},
|
|
157
|
-
}),
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
Örnek anahtarlar: `h=tr.investvio.com|/instruments/aapl?`,
|
|
161
|
-
`h=tr.example.com&l=tr|/…?`.
|
|
162
|
-
|
|
163
|
-
| Alan | Tip | Anlamı |
|
|
164
|
-
| --- | --- | --- |
|
|
165
|
-
| `host` | `boolean` | Public Host'u `h=…` olarak anahtara ekler |
|
|
166
|
-
| `headers` | `string[]` | Verilen istek başlıklarını (`ad=değer`) ekler |
|
|
167
|
-
| `fn` | `(req) => string \| null` | Dönüş değeri bir segment olarak eklenir (tam kontrol) |
|
|
168
|
-
|
|
169
|
-
**Prewarm:** varsayılan ısıtma `http://127.0.0.1:<port>` üzerinden gider.
|
|
170
|
-
`vary.host` açıksa bu yalnızca loopback anahtarını ısıtır; locale sitelerinde
|
|
171
|
-
çoklu origin gerekir:
|
|
172
|
-
|
|
173
|
-
```js
|
|
174
|
-
prewarm: {
|
|
175
|
-
origins: ["http://localhost", "http://tr.localhost"],
|
|
176
|
-
},
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
Port yazılmazsa dinleme portu eklenir. `onVisit` modunda ısıtma, vary açıkken
|
|
180
|
-
ziyaretçinin `Host` başlığını kullanır.
|
|
181
|
-
|
|
182
|
-
## Stale-while-revalidate
|
|
183
|
-
|
|
184
|
-
Girdi yapısı:
|
|
185
|
-
|
|
186
|
-
```
|
|
187
|
-
expiresAt = now + ttl
|
|
188
|
-
staleUntil = now + ttl * 2 (STALE_FACTOR = 1)
|
|
189
|
-
produceMs = son başarılı üretimin süresi (ms)
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
Okuma davranışı:
|
|
193
|
-
|
|
194
|
-
| Durum | Yanıt | Arka plan |
|
|
195
|
-
| --- | --- | --- |
|
|
196
|
-
| `now < expiresAt - leadMs` | Önbellekteki HTML, `HIT` | — |
|
|
197
|
-
| `expiresAt - leadMs ≤ now < expiresAt` | Önbellekteki HTML, `HIT` | **Erken tazeleme** başlar |
|
|
198
|
-
| `expiresAt ≤ now < staleUntil` | Önbellekteki HTML **anında**, `STALE` | Tazeleme başlatılır |
|
|
199
|
-
| `now ≥ staleUntil` | Girdi silinir, taze render, `MISS` (uçuştaki tazeleme varken silinmez) | — |
|
|
200
|
-
|
|
201
|
-
`leadMs` sayfanın load süresini hesaba katar:
|
|
202
|
-
|
|
203
|
-
```
|
|
204
|
-
leadMs = min(max(produceMs * 2, 250ms), ttl / 2)
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
Böylece yavaş bir sayfa TTL dolduğu anda hâlâ soğuk render'a düşmez: taze
|
|
208
|
-
HTML çoğu zaman `expiresAt` gelmeden yazılmış olur. Trafik yoksa bir sweeper
|
|
209
|
-
aynı pencerede girdiyi soft-bayatlatır ve ısıtma kuyruğuna alır
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
`
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
```
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
|
353
|
-
|
|
|
354
|
-
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
`
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
{
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
429
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
[
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
invalidateHtmlCache(
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
bir
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
-
|
|
729
|
-
|
|
730
|
-
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
`
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
|
983
|
-
|
|
|
984
|
-
|
|
|
985
|
-
|
|
|
986
|
-
|
|
|
987
|
-
|
|
|
988
|
-
|
|
|
989
|
-
|
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
//
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
"
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
[prewarm]
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
```
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
###
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
###
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
`
|
|
1371
|
-
`
|
|
1372
|
-
|
|
1373
|
-
`
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
- **
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
- **
|
|
1411
|
-
olabilir;
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1
|
+
# 06 — Önbellek ve prewarm
|
|
2
|
+
|
|
3
|
+
Bu belge JSkelet'in ISR ikamesini bütün ayrıntılarıyla anlatır: HTML TTL
|
|
4
|
+
önbelleği ve stale-while-revalidate davranışı, `revalidate`'in nereden geldiği,
|
|
5
|
+
cache anahtarının nasıl kurulduğu, `X-JSkelet-Cache` başlığının değerleri,
|
|
6
|
+
sıkıştırılmış gövdenin neden önbellekte durduğu, istek içi memoizasyon
|
|
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.
|
|
10
|
+
Kararların arkasındaki ölçüm gerekçeleri [02-mimari.md](./02-mimari.md)'de,
|
|
11
|
+
config alanlarının tam referansı [07-yapilandirma.md](./07-yapilandirma.md)'de.
|
|
12
|
+
|
|
13
|
+
## Genel resim
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
route(controller, { revalidate })
|
|
17
|
+
└─ withHtmlCache(key, ttl, producer) ← TTL + stale-while-revalidate
|
|
18
|
+
└─ withUpstreamTracking(...) ← eksik veri tespiti
|
|
19
|
+
└─ withRequestCache(...) ← istek içi memoizasyon
|
|
20
|
+
└─ produce() → controller + renderPage
|
|
21
|
+
└─ withDataCache(...) ← upstream veri önbelleği
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Sıra önemli: **istek içi cache en içte** olmalı ki aynı render'daki iki çağrı
|
|
25
|
+
tek upstream isteğine düşsün; **upstream takibi HTML cache'in içinde** olmalı ki
|
|
26
|
+
eksik veriyle üretilen çıktı önbelleğe yazılmasın.
|
|
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 + isteğe bağlı `vary`); yani
|
|
47
|
+
önbellekteki bir sayfa onu ilk isteyen kişinin değil, o yolun (ve vary
|
|
48
|
+
parçalarının) cevabıdır.
|
|
49
|
+
|
|
50
|
+
Kullanıcıya bağlı bir sayfa bu yüzden ayrı bir yoldan geçer:
|
|
51
|
+
|
|
52
|
+
```js
|
|
53
|
+
app.get("/panel", route(async ({ req }) => { … }, { private: true }));
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`private: true` üç şeyi birden yapar: önbellek devre dışı kalır, config'in
|
|
57
|
+
`cache.html` deseni bu kararı **ezemez** ve yanıt `private, no-store`,
|
|
58
|
+
`Vary: Cookie` ile, ETag'siz gider. Ayrıntılar ve oturum/CSRF tarafı
|
|
59
|
+
[12-panel-ve-oturum.md](./12-panel-ve-oturum.md)'de.
|
|
60
|
+
|
|
61
|
+
Bayrağı unutursanız framework sessiz kalmaz: controller `Cookie`,
|
|
62
|
+
`Authorization` ya da `req.session`/`req.user` okuduğu anda render işaretlenir
|
|
63
|
+
ve önbelleğe **yazılmaz**. Dev'de istek bir hatayla düşer, üretimde `no-store`
|
|
64
|
+
ile servis edilip loglanır. Koruma bir mazeret değil son savunma — doğru yer
|
|
65
|
+
`private: true`.
|
|
66
|
+
|
|
67
|
+
## `revalidate` — TTL nereden gelir
|
|
68
|
+
|
|
69
|
+
Bir route'un TTL'i iki kaynaktan gelebilir ve **config kazanır**:
|
|
70
|
+
|
|
71
|
+
1. `route(controller, { revalidate: 60 })` — route'un kendi süresi.
|
|
72
|
+
2. `jskelet.config.mjs` → `cache().html` içindeki eşleşen desen. Varsa
|
|
73
|
+
route'unkini ezer.
|
|
74
|
+
|
|
75
|
+
Tek istisna `private: true`: desen eşleşse bile yok sayılır. Kilit tek yönlü,
|
|
76
|
+
çünkü ters yönde bir hata sessiz veri sızıntısı anlamına geliyor.
|
|
77
|
+
|
|
78
|
+
```js
|
|
79
|
+
// jskelet.config.mjs
|
|
80
|
+
export default {
|
|
81
|
+
async cache() {
|
|
82
|
+
return {
|
|
83
|
+
html: {
|
|
84
|
+
"/": 60,
|
|
85
|
+
"/haber/:slug": 300,
|
|
86
|
+
"/etiket/:slug": 120,
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Config ile ezme, tek bir dosyadan tüm sitenin tazelik profilini ayarlamayı
|
|
94
|
+
mümkün kılar; route dosyalarını dolaşmak gerekmez.
|
|
95
|
+
|
|
96
|
+
Çözüm sonucu **yol başına hatırlanır**, böylece her istekte desen taraması
|
|
97
|
+
yapılmaz. Hiç `cache().html` kuralı yoksa doğrudan route'un değeri kullanılır.
|
|
98
|
+
|
|
99
|
+
`revalidate` verilmemişse ya da 0 ise sayfa **hiç önbelleklenmez**: her istek
|
|
100
|
+
render edilir ve yanıt `Cache-Control: private, no-store` ile, ETag'siz gider.
|
|
101
|
+
`X-JSkelet-Cache` başlığı da yazılmaz — önbellek yolu hiç çalışmadı, `MISS`
|
|
102
|
+
demek yanıltıcı olurdu.
|
|
103
|
+
|
|
104
|
+
Dinamik bir sayfaya `no-store` yazılması bilinçli. Hiç direktif taşımayan bir
|
|
105
|
+
yanıtı HTTP "sezgisel olarak önbelleklenebilir" sayıyor; araya giren bir proxy
|
|
106
|
+
ya da tarayıcının geri tuşu, tek bir ziyaretçi için üretilmiş HTML'i
|
|
107
|
+
saklayabiliyordu.
|
|
108
|
+
|
|
109
|
+
Önbellek ayrıca yalnızca `GET` istekleri için devreye girer.
|
|
110
|
+
|
|
111
|
+
## Cache anahtarı
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
`${varyPrefix}${yol}?${izin verilen query parametreleri, sıralı}`
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
`varyPrefix` varsayılan olarak boştur. Query'siz bir istek için anahtar
|
|
118
|
+
`${varyPrefix}${yol}?` biçimindedir. **Query parametresi taşıyan istek
|
|
119
|
+
varsayılan olarak dinamiktir**: önbelleğe hiç girmez ve `private, no-store`
|
|
120
|
+
ile gider. Bir yolun bütün varyantlarını cache'lemek `?utm_source=…` gibi
|
|
121
|
+
sonsuz sayıda anahtar üretiyor ve 500 girdilik store'da LRU, gerçek
|
|
122
|
+
sayfaları kampanya varyantları için dışarı atıyor.
|
|
123
|
+
|
|
124
|
+
Hangi parametrenin çıktıyı gerçekten değiştirdiğini uygulama bildirir —
|
|
125
|
+
`jskelet.config.mjs` → `cache().query`:
|
|
126
|
+
|
|
127
|
+
```js
|
|
128
|
+
cache: () => ({
|
|
129
|
+
html: { "/liste": 60 },
|
|
130
|
+
query: { "/liste": ["sayfa"] },
|
|
131
|
+
}),
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Artık `/liste?sayfa=2` ile `/liste?sayfa=3` ayrı girdiler, `/liste?sayfa=2&utm_source=x`
|
|
135
|
+
ise `?sayfa=2` kopyasını paylaşır: listede olmayan parametre anahtara girmez.
|
|
136
|
+
Bir desen `true` ile eşlenirse bütün parametreler anahtara girer (dikkat: girdi
|
|
137
|
+
sayısını sınırlayan tek şey `maxEntries` olur), `[]` ile eşlenirse query tamamen
|
|
138
|
+
yok sayılır. Ayrıntı: [07-yapilandirma.md](./07-yapilandirma.md).
|
|
139
|
+
|
|
140
|
+
### Host / locale: `cache().vary`
|
|
141
|
+
|
|
142
|
+
CDN zaten tam URL ile ayırır; asıl risk **origin L1** ve Redis HTML anahtarıdır.
|
|
143
|
+
Host'tan locale üreten sitelerde (`tr.example.com` / `en.example.com`) vary
|
|
144
|
+
olmadan ilk locale'in HTML'i diğer host'a servis edilir — Express 5'te istek
|
|
145
|
+
nesnesine locale yazmak kırılgan bir kaçış yoludur.
|
|
146
|
+
|
|
147
|
+
```js
|
|
148
|
+
cache: () => ({
|
|
149
|
+
html: { "/": 300, "/instruments/:slug": 300 },
|
|
150
|
+
vary: {
|
|
151
|
+
// true → public Host (x-forwarded-host || host), lowercase, portsuz
|
|
152
|
+
host: true,
|
|
153
|
+
// veya özel:
|
|
154
|
+
// headers: ["x-locale"],
|
|
155
|
+
// fn: (req) => req.hostname.startsWith("tr.") ? "l=tr" : "l=en",
|
|
156
|
+
},
|
|
157
|
+
}),
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Örnek anahtarlar: `h=tr.investvio.com|/instruments/aapl?`,
|
|
161
|
+
`h=tr.example.com&l=tr|/…?`.
|
|
162
|
+
|
|
163
|
+
| Alan | Tip | Anlamı |
|
|
164
|
+
| --- | --- | --- |
|
|
165
|
+
| `host` | `boolean` | Public Host'u `h=…` olarak anahtara ekler |
|
|
166
|
+
| `headers` | `string[]` | Verilen istek başlıklarını (`ad=değer`) ekler |
|
|
167
|
+
| `fn` | `(req) => string \| null` | Dönüş değeri bir segment olarak eklenir (tam kontrol) |
|
|
168
|
+
|
|
169
|
+
**Prewarm:** varsayılan ısıtma `http://127.0.0.1:<port>` üzerinden gider.
|
|
170
|
+
`vary.host` açıksa bu yalnızca loopback anahtarını ısıtır; locale sitelerinde
|
|
171
|
+
çoklu origin gerekir:
|
|
172
|
+
|
|
173
|
+
```js
|
|
174
|
+
prewarm: {
|
|
175
|
+
origins: ["http://localhost", "http://tr.localhost"],
|
|
176
|
+
},
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Port yazılmazsa dinleme portu eklenir. `onVisit` modunda ısıtma, vary açıkken
|
|
180
|
+
ziyaretçinin `Host` başlığını kullanır.
|
|
181
|
+
|
|
182
|
+
## Stale-while-revalidate
|
|
183
|
+
|
|
184
|
+
Girdi yapısı:
|
|
185
|
+
|
|
186
|
+
```
|
|
187
|
+
expiresAt = now + ttl
|
|
188
|
+
staleUntil = now + ttl * 2 (STALE_FACTOR = 1)
|
|
189
|
+
produceMs = son başarılı üretimin süresi (ms)
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Okuma davranışı:
|
|
193
|
+
|
|
194
|
+
| Durum | Yanıt | Arka plan |
|
|
195
|
+
| --- | --- | --- |
|
|
196
|
+
| `now < expiresAt - leadMs` | Önbellekteki HTML, `HIT` | — |
|
|
197
|
+
| `expiresAt - leadMs ≤ now < expiresAt` | Önbellekteki HTML, `HIT` | **Erken tazeleme** başlar |
|
|
198
|
+
| `expiresAt ≤ now < staleUntil` | Önbellekteki HTML **anında**, `STALE` | Tazeleme başlatılır |
|
|
199
|
+
| `now ≥ staleUntil` | Girdi silinir, taze render, `MISS` (uçuştaki tazeleme varken silinmez) | — |
|
|
200
|
+
|
|
201
|
+
`leadMs` sayfanın load süresini hesaba katar:
|
|
202
|
+
|
|
203
|
+
```
|
|
204
|
+
leadMs = min(max(produceMs * 2, 250ms), ttl / 2)
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Böylece yavaş bir sayfa TTL dolduğu anda hâlâ soğuk render'a düşmez: taze
|
|
208
|
+
HTML çoğu zaman `expiresAt` gelmeden yazılmış olur. Trafik yoksa bir sweeper
|
|
209
|
+
aynı pencerede girdiyi soft-bayatlatır ve ısıtma kuyruğuna alır. Bir tur en
|
|
210
|
+
fazla dört girdi işaretler (süresi en yakın dolacak olan önce); kalanlar
|
|
211
|
+
sonraki saniyelere kalır. `startPrewarm` (`PREWARM=0` değilse) kuyruğu HTTP
|
|
212
|
+
ile boşaltır — klasik `prewarmPaths` olmasa da — ama bu boşaltma klasik turun
|
|
213
|
+
`rps: 0` ayarını kullanmaz: aynı anda tek istek, saniyede en fazla iki.
|
|
214
|
+
|
|
215
|
+
Stale penceresinde tazelemenin hatası isteği etkilemez: eski HTML pencere
|
|
216
|
+
boyunca geçerli kalır ve hata yalnızca loglanır
|
|
217
|
+
(`[html-cache] background refresh failed: …`).
|
|
218
|
+
|
|
219
|
+
Aynı anahtar için eşzamanlı tazelemeler tek bir çalışmada birleştirilir
|
|
220
|
+
(`inflight` haritası): yüz eşzamanlı istek tek render'a düşer.
|
|
221
|
+
|
|
222
|
+
Kazanç: ilk ısıtmadan sonra hiçbir istek render'ı beklemez. Bedel: HTML'deki
|
|
223
|
+
veri en fazla `revalidate + bir tazeleme turu` kadar geride olabilir. Bu bedel
|
|
224
|
+
kabul edilebilir, çünkü fiyat gibi canlı alanlar istemcide WebSocket'ten
|
|
225
|
+
güncelleniyor.
|
|
226
|
+
|
|
227
|
+
Store LRU'dur: erişilen girdi sona taşınır, sınır (`cache().maxEntries`,
|
|
228
|
+
varsayılan 500) aşılınca en eski düşürülür. Config 500'ün üstünü isteyebilir;
|
|
229
|
+
**800'ü geçemez** — daha yükseği uyarıyla 800'e çekilir. Bunun yanında süreç
|
|
230
|
+
içi HTML string + sıkıştırılmış gövde **256 MB**'yi geçemez. Sıkıştırılmış
|
|
231
|
+
kopya tektir: brotli veya gzip, hangisi son istendiyse. Ham HTML durur.
|
|
232
|
+
Sayı tavanının
|
|
233
|
+
altında kalan şişman sayfa veya `vary.host` kopyası da bu bütçede LRU ile
|
|
234
|
+
düşer. Tek sayfa 256 MB'den büyükse saklanmaz; yanıt o istekte yine gider.
|
|
235
|
+
|
|
236
|
+
## Ne önbelleğe yazılır
|
|
237
|
+
|
|
238
|
+
Yalnızca şu iki koşulun **ikisini** birlikte sağlayan çıktı saklanır:
|
|
239
|
+
|
|
240
|
+
1. `status === 200`
|
|
241
|
+
2. `degraded !== true` — render sırasında geçici bir upstream hatası
|
|
242
|
+
bildirilmemiş.
|
|
243
|
+
|
|
244
|
+
Yani 404 sayfaları, redirect'ler ve eksik veriyle üretilmiş HTML önbelleğe
|
|
245
|
+
girmez.
|
|
246
|
+
|
|
247
|
+
## Yanıt başlıkları
|
|
248
|
+
|
|
249
|
+
`route()` her yanıta `X-JSkelet-Cache` yazar (başlık adı `brand.cacheHeader`
|
|
250
|
+
ile değiştirilebilir):
|
|
251
|
+
|
|
252
|
+
| Değer | Anlamı |
|
|
253
|
+
| --- | --- |
|
|
254
|
+
| `HIT` | Önbellekten, taze |
|
|
255
|
+
| `STALE` | Önbellekten, süresi geçmiş; arkada tazeleniyor |
|
|
256
|
+
| `MISS` | Bu istekte render edildi (ya da önbellek kapalı) |
|
|
257
|
+
|
|
258
|
+
Önbelleklenebilir yanıtlarda ayrıca:
|
|
259
|
+
|
|
260
|
+
```
|
|
261
|
+
Cache-Control: public, max-age=0, s-maxage=<revalidate>, stale-while-revalidate=60
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
`max-age=0` tarayıcıda saklamayı kapatır, `s-maxage` ara katmanlara (CDN, ters
|
|
265
|
+
proxy) süreyi bildirir. Böylece CDN önünde durduğunda aynı tazelik modeli iki
|
|
266
|
+
katmanda birlikte çalışır.
|
|
267
|
+
|
|
268
|
+
## Sıkıştırılmış gövdenin saklanması
|
|
269
|
+
|
|
270
|
+
Önbelleğe alınan her girdi bir `encoded` haritası taşır ve HTML ile aynı ömrü
|
|
271
|
+
paylaşır. Bir sayfa ilk kez brotli ya da gzip ile istendiğinde çıktı hesaplanıp
|
|
272
|
+
haritaya konur; sonraki isteklerde aynı buffer gönderilir. Aynı sayfa her
|
|
273
|
+
istekte yeniden brotli'lenmez.
|
|
274
|
+
|
|
275
|
+
Bu yolda `Content-Encoding`, `Vary` ve `Content-Length` doğrudan `route()`
|
|
276
|
+
tarafından yazılır; sıkıştırma middleware'i `Content-Encoding` gördüğü için
|
|
277
|
+
devreye girmez.
|
|
278
|
+
|
|
279
|
+
`HEAD` istekleri sıkıştırılmaz (gövde yok). İstemci ne brotli ne gzip kabul
|
|
280
|
+
ediyorsa düz HTML gönderilir.
|
|
281
|
+
|
|
282
|
+
## İstek içi memoizasyon: `cache()`
|
|
283
|
+
|
|
284
|
+
React'in `cache()` fonksiyonunun karşılığı: aynı istek içinde aynı argümanlarla
|
|
285
|
+
yapılan çağrılar tek kez çalışır.
|
|
286
|
+
|
|
287
|
+
```js
|
|
288
|
+
// lib/api/articles.js
|
|
289
|
+
import { cache } from "jskelet";
|
|
290
|
+
|
|
291
|
+
export const getArticle = cache(async (slug) => {
|
|
292
|
+
const response = await fetch(`${process.env.API_ORIGIN}/articles/${slug}`);
|
|
293
|
+
return response.json();
|
|
294
|
+
});
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
Artık aynı render'da hem controller hem `hooks.layoutContext()` aynı yazıyı
|
|
298
|
+
isterse tek upstream isteği yapılır.
|
|
299
|
+
|
|
300
|
+
Ayrıntılar:
|
|
301
|
+
|
|
302
|
+
- Bağlam `AsyncLocalStorage` ile taşınır ve `route()` içinde
|
|
303
|
+
`withRequestCache()` tarafından kurulur.
|
|
304
|
+
- **Bağlam yoksa memoizasyon devre dışı kalır** ve fonksiyon doğrudan çağrılır.
|
|
305
|
+
Bir script'ten ya da başka bir sürecin içinden çağırmak güvenlidir.
|
|
306
|
+
- Anahtar `JSON.stringify(args)`; argümansız çağrılar `""` anahtarını
|
|
307
|
+
paylaşır. Serileştirilemeyen argümanlar (fonksiyon, `Symbol`, döngüsel nesne)
|
|
308
|
+
ile kullanmayın.
|
|
309
|
+
- Saklanan şey fonksiyonun **dönüş değeridir**, yani `async` fonksiyonlarda
|
|
310
|
+
Promise'in kendisi. Aynı Promise paylaşıldığı için eşzamanlı çağrılar da
|
|
311
|
+
birleşir.
|
|
312
|
+
- `withRequestCache(run)` dışa açıktır; `route()` dışında (ör. kendi yazdığınız
|
|
313
|
+
bir Express handler'ında) aynı kapsamı kurmak için kullanılabilir.
|
|
314
|
+
|
|
315
|
+
## İstekler arası veri önbelleği: `withDataCache`
|
|
316
|
+
|
|
317
|
+
`cache()` yalnızca **tek bir istek** boyunca yaşar. Uzun kuyruğu API kotasından
|
|
318
|
+
korumak için gereken şey istekler arasında yaşayan, TTL'li ve kendi kendini
|
|
319
|
+
tazeleyen bir veri katmanı:
|
|
320
|
+
|
|
321
|
+
```js
|
|
322
|
+
// lib/api/articles.js
|
|
323
|
+
import { withDataCache, reportUpstreamFailure } from "jskelet";
|
|
324
|
+
|
|
325
|
+
export async function getArticle(slug) {
|
|
326
|
+
return withDataCache(`haber:${slug}`, 600, async () => {
|
|
327
|
+
const response = await fetch(`${process.env.API_ORIGIN}/articles/${slug}`);
|
|
328
|
+
|
|
329
|
+
if (!response.ok) {
|
|
330
|
+
reportUpstreamFailure({ status: response.status, path: `/articles/${slug}` });
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return response.json();
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
Aynı kalıbın sarmalayıcı biçimi — anahtar argümanlardan üretilir:
|
|
340
|
+
|
|
341
|
+
```js
|
|
342
|
+
import { dataCache } from "jskelet";
|
|
343
|
+
|
|
344
|
+
export const getArticle = dataCache(
|
|
345
|
+
async (slug) => apiGet(`/articles/${slug}`),
|
|
346
|
+
{ key: "haber", revalidate: 600 },
|
|
347
|
+
);
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
Davranış:
|
|
351
|
+
|
|
352
|
+
| Durum | Sonuç |
|
|
353
|
+
| --- | --- |
|
|
354
|
+
| Taze girdi | Anında döner, `producer` çalışmaz |
|
|
355
|
+
| TTL geçmiş, bayat pencere sürüyor | Bayat değer **anında** döner, tazeleme arkada yürür |
|
|
356
|
+
| Girdi yok | `producer` beklenir |
|
|
357
|
+
| `producer` hata verdi, bayat girdi var | Bayat değer döner, uyarı: `[data-cache] producer failed, serving stale value: …` |
|
|
358
|
+
| `producer` hata verdi, girdi yok | Hata çağırana gider |
|
|
359
|
+
|
|
360
|
+
Ayrıntılar:
|
|
361
|
+
|
|
362
|
+
- **Aynı anahtarı eşzamanlı isteyen çağrılar tek upstream isteğine düşer.**
|
|
363
|
+
Isıtma turlarında kotayı en çok kurtaran davranış bu: 50 sayfa aynı endeks
|
|
364
|
+
verisini istiyorsa API bir kez çağrılır.
|
|
365
|
+
- **`null` ve `undefined` saklanmaz.** Uygulamaların HTTP istemcisi hatada
|
|
366
|
+
genellikle `null` döner; bunu saklamak geçici bir 429'u TTL boyunca "veri yok"
|
|
367
|
+
hâline dondurmak olurdu. Boş cevabı bilinçli olarak saklamak isteyen
|
|
368
|
+
`{ storeEmpty: true }` verir.
|
|
369
|
+
- **Bayat pencere HTML'dekinden uzun**: `staleFactor` varsayılanı 10, yani girdi
|
|
370
|
+
TTL'inin 11 katı boyunca acil durum yedeği olarak kalır. Bayat veri, eksik
|
|
371
|
+
sayfadan iyidir. Anahtar başına `{ staleFactor: 0 }` ile kapatılabilir.
|
|
372
|
+
- Anahtar tamamen uygulamanın: dil, sürüm, sayfa numarası gibi ayrımlar anahtara
|
|
373
|
+
yazılır (`haber:tr:v2:${slug}`).
|
|
374
|
+
- TTL `0` verildiğinde önbellek devre dışı kalır ve `producer` her çağrıda
|
|
375
|
+
çalışır — bir ayarı geçici olarak kapatmak için yeterli.
|
|
376
|
+
- **Bayt tavanı 64 MB.** Sayı sınırı şişman JSON'u tutmaz; süreç içi gövdeler
|
|
377
|
+
bu tavanı geçemez ve config yükseltemez. Tek değer tavanı aşıyorsa saklanmaz,
|
|
378
|
+
çağıran sonucu yine alır. Tahliye en eski girdiden olur.
|
|
379
|
+
|
|
380
|
+
Yönetim yüzeyi:
|
|
381
|
+
|
|
382
|
+
| Fonksiyon | Ne yapar |
|
|
383
|
+
| --- | --- |
|
|
384
|
+
| `withDataCache(key, ttlSeconds, producer, options?)` | Ana giriş noktası |
|
|
385
|
+
| `dataCache(fn, { key, revalidate, … })` | Fonksiyon sarmalayıcısı |
|
|
386
|
+
| `clearDataCache(prefix?)` | Önek eşleşen girdileri (ya da tümünü) düşürür, silinen sayısını döner |
|
|
387
|
+
| `getDataCacheSize()` | Girdi sayısı |
|
|
388
|
+
| `getDataCacheEntries()` | Döküm: `{ key, stale, expiresIn }`. Değerin kendisi dönmez. |
|
|
389
|
+
|
|
390
|
+
`clearDataCache("haber:")`, "bu içerik güncellendi" webhook'unun karşılığıdır:
|
|
391
|
+
tek bir bölümün verisini düşürür ve **o veriyi okumuş HTML sayfalarını da**
|
|
392
|
+
bayatlatır, yani güncelleme TTL beklenmeden görünür. Ayrıntısı aşağıda,
|
|
393
|
+
"Otomatik bağımlılık" bölümünde.
|
|
394
|
+
|
|
395
|
+
## Degraded render: `reportUpstreamFailure`
|
|
396
|
+
|
|
397
|
+
Render sırasında upstream düştüyse çıktı eksik veri içeriyor demektir. Böyle bir
|
|
398
|
+
HTML'i tüm TTL boyunca servis etmek yerine önbelleğe **hiç yazmamak** doğru
|
|
399
|
+
davranış: sonraki istek yeniden dener.
|
|
400
|
+
|
|
401
|
+
Bu bilgi iki yoldan gelir.
|
|
402
|
+
|
|
403
|
+
### Otomatik izleme (varsayılan)
|
|
404
|
+
|
|
405
|
+
`createApp()` açılışta `globalThis.fetch`i sarar ve render sırasında yapılan
|
|
406
|
+
çağrılardaki **geçici** hataları (`429`, `5xx`, ağ hatası) kendiliğinden
|
|
407
|
+
bildirir. Uygulama tarafında hiçbir satır gerekmez; `fetch` ile konuşan bir API
|
|
408
|
+
istemcisi varsa rate limit koruması hazırdır.
|
|
409
|
+
|
|
410
|
+
Ayrıntılar:
|
|
411
|
+
|
|
412
|
+
- Yalnızca bir render bağlamı içindeki çağrılar sayılır. Script'ten, cron'dan
|
|
413
|
+
ya da istek dışı bir yerden yapılan `fetch` dokunulmaz kalır.
|
|
414
|
+
- Kendi sunucumuza yapılan istekler (`localhost`, `127.0.0.1`) atlanır: ısıtma
|
|
415
|
+
turu ve sağlık kontrolü upstream değildir.
|
|
416
|
+
- `404`/`403` gibi deterministik cevaplar **otomatik olarak bildirilmez**. Çoğu
|
|
417
|
+
API'de `404` "böyle bir kayıt yok" demektir; onu eksik veri saymak her yok
|
|
418
|
+
sayfasında yanlış uyarı üretirdi.
|
|
419
|
+
- Kapatmak için `cache().trackUpstream: false`. `fetch`i kendisi saran bir
|
|
420
|
+
uygulama (ölçüm, retry, circuit breaker) bunu tercih edebilir.
|
|
421
|
+
|
|
422
|
+
### Elle bildirim
|
|
423
|
+
|
|
424
|
+
`fetch` kullanmayan bir istemci (veritabanı sürücüsü, gRPC, satıcı SDK'sı) ya da
|
|
425
|
+
kalıcı hataları da işaretlemek isteyen bir katman için sözleşme aynı kaldı.
|
|
426
|
+
Bağımlılık yönü bilinçli olarak tersine çevrilmiştir: framework veri katmanını
|
|
427
|
+
tanımaz, veri katmanı framework'e haber verir. Hiç çağıran olmazsa maliyet boş
|
|
428
|
+
bir dizidir. Aynı hata her iki yoldan bildirilirse tekilleştirilir.
|
|
429
|
+
|
|
430
|
+
```js
|
|
431
|
+
// lib/api/client.js
|
|
432
|
+
import { reportUpstreamFailure } from "jskelet";
|
|
433
|
+
|
|
434
|
+
export async function apiGet(path) {
|
|
435
|
+
try {
|
|
436
|
+
const response = await fetch(`${process.env.API_ORIGIN}${path}`);
|
|
437
|
+
|
|
438
|
+
if (!response.ok) {
|
|
439
|
+
reportUpstreamFailure({ status: response.status, path });
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return response.json();
|
|
444
|
+
} catch (error) {
|
|
445
|
+
// Yanıt hiç gelmedi: status 0 ağ hatası anlamına gelir.
|
|
446
|
+
reportUpstreamFailure({ status: 0, path });
|
|
447
|
+
return null;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
### Loader sözleşmesi: boş liste ≠ hata
|
|
453
|
+
|
|
454
|
+
`catch → []` (veya `null`) ile yutulan bir upstream hatası, yanlış mapping ile
|
|
455
|
+
aynı görünür: boş UI. Rate limit ve geçici hatalar logda doğru yönde
|
|
456
|
+
işaretlense bile ziyaretçi “veri yok” sanır. Widget loader’ları sessiz
|
|
457
|
+
`[]`’ye gömülmek yerine sonucu ayırsın:
|
|
458
|
+
|
|
459
|
+
```js
|
|
460
|
+
/**
|
|
461
|
+
* @returns {Promise<{ items: object[], error: Error | null }>}
|
|
462
|
+
*/
|
|
463
|
+
export async function loadTickerItems() {
|
|
464
|
+
try {
|
|
465
|
+
const items = await apiGet("/ticker");
|
|
466
|
+
if (!items) {
|
|
467
|
+
return { items: [], error: new Error("Upstream returned no data") };
|
|
468
|
+
}
|
|
469
|
+
return { items, error: null };
|
|
470
|
+
} catch (error) {
|
|
471
|
+
return {
|
|
472
|
+
items: [],
|
|
473
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
Uygulama tarafında ortak bir `LoadErrorState` bileşeni (veya eşdeğeri) bu
|
|
480
|
+
`error` alanını göstersin; her widget kendi boş hâline düşmesin:
|
|
481
|
+
|
|
482
|
+
```js
|
|
483
|
+
// views/components/load-error-state.js
|
|
484
|
+
import { esc } from "jskelet/html";
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* @param {{ message?: string, title?: string }} props
|
|
488
|
+
* @returns {string}
|
|
489
|
+
*/
|
|
490
|
+
export function LoadErrorState({ message, title = "Veri yüklenemedi" }) {
|
|
491
|
+
return `<div role="alert" data-load-error class="…">
|
|
492
|
+
<p>${esc(title)}</p>
|
|
493
|
+
${message ? `<p>${esc(message)}</p>` : ""}
|
|
494
|
+
</div>`;
|
|
495
|
+
}
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
```html
|
|
499
|
+
{#if error}
|
|
500
|
+
<LoadErrorState :message="error.message" />
|
|
501
|
+
{#else if items.length}
|
|
502
|
+
{#each items as item}
|
|
503
|
+
…
|
|
504
|
+
{/each}
|
|
505
|
+
{#else}
|
|
506
|
+
<p>Kayıt yok</p>
|
|
507
|
+
{/if}
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
Framework markaya özel UI taşımaz; `LoadErrorState` uygulama bileşenidir.
|
|
511
|
+
Önemli olan sözleşme: `{ items, error }` (veya eşdeğeri) ve hata ile “gerçekten
|
|
512
|
+
boş”un şablonda ayrı kolları.
|
|
513
|
+
|
|
514
|
+
### Geçici ve kalıcı hata ayrımı
|
|
515
|
+
|
|
516
|
+
| Durum | Sayılır | Sonuç |
|
|
517
|
+
| --- | --- | --- |
|
|
518
|
+
| `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 (…)` |
|
|
519
|
+
| Diğerleri (`400`, `403`, `404`, …) | **Kalıcı** | Yalnızca uyarı: `[render] <path> was produced with missing data, upstream is failing permanently (…)`. Önbellek engellenmez. |
|
|
520
|
+
|
|
521
|
+
Kalıcı hataların önbelleği engellememesi bilinçli: deterministik cevaplar tekrar
|
|
522
|
+
denemekle düzelmez. Onlar yüzünden önbelleği kapatmak sayfayı her ziyarette
|
|
523
|
+
baştan render etmek olur — içerik yine aynı eksik hâliyle döner, ziyaretçi
|
|
524
|
+
sadece render süresini öder.
|
|
525
|
+
|
|
526
|
+
Eksik veriyle üretilen çıktı **paylaşılan önbelleklere de sunulmaz**: `degraded`
|
|
527
|
+
bir yanıt `public, s-maxage=…` değil `private, no-store` alır. Süreç içi
|
|
528
|
+
önbelleğe yazmama kararını CDN'de geri almak, aynı hatayı bir katman yukarıda
|
|
529
|
+
tekrarlamak olurdu. Teşhis başlığı (`X-JSkelet-Cache: MISS`) yine yazılır.
|
|
530
|
+
|
|
531
|
+
### `notFound()` geçici hataya denk gelirse
|
|
532
|
+
|
|
533
|
+
Veri gelmediği için `notFound()` çağıran bir controller, upstream rate limit'e
|
|
534
|
+
girdiğinde tüm siteyi 404'e çevirebilir — ve bu 404'ler önbelleğe girdiği için
|
|
535
|
+
geçici bir kota sorunu TTL boyunca "bu sayfa yok" cevabına dönüşür. Arama motoru
|
|
536
|
+
için bu kalıcı bir kayıp.
|
|
537
|
+
|
|
538
|
+
Framework bu durumu ayırır: render sırasında **geçici** bir upstream hatası
|
|
539
|
+
varsa `notFound()` 404 olarak servis edilmez. Sırayla:
|
|
540
|
+
|
|
541
|
+
1. Sayfa kısa bir beklemeden sonra **yeniden denenir** (varsayılan bir kez,
|
|
542
|
+
300 ms sonra). Deneme kendi upstream ve istek içi cache bağlamında koşar;
|
|
543
|
+
ilk turun hatası da memoize edilmiş boş cevabı da ikinci turu etkilemez.
|
|
544
|
+
2. İkinci tur sayfayı üretebilirse ziyaretçi **gerçek içeriği** görür ve çıktı
|
|
545
|
+
normal şekilde önbelleğe girer. Isıtma günlükleri bunun sık olduğunu
|
|
546
|
+
gösteriyor: aynı yol saniyeler sonra 200 dönüyor.
|
|
547
|
+
3. Denemeler tükendiyse yanıt `503` olur — önbelleğe girmez, `Retry-After`
|
|
548
|
+
taşır, sonraki istek yine gerçek içeriği üretebilir.
|
|
549
|
+
|
|
550
|
+
| Render sırasında | `notFound()` sonucu |
|
|
551
|
+
| --- | --- |
|
|
552
|
+
| Geçici hata var (`429`, `5xx`, ağ hatası) | Tekrar dene → başarılıysa sayfa; hâlâ olmuyorsa `503`, `Retry-After: 30`, `no-store` |
|
|
553
|
+
| Tekrar denemede upstream sağlam cevap verip "yok" dedi | Normal `404` |
|
|
554
|
+
| Kalıcı hata var (`404`, `403`…) ya da hata yok | Normal `404`, tekrar denenmez |
|
|
555
|
+
|
|
556
|
+
Log satırları:
|
|
557
|
+
|
|
558
|
+
```
|
|
559
|
+
[render] /haber/x returned notFound() while upstream is failing (429 /api/...), retrying (1/1)
|
|
560
|
+
[render] /haber/x could not be produced, upstream is still failing (429 /api/...), serving an uncached 503 instead of a 404
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
Yani **var olan bir sayfa hiçbir koşulda 404'e dönüşmez**: ya gerçek içerik
|
|
564
|
+
gelir, ya önbelleğe girmeyen bir 503. Hiçbir şey "yok" olarak dondurulmaz.
|
|
565
|
+
|
|
566
|
+
Tekrar denemenin maliyeti upstream'e binen ikinci bir istek turudur; bu yüzden
|
|
567
|
+
varsayılan tek deneme. Ayar `cache().transientRetry`:
|
|
568
|
+
|
|
569
|
+
```js
|
|
570
|
+
cache: {
|
|
571
|
+
transientRetry: { attempts: 2, delayMs: 500 },
|
|
572
|
+
}
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
`transientRetry: false` (ya da `attempts: 0`) tekrarı kapatır ve doğrudan 503'e
|
|
576
|
+
düşer.
|
|
577
|
+
|
|
578
|
+
## Upstream hız freni: `cache().upstream`
|
|
579
|
+
|
|
580
|
+
Buraya kadarki her şey 429 **geldikten sonra** ne olacağını anlatıyor. Bu bölüm
|
|
581
|
+
429'u en baştan almamakla ilgili.
|
|
582
|
+
|
|
583
|
+
Fren `trackUpstreamFetch()` sarmalayıcısının içinde, yani gerçek `fetch`
|
|
584
|
+
çağrısının geçtiği yerde duruyor. Isıtma turundaki `prewarm.rps` bu işi
|
|
585
|
+
yapamaz: o, kendi sunucumuza atılan **sayfa** isteklerini sayıyor, ama bir
|
|
586
|
+
sayfa render'ı bir API çağrısı da yapabilir yirmi tane de. Kotayı bağlayan şey
|
|
587
|
+
sayfa sayısı değil, çağrı sayısı — ve fren buraya konduğunda ısıtma da gerçek
|
|
588
|
+
trafik de aynı bütçeden harcar.
|
|
589
|
+
|
|
590
|
+
Varsayılan **kapalı**: `rate` verilmedikçe hiçbir istek beklemez ve maliyet bir
|
|
591
|
+
daldan ibarettir.
|
|
592
|
+
|
|
593
|
+
```js
|
|
594
|
+
// jskelet.config.mjs
|
|
595
|
+
cache: () => ({
|
|
596
|
+
upstream: {
|
|
597
|
+
rate: 10, // saniyedeki tavan (host başına)
|
|
598
|
+
burst: 20, // kısa patlama toleransı
|
|
599
|
+
concurrency: 8, // aynı anda uçan çağrı
|
|
600
|
+
hosts: {
|
|
601
|
+
// Kotası farklı olan uçlar ayrı ayarlanır.
|
|
602
|
+
"api.example.com": { rate: 3, concurrency: 2 },
|
|
603
|
+
},
|
|
604
|
+
},
|
|
605
|
+
}),
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
### Üç mekanizma, üç farklı sınır
|
|
609
|
+
|
|
610
|
+
| Mekanizma | Neyi sınırlar | Ayar |
|
|
611
|
+
| --- | --- | --- |
|
|
612
|
+
| Token bucket | Ortalama hız (çağrı/saniye) | `rate`, `burst` |
|
|
613
|
+
| Eşzamanlılık | Anlık baskı (aynı anda uçan çağrı) | `concurrency` |
|
|
614
|
+
| AIMD | Doğru hızın ne olduğu | `minRate`, `increaseStep`, `increaseIntervalMs`, `decreaseIntervalMs` |
|
|
615
|
+
|
|
616
|
+
Üçüncüsü asıl fikir. Sabit bir hız her zaman ya çok yavaş ya çok hızlıdır:
|
|
617
|
+
kotanın gerçek sınırını kimse config'e doğru yazamaz, üstelik gün içinde
|
|
618
|
+
değişir. Bu yüzden `rate` bir **tavan** olarak alınır ve gerçek hız upstream'in
|
|
619
|
+
cevabına göre oynar:
|
|
620
|
+
|
|
621
|
+
- **429 ya da 503** → hız yarıya iner (çarpımsal azalma). Yanıt `Retry-After`
|
|
622
|
+
taşıyorsa kova o süre boyunca tamamen durur — upstream sana ne kadar
|
|
623
|
+
bekleyeceğini zaten söylüyor.
|
|
624
|
+
- **Temiz geçen her pencere** → hız `increaseStep` kadar yukarı çıkar
|
|
625
|
+
(toplamsal artış), `rate` tavanına kadar.
|
|
626
|
+
|
|
627
|
+
Azalmanın çarpımsal, artışın toplamsal olması bilinçli. Tersi olsaydı her
|
|
628
|
+
pencerede yeniden 429 yenirdi.
|
|
629
|
+
|
|
630
|
+
### Devre kesici
|
|
631
|
+
|
|
632
|
+
Art arda `breakerFailures` (varsayılan 5) tane 429 alan bir host
|
|
633
|
+
`breakerCooldownMs` süresince tamamen baypas edilir: çağrı hiç yapılmaz,
|
|
634
|
+
doğrudan geçici hata olarak bildirilir.
|
|
635
|
+
|
|
636
|
+
Sert görünüyor ama asimetri bunu gerektiriyor: 429 geçici sayıldığı için o
|
|
637
|
+
çağrıyla üretilen HTML **önbelleğe yazılmaz**. Yani rate limit'e girmiş bir
|
|
638
|
+
turda kota harcanır ve karşılığında hiçbir şey saklanmaz. Üstüne bir sonraki
|
|
639
|
+
tur aynı sayfayı yine soğuk bulup yine dener. Kesici bu boşa yanmayı kesiyor.
|
|
640
|
+
|
|
641
|
+
```
|
|
642
|
+
[upstream] api.example.com: 5 consecutive rate limits — bypassing for 10000ms (rate is now 1.2/s)
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
Yalnızca 429 ve 503 sayılır. `400`/`404` bir kota sorunu değil, `500` de öyle:
|
|
646
|
+
onlar için yavaşlamak arızayı düzeltmez, sadece siteyi yavaşlatır.
|
|
647
|
+
|
|
648
|
+
### Durumu görmek
|
|
649
|
+
|
|
650
|
+
`getUpstreamLimiterStatus()` host başına o anki hızı, uçuştaki çağrıyı ve
|
|
651
|
+
sayaçları döner; dev panelinin **Server** sekmesi de aynı bilgiyi basıyor.
|
|
652
|
+
429 fırtınasında "şu an saniyede kaça indi" bilgisi olmadan ayar yapmak
|
|
653
|
+
körlemesine olur.
|
|
654
|
+
|
|
655
|
+
```js
|
|
656
|
+
import { getUpstreamLimiterStatus } from "jskelet";
|
|
657
|
+
|
|
658
|
+
// [{ host: "api.example.com", rate: 2.5, maxRate: 10, concurrency: 8,
|
|
659
|
+
// active: 3, throttled: 12, rejected: 40, bypassed: false, blockedMs: 0 }]
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
### Freni açmadan önce
|
|
663
|
+
|
|
664
|
+
Hız freni son çare. Aynı upstream yanıtını yüzlerce sayfa çekiyorsa asıl çözüm
|
|
665
|
+
[`withDataCache`](#istekler-arası-veri-önbelleği-withdatacache) TTL'ini tur
|
|
666
|
+
aralığından uzun tutmak: 400 sayfalık bir tur, ortak bir uç için tek çağrı
|
|
667
|
+
yapar. Fren o çağrıları yavaşlatır, sayısını azaltmaz.
|
|
668
|
+
|
|
669
|
+
## Önbelleği yönetmek
|
|
670
|
+
|
|
671
|
+
`jskelet` şu fonksiyonları dışa açar:
|
|
672
|
+
|
|
673
|
+
| Fonksiyon | Ne yapar |
|
|
674
|
+
| --- | --- |
|
|
675
|
+
| `withHtmlCache(key, ttlSeconds, producer)` | Önbelleği doğrudan kullanmak için. `ttlSeconds` 0 ise producer her zaman çalışır. |
|
|
676
|
+
| `invalidateHtmlCache(target, options?)` | Eşleşen sayfaları bayatlatır (ya da `{ hard: true }` ile düşürür), etkilenen sayı döner. |
|
|
677
|
+
| `clearHtmlCache()` | Store'u tamamen boşaltır. |
|
|
678
|
+
| `getHtmlCacheSize()` | Girdi sayısı. |
|
|
679
|
+
| `getHtmlCacheEntries()` | Döküm: `{ key, bytes, status, stale, expiresIn, encodings, deps }`. HTML gövdesi dönmez, yalnızca boyutu. |
|
|
680
|
+
|
|
681
|
+
### Hedefli invalidation
|
|
682
|
+
|
|
683
|
+
TTL'i beklemekle tüm önbelleği boşaltmak arasındaki boşluğu `invalidateHtmlCache()`
|
|
684
|
+
doldurur:
|
|
685
|
+
|
|
686
|
+
```js
|
|
687
|
+
import { invalidateHtmlCache } from "jskelet";
|
|
688
|
+
|
|
689
|
+
invalidateHtmlCache("/haber/abc"); // o yol ve altı
|
|
690
|
+
invalidateHtmlCache("/haber/:slug"); // desen sözdizimi
|
|
691
|
+
invalidateHtmlCache([/-yorumlar$/, "/"]); // RegExp ve liste
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
Varsayılan davranış **bayatlatmaktır**, silmek değil: girdi süresi geçmiş
|
|
695
|
+
sayılır ve normal stale-while-revalidate yoluna düşer. Bir webhook beş yüz
|
|
696
|
+
sayfayı birden düşürdüğünde sert silme, tam da içeriğin güncellendiği anda beş
|
|
697
|
+
yüz soğuk render başlatır ve upstream'i döver. Bayatlatmada ziyaretçi eski
|
|
698
|
+
HTML'i beklemeden alır, tazeleme arkada ve anahtar başına tek seferde koşar.
|
|
699
|
+
Eski HTML'in gerçekten geçersiz olduğu durumlar için `{ hard: true }`.
|
|
700
|
+
|
|
701
|
+
Anahtar `yol?query` olduğundan eşleştirme **yol kısmına** yapılır: bir yolun
|
|
702
|
+
bütün query varyantları (`?utm_source=…` dahil) tek çağrıyla düşer. Düz
|
|
703
|
+
string'te önek segment sınırında kesilir — `/haber` kuralı `/haberler`i
|
|
704
|
+
etkilemez.
|
|
705
|
+
|
|
706
|
+
Uçuşta olan bir render de hedeflenir: purge'den önce başlamış bir tur, sonucu
|
|
707
|
+
artık eski veriyi taşıdığı için önbelleğe **yazılmaz** ve bir sonraki istek yeni
|
|
708
|
+
bir tur başlatır.
|
|
709
|
+
|
|
710
|
+
### Otomatik bağımlılık: `clearDataCache` HTML'i de tazeler
|
|
711
|
+
|
|
712
|
+
Hangi sayfanın hangi içerikten etkilendiğini bildirmek gerekmez. Render
|
|
713
|
+
sırasında okunan her `withDataCache` anahtarı kaydedilir; `clearDataCache()` bir
|
|
714
|
+
anahtarı düşürdüğünde onu **fiilen okumuş** bütün HTML girdileri bayatlar.
|
|
715
|
+
|
|
716
|
+
```js
|
|
717
|
+
// "bu haber güncellendi" webhook'u
|
|
718
|
+
clearDataCache(`haber:${slug}`);
|
|
719
|
+
```
|
|
720
|
+
|
|
721
|
+
Bu tek satır haber detayını, o haberi listeleyen ana sayfayı ve etiket
|
|
722
|
+
sayfasını birlikte tazeler — çünkü üçü de o anahtarı okumuştu. Elle tag'lemede
|
|
723
|
+
en sık yapılan hata (detayı işaretleyip listeyi unutmak) burada yapısal olarak
|
|
724
|
+
mümkün değil: bildirim değil, gözlem var.
|
|
725
|
+
|
|
726
|
+
Ayrıntılar:
|
|
727
|
+
|
|
728
|
+
- Bağımlılık **her tazelemede yeniden** toplanır; sayfanın okuduğu anahtarlar
|
|
729
|
+
zamanla değişebilir.
|
|
730
|
+
- Render sürerken gelen bir purge de yakalanır: o turun çıktısı "doğduğu anda
|
|
731
|
+
bayat" olacağı için önbelleğe yazılmaz.
|
|
732
|
+
- Sayfa başına bağımlılık sayısı `getHtmlCacheEntries()` dökümünde `deps`
|
|
733
|
+
alanında görünür. Bir invalidation beklediğiniz sayfayı tazelemiyorsa önce
|
|
734
|
+
buraya bakın: sayfa o veriyi `withDataCache` üzerinden okumuyor olabilir.
|
|
735
|
+
- `withDataCache` kullanmayan bir uygulamada kaydedilecek bir şey yoktur;
|
|
736
|
+
`cache().trackDependencies: false` ile izleme tamamen kapatılabilir.
|
|
737
|
+
- Bayatlatılan yollar ısıtma kuyruğunun **başına** alınır. `prewarm` kuruluysa
|
|
738
|
+
sayfa, ziyaretçi gelmesini beklemeden tazelenir ve tur özeti bunu ayırt eder:
|
|
739
|
+
`[prewarm] warmed 12/12 pages, 3 invalidated (0.4s)`.
|
|
740
|
+
|
|
741
|
+
Bir yönetim ucu yazmak için:
|
|
742
|
+
|
|
743
|
+
```js
|
|
744
|
+
import { clearHtmlCache, getHtmlCacheEntries } from "jskelet";
|
|
745
|
+
|
|
746
|
+
export default function register(app) {
|
|
747
|
+
app.post("/_admin/cache/temizle", (req, res) => {
|
|
748
|
+
if (req.headers["x-admin-token"] !== process.env.ADMIN_TOKEN) {
|
|
749
|
+
res.status(404).end();
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
clearHtmlCache();
|
|
753
|
+
res.json({ ok: true });
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
app.get("/_admin/cache", (req, res) => {
|
|
757
|
+
res.json(getHtmlCacheEntries());
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
```
|
|
761
|
+
|
|
762
|
+
Dev sunucusu ayrıca manifest her değiştiğinde önbelleği kendiliğinden temizler:
|
|
763
|
+
saklanan HTML eski hash'li varlık URL'lerini taşıyor olur ve temizlenmezse sayfa
|
|
764
|
+
silinmiş dosyayı istemeye devam eder ([09-dev-araclari.md](./09-dev-araclari.md)).
|
|
765
|
+
|
|
766
|
+
Önbellek süreç belleğinde yaşadığı için birden fazla süreç/kopya çalıştırıyorsanız
|
|
767
|
+
her birinin kendi önbelleği olur; `clearHtmlCache()` yalnızca çağrıldığı süreci
|
|
768
|
+
etkiler. Birden fazla instance çalıştıran bir kurulumda bunu aşmanın yolu bir
|
|
769
|
+
sonraki bölümde.
|
|
770
|
+
|
|
771
|
+
## Paylaşımlı önbellek: Redis
|
|
772
|
+
|
|
773
|
+
Varsayılan önbellek tek prosese ait. Bu, tek instance çalışan bir sitede en hızlı
|
|
774
|
+
ve en basit kurulum — ama üç kopya çalıştırdığınızda iki sorun çıkar:
|
|
775
|
+
|
|
776
|
+
1. **Her kopya kendi başına ısınır.** Yeni bir instance açıldığında ya da bir
|
|
777
|
+
deploy sonrası konteyner yenilendiğinde önbellek boştur; aynı sayfa üç kez
|
|
778
|
+
render edilir, aynı veri üç kez çekilir.
|
|
779
|
+
2. **Invalidation tek kopyaya ulaşır.** `invalidateHtmlCache()` çağıran webhook
|
|
780
|
+
yalnızca isteği alan instance'ı tazeler; diğerleri TTL'i bekler. Ziyaretçi
|
|
781
|
+
hangi kopyaya düştüğüne göre eski ya da yeni içeriği görür.
|
|
782
|
+
|
|
783
|
+
`cache().redis` bu iki sorunu çözer. Redis **birincil store olmaz**: bellek içi
|
|
784
|
+
önbellek (L1) aynen kalır ve her istek onu okur; Redis ikinci kademedir (L2).
|
|
785
|
+
|
|
786
|
+
```js
|
|
787
|
+
// jskelet.config.mjs
|
|
788
|
+
export default {
|
|
789
|
+
cache() {
|
|
790
|
+
return {
|
|
791
|
+
html: { "/haber/:slug": 300 },
|
|
792
|
+
redis: {
|
|
793
|
+
enabled: true,
|
|
794
|
+
url: process.env.REDIS_URL,
|
|
795
|
+
namespace: "haber-sitesi",
|
|
796
|
+
},
|
|
797
|
+
};
|
|
798
|
+
},
|
|
799
|
+
};
|
|
800
|
+
```
|
|
801
|
+
|
|
802
|
+
`ioredis` opsiyonel bir peer bağımlılıktır, uygulamanın kendisine kurulur:
|
|
803
|
+
|
|
804
|
+
```bash
|
|
805
|
+
npm install ioredis
|
|
806
|
+
```
|
|
807
|
+
|
|
808
|
+
Kurulmadıysa ya da Redis'e bağlanılamıyorsa uyarı basılır ve site **bellek içi
|
|
809
|
+
önbellekle çalışmaya devam eder**. Redis çalışırken düşerse aynı şey olur: bir
|
|
810
|
+
devre kesici art arda beş hatadan sonra katmanı beş saniye baypas eder, böylece
|
|
811
|
+
her istek ağ zaman aşımı beklemez.
|
|
812
|
+
|
|
813
|
+
### Ne kazanırsınız
|
|
814
|
+
|
|
815
|
+
- **Soğuk instance sıcak önbellek bulur.** L1'de olmayan bir yol için render
|
|
816
|
+
çalışmadan önce Redis okunur; başka bir kopya o sayfayı ürettiyse render hiç
|
|
817
|
+
çalışmaz.
|
|
818
|
+
- **Veri önbelleği kotayı bir kez harcar.** `withDataCache` aynı mantıkla
|
|
819
|
+
çalışır ve kazanç burada daha büyük: JSON küçük, bir kopyanın çektiği veri
|
|
820
|
+
hepsine yeter.
|
|
821
|
+
- **Invalidation her kopyaya gider.** `invalidateHtmlCache()`,
|
|
822
|
+
`clearHtmlCache()` ve `clearDataCache()` bir pub/sub kanalına mesaj bırakır;
|
|
823
|
+
her instance kendi L1'ine aynı işlemi uygular. Deseni yayınlar, eşleşen
|
|
824
|
+
anahtarları değil — hangi yolun nerede sıcak olduğu kopyaya bağlı.
|
|
825
|
+
|
|
826
|
+
### Anahtar düzeni
|
|
827
|
+
|
|
828
|
+
```
|
|
829
|
+
_jskelet:{namespace}:{buildId}:html:{vary|}{yol}?{query}
|
|
830
|
+
_jskelet:{namespace}:{buildId}:data:{anahtar}
|
|
831
|
+
_jskelet:{namespace}:events
|
|
832
|
+
```
|
|
833
|
+
|
|
834
|
+
`buildId` her build'de değişir (`jskelet build` bunu `.jskelet/build.json`
|
|
835
|
+
dosyasına yazar) ve **zorunlu bir parçadır**: saklanan HTML hash'li varlık
|
|
836
|
+
yollarını gömüyor, yani bir deploy'dan sonra eski HTML geçersizdir. Kimlik
|
|
837
|
+
önekte durduğu için yeni sürüm kendiliğinden yeni bir isim alanına yazar, eski
|
|
838
|
+
anahtarlar TTL ile ölür — elle temizlik ya da `FLUSHDB` gerekmez. Build
|
|
839
|
+
çalıştırılmadıysa kimlik `dev` olur.
|
|
840
|
+
|
|
841
|
+
`namespace` aynı Redis'i paylaşan birden fazla uygulamayı ayırır. Olay kanalı
|
|
842
|
+
bilinçli olarak `buildId` **taşımaz**: deploy sırasında eski ve yeni sürüm yan
|
|
843
|
+
yana koşuyor ve bir purge ikisine de ulaşmalı.
|
|
844
|
+
|
|
845
|
+
1 KB ve üstü HTML ve veri girdileri düz JSON olarak durmaz: `JSK\x01` önekli
|
|
846
|
+
brotli gövde yazılır (kalite 5, metin modu — yanıt sıkıştırmasıyla aynı ayar).
|
|
847
|
+
L1 yine çözülmüş nesne tutar; sıkıştırma yalnızca L1 miss'ten sonraki
|
|
848
|
+
paylaşımda, yanıtı bekletmeden çalışır. Daha küçük kayıtlar düz JSON kalır,
|
|
849
|
+
eski düz JSON kayıtlar da okunur. zstd kullanılmaz: `node:zlib` içindeki zstd
|
|
850
|
+
22.15'ten itibaren var, motor ise `>=22`.
|
|
851
|
+
|
|
852
|
+
Redis kapalıyken (ya da bağlanamadığında) aynı gövde `.jskelet/cache/<buildId>/`
|
|
853
|
+
altına dosya olarak yazılır. Süreç yeniden açılınca L1 boştur ama diskteki
|
|
854
|
+
taze kopya render'ı atlatır. Bu tek makine içindir: birden fazla instance aynı
|
|
855
|
+
dizini paylaşmaz, küme için Redis durur. Yeni bir `buildId` eskisinin dizinini
|
|
856
|
+
bir sonraki yazışta siler. `clearHtmlCache()` ve invalidation dosyayı da düşürür.
|
|
857
|
+
|
|
858
|
+
### Bilmeniz gereken takaslar
|
|
859
|
+
|
|
860
|
+
- **Kişiye özel çıktı paylaşılmaz.** `storable: false` işaretlenen bir render
|
|
861
|
+
(cookie/`Authorization` okuyan sayfa) Redis'e hiç yazılmaz. Tek prosesteyken
|
|
862
|
+
bile geçerli olan bu kural paylaşımlı kademede daha da kritik: sızması, bir
|
|
863
|
+
kullanıcının HTML'ini tüm kümeye servis etmek olur. `degraded` render ve 200
|
|
864
|
+
dışındaki durum kodları da paylaşılmaz.
|
|
865
|
+
- **Sıkıştırılmış gövdeler varsayılan olarak yerel kalır.** `storeEncoded: true`
|
|
866
|
+
ile açılabilir, ama girdi başına boyutu iki-üç katına çıkarır; brotli'yi
|
|
867
|
+
yeniden üretmek çoğu zaman Redis'ten indirmekten ucuzdur.
|
|
868
|
+
- **Yumuşak invalidation Redis kopyasını siler.** Bayatlatmanın Redis karşılığı
|
|
869
|
+
her anahtar için oku-değiştir-yaz turu demek ve bir webhook binlerce anahtarı
|
|
870
|
+
birden düşürüyor. Silmenin bedeli, o yolu hiç görmemiş bir kopyanın bir kez
|
|
871
|
+
render etmesi; L1'i sıcak olan kopyalar eski HTML'i bayat pencerede servis
|
|
872
|
+
etmeye devam eder.
|
|
873
|
+
- **Yalnızca taze girdi kabul edilir.** Bayat bir kopyayı L1'e almak tazelemeyi
|
|
874
|
+
sonsuza kadar ertelerdi: girdi bayat kalır, her tur yine Redis'i okur ve
|
|
875
|
+
render hiç çalışmaz.
|
|
876
|
+
- **Tutarlılık nihai.** Bir purge ile o purge'ün her kopyaya ulaşması arasında
|
|
877
|
+
kısa bir pencere var. Bu pencerede bir kopya eski HTML'i servis edebilir;
|
|
878
|
+
süresi TTL ile sınırlı.
|
|
879
|
+
- **Dev'de kapalı tutun.** Dev sunucusu manifest her değiştiğinde önbelleği
|
|
880
|
+
boşaltıyor; paylaşımlı bir store bunu anlamsızlaştırır. `enabled` yalnızca
|
|
881
|
+
açıkça `true` verildiğinde açılır.
|
|
882
|
+
|
|
883
|
+
### Durumu görmek
|
|
884
|
+
|
|
885
|
+
```js
|
|
886
|
+
import { getRedisStatus } from "jskelet";
|
|
887
|
+
|
|
888
|
+
app.get("/api/healthcheck", (req, res) => {
|
|
889
|
+
res.json({ ok: true, cache: getRedisStatus() });
|
|
890
|
+
});
|
|
891
|
+
```
|
|
892
|
+
|
|
893
|
+
Bağlantı kurulmamışken de güvenle çağrılır. Dönen nesne
|
|
894
|
+
`{ enabled, connected, keyPrefix, buildId, errors, bypassed }`; `bypassed` devre
|
|
895
|
+
kesicinin açık olduğunu, `errors` toplam komut hatasını gösterir. Aynı özet dev
|
|
896
|
+
panelinin raporunda da var ([09-dev-araclari.md](./09-dev-araclari.md)).
|
|
897
|
+
|
|
898
|
+
İki teşhis yüzeyi daha var:
|
|
899
|
+
|
|
900
|
+
| Çağrı | Ne der |
|
|
901
|
+
| --- | --- |
|
|
902
|
+
| `getRedisDetails()` | Bağlantının **nereye** kurulduğu: adres, TLS, veritabanı, `namespace`, hangi türlerin paylaşıldığı, purge yayınına abone olunup olunmadığı. Şifre asla dönmez — bağlantı URL'i sır taşıyor olabilir. |
|
|
903
|
+
| `inspectRedis()` | Paylaşımlı kademede gerçekten ne durduğu: tür başına anahtar sayısı, `DBSIZE` ve `used_memory`. Bir `SCAN` turu olduğu için **istek yolunda çağrılmaz**; yönetim panelinde de ayrı bir düğmeye bağlı. |
|
|
904
|
+
|
|
905
|
+
Ayarların tam listesi: [07-yapilandirma.md](./07-yapilandirma.md).
|
|
906
|
+
|
|
907
|
+
## Yönetim paneli
|
|
908
|
+
|
|
909
|
+
Yukarıdaki `getHtmlCacheEntries()` / `getRedisStatus()` uçlarını elle yazmak
|
|
910
|
+
yerine framework hazır bir panel taşıyor. Dev overlay'den ayrı bir şey: overlay
|
|
911
|
+
yalnızca `NODE_ENV=development` iken var, panel ise ortama bakmaz — "bu sayfa
|
|
912
|
+
neden bayat", "webhook purge'ü geçti mi", "Redis gerçekten bağlı mı" soruları
|
|
913
|
+
üretimde soruluyor.
|
|
914
|
+
|
|
915
|
+
Panel üst düzey `admin()` ile açılır (`cache()` içinde değil) ve kökü
|
|
916
|
+
`/_jskelet/admin`'dır. Altında Overview, Cache, Routes, Views, Logs ve System
|
|
917
|
+
sayfaları vardır; Cache sayfası eski tek sayfalık panelle aynı işlemleri
|
|
918
|
+
taşır.
|
|
919
|
+
|
|
920
|
+
```js
|
|
921
|
+
// jskelet.config.mjs
|
|
922
|
+
export default {
|
|
923
|
+
admin() {
|
|
924
|
+
return {
|
|
925
|
+
enabled: process.env.JSKELET_ADMIN === "1",
|
|
926
|
+
allowIps: ["10.0.0.0/8"], // boş = IP kısıtı yok
|
|
927
|
+
blockBots: true,
|
|
928
|
+
};
|
|
929
|
+
},
|
|
930
|
+
cache() {
|
|
931
|
+
return {
|
|
932
|
+
html: { "/haber/:slug": 300 },
|
|
933
|
+
};
|
|
934
|
+
},
|
|
935
|
+
};
|
|
936
|
+
```
|
|
937
|
+
|
|
938
|
+
`enabled` verilmedikçe **hiçbir şey mount edilmez**: yol yoktur, modül
|
|
939
|
+
yüklenmez, üretim sürecinde hiçbir maliyeti olmaz. Ortam değişkeni
|
|
940
|
+
(`JSKELET_ADMIN=1`) config'i ezer; panel genelde bir arıza sırasında tek
|
|
941
|
+
seferlik açılıyor ve o an config dosyasını değiştirip yeniden dağıtmak
|
|
942
|
+
istenmiyor.
|
|
943
|
+
|
|
944
|
+
Panel açıldığında sunucu logu şifreyi basar:
|
|
945
|
+
|
|
946
|
+
```
|
|
947
|
+
┌─ ADMIN ──────────────────────────────────────┐
|
|
948
|
+
│ http://localhost:3000/_jskelet/admin │
|
|
949
|
+
│ │
|
|
950
|
+
│ password 3f9c… │
|
|
951
|
+
│ │
|
|
952
|
+
│ Valid until this process restarts. │
|
|
953
|
+
└──────────────────────────────────────────────┘
|
|
954
|
+
```
|
|
955
|
+
|
|
956
|
+
### Erişim ve güvenlik
|
|
957
|
+
|
|
958
|
+
- **Şifre her süreç başlangıcında yeniden üretilir** (32 haneli, onaltılık) ve
|
|
959
|
+
yalnızca logda görünür. Kalıcı bir sır tutulmuyor: sızması önbelleği boşaltma
|
|
960
|
+
yetkisi demek ve bir deploy eski erişimi kendiliğinden iptal etmeli.
|
|
961
|
+
- **Şifre query string ile kabul edilmez.** Erişim logları, tarayıcı geçmişi ve
|
|
962
|
+
`Referer` başlığı sırrı taşımasın; giriş yalnızca form üzerinden.
|
|
963
|
+
- **`allowIps`** verilirse (exact IP veya CIDR) listede olmayan her istek
|
|
964
|
+
login dahil `404` alır.
|
|
965
|
+
- **`blockBots`** (varsayılan `true`) bilinen crawler UA'larını (Googlebot,
|
|
966
|
+
Bingbot, Ahrefs, …) `404` ile reddeder.
|
|
967
|
+
- **Üç başarısız denemede IP 24 saat yasaklanır** (`banAttempts`, `banHours`).
|
|
968
|
+
Yanlış şifre kadar oturumsuz yazma isteği de sayılır; başarılı giriş sayacı
|
|
969
|
+
sıfırlar.
|
|
970
|
+
- **Yasaklı ve yetkisiz her cevap `404`.** 401/403 panelin var olduğunu
|
|
971
|
+
doğrular, 404 hiç yokmuş gibi davranır. Panelin dışındaki site etkilenmez.
|
|
972
|
+
- **Hiçbir yeri indekslenmez:** her cevapta `X-Robots-Tag: noindex, nofollow,
|
|
973
|
+
noarchive, nosnippet`, `Cache-Control: no-store` ve `Referrer-Policy:
|
|
974
|
+
no-referrer`. Yol ayrıca ısıtma listesinden ve gezinme ipuçlarından muaftır.
|
|
975
|
+
- Aksiyonlar `X-JSkelet-Admin` başlığı ister — çapraz siteden gönderilemeyen
|
|
976
|
+
bir başlık, yani panelin kendi CSRF freni.
|
|
977
|
+
- Oturumlar ve yasak sayaçları süreç belleğinde durur; şifresi zaten her
|
|
978
|
+
restart'ta değişen bir panel için diske yazmak yanlış takas olurdu.
|
|
979
|
+
|
|
980
|
+
### Panelde ne var
|
|
981
|
+
|
|
982
|
+
| Bölüm | Gösterdiği |
|
|
983
|
+
| --- | --- |
|
|
984
|
+
| Overview | HTML/data/Redis/prewarm kartları ve upstream freni özeti |
|
|
985
|
+
| Cache | Paylaşımlı kademe, Cloudflare, aksiyonlar, girdi listesi (eski panel) |
|
|
986
|
+
| Routes | Express'e kayıtlı path/method'lar, route modül dosyaları, son istek özeti |
|
|
987
|
+
| Views | `views/` altındaki şablon envanteri |
|
|
988
|
+
| Logs | Canlı SSE kuyruğu; method/status/cache/kind/path ve metin filtresi |
|
|
989
|
+
| System | Makine RAM / disk |
|
|
990
|
+
|
|
991
|
+
Liste **anahtar bazında filtrelenir** ve filtre sunucuda uygulanır: veri
|
|
992
|
+
önbelleğinde on binlerce anahtar olabiliyor. Her istekte en fazla 500 satır
|
|
993
|
+
döner; başlıktaki sayaç kaç eşleşmenin kesildiğini söyler. HTML gövdesi ve veri
|
|
994
|
+
değerleri **hiç dönmez** — panelin işi durumu göstermek, içeriği dışa vermek
|
|
995
|
+
değil.
|
|
996
|
+
|
|
997
|
+
### Panelden yapılabilenler
|
|
998
|
+
|
|
999
|
+
| İşlem | Karşılığı |
|
|
1000
|
+
| --- | --- |
|
|
1001
|
+
| Invalidate (hedef + `hard`) | `invalidateHtmlCache(target, { hard })` |
|
|
1002
|
+
| Tek satırı `drop` | `dropHtmlCacheKey(key)` / `dropDataCacheKey(key)` |
|
|
1003
|
+
| Clear HTML cache | `clearHtmlCache()` |
|
|
1004
|
+
| Clear data cache (önek opsiyonel) | `clearDataCache(prefix)` |
|
|
1005
|
+
| Drop shared keys | Redis'teki `html` ya da `data` isim alanını tarar ve düşürür |
|
|
1006
|
+
| Count keys in Redis | `inspectRedis()` — tür başına anahtar sayısı, `DBSIZE` ve `used_memory` |
|
|
1007
|
+
| Prewarm | `prewarm()` — tur arkada koşar, ilerleme kartta görünür |
|
|
1008
|
+
| Cloudflare purge (her şey / bellekteki URL'ler / prefix / host / tag) | `purgeCloudflare()` |
|
|
1009
|
+
| Cloudflare ayarı ya da özelliği değiştirmek | Zone ayarları ve Tiered Cache / Cache Reserve |
|
|
1010
|
+
|
|
1011
|
+
Hepsi paylaşımlı kademeye de yayılır: tek kopyanın önbelleğini boşaltmak,
|
|
1012
|
+
kümede çalışan bir kurulumda "temizledim ama hâlâ eski" sorusunu üretir.
|
|
1013
|
+
|
|
1014
|
+
Panel iki dilde: header'daki seçici Türkçe ile İngilizce arasında geçiş yapar.
|
|
1015
|
+
İlk açılışta tarayıcının diline bakılır, seçim `localStorage`'da tutulur ve
|
|
1016
|
+
giriş sayfasına da uygulanır. Dil değişimi hiçbir isteğe yol açmaz. Sunucu
|
|
1017
|
+
tarafı arayüz dilini hiç bilmez: `/action` cevabı metin değil bir kod döner
|
|
1018
|
+
(`{ ok, code, params }`) ve cümleyi panel kurar — framework'ün log'u ve API'si
|
|
1019
|
+
tek dilde kalır.
|
|
1020
|
+
|
|
1021
|
+
Listedeki tek satırı silmek `invalidateHtmlCache()`ten farklıdır: o yol
|
|
1022
|
+
desenine bakar ve bir yolun **bütün** query varyantlarını düşürür,
|
|
1023
|
+
`dropHtmlCacheKey()` ise tam anahtarı alır — `/liste?sayfa=2` düşerken
|
|
1024
|
+
`/liste?sayfa=3` sıcak kalır.
|
|
1025
|
+
|
|
1026
|
+
## CDN kademesi: Cloudflare
|
|
1027
|
+
|
|
1028
|
+
Buraya kadar anlatılan her şey **origin** önbelleği. Önünde Cloudflare varsa
|
|
1029
|
+
ziyaretçinin gördüğü HTML çoğu zaman hiç size ulaşmıyor: edge'deki kopya
|
|
1030
|
+
TTL'ini doldurana kadar servis edilir. Bu yüzden `invalidateHtmlCache()`
|
|
1031
|
+
tek başına "sayfayı güncelledim ama eski hâli görünüyor" sorununu çözmez —
|
|
1032
|
+
origin tazelenir, edge beklemeye devam eder.
|
|
1033
|
+
|
|
1034
|
+
JSkelet iki kademeyi aynı yerden yönetilebilir kılar.
|
|
1035
|
+
|
|
1036
|
+
### Kurulum
|
|
1037
|
+
|
|
1038
|
+
Token bir sır; config dosyasına değil ortama yazılır:
|
|
1039
|
+
|
|
1040
|
+
```bash
|
|
1041
|
+
JSKELET_CLOUDFLARE_KEY=... # API token
|
|
1042
|
+
JSKELET_CLOUDFLARE_ZONE_ID=... # zone kimliği
|
|
1043
|
+
JSKELET_CLOUDFLARE_HOSTNAME=example.com # opsiyonel
|
|
1044
|
+
```
|
|
1045
|
+
|
|
1046
|
+
Token'a gereken izinler, yapmak istediğinize göre: purge için `Zone.Cache
|
|
1047
|
+
Purge`, ayarları değiştirmek için `Zone.Zone Settings`, isabet oranı ve edge
|
|
1048
|
+
kırılımı için `Zone.Analytics` (salt okunur). Yalnızca purge izni verilen bir
|
|
1049
|
+
token'la panel açılır, ayar bölümleri hata yazar.
|
|
1050
|
+
|
|
1051
|
+
Zone kimliği ve site adı sır olmadığı için `jskelet.config.mjs` içinden de
|
|
1052
|
+
verilebilir; env her zaman önceliklidir:
|
|
1053
|
+
|
|
1054
|
+
```js
|
|
1055
|
+
cache: {
|
|
1056
|
+
cloudflare: {
|
|
1057
|
+
zoneId: "…",
|
|
1058
|
+
hostname: "example.com", // purge tam URL ister; yol → URL çevrimi için
|
|
1059
|
+
analyticsHours: 24,
|
|
1060
|
+
},
|
|
1061
|
+
}
|
|
1062
|
+
```
|
|
1063
|
+
|
|
1064
|
+
`hostname` verilmezse purge URL'leri panelin açıldığı origin'den türetilir.
|
|
1065
|
+
Paneli iç bir adresten (`http://10.0.0.4:3000`) açıyorsanız bu adresin
|
|
1066
|
+
Cloudflare'de karşılığı yok; o kurulumda `hostname` zorunlu.
|
|
1067
|
+
|
|
1068
|
+
### Ne yapılabilir
|
|
1069
|
+
|
|
1070
|
+
Cloudflare'in cache yüzeyinde ne varsa panelde de var:
|
|
1071
|
+
|
|
1072
|
+
| İşlem | Not |
|
|
1073
|
+
| --- | --- |
|
|
1074
|
+
| Purge everything | Zone'un tamamı. En kaba araç; ısınma maliyeti yüksek |
|
|
1075
|
+
| Purge by URL | Panelin o an bellekte tuttuğu sayfalar tek düğmeyle; ya da satır başına `cf purge` |
|
|
1076
|
+
| Purge by prefix / host / tag | Artık her planda çalışıyor; istek başına 100 anahtar |
|
|
1077
|
+
| Development mode | Üç saat boyunca edge önbelleğini baypas eder, sonra kendiliğinden kapanır |
|
|
1078
|
+
| Cache level, browser cache TTL, query string sıralaması, Always Online | Zone ayarları |
|
|
1079
|
+
| Tiered Cache, Regional Tiered Cache, Cache Reserve | Plana bağlı; kapalı planda "unavailable" görünür |
|
|
1080
|
+
| Clear Cache Reserve | Purge'den ayrı: `purge_everything` edge'i düşürür, R2'deki kalıcı kopya kalır |
|
|
1081
|
+
|
|
1082
|
+
Uzun URL listeleri istek başına 100 anahtarlık partilere bölünür ve **sırayla**
|
|
1083
|
+
gönderilir. Paralel göndermek Free planda dakikada beş isteklik hız freni
|
|
1084
|
+
yüzünden yarısı reddedilen bir tur demek.
|
|
1085
|
+
|
|
1086
|
+
Kod tarafında aynı yüzey:
|
|
1087
|
+
|
|
1088
|
+
```js
|
|
1089
|
+
import { invalidateHtmlCache, purgeCloudflare, toCloudflareUrls } from "jskelet";
|
|
1090
|
+
|
|
1091
|
+
export async function onPostPublished(slug) {
|
|
1092
|
+
const paths = ["/", `/blog/${slug}`];
|
|
1093
|
+
|
|
1094
|
+
invalidateHtmlCache(paths); // origin
|
|
1095
|
+
await purgeCloudflare({ files: toCloudflareUrls(paths) }); // edge
|
|
1096
|
+
}
|
|
1097
|
+
```
|
|
1098
|
+
|
|
1099
|
+
Bu modüldeki hiçbir fonksiyon fırlatmaz: token yoksa, Cloudflare 403 dönerse
|
|
1100
|
+
ya da ağ düşerse sonuç `{ ok: false, error }` olur. Bir CDN arızası içerik
|
|
1101
|
+
yayınlama akışını kesmemeli.
|
|
1102
|
+
|
|
1103
|
+
### "Bu sayfa kaç edge'de cache'li?" — sorulabilen ve sorulamayan
|
|
1104
|
+
|
|
1105
|
+
Cloudflare API'sinde bir objenin **envanterini** veren uç yok. Yüzlerce şehirde
|
|
1106
|
+
birbirinden bağımsız önbellekler var ve hiçbiri "şu an bende bu URL'in kopyası
|
|
1107
|
+
var mı" sorusuna cevap vermiyor. Panel bu yüzden envanter değil **gözlem**
|
|
1108
|
+
gösterir: bir yol girip sorguladığınızda GraphQL analitiğinden son N saatte
|
|
1109
|
+
hangi kolonun (IST, FRA, AMS…) o yolu kaç kez cache'ten, kaç kez origin'den
|
|
1110
|
+
servis ettiği gelir.
|
|
1111
|
+
|
|
1112
|
+
```js
|
|
1113
|
+
const report = await fetchPathEdges({ path: "/blog", hours: 24 });
|
|
1114
|
+
// → { colos: [{ colo: "IST", hits: 7, misses: 2 }, …], hits, misses }
|
|
1115
|
+
```
|
|
1116
|
+
|
|
1117
|
+
Okurken iki sınırı akılda tutun: hiç istek almamış bir edge listede görünmez,
|
|
1118
|
+
kopyası olsa da; ve veri kümesi örneklemeli, yani oranlar güvenilir ama mutlak
|
|
1119
|
+
sayılar yaklaşıktır.
|
|
1120
|
+
|
|
1121
|
+
Seçtiğiniz bir edge'i **ısıtmanın** da yolu yok. Bir obje ancak o koloya
|
|
1122
|
+
yönlenen gerçek bir istekle o edge'in önbelleğine giriyor; sunucudan
|
|
1123
|
+
"Frankfurt'a bunu cache'let" diyemezsiniz. Pratikte yapılabilen üç şey var:
|
|
1124
|
+
|
|
1125
|
+
- **Origin'i ısıtmak** (`prewarm`): ilk isteği alan edge cevabı hazır bulur,
|
|
1126
|
+
o istek yavaşlamaz.
|
|
1127
|
+
- **Tiered Cache**: edge'ler origin'e doğrudan gitmez, aradaki üst katmandan
|
|
1128
|
+
besleniyor; bir şehirdeki ilk istek diğer şehirler için de ısıtma sayılır.
|
|
1129
|
+
- **Cache Reserve**: uzun kuyruklu içerik için R2'de kalıcı kopya; edge
|
|
1130
|
+
düşünce istek origin'e kadar inmiyor.
|
|
1131
|
+
|
|
1132
|
+
`hit` oranınız düşükse önce cevabın cache'lenebilir olup olmadığına bakın:
|
|
1133
|
+
`Cache-Control: private`, `Set-Cookie` ve query string ayarları edge'in
|
|
1134
|
+
cache'lememe kararının en sık sebepleri, ve bu panelde `dynamic` olarak
|
|
1135
|
+
görünür.
|
|
1136
|
+
|
|
1137
|
+
## Prewarm — açılışta veya ziyarette ısıtma
|
|
1138
|
+
|
|
1139
|
+
Next'teki build-time prerender'ın karşılığı, ama çıktı diske yazılmaz: önbellek
|
|
1140
|
+
süreç belleğinde yaşadığı için ısıtma da süreç ayağa kalkınca (klasik mod) ya
|
|
1141
|
+
da trafik geldikçe (`onVisit`) yapılır. Kazanç aynı — tıklanan / komşu sayfa
|
|
1142
|
+
soğuk render'ı beklemez — fakat veri dondurulmaz; her girdi route'un
|
|
1143
|
+
`revalidate` süresiyle yaşlanır ve stale-while-revalidate ile arkada tazelenir.
|
|
1144
|
+
|
|
1145
|
+
Isıtma **gerçek HTTP istekleriyle** yapılır (`http://127.0.0.1:<port>` ya da
|
|
1146
|
+
`cache().prewarm.origins`), çünkü cache anahtarı, sıkıştırma ve middleware
|
|
1147
|
+
zinciri normal trafikle bire bir aynı olsun. `vary.host` açıksa varsayılan
|
|
1148
|
+
loopback yalnızca o host'un anahtarını ısıtır — locale sitelerinde
|
|
1149
|
+
`origins: ["http://localhost", "http://tr.localhost"]` gibi çoklu origin
|
|
1150
|
+
gerekir.
|
|
1151
|
+
|
|
1152
|
+
İki mod **karşılıklı dışlayıcıdır**. `cache().prewarm.onVisit` açıksa klasik
|
|
1153
|
+
alanlar (`max`, `priority`, `rotate`, `intervalSeconds`, …) ve
|
|
1154
|
+
`hooks.prewarmPaths` birlikte verilemez — config yüklenirken hata fırlar.
|
|
1155
|
+
Tersi de geçerli: klasik liste ısıtması kullanıyorsanız `onVisit` yazmayın.
|
|
1156
|
+
|
|
1157
|
+
### Mod: `onVisit` — ziyaret edilen sayfanın linkleri
|
|
1158
|
+
|
|
1159
|
+
Bir kullanıcı herkese açık, önbelleklenebilir bir sayfayı (`public` HTML, 200)
|
|
1160
|
+
aldığında framework yanıt HTML'indeki aynı-origin `<a href>` yollarını (üstten
|
|
1161
|
+
alta, `perPage` kadar) kuyruğa alır ve arka planda ısıtır. Bir sonraki tıklama
|
|
1162
|
+
veya aynı sayfaya gelen başka ziyaretçi çoğu zaman `HIT` görür.
|
|
1163
|
+
|
|
1164
|
+
```js
|
|
1165
|
+
// jskelet.config.mjs
|
|
1166
|
+
export default {
|
|
1167
|
+
async cache() {
|
|
1168
|
+
return {
|
|
1169
|
+
html: { "/": 60, "/haber/:slug": 300 },
|
|
1170
|
+
prewarm: {
|
|
1171
|
+
onVisit: {
|
|
1172
|
+
perPage: 20, // sayfa başına en fazla link; tavan 20
|
|
1173
|
+
concurrency: 2, // tavan 2
|
|
1174
|
+
rps: 2, // tavan 2; 0 da 2'ye çekilir
|
|
1175
|
+
},
|
|
1176
|
+
},
|
|
1177
|
+
};
|
|
1178
|
+
},
|
|
1179
|
+
};
|
|
1180
|
+
```
|
|
1181
|
+
|
|
1182
|
+
`onVisit: true` de yeterlidir (varsayılan `perPage: 20`).
|
|
1183
|
+
|
|
1184
|
+
Kurallar:
|
|
1185
|
+
|
|
1186
|
+
- Yalnızca `route()` ile giden **public + cache'lenebilir** 200 HTML tetikler;
|
|
1187
|
+
`private`, degraded veya `no-store` yanıtlar link çıkarmaz.
|
|
1188
|
+
- Isıtma isteğinin kendi UA'sı (`brand.prewarmUserAgent`) tetiklemez — sonsuz
|
|
1189
|
+
crawl olmaz.
|
|
1190
|
+
- Zaten taze olan yollar kuyruğa girmez. Anahtar `h=host|/yol?` biçimindedir;
|
|
1191
|
+
kontrol vary önekini ve sondaki `?` işaretini de görür. `vary.host` açıkken
|
|
1192
|
+
yalnızca bu isteğin host'u sıcak sayılır.
|
|
1193
|
+
- Bekleyen kuyruk en fazla 64 yoldur; taşan link bu turda alınmaz.
|
|
1194
|
+
- `perPage` 20, `rps` 2, `concurrency` 2 tavanıdır. Daha yükseği (ve `rps: 0`)
|
|
1195
|
+
uyarıyla tavana çekilir. Isıtma isteği loopback'e gider; `vary.host` açıkken
|
|
1196
|
+
public host `x-forwarded-host` ile taşınır, `h=127.0.0.1` diye ikinci girdi
|
|
1197
|
+
açılmaz.
|
|
1198
|
+
- `nofollow`, `target="_blank"`, `data-no-prefetch`, `prewarmSkip` ve
|
|
1199
|
+
`navigation.exclude` Speculation Rules ile aynı muafiyetleri paylaşır.
|
|
1200
|
+
- Query string ısıtılmaz (varsayılan cache politikası query'yi dinamik sayar).
|
|
1201
|
+
- Açılışta otomatik tur yoktur; ilk ziyaretçi o sayfa için hâlâ MISS
|
|
1202
|
+
ödeyebilir. Kritik yolları deploy öncesi sıcak tutmak istiyorsanız klasik
|
|
1203
|
+
modu veya readiness + seed tercih edin.
|
|
1204
|
+
- `PREWARM=0` onVisit'i de kapatır. `PREWARM_MAX` / `PREWARM_INTERVAL_SECONDS`
|
|
1205
|
+
/ `PREWARM_DELAY_MS` / `PREWARM_RETRY_DELAY_MS` onVisit ile birlikte
|
|
1206
|
+
kullanılamaz (hata).
|
|
1207
|
+
|
|
1208
|
+
### Mod: klasik — `hooks.prewarmPaths()`
|
|
1209
|
+
|
|
1210
|
+
Hangi yolların ısıtılacağını uygulama bildirir; genelde sitemap üreten
|
|
1211
|
+
fonksiyonun aynısıdır.
|
|
1212
|
+
|
|
1213
|
+
```js
|
|
1214
|
+
// jskelet.config.mjs
|
|
1215
|
+
export default {
|
|
1216
|
+
hooks: {
|
|
1217
|
+
async prewarmPaths() {
|
|
1218
|
+
const slugs = await getAllArticleSlugs();
|
|
1219
|
+
return ["/", "/piyasalar", ...slugs.map((slug) => `/haber/${slug}`)];
|
|
1220
|
+
},
|
|
1221
|
+
},
|
|
1222
|
+
};
|
|
1223
|
+
```
|
|
1224
|
+
|
|
1225
|
+
Kurallar:
|
|
1226
|
+
|
|
1227
|
+
- Dizi döndürmezse uyarı basılır ve ısıtma yapılmaz.
|
|
1228
|
+
- Yalnızca `/` ile başlayan string'ler alınır.
|
|
1229
|
+
- `prewarmSkip` öneklerinden biriyle başlayanlar atlanır. Varsayılan liste:
|
|
1230
|
+
`/api/`, `/_fragment/`, `/__jskelet/`. Oturuma bağlı sayfalar ısıtılmamalı.
|
|
1231
|
+
- Tekilleştirme **sırayı korur**: `priority` verilmediğinde uygulamanın verdiği
|
|
1232
|
+
sıra anlamlıdır — en önemli sayfaları başa koyun.
|
|
1233
|
+
- Bu hook tanımlı değilse klasik ısıtma hiç kurulmaz; zamanlayıcı bile açılmaz.
|
|
1234
|
+
(`onVisit` modunda hook **yasaktır**, yukarıya bakın.)
|
|
1235
|
+
|
|
1236
|
+
### Tur mantığı (klasik)
|
|
1237
|
+
|
|
1238
|
+
1. Liste toplanır. `max`'tan (varsayılan 400) uzunsa bir dilim seçilir:
|
|
1239
|
+
`priority` eşleşenler **her turda** başa alınır, kalan yerler kuyruktan
|
|
1240
|
+
doldurulur.
|
|
1241
|
+
2. `concurrency` işçi paralel olarak istek atar (prod'da 4, dev'de 1). Dev'de
|
|
1242
|
+
tek işçi: tarama, o an tarayıcıda açtığın sayfanın render'ıyla CPU için
|
|
1243
|
+
yarışmasın.
|
|
1244
|
+
3. `rps` verilmişse tur bu hızın üstüne çıkmaz — paralellik ne olursa olsun.
|
|
1245
|
+
Dev'de varsayılan olarak saniyede 4 istek uygulanır: render tek bir olay
|
|
1246
|
+
döngüsünde çalıştığı için aralıksız bir tur, sayfa isteklerini ve dev
|
|
1247
|
+
panelinin canlı kanalını arkasında bekletiyor.
|
|
1248
|
+
4. **Geçici** hata alan yollar için **tek seri tekrar turu** yapılır
|
|
1249
|
+
(`concurrency: 1`). `400`/`403`/`404` gibi kalıcı cevaplar tekrar turuna
|
|
1250
|
+
girmez: deterministik bir hata tekrar denemekle düzelmez ve o çağrılar
|
|
1251
|
+
kotadan karşılıksız yer. Özette `N not retried (permanent)` olarak görünür.
|
|
1252
|
+
5. Bekleme süresi `retryDelayMs`, ama hız freni açıkken **freni bekleten süre**
|
|
1253
|
+
varsa o kazanır: 10 saniye kapalı kalacak bir devre kesiciden 2 saniye sonra
|
|
1254
|
+
tekrar denemek, aynı 429'u peşin peşin almak olurdu.
|
|
1255
|
+
6. Özet loglanır:
|
|
1256
|
+
`[prewarm] warmed 128/130 pages, 2 failed, 5 recovered on the retry pass (12.4s)`
|
|
1257
|
+
|
|
1258
|
+
Ardından turun upstream'e ne kadar dokunduğu basılır:
|
|
1259
|
+
|
|
1260
|
+
```text
|
|
1261
|
+
[prewarm] 12 upstream calls for 430 data reads (97% from the data cache, 38 coalesced)
|
|
1262
|
+
```
|
|
1263
|
+
|
|
1264
|
+
Bu satır ayarın hangi yönde değişmesi gerektiğini söyleyen tek satır. Oran
|
|
1265
|
+
düşükse çözüm hız freni değil, `withDataCache` TTL'ini tur aralığından uzun
|
|
1266
|
+
tutmak: fren çağrıları yavaşlatır, sayısını azaltmaz. Aynı sayaçlara
|
|
1267
|
+
`getDataCacheStats()` ile ya da dev raporundaki **Data cache** kartından da
|
|
1268
|
+
bakılabilir.
|
|
1269
|
+
|
|
1270
|
+
Tur sırasında oluşan istek hataları ve sayfa başına basılan render uyarıları
|
|
1271
|
+
(`was produced with missing data`, `returned notFound() while upstream is
|
|
1272
|
+
failing`, `could not be produced`) tek tek loglanmaz; sayılır ve tur bitince
|
|
1273
|
+
özetin ardından, en sık görülen türler başta olacak şekilde basılır:
|
|
1274
|
+
|
|
1275
|
+
```text
|
|
1276
|
+
[prewarm] 137 problems were not logged individually:
|
|
1277
|
+
94× missing data, upstream is failing permanently (403 /api/v1/polls)
|
|
1278
|
+
37× missing data, upstream is failing permanently (400 /api/v1/posts)
|
|
1279
|
+
6× 500 Cannot read properties of undefined (reading 'title')
|
|
1280
|
+
```
|
|
1281
|
+
|
|
1282
|
+
Böylece bir anlık upstream arızası "warmed …" satırını yüzlerce yığın izinin
|
|
1283
|
+
altına gömmüyor. Gerçek trafiğin hataları eskisi gibi anında loglanır, tek bir
|
|
1284
|
+
yolun ayrıntısı için dev panelindeki **Prewarming** sekmesine bakılır.
|
|
1285
|
+
|
|
1286
|
+
### Isıtma sırası: `priority`
|
|
1287
|
+
|
|
1288
|
+
```js
|
|
1289
|
+
// jskelet.config.mjs
|
|
1290
|
+
cache: () => ({
|
|
1291
|
+
prewarm: {
|
|
1292
|
+
priority: [
|
|
1293
|
+
"/",
|
|
1294
|
+
"/piyasalar/:path*",
|
|
1295
|
+
/-yorumlar$/,
|
|
1296
|
+
],
|
|
1297
|
+
},
|
|
1298
|
+
}),
|
|
1299
|
+
```
|
|
1300
|
+
|
|
1301
|
+
Desen sözdizimi (`/haber/:slug`) ve doğrudan `RegExp` birlikte kullanılabilir;
|
|
1302
|
+
ikincisi "sonu `-yorumlar` ile bitenler" gibi desen sözdiziminin karşılamadığı
|
|
1303
|
+
kurallar için. Önce yazılan önce ısınır, hiçbirine uymayan yollar kuyruğa gider
|
|
1304
|
+
ve kendi aralarındaki sırayı korur.
|
|
1305
|
+
|
|
1306
|
+
### Damla damla ısıtma: `rotate` + `rps` + `intervalSeconds`
|
|
1307
|
+
|
|
1308
|
+
10.000 yolluk bir sitede tek turda her şeyi ısıtmak ne mümkün (HTML önbelleği
|
|
1309
|
+
500 girdi tutar) ne de doğru (API kotası dolar). Doğru davranış listeyi zamana
|
|
1310
|
+
yaymak:
|
|
1311
|
+
|
|
1312
|
+
```js
|
|
1313
|
+
prewarm: {
|
|
1314
|
+
max: 300, // her turda 300 sayfa
|
|
1315
|
+
rps: 4, // saniyede en fazla 4 istek
|
|
1316
|
+
intervalSeconds: 300, // 5 dakikada bir tur
|
|
1317
|
+
rotate: true, // kuyruk kaldığı yerden devam eder
|
|
1318
|
+
priority: ["/", "/piyasalar/:path*"],
|
|
1319
|
+
}
|
|
1320
|
+
```
|
|
1321
|
+
|
|
1322
|
+
Bu kurulumda öncelikli sayfalar her turda tazelenir, geri kalan kuyruk turlar
|
|
1323
|
+
boyunca baştan sona dolaşılır ve upstream saniyede dört isteğin üstünü hiç
|
|
1324
|
+
görmez. Veri önbelleği ile birlikte kullanıldığında ikinci turdan sonra ısıtma
|
|
1325
|
+
API'ye neredeyse hiç gitmez: veri katmanından okur.
|
|
1326
|
+
|
|
1327
|
+
Rotasyon açıkken sınırın dışında kalan yollar kaybolmuyor, bir sonraki tura
|
|
1328
|
+
kalıyor; log bunu ayırt eder:
|
|
1329
|
+
`… , 700 deferred to the next pass`. `rotate: false` ile klasik davranışa
|
|
1330
|
+
dönülür — her tur listenin aynı ilk dilimini ısıtır ve gerisi hiç ısınmaz
|
|
1331
|
+
(`… , 700 over the limit`).
|
|
1332
|
+
|
|
1333
|
+
Bir tur `intervalSeconds`'tan uzun sürerse yeni tur başlatılmaz; üst üste binen
|
|
1334
|
+
turlar upstream'e iki kat yük bindirirdi.
|
|
1335
|
+
|
|
1336
|
+
İstekler `user-agent: jskelet-prewarm` (`brand.prewarmUserAgent`) ve
|
|
1337
|
+
`accept-encoding: br, gzip` başlıklarıyla gider; ikincisi sıkıştırılmış gövdenin
|
|
1338
|
+
de önbelleğe girmesi için.
|
|
1339
|
+
|
|
1340
|
+
Dev gate açıksa ısıtma token'ı çerez olarak taşır; yoksa gate tüm sayfalara
|
|
1341
|
+
404 döner ve önbellek hiç dolmaz. `DEV_TOKEN` tek başına gate'i açmaz.
|
|
1342
|
+
|
|
1343
|
+
Dev panelindeki istek listesi ve terminal, `prewarmUserAgent` taşıyan istekleri
|
|
1344
|
+
filtreler: yüzlerce ısıtma isteği görünümü doldurmasın. İlerleme baloncuğun
|
|
1345
|
+
yanındaki rozette görünür.
|
|
1346
|
+
|
|
1347
|
+
### Zamanlama
|
|
1348
|
+
|
|
1349
|
+
- Isıtma açılışta **gecikmeyle** başlar: ilk gerçek isteklerle yarışmasın.
|
|
1350
|
+
Varsayılan gecikme prod'da 500 ms, dev'de 3000 ms. Dev'de daha uzun, çünkü
|
|
1351
|
+
dosya kaydı süreci yeniden başlattığı için zamanlayıcı da ölür; yalnızca
|
|
1352
|
+
sunucu bir süre sakin kalınca ısınır.
|
|
1353
|
+
- `PREWARM_INTERVAL_SECONDS` / `cache().prewarm.intervalSeconds` > 0 ise tur
|
|
1354
|
+
periyodik tekrarlanır. Girdiler `revalidate` ile yaşlandığı ve
|
|
1355
|
+
stale-while-revalidate sayesinde ziyaretçi beklemediği için bu **opsiyoneldir**;
|
|
1356
|
+
hiç ziyaret edilmeyen sayfaları da sıcak tutmak isteyen kurulumlar için.
|
|
1357
|
+
- Tüm zamanlayıcılar `unref()` edilmiştir: süreç kapanışını geciktirmezler.
|
|
1358
|
+
- Hiçbir ısıtma hatası süreci düşürmez.
|
|
1359
|
+
|
|
1360
|
+
### Ayarlar
|
|
1361
|
+
|
|
1362
|
+
Öncelik sırası: **ortam değişkeni → config → kod varsayılanı.** Env önde, çünkü
|
|
1363
|
+
tek seferlik deneyler config'i düzenlemeden yapılabilsin.
|
|
1364
|
+
|
|
1365
|
+
| Ayar | Env | `cache().prewarm` | Varsayılan |
|
|
1366
|
+
| --- | --- | --- | --- |
|
|
1367
|
+
| Açık/kapalı | `PREWARM=0` kapatır, `PREWARM=1` config'i ezip açar | `enabled` | `true` |
|
|
1368
|
+
| Tur başına en fazla yol | `PREWARM_MAX` | `max` | `400` |
|
|
1369
|
+
| Paralellik | `PREWARM_CONCURRENCY` | `concurrency` | prod 4, dev 1 |
|
|
1370
|
+
| Saniyedeki istek | `PREWARM_RPS` | `rps` | prod `0` (sınırsız), dev 4 |
|
|
1371
|
+
| Başlangıç gecikmesi (ms) | `PREWARM_DELAY_MS` | `delayMs` | prod 500, dev 3000 |
|
|
1372
|
+
| Tekrar turu gecikmesi (ms) | `PREWARM_RETRY_DELAY_MS` | `retryDelayMs` | `2000` |
|
|
1373
|
+
| Periyot (saniye) | `PREWARM_INTERVAL_SECONDS` | `intervalSeconds` | `0` (kapalı) |
|
|
1374
|
+
| Kuyruk rotasyonu | — | `rotate` | `true` |
|
|
1375
|
+
| Isıtma sırası | — | `priority` | `[]` |
|
|
1376
|
+
|
|
1377
|
+
Sayısal ayarlar yalnızca **pozitif ve sonlu** değer kabul eder; geçersiz bir
|
|
1378
|
+
değer sessizce bir sonraki katmana düşer.
|
|
1379
|
+
|
|
1380
|
+
### Elle tetikleme
|
|
1381
|
+
|
|
1382
|
+
```js
|
|
1383
|
+
import { prewarm, prewarmProgress } from "jskelet";
|
|
1384
|
+
|
|
1385
|
+
await prewarm({ origin: "http://127.0.0.1:3000" }); // hook'tan yollar
|
|
1386
|
+
await prewarm({ origin, paths: ["/", "/piyasalar"] }); // yalnızca bu yollar
|
|
1387
|
+
await prewarm({ origin, quiet: true }); // özet basmadan
|
|
1388
|
+
```
|
|
1389
|
+
|
|
1390
|
+
`paths` verilirse hook hiç çağrılmaz. Dönüş değeri
|
|
1391
|
+
`{ ok, failed, total, elapsed }`.
|
|
1392
|
+
|
|
1393
|
+
`prewarmProgress` canlı durumu tutar ve dev paneli bunu okur:
|
|
1394
|
+
|
|
1395
|
+
```js
|
|
1396
|
+
{
|
|
1397
|
+
active, done, total, ok, failed, startedAt, finishedAt,
|
|
1398
|
+
entries: [{ path, status, ms, bytes, cache, error }],
|
|
1399
|
+
}
|
|
1400
|
+
```
|
|
1401
|
+
|
|
1402
|
+
`entries` içindeki `cache` alanı o yolun `X-JSkelet-Cache` yanıtıdır; ısıtma
|
|
1403
|
+
turunun gerçekten `MISS` → önbellek doldurup doldurmadığını buradan görürsünüz.
|
|
1404
|
+
|
|
1405
|
+
## Teşhis: sık görülen durumlar
|
|
1406
|
+
|
|
1407
|
+
- **Her istek `MISS` dönüyor.** Route'a `revalidate` verilmemiş ya da
|
|
1408
|
+
`cache().html` içindeki desen 0 saniye veriyor. Veya sayfa `status: 200`
|
|
1409
|
+
dışında bir kod dönüyor.
|
|
1410
|
+
- **Sayfa `MISS` dönüyor ama upstream sağlam.** Geçici bir upstream hatası
|
|
1411
|
+
bildirilmiş olabilir; logda `was produced with missing data, not caching it`
|
|
1412
|
+
satırını arayın.
|
|
1413
|
+
- **Sürekli eski veri.** `revalidate` çok yüksek; unutmayın ki gerçek gecikme en
|
|
1414
|
+
fazla `revalidate` + bir tazeleme turudur.
|
|
1415
|
+
- **Önbellek şişiyor.** Query parametreleri anahtara girdiği için kampanya
|
|
1416
|
+
parametreleri girdi çoğaltıyor olabilir.
|
|
1417
|
+
- **Yanlış dil / host HTML'i geliyor.** Host'tan locale üreten bir sitede
|
|
1418
|
+
`cache().vary.host: true` yoksa ilk locale'in HTML'i diğer host'a servis
|
|
1419
|
+
edilir. Prewarm yalnızca `127.0.0.1` ile ısınıyorsa `prewarm.origins` ile
|
|
1420
|
+
locale host'larını ekleyin.
|
|
1421
|
+
- **Isıtma hiç çalışmıyor.** Klasik modda `hooks.prewarmPaths` tanımlı değil,
|
|
1422
|
+
`PREWARM=0` ayarlı ya da `cache().prewarm.enabled === false`. `onVisit`
|
|
1423
|
+
modunda `listen` sonrası logda `onVisit mode` satırını ve public cache'li
|
|
1424
|
+
bir sayfa gezildiğini doğrulayın.
|
|
1425
|
+
- **Config `onVisit` + `max` / `prewarmPaths` ile düşüyor.** İki mod karşılıklı
|
|
1426
|
+
dışlayıcı; yalnızca birini kullanın.
|
|
1427
|
+
- **Isıtma turu API'yi 429'a sokuyor.** `rps` verilmemiş. `concurrency`
|
|
1428
|
+
düşürmek yeterli değil; kotayı koruyan ayar toplam hız. Kalıcı çözüm veri
|
|
1429
|
+
önbelleği: ikinci turdan sonra ısıtma upstream'e gitmez.
|
|
1430
|
+
- **Isıtma listesi `max`'tan uzun ve sonu hiç ısınmıyor.** `rotate: false`
|
|
1431
|
+
olabilir; logdaki `over the limit` ifadesi bunu gösterir.
|
|
1432
|
+
- **Bir bölümün tamamı 404 dönüyor.** Upstream düşmüş olabilir. Artık bu durumda
|
|
1433
|
+
sayfa bir kez daha denenir, olmazsa 404 değil önbelleğe girmeyen 503 döner;
|
|
1434
|
+
logda `returned notFound() while upstream is failing` satırını arayın. Hâlâ
|
|
1435
|
+
404 görüyorsanız hata `fetch` dışı bir istemciden geliyor olabilir
|
|
1436
|
+
(`reportUpstreamFailure()` gerekir) ya da `cache().trackUpstream` kapatılmış.
|
|
1437
|
+
|
|
1438
|
+
## Sırada ne var
|
|
1439
|
+
|
|
1440
|
+
- Config alanlarının tam referansı ve env tablosu:
|
|
1441
|
+
[07-yapilandirma.md](./07-yapilandirma.md)
|
|
1442
|
+
- Önbelleği dev panelinden izlemek: [09-dev-araclari.md](./09-dev-araclari.md)
|
|
1443
|
+
- CDN/ters proxy ile birlikte kullanım: [10-dagitim.md](./10-dagitim.md)
|