jskelet 0.1.3 → 0.1.5
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 +31 -6
- package/docs/06-cache.md +65 -12
- package/docs/07-yapilandirma.md +20 -2
- package/docs/09-dev-araclari.md +27 -5
- package/docs/en/06-caching.md +70 -15
- package/docs/en/07-configuration.md +21 -2
- package/docs/en/09-dev-tools.md +26 -3
- package/package.json +1 -1
- package/src/client/devtools/overlay.js +111 -46
- package/src/config/defaults.js +14 -0
- package/src/config/index.js +24 -3
- package/src/server/create-app.js +14 -1
- package/src/server/dev/devtools.js +106 -15
- package/src/server/dev/socket.js +157 -0
- package/src/server/render.js +111 -38
- package/src/server/upstream-tracking.js +103 -13
package/CHANGELOG.md
CHANGED
|
@@ -31,23 +31,48 @@ one is listed under a **Breaking** heading.
|
|
|
31
31
|
retry pass, since rate limit windows are measured in seconds.
|
|
32
32
|
- `cache().maxEntries` configures the HTML cache limit, which used to be a fixed
|
|
33
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.
|
|
34
46
|
- The dev report now includes the data cache entry count under `cache.data`.
|
|
35
47
|
|
|
36
48
|
### Changed
|
|
37
49
|
|
|
50
|
+
- The dev tools panel is now fed over a WebSocket (`<devBasePath>/ws`) instead of
|
|
51
|
+
polling `/stats` every two seconds. The server pushes statistics as they change
|
|
52
|
+
and sends live reload and CSS hot-swap events over the same connection, so an
|
|
53
|
+
open tab no longer keeps hitting the server while the panel is closed. No new
|
|
54
|
+
dependency is involved; if the socket cannot be opened, the panel falls back to
|
|
55
|
+
the previous SSE plus polling path.
|
|
38
56
|
- `notFound()` is no longer served as a 404 when a transient upstream failure
|
|
39
|
-
(`429`, `5xx`, network error)
|
|
40
|
-
|
|
41
|
-
|
|
57
|
+
(`429`, `5xx`, network error) happened during the same render. The page is
|
|
58
|
+
retried first and, if upstream is still failing, responds with an uncached
|
|
59
|
+
`503` and `Retry-After`. A temporary rate limit is no longer frozen into "this
|
|
60
|
+
page does not exist" for the whole TTL. A retry that gets a clean answer saying
|
|
61
|
+
the page is gone still returns a normal 404.
|
|
42
62
|
- Responses produced with missing data are no longer offered to shared caches:
|
|
43
63
|
a `degraded` render is sent with `private, no-store` instead of
|
|
44
64
|
`public, s-maxage=…`. The `X-JSkelet-Cache` diagnostic header is still written.
|
|
45
65
|
- The prewarm summary distinguishes paths left for the next pass
|
|
46
66
|
(`700 deferred to the next pass`) from paths dropped entirely
|
|
47
67
|
(`700 over the limit`).
|
|
48
|
-
- The changelog page of the marketing example is generated from the
|
|
49
|
-
|
|
50
|
-
|
|
68
|
+
- The changelog page of the marketing example is generated from the project's
|
|
69
|
+
`CHANGELOG.md` instead of a hand-written list, and shows the version published
|
|
70
|
+
on npm next to the installed one.
|
|
71
|
+
- The marketing example reads its markdown (documentation and changelog) from
|
|
72
|
+
the repository over GitHub's raw endpoint, falling back to the installed
|
|
73
|
+
package when the network is unavailable, so a deployment that ships without
|
|
74
|
+
`node_modules` can still serve the docs. In development the local file wins
|
|
75
|
+
and nothing is cached. The branch is overridable with `DOCS_REF`.
|
|
51
76
|
|
|
52
77
|
## [0.1.2] - 2026-08-30
|
|
53
78
|
|
package/docs/06-cache.md
CHANGED
|
@@ -5,8 +5,8 @@ Bu belge JSkelet'in ISR ikamesini bütün ayrıntılarıyla anlatır: HTML TTL
|
|
|
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
7
|
(`withRequestCache` / `cache()`), veri önbelleği (`withDataCache`), upstream
|
|
8
|
-
hatalarının önbelleği nasıl etkilediği (
|
|
9
|
-
açılışındaki ısıtma turu.
|
|
8
|
+
hatalarının önbelleği nasıl etkilediği (otomatik izleme ve
|
|
9
|
+
`reportUpstreamFailure`) ve sunucu açılışındaki ısıtma turu.
|
|
10
10
|
Kararların arkasındaki ölçüm gerekçeleri [02-mimari.md](./02-mimari.md)'de,
|
|
11
11
|
config alanlarının tam referansı [07-yapilandirma.md](./07-yapilandirma.md)'de.
|
|
12
12
|
|
|
@@ -315,9 +315,34 @@ Render sırasında upstream düştüyse çıktı eksik veri içeriyor demektir.
|
|
|
315
315
|
HTML'i tüm TTL boyunca servis etmek yerine önbelleğe **hiç yazmamak** doğru
|
|
316
316
|
davranış: sonraki istek yeniden dener.
|
|
317
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ı.
|
|
318
343
|
Bağımlılık yönü bilinçli olarak tersine çevrilmiştir: framework veri katmanını
|
|
319
344
|
tanımaz, veri katmanı framework'e haber verir. Hiç çağıran olmazsa maliyet boş
|
|
320
|
-
bir dizidir.
|
|
345
|
+
bir dizidir. Aynı hata her iki yoldan bildirilirse tekilleştirilir.
|
|
321
346
|
|
|
322
347
|
```js
|
|
323
348
|
// lib/api/client.js
|
|
@@ -366,18 +391,44 @@ geçici bir kota sorunu TTL boyunca "bu sayfa yok" cevabına dönüşür. Arama
|
|
|
366
391
|
için bu kalıcı bir kayıp.
|
|
367
392
|
|
|
368
393
|
Framework bu durumu ayırır: render sırasında **geçici** bir upstream hatası
|
|
369
|
-
|
|
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.
|
|
370
404
|
|
|
371
405
|
| Render sırasında | `notFound()` sonucu |
|
|
372
406
|
| --- | --- |
|
|
373
|
-
| Geçici hata var (`429`, `5xx`, ağ hatası) | `503`, `Retry-After: 30`, `no-store`
|
|
374
|
-
|
|
|
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ı:
|
|
375
412
|
|
|
376
|
-
|
|
377
|
-
|
|
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
|
+
```
|
|
378
429
|
|
|
379
|
-
|
|
380
|
-
|
|
430
|
+
`transientRetry: false` (ya da `attempts: 0`) tekrarı kapatır ve doğrudan 503'e
|
|
431
|
+
düşer.
|
|
381
432
|
|
|
382
433
|
## Önbelleği yönetmek
|
|
383
434
|
|
|
@@ -612,8 +663,10 @@ turunun gerçekten `MISS` → önbellek doldurup doldurmadığını buradan gör
|
|
|
612
663
|
- **Isıtma listesi `max`'tan uzun ve sonu hiç ısınmıyor.** `rotate: false`
|
|
613
664
|
olabilir; logdaki `over the limit` ifadesi bunu gösterir.
|
|
614
665
|
- **Bir bölümün tamamı 404 dönüyor.** Upstream düşmüş olabilir. Artık bu durumda
|
|
615
|
-
404 değil önbelleğe girmeyen 503
|
|
616
|
-
upstream is failing` satırını arayın.
|
|
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ış.
|
|
617
670
|
|
|
618
671
|
## Sırada ne var
|
|
619
672
|
|
package/docs/07-yapilandirma.md
CHANGED
|
@@ -560,9 +560,9 @@ Ayrıntı: [03-routing.md](./03-routing.md).
|
|
|
560
560
|
## `cache()`
|
|
561
561
|
|
|
562
562
|
**Tip:**
|
|
563
|
-
`() => { html?: Record<string, number>, maxEntries?: number, data?: object, prewarm?: object }` —
|
|
563
|
+
`() => { html?: Record<string, number>, maxEntries?: number, data?: object, trackUpstream?: boolean, transientRetry?: object | false, prewarm?: object }` —
|
|
564
564
|
**Varsayılan:**
|
|
565
|
-
`{ html: {}, maxEntries: 500, data: { maxEntries: 10000, staleFactor: 10 }, prewarm: { enabled: true, max: 400, intervalSeconds: 0 } }`
|
|
565
|
+
`{ html: {}, maxEntries: 500, data: { maxEntries: 10000, staleFactor: 10 }, trackUpstream: true, transientRetry: { attempts: 1, delayMs: 300 }, prewarm: { enabled: true, max: 400, intervalSeconds: 0 } }`
|
|
566
566
|
|
|
567
567
|
### `cache().html`
|
|
568
568
|
|
|
@@ -599,6 +599,24 @@ Upstream veri önbelleği (`withDataCache`). Ayrıntı: [06-cache.md](./06-cache
|
|
|
599
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
600
|
| `staleFactor` | `number` | `10` | TTL dolduktan sonra girdinin kaç TTL boyunca daha kullanılabileceği. `0` → bayat servis yok. |
|
|
601
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
|
+
|
|
602
620
|
### `cache().prewarm`
|
|
603
621
|
|
|
604
622
|
| Alan | Tip | Varsayılan | Anlamı |
|
package/docs/09-dev-araclari.md
CHANGED
|
@@ -111,9 +111,9 @@ Restart satırı değişen dosyayı ya da sayısını gösterir:
|
|
|
111
111
|
|
|
112
112
|
## CSS hot-swap ve tam yenileme
|
|
113
113
|
|
|
114
|
-
Dev sunucusu `.jskelet/manifest.json` dosyasını izler ve
|
|
115
|
-
(`<devBasePath>/
|
|
116
|
-
|
|
114
|
+
Dev sunucusu `.jskelet/manifest.json` dosyasını izler ve olayları canlı kanal
|
|
115
|
+
(`<devBasePath>/ws`) üzerinden tarayıcıya yayınlar. Manifest her build turunda
|
|
116
|
+
yeniden yazıldığı için değişiklik tespiti manifest üzerinden yapılır.
|
|
117
117
|
|
|
118
118
|
| Değişen | Davranış |
|
|
119
119
|
| --- | --- |
|
|
@@ -131,6 +131,27 @@ Sunucu yeniden başladığında overlay bunu **boot kimliğinden** anlar: her s
|
|
|
131
131
|
kendine özgü bir `boot` değeri yayınlar, overlay değişikliği görüp "restarted"
|
|
132
132
|
bilgisini gösterir ve kendi durumunu sıfırlamaz.
|
|
133
133
|
|
|
134
|
+
## Canlı kanal
|
|
135
|
+
|
|
136
|
+
Overlay'e giden her şey — istatistikler, live reload ve CSS hot-swap olayları —
|
|
137
|
+
tek bir WebSocket üzerinden gelir (`<devBasePath>/ws`). Panel eskiden
|
|
138
|
+
istatistikleri iki saniyede bir çekiyordu; açık her sekme, panel kapalıyken bile
|
|
139
|
+
sunucuya sürekli istek atıyordu. Artık sunucu değişiklik oldukça iter: bir istek
|
|
140
|
+
ya da hata kaydedildiğinde (120 ms birleştirilerek), ısıtma sürerken saniyede
|
|
141
|
+
bir, geri kalan zamanda yalnızca uptime/bellek tazelensin diye dört saniyede bir.
|
|
142
|
+
Bağlı panel yoksa hiçbir şey hesaplanmaz.
|
|
143
|
+
|
|
144
|
+
El sıkışma HTTP `upgrade` olayında geçtiği ve o olay middleware zincirine hiç
|
|
145
|
+
uğramadığı için kanal `listen` sonrası doğrudan sunucuya bağlanır
|
|
146
|
+
(`attachDevSocket`). Sunucu tarafı `ws` gibi bir bağımlılık kullanmaz: yalnızca
|
|
147
|
+
sunucu→istemci metin çerçevesi yazmak ve istemcinin ping/close çerçevelerini
|
|
148
|
+
yanıtlamak gerekiyor.
|
|
149
|
+
|
|
150
|
+
Soket hiç açılamazsa (araya giren bir proxy WebSocket'i geçirmiyor olabilir)
|
|
151
|
+
overlay eski yola düşer: `/events` SSE akışı + `/stats` yoklaması. Soket kurulup
|
|
152
|
+
sonra düşerse — yani sunucu yeniden başlıyorsa — yarım saniyede bir yeniden
|
|
153
|
+
bağlanır ve gösterge bu sırada "bağlantı yok" der.
|
|
154
|
+
|
|
134
155
|
## Devtools overlay
|
|
135
156
|
|
|
136
157
|
Sağ altta yüzen bir baloncuk; `Alt+D` ile açılır, `Esc` ya da karartma alanına
|
|
@@ -236,8 +257,9 @@ Rapor katmanı yalnızca development'ta yüklenir, üretim çıktısına hiç gi
|
|
|
236
257
|
| --- | --- | --- |
|
|
237
258
|
| `/overlay.js` | GET | Overlay script'i |
|
|
238
259
|
| `/logo.png` | GET | Overlay logosu |
|
|
239
|
-
| `/
|
|
240
|
-
| `/
|
|
260
|
+
| `/ws` | GET (upgrade) | Canlı kanal: istatistikler, live reload ve CSS hot-swap olayları |
|
|
261
|
+
| `/events` | GET | SSE: yalnızca WebSocket kurulamazsa kullanılan yedek olay akışı |
|
|
262
|
+
| `/stats` | GET | Anlık istatistikler; aynı yedek yolun veri ucu |
|
|
241
263
|
| `/report` | GET | Rapor sayfası (HTML) |
|
|
242
264
|
| `/report.js` | GET | Rapor sayfasının script'i |
|
|
243
265
|
| `/report/data` | GET | Raporun tek veri kaynağı (JSON) |
|
package/docs/en/06-caching.md
CHANGED
|
@@ -5,7 +5,7 @@ cache and its stale-while-revalidate behaviour, where `revalidate` comes from,
|
|
|
5
5
|
how the cache key is built, the values of the `X-JSkelet-Cache` header, why the
|
|
6
6
|
compressed body is kept in the cache, per-request memoization
|
|
7
7
|
(`withRequestCache` / `cache()`), the data cache (`withDataCache`), how upstream
|
|
8
|
-
failures affect the cache (`reportUpstreamFailure`) and the prewarm round at
|
|
8
|
+
failures affect the cache (automatic tracking and `reportUpstreamFailure`) and the prewarm round at
|
|
9
9
|
server startup. The
|
|
10
10
|
measurement rationale behind the decisions is in
|
|
11
11
|
[02-architecture.md](./02-architecture.md), and the full reference of config
|
|
@@ -324,9 +324,35 @@ If upstream went down during the render, the output contains missing data.
|
|
|
324
324
|
Rather than serving such HTML for the whole TTL, the right behaviour is to
|
|
325
325
|
**never write it** to the cache: the next request tries again.
|
|
326
326
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
327
|
+
This information arrives through two paths.
|
|
328
|
+
|
|
329
|
+
### Automatic tracking (the default)
|
|
330
|
+
|
|
331
|
+
At startup `createApp()` wraps `globalThis.fetch` and reports **transient**
|
|
332
|
+
failures (`429`, `5xx`, network errors) from calls made during a render on its
|
|
333
|
+
own. No application code is needed; if your API client talks over `fetch`, the
|
|
334
|
+
rate limit protection is already in place.
|
|
335
|
+
|
|
336
|
+
The details:
|
|
337
|
+
|
|
338
|
+
- Only calls inside a render scope count. A `fetch` from a script, a cron job or
|
|
339
|
+
anywhere outside a request is left untouched.
|
|
340
|
+
- Requests to our own server (`localhost`, `127.0.0.1`) are skipped: the warm-up
|
|
341
|
+
round and the health check are not upstream.
|
|
342
|
+
- Deterministic answers such as `404`/`403` are **not** reported automatically.
|
|
343
|
+
In most APIs a `404` means "no such record"; treating it as missing data would
|
|
344
|
+
produce a false warning on every not-found page.
|
|
345
|
+
- To turn it off: `cache().trackUpstream: false`. An application that wraps
|
|
346
|
+
`fetch` itself (metrics, retries, a circuit breaker) may prefer that.
|
|
347
|
+
|
|
348
|
+
### Manual reporting
|
|
349
|
+
|
|
350
|
+
For a client that does not use `fetch` (a database driver, gRPC, a vendor SDK),
|
|
351
|
+
or for a layer that wants to flag permanent failures too, the contract is
|
|
352
|
+
unchanged. The dependency direction is deliberately inverted: the framework does
|
|
353
|
+
not know about the data layer, the data layer notifies the framework. If nobody
|
|
354
|
+
ever calls it, the cost is an empty array. If the same failure arrives through
|
|
355
|
+
both paths it is de-duplicated.
|
|
330
356
|
|
|
331
357
|
```js
|
|
332
358
|
// lib/api/client.js
|
|
@@ -374,19 +400,45 @@ site into 404s when upstream is rate limited — and because those 404s enter th
|
|
|
374
400
|
cache, a temporary quota problem becomes a "this page does not exist" answer for
|
|
375
401
|
the whole TTL. For a search engine that is a permanent loss.
|
|
376
402
|
|
|
377
|
-
The framework separates the two cases: if a **transient** upstream failure
|
|
378
|
-
|
|
403
|
+
The framework separates the two cases: if a **transient** upstream failure
|
|
404
|
+
happened during the render, `notFound()` is not served as a 404. In order:
|
|
405
|
+
|
|
406
|
+
1. The page is **retried** after a short delay (once by default, after 300 ms).
|
|
407
|
+
The retry runs in its own upstream and per-request cache scope, so neither
|
|
408
|
+
the first round's failure nor its memoized empty answers affect it.
|
|
409
|
+
2. If the second round can produce the page, the visitor sees the **real
|
|
410
|
+
content** and the output is cached normally. Warm-up logs show this is
|
|
411
|
+
common: the same path returns 200 seconds later.
|
|
412
|
+
3. If the retries are exhausted the response is a `503` — not cached, carrying
|
|
413
|
+
`Retry-After`, and the next request can still produce the real content.
|
|
379
414
|
|
|
380
415
|
| During the render | Result of `notFound()` |
|
|
381
416
|
| --- | --- |
|
|
382
|
-
| A transient failure exists (`429`, `5xx`, network error) | `503`, `Retry-After: 30`, `no-store`
|
|
383
|
-
|
|
|
417
|
+
| A transient failure exists (`429`, `5xx`, network error) | Retry → the page if it succeeds; otherwise `503`, `Retry-After: 30`, `no-store` |
|
|
418
|
+
| The retry got a clean answer saying "not there" | A normal `404` |
|
|
419
|
+
| A permanent failure (`404`, `403`…) or no failure | A normal `404`, no retry |
|
|
420
|
+
|
|
421
|
+
The log lines:
|
|
384
422
|
|
|
385
|
-
|
|
386
|
-
|
|
423
|
+
```
|
|
424
|
+
[render] /news/x returned notFound() while upstream is failing (429 /api/...), retrying (1/1)
|
|
425
|
+
[render] /news/x could not be produced, upstream is still failing (429 /api/...), serving an uncached 503 instead of a 404
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
So **an existing page never turns into a 404**: either the real content arrives,
|
|
429
|
+
or an uncached 503 does. Nothing is frozen as "missing".
|
|
430
|
+
|
|
431
|
+
The cost of a retry is a second round of requests on upstream, which is why the
|
|
432
|
+
default is a single attempt. The setting is `cache().transientRetry`:
|
|
433
|
+
|
|
434
|
+
```js
|
|
435
|
+
cache: {
|
|
436
|
+
transientRetry: { attempts: 2, delayMs: 500 },
|
|
437
|
+
}
|
|
438
|
+
```
|
|
387
439
|
|
|
388
|
-
|
|
389
|
-
|
|
440
|
+
`transientRetry: false` (or `attempts: 0`) disables the retry and falls straight
|
|
441
|
+
through to the 503.
|
|
390
442
|
|
|
391
443
|
## Managing the cache
|
|
392
444
|
|
|
@@ -628,9 +680,12 @@ filled the cache.
|
|
|
628
680
|
does not reach upstream.
|
|
629
681
|
- **The warm-up list is longer than `max` and its tail never warms.** `rotate`
|
|
630
682
|
may be `false`; the `over the limit` phrase in the log shows this.
|
|
631
|
-
- **A whole section returns 404.** Upstream may be down.
|
|
632
|
-
does not enter the cache is
|
|
633
|
-
`returned notFound() while upstream is failing`
|
|
683
|
+
- **A whole section returns 404.** Upstream may be down. The page is now retried
|
|
684
|
+
once and, failing that, a 503 that does not enter the cache is returned
|
|
685
|
+
instead of a 404; look for the `returned notFound() while upstream is failing`
|
|
686
|
+
line in the log. If you still see 404s, the failure may come from a non-`fetch`
|
|
687
|
+
client (which needs `reportUpstreamFailure()`) or `cache().trackUpstream` is
|
|
688
|
+
off.
|
|
634
689
|
|
|
635
690
|
## What's next
|
|
636
691
|
|
|
@@ -572,9 +572,9 @@ Details: [03-routing.md](./03-routing.md).
|
|
|
572
572
|
## `cache()`
|
|
573
573
|
|
|
574
574
|
**Type:**
|
|
575
|
-
`() => { html?: Record<string, number>, maxEntries?: number, data?: object, prewarm?: object }` —
|
|
575
|
+
`() => { html?: Record<string, number>, maxEntries?: number, data?: object, trackUpstream?: boolean, transientRetry?: object | false, prewarm?: object }` —
|
|
576
576
|
**Default:**
|
|
577
|
-
`{ html: {}, maxEntries: 500, data: { maxEntries: 10000, staleFactor: 10 }, prewarm: { enabled: true, max: 400, intervalSeconds: 0 } }`
|
|
577
|
+
`{ html: {}, maxEntries: 500, data: { maxEntries: 10000, staleFactor: 10 }, trackUpstream: true, transientRetry: { attempts: 1, delayMs: 300 }, prewarm: { enabled: true, max: 400, intervalSeconds: 0 } }`
|
|
578
578
|
|
|
579
579
|
### `cache().html`
|
|
580
580
|
|
|
@@ -613,6 +613,25 @@ The upstream data cache (`withDataCache`). Details:
|
|
|
613
613
|
| `maxEntries` | `number` | `10000` | The LRU entry limit. The limit is high because JSON is tens of times smaller than HTML. |
|
|
614
614
|
| `staleFactor` | `number` | `10` | For how many TTLs an entry stays usable after the TTL expired. `0` → no stale serving. |
|
|
615
615
|
|
|
616
|
+
### `cache().trackUpstream`
|
|
617
|
+
|
|
618
|
+
**Type:** `boolean` — **Default:** `true`
|
|
619
|
+
|
|
620
|
+
When on, `globalThis.fetch` is wrapped and transient upstream failures (`429`,
|
|
621
|
+
`5xx`, network) during a render are reported automatically; calling
|
|
622
|
+
`reportUpstreamFailure()` is not required. An application that wraps `fetch`
|
|
623
|
+
itself can turn this off.
|
|
624
|
+
|
|
625
|
+
### `cache().transientRetry`
|
|
626
|
+
|
|
627
|
+
**Type:** `{ attempts?: number, delayMs?: number } | false` —
|
|
628
|
+
**Default:** `{ attempts: 1, delayMs: 300 }`
|
|
629
|
+
|
|
630
|
+
How many extra times a page is tried when `notFound()` was called because of a
|
|
631
|
+
transient upstream failure. The point is that an existing page never turns into
|
|
632
|
+
a 404; if the retries are exhausted the response is an uncached 503. `false` or
|
|
633
|
+
`attempts: 0` disables the retry. Details: [06-caching.md](./06-caching.md).
|
|
634
|
+
|
|
616
635
|
### `cache().prewarm`
|
|
617
636
|
|
|
618
637
|
| Field | Type | Default | Meaning |
|
package/docs/en/09-dev-tools.md
CHANGED
|
@@ -116,7 +116,7 @@ If `JSKELET_VERBOSE=1` is set, all files are listed when more than one changed.
|
|
|
116
116
|
## CSS hot-swap and full reload
|
|
117
117
|
|
|
118
118
|
The dev server watches `.jskelet/manifest.json` and broadcasts events to the
|
|
119
|
-
browser over
|
|
119
|
+
browser over the live channel (`<devBasePath>/ws`). Since the manifest is
|
|
120
120
|
rewritten on every build round, change detection is done through the manifest.
|
|
121
121
|
|
|
122
122
|
| What changed | Behavior |
|
|
@@ -134,6 +134,28 @@ When the server restarts, the overlay figures it out from the **boot id**: every
|
|
|
134
134
|
process broadcasts a unique `boot` value, the overlay sees the change, shows the
|
|
135
135
|
"restarted" note and does not reset its own state.
|
|
136
136
|
|
|
137
|
+
## The live channel
|
|
138
|
+
|
|
139
|
+
Everything the overlay shows — statistics, live reload and CSS hot-swap events —
|
|
140
|
+
arrives over a single WebSocket (`<devBasePath>/ws`). The panel used to poll for
|
|
141
|
+
statistics every two seconds, so every open tab kept hitting the server even
|
|
142
|
+
while the panel was closed. Now the server pushes as things change: when a
|
|
143
|
+
request or an error is recorded (coalesced over 120 ms), once per second while
|
|
144
|
+
prewarming runs, and every four seconds otherwise so uptime and memory stay
|
|
145
|
+
fresh. Nothing is computed when no panel is connected.
|
|
146
|
+
|
|
147
|
+
The handshake happens on the HTTP `upgrade` event, and that event never reaches
|
|
148
|
+
the middleware chain, so the channel is attached straight to the server after
|
|
149
|
+
`listen` (`attachDevSocket`). The server side pulls in no dependency such as
|
|
150
|
+
`ws`: all it needs is to write server-to-client text frames and to answer the
|
|
151
|
+
client's ping/close frames.
|
|
152
|
+
|
|
153
|
+
If the socket cannot be opened at all (a proxy in between may not pass WebSocket
|
|
154
|
+
through), the overlay falls back to the old path: the `/events` SSE stream plus
|
|
155
|
+
polling `/stats`. If the socket opens and later drops — that is, the server is
|
|
156
|
+
restarting — it reconnects every half second and the indicator reads
|
|
157
|
+
"server restarting…" in the meantime.
|
|
158
|
+
|
|
137
159
|
## Devtools overlay
|
|
138
160
|
|
|
139
161
|
A floating bubble in the bottom right; opened with `Alt+D`, closed with `Esc` or
|
|
@@ -240,8 +262,9 @@ Under `brand.devBasePath` (default `/__jskelet/dev`):
|
|
|
240
262
|
| --- | --- | --- |
|
|
241
263
|
| `/overlay.js` | GET | The overlay script |
|
|
242
264
|
| `/logo.png` | GET | The overlay logo |
|
|
243
|
-
| `/
|
|
244
|
-
| `/
|
|
265
|
+
| `/ws` | GET (upgrade) | Live channel: statistics, live reload and CSS hot-swap events |
|
|
266
|
+
| `/events` | GET | SSE: the fallback event stream, used only when WebSocket cannot be established |
|
|
267
|
+
| `/stats` | GET | Current statistics; the data endpoint of that same fallback |
|
|
245
268
|
| `/report` | GET | The report page (HTML) |
|
|
246
269
|
| `/report.js` | GET | The report page's script |
|
|
247
270
|
| `/report/data` | GET | The report's single data source (JSON) |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jskelet",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "A framework that feels like no framework: Express 5 + EJS server rendering, vanilla JS islands, Tailwind v4 and an in-process HTML TTL cache.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|