jskelet 0.7.2 → 0.7.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -10,6 +10,9 @@ one is listed under a **Breaking** heading.
10
10
 
11
11
  ### Changed
12
12
 
13
+ - Redis HTML is stored as a small JSON meta key, a raw brotli `:body` key, and
14
+ optional `:enc:br` / `:enc:gzip` keys on a second connection. HTML command
15
+ timeouts use their own circuit breaker and no longer bypass the data cache.
13
16
  - `.jsk` codegen merges adjacent static HTML into fewer `+=` ops and drops
14
17
  template indentation (newline-only whitespace). A lone space between
15
18
  inline siblings is kept; `pre` / `textarea` / `script` / `style` / `code`
@@ -73,6 +76,13 @@ one is listed under a **Breaking** heading.
73
76
 
74
77
  ### Added
75
78
 
79
+ - `cache().redis.htmlCommandTimeoutMs` (default `2000`) limits only the HTML
80
+ Redis connection. `commandTimeoutMs` stays `200` for data, publish, and scan.
81
+ The admin Redis card shows both timeouts.
82
+ - `stylesheets({ blocking: false })` and `<Stylesheets :blocking="false" />`
83
+ emit each stylesheet with `media="print"` and an `onload` that switches it
84
+ to `all`, plus a `<noscript>` fallback, so the links do not block rendering.
85
+ The default stays render-blocking.
76
86
  - Dev tools **Server** tab can clear the HTML cache for the open page, every
77
87
  HTML entry, or the data cache. Live stats include HTML/data entry counts;
78
88
  new endpoints under `devBasePath`: `POST /cache/clear-html`,
package/docs/02-mimari.md CHANGED
@@ -155,9 +155,17 @@ kapsamadığı için sheet gelince sayfa yeniden akıyordu (bir liste sayfasınd
155
155
  ziyarette zaten `immutable` önbellekten geliyor.
156
156
 
157
157
  Sayfaya özel kurallar için `styles/pages/*.css` + controller `styles: [...]`
158
- eklenebilir ([08-build.md](./08-build.md)); bunlar da render-blocking basılır
159
- ama yalnızca ilgili sayfada. Tailwind utility'leri global sheet'te kalır —
160
- sayfa sheet'inde tam `@import "tailwindcss"` utility çıktısını tekrarlar.
158
+ eklenebilir ([08-build.md](./08-build.md)); bunlar da varsayılan olarak
159
+ render-blocking basılır ama yalnızca ilgili sayfada. Tailwind utility'leri
160
+ global sheet'te kalır — sayfa sheet'inde tam `@import "tailwindcss"` utility
161
+ çıktısını tekrarlar.
162
+
163
+ Erken boyama isteyen bir layout bu varsayılandan çıkabilir:
164
+ `stylesheets({ blocking: false })` veya şablonda
165
+ `<Stylesheets :blocking="false" />`. Link `media="print"` ile iner, `onload`
166
+ onu `all` yapar; JS kapalıyken `<noscript>` içindeki normal link uygulanır.
167
+ Bu, yukarıdaki CLS takasının tersidir: ilk boyama erken gelir, sheet gelince
168
+ düzen kayabilir.
161
169
 
162
170
  Aynı mantık ikonlarda da var: her ikon için ayrı istek yerine, build zamanında
163
171
  yalnızca kaynakta kullanılan sembollerden bir SVG sprite üretilir. Tüm Phosphor
@@ -113,7 +113,7 @@ script ve JSON-LD döngüleri için yerleşikler:
113
113
 
114
114
  | Etiket | Props | Çıktı |
115
115
  | --- | --- | --- |
116
- | `Stylesheets` | `styles` | `app.css` + sayfa sheet’leri (`data-jskelet-css`) |
116
+ | `Stylesheets` | `styles`, `blocking` | `app.css` + sayfa sheet’leri (`data-jskelet-css`). `blocking` varsayılan `true` |
117
117
  | `BodyScripts` | `entries`, `devtools`, `devBasePath` | `main.js`, entry’ler, isteğe bağlı overlay |
118
118
  | `JsonLd` | `items` (`structuredData`) | `application/ld+json` script’leri |
119
119
 
@@ -181,9 +181,12 @@ Dikkat edilecek noktalar:
181
181
 
182
182
  - **`extraHead` en başta.** Kaynak ipuçlarını (`preconnect`, LCP `preload`)
183
183
  geciktirmek doğrudan LCP'ye yazılır.
184
- - **Global `app.css` render-blocking** ve gerekçesi
184
+ - **Global `app.css` varsayılan olarak render-blocking** ve gerekçesi
185
185
  [02-mimari.md](./02-mimari.md)'de. Controller `styles: [...]` ile ek sayfa
186
- sheet'leri de aynı şekilde basılır. Build çalışmadıysa `hasAsset` false olur
186
+ sheet'leri de aynı şekilde basılır. `<Stylesheets :blocking="false" />`
187
+ (`stylesheets({ blocking: false })`) linkleri `media="print"` + `onload`
188
+ ile basar; JS kapalıyken `<noscript>` yedeği uygulanır. Boyama beklenmez,
189
+ sheet gelince düzen kayabilir. Build çalışmadıysa `hasAsset` false olur
187
190
  ve etiket hiç basılmaz.
188
191
  - **`hasAsset` kontrolleri** build eksikken sayfanın 404 veren dosyaları
189
192
  istememesini sağlar (`Stylesheets` / `BodyScripts` içinde).
@@ -432,6 +435,32 @@ icon({ name: "ArrowRight", weight: "bold", size: 20, class: "text-slate-500" });
432
435
  çalışma anında hesaplanan bir çağrı eksik sembole işaret ederse ekranda
433
436
  sessizce boşluk kalır ([08-build.md](./08-build.md)).
434
437
 
438
+ ### `stylesheets(props)`
439
+
440
+ Layout `<head>` içindeki global `app.css` ve controller `styles` dizisi.
441
+ Doğrudan çağırmak yerine yerleşik etiket kullanılır:
442
+
443
+ ```jsk
444
+ <Stylesheets :styles="styles" />
445
+ <Stylesheets :styles="styles" :blocking="false" />
446
+ ```
447
+
448
+ ```js
449
+ stylesheets({ styles: ["home.css"] });
450
+ stylesheets({ styles: ["home.css"], blocking: false });
451
+ ```
452
+
453
+ - `styles` manifest anahtarlarıdır (`"home.css"` → `styles/pages/home.css`).
454
+ `hasAsset` false olanlar basılmaz.
455
+ - `blocking` varsayılan `true`: `<link rel="stylesheet" data-jskelet-css="…">`.
456
+ Yalnızca boolean `false` beklemeyi kapatır. Çıktı `media="print"` ve
457
+ `onload="this.media='all'"` olur; eşleşmeyen media boyamayı bekletmez.
458
+ `<noscript>` içindeki ikinci link, JS kapalıyken stillerin print'te
459
+ kalmamasını sağlar. Inline `onload` çalışmayan bir CSP'de (`script-src`
460
+ içinde `'unsafe-inline'` veya `script-src-attr` yok) sheet `print`'te kalır.
461
+ - `data-jskelet-css` dev hot-swap'ın doğru linki seçmesi içindir; her iki
462
+ modda da yazılır.
463
+
435
464
  ### `preloadImage(props)`
