jskelet 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +87 -4
- package/docs/06-cache.md +255 -20
- package/docs/07-yapilandirma.md +75 -6
- package/docs/en/06-caching.md +264 -23
- package/docs/en/07-configuration.md +78 -6
- package/package.json +1 -1
- package/src/config/defaults.js +63 -0
- package/src/config/index.js +82 -5
- package/src/index.js +7 -0
- package/src/server/create-app.js +6 -0
- package/src/server/data-cache.js +244 -0
- package/src/server/dev/report.js +8 -1
- package/src/server/html-cache.js +22 -2
- package/src/server/prewarm.js +159 -14
- package/src/server/render.js +155 -29
- package/src/server/upstream-tracking.js +103 -13
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,68 @@ one is listed under a **Breaking** heading.
|
|
|
10
10
|
|
|
11
11
|
### Added
|
|
12
12
|
|
|
13
|
+
- An upstream data cache: `withDataCache(key, ttlSeconds, producer)` and the
|
|
14
|
+
`dataCache(fn, { key, revalidate })` wrapper, with `clearDataCache(prefix?)`,
|
|
15
|
+
`getDataCacheSize()` and `getDataCacheEntries()` alongside them. It keeps JSON
|
|
16
|
+
rather than HTML, so its default limit is 10,000 entries: a long-tail page that
|
|
17
|
+
was never prewarmed still renders without touching the API. Concurrent reads of
|
|
18
|
+
the same key collapse into one upstream request, an expired entry is served
|
|
19
|
+
immediately while it refreshes in the background, a failing producer falls back
|
|
20
|
+
to the stale value, and empty answers (`null`/`undefined`) are not stored
|
|
21
|
+
unless `storeEmpty: true` is passed.
|
|
22
|
+
- `cache().prewarm.priority` decides the warm-up order and accepts both the
|
|
23
|
+
config pattern syntax (`/news/:slug`) and plain regular expressions. Matching
|
|
24
|
+
paths are warmed on every pass.
|
|
25
|
+
- Drip prewarming for large sites: `cache().prewarm.rps` (also `PREWARM_RPS`)
|
|
26
|
+
caps requests per second regardless of parallelism, and `rotate` (on by
|
|
27
|
+
default) makes periodic passes continue through the queue where the previous
|
|
28
|
+
one stopped instead of re-warming the same first slice. A pass is skipped while
|
|
29
|
+
the previous one is still running.
|
|
30
|
+
- `cache().prewarm.retryDelayMs` (also `PREWARM_RETRY_DELAY_MS`) waits before the
|
|
31
|
+
retry pass, since rate limit windows are measured in seconds.
|
|
32
|
+
- `cache().maxEntries` configures the HTML cache limit, which used to be a fixed
|
|
33
|
+
500.
|
|
34
|
+
- Transient upstream failures are now detected without any application code:
|
|
35
|
+
`globalThis.fetch` is wrapped during startup and `429`, `5xx` and network
|
|
36
|
+
errors raised inside a render are reported on their own, so rate limits stop
|
|
37
|
+
turning existing pages into 404s even when the data layer never calls
|
|
38
|
+
`reportUpstreamFailure()`. Requests outside a render and requests to the
|
|
39
|
+
server itself are ignored, deterministic answers such as `404` are not
|
|
40
|
+
reported, and the wrapper can be turned off with `cache().trackUpstream:
|
|
41
|
+
false`.
|
|
42
|
+
- `cache().transientRetry` (`{ attempts: 1, delayMs: 300 }` by default) retries a
|
|
43
|
+
page that called `notFound()` while upstream was failing. Each attempt runs in
|
|
44
|
+
a fresh upstream and per-request cache scope, so a page whose data arrives on
|
|
45
|
+
the second try is served and cached as usual instead of degrading to an error.
|
|
46
|
+
- The dev report now includes the data cache entry count under `cache.data`.
|
|
47
|
+
|
|
48
|
+
### Changed
|
|
49
|
+
|
|
50
|
+
- `notFound()` is no longer served as a 404 when a transient upstream failure
|
|
51
|
+
(`429`, `5xx`, network error) happened during the same render. The page is
|
|
52
|
+
retried first and, if upstream is still failing, responds with an uncached
|
|
53
|
+
`503` and `Retry-After`. A temporary rate limit is no longer frozen into "this
|
|
54
|
+
page does not exist" for the whole TTL. A retry that gets a clean answer saying
|
|
55
|
+
the page is gone still returns a normal 404.
|
|
56
|
+
- Responses produced with missing data are no longer offered to shared caches:
|
|
57
|
+
a `degraded` render is sent with `private, no-store` instead of
|
|
58
|
+
`public, s-maxage=…`. The `X-JSkelet-Cache` diagnostic header is still written.
|
|
59
|
+
- The prewarm summary distinguishes paths left for the next pass
|
|
60
|
+
(`700 deferred to the next pass`) from paths dropped entirely
|
|
61
|
+
(`700 over the limit`).
|
|
62
|
+
- The changelog page of the marketing example is generated from the project's
|
|
63
|
+
`CHANGELOG.md` instead of a hand-written list, and shows the version published
|
|
64
|
+
on npm next to the installed one.
|
|
65
|
+
- The marketing example reads its markdown (documentation and changelog) from
|
|
66
|
+
the repository over GitHub's raw endpoint, falling back to the installed
|
|
67
|
+
package when the network is unavailable, so a deployment that ships without
|
|
68
|
+
`node_modules` can still serve the docs. In development the local file wins
|
|
69
|
+
and nothing is cached. The branch is overridable with `DOCS_REF`.
|
|
70
|
+
|
|
71
|
+
## [0.1.2] - 2026-08-30
|
|
72
|
+
|
|
73
|
+
### Added
|
|
74
|
+
|
|
13
75
|
- `route(fn, { private: true })` for pages that depend on the visitor. The HTML
|
|
14
76
|
cache is bypassed, `cache.html` patterns can no longer turn caching on for
|
|
15
77
|
that route, and the response is sent with `private, no-store`, `Vary: Cookie`
|
|
@@ -42,8 +104,6 @@ one is listed under a **Breaking** heading.
|
|
|
42
104
|
a private page, a paginated table fragment, a CSRF-protected mutation and an
|
|
43
105
|
island with cleanup, covered by its own `smoke.mjs`.
|
|
44
106
|
- An npm version badge in the `README`, linking to the package page.
|
|
45
|
-
- English `README`, plus `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`, `SECURITY.md`,
|
|
46
|
-
a `LICENSE` file, issue and pull request templates, and a CI workflow.
|
|
47
107
|
- An English edition of the documentation under `docs/en/`, mirroring every
|
|
48
108
|
chapter of the Turkish `docs/`.
|
|
49
109
|
- The dev overlay now compares the installed version against the `latest` tag on
|
|
@@ -76,7 +136,28 @@ one is listed under a **Breaking** heading.
|
|
|
76
136
|
either; a stored "you need to sign in" redirect used to follow the visitor
|
|
77
137
|
even after signing in.
|
|
78
138
|
|
|
79
|
-
## [0.1.
|
|
139
|
+
## [0.1.1] - 2026-08-30
|
|
140
|
+
|
|
141
|
+
### Added
|
|
142
|
+
|
|
143
|
+
- English `README`, plus `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`, `SECURITY.md`,
|
|
144
|
+
a `LICENSE` file, issue and pull request templates, and a CI workflow.
|
|
145
|
+
- An English-first `examples/marketing` with a Turkish translation, serving the
|
|
146
|
+
package documentation under `/docs` and reading its version, dependencies and
|
|
147
|
+
bundle sizes from the installed package.
|
|
148
|
+
|
|
149
|
+
### Changed
|
|
150
|
+
|
|
151
|
+
- The install instructions point at the npm package instead of the git
|
|
152
|
+
repository.
|
|
153
|
+
|
|
154
|
+
### Fixed
|
|
155
|
+
|
|
156
|
+
- No more white flash between pages: the page background moved onto the root
|
|
157
|
+
element, so it applies before the body paints. Reduced-motion preferences now
|
|
158
|
+
switch off the decorative animations as well, not just page transitions.
|
|
159
|
+
|
|
160
|
+
## [0.1.0] - 2026-08-30
|
|
80
161
|
|
|
81
162
|
Initial release.
|
|
82
163
|
|
|
@@ -99,5 +180,7 @@ Initial release.
|
|
|
99
180
|
- Documentation under `docs/` and three examples: `minimal`, `blog`,
|
|
100
181
|
`marketing`.
|
|
101
182
|
|
|
102
|
-
[Unreleased]: https://github.com/ayberkenis/jskelet/compare/v0.1.
|
|
183
|
+
[Unreleased]: https://github.com/ayberkenis/jskelet/compare/v0.1.2...HEAD
|
|
184
|
+
[0.1.2]: https://github.com/ayberkenis/jskelet/compare/v0.1.1...v0.1.2
|
|
185
|
+
[0.1.1]: https://github.com/ayberkenis/jskelet/compare/v0.1.0...v0.1.1
|
|
103
186
|
[0.1.0]: https://github.com/ayberkenis/jskelet/releases/tag/v0.1.0
|
package/docs/06-cache.md
CHANGED
|
@@ -4,8 +4,9 @@ Bu belge JSkelet'in ISR ikamesini bütün ayrıntılarıyla anlatır: HTML TTL
|
|
|
4
4
|
önbelleği ve stale-while-revalidate davranışı, `revalidate`'in nereden geldiği,
|
|
5
5
|
cache anahtarının nasıl kurulduğu, `X-JSkelet-Cache` başlığının değerleri,
|
|
6
6
|
sıkıştırılmış gövdenin neden önbellekte durduğu, istek içi memoizasyon
|
|
7
|
-
(`withRequestCache` / `cache()`),
|
|
8
|
-
|
|
7
|
+
(`withRequestCache` / `cache()`), veri önbelleği (`withDataCache`), upstream
|
|
8
|
+
hatalarının önbelleği nasıl etkilediği (otomatik izleme ve
|
|
9
|
+
`reportUpstreamFailure`) ve sunucu açılışındaki ısıtma turu.
|
|
9
10
|
Kararların arkasındaki ölçüm gerekçeleri [02-mimari.md](./02-mimari.md)'de,
|
|
10
11
|
config alanlarının tam referansı [07-yapilandirma.md](./07-yapilandirma.md)'de.
|
|
11
12
|
|
|
@@ -17,12 +18,28 @@ route(controller, { revalidate })
|
|
|
17
18
|
└─ withUpstreamTracking(...) ← eksik veri tespiti
|
|
18
19
|
└─ withRequestCache(...) ← istek içi memoizasyon
|
|
19
20
|
└─ produce() → controller + renderPage
|
|
21
|
+
└─ withDataCache(...) ← upstream veri önbelleği
|
|
20
22
|
```
|
|
21
23
|
|
|
22
24
|
Sıra önemli: **istek içi cache en içte** olmalı ki aynı render'daki iki çağrı
|
|
23
25
|
tek upstream isteğine düşsün; **upstream takibi HTML cache'in içinde** olmalı ki
|
|
24
26
|
eksik veriyle üretilen çıktı önbelleğe yazılmasın.
|
|
25
27
|
|
|
28
|
+
İki önbelleğin iş bölümü:
|
|
29
|
+
|
|
30
|
+
| | HTML önbelleği | Veri önbelleği |
|
|
31
|
+
| --- | --- | --- |
|
|
32
|
+
| Ne tutar | Sayfanın tamamı (+ sıkıştırılmış gövdesi) | Upstream'den gelen JSON |
|
|
33
|
+
| Girdi boyutu | ~100-200 kB | ~1-20 kB |
|
|
34
|
+
| Girdi sınırı | 500 (`cache().maxEntries`) | 10.000 (`cache().data.maxEntries`) |
|
|
35
|
+
| Kime yarar | Trafiği olan sayfalar: render bile edilmez | Uzun kuyruk: render edilir ama API'ye gidilmez |
|
|
36
|
+
|
|
37
|
+
Bu ayrım pratikte şuna karşılık gelir: on binlerce yollu bir sitede sayfaların
|
|
38
|
+
tamamını HTML olarak sıcak tutmak mümkün değil — 500 girdiyi aşan ısıtma kendi
|
|
39
|
+
ısıttığını siler. Uzun kuyruk için hedef "HTML hazır olsun" değil, **"sayfayı
|
|
40
|
+
üretecek veri API'ye gitmeden bulunsun"** olmalı. O zaman hiç ısıtılmamış bir
|
|
41
|
+
sayfa da ilk ziyaretçide milisaniyeler içinde üretilir ve kota harcamaz.
|
|
42
|
+
|
|
26
43
|
## Public ve kişiye özel ayrımı
|
|
27
44
|
|
|
28
45
|
Bu belgedeki her şey **herkese aynı gidebilen** HTML için geçerli. Cache
|
|
@@ -102,8 +119,8 @@ ile `/liste?sayfa=3` ayrı girdilerdir.
|
|
|
102
119
|
Bunun pratik sonucu: query string'e bağlı olmayan bir sayfa, farklı kampanya
|
|
103
120
|
parametreleriyle (`?utm_source=…`) çağrıldığında her kombinasyon için ayrı bir
|
|
104
121
|
girdi üretir. Bu tür parametreleri ters proxy katmanında temizlemek ya da
|
|
105
|
-
önbelleği kapatmak (`revalidate` vermemek) makul bir önlemdir; store
|
|
106
|
-
500 girdi tutar ve LRU ile en eskiyi düşürür.
|
|
122
|
+
önbelleği kapatmak (`revalidate` vermemek) makul bir önlemdir; store varsayılan
|
|
123
|
+
olarak en fazla 500 girdi tutar ve LRU ile en eskiyi düşürür.
|
|
107
124
|
|
|
108
125
|
## Stale-while-revalidate
|
|
109
126
|
|
|
@@ -134,8 +151,8 @@ veri en fazla `revalidate + bir tazeleme turu` kadar geride olabilir. Bu bedel
|
|
|
134
151
|
kabul edilebilir, çünkü fiyat gibi canlı alanlar istemcide WebSocket'ten
|
|
135
152
|
güncelleniyor.
|
|
136
153
|
|
|
137
|
-
Store LRU'dur: erişilen girdi sona taşınır,
|
|
138
|
-
eski düşürülür.
|
|
154
|
+
Store LRU'dur: erişilen girdi sona taşınır, sınır (`cache().maxEntries`,
|
|
155
|
+
varsayılan 500) aşılınca en eski düşürülür.
|
|
139
156
|
|
|
140
157
|
## Ne önbelleğe yazılır
|
|
141
158
|
|
|
@@ -216,15 +233,116 @@ Ayrıntılar:
|
|
|
216
233
|
- `withRequestCache(run)` dışa açıktır; `route()` dışında (ör. kendi yazdığınız
|
|
217
234
|
bir Express handler'ında) aynı kapsamı kurmak için kullanılabilir.
|
|
218
235
|
|
|
236
|
+
## İstekler arası veri önbelleği: `withDataCache`
|
|
237
|
+
|
|
238
|
+
`cache()` yalnızca **tek bir istek** boyunca yaşar. Uzun kuyruğu API kotasından
|
|
239
|
+
korumak için gereken şey istekler arasında yaşayan, TTL'li ve kendi kendini
|
|
240
|
+
tazeleyen bir veri katmanı:
|
|
241
|
+
|
|
242
|
+
```js
|
|
243
|
+
// lib/api/articles.js
|
|
244
|
+
import { withDataCache, reportUpstreamFailure } from "jskelet";
|
|
245
|
+
|
|
246
|
+
export async function getArticle(slug) {
|
|
247
|
+
return withDataCache(`haber:${slug}`, 600, async () => {
|
|
248
|
+
const response = await fetch(`${process.env.API_ORIGIN}/articles/${slug}`);
|
|
249
|
+
|
|
250
|
+
if (!response.ok) {
|
|
251
|
+
reportUpstreamFailure({ status: response.status, path: `/articles/${slug}` });
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return response.json();
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Aynı kalıbın sarmalayıcı biçimi — anahtar argümanlardan üretilir:
|
|
261
|
+
|
|
262
|
+
```js
|
|
263
|
+
import { dataCache } from "jskelet";
|
|
264
|
+
|
|
265
|
+
export const getArticle = dataCache(
|
|
266
|
+
async (slug) => apiGet(`/articles/${slug}`),
|
|
267
|
+
{ key: "haber", revalidate: 600 },
|
|
268
|
+
);
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Davranış:
|
|
272
|
+
|
|
273
|
+
| Durum | Sonuç |
|
|
274
|
+
| --- | --- |
|
|
275
|
+
| Taze girdi | Anında döner, `producer` çalışmaz |
|
|
276
|
+
| TTL geçmiş, bayat pencere sürüyor | Bayat değer **anında** döner, tazeleme arkada yürür |
|
|
277
|
+
| Girdi yok | `producer` beklenir |
|
|
278
|
+
| `producer` hata verdi, bayat girdi var | Bayat değer döner, uyarı: `[data-cache] producer failed, serving stale value: …` |
|
|
279
|
+
| `producer` hata verdi, girdi yok | Hata çağırana gider |
|
|
280
|
+
|
|
281
|
+
Ayrıntılar:
|
|
282
|
+
|
|
283
|
+
- **Aynı anahtarı eşzamanlı isteyen çağrılar tek upstream isteğine düşer.**
|
|
284
|
+
Isıtma turlarında kotayı en çok kurtaran davranış bu: 50 sayfa aynı endeks
|
|
285
|
+
verisini istiyorsa API bir kez çağrılır.
|
|
286
|
+
- **`null` ve `undefined` saklanmaz.** Uygulamaların HTTP istemcisi hatada
|
|
287
|
+
genellikle `null` döner; bunu saklamak geçici bir 429'u TTL boyunca "veri yok"
|
|
288
|
+
hâline dondurmak olurdu. Boş cevabı bilinçli olarak saklamak isteyen
|
|
289
|
+
`{ storeEmpty: true }` verir.
|
|
290
|
+
- **Bayat pencere HTML'dekinden uzun**: `staleFactor` varsayılanı 10, yani girdi
|
|
291
|
+
TTL'inin 11 katı boyunca acil durum yedeği olarak kalır. Bayat veri, eksik
|
|
292
|
+
sayfadan iyidir. Anahtar başına `{ staleFactor: 0 }` ile kapatılabilir.
|
|
293
|
+
- Anahtar tamamen uygulamanın: dil, sürüm, sayfa numarası gibi ayrımlar anahtara
|
|
294
|
+
yazılır (`haber:tr:v2:${slug}`).
|
|
295
|
+
- TTL `0` verildiğinde önbellek devre dışı kalır ve `producer` her çağrıda
|
|
296
|
+
çalışır — bir ayarı geçici olarak kapatmak için yeterli.
|
|
297
|
+
|
|
298
|
+
Yönetim yüzeyi:
|
|
299
|
+
|
|
300
|
+
| Fonksiyon | Ne yapar |
|
|
301
|
+
| --- | --- |
|
|
302
|
+
| `withDataCache(key, ttlSeconds, producer, options?)` | Ana giriş noktası |
|
|
303
|
+
| `dataCache(fn, { key, revalidate, … })` | Fonksiyon sarmalayıcısı |
|
|
304
|
+
| `clearDataCache(prefix?)` | Önek eşleşen girdileri (ya da tümünü) düşürür, silinen sayısını döner |
|
|
305
|
+
| `getDataCacheSize()` | Girdi sayısı |
|
|
306
|
+
| `getDataCacheEntries()` | Döküm: `{ key, stale, expiresIn }`. Değerin kendisi dönmez. |
|
|
307
|
+
|
|
308
|
+
`clearDataCache("haber:")`, "bu içerik güncellendi" webhook'unun karşılığıdır:
|
|
309
|
+
tek bir bölümün verisini düşürüp HTML'in bir sonraki tazelemesinde yeni içeriği
|
|
310
|
+
almasını sağlar.
|
|
311
|
+
|
|
219
312
|
## Degraded render: `reportUpstreamFailure`
|
|
220
313
|
|
|
221
314
|
Render sırasında upstream düştüyse çıktı eksik veri içeriyor demektir. Böyle bir
|
|
222
315
|
HTML'i tüm TTL boyunca servis etmek yerine önbelleğe **hiç yazmamak** doğru
|
|
223
316
|
davranış: sonraki istek yeniden dener.
|
|
224
317
|
|
|
318
|
+
Bu bilgi iki yoldan gelir.
|
|
319
|
+
|
|
320
|
+
### Otomatik izleme (varsayılan)
|
|
321
|
+
|
|
322
|
+
`createApp()` açılışta `globalThis.fetch`i sarar ve render sırasında yapılan
|
|
323
|
+
çağrılardaki **geçici** hataları (`429`, `5xx`, ağ hatası) kendiliğinden
|
|
324
|
+
bildirir. Uygulama tarafında hiçbir satır gerekmez; `fetch` ile konuşan bir API
|
|
325
|
+
istemcisi varsa rate limit koruması hazırdır.
|
|
326
|
+
|
|
327
|
+
Ayrıntılar:
|
|
328
|
+
|
|
329
|
+
- Yalnızca bir render bağlamı içindeki çağrılar sayılır. Script'ten, cron'dan
|
|
330
|
+
ya da istek dışı bir yerden yapılan `fetch` dokunulmaz kalır.
|
|
331
|
+
- Kendi sunucumuza yapılan istekler (`localhost`, `127.0.0.1`) atlanır: ısıtma
|
|
332
|
+
turu ve sağlık kontrolü upstream değildir.
|
|
333
|
+
- `404`/`403` gibi deterministik cevaplar **otomatik olarak bildirilmez**. Çoğu
|
|
334
|
+
API'de `404` "böyle bir kayıt yok" demektir; onu eksik veri saymak her yok
|
|
335
|
+
sayfasında yanlış uyarı üretirdi.
|
|
336
|
+
- Kapatmak için `cache().trackUpstream: false`. `fetch`i kendisi saran bir
|
|
337
|
+
uygulama (ölçüm, retry, circuit breaker) bunu tercih edebilir.
|
|
338
|
+
|
|
339
|
+
### Elle bildirim
|
|
340
|
+
|
|
341
|
+
`fetch` kullanmayan bir istemci (veritabanı sürücüsü, gRPC, satıcı SDK'sı) ya da
|
|
342
|
+
kalıcı hataları da işaretlemek isteyen bir katman için sözleşme aynı kaldı.
|
|
225
343
|
Bağımlılık yönü bilinçli olarak tersine çevrilmiştir: framework veri katmanını
|
|
226
344
|
tanımaz, veri katmanı framework'e haber verir. Hiç çağıran olmazsa maliyet boş
|
|
227
|
-
bir dizidir.
|
|
345
|
+
bir dizidir. Aynı hata her iki yoldan bildirilirse tekilleştirilir.
|
|
228
346
|
|
|
229
347
|
```js
|
|
230
348
|
// lib/api/client.js
|
|
@@ -260,6 +378,58 @@ denemekle düzelmez. Onlar yüzünden önbelleği kapatmak sayfayı her ziyarett
|
|
|
260
378
|
baştan render etmek olur — içerik yine aynı eksik hâliyle döner, ziyaretçi
|
|
261
379
|
sadece render süresini öder.
|
|
262
380
|
|
|
381
|
+
Eksik veriyle üretilen çıktı **paylaşılan önbelleklere de sunulmaz**: `degraded`
|
|
382
|
+
bir yanıt `public, s-maxage=…` değil `private, no-store` alır. Süreç içi
|
|
383
|
+
önbelleğe yazmama kararını CDN'de geri almak, aynı hatayı bir katman yukarıda
|
|
384
|
+
tekrarlamak olurdu. Teşhis başlığı (`X-JSkelet-Cache: MISS`) yine yazılır.
|
|
385
|
+
|
|
386
|
+
### `notFound()` geçici hataya denk gelirse
|
|
387
|
+
|
|
388
|
+
Veri gelmediği için `notFound()` çağıran bir controller, upstream rate limit'e
|
|
389
|
+
girdiğinde tüm siteyi 404'e çevirebilir — ve bu 404'ler önbelleğe girdiği için
|
|
390
|
+
geçici bir kota sorunu TTL boyunca "bu sayfa yok" cevabına dönüşür. Arama motoru
|
|
391
|
+
için bu kalıcı bir kayıp.
|
|
392
|
+
|
|
393
|
+
Framework bu durumu ayırır: render sırasında **geçici** bir upstream hatası
|
|
394
|
+
varsa `notFound()` 404 olarak servis edilmez. Sırayla:
|
|
395
|
+
|
|
396
|
+
1. Sayfa kısa bir beklemeden sonra **yeniden denenir** (varsayılan bir kez,
|
|
397
|
+
300 ms sonra). Deneme kendi upstream ve istek içi cache bağlamında koşar;
|
|
398
|
+
ilk turun hatası da memoize edilmiş boş cevabı da ikinci turu etkilemez.
|
|
399
|
+
2. İkinci tur sayfayı üretebilirse ziyaretçi **gerçek içeriği** görür ve çıktı
|
|
400
|
+
normal şekilde önbelleğe girer. Isıtma günlükleri bunun sık olduğunu
|
|
401
|
+
gösteriyor: aynı yol saniyeler sonra 200 dönüyor.
|
|
402
|
+
3. Denemeler tükendiyse yanıt `503` olur — önbelleğe girmez, `Retry-After`
|
|
403
|
+
taşır, sonraki istek yine gerçek içeriği üretebilir.
|
|
404
|
+
|
|
405
|
+
| Render sırasında | `notFound()` sonucu |
|
|
406
|
+
| --- | --- |
|
|
407
|
+
| Geçici hata var (`429`, `5xx`, ağ hatası) | Tekrar dene → başarılıysa sayfa; hâlâ olmuyorsa `503`, `Retry-After: 30`, `no-store` |
|
|
408
|
+
| Tekrar denemede upstream sağlam cevap verip "yok" dedi | Normal `404` |
|
|
409
|
+
| Kalıcı hata var (`404`, `403`…) ya da hata yok | Normal `404`, tekrar denenmez |
|
|
410
|
+
|
|
411
|
+
Log satırları:
|
|
412
|
+
|
|
413
|
+
```
|
|
414
|
+
[render] /haber/x returned notFound() while upstream is failing (429 /api/...), retrying (1/1)
|
|
415
|
+
[render] /haber/x could not be produced, upstream is still failing (429 /api/...), serving an uncached 503 instead of a 404
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
Yani **var olan bir sayfa hiçbir koşulda 404'e dönüşmez**: ya gerçek içerik
|
|
419
|
+
gelir, ya önbelleğe girmeyen bir 503. Hiçbir şey "yok" olarak dondurulmaz.
|
|
420
|
+
|
|
421
|
+
Tekrar denemenin maliyeti upstream'e binen ikinci bir istek turudur; bu yüzden
|
|
422
|
+
varsayılan tek deneme. Ayar `cache().transientRetry`:
|
|
423
|
+
|
|
424
|
+
```js
|
|
425
|
+
cache: {
|
|
426
|
+
transientRetry: { attempts: 2, delayMs: 500 },
|
|
427
|
+
}
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
`transientRetry: false` (ya da `attempts: 0`) tekrarı kapatır ve doğrudan 503'e
|
|
431
|
+
düşer.
|
|
432
|
+
|
|
263
433
|
## Önbelleği yönetmek
|
|
264
434
|
|
|
265
435
|
`jskelet` şu fonksiyonları dışa açar:
|
|
@@ -335,24 +505,75 @@ Kurallar:
|
|
|
335
505
|
- Yalnızca `/` ile başlayan string'ler alınır.
|
|
336
506
|
- `prewarmSkip` öneklerinden biriyle başlayanlar atlanır. Varsayılan liste:
|
|
337
507
|
`/api/`, `/_fragment/`, `/__jskelet/`. Oturuma bağlı sayfalar ısıtılmamalı.
|
|
338
|
-
- Tekilleştirme **sırayı korur**:
|
|
339
|
-
|
|
340
|
-
koyun.
|
|
508
|
+
- Tekilleştirme **sırayı korur**: `priority` verilmediğinde uygulamanın verdiği
|
|
509
|
+
sıra anlamlıdır — en önemli sayfaları başa koyun.
|
|
341
510
|
- Bu hook tanımlı değilse ısıtma hiç kurulmaz; zamanlayıcı bile açılmaz.
|
|
342
511
|
|
|
343
512
|
### Tur mantığı
|
|
344
513
|
|
|
345
|
-
1. Liste toplanır
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
514
|
+
1. Liste toplanır. `max`'tan (varsayılan 400) uzunsa bir dilim seçilir:
|
|
515
|
+
`priority` eşleşenler **her turda** başa alınır, kalan yerler kuyruktan
|
|
516
|
+
doldurulur.
|
|
517
|
+
2. `concurrency` işçi paralel olarak istek atar (prod'da 4, dev'de 2). Dev'de
|
|
518
|
+
daha az paralellik: tarama, o an tarayıcıda açtığın sayfanın render'ıyla CPU
|
|
519
|
+
için yarışmasın.
|
|
520
|
+
3. `rps` verilmişse tur bu hızın üstüne çıkmaz — paralellik ne olursa olsun.
|
|
521
|
+
4. Başarısız yollar için, `retryDelayMs` bekledikten sonra **tek seri tekrar
|
|
522
|
+
turu** yapılır (`concurrency: 1`). Bekleme bilinçli: rate limit pencereleri
|
|
523
|
+
saniye mertebesinde, hemen tekrar denemek aynı 429'u almak demek.
|
|
524
|
+
5. Özet loglanır:
|
|
354
525
|
`[prewarm] warmed 128/130 pages, 2 failed, 5 recovered on the retry pass (12.4s)`
|
|
355
526
|
|
|
527
|
+
### Isıtma sırası: `priority`
|
|
528
|
+
|
|
529
|
+
```js
|
|
530
|
+
// jskelet.config.mjs
|
|
531
|
+
cache: () => ({
|
|
532
|
+
prewarm: {
|
|
533
|
+
priority: [
|
|
534
|
+
"/",
|
|
535
|
+
"/piyasalar/:path*",
|
|
536
|
+
/-yorumlar$/,
|
|
537
|
+
],
|
|
538
|
+
},
|
|
539
|
+
}),
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
Desen sözdizimi (`/haber/:slug`) ve doğrudan `RegExp` birlikte kullanılabilir;
|
|
543
|
+
ikincisi "sonu `-yorumlar` ile bitenler" gibi desen sözdiziminin karşılamadığı
|
|
544
|
+
kurallar için. Önce yazılan önce ısınır, hiçbirine uymayan yollar kuyruğa gider
|
|
545
|
+
ve kendi aralarındaki sırayı korur.
|
|
546
|
+
|
|
547
|
+
### Damla damla ısıtma: `rotate` + `rps` + `intervalSeconds`
|
|
548
|
+
|
|
549
|
+
10.000 yolluk bir sitede tek turda her şeyi ısıtmak ne mümkün (HTML önbelleği
|
|
550
|
+
500 girdi tutar) ne de doğru (API kotası dolar). Doğru davranış listeyi zamana
|
|
551
|
+
yaymak:
|
|
552
|
+
|
|
553
|
+
```js
|
|
554
|
+
prewarm: {
|
|
555
|
+
max: 300, // her turda 300 sayfa
|
|
556
|
+
rps: 4, // saniyede en fazla 4 istek
|
|
557
|
+
intervalSeconds: 300, // 5 dakikada bir tur
|
|
558
|
+
rotate: true, // kuyruk kaldığı yerden devam eder
|
|
559
|
+
priority: ["/", "/piyasalar/:path*"],
|
|
560
|
+
}
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
Bu kurulumda öncelikli sayfalar her turda tazelenir, geri kalan kuyruk turlar
|
|
564
|
+
boyunca baştan sona dolaşılır ve upstream saniyede dört isteğin üstünü hiç
|
|
565
|
+
görmez. Veri önbelleği ile birlikte kullanıldığında ikinci turdan sonra ısıtma
|
|
566
|
+
API'ye neredeyse hiç gitmez: veri katmanından okur.
|
|
567
|
+
|
|
568
|
+
Rotasyon açıkken sınırın dışında kalan yollar kaybolmuyor, bir sonraki tura
|
|
569
|
+
kalıyor; log bunu ayırt eder:
|
|
570
|
+
`… , 700 deferred to the next pass`. `rotate: false` ile klasik davranışa
|
|
571
|
+
dönülür — her tur listenin aynı ilk dilimini ısıtır ve gerisi hiç ısınmaz
|
|
572
|
+
(`… , 700 over the limit`).
|
|
573
|
+
|
|
574
|
+
Bir tur `intervalSeconds`'tan uzun sürerse yeni tur başlatılmaz; üst üste binen
|
|
575
|
+
turlar upstream'e iki kat yük bindirirdi.
|
|
576
|
+
|
|
356
577
|
İstekler `user-agent: jskelet-prewarm` (`brand.prewarmUserAgent`) ve
|
|
357
578
|
`accept-encoding: br, gzip` başlıklarıyla gider; ikincisi sıkıştırılmış gövdenin
|
|
358
579
|
de önbelleğe girmesi için.
|
|
@@ -385,10 +606,14 @@ tek seferlik deneyler config'i düzenlemeden yapılabilsin.
|
|
|
385
606
|
| Ayar | Env | `cache().prewarm` | Varsayılan |
|
|
386
607
|
| --- | --- | --- | --- |
|
|
387
608
|
| Açık/kapalı | `PREWARM=0` kapatır, `PREWARM=1` config'i ezip açar | `enabled` | `true` |
|
|
388
|
-
|
|
|
609
|
+
| Tur başına en fazla yol | `PREWARM_MAX` | `max` | `400` |
|
|
389
610
|
| Paralellik | `PREWARM_CONCURRENCY` | `concurrency` | prod 4, dev 2 |
|
|
611
|
+
| Saniyedeki istek | `PREWARM_RPS` | `rps` | `0` (sınırsız) |
|
|
390
612
|
| Başlangıç gecikmesi (ms) | `PREWARM_DELAY_MS` | `delayMs` | prod 500, dev 3000 |
|
|
613
|
+
| Tekrar turu gecikmesi (ms) | `PREWARM_RETRY_DELAY_MS` | `retryDelayMs` | `2000` |
|
|
391
614
|
| Periyot (saniye) | `PREWARM_INTERVAL_SECONDS` | `intervalSeconds` | `0` (kapalı) |
|
|
615
|
+
| Kuyruk rotasyonu | — | `rotate` | `true` |
|
|
616
|
+
| Isıtma sırası | — | `priority` | `[]` |
|
|
392
617
|
|
|
393
618
|
Sayısal ayarlar yalnızca **pozitif ve sonlu** değer kabul eder; geçersiz bir
|
|
394
619
|
değer sessizce bir sonraki katmana düşer.
|
|
@@ -432,6 +657,16 @@ turunun gerçekten `MISS` → önbellek doldurup doldurmadığını buradan gör
|
|
|
432
657
|
parametreleri girdi çoğaltıyor olabilir.
|
|
433
658
|
- **Isıtma hiç çalışmıyor.** `hooks.prewarmPaths` tanımlı değil, `PREWARM=0`
|
|
434
659
|
ayarlı ya da `cache().prewarm.enabled === false`.
|
|
660
|
+
- **Isıtma turu API'yi 429'a sokuyor.** `rps` verilmemiş. `concurrency`
|
|
661
|
+
düşürmek yeterli değil; kotayı koruyan ayar toplam hız. Kalıcı çözüm veri
|
|
662
|
+
önbelleği: ikinci turdan sonra ısıtma upstream'e gitmez.
|
|
663
|
+
- **Isıtma listesi `max`'tan uzun ve sonu hiç ısınmıyor.** `rotate: false`
|
|
664
|
+
olabilir; logdaki `over the limit` ifadesi bunu gösterir.
|
|
665
|
+
- **Bir bölümün tamamı 404 dönüyor.** Upstream düşmüş olabilir. Artık bu durumda
|
|
666
|
+
sayfa bir kez daha denenir, olmazsa 404 değil önbelleğe girmeyen 503 döner;
|
|
667
|
+
logda `returned notFound() while upstream is failing` satırını arayın. Hâlâ
|
|
668
|
+
404 görüyorsanız hata `fetch` dışı bir istemciden geliyor olabilir
|
|
669
|
+
(`reportUpstreamFailure()` gerekir) ya da `cache().trackUpstream` kapatılmış.
|
|
435
670
|
|
|
436
671
|
## Sırada ne var
|
|
437
672
|
|
package/docs/07-yapilandirma.md
CHANGED
|
@@ -115,7 +115,17 @@ export default {
|
|
|
115
115
|
async cache() {
|
|
116
116
|
return {
|
|
117
117
|
html: { "/": 60, "/haber/:slug": 300 },
|
|
118
|
-
|
|
118
|
+
maxEntries: 500,
|
|
119
|
+
data: { maxEntries: 10000, staleFactor: 10 },
|
|
120
|
+
prewarm: {
|
|
121
|
+
enabled: true,
|
|
122
|
+
max: 400,
|
|
123
|
+
concurrency: 4,
|
|
124
|
+
rps: 0,
|
|
125
|
+
intervalSeconds: 0,
|
|
126
|
+
rotate: true,
|
|
127
|
+
priority: ["/", "/haber/:slug"],
|
|
128
|
+
},
|
|
119
129
|
};
|
|
120
130
|
},
|
|
121
131
|
|
|
@@ -549,8 +559,10 @@ Ayrıntı: [03-routing.md](./03-routing.md).
|
|
|
549
559
|
|
|
550
560
|
## `cache()`
|
|
551
561
|
|
|
552
|
-
**Tip:**
|
|
553
|
-
|
|
562
|
+
**Tip:**
|
|
563
|
+
`() => { html?: Record<string, number>, maxEntries?: number, data?: object, trackUpstream?: boolean, transientRetry?: object | false, prewarm?: object }` —
|
|
564
|
+
**Varsayılan:**
|
|
565
|
+
`{ html: {}, maxEntries: 500, data: { maxEntries: 10000, staleFactor: 10 }, trackUpstream: true, transientRetry: { attempts: 1, delayMs: 300 }, prewarm: { enabled: true, max: 400, intervalSeconds: 0 } }`
|
|
554
566
|
|
|
555
567
|
### `cache().html`
|
|
556
568
|
|
|
@@ -570,18 +582,73 @@ Tek istisna `route(fn, { private: true })`: bu route'ta desen eşleşse bile yok
|
|
|
570
582
|
sayılır. Kilidin tek yönlü olması bilinçli — ters yönde bir hata, bir
|
|
571
583
|
kullanıcının HTML'inin bir başkasına servis edilmesi anlamına geliyor.
|
|
572
584
|
|
|
585
|
+
### `cache().maxEntries`
|
|
586
|
+
|
|
587
|
+
**Tip:** `number` — **Varsayılan:** `500`
|
|
588
|
+
|
|
589
|
+
HTML önbelleğinin girdi sınırı. Girdi başına yüz kilobayt düştüğü için bu sayıyı
|
|
590
|
+
yükseltmek belleği hızla tüketir; on binlerce yollu bir siteyi buradan çözmeye
|
|
591
|
+
çalışmak yanlış katman, doğru yer `cache().data`.
|
|
592
|
+
|
|
593
|
+
### `cache().data`
|
|
594
|
+
|
|
595
|
+
Upstream veri önbelleği (`withDataCache`). Ayrıntı: [06-cache.md](./06-cache.md).
|
|
596
|
+
|
|
597
|
+
| Alan | Tip | Varsayılan | Anlamı |
|
|
598
|
+
| --- | --- | --- | --- |
|
|
599
|
+
| `maxEntries` | `number` | `10000` | LRU girdi sınırı. JSON, HTML'e göre onlarca kat küçük olduğu için sınır yüksek. |
|
|
600
|
+
| `staleFactor` | `number` | `10` | TTL dolduktan sonra girdinin kaç TTL boyunca daha kullanılabileceği. `0` → bayat servis yok. |
|
|
601
|
+
|
|
602
|
+
### `cache().trackUpstream`
|
|
603
|
+
|
|
604
|
+
**Tip:** `boolean` — **Varsayılan:** `true`
|
|
605
|
+
|
|
606
|
+
Açıkken `globalThis.fetch` sarılır ve render sırasındaki geçici upstream
|
|
607
|
+
hataları (`429`, `5xx`, ağ) kendiliğinden bildirilir; `reportUpstreamFailure()`
|
|
608
|
+
çağırmak gerekmez. `fetch`i kendisi saran bir uygulama bunu kapatabilir.
|
|
609
|
+
|
|
610
|
+
### `cache().transientRetry`
|
|
611
|
+
|
|
612
|
+
**Tip:** `{ attempts?: number, delayMs?: number } | false` —
|
|
613
|
+
**Varsayılan:** `{ attempts: 1, delayMs: 300 }`
|
|
614
|
+
|
|
615
|
+
Geçici bir upstream hatası yüzünden `notFound()` çağrılan sayfa kaç kez daha
|
|
616
|
+
denenir. Amaç var olan bir sayfanın 404'e dönüşmemesi; denemeler tükenirse yanıt
|
|
617
|
+
önbelleğe girmeyen bir 503 olur. `false` ya da `attempts: 0` tekrarı kapatır.
|
|
618
|
+
Ayrıntı: [06-cache.md](./06-cache.md).
|
|
619
|
+
|
|
573
620
|
### `cache().prewarm`
|
|
574
621
|
|
|
575
622
|
| Alan | Tip | Varsayılan | Anlamı |
|
|
576
623
|
| --- | --- | --- | --- |
|
|
577
624
|
| `enabled` | `boolean` | `true` | `false` ise ısıtma yapılmaz (`PREWARM=1` ile ezilebilir) |
|
|
578
|
-
| `max` | `number` | `400` |
|
|
625
|
+
| `max` | `number` | `400` | Bir turda en fazla kaç yol ısıtılır |
|
|
579
626
|
| `concurrency` | `number` | prod 4, dev 2 | Paralel işçi sayısı |
|
|
627
|
+
| `rps` | `number` | `0` | Saniyedeki en fazla ısıtma isteği; `0` sınırsız. Upstream kotasını koruyan ayar bu. |
|
|
580
628
|
| `delayMs` | `number` | prod 500, dev 3000 | Açılıştan sonra ilk turun gecikmesi |
|
|
629
|
+
| `retryDelayMs` | `number` | `2000` | Tekrar turundan önce beklenen süre |
|
|
581
630
|
| `intervalSeconds` | `number` | `0` | 0'dan büyükse tur periyodik tekrarlanır |
|
|
631
|
+
| `rotate` | `boolean` | `true` | Liste `max`'tan uzunsa periyodik turlar kaldığı yerden devam eder |
|
|
632
|
+
| `priority` | `(string \| RegExp)[]` | `[]` | Isıtma sırası; eşleşen yollar her turda başa alınır |
|
|
582
633
|
|
|
583
|
-
|
|
584
|
-
|
|
634
|
+
`priority` iki biçim kabul eder: config'in her yerinde geçerli olan desen
|
|
635
|
+
sözdizimi ve doğrudan `RegExp`. Önce yazılan önce ısınır.
|
|
636
|
+
|
|
637
|
+
```js
|
|
638
|
+
prewarm: {
|
|
639
|
+
max: 500,
|
|
640
|
+
rps: 4,
|
|
641
|
+
intervalSeconds: 300,
|
|
642
|
+
priority: [
|
|
643
|
+
"/", // ana sayfa
|
|
644
|
+
"/piyasalar/:path*", // tüm piyasa bölümü
|
|
645
|
+
/-yorumlar$/, // desen sözdiziminin karşılamadığı kural
|
|
646
|
+
],
|
|
647
|
+
}
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
Sayısal alanların her biri aynı adı taşıyan ortam değişkeniyle ezilebilir; env
|
|
651
|
+
önceliklidir. Ayrıntı: [06-cache.md](./06-cache.md).
|
|
585
652
|
|
|
586
653
|
## `hooks`
|
|
587
654
|
|
|
@@ -676,7 +743,9 @@ basılmaz.
|
|
|
676
743
|
| `PREWARM` | `startPrewarm` | — | `0` ısıtmayı kapatır; `1` config'teki `enabled: false`'u ezip açar |
|
|
677
744
|
| `PREWARM_MAX` | `prewarm` | `400` | En fazla kaç yol ısıtılır |
|
|
678
745
|
| `PREWARM_CONCURRENCY` | `prewarm` | prod 4, dev 2 | Paralel işçi sayısı |
|
|
746
|
+
| `PREWARM_RPS` | `prewarm` | `0` | Saniyedeki en fazla ısıtma isteği; `0` sınırsız |
|
|
679
747
|
| `PREWARM_DELAY_MS` | `startPrewarm` | prod 500, dev 3000 | İlk turun gecikmesi |
|
|
748
|
+
| `PREWARM_RETRY_DELAY_MS` | `prewarm` | `2000` | Tekrar turundan önceki bekleme |
|
|
680
749
|
| `PREWARM_INTERVAL_SECONDS` | `startPrewarm` | `0` | 0'dan büyükse periyodik tur |
|
|
681
750
|
| `JSKELET_VERBOSE` | `jskelet dev` | — | `1` ise restart'ta değişen dosyaların tamamı listelenir |
|
|
682
751
|
| `JSKELET_COLOR` | `jskelet/log` | — | `1` ise renk zorlanır. Alt süreçler boruya yazdığı için renk algılaması kapanır; `jskelet dev` bunu kendisi ayarlar. |
|