jskelet 0.5.0 → 0.5.1
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 +16 -2
- package/docs/07-yapilandirma.md +3 -1
- package/docs/en/06-caching.md +16 -2
- package/docs/en/07-configuration.md +3 -1
- package/package.json +1 -1
- package/src/server/html-cache.js +144 -9
- package/src/server/prewarm.js +57 -3
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,12 @@ one is listed under a **Breaking** heading.
|
|
|
10
10
|
|
|
11
11
|
### Added
|
|
12
12
|
|
|
13
|
+
- Early HTML cache refresh before TTL expiry: the last successful produce time
|
|
14
|
+
(`produceMs`) sets a lead window (`min(max(produceMs×2, 250ms), ttl/2)`). A
|
|
15
|
+
still-fresh `HIT` in that window revalidates in the background; idle entries
|
|
16
|
+
are soft-staled by a sweeper and drained over HTTP even without classic
|
|
17
|
+
`prewarmPaths` (`PREWARM=0` disables both). In-flight refreshes no longer drop
|
|
18
|
+
the entry when `staleUntil` elapses.
|
|
13
19
|
- Route-level stylesheets: put files in `styles/pages/*.css` and load them from
|
|
14
20
|
the controller with `styles: ["home.css"]` (same contract as island
|
|
15
21
|
`entries`). The layout emits them after global `app.css`; dev hot-swaps any
|
package/docs/06-cache.md
CHANGED
|
@@ -142,15 +142,29 @@ Girdi yapısı:
|
|
|
142
142
|
```
|
|
143
143
|
expiresAt = now + ttl
|
|
144
144
|
staleUntil = now + ttl * 2 (STALE_FACTOR = 1)
|
|
145
|
+
produceMs = son başarılı üretimin süresi (ms)
|
|
145
146
|
```
|
|
146
147
|
|
|
147
148
|
Okuma davranışı:
|
|
148
149
|
|
|
149
150
|
| Durum | Yanıt | Arka plan |
|
|
150
151
|
| --- | --- | --- |
|
|
151
|
-
| `now < expiresAt` | Önbellekteki HTML, `HIT` | — |
|
|
152
|
+
| `now < expiresAt - leadMs` | Önbellekteki HTML, `HIT` | — |
|
|
153
|
+
| `expiresAt - leadMs ≤ now < expiresAt` | Önbellekteki HTML, `HIT` | **Erken tazeleme** başlar |
|
|
152
154
|
| `expiresAt ≤ now < staleUntil` | Önbellekteki HTML **anında**, `STALE` | Tazeleme başlatılır |
|
|
153
|
-
| `now ≥ staleUntil` | Girdi silinir, taze render, `MISS` | — |
|
|
155
|
+
| `now ≥ staleUntil` | Girdi silinir, taze render, `MISS` (uçuştaki tazeleme varken silinmez) | — |
|
|
156
|
+
|
|
157
|
+
`leadMs` sayfanın load süresini hesaba katar:
|
|
158
|
+
|
|
159
|
+
```
|
|
160
|
+
leadMs = min(max(produceMs * 2, 250ms), ttl / 2)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Böylece yavaş bir sayfa TTL dolduğu anda hâlâ soğuk render'a düşmez: taze
|
|
164
|
+
HTML çoğu zaman `expiresAt` gelmeden yazılmış olur. Trafik yoksa bir sweeper
|
|
165
|
+
aynı pencerede girdiyi soft-bayatlatır ve ısıtma kuyruğuna alır; `startPrewarm`
|
|
166
|
+
( `PREWARM=0` değilse) kuyruğu HTTP ile boşaltır — klasik `prewarmPaths`
|
|
167
|
+
olmasa da.
|
|
154
168
|
|
|
155
169
|
Stale penceresinde tazelemenin hatası isteği etkilemez: eski HTML pencere
|
|
156
170
|
boyunca geçerli kalır ve hata yalnızca loglanır
|
package/docs/07-yapilandirma.md
CHANGED
|
@@ -622,7 +622,9 @@ Ayrıntı: [03-routing.md](./03-routing.md).
|
|
|
622
622
|
|
|
623
623
|
Desen → saniye eşlemesi. Eşleşen kural, route'un kendi `revalidate` değerini
|
|
624
624
|
**ezer**. Negatif ya da sonlu olmayan değerler yok sayılır; `0` "önbellekleme"
|
|
625
|
-
anlamına gelir.
|
|
625
|
+
anlamına gelir. TTL dolmadan önce framework, son render süresine göre erken
|
|
626
|
+
arka plan tazelemesi başlatır (ayrı bir config alanı yok; ayrıntı
|
|
627
|
+
[06-cache.md](./06-cache.md)).
|
|
626
628
|
|
|
627
629
|
```js
|
|
628
630
|
html: {
|
package/docs/en/06-caching.md
CHANGED
|
@@ -150,15 +150,29 @@ The entry structure:
|
|
|
150
150
|
```
|
|
151
151
|
expiresAt = now + ttl
|
|
152
152
|
staleUntil = now + ttl * 2 (STALE_FACTOR = 1)
|
|
153
|
+
produceMs = duration of the last successful produce (ms)
|
|
153
154
|
```
|
|
154
155
|
|
|
155
156
|
Read behaviour:
|
|
156
157
|
|
|
157
158
|
| State | Response | Background |
|
|
158
159
|
| --- | --- | --- |
|
|
159
|
-
| `now < expiresAt` | The cached HTML, `HIT` | — |
|
|
160
|
+
| `now < expiresAt - leadMs` | The cached HTML, `HIT` | — |
|
|
161
|
+
| `expiresAt - leadMs ≤ now < expiresAt` | The cached HTML, `HIT` | **Early refresh** starts |
|
|
160
162
|
| `expiresAt ≤ now < staleUntil` | The cached HTML **immediately**, `STALE` | A refresh is started |
|
|
161
|
-
| `now ≥ staleUntil` | The entry is deleted, fresh render, `MISS` | — |
|
|
163
|
+
| `now ≥ staleUntil` | The entry is deleted, fresh render, `MISS` (not while a refresh is in flight) | — |
|
|
164
|
+
|
|
165
|
+
`leadMs` accounts for the page’s load time:
|
|
166
|
+
|
|
167
|
+
```
|
|
168
|
+
leadMs = min(max(produceMs * 2, 250ms), ttl / 2)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
So a slow page does not fall back to a cold render the moment TTL ends: fresh
|
|
172
|
+
HTML is usually written before `expiresAt`. With no traffic, a sweeper
|
|
173
|
+
soft-stales the entry in the same window and queues it for warming;
|
|
174
|
+
`startPrewarm` (unless `PREWARM=0`) drains that queue over HTTP — even when
|
|
175
|
+
classic `prewarmPaths` is absent.
|
|
162
176
|
|
|
163
177
|
A failure of the refresh inside the stale window does not affect the request:
|
|
164
178
|
the old HTML stays valid for the whole window and the error is only logged
|
|
@@ -635,7 +635,9 @@ Details: [03-routing.md](./03-routing.md).
|
|
|
635
635
|
|
|
636
636
|
A pattern → seconds mapping. A matching rule **overrides** the route's own
|
|
637
637
|
`revalidate` value. Negative or non-finite values are ignored; `0` means "no
|
|
638
|
-
caching".
|
|
638
|
+
caching". Before TTL ends the framework starts an early background refresh
|
|
639
|
+
based on the last render duration (no separate config field; see
|
|
640
|
+
[06-caching.md](./06-caching.md)).
|
|
639
641
|
|
|
640
642
|
The one exception is `route(fn, { private: true })`: on that route a matching
|
|
641
643
|
pattern is ignored. The lock is deliberately one-way — a mistake in the other
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jskelet",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "A framework that feels like no framework: Express 5 + build-time .jsk (or EJS) SSR, vanilla JS islands, Tailwind v4 and an in-process HTML TTL cache.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/server/html-cache.js
CHANGED
|
@@ -7,6 +7,11 @@
|
|
|
7
7
|
* tazeleme turu` kadar geride olabilir. Fiyat gibi canlı alanlar istemcide
|
|
8
8
|
* WebSocket'ten güncellendiği için bu gecikme ekranda görünmez.
|
|
9
9
|
*
|
|
10
|
+
* TTL dolmadan önce de tazelenir (**erken tazeleme**): son başarılı üretimin
|
|
11
|
+
* süresi (`produceMs`) kadar önden arka plan refresh başlar, böylece yavaş
|
|
12
|
+
* bir sayfa TTL anında hâlâ soğuk render'a düşmez. Trafik yoksa sweeper
|
|
13
|
+
* girdiyi soft-bayatlatır ve ısıtma kuyruğuna alır.
|
|
14
|
+
*
|
|
10
15
|
* TTL'in yanında ikinci bir tazelik kaynağı daha var: **hedefli
|
|
11
16
|
* invalidation**. Bir içerik güncellendiğinde tüm önbelleği boşaltmak
|
|
12
17
|
* (`clearHtmlCache()`) o an sıcak olan her sayfayı soğuk render'a çevirir;
|
|
@@ -50,9 +55,12 @@ import {
|
|
|
50
55
|
* `encoded` haritası yanıt yolunda (`sendHtml`) doluyor, yani yazma anında
|
|
51
56
|
* boş; `storeEncoded` açıkken harita büyüdüğünde girdi yeniden paylaşılır.
|
|
52
57
|
*
|
|
58
|
+
* `produceMs`: son başarılı üretimin süresi. Erken tazeleme penceresi bundan
|
|
59
|
+
* türetilir; Redis'ten gelen kopyada yoksa varsayılan kullanılır.
|
|
60
|
+
*
|
|
53
61
|
* @typedef {{ html: string, status: number, expiresAt: number,
|
|
54
62
|
* staleUntil: number, encoded: Map<string, Buffer>, deps: Set<string>,
|
|
55
|
-
* storedAt: number, sharedEncodings: number }} HtmlEntry
|
|
63
|
+
* storedAt: number, sharedEncodings: number, produceMs: number }} HtmlEntry
|
|
56
64
|
*/
|
|
57
65
|
|
|
58
66
|
/**
|
|
@@ -94,12 +102,27 @@ function trackDependencies() {
|
|
|
94
102
|
*/
|
|
95
103
|
const STALE_FACTOR = 1;
|
|
96
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Redis'ten gelen veya süresi bilinmeyen girdiler için erken tazeleme lead'i.
|
|
107
|
+
* Ölçülmüş `produceMs` yokken aşırı iyimser (0) kalmamak için.
|
|
108
|
+
*/
|
|
109
|
+
const DEFAULT_PRODUCE_MS = 500;
|
|
110
|
+
|
|
111
|
+
/** Erken tazelemenin alt sınırı — çok hızlı sayfalar da TTL'den önce ısınsın. */
|
|
112
|
+
const EARLY_REFRESH_MIN_MS = 250;
|
|
113
|
+
|
|
114
|
+
/** Trafiksiz girdileri erken pencerede soft-bayatlatma aralığı. */
|
|
115
|
+
const EARLY_SWEEP_INTERVAL_MS = 1000;
|
|
116
|
+
|
|
97
117
|
/** @type {Map<string, HtmlEntry>} */
|
|
98
118
|
const store = new Map();
|
|
99
119
|
|
|
100
120
|
/** @type {Map<string, Promise<{ html: string, status: number }>>} */
|
|
101
121
|
const inflight = new Map();
|
|
102
122
|
|
|
123
|
+
/** @type {ReturnType<typeof setInterval> | null} */
|
|
124
|
+
let earlySweepTimer = null;
|
|
125
|
+
|
|
103
126
|
/**
|
|
104
127
|
* Uçuştaki her tazelemenin kimliği. Bir girdi tazelenirken invalidate
|
|
105
128
|
* edilirse o tazelemenin sonucu **artık geçersizdir**: render, purge'den önce
|
|
@@ -148,6 +171,54 @@ const purgedDeps = new Map();
|
|
|
148
171
|
|
|
149
172
|
const MAX_PURGED_DEPS = 1000;
|
|
150
173
|
|
|
174
|
+
/**
|
|
175
|
+
* Erken tazeleme lead'i: son render süresinin 2 katı (en az 250 ms), TTL'in
|
|
176
|
+
* yarısından fazla olamaz — kısa TTL'lerde sürekli refresh döngüsü olmasın.
|
|
177
|
+
*
|
|
178
|
+
* @param {number} produceMs
|
|
179
|
+
* @param {number} ttlMs
|
|
180
|
+
* @returns {number}
|
|
181
|
+
*/
|
|
182
|
+
export function earlyRefreshLeadMs(produceMs, ttlMs) {
|
|
183
|
+
const measured =
|
|
184
|
+
Number.isFinite(produceMs) && produceMs > 0 ? produceMs : DEFAULT_PRODUCE_MS;
|
|
185
|
+
const lead = Math.max(measured * 2, EARLY_REFRESH_MIN_MS);
|
|
186
|
+
if (!Number.isFinite(ttlMs) || ttlMs <= 0) return lead;
|
|
187
|
+
return Math.min(lead, ttlMs / 2);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* @param {HtmlEntry} entry
|
|
192
|
+
* @returns {number}
|
|
193
|
+
*/
|
|
194
|
+
function entryTtlMs(entry) {
|
|
195
|
+
// Soft-bayatlatılmış girdide expiresAt 0; orijinal TTL storedAt farkından
|
|
196
|
+
// okunamaz. O durumda produceMs üzerinden güvenli bir üst sınır yeter.
|
|
197
|
+
if (entry.expiresAt > entry.storedAt) return entry.expiresAt - entry.storedAt;
|
|
198
|
+
return Math.max(entry.produceMs * 4, EARLY_REFRESH_MIN_MS * 2);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* @param {HtmlEntry} entry
|
|
203
|
+
* @param {number} [now]
|
|
204
|
+
* @returns {boolean}
|
|
205
|
+
*/
|
|
206
|
+
function isEarly(entry, now = Date.now()) {
|
|
207
|
+
if (now >= entry.expiresAt) return false;
|
|
208
|
+
const lead = earlyRefreshLeadMs(entry.produceMs, entryTtlMs(entry));
|
|
209
|
+
return now >= entry.expiresAt - lead;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* @param {unknown} value
|
|
214
|
+
* @returns {number}
|
|
215
|
+
*/
|
|
216
|
+
function normalizeProduceMs(value) {
|
|
217
|
+
const n = Number(value);
|
|
218
|
+
if (Number.isFinite(n) && n >= 0) return Math.round(n);
|
|
219
|
+
return DEFAULT_PRODUCE_MS;
|
|
220
|
+
}
|
|
221
|
+
|
|
151
222
|
/**
|
|
152
223
|
* Girdiyi ters indeksten söker. Bu adım atlanırsa indeks, düşen girdilerin
|
|
153
224
|
* anahtarlarını tutmaya devam eder ve sessizce sızar.
|
|
@@ -183,7 +254,7 @@ function drop(key) {
|
|
|
183
254
|
/**
|
|
184
255
|
* @param {string} key
|
|
185
256
|
* @returns {{ html: string, status: number, encoded: Map<string, Buffer>,
|
|
186
|
-
* stale: boolean } | null}
|
|
257
|
+
* stale: boolean, early: boolean } | null}
|
|
187
258
|
*/
|
|
188
259
|
function read(key) {
|
|
189
260
|
const entry = store.get(key);
|
|
@@ -191,8 +262,12 @@ function read(key) {
|
|
|
191
262
|
|
|
192
263
|
const now = Date.now();
|
|
193
264
|
if (now >= entry.staleUntil) {
|
|
194
|
-
|
|
195
|
-
|
|
265
|
+
// Uçuştaki tazeleme bitene kadar girdiyi tut: yavaş upstream'de
|
|
266
|
+
// staleUntil dolup MISS'e düşmek erken tazelemenin amacını bozar.
|
|
267
|
+
if (!inflight.has(key)) {
|
|
268
|
+
drop(key);
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
196
271
|
}
|
|
197
272
|
|
|
198
273
|
// LRU: erişilen girdiyi sona taşı.
|
|
@@ -206,11 +281,13 @@ function read(key) {
|
|
|
206
281
|
share(key, entry);
|
|
207
282
|
}
|
|
208
283
|
|
|
284
|
+
const stale = now >= entry.expiresAt;
|
|
209
285
|
return {
|
|
210
286
|
html: entry.html,
|
|
211
287
|
status: entry.status,
|
|
212
288
|
encoded: entry.encoded,
|
|
213
|
-
stale
|
|
289
|
+
stale,
|
|
290
|
+
early: !stale && isEarly(entry, now),
|
|
214
291
|
};
|
|
215
292
|
}
|
|
216
293
|
|
|
@@ -234,6 +311,7 @@ function share(key, entry) {
|
|
|
234
311
|
storedAt: entry.storedAt,
|
|
235
312
|
expiresAt: entry.expiresAt,
|
|
236
313
|
staleUntil: entry.staleUntil,
|
|
314
|
+
produceMs: entry.produceMs,
|
|
237
315
|
deps: [...entry.deps],
|
|
238
316
|
};
|
|
239
317
|
|
|
@@ -298,6 +376,7 @@ async function readShared(key) {
|
|
|
298
376
|
deps,
|
|
299
377
|
storedAt,
|
|
300
378
|
sharedEncodings: encoded.size,
|
|
379
|
+
produceMs: normalizeProduceMs(payload.produceMs),
|
|
301
380
|
};
|
|
302
381
|
}
|
|
303
382
|
|
|
@@ -306,8 +385,9 @@ async function readShared(key) {
|
|
|
306
385
|
* @param {{ html: string, status: number }} value
|
|
307
386
|
* @param {number} ttlSeconds
|
|
308
387
|
* @param {Set<string> | null} deps Render sırasında okunan veri anahtarları.
|
|
388
|
+
* @param {number} [produceMs] Son üretimin süresi (ms).
|
|
309
389
|
*/
|
|
310
|
-
function write(key, value, ttlSeconds, deps = null) {
|
|
390
|
+
function write(key, value, ttlSeconds, deps = null, produceMs = DEFAULT_PRODUCE_MS) {
|
|
311
391
|
const now = Date.now();
|
|
312
392
|
|
|
313
393
|
/** @type {HtmlEntry} */
|
|
@@ -322,6 +402,7 @@ function write(key, value, ttlSeconds, deps = null) {
|
|
|
322
402
|
deps: deps ?? new Set(),
|
|
323
403
|
storedAt: now,
|
|
324
404
|
sharedEncodings: 0,
|
|
405
|
+
produceMs: normalizeProduceMs(produceMs),
|
|
325
406
|
};
|
|
326
407
|
|
|
327
408
|
install(key, entry);
|
|
@@ -385,6 +466,9 @@ function refresh(key, ttlSeconds, producer) {
|
|
|
385
466
|
const pending = inflight.get(key);
|
|
386
467
|
if (pending) return pending;
|
|
387
468
|
|
|
469
|
+
// Tazeleme sürerken staleUntil dolmasın: drop → MISS yolu kapanır.
|
|
470
|
+
extendStaleWhileRefreshing(key);
|
|
471
|
+
|
|
388
472
|
const token = {};
|
|
389
473
|
tokens.set(key, token);
|
|
390
474
|
|
|
@@ -397,6 +481,17 @@ function refresh(key, ttlSeconds, producer) {
|
|
|
397
481
|
return task;
|
|
398
482
|
}
|
|
399
483
|
|
|
484
|
+
/**
|
|
485
|
+
* @param {string} key
|
|
486
|
+
*/
|
|
487
|
+
function extendStaleWhileRefreshing(key) {
|
|
488
|
+
const entry = store.get(key);
|
|
489
|
+
if (!entry) return;
|
|
490
|
+
const lead = earlyRefreshLeadMs(entry.produceMs, entryTtlMs(entry));
|
|
491
|
+
const floor = Date.now() + lead;
|
|
492
|
+
if (entry.staleUntil < floor) entry.staleUntil = floor;
|
|
493
|
+
}
|
|
494
|
+
|
|
400
495
|
/**
|
|
401
496
|
* @param {string} key
|
|
402
497
|
* @param {number} ttlSeconds
|
|
@@ -436,7 +531,7 @@ async function produce(key, ttlSeconds, producer, token) {
|
|
|
436
531
|
// öncesinde başlamış demektir; yazmak az önce düşürüleni geri koyardı.
|
|
437
532
|
const valid = tokens.get(key) === token && !readsPurgedData(deps, startedAt);
|
|
438
533
|
if (valid && value.status === 200 && !value.degraded && value.storable !== false) {
|
|
439
|
-
write(key, value, ttlSeconds, deps);
|
|
534
|
+
write(key, value, ttlSeconds, deps, Date.now() - startedAt);
|
|
440
535
|
}
|
|
441
536
|
|
|
442
537
|
return value;
|
|
@@ -447,7 +542,7 @@ async function produce(key, ttlSeconds, producer, token) {
|
|
|
447
542
|
* @param {number} ttlSeconds 0 → cache yok
|
|
448
543
|
* @param {() => Promise<{ html: string, status: number }>} producer
|
|
449
544
|
* @returns {Promise<{ html: string, status: number, cached: boolean,
|
|
450
|
-
* stale?: boolean, encoded?: Map<string, Buffer> }>}
|
|
545
|
+
* stale?: boolean, early?: boolean, encoded?: Map<string, Buffer> }>}
|
|
451
546
|
*/
|
|
452
547
|
export async function withHtmlCache(key, ttlSeconds, producer) {
|
|
453
548
|
if (!ttlSeconds) {
|
|
@@ -460,7 +555,8 @@ export async function withHtmlCache(key, ttlSeconds, producer) {
|
|
|
460
555
|
if (hit) {
|
|
461
556
|
// Süresi geçmiş girdi anında döner; tazeleme arkada yürür ve hatası
|
|
462
557
|
// isteği etkilemez (eski HTML stale penceresi boyunca geçerli kalır).
|
|
463
|
-
|
|
558
|
+
// Erken pencerede de aynı: hâlâ HIT, ama TTL dolmadan taze HTML yazılsın.
|
|
559
|
+
if (hit.stale || hit.early) {
|
|
464
560
|
invalidated.delete(key);
|
|
465
561
|
void refresh(key, ttlSeconds, producer).catch((error) => {
|
|
466
562
|
console.error(`[html-cache] background refresh failed: ${key}`, error);
|
|
@@ -829,3 +925,42 @@ export function isHtmlCacheFresh(pathname) {
|
|
|
829
925
|
if (!entry) return false;
|
|
830
926
|
return Date.now() < entry.expiresAt;
|
|
831
927
|
}
|
|
928
|
+
|
|
929
|
+
/**
|
|
930
|
+
* Erken tazeleme penceresine girmiş (veya TTL'i dolmuş) trafiksiz girdileri
|
|
931
|
+
* soft-bayatlatır ve ısıtma kuyruğuna alır. HTTP ısıtması producer'sız
|
|
932
|
+
* çalıştığı için soft-bayat şart: taze HIT yenileme tetiklemez.
|
|
933
|
+
*
|
|
934
|
+
* @returns {number} İşaretlenen girdi sayısı.
|
|
935
|
+
*/
|
|
936
|
+
export function sweepEarlyExpiry() {
|
|
937
|
+
const now = Date.now();
|
|
938
|
+
let marked = 0;
|
|
939
|
+
|
|
940
|
+
for (const [key, entry] of store) {
|
|
941
|
+
if (inflight.has(key)) continue;
|
|
942
|
+
// Zaten soft-bayat / kuyrukta — her saniye yeniden ekleme.
|
|
943
|
+
if (entry.expiresAt === 0) continue;
|
|
944
|
+
if (now < entry.expiresAt && !isEarly(entry, now)) continue;
|
|
945
|
+
|
|
946
|
+
entry.expiresAt = 0;
|
|
947
|
+
if (invalidated.size < MAX_INVALIDATED) invalidated.add(key);
|
|
948
|
+
marked += 1;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
return marked;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
/**
|
|
955
|
+
* Trafiksiz sayfaların TTL öncesi soft-bayatlatılması. `startPrewarm` açar;
|
|
956
|
+
* `PREWARM=0` iken hiç kurulmaz. `unref` — süreç kapanışını geciktirmez.
|
|
957
|
+
*
|
|
958
|
+
* @returns {void}
|
|
959
|
+
*/
|
|
960
|
+
export function startEarlyExpirySweep() {
|
|
961
|
+
if (earlySweepTimer) return;
|
|
962
|
+
earlySweepTimer = setInterval(() => {
|
|
963
|
+
sweepEarlyExpiry();
|
|
964
|
+
}, EARLY_SWEEP_INTERVAL_MS);
|
|
965
|
+
earlySweepTimer.unref();
|
|
966
|
+
}
|
package/src/server/prewarm.js
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
import process from "node:process";
|
|
20
20
|
import { getConfig, hook } from "../config/index.js";
|
|
21
21
|
import { getRequestContext } from "../http/request-context.js";
|
|
22
|
-
import { isHtmlCacheFresh, takeInvalidatedPaths } from "./html-cache.js";
|
|
22
|
+
import { isHtmlCacheFresh, takeInvalidatedPaths, startEarlyExpirySweep } from "./html-cache.js";
|
|
23
23
|
import { getDataCacheStats } from "./data-cache.js";
|
|
24
24
|
import { isTransientStatus } from "./upstream-tracking.js";
|
|
25
25
|
import { upstreamCooldownMs } from "./upstream-limiter.js";
|
|
@@ -786,7 +786,8 @@ async function drainVisitWarm() {
|
|
|
786
786
|
|
|
787
787
|
/**
|
|
788
788
|
* Açılışta ısıtmayı tetikler. `listen` geri çağrısından çağrılır.
|
|
789
|
-
* `onVisit` modunda zamanlayıcı yok
|
|
789
|
+
* `onVisit` modunda klasik zamanlayıcı yok; yine de erken-TTL invalidation
|
|
790
|
+
* drain'i çalışır — soft-bayatlayan sweeper'ın kuyruğu boşalmasın.
|
|
790
791
|
*
|
|
791
792
|
* @param {{ port: number }} options
|
|
792
793
|
* @returns {void}
|
|
@@ -797,6 +798,11 @@ export function startPrewarm({ port }) {
|
|
|
797
798
|
|
|
798
799
|
const origin = `http://127.0.0.1:${port}`;
|
|
799
800
|
|
|
801
|
+
// Klasik `prewarmPaths` olmasa da TTL öncesi soft-bayatlayan girdiler
|
|
802
|
+
// HTTP ile ısıtılsın. `PREWARM=0` yukarıda her şeyi keser.
|
|
803
|
+
startEarlyExpirySweep();
|
|
804
|
+
startExpiryWarmDrain(origin);
|
|
805
|
+
|
|
800
806
|
if (config.prewarm?.onVisit?.enabled) {
|
|
801
807
|
const classicEnv = CLASSIC_PREWARM_ENV.filter((key) => process.env[key]);
|
|
802
808
|
if (classicEnv.length) {
|
|
@@ -814,7 +820,8 @@ export function startPrewarm({ port }) {
|
|
|
814
820
|
}
|
|
815
821
|
|
|
816
822
|
if (process.env.PREWARM !== "1" && config.prewarm?.enabled === false) return;
|
|
817
|
-
// Isıtacak yol bildirmeyen bir projede
|
|
823
|
+
// Isıtacak yol bildirmeyen bir projede klasik tur zamanlayıcısı gerekmez;
|
|
824
|
+
// expiry drain yine de yukarıda kuruldu.
|
|
818
825
|
if (typeof config.hooks?.prewarmPaths !== "function") return;
|
|
819
826
|
|
|
820
827
|
const isDev = process.env.NODE_ENV === "development";
|
|
@@ -848,3 +855,50 @@ export function startPrewarm({ port }) {
|
|
|
848
855
|
const interval = setting("PREWARM_INTERVAL_SECONDS", "intervalSeconds", 0);
|
|
849
856
|
if (interval > 0) setInterval(() => void run(), interval * 1000).unref();
|
|
850
857
|
}
|
|
858
|
+
|
|
859
|
+
/** @type {string | null} */
|
|
860
|
+
let expiryOrigin = null;
|
|
861
|
+
|
|
862
|
+
/** @type {boolean} */
|
|
863
|
+
let expiryDraining = false;
|
|
864
|
+
|
|
865
|
+
/** @type {ReturnType<typeof setInterval> | null} */
|
|
866
|
+
let expiryDrainTimer = null;
|
|
867
|
+
|
|
868
|
+
/**
|
|
869
|
+
* Soft-bayat / invalidate kuyruğunu periyodik boşaltır. Klasik tur ve onVisit
|
|
870
|
+
* aynı kuyruğu da okur; bu drain `prewarmPaths` yokken de çalışır.
|
|
871
|
+
*
|
|
872
|
+
* @param {string} origin
|
|
873
|
+
* @returns {void}
|
|
874
|
+
*/
|
|
875
|
+
function startExpiryWarmDrain(origin) {
|
|
876
|
+
expiryOrigin = origin;
|
|
877
|
+
if (expiryDrainTimer) return;
|
|
878
|
+
expiryDrainTimer = setInterval(() => {
|
|
879
|
+
void drainExpiryWarm();
|
|
880
|
+
}, 1000);
|
|
881
|
+
expiryDrainTimer.unref();
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/**
|
|
885
|
+
* @returns {Promise<void>}
|
|
886
|
+
*/
|
|
887
|
+
async function drainExpiryWarm() {
|
|
888
|
+
if (expiryDraining || !expiryOrigin) return;
|
|
889
|
+
|
|
890
|
+
const paths = takeInvalidatedPaths();
|
|
891
|
+
if (!paths.length) return;
|
|
892
|
+
|
|
893
|
+
expiryDraining = true;
|
|
894
|
+
try {
|
|
895
|
+
const cold = paths.filter((path) => !isHtmlCacheFresh(path));
|
|
896
|
+
if (!cold.length) return;
|
|
897
|
+
|
|
898
|
+
await prewarm({ origin: expiryOrigin, paths: cold, quiet: true });
|
|
899
|
+
} catch (error) {
|
|
900
|
+
console.error("[prewarm] expiry warm failed", error);
|
|
901
|
+
} finally {
|
|
902
|
+
expiryDraining = false;
|
|
903
|
+
}
|
|
904
|
+
}
|