jskelet 0.7.3 → 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 +6 -0
- package/docs/06-cache.md +35 -16
- package/docs/07-yapilandirma.md +10 -8
- package/docs/en/06-caching.md +34 -16
- package/docs/en/07-configuration.md +7 -4
- package/package.json +1 -1
- package/src/client/admin/i18n.js +4 -0
- package/src/client/admin/panel.js +8 -2
- package/src/config/defaults.js +10 -2
- package/src/config/index.js +7 -1
- package/src/server/html-cache.js +90 -35
- package/src/server/redis.js +332 -10
- package/types/config/defaults.d.ts +10 -2
- package/types/config/index.d.ts +7 -0
- package/types/server/dev/report.d.ts +2 -0
- package/types/server/redis.d.ts +43 -3
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,9 @@ 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.
|
|
76
82
|
- `stylesheets({ blocking: false })` and `<Stylesheets :blocking="false" />`
|
|
77
83
|
emit each stylesheet with `media="print"` and an `onload` that switches it
|
|
78
84
|
to `all`, plus a `<noscript>` fallback, so the links do not block rendering.
|
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:
|
|
835
|
-
|
|
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
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
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
|
|
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
|
|
921
|
-
|
|
922
|
-
|
|
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
|
|
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
|
|
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 |
|
package/docs/07-yapilandirma.md
CHANGED
|
@@ -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
|
|
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` |
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
`buildId`
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
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/en/06-caching.md
CHANGED
|
@@ -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:
|
|
853
|
-
|
|
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
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
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
|
|
912
|
-
|
|
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
|
|
940
|
-
|
|
941
|
-
|
|
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:
|
|
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()` —
|
|
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
|
|
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` |
|
|
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
|
|
987
|
-
|
|
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).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jskelet",
|
|
3
|
-
"version": "0.7.
|
|
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",
|
package/src/client/admin/i18n.js
CHANGED
|
@@ -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
|
|
560
|
-
|
|
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")],
|
package/src/config/defaults.js
CHANGED
|
@@ -297,11 +297,19 @@ export const DEFAULT_REDIS = {
|
|
|
297
297
|
/** pub/sub üzerinden invalidation yayını. */
|
|
298
298
|
events: true,
|
|
299
299
|
/**
|
|
300
|
-
*
|
|
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
|
/**
|
package/src/config/index.js
CHANGED
|
@@ -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
|
|
package/src/server/html-cache.js
CHANGED
|
@@ -46,8 +46,8 @@ import {
|
|
|
46
46
|
publishCacheEvent,
|
|
47
47
|
redisDrop,
|
|
48
48
|
redisDropMatching,
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
redisGetHtml,
|
|
50
|
+
redisShareHtml,
|
|
51
51
|
redisShares,
|
|
52
52
|
redisSharesEncoded,
|
|
53
53
|
} from "./redis.js";
|
|
@@ -372,6 +372,10 @@ function read(key) {
|
|
|
372
372
|
* Girdiyi paylaşımlı kademeye yazar. Ateşle-unut: yanıt yolunda beklenmez,
|
|
373
373
|
* L1 kopyası bu isteği zaten karşılıyor.
|
|
374
374
|
*
|
|
375
|
+
* Redis'te gövde meta JSON'unun içinde durmaz. 1 MB HTML + base64 kodlama
|
|
376
|
+
* tek SET'te `commandTimeoutMs`'i aşıyordu ve zaman aşımı veri sayacını da
|
|
377
|
+
* kapatıyordu. Meta küçük kalır; brotli bayt ayrı anahtardadır.
|
|
378
|
+
*
|
|
375
379
|
* @param {string} key
|
|
376
380
|
* @param {HtmlEntry} entry
|
|
377
381
|
*/
|
|
@@ -383,8 +387,12 @@ function share(key, entry) {
|
|
|
383
387
|
const ttlMs = entry.staleUntil - Date.now();
|
|
384
388
|
if (ttlMs <= 0) return;
|
|
385
389
|
|
|
386
|
-
|
|
387
|
-
|
|
390
|
+
if (onRedis) {
|
|
391
|
+
shareOnRedis(key, entry, ttlMs);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
diskSetJson("html", key, {
|
|
388
396
|
html: entry.html,
|
|
389
397
|
status: entry.status,
|
|
390
398
|
storedAt: entry.storedAt,
|
|
@@ -392,20 +400,41 @@ function share(key, entry) {
|
|
|
392
400
|
staleUntil: entry.staleUntil,
|
|
393
401
|
produceMs: entry.produceMs,
|
|
394
402
|
deps: [...entry.deps],
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* @param {string} key
|
|
408
|
+
* @param {HtmlEntry} entry
|
|
409
|
+
* @param {number} ttlMs
|
|
410
|
+
*/
|
|
411
|
+
function shareOnRedis(key, entry, ttlMs) {
|
|
412
|
+
/** @type {Record<string, unknown>} */
|
|
413
|
+
const meta = {
|
|
414
|
+
status: entry.status,
|
|
415
|
+
storedAt: entry.storedAt,
|
|
416
|
+
expiresAt: entry.expiresAt,
|
|
417
|
+
staleUntil: entry.staleUntil,
|
|
418
|
+
produceMs: entry.produceMs,
|
|
419
|
+
deps: [...entry.deps],
|
|
395
420
|
};
|
|
396
421
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
422
|
+
// Yanıt brotli'si HTML brotli'si ile aynı bayt (kalite 5, metin). İkinci
|
|
423
|
+
// sıkıştırma yok; `enc:br` storeEncoded açıksa bu tamponun kopyasıdır.
|
|
424
|
+
const encoded = redisSharesEncoded() && entry.encoded.size ? entry.encoded : null;
|
|
425
|
+
if (encoded) entry.sharedEncodings = entry.encoded.size;
|
|
426
|
+
|
|
427
|
+
const ready = entry.encoded.get("br");
|
|
428
|
+
if (ready) {
|
|
429
|
+
redisShareHtml(key, meta, ready, encoded, ttlMs);
|
|
430
|
+
return;
|
|
405
431
|
}
|
|
406
432
|
|
|
407
|
-
|
|
408
|
-
|
|
433
|
+
encodeText(entry.html, "br")
|
|
434
|
+
.then((body) => redisShareHtml(key, meta, body, encoded, ttlMs))
|
|
435
|
+
.catch(() => {
|
|
436
|
+
// Gövdesiz meta okununca kayıt yok sayılır; yarım yazmanın faydası yok.
|
|
437
|
+
});
|
|
409
438
|
}
|
|
410
439
|
|
|
411
440
|
/**
|
|
@@ -441,16 +470,24 @@ async function readShared(key) {
|
|
|
441
470
|
sharedMissUntil.delete(key);
|
|
442
471
|
}
|
|
443
472
|
|
|
444
|
-
const payload = onRedis
|
|
445
|
-
|
|
446
|
-
|
|
473
|
+
const payload = onRedis ? await redisGetHtml(key) : await diskGetJson("html", key);
|
|
474
|
+
return finishShared(key, payload, !onRedis);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* @param {string} key
|
|
479
|
+
* @param {any} payload
|
|
480
|
+
* @param {boolean} fromDisk
|
|
481
|
+
* @returns {HtmlEntry | null}
|
|
482
|
+
*/
|
|
483
|
+
function finishShared(key, payload, fromDisk) {
|
|
447
484
|
if (!payload || typeof payload.html !== "string") {
|
|
448
485
|
rememberSharedMiss(key);
|
|
449
486
|
return null;
|
|
450
487
|
}
|
|
451
488
|
if (typeof payload.expiresAt !== "number" || Date.now() >= payload.expiresAt) {
|
|
452
489
|
// Dosyanın TTL'i yok; süresi dolmuş kopya okununca silinir.
|
|
453
|
-
if (
|
|
490
|
+
if (fromDisk) diskDrop("html", [key]);
|
|
454
491
|
rememberSharedMiss(key);
|
|
455
492
|
return null;
|
|
456
493
|
}
|
|
@@ -464,23 +501,7 @@ async function readShared(key) {
|
|
|
464
501
|
// etmek olurdu.
|
|
465
502
|
if (readsPurgedData(deps, storedAt)) return null;
|
|
466
503
|
|
|
467
|
-
|
|
468
|
-
const encoded = new Map();
|
|
469
|
-
if (payload.encoded && typeof payload.encoded === "object") {
|
|
470
|
-
for (const [encoding, base64] of Object.entries(payload.encoded)) {
|
|
471
|
-
if (typeof base64 === "string") {
|
|
472
|
-
encoded.set(encoding, Buffer.from(base64, "base64"));
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
// Eski bir replica iki kodlama yazmış olabilir. L1 tek kopya tutar;
|
|
476
|
-
// brotli varsa o kalır.
|
|
477
|
-
if (encoded.size > 1) {
|
|
478
|
-
const keep = encoded.has("br") ? "br" : encoded.keys().next().value;
|
|
479
|
-
const buffer = keep ? encoded.get(keep) : undefined;
|
|
480
|
-
encoded.clear();
|
|
481
|
-
if (keep && buffer) encoded.set(keep, buffer);
|
|
482
|
-
}
|
|
483
|
-
}
|
|
504
|
+
const encoded = normalizeEncoded(payload.encoded);
|
|
484
505
|
|
|
485
506
|
return {
|
|
486
507
|
html: payload.html,
|
|
@@ -497,6 +518,40 @@ async function readShared(key) {
|
|
|
497
518
|
};
|
|
498
519
|
}
|
|
499
520
|
|
|
521
|
+
/**
|
|
522
|
+
* Disk hâlâ base64 taşır. Redis `Map<string, Buffer>` döner. L1 tek kodlama
|
|
523
|
+
* tutar; brotli varsa o kalır.
|
|
524
|
+
*
|
|
525
|
+
* @param {unknown} raw
|
|
526
|
+
* @returns {Map<string, Buffer>}
|
|
527
|
+
*/
|
|
528
|
+
function normalizeEncoded(raw) {
|
|
529
|
+
/** @type {Map<string, Buffer>} */
|
|
530
|
+
const encoded = new Map();
|
|
531
|
+
|
|
532
|
+
if (raw instanceof Map) {
|
|
533
|
+
for (const [encoding, buffer] of raw) {
|
|
534
|
+
if (typeof encoding === "string" && Buffer.isBuffer(buffer) && buffer.length) {
|
|
535
|
+
encoded.set(encoding, buffer);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
} else if (raw && typeof raw === "object") {
|
|
539
|
+
for (const [encoding, value] of Object.entries(raw)) {
|
|
540
|
+
if (typeof value === "string") encoded.set(encoding, Buffer.from(value, "base64"));
|
|
541
|
+
else if (Buffer.isBuffer(value) && value.length) encoded.set(encoding, value);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
if (encoded.size > 1) {
|
|
546
|
+
const keep = encoded.has("br") ? "br" : encoded.keys().next().value;
|
|
547
|
+
const buffer = keep ? encoded.get(keep) : undefined;
|
|
548
|
+
encoded.clear();
|
|
549
|
+
if (keep && buffer) encoded.set(keep, buffer);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
return encoded;
|
|
553
|
+
}
|
|
554
|
+
|
|
500
555
|
/**
|
|
501
556
|
* @param {string} key
|
|
502
557
|
* @param {{ html: string, status: number }} value
|
package/src/server/redis.js
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* fırlatmaz; okuma `null`, yazma sessiz no-op'a döner.
|
|
20
20
|
*/
|
|
21
21
|
import crypto from "node:crypto";
|
|
22
|
+
import zlib from "node:zlib";
|
|
22
23
|
import { DEFAULT_REDIS } from "../config/defaults.js";
|
|
23
24
|
import { tryImportFromApp } from "../build/resolve-peer.mjs";
|
|
24
25
|
import { getBuildId } from "./assets.js";
|
|
@@ -32,6 +33,14 @@ import { decodeCacheValueAsync, encodeCacheValue } from "./cache-blob.js";
|
|
|
32
33
|
|
|
33
34
|
/** @type {any} */
|
|
34
35
|
let client = null;
|
|
36
|
+
/**
|
|
37
|
+
* HTML GET/SET. `commandTimeout` ioredis'te istemci genelinde: gövde yazması
|
|
38
|
+
* veri okumasının 200 ms tavanını paylaşırsa yavaş sayfa küçük anahtarları da
|
|
39
|
+
* bekletir. Ayrı bağlantı bu yüzden var.
|
|
40
|
+
*
|
|
41
|
+
* @type {any}
|
|
42
|
+
*/
|
|
43
|
+
let htmlClient = null;
|
|
35
44
|
/** @type {any} */
|
|
36
45
|
let subscriber = null;
|
|
37
46
|
|
|
@@ -53,18 +62,27 @@ const originId = crypto.randomUUID();
|
|
|
53
62
|
const listeners = [];
|
|
54
63
|
|
|
55
64
|
let errors = 0;
|
|
65
|
+
let htmlErrors = 0;
|
|
56
66
|
|
|
57
67
|
/**
|
|
58
68
|
* Devre kesici. Redis düştüğünde her istek `commandTimeoutMs` beklemesin:
|
|
59
69
|
* art arda gelen hatalardan sonra istemci bir süre baypas edilir. Süre
|
|
60
70
|
* dolduğunda tek bir komut denenir ve başarılıysa sayaç sıfırlanır.
|
|
71
|
+
*
|
|
72
|
+
* HTML'in sayacı ayrı. Gövde zaman aşımı `consecutiveFailures`'a yazılmaz:
|
|
73
|
+
* beş yavaş sayfa, veri önbelleğini beş saniye kapatmasın.
|
|
61
74
|
*/
|
|
62
75
|
let consecutiveFailures = 0;
|
|
63
76
|
let bypassUntil = 0;
|
|
77
|
+
let htmlConsecutiveFailures = 0;
|
|
78
|
+
let htmlBypassUntil = 0;
|
|
64
79
|
|
|
65
80
|
const MAX_FAILURES = 5;
|
|
66
81
|
const BYPASS_MS = 5000;
|
|
67
82
|
|
|
83
|
+
/** Yanıt için saklanan kodlamalar. Okuma bunları aynı pipeline'a koyar. */
|
|
84
|
+
const HTML_RESPONSE_ENCODINGS = ["br", "gzip"];
|
|
85
|
+
|
|
68
86
|
/** @returns {boolean} */
|
|
69
87
|
function usable() {
|
|
70
88
|
if (!client) return false;
|
|
@@ -72,6 +90,13 @@ function usable() {
|
|
|
72
90
|
return true;
|
|
73
91
|
}
|
|
74
92
|
|
|
93
|
+
/** @returns {boolean} */
|
|
94
|
+
function htmlUsable() {
|
|
95
|
+
if (!htmlClient) return false;
|
|
96
|
+
if (htmlBypassUntil && Date.now() < htmlBypassUntil) return false;
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
|
|
75
100
|
/**
|
|
76
101
|
* @param {string} action
|
|
77
102
|
* @param {unknown} error
|
|
@@ -101,6 +126,36 @@ function noteSuccess() {
|
|
|
101
126
|
bypassUntil = 0;
|
|
102
127
|
}
|
|
103
128
|
|
|
129
|
+
/**
|
|
130
|
+
* HTML bağlantısının hataları. Veri sayacına dokunmaz.
|
|
131
|
+
*
|
|
132
|
+
* @param {string} action
|
|
133
|
+
* @param {unknown} error
|
|
134
|
+
*/
|
|
135
|
+
function noteHtmlFailure(action, error) {
|
|
136
|
+
htmlErrors += 1;
|
|
137
|
+
htmlConsecutiveFailures += 1;
|
|
138
|
+
|
|
139
|
+
if (htmlConsecutiveFailures >= MAX_FAILURES && !htmlBypassUntil) {
|
|
140
|
+
htmlBypassUntil = Date.now() + BYPASS_MS;
|
|
141
|
+
console.warn(
|
|
142
|
+
`[redis] ${MAX_FAILURES} consecutive html failures — bypassing html for ${BYPASS_MS}ms`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (htmlConsecutiveFailures <= MAX_FAILURES) {
|
|
147
|
+
console.warn(
|
|
148
|
+
`[redis] html ${action} failed`,
|
|
149
|
+
error instanceof Error ? error.message : error,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function noteHtmlSuccess() {
|
|
155
|
+
htmlConsecutiveFailures = 0;
|
|
156
|
+
htmlBypassUntil = 0;
|
|
157
|
+
}
|
|
158
|
+
|
|
104
159
|
/**
|
|
105
160
|
* Bağlantıyı kurar. `createApp()` config yüklendikten sonra bir kez çağırır.
|
|
106
161
|
*
|
|
@@ -135,6 +190,7 @@ export async function connectRedis(config) {
|
|
|
135
190
|
// ayağa kalkmalı, sadece L2 devre dışı kalır.
|
|
136
191
|
lazyConnect: true,
|
|
137
192
|
// Tek bir komut isteği bloklayan adım; kuyrukta birikmesine izin verilmez.
|
|
193
|
+
// Bu tavan veri, yayın ve SCAN içindir. HTML ayrı bağlantıda.
|
|
138
194
|
commandTimeout: redis.commandTimeoutMs,
|
|
139
195
|
maxRetriesPerRequest: 1,
|
|
140
196
|
enableOfflineQueue: false,
|
|
@@ -165,12 +221,52 @@ export async function connectRedis(config) {
|
|
|
165
221
|
// sürüm yan yana koşuyor ve purge mesajı ikisine de ulaşmalı.
|
|
166
222
|
channel = `${redis.keyPrefix}:${redis.namespace}:events`;
|
|
167
223
|
|
|
224
|
+
if (redis.html) await connectHtml(Redis, redis, options);
|
|
168
225
|
if (redis.events) await subscribe(Redis, redis, options);
|
|
169
226
|
|
|
170
227
|
console.log(`[redis] connected — keys under \`${prefix}:\``);
|
|
171
228
|
return true;
|
|
172
229
|
}
|
|
173
230
|
|
|
231
|
+
/**
|
|
232
|
+
* HTML gövdesi için ayrı bağlantı. `duplicate` host, şifre ve db'yi kopyalar;
|
|
233
|
+
* `commandTimeout` ise bu istemciye özel kalır.
|
|
234
|
+
*
|
|
235
|
+
* Bağlanamazsa veri katmanı ayakta kalır — HTML L1'de durur.
|
|
236
|
+
*
|
|
237
|
+
* @param {Function} Redis
|
|
238
|
+
* @param {RedisConfig} redis
|
|
239
|
+
* @param {Record<string, unknown>} options
|
|
240
|
+
*/
|
|
241
|
+
async function connectHtml(Redis, redis, options) {
|
|
242
|
+
const htmlOptions = { ...options, commandTimeout: redis.htmlCommandTimeoutMs };
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
htmlClient =
|
|
246
|
+
typeof client.duplicate === "function"
|
|
247
|
+
? client.duplicate({ commandTimeout: redis.htmlCommandTimeoutMs })
|
|
248
|
+
: redis.url
|
|
249
|
+
? new Redis(redis.url, htmlOptions)
|
|
250
|
+
: new Redis(htmlOptions);
|
|
251
|
+
|
|
252
|
+
htmlClient.on("error", (error) => noteHtmlFailure("connection", error));
|
|
253
|
+
if (htmlClient.status !== "ready" && htmlClient.status !== "connecting") {
|
|
254
|
+
await htmlClient.connect();
|
|
255
|
+
}
|
|
256
|
+
} catch (error) {
|
|
257
|
+
console.warn(
|
|
258
|
+
"[redis] html connection failed — HTML stays in-process:",
|
|
259
|
+
error instanceof Error ? error.message : error,
|
|
260
|
+
);
|
|
261
|
+
try {
|
|
262
|
+
htmlClient?.disconnect();
|
|
263
|
+
} catch {
|
|
264
|
+
// Zaten kopmuş olabilir.
|
|
265
|
+
}
|
|
266
|
+
htmlClient = null;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
174
270
|
/**
|
|
175
271
|
* pub/sub için ikinci bir bağlantı: abone olmuş bir istemci normal komut
|
|
176
272
|
* kabul etmez.
|
|
@@ -264,7 +360,8 @@ export function publishCacheEvent(event) {
|
|
|
264
360
|
* @returns {boolean}
|
|
265
361
|
*/
|
|
266
362
|
export function redisShares(kind) {
|
|
267
|
-
|
|
363
|
+
if (settings[kind] !== true) return false;
|
|
364
|
+
return kind === "html" ? htmlUsable() : usable();
|
|
268
365
|
}
|
|
269
366
|
|
|
270
367
|
/** @returns {boolean} */
|
|
@@ -281,6 +378,192 @@ export function cacheKey(kind, key) {
|
|
|
281
378
|
return `${prefix}:${kind}:${key}`;
|
|
282
379
|
}
|
|
283
380
|
|
|
381
|
+
/**
|
|
382
|
+
* HTML kaydını parçalı yazar. Meta küçük JSON'dur; gövde ham brotli baytıdır
|
|
383
|
+
* (base64 yok, JSON içinde ikinci brotli yok). `encoded` varsa her kodlama
|
|
384
|
+
* `:enc:` anahtarına olduğu gibi gider.
|
|
385
|
+
*
|
|
386
|
+
* Ateşle-unut ve tek sayaç adımı: üç SET'ten biri düşerse bu bir hatadır,
|
|
387
|
+
* veri devre kesicisine yazılmaz.
|
|
388
|
+
*
|
|
389
|
+
* @param {string} key Önbellek anahtarı (`yol?query`), öneksiz.
|
|
390
|
+
* @param {Record<string, unknown>} meta
|
|
391
|
+
* @param {Buffer} body
|
|
392
|
+
* @param {Map<string, Buffer> | null} encoded
|
|
393
|
+
* @param {number} ttlMs
|
|
394
|
+
*/
|
|
395
|
+
export function redisShareHtml(key, meta, body, encoded, ttlMs) {
|
|
396
|
+
if (!htmlUsable() || !(ttlMs > 0)) return;
|
|
397
|
+
|
|
398
|
+
/** @type {string} */
|
|
399
|
+
let json;
|
|
400
|
+
try {
|
|
401
|
+
json = JSON.stringify(meta);
|
|
402
|
+
} catch (error) {
|
|
403
|
+
noteHtmlFailure("serialize", error);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const base = cacheKey("html", key);
|
|
408
|
+
const px = Math.ceil(ttlMs);
|
|
409
|
+
/** @type {Promise<unknown>[]} */
|
|
410
|
+
const writes = [
|
|
411
|
+
htmlClient.set(base, json, "PX", px),
|
|
412
|
+
htmlClient.set(`${base}:body`, body, "PX", px),
|
|
413
|
+
];
|
|
414
|
+
|
|
415
|
+
if (encoded) {
|
|
416
|
+
for (const [encoding, buffer] of encoded) {
|
|
417
|
+
if (!encoding || !Buffer.isBuffer(buffer) || !buffer.length) continue;
|
|
418
|
+
writes.push(htmlClient.set(`${base}:enc:${encoding}`, buffer, "PX", px));
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
Promise.all(writes)
|
|
423
|
+
.then(() => noteHtmlSuccess())
|
|
424
|
+
.catch((error) => noteHtmlFailure("set", error));
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Meta + gövde (+ açıksa kodlamalar) tek pipeline. Parça eksikse kayıt yok
|
|
429
|
+
* sayılır; bu bir hata değildir. Bağlantı hatası HTML sayacına yazılır.
|
|
430
|
+
*
|
|
431
|
+
* @param {string} key Önbellek anahtarı (`yol?query`), öneksiz.
|
|
432
|
+
* @returns {Promise<{ html: string, status: unknown, storedAt: unknown,
|
|
433
|
+
* expiresAt: unknown, staleUntil: unknown, produceMs: unknown, deps: unknown,
|
|
434
|
+
* encoded: Map<string, Buffer> } | null>}
|
|
435
|
+
*/
|
|
436
|
+
export async function redisGetHtml(key) {
|
|
437
|
+
if (!htmlUsable()) return null;
|
|
438
|
+
|
|
439
|
+
const base = cacheKey("html", key);
|
|
440
|
+
/** @type {string[]} */
|
|
441
|
+
const keys = [base, `${base}:body`];
|
|
442
|
+
if (settings.storeEncoded === true) {
|
|
443
|
+
for (const encoding of HTML_RESPONSE_ENCODINGS) keys.push(`${base}:enc:${encoding}`);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
try {
|
|
447
|
+
const values = await readHtmlBuffers(keys);
|
|
448
|
+
noteHtmlSuccess();
|
|
449
|
+
|
|
450
|
+
const metaRaw = values[0];
|
|
451
|
+
const bodyRaw = values[1];
|
|
452
|
+
if (metaRaw == null || bodyRaw == null) return null;
|
|
453
|
+
|
|
454
|
+
const metaText = asText(metaRaw);
|
|
455
|
+
if (!metaText) return null;
|
|
456
|
+
const meta = JSON.parse(metaText);
|
|
457
|
+
if (!meta || typeof meta !== "object") return null;
|
|
458
|
+
|
|
459
|
+
const html = await inflateBrotli(bodyRaw);
|
|
460
|
+
/** @type {Map<string, Buffer>} */
|
|
461
|
+
const encoded = new Map();
|
|
462
|
+
if (settings.storeEncoded === true) {
|
|
463
|
+
HTML_RESPONSE_ENCODINGS.forEach((encoding, index) => {
|
|
464
|
+
const buffer = asBuffer(values[2 + index]);
|
|
465
|
+
if (buffer?.length) encoded.set(encoding, buffer);
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return {
|
|
470
|
+
status: meta.status,
|
|
471
|
+
storedAt: meta.storedAt,
|
|
472
|
+
expiresAt: meta.expiresAt,
|
|
473
|
+
staleUntil: meta.staleUntil,
|
|
474
|
+
produceMs: meta.produceMs,
|
|
475
|
+
deps: meta.deps,
|
|
476
|
+
html,
|
|
477
|
+
encoded,
|
|
478
|
+
};
|
|
479
|
+
} catch (error) {
|
|
480
|
+
noteHtmlFailure("get", error);
|
|
481
|
+
return null;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* @param {string[]} keys
|
|
487
|
+
* @returns {Promise<(Buffer | string | null)[]>}
|
|
488
|
+
*/
|
|
489
|
+
async function readHtmlBuffers(keys) {
|
|
490
|
+
if (typeof htmlClient.pipeline === "function") {
|
|
491
|
+
const pipe = htmlClient.pipeline();
|
|
492
|
+
const getter = typeof pipe.getBuffer === "function" ? "getBuffer" : "get";
|
|
493
|
+
for (const key of keys) pipe[getter](key);
|
|
494
|
+
const rows = await pipe.exec();
|
|
495
|
+
if (!Array.isArray(rows)) return keys.map(() => null);
|
|
496
|
+
return rows.map((row) => {
|
|
497
|
+
if (!row) return null;
|
|
498
|
+
const [error, value] = row;
|
|
499
|
+
if (error) throw error;
|
|
500
|
+
return value ?? null;
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const getter = typeof htmlClient.getBuffer === "function" ? "getBuffer" : "get";
|
|
505
|
+
return Promise.all(keys.map((key) => htmlClient[getter](key)));
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* @param {unknown} value
|
|
510
|
+
* @returns {string | null}
|
|
511
|
+
*/
|
|
512
|
+
function asText(value) {
|
|
513
|
+
if (typeof value === "string") return value;
|
|
514
|
+
if (Buffer.isBuffer(value)) return value.toString("utf8");
|
|
515
|
+
return null;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* @param {unknown} value
|
|
520
|
+
* @returns {Buffer | null}
|
|
521
|
+
*/
|
|
522
|
+
function asBuffer(value) {
|
|
523
|
+
if (Buffer.isBuffer(value)) return value;
|
|
524
|
+
if (typeof value === "string") return Buffer.from(value);
|
|
525
|
+
return null;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* `:body` JSK sarmalayıcısı taşımaz; tamamı brotli'dir.
|
|
530
|
+
*
|
|
531
|
+
* @param {unknown} raw
|
|
532
|
+
* @returns {Promise<string>}
|
|
533
|
+
*/
|
|
534
|
+
function inflateBrotli(raw) {
|
|
535
|
+
const body = asBuffer(raw);
|
|
536
|
+
if (!body?.length) return Promise.reject(new Error("empty html body"));
|
|
537
|
+
|
|
538
|
+
return new Promise((resolve, reject) => {
|
|
539
|
+
zlib.brotliDecompress(body, (error, out) => {
|
|
540
|
+
if (error) reject(error);
|
|
541
|
+
else resolve(out.toString("utf8"));
|
|
542
|
+
});
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* `:body` ve `:enc:*` sayfa kaydı değil, gövde parçasıdır.
|
|
548
|
+
*
|
|
549
|
+
* @param {string} key
|
|
550
|
+
* @returns {boolean}
|
|
551
|
+
*/
|
|
552
|
+
function isHtmlFragmentKey(key) {
|
|
553
|
+
return key.endsWith(":body") || key.endsWith(":enc:br") || key.endsWith(":enc:gzip");
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* @param {string} key
|
|
558
|
+
* @returns {boolean}
|
|
559
|
+
*/
|
|
560
|
+
function isHtmlMetaKey(key) {
|
|
561
|
+
// `data` anahtarı da `:html:` metnini taşıyabilir; tür segmenti önekten hemen
|
|
562
|
+
// sonra gelir. Parça anahtarları (`:body`, `:enc:`) ayrıca düşürülür.
|
|
563
|
+
if (!prefix || !key.startsWith(`${prefix}:html:`)) return false;
|
|
564
|
+
return !isHtmlFragmentKey(key);
|
|
565
|
+
}
|
|
566
|
+
|
|
284
567
|
/**
|
|
285
568
|
* @param {string} key
|
|
286
569
|
* @returns {Promise<any | null>} Girdi yoksa, ayrıştırılamıyorsa ya da Redis
|
|
@@ -348,13 +631,41 @@ export function redisSetJson(key, value, ttlMs) {
|
|
|
348
631
|
* @param {string[]} keys
|
|
349
632
|
*/
|
|
350
633
|
export function redisDrop(keys) {
|
|
351
|
-
if (!
|
|
634
|
+
if (!keys.length) return;
|
|
635
|
+
|
|
636
|
+
/** @type {string[]} */
|
|
637
|
+
const dataKeys = [];
|
|
638
|
+
/** @type {string[]} */
|
|
639
|
+
const htmlKeys = [];
|
|
640
|
+
|
|
641
|
+
for (const key of keys) {
|
|
642
|
+
if (isHtmlMetaKey(key)) {
|
|
643
|
+
// Gövde ve kodlama anahtarları meta ile birlikte düşmezse 1 MB'lık
|
|
644
|
+
// parça TTL'e kadar kalır. Eksik anahtarda UNLINK no-op'tur.
|
|
645
|
+
htmlKeys.push(key, `${key}:body`, `${key}:enc:br`, `${key}:enc:gzip`);
|
|
646
|
+
} else if (!isHtmlFragmentKey(key)) {
|
|
647
|
+
dataKeys.push(key);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
352
650
|
|
|
651
|
+
if (dataKeys.length && usable()) unlinkKeys(client, dataKeys, noteFailure, noteSuccess);
|
|
652
|
+
if (htmlKeys.length && htmlUsable()) {
|
|
653
|
+
unlinkKeys(htmlClient, htmlKeys, noteHtmlFailure, noteHtmlSuccess);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* @param {any} connection
|
|
659
|
+
* @param {string[]} keys
|
|
660
|
+
* @param {(action: string, error: unknown) => void} fail
|
|
661
|
+
* @param {() => void} succeed
|
|
662
|
+
*/
|
|
663
|
+
function unlinkKeys(connection, keys, fail, succeed) {
|
|
353
664
|
// `UNLINK` silmeyi arka plana atar: bir webhook binlerce anahtar
|
|
354
665
|
// düşürdüğünde `DEL` sunucuyu bloklar.
|
|
355
|
-
Promise.resolve(
|
|
356
|
-
.then(
|
|
357
|
-
.catch((error) =>
|
|
666
|
+
Promise.resolve(connection.unlink(...keys))
|
|
667
|
+
.then(() => succeed())
|
|
668
|
+
.catch((error) => fail("unlink", error));
|
|
358
669
|
}
|
|
359
670
|
|
|
360
671
|
/**
|
|
@@ -409,7 +720,8 @@ export async function redisDropMatching(kind, match) {
|
|
|
409
720
|
* Dev raporu için durum özeti. Bağlantı yoksa da güvenle çağrılabilir.
|
|
410
721
|
*
|
|
411
722
|
* @returns {{ enabled: boolean, connected: boolean, keyPrefix: string,
|
|
412
|
-
* buildId: string, errors: number, bypassed: boolean
|
|
723
|
+
* buildId: string, errors: number, bypassed: boolean, htmlErrors: number,
|
|
724
|
+
* htmlBypassed: boolean }}
|
|
413
725
|
*/
|
|
414
726
|
export function getRedisStatus() {
|
|
415
727
|
const [, , buildId = ""] = prefix ? prefix.split(":") : [];
|
|
@@ -421,6 +733,8 @@ export function getRedisStatus() {
|
|
|
421
733
|
buildId,
|
|
422
734
|
errors,
|
|
423
735
|
bypassed: Boolean(bypassUntil && Date.now() < bypassUntil),
|
|
736
|
+
htmlErrors,
|
|
737
|
+
htmlBypassed: Boolean(htmlBypassUntil && Date.now() < htmlBypassUntil),
|
|
424
738
|
};
|
|
425
739
|
}
|
|
426
740
|
|
|
@@ -435,7 +749,7 @@ export function getRedisStatus() {
|
|
|
435
749
|
* @returns {{ address: string, secure: boolean, db: string | null,
|
|
436
750
|
* namespace: string, keyPrefix: string, html: boolean, data: boolean,
|
|
437
751
|
* storeEncoded: boolean, events: boolean, commandTimeoutMs: number,
|
|
438
|
-
* subscribed: boolean }}
|
|
752
|
+
* htmlCommandTimeoutMs: number, subscribed: boolean }}
|
|
439
753
|
*/
|
|
440
754
|
export function getRedisDetails() {
|
|
441
755
|
let address = "localhost:6379 (ioredis default)";
|
|
@@ -466,6 +780,7 @@ export function getRedisDetails() {
|
|
|
466
780
|
storeEncoded: settings.storeEncoded === true,
|
|
467
781
|
events: settings.events === true,
|
|
468
782
|
commandTimeoutMs: settings.commandTimeoutMs,
|
|
783
|
+
htmlCommandTimeoutMs: settings.htmlCommandTimeoutMs,
|
|
469
784
|
subscribed: Boolean(subscriber),
|
|
470
785
|
};
|
|
471
786
|
}
|
|
@@ -526,7 +841,7 @@ async function count(kind) {
|
|
|
526
841
|
500,
|
|
527
842
|
);
|
|
528
843
|
cursor = next;
|
|
529
|
-
total += keys.length;
|
|
844
|
+
total += kind === "html" ? keys.filter((key) => !isHtmlFragmentKey(key)).length : keys.length;
|
|
530
845
|
} while (cursor !== "0");
|
|
531
846
|
|
|
532
847
|
return total;
|
|
@@ -539,8 +854,9 @@ async function count(kind) {
|
|
|
539
854
|
* @returns {Promise<void>}
|
|
540
855
|
*/
|
|
541
856
|
export async function disconnectRedis() {
|
|
542
|
-
const open = [subscriber, client].filter(Boolean);
|
|
857
|
+
const open = [...new Set([subscriber, htmlClient, client].filter(Boolean))];
|
|
543
858
|
client = null;
|
|
859
|
+
htmlClient = null;
|
|
544
860
|
subscriber = null;
|
|
545
861
|
|
|
546
862
|
await Promise.all(
|
|
@@ -564,16 +880,22 @@ export async function disconnectRedis() {
|
|
|
564
880
|
*
|
|
565
881
|
* @param {any} fake `null` → katman kapatılır.
|
|
566
882
|
* @param {Partial<RedisConfig>} [overrides]
|
|
883
|
+
* @param {any} [htmlFake] Verilmezse `fake` hem veri hem HTML istemcisidir.
|
|
884
|
+
* HTML zaman aşımını veri sayacından ayırmak için ayrı bir sahte verilir.
|
|
567
885
|
*/
|
|
568
|
-
export function setRedisClientForTests(fake, overrides = {}) {
|
|
886
|
+
export function setRedisClientForTests(fake, overrides = {}, htmlFake = fake) {
|
|
569
887
|
client = fake;
|
|
888
|
+
htmlClient = htmlFake;
|
|
570
889
|
subscriber = null;
|
|
571
890
|
settings = { ...DEFAULT_REDIS, enabled: Boolean(fake), ...overrides };
|
|
572
891
|
prefix = fake ? `${settings.keyPrefix}:${settings.namespace}:test` : "";
|
|
573
892
|
channel = `${settings.keyPrefix}:${settings.namespace}:events`;
|
|
574
893
|
errors = 0;
|
|
894
|
+
htmlErrors = 0;
|
|
575
895
|
consecutiveFailures = 0;
|
|
896
|
+
htmlConsecutiveFailures = 0;
|
|
576
897
|
bypassUntil = 0;
|
|
898
|
+
htmlBypassUntil = 0;
|
|
577
899
|
}
|
|
578
900
|
|
|
579
901
|
/**
|
|
@@ -260,11 +260,19 @@ export declare const DEFAULT_REDIS: {
|
|
|
260
260
|
/** pub/sub üzerinden invalidation yayını. */
|
|
261
261
|
events: boolean;
|
|
262
262
|
/**
|
|
263
|
-
*
|
|
263
|
+
* Veri, yayın ve SCAN için tek komut tavanı. Önbellek okuması isteği
|
|
264
264
|
* bloklayan bir adım: Redis takıldığında render'a düşmek, ağı beklemekten
|
|
265
|
-
* iyidir.
|
|
265
|
+
* iyidir. HTML gövdesi bu süreye sığmaz; o bağlantının tavanı
|
|
266
|
+
* `htmlCommandTimeoutMs`.
|
|
266
267
|
*/
|
|
267
268
|
commandTimeoutMs: number;
|
|
269
|
+
/**
|
|
270
|
+
* Yalnızca HTML bağlantısı. ioredis `commandTimeout` istemci genelinde
|
|
271
|
+
* olduğu için gövde ayrı bağlantıda durur. Verilmezse 2000: uygulama
|
|
272
|
+
* config'i değiştirmek zorunda kalmasın. Parçalı yazma normalde bunun
|
|
273
|
+
* altında biter; tavan, gövde beklenmedik büyürse diye durur.
|
|
274
|
+
*/
|
|
275
|
+
htmlCommandTimeoutMs: number;
|
|
268
276
|
};
|
|
269
277
|
/**
|
|
270
278
|
* Kalıcı log sink'leri (dosya + S3).
|
package/types/config/index.d.ts
CHANGED
|
@@ -34,7 +34,14 @@ export type RedisConfig = {
|
|
|
34
34
|
* pub/sub invalidation yayını.
|
|
35
35
|
*/
|
|
36
36
|
events: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Veri, yayın ve SCAN tavanı.
|
|
39
|
+
*/
|
|
37
40
|
commandTimeoutMs: number;
|
|
41
|
+
/**
|
|
42
|
+
* Yalnızca HTML bağlantısının tavanı.
|
|
43
|
+
*/
|
|
44
|
+
htmlCommandTimeoutMs: number;
|
|
38
45
|
};
|
|
39
46
|
export type LogKind = "http" | "event" | "error";
|
|
40
47
|
export type LogsConfig = {
|
package/types/server/redis.d.ts
CHANGED
|
@@ -48,6 +48,40 @@ export declare function redisSharesEncoded(): boolean;
|
|
|
48
48
|
* @returns {string}
|
|
49
49
|
*/
|
|
50
50
|
export declare function cacheKey(kind: "html" | "data", key: string): string;
|
|
51
|
+
/**
|
|
52
|
+
* HTML kaydını parçalı yazar. Meta küçük JSON'dur; gövde ham brotli baytıdır
|
|
53
|
+
* (base64 yok, JSON içinde ikinci brotli yok). `encoded` varsa her kodlama
|
|
54
|
+
* `:enc:` anahtarına olduğu gibi gider.
|
|
55
|
+
*
|
|
56
|
+
* Ateşle-unut ve tek sayaç adımı: üç SET'ten biri düşerse bu bir hatadır,
|
|
57
|
+
* veri devre kesicisine yazılmaz.
|
|
58
|
+
*
|
|
59
|
+
* @param {string} key Önbellek anahtarı (`yol?query`), öneksiz.
|
|
60
|
+
* @param {Record<string, unknown>} meta
|
|
61
|
+
* @param {Buffer} body
|
|
62
|
+
* @param {Map<string, Buffer> | null} encoded
|
|
63
|
+
* @param {number} ttlMs
|
|
64
|
+
*/
|
|
65
|
+
export declare function redisShareHtml(key: string, meta: Record<string, unknown>, body: Buffer, encoded: Map<string, Buffer> | null, ttlMs: number): void;
|
|
66
|
+
/**
|
|
67
|
+
* Meta + gövde (+ açıksa kodlamalar) tek pipeline. Parça eksikse kayıt yok
|
|
68
|
+
* sayılır; bu bir hata değildir. Bağlantı hatası HTML sayacına yazılır.
|
|
69
|
+
*
|
|
70
|
+
* @param {string} key Önbellek anahtarı (`yol?query`), öneksiz.
|
|
71
|
+
* @returns {Promise<{ html: string, status: unknown, storedAt: unknown,
|
|
72
|
+
* expiresAt: unknown, staleUntil: unknown, produceMs: unknown, deps: unknown,
|
|
73
|
+
* encoded: Map<string, Buffer> } | null>}
|
|
74
|
+
*/
|
|
75
|
+
export declare function redisGetHtml(key: string): Promise<{
|
|
76
|
+
html: string;
|
|
77
|
+
status: unknown;
|
|
78
|
+
storedAt: unknown;
|
|
79
|
+
expiresAt: unknown;
|
|
80
|
+
staleUntil: unknown;
|
|
81
|
+
produceMs: unknown;
|
|
82
|
+
deps: unknown;
|
|
83
|
+
encoded: Map<string, Buffer>;
|
|
84
|
+
} | null>;
|
|
51
85
|
/**
|
|
52
86
|
* @param {string} key
|
|
53
87
|
* @returns {Promise<any | null>} Girdi yoksa, ayrıştırılamıyorsa ya da Redis
|
|
@@ -85,7 +119,8 @@ export declare function redisDropMatching(kind: "html" | "data", match?: (key: s
|
|
|
85
119
|
* Dev raporu için durum özeti. Bağlantı yoksa da güvenle çağrılabilir.
|
|
86
120
|
*
|
|
87
121
|
* @returns {{ enabled: boolean, connected: boolean, keyPrefix: string,
|
|
88
|
-
* buildId: string, errors: number, bypassed: boolean
|
|
122
|
+
* buildId: string, errors: number, bypassed: boolean, htmlErrors: number,
|
|
123
|
+
* htmlBypassed: boolean }}
|
|
89
124
|
*/
|
|
90
125
|
export declare function getRedisStatus(): {
|
|
91
126
|
enabled: boolean;
|
|
@@ -94,6 +129,8 @@ export declare function getRedisStatus(): {
|
|
|
94
129
|
buildId: string;
|
|
95
130
|
errors: number;
|
|
96
131
|
bypassed: boolean;
|
|
132
|
+
htmlErrors: number;
|
|
133
|
+
htmlBypassed: boolean;
|
|
97
134
|
};
|
|
98
135
|
/**
|
|
99
136
|
* Bağlantının **nereye** kurulduğu ve hangi ayarlarla çalıştığı.
|
|
@@ -106,7 +143,7 @@ export declare function getRedisStatus(): {
|
|
|
106
143
|
* @returns {{ address: string, secure: boolean, db: string | null,
|
|
107
144
|
* namespace: string, keyPrefix: string, html: boolean, data: boolean,
|
|
108
145
|
* storeEncoded: boolean, events: boolean, commandTimeoutMs: number,
|
|
109
|
-
* subscribed: boolean }}
|
|
146
|
+
* htmlCommandTimeoutMs: number, subscribed: boolean }}
|
|
110
147
|
*/
|
|
111
148
|
export declare function getRedisDetails(): {
|
|
112
149
|
address: string;
|
|
@@ -119,6 +156,7 @@ export declare function getRedisDetails(): {
|
|
|
119
156
|
storeEncoded: boolean;
|
|
120
157
|
events: boolean;
|
|
121
158
|
commandTimeoutMs: number;
|
|
159
|
+
htmlCommandTimeoutMs: number;
|
|
122
160
|
subscribed: boolean;
|
|
123
161
|
};
|
|
124
162
|
/**
|
|
@@ -152,8 +190,10 @@ export declare function disconnectRedis(): Promise<void>;
|
|
|
152
190
|
*
|
|
153
191
|
* @param {any} fake `null` → katman kapatılır.
|
|
154
192
|
* @param {Partial<RedisConfig>} [overrides]
|
|
193
|
+
* @param {any} [htmlFake] Verilmezse `fake` hem veri hem HTML istemcisidir.
|
|
194
|
+
* HTML zaman aşımını veri sayacından ayırmak için ayrı bir sahte verilir.
|
|
155
195
|
*/
|
|
156
|
-
export declare function setRedisClientForTests(fake: any, overrides?: Partial<RedisConfig
|
|
196
|
+
export declare function setRedisClientForTests(fake: any, overrides?: Partial<RedisConfig>, htmlFake?: any): void;
|
|
157
197
|
/**
|
|
158
198
|
* Testler için: abone kanalından gelmiş gibi olay besler. `originId`
|
|
159
199
|
* verilmezse uzak bir node varsayılır; kendi kimliğini taşıyan bir yayını
|