436
465
 
437
466
  ```js
package/docs/06-cache.md CHANGED
@@ -831,9 +831,11 @@ npm install ioredis
831
831
  ```
832
832
 
833
833
  Kurulmadıysa ya da Redis'e bağlanılamıyorsa uyarı basılır ve site **bellek içi
834
- önbellekle çalışmaya devam eder**. Redis çalışırken düşerse aynı şey olur: bir
835
- devre kesici art arda beş hatadan sonra katmanı beş saniye baypas eder, böylece
836
- her istek ağ zaman aşımı beklemez.
834
+ önbellekle çalışmaya devam eder**. Redis çalışırken düşerse aynı şey olur:
835
+ veri ve HTML için ayrı devre kesiciler vardır. Art arda beş hata ilgili
836
+ katmanı beş saniye baypas eder, böylece her istek ağ zaman aşımı beklemez.
837
+ HTML zaman aşımı veri sayacına yazılmaz; yavaş bir sayfa küçük veri
838
+ anahtarlarını kapatmaz.
837
839
 
838
840
  ### Ne kazanırsınız
839
841
 
@@ -852,6 +854,8 @@ her istek ağ zaman aşımı beklemez.
852
854
 
853
855
  ```
854
856
  _jskelet:{namespace}:{buildId}:html:{vary|}{yol}?{query}
857
+ _jskelet:{namespace}:{buildId}:html:{vary|}{yol}?{query}:body
858
+ _jskelet:{namespace}:{buildId}:html:{vary|}{yol}?{query}:enc:{br|gzip}
855
859
  _jskelet:{namespace}:{buildId}:data:{anahtar}
856
860
  _jskelet:{namespace}:events
857
861
  ```
@@ -867,12 +871,25 @@ anahtarlar TTL ile ölür — elle temizlik ya da `FLUSHDB` gerekmez. Build
867
871
  bilinçli olarak `buildId` **taşımaz**: deploy sırasında eski ve yeni sürüm yan
868
872
  yana koşuyor ve bir purge ikisine de ulaşmalı.
869
873
 
870
- 1 KB ve üstü HTML ve veri girdileri düz JSON olarak durmaz: `JSK\x01` önekli
871
- brotli gövde yazılır (kalite 5, metin modu — yanıt sıkıştırmasıyla aynı ayar).
872
- L1 yine çözülmüş nesne tutar; sıkıştırma yalnızca L1 miss'ten sonraki
873
- paylaşımda, yanıtı bekletmeden çalışır. Daha küçük kayıtlar düz JSON kalır,
874
- eski düz JSON kayıtlar da okunur. zstd kullanılmaz: `node:zlib` içindeki zstd
875
- 22.15'ten itibaren var, motor ise `>=22`.
874
+ HTML üç parçaya yazılır. Meta anahtarı küçük bir JSON'dur (`status`,
875
+ `storedAt`, `expiresAt`, `staleUntil`, `produceMs`, `deps`) ve gövde içermez.
876
+ `:body` HTML'in brotli baytıdır; base64 yoktur ve bu bayt bir JSON'un içine
877
+ ikinci kez sıkıştırılmaz. `storeEncoded` açıksa yanıt için üretilmiş brotli
878
+ veya gzip gövdesi `:enc:br` / `:enc:gzip` anahtarına olduğu gibi yazılır.
879
+ Okuma meta ile gövdeyi tek pipeline'da ister; parçalardan biri yoksa kayıt yok
880
+ sayılır ve sayfa render edilir.
881
+
882
+ Veri girdileri 1 KB ve üstünde düz JSON olarak durmaz: `JSK\x01` önekli brotli
883
+ gövde yazılır (kalite 5, metin modu — yanıt sıkıştırmasıyla aynı ayar). Daha
884
+ küçük veri kayıtları düz JSON kalır. HTML bu sarmalayıcıyı kullanmaz. zstd
885
+ kullanılmaz: `node:zlib` içindeki zstd 22.15'ten itibaren var, motor ise
886
+ `>=22`.
887
+
888
+ `commandTimeoutMs` (varsayılan 200) veri, yayın ve `SCAN` komutlarına
889
+ uygulanır. HTML okuma ve yazma ayrı bir bağlantıdadır; `htmlCommandTimeoutMs`
890
+ (varsayılan 2000) yalnız o bağlantının komutlarını sınırlar. ioredis zaman
891
+ aşımı istemci genelinde olduğu için iki süre tek bağlantıda ayrılamaz.
892
+ Uygulama bu alanı vermezse framework 2000 kullanır.
876
893
 
877
894
  Redis kapalıyken (ya da bağlanamadığında) aynı gövde `.jskelet/cache/<buildId>/`
878
895
  altına dosya olarak yazılır. Süreç yeniden açılınca L1 boştur ama diskteki
@@ -889,8 +906,8 @@ bir sonraki yazışta siler. `clearHtmlCache()` ve invalidation dosyayı da dü
889
906
  dışındaki durum kodları da paylaşılmaz.
890
907
  - **Sıkıştırılmış gövdeler varsayılan olarak paylaşılır.** `storeEncoded`
891
908
  (varsayılan `true`) soğuk replica'nın L2 HIT'te yeniden brotli ödemesini
892
- önler; girdi başına boyutu iki-üç katına çıkarır. L2 boyutunu kısıtlamak
893
- için `storeEncoded: false` verin.
909
+ önler. Gövde `:enc:{br|gzip}` anahtarına ham bayt olarak yazılır; meta
910
+ JSON'unun içine base64 girmez. Kapatmak için `storeEncoded: false` verin.
894
911
  - **Yumuşak invalidation Redis kopyasını siler.** Bayatlatmanın Redis karşılığı
895
912
  her anahtar için oku-değiştir-yaz turu demek ve bir webhook binlerce anahtarı
896
913
  birden düşürüyor. Silmenin bedeli, o yolu hiç görmemiş bir kopyanın bir kez
@@ -917,16 +934,18 @@ app.get("/api/healthcheck", (req, res) => {
917
934
  ```
918
935
 
919
936
  Bağlantı kurulmamışken de güvenle çağrılır. Dönen nesne
920
- `{ enabled, connected, keyPrefix, buildId, errors, bypassed }`; `bypassed` devre
921
- kesicinin açık olduğunu, `errors` toplam komut hatasını gösterir. Aynı özet dev
922
- panelinin raporunda da var ([09-dev-araclari.md](./09-dev-araclari.md)).
937
+ `{ enabled, connected, keyPrefix, buildId, errors, bypassed, htmlErrors, htmlBypassed }`.
938
+ `bypassed` veri katmanının, `htmlBypassed` HTML katmanının baypasta olduğunu
939
+ gösterir. `errors` veri komutlarının, `htmlErrors` HTML komutlarının toplam
940
+ hatasıdır. Aynı özet dev panelinin raporunda da var
941
+ ([09-dev-araclari.md](./09-dev-araclari.md)).
923
942
 
924
943
  İki teşhis yüzeyi daha var:
925
944
 
926
945
  | Çağrı | Ne der |
927
946
  | --- | --- |
928
947
  | `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. |
929
- | `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ı. |
948
+ | `inspectRedis()` | Paylaşımlı kademede gerçekten ne durduğu: tür başına kayıt sayısı (HTML'de gövde ve kodlama parçaları tek sayfa sayılır), `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ı. |
930
949
 
931
950
  Ayarların tam listesi: [07-yapilandirma.md](./07-yapilandirma.md).
932
951
 
@@ -1029,7 +1048,7 @@ değil.
1029
1048
  | Clear HTML cache | `clearHtmlCache()` |
1030
1049
  | Clear data cache (önek opsiyonel) | `clearDataCache(prefix)` |
1031
1050
  | Drop shared keys | Redis'teki `html` ya da `data` isim alanını tarar ve düşürür |
1032
- | Count keys in Redis | `inspectRedis()` — tür başına anahtar sayısı, `DBSIZE` ve `used_memory` |
1051
+ | Count keys in Redis | `inspectRedis()` — tür başına kayıt (HTML parçaları tek sayfa sayılır), `DBSIZE` ve `used_memory` |
1033
1052
  | Prewarm | `prewarm()` — tur arkada koşar, ilerleme kartta görünür |
1034
1053
  | Cloudflare purge (her şey / bellekteki URL'ler / prefix / host / tag) | `purgeCloudflare()` |
1035
1054
  | Cloudflare ayarı ya da özelliği değiştirmek | Zone ayarları ve Tiered Cache / Cache Reserve |
@@ -960,15 +960,17 @@ kurulmadıysa ya da bağlanılamıyorsa uyarı basılır ve site bellek içi ön
960
960
  | `keyPrefix` | `string` | `"_jskelet"` | Anahtar düzeninin kökü |
961
961
  | `html` | `boolean` | `true` | HTML gövdeleri paylaşılsın mı |
962
962
  | `data` | `boolean` | `true` | `withDataCache` girdileri paylaşılsın mı |
963
- | `storeEncoded` | `boolean` | `true` | Brotli/gzip gövdeleri de paylaşılsın mı; girdi başına boyutu iki-üç katına çıkarır |
963
+ | `storeEncoded` | `boolean` | `true` | Brotli/gzip gövdeleri ayrı `:enc:` anahtarına yazılsın mı |
964
964
  | `events` | `boolean` | `true` | pub/sub üzerinden invalidation yayını |
965
- | `commandTimeoutMs` | `number` | `200` | Tek bir komutun en fazla bekletebileceği süre |
966
-
967
- Anahtarlar `_jskelet:{namespace}:{buildId}:html:{yol}?{query}` biçiminde yaşar.
968
- `buildId` her build'de değişir, böylece deploy sonrası eski HTML kendiliğinden
969
- geçersiz olur. Kişiye özel (`storable: false`), `degraded` ve 200 dışındaki
970
- yanıtlar paylaşımlı kademeye hiç yazılmaz. Takaslar ve teşhis:
971
- [06-cache.md](./06-cache.md).
965
+ | `commandTimeoutMs` | `number` | `200` | Veri, yayın ve `SCAN` için tek komutun tavanı |
966
+ | `htmlCommandTimeoutMs` | `number` | `2000` | Yalnızca HTML bağlantısının komut tavanı. Verilmezse 2000 |
967
+
968
+ Anahtarlar parçalıdır: `_jskelet:{namespace}:{buildId}:html:{yol}?{query}` küçük
969
+ meta JSON'u, aynı anahtarın `:body` soneki HTML'in brotli baytını, `:enc:br`
970
+ ve `:enc:gzip` ise yanıt kodlamasını tutar. `buildId` her build'de değişir,
971
+ böylece deploy sonrası eski HTML kendiliğinden geçersiz olur. Kişiye özel
972
+ (`storable: false`), `degraded` ve 200 dışındaki yanıtlar paylaşımlı kademeye
973
+ hiç yazılmaz. Takaslar ve teşhis: [06-cache.md](./06-cache.md).
972
974
 
973
975
  ```js
974
976
  redis: {
package/docs/08-build.md CHANGED
@@ -141,8 +141,10 @@ minifikasyon → `writeAsset("app.css", …)`.
141
141
  şekilde yavaşlatıyor.
142
142
  - **lightningcss opsiyoneldir:** yoksa Tailwind'in kendi çıktısı kullanılır,
143
143
  yalnızca birkaç kB daha büyük olur.
144
- - Global çıktı `app.css`'tir ve layout onu her sayfada render-blocking olarak
145
- yükler. Ayrı bir "critical CSS" üretilmemesinin ölçüm gerekçesi
144
+ - Global çıktı `app.css`'tir ve layout onu her sayfada varsayılan olarak
145
+ render-blocking yükler. `<Stylesheets :blocking="false" />` bu beklemeyi
146
+ kapatır ([04-render-ve-sablonlar.md](./04-render-ve-sablonlar.md)). Ayrı bir
147
+ "critical CSS" üretilmemesinin ölçüm gerekçesi
146
148
  [02-mimari.md](./02-mimari.md)'de.
147
149
 
148
150
  ### Sayfa stylesheet'leri (`styles/pages/`)
@@ -166,6 +168,7 @@ kalmalı — dosyada tam `@import "tailwindcss"` utility çıktısını tekrarla
166
168
 
167
169
  Layout `app.css`ten sonra `styles` dizisindeki her sheet için
168
170
  `<link data-jskelet-css="…">` basar; `hasAsset` false ise etiket yok.
171
+ `blocking: false` aynı linklere `media="print"` ve `onload` ekler.
169
172
 
170
173
  ### `@source` direktifleri zorunludur
171
174
 
@@ -160,9 +160,16 @@ both faster and free of CLS; on the second visit it already comes from the
160
160
 
161
161
  Page-specific rules can use `styles/pages/*.css` plus controller
162
162
  `styles: [...]` ([08-build.md](./08-build.md)); those are also render-blocking
163
- but only on the pages that ask for them. Keep Tailwind utilities in the global
164
- sheet — a full `@import "tailwindcss"` in a page sheet duplicates utility
165
- output.
163
+ by default, but only on the pages that ask for them. Keep Tailwind utilities
164
+ in the global sheet — a full `@import "tailwindcss"` in a page sheet
165
+ duplicates utility output.
166
+
167
+ A layout that wants an earlier first paint can leave this default:
168
+ `stylesheets({ blocking: false })`, or `<Stylesheets :blocking="false" />`
169
+ in the template. The link downloads with `media="print"`, `onload` switches
170
+ it to `all`, and a plain link inside `<noscript>` applies when JS is off.
171
+ That is the reverse of the CLS tradeoff above: first paint arrives sooner,
172
+ and the layout can shift when the sheet arrives.
166
173
 
167
174
  The same logic applies to icons: instead of a separate request per icon, an SVG
168
175
  sprite is produced at build time from only the symbols actually used in the
@@ -180,10 +180,14 @@ Points to watch:
180
180
 
181
181
  - **`extraHead` comes first.** Delaying resource hints (`preconnect`, LCP
182
182
  `preload`) writes straight into LCP.
183
- - **`<Stylesheets />` emits global `app.css` (render-blocking)** plus controller
184
- `styles: [...]`, with the reasoning in
185
- [02-architecture.md](./02-architecture.md). If the build has not run,
186
- `hasAsset` is false inside the tag and nothing is emitted.
183
+ - **`<Stylesheets />` emits global `app.css` render-blocking by default**,
184
+ plus controller `styles: [...]`, with the reasoning in
185
+ [02-architecture.md](./02-architecture.md).
186
+ `<Stylesheets :blocking="false" />` (`stylesheets({ blocking: false })`)
187
+ emits the links with `media="print"` plus `onload`, and a `<noscript>`
188
+ fallback when JS is off. Painting is not held, and the layout can shift
189
+ when the sheet arrives. If the build has not run, `hasAsset` is false
190
+ inside the tag and nothing is emitted.
187
191
  - **`<BodyScripts />` emits `main.js`, page `entries`, and the
188
192
  development-only overlay.** The overlay script exists only when
189
193
  `NODE_ENV=development`; it is absent from production output.
@@ -436,6 +440,34 @@ icon({ name: "ArrowRight", weight: "bold", size: 20, class: "text-slate-500" });
436
440
  points at a missing symbol, the screen is silently left blank
437
441
  ([08-build.md](./08-build.md)).
438
442
 
443
+ ### `stylesheets(props)`
444
+
445
+ Global `app.css` and the controller `styles` list for the layout `<head>`.
446
+ The built-in tag is the usual call site:
447
+
448
+ ```jsk
449
+ <Stylesheets :styles="styles" />
450
+ <Stylesheets :styles="styles" :blocking="false" />
451
+ ```
452
+
453
+ ```js
454
+ stylesheets({ styles: ["home.css"] });
455
+ stylesheets({ styles: ["home.css"], blocking: false });
456
+ ```
457
+
458
+ - `styles` are manifest keys (`"home.css"` → `styles/pages/home.css`). Names
459
+ for which `hasAsset` is false are omitted.
460
+ - `blocking` defaults to `true`:
461
+ `<link rel="stylesheet" data-jskelet-css="…">`. Only the boolean `false`
462
+ turns waiting off. The output is `media="print"` and
463
+ `onload="this.media='all'"`; a non-matching media does not hold painting.
464
+ A second link inside `<noscript>` keeps styles from staying on `print`
465
+ when JS is off. Under a CSP that blocks inline handlers (no
466
+ `'unsafe-inline'` in `script-src`, and no `script-src-attr`) the sheet
467
+ stays on `print`.
468
+ - `data-jskelet-css` is how dev hot-swap picks the right link; it is written
469
+ in both modes.
470
+
439
471
  ### `preloadImage(props)`
440
472
 
441
473
  ```js
@@ -849,8 +849,10 @@ npm install ioredis
849
849
 
850
850
  If it is not installed, or Redis cannot be reached, a warning is printed and the
851
851
  site **keeps running on the in-process cache**. The same happens if Redis goes
852
- down while running: a circuit breaker bypasses the tier for five seconds after
853
- five consecutive failures, so requests do not each wait for a network timeout.
852
+ down while running: data and HTML have separate circuit breakers. Five
853
+ consecutive failures bypass that tier for five seconds, so requests do not each
854
+ wait for a network timeout. An HTML timeout is not charged to the data counter,
855
+ so a slow page does not shut the small data keys.
854
856
 
855
857
  ### What you get
856
858
 
@@ -870,6 +872,8 @@ five consecutive failures, so requests do not each wait for a network timeout.
870
872
 
871
873
  ```
872
874
  _jskelet:{namespace}:{buildId}:html:{vary|}{path}?{query}
875
+ _jskelet:{namespace}:{buildId}:html:{vary|}{path}?{query}:body
876
+ _jskelet:{namespace}:{buildId}:html:{vary|}{path}?{query}:enc:{br|gzip}
873
877
  _jskelet:{namespace}:{buildId}:data:{key}
874
878
  _jskelet:{namespace}:events
875
879
  ```
@@ -885,13 +889,25 @@ build has not been run the id is `dev`.
885
889
  deliberately does **not** carry `buildId`: during a deploy the old and the new
886
890
  version run side by side and a purge has to reach both.
887
891
 
888
- HTML and data entries of 1 KB or more are not stored as plain JSON. The shared
889
- tier receives a brotli body prefixed with `JSK\x01` (quality 5, text mode — the
890
- same settings as response compression). L1 still holds the decoded value;
891
- compression runs only when sharing after an L1 miss, and it does not delay the
892
- response. Smaller records stay plain JSON, and older plain JSON records are
893
- still read. zstd is not used: `node:zlib` gained it in 22.15, while the engine
894
- range is `>=22`.
892
+ HTML is stored as three parts. The meta key is a small JSON document
893
+ (`status`, `storedAt`, `expiresAt`, `staleUntil`, `produceMs`, `deps`) and does
894
+ not contain the body. `:body` is the brotli bytes of the HTML: no base64, and
895
+ those bytes are not compressed again inside JSON. When `storeEncoded` is on,
896
+ the brotli or gzip body produced for the response is written as-is to
897
+ `:enc:br` / `:enc:gzip`. A read pipelines meta and body; if either part is
898
+ missing the record counts as absent and the page is rendered.
899
+
900
+ Data entries of 1 KB or more are not stored as plain JSON. They are a brotli
901
+ body prefixed with `JSK\x01` (quality 5, text mode — the same settings as
902
+ response compression). Smaller data records stay plain JSON. HTML does not use
903
+ that wrapper. zstd is not used: `node:zlib` gained it in 22.15, while the
904
+ engine range is `>=22`.
905
+
906
+ `commandTimeoutMs` (default 200) applies to data, publish, and `SCAN`. HTML
907
+ reads and writes use a second connection; `htmlCommandTimeoutMs` (default
908
+ 2000) limits only that connection. ioredis applies `commandTimeout` to the
909
+ whole client, so the two budgets cannot share one connection. When an
910
+ application omits the field, the framework uses 2000.
895
911
 
896
912
  When Redis is off, or cannot connect, the same body is written under
897
913
  `.jskelet/cache/<buildId>/`. After a restart L1 is empty, but a fresh file
@@ -908,8 +924,9 @@ file too.
908
924
  tier: a leak would mean serving one user's HTML to the whole cluster.
909
925
  `degraded` renders and non-200 status codes are not shared either.
910
926
  - **Compressed bodies are shared by default.** `storeEncoded` (default `true`)
911
- keeps a cold replica from paying brotli again on an L2 HIT; it doubles or
912
- triples the size per entry. Set `storeEncoded: false` to keep L2 lean.
927
+ keeps a cold replica from paying brotli again on an L2 HIT. The body is
928
+ stored as raw bytes on an `:enc:{br|gzip}` key; it is not base64 inside the
929
+ meta JSON. Set `storeEncoded: false` to skip those keys.
913
930
  - **A soft invalidation deletes the Redis copy.** Staling in Redis would mean a
914
931
  read-modify-write round per key, and a webhook drops thousands of keys at
915
932
  once. The cost of deleting is one render on a replica that never saw that
@@ -936,9 +953,10 @@ app.get("/api/healthcheck", (req, res) => {
936
953
  ```
937
954
 
938
955
  Safe to call even with no connection. The returned object is
939
- `{ enabled, connected, keyPrefix, buildId, errors, bypassed }`; `bypassed` tells
940
- you the circuit breaker is open and `errors` is the total command failure count.
941
- The same summary is in the dev panel report
956
+ `{ enabled, connected, keyPrefix, buildId, errors, bypassed, htmlErrors, htmlBypassed }`.
957
+ `bypassed` means the data tier is in its bypass window and `htmlBypassed` means
958
+ the HTML connection is. `errors` counts data-command failures and `htmlErrors`
959
+ counts HTML-command failures. The same summary is in the dev panel report
942
960
  ([09-dev-tools.md](./09-dev-tools.md)).
943
961
 
944
962
  Two more diagnostic surfaces:
@@ -946,7 +964,7 @@ Two more diagnostic surfaces:
946
964
  | Call | What it tells you |
947
965
  | --- | --- |
948
966
  | `getRedisDetails()` | **Where** the connection points: address, TLS, database, `namespace`, which kinds are shared, whether the purge channel is subscribed. The password is never returned — a connection URL may carry one. |
949
- | `inspectRedis()` | What is actually in the shared tier: keys per kind, `DBSIZE` and `used_memory`. It runs a `SCAN`, so **never call it on the request path**; in the admin panel it sits behind its own button. |
967
+ | `inspectRedis()` | What is actually in the shared tier: records per kind (HTML body and encoding fragments count as one page), `DBSIZE` and `used_memory`. It runs a `SCAN`, so **never call it on the request path**; in the admin panel it sits behind its own button. |
950
968
 
951
969
  The full list of settings: [07-configuration.md](./07-configuration.md).
952
970
 
@@ -1030,7 +1048,7 @@ export content.
1030
1048
  | Clear HTML cache | `clearHtmlCache()` |
1031
1049
  | Clear data cache (optional prefix) | `clearDataCache(prefix)` |
1032
1050
  | Drop shared keys | Scans and unlinks the `html` or `data` namespace in Redis |
1033
- | Count keys in Redis | `inspectRedis()` — keys per kind, `DBSIZE` and `used_memory` |
1051
+ | Count keys in Redis | `inspectRedis()` — records per kind (HTML fragments count as one page), `DBSIZE` and `used_memory` |
1034
1052
  | Prewarm | `prewarm()` — the pass runs in the background, progress shows in the card |
1035
1053
  | Cloudflare purge (everything / URLs held here / prefix / host / tag) | `purgeCloudflare()` |
1036
1054
  | Change a Cloudflare setting or feature | Zone settings and Tiered Cache / Cache Reserve |
@@ -979,12 +979,15 @@ the site keeps running on the in-process cache.
979
979
  | `keyPrefix` | `string` | `"_jskelet"` | Root of the key layout |
980
980
  | `html` | `boolean` | `true` | Whether HTML bodies are shared |
981
981
  | `data` | `boolean` | `true` | Whether `withDataCache` entries are shared |
982
- | `storeEncoded` | `boolean` | `true` | Whether brotli/gzip bodies are shared too; doubles or triples the size per entry |
982
+ | `storeEncoded` | `boolean` | `true` | Whether brotli/gzip bodies are written to their own `:enc:` keys |
983
983
  | `events` | `boolean` | `true` | Invalidation broadcast over pub/sub |
984
- | `commandTimeoutMs` | `number` | `200` | At most how long a single command may block |
984
+ | `commandTimeoutMs` | `number` | `200` | Per-command ceiling for data, publish, and `SCAN` |
985
+ | `htmlCommandTimeoutMs` | `number` | `2000` | Per-command ceiling for the HTML connection only. `2000` when omitted |
985
986
 
986
- Keys live as `_jskelet:{namespace}:{buildId}:html:{path}?{query}`. `buildId`
987
- changes with every build, so old HTML becomes invalid on its own after a deploy.
987
+ Keys are split: `_jskelet:{namespace}:{buildId}:html:{path}?{query}` is the
988
+ small meta JSON, the `:body` suffix is the brotli bytes of the HTML, and
989
+ `:enc:br` / `:enc:gzip` hold the response encoding. `buildId` changes with
990
+ every build, so old HTML becomes invalid on its own after a deploy.
988
991
  Personalised (`storable: false`), `degraded` and non-200 responses are never
989
992
  written to the shared tier. Trade-offs and diagnosis:
990
993
  [06-caching.md](./06-caching.md).
@@ -145,9 +145,11 @@ The pipeline: PostCSS + `@tailwindcss/postcss` → minification with lightningcs
145
145
  noticeably.
146
146
  - **lightningcss is optional:** without it, Tailwind's own output is used, and it
147
147
  is only a few kB bigger.
148
- - The global output is `app.css` and the layout loads it render-blocking on every
149
- page. The measurement-based reasoning for not producing a separate "critical
150
- CSS" is in [02-architecture.md](./02-architecture.md).
148
+ - The global output is `app.css` and the layout loads it render-blocking on
149
+ every page by default. `<Stylesheets :blocking="false" />` turns that wait
150
+ off ([04-rendering.md](./04-rendering.md)). The measurement-based reasoning
151
+ for not producing a separate "critical CSS" is in
152
+ [02-architecture.md](./02-architecture.md).
151
153
 
152
154
  ### Page stylesheets (`styles/pages/`)
153
155
 
@@ -170,7 +172,8 @@ Page CSS is for page-specific rules. Keep Tailwind utilities in the global
170
172
  sheet — a full `@import "tailwindcss"` in a page file duplicates utility output.
171
173
 
172
174
  After `app.css`, the layout emits a `<link data-jskelet-css="…">` for each name
173
- in `styles`; if `hasAsset` is false the tag is omitted.
175
+ in `styles`; if `hasAsset` is false the tag is omitted. `blocking: false`
176
+ adds `media="print"` and `onload` to those same links.
174
177
 
175
178
  ### `@source` directives are mandatory
176
179
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jskelet",
3
- "version": "0.7.2",
3
+ "version": "0.7.4",
4
4
  "description": "A framework that feels like no framework: Express 5 + build-time .jsk SSR (optional EJS peer), vanilla JS islands, Tailwind v4 and an in-process HTML TTL cache.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -10,7 +10,8 @@
10
10
  * CLS 0.307) ve aynı ~27 KB her HTML yanıtında tekrar ediyordu. Sıkıştırılmış
11
11
  * global sheet'i render-blocking bırakmak hem daha hızlı hem CLS'siz; ikinci
12
12
  * ziyarette zaten immutable önbellekten geliyor. Sayfa sheet'leri de
13
- * render-blocking; yalnızca ilgili sayfada ek maliyet.
13
+ * varsayılan olarak render-blocking; yalnızca ilgili sayfada ek maliyet.
14
+ * Layout `stylesheets({ blocking: false })` ile bu beklemeyi kapatabilir.
14
15
  *
15
16
  * Tailwind utility'leri global sheet'te kalmalı. Sayfa CSS'inde tam
16
17
  * `@import "tailwindcss"` utility çıktısını tekrarlar; sayfaya özel kurallar
@@ -90,7 +90,9 @@ export const MESSAGES = {
90
90
  "redis.broadcast.publish": "publish only",
91
91
  "redis.broadcast.local": "local only",
92
92
  "redis.timeout": "Command timeout",
93
+ "redis.htmlTimeout": "HTML command timeout",
93
94
  "redis.errors": "Command errors",
95
+ "redis.htmlErrors": "HTML command errors",
94
96
  "redis.tier": "Tier",
95
97
  "redis.tier.inProcess": "in-process only",
96
98
  "redis.replicas": "Replicas sharing this cache",
@@ -394,7 +396,9 @@ export const MESSAGES = {
394
396
  "redis.broadcast.publish": "yalnızca yayın",
395
397
  "redis.broadcast.local": "yalnızca yerel",
396
398
  "redis.timeout": "Komut zaman aşımı",
399
+ "redis.htmlTimeout": "HTML komut zaman aşımı",
397
400
  "redis.errors": "Komut hatası",
401
+ "redis.htmlErrors": "HTML komut hatası",
398
402
  "redis.tier": "Kademe",
399
403
  "redis.tier.inProcess": "yalnızca süreç içi",
400
404
  "redis.replicas": "Bu önbelleği paylaşan kopya",
@@ -556,8 +556,12 @@ function renderRedis(redis, state) {
556
556
  [t("redis.buildId"), redis.buildId || "dev"],
557
557
  [
558
558
  t("redis.sharesHtml"),
559
- redis.html ? t("state.yes") : t("state.no"),
560
- redis.html ? "on" : "off",
559
+ !redis.html
560
+ ? t("state.no")
561
+ : redis.htmlBypassed
562
+ ? t("state.bypassed")
563
+ : t("state.yes"),
564
+ redis.html && !redis.htmlBypassed ? "on" : "off",
561
565
  ],
562
566
  [
563
567
  t("redis.sharesData"),
@@ -579,7 +583,9 @@ function renderRedis(redis, state) {
579
583
  redis.events && redis.subscribed ? "on" : "off",
580
584
  ],
581
585
  [t("redis.timeout"), `${redis.commandTimeoutMs} ${t("unit.ms")}`],
586
+ [t("redis.htmlTimeout"), `${redis.htmlCommandTimeoutMs} ${t("unit.ms")}`],
582
587
  [t("redis.errors"), String(redis.errors), redis.errors ? "bad" : "on"],
588
+ [t("redis.htmlErrors"), String(redis.htmlErrors ?? 0), redis.htmlErrors ? "bad" : "on"],
583
589
  ]
584
590
  : [
585
591
  [t("redis.tier"), t("redis.tier.inProcess")],
@@ -297,11 +297,19 @@ export const DEFAULT_REDIS = {
297
297
  /** pub/sub üzerinden invalidation yayını. */
298
298
  events: true,
299
299
  /**
300
- * Tek bir komutun en fazla bekletebileceği süre. Önbellek okuması isteği
300
+ * Veri, yayın ve SCAN için tek komut tavanı. Önbellek okuması isteği
301
301
  * bloklayan bir adım: Redis takıldığında render'a düşmek, ağı beklemekten
302
- * iyidir.
302
+ * iyidir. HTML gövdesi bu süreye sığmaz; o bağlantının tavanı
303
+ * `htmlCommandTimeoutMs`.
303
304
  */
304
305
  commandTimeoutMs: 200,
306
+ /**
307
+ * Yalnızca HTML bağlantısı. ioredis `commandTimeout` istemci genelinde
308
+ * olduğu için gövde ayrı bağlantıda durur. Verilmezse 2000: uygulama
309
+ * config'i değiştirmek zorunda kalmasın. Parçalı yazma normalde bunun
310
+ * altında biter; tavan, gövde beklenmedik büyürse diye durur.
311
+ */
312
+ htmlCommandTimeoutMs: 2000,
305
313
  };
306
314
 
307
315
  /**
@@ -95,7 +95,8 @@ const CONFIG_FILE = "jskelet.config.mjs";
95
95
  * @property {boolean} data Veri önbelleği paylaşılsın mı.
96
96
  * @property {boolean} storeEncoded Sıkıştırılmış gövdeler de paylaşılsın mı.
97
97
  * @property {boolean} events pub/sub invalidation yayını.
98
- * @property {number} commandTimeoutMs
98
+ * @property {number} commandTimeoutMs Veri, yayın ve SCAN tavanı.
99
+ * @property {number} htmlCommandTimeoutMs Yalnızca HTML bağlantısının tavanı.
99
100
  */
100
101
 
101
102
  /**
@@ -340,6 +341,7 @@ function normalizePriority(raw) {
340
341
  function normalizeRedis(raw) {
341
342
  const source = /** @type {Record<string, any>} */ (raw ?? {});
342
343
  const timeout = Number(source.commandTimeoutMs);
344
+ const htmlTimeout = Number(source.htmlCommandTimeoutMs);
343
345
 
344
346
  return {
345
347
  enabled: source.enabled === true,
@@ -354,6 +356,10 @@ function normalizeRedis(raw) {
354
356
  Number.isFinite(timeout) && timeout > 0
355
357
  ? Math.floor(timeout)
356
358
  : DEFAULT_REDIS.commandTimeoutMs,
359
+ htmlCommandTimeoutMs:
360
+ Number.isFinite(htmlTimeout) && htmlTimeout > 0
361
+ ? Math.floor(htmlTimeout)
362
+ : DEFAULT_REDIS.htmlCommandTimeoutMs,
357
363
  };
358
364
  }
359
365