jskelet 0.2.3 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +132 -132
- package/CHANGELOG.md +15 -0
- package/LICENSE +21 -21
- package/bin/jskelet.mjs +103 -103
- package/docs/01-baslangic.md +285 -285
- package/docs/02-mimari.md +287 -287
- package/docs/03-routing.md +480 -480
- package/docs/04-render-ve-sablonlar.md +490 -490
- package/docs/05-islands.md +482 -482
- package/docs/06-cache.md +1209 -1202
- package/docs/08-build.md +366 -366
- package/docs/09-dev-araclari.md +335 -335
- package/docs/10-dagitim.md +329 -329
- package/docs/12-panel-ve-oturum.md +384 -384
- package/docs/README.md +105 -105
- package/docs/en/01-getting-started.md +292 -292
- package/docs/en/02-architecture.md +305 -305
- package/docs/en/03-routing.md +497 -497
- package/docs/en/04-rendering.md +504 -504
- package/docs/en/05-islands.md +492 -492
- package/docs/en/06-caching.md +1239 -1232
- package/docs/en/07-configuration.md +986 -986
- package/docs/en/08-build.md +383 -383
- package/docs/en/09-dev-tools.md +342 -342
- package/docs/en/10-deployment.md +332 -332
- package/docs/en/11-migration.md +359 -359
- package/docs/en/12-dashboards-and-sessions.md +392 -392
- package/docs/en/README.md +112 -112
- package/package.json +102 -102
- package/src/build/ensure-build.mjs +15 -15
- package/src/build/paths.mjs +143 -143
- package/src/build/resolve-peer.mjs +36 -36
- package/src/build/tasks/client.mjs +268 -268
- package/src/build/tasks/css.mjs +124 -124
- package/src/build/tasks/fonts.mjs +146 -146
- package/src/build/tasks/icons.mjs +224 -224
- package/src/build/tasks/images.mjs +244 -244
- package/src/build/tasks/precompress.mjs +78 -78
- package/src/client/cache-panel/i18n.js +670 -0
- package/src/client/cache-panel/login.html +74 -71
- package/src/client/cache-panel/panel.css +756 -740
- package/src/client/cache-panel/panel.html +308 -307
- package/src/client/cache-panel/panel.js +915 -808
- package/src/client/devtools/report.html +185 -185
- package/src/client/devtools/report.js +725 -725
- package/src/client/dom.js +95 -95
- package/src/client/form.js +192 -192
- package/src/client/index.js +35 -35
- package/src/client/registry.js +297 -297
- package/src/client/safe-image.js +91 -91
- package/src/client/store.js +36 -36
- package/src/client/swap.js +188 -188
- package/src/config/pattern.js +107 -107
- package/src/http/control-flow.js +71 -71
- package/src/http/cookies.js +257 -257
- package/src/http/request-cache.js +46 -46
- package/src/http/request-context.js +162 -162
- package/src/index.js +83 -83
- package/src/init.mjs +221 -221
- package/src/runtime/alias-hooks.mjs +119 -119
- package/src/runtime/register.mjs +4 -4
- package/src/server/assets.js +147 -147
- package/src/server/cache-deps.js +42 -42
- package/src/server/cache-panel.js +759 -738
- package/src/server/cloudflare.js +607 -595
- package/src/server/create-app.js +291 -291
- package/src/server/data-cache.js +462 -462
- package/src/server/dev/report.js +369 -369
- package/src/server/dev/socket.js +170 -170
- package/src/server/dev/version-check.mjs +139 -139
- package/src/server/html-cache.js +817 -817
- package/src/server/metadata.js +102 -102
- package/src/server/middleware/compression.js +205 -205
- package/src/server/middleware/csrf.js +134 -134
- package/src/server/middleware/dev-gate.js +62 -62
- package/src/server/middleware/headers.js +37 -37
- package/src/server/middleware/redirects.js +32 -32
- package/src/server/middleware/static-precompressed.js +100 -100
- package/src/server/middleware/upstream-proxy.js +141 -141
- package/src/server/prewarm.js +601 -601
- package/src/server/redis.js +569 -569
- package/src/server/router.js +128 -128
- package/src/server/status-page.js +164 -164
- package/src/server/upstream-limiter.js +376 -376
- package/src/server/upstream-tracking.js +166 -166
- package/src/start.mjs +7 -7
- package/src/templates/layout.ejs +44 -44
- package/src/version.mjs +31 -31
- package/src/views/components/loader.js +85 -85
- package/src/views/helpers/html.js +102 -102
- package/src/views/helpers/tags.js +245 -245
package/src/server/prewarm.js
CHANGED
|
@@ -1,601 +1,601 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Sunucu açılışında sayfaları önden render edip HTML cache'ini doldurur.
|
|
3
|
-
*
|
|
4
|
-
* Next'teki build-time prerender'ın karşılığı, ama çıktı diske yazılmaz:
|
|
5
|
-
* HTML cache süreç belleğinde yaşadığı için ısıtma da süreç ayağa kalkınca
|
|
6
|
-
* yapılır. Kazanç aynı — ilk ziyaretçi soğuk render'ı beklemez — fakat veri
|
|
7
|
-
* dondurulmaz: her girdi route'un `revalidate` süresiyle yaşlanır ve
|
|
8
|
-
* stale-while-revalidate ile arkada tazelenir.
|
|
9
|
-
*
|
|
10
|
-
* Isıtma gerçek HTTP istekleriyle yapılır: cache anahtarı, sıkıştırma ve
|
|
11
|
-
* middleware zinciri normal trafikle bire bir aynı olsun. Hangi yolların
|
|
12
|
-
* ısıtılacağını uygulama `hooks.prewarmPaths()` ile bildirir; genelde
|
|
13
|
-
* sitemap üreten fonksiyonun aynısıdır.
|
|
14
|
-
*
|
|
15
|
-
* On binlerce yolluk bir sitede tur bir "damla damla" tarayıcıya dönüşür:
|
|
16
|
-
* `priority` desenleri her turda başa alınır, geri kalan kuyruk turlar
|
|
17
|
-
* arasında kaldığı yerden devam eder (`rotate`) ve `rps` toplam hızı upstream
|
|
18
|
-
* kotasının altında tutar. Amaç, kimse gelmese bile hiçbir sayfanın soğuk
|
|
19
|
-
* kalmaması — ama bunu API'yi düşürmeden yapmak.
|
|
20
|
-
*/
|
|
21
|
-
import process from "node:process";
|
|
22
|
-
import { getConfig, hook } from "../config/index.js";
|
|
23
|
-
import { getRequestContext } from "../http/request-context.js";
|
|
24
|
-
import { takeInvalidatedPaths } from "./html-cache.js";
|
|
25
|
-
import { getDataCacheStats } from "./data-cache.js";
|
|
26
|
-
import { isTransientStatus } from "./upstream-tracking.js";
|
|
27
|
-
import { upstreamCooldownMs } from "./upstream-limiter.js";
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Isıtmanın canlı durumu. Dev araçları bunu okuyup ilerlemeyi gösterir;
|
|
31
|
-
* üretimde kimse okumazsa da maliyeti bir nesnedir.
|
|
32
|
-
*/
|
|
33
|
-
export const prewarmProgress = {
|
|
34
|
-
active: false,
|
|
35
|
-
done: 0,
|
|
36
|
-
total: 0,
|
|
37
|
-
ok: 0,
|
|
38
|
-
failed: 0,
|
|
39
|
-
/** @type {number | null} */
|
|
40
|
-
startedAt: null,
|
|
41
|
-
/** @type {number | null} */
|
|
42
|
-
finishedAt: null,
|
|
43
|
-
/**
|
|
44
|
-
* Denenen her yolun sonucu; dev panelindeki Prewarming sekmesi bunu listeler.
|
|
45
|
-
* @type {{ path: string, status: number, ms: number, bytes: number,
|
|
46
|
-
* cache: string | null, error: string | null }[]}
|
|
47
|
-
*/
|
|
48
|
-
entries: [],
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Isıtma turu sırasında bastırılan uyarılar: mesaj → kaç kez görüldü.
|
|
53
|
-
*
|
|
54
|
-
* Yüzlerce yolu tarayan bir tur, upstream bir an için tıksırdığında yüzlerce
|
|
55
|
-
* satır loga döküyor ve asıl bilgi (kaç sayfa ısındı) kayboluyor. Tur boyunca
|
|
56
|
-
* mesajlar burada toplanır, tur bitince tek satırda özetlenir. Yol adı
|
|
57
|
-
* anahtara girmez: gruplanabilmesi için mesajın kendisi yeterli, tek bir yolun
|
|
58
|
-
* ayrıntısı zaten `entries` üzerinden dev panelinde görünüyor.
|
|
59
|
-
* @type {Map<string, number>}
|
|
60
|
-
*/
|
|
61
|
-
const suppressed = new Map();
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* İstek ısıtma turunun kendi isteği mi? Yalnızca tur çalışırken ve istek
|
|
65
|
-
* ısıtmanın user-agent'ıyla geldiğinde doğru; gerçek trafiğin hataları her
|
|
66
|
-
* zaman loglanmaya devam eder.
|
|
67
|
-
*
|
|
68
|
-
* @param {{ get?: (name: string) => string | undefined } | null | undefined} req
|
|
69
|
-
* @returns {boolean}
|
|
70
|
-
*/
|
|
71
|
-
export function isPrewarmRequest(req) {
|
|
72
|
-
if (!prewarmProgress.active || !req) return false;
|
|
73
|
-
const ua = req.get?.("user-agent");
|
|
74
|
-
return Boolean(ua) && ua === getConfig().brand.prewarmUserAgent;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Şu an işlenen istek ısıtma turuna mı ait? `req` elde olmayan derin
|
|
79
|
-
* katmanlar (render, cache) için: yanıt nesnesi istek bağlamında taşınıyor.
|
|
80
|
-
*
|
|
81
|
-
* @returns {boolean}
|
|
82
|
-
*/
|
|
83
|
-
function inPrewarmRequest() {
|
|
84
|
-
if (!prewarmProgress.active) return false;
|
|
85
|
-
return isPrewarmRequest(getRequestContext()?.res?.req);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Isıtma turuna ait bir uyarıyı loglamak yerine sayar.
|
|
90
|
-
*
|
|
91
|
-
* @param {string} message Gruplama anahtarı; yol adı içermemeli.
|
|
92
|
-
* @returns {boolean} `true` ise sayıldı, çağıran taraf loglamamalı.
|
|
93
|
-
*/
|
|
94
|
-
export function suppressForPrewarm(message) {
|
|
95
|
-
if (!inPrewarmRequest()) return false;
|
|
96
|
-
suppressed.set(message, (suppressed.get(message) ?? 0) + 1);
|
|
97
|
-
return true;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Bastırılan bir istek hatasını sayaca ekler. Yığın izi saklanmaz: özet
|
|
102
|
-
* satırının amacı "neyin bozulduğunu" göstermek, hatayı ayıklamak değil.
|
|
103
|
-
*
|
|
104
|
-
* @param {number} status
|
|
105
|
-
* @param {unknown} error
|
|
106
|
-
* @returns {void}
|
|
107
|
-
*/
|
|
108
|
-
export function notePrewarmError(status, error) {
|
|
109
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
110
|
-
const key = `${status} ${message.split("\n")[0]}`;
|
|
111
|
-
suppressed.set(key, (suppressed.get(key) ?? 0) + 1);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/** @param {unknown} value @param {number} fallback */
|
|
115
|
-
function num(value, fallback) {
|
|
116
|
-
const parsed = Number(value);
|
|
117
|
-
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Ayar sırası: ortam değişkeni → `jskelet.config.mjs` → kod varsayılanı.
|
|
122
|
-
* Env önde, çünkü tek seferlik deneyler config'i düzenlemeden yapılabilsin.
|
|
123
|
-
*
|
|
124
|
-
* @param {string} envKey
|
|
125
|
-
* @param {string} configKey
|
|
126
|
-
* @param {number} fallback
|
|
127
|
-
* @returns {number}
|
|
128
|
-
*/
|
|
129
|
-
function setting(envKey, configKey, fallback) {
|
|
130
|
-
return num(process.env[envKey], num(getConfig().prewarm?.[configKey], fallback));
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* @returns {Promise<string[]>}
|
|
135
|
-
*/
|
|
136
|
-
async function collectPaths() {
|
|
137
|
-
const { prewarmSkip } = getConfig();
|
|
138
|
-
const paths = await hook("prewarmPaths", []);
|
|
139
|
-
|
|
140
|
-
if (!Array.isArray(paths)) {
|
|
141
|
-
console.warn("[prewarm] hooks.prewarmPaths() must return an array, ignoring it");
|
|
142
|
-
return [];
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Tekilleştirme sırayı korur: liste `PREWARM_MAX` ile budandığı için
|
|
146
|
-
// uygulamanın verdiği öncelik sırası anlamlıdır.
|
|
147
|
-
return [...new Set(paths)].filter(
|
|
148
|
-
(candidate) =>
|
|
149
|
-
typeof candidate === "string" &&
|
|
150
|
-
candidate.startsWith("/") &&
|
|
151
|
-
!prewarmSkip.some((prefix) => candidate.startsWith(prefix)),
|
|
152
|
-
);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* `cache().prewarm.priority` desenlerine göre sıralar. Eşleşmeyen yollar
|
|
157
|
-
* listenin sonuna, kendi aralarındaki sırayı koruyarak gider — uygulamanın
|
|
158
|
-
* verdiği sıra hâlâ anlamlı olsun.
|
|
159
|
-
*
|
|
160
|
-
* @param {string[]} paths
|
|
161
|
-
* @returns {{ head: string[], tail: string[] }}
|
|
162
|
-
* `head` öncelikli yollar (her turda ısıtılır), `tail` geri kalan kuyruk
|
|
163
|
-
* (turlar arasında dolaşılır).
|
|
164
|
-
*/
|
|
165
|
-
function byPriority(paths) {
|
|
166
|
-
const rules = getConfig().prewarmPriority;
|
|
167
|
-
if (!rules.length) return { head: [], tail: paths };
|
|
168
|
-
|
|
169
|
-
/** @type {string[][]} */
|
|
170
|
-
const buckets = rules.map(() => []);
|
|
171
|
-
/** @type {string[]} */
|
|
172
|
-
const tail = [];
|
|
173
|
-
|
|
174
|
-
for (const candidate of paths) {
|
|
175
|
-
const rank = rules.findIndex((rule) => rule.test(candidate));
|
|
176
|
-
if (rank === -1) tail.push(candidate);
|
|
177
|
-
else buckets[rank].push(candidate);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
return { head: buckets.flat(), tail };
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Kuyruğun kaldığı yer. Periyodik turlar listeyi baştan ısıtıp aynı ilk
|
|
185
|
-
* `max` yolu tekrar tekrar tazelemesin: her tur bir sonraki dilimi alır ve
|
|
186
|
-
* yeterli tur sonunda liste baştan sona ısınır.
|
|
187
|
-
*/
|
|
188
|
-
let queueCursor = 0;
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Bir turda ısıtılacak dilimi seçer: önce `priority` eşleşenler, sonra
|
|
192
|
-
* kuyruğun sırası gelen parçası. Dışa açık olması bilinçli — sıralama ve
|
|
193
|
-
* rotasyon, tur çalışmadan doğrulanabilen tek davranış.
|
|
194
|
-
*
|
|
195
|
-
* @param {string[]} all
|
|
196
|
-
* @param {number} limit
|
|
197
|
-
* @param {boolean} rotate
|
|
198
|
-
* @returns {string[]}
|
|
199
|
-
*/
|
|
200
|
-
export function selectPrewarmPaths(all, limit, rotate = true) {
|
|
201
|
-
if (all.length <= limit) return all;
|
|
202
|
-
|
|
203
|
-
const { head, tail } = byPriority(all);
|
|
204
|
-
const selected = head.slice(0, limit);
|
|
205
|
-
const room = limit - selected.length;
|
|
206
|
-
if (room <= 0 || !tail.length) return selected;
|
|
207
|
-
|
|
208
|
-
if (!rotate) return [...selected, ...tail.slice(0, room)];
|
|
209
|
-
|
|
210
|
-
// Dilim listenin sonunu aşarsa başa sarar: kuyruk halkasal dolaşılır.
|
|
211
|
-
const start = queueCursor % tail.length;
|
|
212
|
-
const slice = tail.slice(start, start + room);
|
|
213
|
-
if (slice.length < room) slice.push(...tail.slice(0, room - slice.length));
|
|
214
|
-
queueCursor = (start + room) % tail.length;
|
|
215
|
-
|
|
216
|
-
return [...selected, ...slice];
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/** @param {number} ms */
|
|
220
|
-
function sleep(ms) {
|
|
221
|
-
return new Promise((resolve) => {
|
|
222
|
-
setTimeout(resolve, ms).unref?.();
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* Saniyedeki istek sayısını sınırlar. Fren `concurrency`'den bağımsız
|
|
228
|
-
* olmalı: paralellik gecikmeyi kapatmak için var, kotayı koruyan şey toplam
|
|
229
|
-
* hız. İşçiler aynı sayacı paylaştığı için sıra kimde olursa olsun tur
|
|
230
|
-
* verilen hızın üstüne çıkmaz.
|
|
231
|
-
*
|
|
232
|
-
* @param {number} rps 0 → sınırsız
|
|
233
|
-
* @returns {() => Promise<void>}
|
|
234
|
-
*/
|
|
235
|
-
function createPacer(rps) {
|
|
236
|
-
if (!rps) return async () => {};
|
|
237
|
-
|
|
238
|
-
const gap = 1000 / rps;
|
|
239
|
-
let nextSlot = 0;
|
|
240
|
-
|
|
241
|
-
return async () => {
|
|
242
|
-
const now = Date.now();
|
|
243
|
-
const slot = Math.max(now, nextSlot);
|
|
244
|
-
nextSlot = slot + gap;
|
|
245
|
-
if (slot > now) await sleep(slot - now);
|
|
246
|
-
};
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* `DEV_TOKEN` ayarlıyken `devGate` token taşımayan her isteğe 404 döner.
|
|
251
|
-
* Isıtma kendi sunucusuna istek attığı için token'ı çerez olarak taşımalı;
|
|
252
|
-
* yoksa tüm sayfalar 404 alır ve önbellek hiç dolmaz.
|
|
253
|
-
*
|
|
254
|
-
* @returns {Record<string, string>}
|
|
255
|
-
*/
|
|
256
|
-
function devGateHeader() {
|
|
257
|
-
const token = process.env.DEV_TOKEN;
|
|
258
|
-
if (!token) return {};
|
|
259
|
-
|
|
260
|
-
const cookie = getConfig().brand.devTokenCookie;
|
|
261
|
-
return { cookie: `${cookie}=${encodeURIComponent(token)}` };
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
/**
|
|
265
|
-
* @param {string} origin
|
|
266
|
-
* @param {string[]} paths
|
|
267
|
-
* @param {number} concurrency
|
|
268
|
-
* @param {(ok: number, failed: number) => void} [report]
|
|
269
|
-
* Tur ilerlemesini `prewarmProgress`'e yazar. Tekrar turunda sayaçların
|
|
270
|
-
* anlamı değiştiği için çağıran taraf kendi formülünü verir.
|
|
271
|
-
* @param {() => Promise<void>} [pace] İstek başına beklenen hız freni.
|
|
272
|
-
* @returns {Promise<{ ok: number, failed: number,
|
|
273
|
-
* failures: { path: string, status: number }[] }>}
|
|
274
|
-
* `failures` durum koduyla birlikte döner: tekrar turuna yalnızca geçici
|
|
275
|
-
* hatalar alınıyor, kalıcı olanı yeniden denemek boşa çağrı.
|
|
276
|
-
*/
|
|
277
|
-
async function crawl(origin, paths, concurrency, report = undefined, pace = undefined) {
|
|
278
|
-
const { brand } = getConfig();
|
|
279
|
-
const cacheHeader = brand.cacheHeader.toLowerCase();
|
|
280
|
-
|
|
281
|
-
let index = 0;
|
|
282
|
-
let ok = 0;
|
|
283
|
-
let failed = 0;
|
|
284
|
-
/** @type {{ path: string, status: number }[]} */
|
|
285
|
-
const failures = [];
|
|
286
|
-
|
|
287
|
-
async function worker() {
|
|
288
|
-
while (index < paths.length) {
|
|
289
|
-
const target = paths[index];
|
|
290
|
-
index += 1;
|
|
291
|
-
|
|
292
|
-
if (pace) await pace();
|
|
293
|
-
|
|
294
|
-
const startedAt = Date.now();
|
|
295
|
-
|
|
296
|
-
try {
|
|
297
|
-
const response = await fetch(`${origin}${target}`, {
|
|
298
|
-
headers: {
|
|
299
|
-
// Sıkıştırılmış gövde de cache'lensin.
|
|
300
|
-
"accept-encoding": "br, gzip",
|
|
301
|
-
"user-agent": brand.prewarmUserAgent,
|
|
302
|
-
...devGateHeader(),
|
|
303
|
-
},
|
|
304
|
-
});
|
|
305
|
-
// Gövde okunmadan bağlantı açık kalır.
|
|
306
|
-
const body = await response.arrayBuffer();
|
|
307
|
-
if (response.ok) ok += 1;
|
|
308
|
-
else {
|
|
309
|
-
failed += 1;
|
|
310
|
-
failures.push({ path: target, status: response.status });
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
prewarmProgress.entries.push({
|
|
314
|
-
path: target,
|
|
315
|
-
status: response.status,
|
|
316
|
-
ms: Date.now() - startedAt,
|
|
317
|
-
bytes: body.byteLength,
|
|
318
|
-
cache: response.headers.get(cacheHeader),
|
|
319
|
-
error: response.ok ? null : `HTTP ${response.status}`,
|
|
320
|
-
});
|
|
321
|
-
} catch (error) {
|
|
322
|
-
failed += 1;
|
|
323
|
-
// Yanıt hiç gelmedi: ağ hatası her zaman geçici, tekrar denenir.
|
|
324
|
-
failures.push({ path: target, status: 0 });
|
|
325
|
-
prewarmProgress.entries.push({
|
|
326
|
-
path: target,
|
|
327
|
-
status: 0,
|
|
328
|
-
ms: Date.now() - startedAt,
|
|
329
|
-
bytes: 0,
|
|
330
|
-
cache: null,
|
|
331
|
-
error: error instanceof Error ? error.message : String(error),
|
|
332
|
-
});
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
if (report) {
|
|
336
|
-
report(ok, failed);
|
|
337
|
-
} else {
|
|
338
|
-
prewarmProgress.done = ok + failed;
|
|
339
|
-
prewarmProgress.ok = ok;
|
|
340
|
-
prewarmProgress.failed = failed;
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
await Promise.all(
|
|
346
|
-
Array.from({ length: Math.min(concurrency, paths.length) }, worker),
|
|
347
|
-
);
|
|
348
|
-
|
|
349
|
-
return { ok, failed, failures };
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
/**
|
|
353
|
-
* Tekrar turuna girecek yollar. Yalnızca geçici hatalar: `400`/`403`/`404` gibi
|
|
354
|
-
* deterministik cevaplar tekrar denemekle düzelmez ve o çağrılar kotadan
|
|
355
|
-
* karşılıksız yiyor. Sınıflandırma render tarafıyla aynı listeden
|
|
356
|
-
* (`isTransientStatus`), yoksa iki yer birbirinden kayar.
|
|
357
|
-
*
|
|
358
|
-
* @param {{ path: string, status: number }[]} failures
|
|
359
|
-
* @returns {string[]}
|
|
360
|
-
*/
|
|
361
|
-
function retryablePaths(failures) {
|
|
362
|
-
return failures
|
|
363
|
-
.filter((failure) => isTransientStatus(failure.status))
|
|
364
|
-
.map((failure) => failure.path);
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
/**
|
|
368
|
-
* Tekrar turundan önce beklenecek süre.
|
|
369
|
-
*
|
|
370
|
-
* Sabit bekleme yanlış soruyu cevaplıyordu: doğru süreyi upstream biliyor.
|
|
371
|
-
* Hız freni açıkken `Retry-After` ya da devre kesicinin soğuma süresi zaten
|
|
372
|
-
* elimizde; 10 saniye kapalı kalacak bir kesiciden 2 saniye sonra tekrar
|
|
373
|
-
* denemek, aynı 429'u peşin peşin almak demek.
|
|
374
|
-
*
|
|
375
|
-
* @returns {number} ms
|
|
376
|
-
*/
|
|
377
|
-
function retryDelayMs() {
|
|
378
|
-
const configured = setting("PREWARM_RETRY_DELAY_MS", "retryDelayMs", 2000);
|
|
379
|
-
const cooldown = upstreamCooldownMs();
|
|
380
|
-
|
|
381
|
-
// Fren kapalıysa `cooldown` 0 olur ve davranış eskisi gibi kalır. Üst sınır
|
|
382
|
-
// turun sonsuza kadar açık kalmasını engelliyor.
|
|
383
|
-
return Math.min(Math.max(configured, cooldown), 60_000);
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
/**
|
|
387
|
-
* Turun veri önbelleği üzerinden upstream'e ne kadar dokunduğunu özetler.
|
|
388
|
-
*
|
|
389
|
-
* @param {ReturnType<typeof getDataCacheStats>} before Tur başındaki sayaçlar.
|
|
390
|
-
* @returns {string} Okunacak bir şey yoksa boş dize.
|
|
391
|
-
*/
|
|
392
|
-
function upstreamUsage(before) {
|
|
393
|
-
const after = getDataCacheStats();
|
|
394
|
-
const reads = after.reads - before.reads;
|
|
395
|
-
if (reads <= 0) return "";
|
|
396
|
-
|
|
397
|
-
const produced = after.produced - before.produced;
|
|
398
|
-
const coalesced = after.coalesced - before.coalesced;
|
|
399
|
-
const shared = after.shared - before.shared;
|
|
400
|
-
const served = reads - produced;
|
|
401
|
-
const ratio = Math.round((served / reads) * 100);
|
|
402
|
-
|
|
403
|
-
return (
|
|
404
|
-
`${produced} upstream call${produced === 1 ? "" : "s"} for ${reads} data read${reads === 1 ? "" : "s"} ` +
|
|
405
|
-
`(${ratio}% from the data cache` +
|
|
406
|
-
`${coalesced ? `, ${coalesced} coalesced` : ""}` +
|
|
407
|
-
`${shared ? `, ${shared} from the shared tier` : ""})`
|
|
408
|
-
);
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
/**
|
|
412
|
-
* @param {{ origin: string, quiet?: boolean, paths?: string[] }} options
|
|
413
|
-
* `paths` verilirse hook çağrılmaz, yalnızca o yollar ısıtılır (dev
|
|
414
|
-
* panelindeki "tekrar dene" bunu kullanır).
|
|
415
|
-
* @returns {Promise<{ ok: number, failed: number, total: number, elapsed: number }>}
|
|
416
|
-
*/
|
|
417
|
-
export async function prewarm({ origin, quiet = false, paths: only }) {
|
|
418
|
-
const started = Date.now();
|
|
419
|
-
const limit = setting("PREWARM_MAX", "max", 400);
|
|
420
|
-
const isDev = process.env.NODE_ENV === "development";
|
|
421
|
-
|
|
422
|
-
// Dev'de tek işçi: tarama, o an tarayıcıda açtığın sayfanın render'ıyla CPU
|
|
423
|
-
// için yarışmasın.
|
|
424
|
-
const concurrency = setting("PREWARM_CONCURRENCY", "concurrency", isDev ? 1 : 4);
|
|
425
|
-
|
|
426
|
-
// Render tek bir olay döngüsünde çalışıyor: aralıksız bir tur, geliştirme
|
|
427
|
-
// sırasında sayfa isteklerini ve dev panelinin kanalını arkasında bekletiyor.
|
|
428
|
-
// Dev'de varsayılan bir hız freni bu yüzden var; üretimde ısıtma bir kez
|
|
429
|
-
// olup bittiği için fren yalnızca istenirse (`prewarm.rps`) devreye girer.
|
|
430
|
-
const rps = num(process.env.PREWARM_RPS, num(getConfig().prewarm?.rps, isDev ? 4 : 0));
|
|
431
|
-
const pace = createPacer(rps);
|
|
432
|
-
|
|
433
|
-
const all = only?.length ? only : await collectPaths();
|
|
434
|
-
// Elle verilen liste budanmaz ve sıralanmaz: çağıran tam olarak neyi
|
|
435
|
-
// istediğini biliyor (dev panelindeki "tekrar dene" bunu kullanır).
|
|
436
|
-
const selected = only?.length
|
|
437
|
-
? all
|
|
438
|
-
: selectPrewarmPaths(all, limit, getConfig().prewarm?.rotate !== false);
|
|
439
|
-
|
|
440
|
-
// Invalidate edilmiş sayfalar kuyruğun önüne geçer: "içerik güncellendi"
|
|
441
|
-
// bilgisi geldiğinde sayfa, ziyaretçi gelmesini beklemeden tazelenir. Bu
|
|
442
|
-
// yollar `max` bütçesinin dışında tutulur — sayıları zaten gerçekleşen
|
|
443
|
-
// invalidation kadar ve rotasyonun sırasını bozmaları istenmez.
|
|
444
|
-
const pending = only?.length ? [] : takeInvalidatedPaths();
|
|
445
|
-
const paths = pending.length
|
|
446
|
-
? [...new Set([...pending, ...selected])]
|
|
447
|
-
: selected;
|
|
448
|
-
|
|
449
|
-
Object.assign(prewarmProgress, {
|
|
450
|
-
active: true,
|
|
451
|
-
done: 0,
|
|
452
|
-
total: paths.length,
|
|
453
|
-
ok: 0,
|
|
454
|
-
failed: 0,
|
|
455
|
-
startedAt: started,
|
|
456
|
-
finishedAt: null,
|
|
457
|
-
entries: [],
|
|
458
|
-
});
|
|
459
|
-
suppressed.clear();
|
|
460
|
-
|
|
461
|
-
// Kotanın gerçekten ne kadar harcandığı ancak veri önbelleğinden görülür:
|
|
462
|
-
// 400 sayfalık bir tur, ortak bir uç için tek çağrı da yapabilir dört yüz de.
|
|
463
|
-
const dataBefore = getDataCacheStats();
|
|
464
|
-
|
|
465
|
-
let ok = 0;
|
|
466
|
-
let failed = 0;
|
|
467
|
-
let recovered = 0;
|
|
468
|
-
let skippedRetry = 0;
|
|
469
|
-
try {
|
|
470
|
-
/** @type {{ path: string, status: number }[]} */
|
|
471
|
-
let failures;
|
|
472
|
-
({ ok, failed, failures } = await crawl(
|
|
473
|
-
origin,
|
|
474
|
-
paths,
|
|
475
|
-
concurrency,
|
|
476
|
-
undefined,
|
|
477
|
-
pace,
|
|
478
|
-
));
|
|
479
|
-
|
|
480
|
-
// Hatalar çoğunlukla upstream rate limit'i (429): ilk tur yüzlerce sayfayı
|
|
481
|
-
// aynı anda çekerken API'yi zorluyor. Tek seri tekrar turu bu sayfaların
|
|
482
|
-
// önbelleğe girmesini sağlıyor; aksi hâlde ziyaretçi soğuk render'ı öder.
|
|
483
|
-
const retryPaths = retryablePaths(failures);
|
|
484
|
-
skippedRetry = failures.length - retryPaths.length;
|
|
485
|
-
|
|
486
|
-
if (retryPaths.length) {
|
|
487
|
-
const firstOk = ok;
|
|
488
|
-
const firstFailed = failed;
|
|
489
|
-
|
|
490
|
-
await sleep(retryDelayMs());
|
|
491
|
-
|
|
492
|
-
const retry = await crawl(
|
|
493
|
-
origin,
|
|
494
|
-
retryPaths,
|
|
495
|
-
1,
|
|
496
|
-
(retriedOk) => {
|
|
497
|
-
// Tekrar turunda her başarı bir hatayı başarıya çevirir.
|
|
498
|
-
prewarmProgress.ok = firstOk + retriedOk;
|
|
499
|
-
prewarmProgress.failed = firstFailed - retriedOk;
|
|
500
|
-
},
|
|
501
|
-
pace,
|
|
502
|
-
);
|
|
503
|
-
recovered = retry.ok;
|
|
504
|
-
ok += retry.ok;
|
|
505
|
-
failed -= retry.ok;
|
|
506
|
-
}
|
|
507
|
-
} finally {
|
|
508
|
-
prewarmProgress.active = false;
|
|
509
|
-
prewarmProgress.finishedAt = Date.now();
|
|
510
|
-
}
|
|
511
|
-
const elapsed = Date.now() - started;
|
|
512
|
-
|
|
513
|
-
if (!quiet && paths.length) {
|
|
514
|
-
const skipped = all.length - selected.length;
|
|
515
|
-
// Rotasyon açıkken sınırın dışında kalan yollar kaybolmuyor, bir sonraki
|
|
516
|
-
// tura kalıyor; log bunu ayırt etmeli, yoksa "400 yol atlandı" satırı
|
|
517
|
-
// hatalı bir kurulum sanılıyor.
|
|
518
|
-
const rotate = !only?.length && getConfig().prewarm?.rotate !== false;
|
|
519
|
-
console.log(
|
|
520
|
-
`[prewarm] warmed ${ok}/${paths.length} pages` +
|
|
521
|
-
`${pending.length ? `, ${pending.length} invalidated` : ""}` +
|
|
522
|
-
`${failed ? `, ${failed} failed` : ""}` +
|
|
523
|
-
`${recovered ? `, ${recovered} recovered on the retry pass` : ""}` +
|
|
524
|
-
`${skippedRetry ? `, ${skippedRetry} not retried (permanent)` : ""}` +
|
|
525
|
-
`${skipped > 0 ? `, ${skipped} ${rotate ? "deferred to the next pass" : "over the limit"}` : ""}` +
|
|
526
|
-
` (${(elapsed / 1000).toFixed(1)}s)`,
|
|
527
|
-
);
|
|
528
|
-
|
|
529
|
-
// Turun upstream'e ne kadar dokunduğu. Asıl karar bu satıra bakılarak
|
|
530
|
-
// veriliyor: oran düşükse çözüm hız freni değil, `withDataCache` TTL'ini
|
|
531
|
-
// tur aralığından uzun tutmak — fren çağrıları yavaşlatır, sayısını
|
|
532
|
-
// azaltmaz.
|
|
533
|
-
const upstreamCalls = upstreamUsage(dataBefore);
|
|
534
|
-
if (upstreamCalls) console.log(`[prewarm] ${upstreamCalls}`);
|
|
535
|
-
|
|
536
|
-
// Tur boyunca bastırılan hatalar: en sık görülenler önce, liste uzarsa
|
|
537
|
-
// kalanı tek satırda toplanır. Amaç, logu şişirmeden "ne bozuldu"yu
|
|
538
|
-
// görünür tutmak.
|
|
539
|
-
if (suppressed.size) {
|
|
540
|
-
const ranked = [...suppressed.entries()].sort((a, b) => b[1] - a[1]);
|
|
541
|
-
const shown = ranked.slice(0, 5);
|
|
542
|
-
const rest = ranked.slice(shown.length).reduce((sum, [, n]) => sum + n, 0);
|
|
543
|
-
const total = ranked.reduce((sum, [, n]) => sum + n, 0);
|
|
544
|
-
|
|
545
|
-
console.warn(
|
|
546
|
-
`[prewarm] ${total} problem${total === 1 ? " was" : "s were"} not logged individually:\n` +
|
|
547
|
-
shown.map(([message, n]) => ` ${n}× ${message}`).join("\n") +
|
|
548
|
-
(rest ? `\n … ${rest} more in ${ranked.length - shown.length} other kinds` : ""),
|
|
549
|
-
);
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
return { ok, failed, total: paths.length, elapsed };
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
/**
|
|
557
|
-
* Açılışta ısıtmayı tetikler. `listen` geri çağrısından çağrılır; isteğe
|
|
558
|
-
* bağlı olarak periyodik tekrarlar. Hiçbir hata süreci düşürmez.
|
|
559
|
-
*
|
|
560
|
-
* @param {{ port: number }} options
|
|
561
|
-
* @returns {void}
|
|
562
|
-
*/
|
|
563
|
-
export function startPrewarm({ port }) {
|
|
564
|
-
const config = getConfig();
|
|
565
|
-
if (process.env.PREWARM === "0") return;
|
|
566
|
-
if (process.env.PREWARM !== "1" && config.prewarm?.enabled === false) return;
|
|
567
|
-
// Isıtacak yol bildirmeyen bir projede zamanlayıcı kurmanın anlamı yok.
|
|
568
|
-
if (typeof config.hooks?.prewarmPaths !== "function") return;
|
|
569
|
-
|
|
570
|
-
const isDev = process.env.NODE_ENV === "development";
|
|
571
|
-
const origin = `http://127.0.0.1:${port}`;
|
|
572
|
-
|
|
573
|
-
// Hız frenli bir tur `intervalSeconds`'tan uzun sürebilir; üst üste binen
|
|
574
|
-
// turlar `prewarmProgress`'i bozar ve upstream'e iki kat yük bindirir.
|
|
575
|
-
let running = false;
|
|
576
|
-
const run = async () => {
|
|
577
|
-
if (running) return;
|
|
578
|
-
running = true;
|
|
579
|
-
try {
|
|
580
|
-
await prewarm({ origin });
|
|
581
|
-
} catch (error) {
|
|
582
|
-
console.error("[prewarm] failed", error);
|
|
583
|
-
} finally {
|
|
584
|
-
running = false;
|
|
585
|
-
}
|
|
586
|
-
};
|
|
587
|
-
|
|
588
|
-
// Isıtma ilk isteklerle yarışmasın diye gecikmeyle başlar. Dev'de gecikme
|
|
589
|
-
// daha uzun: dosya kaydı süreci yeniden başlattığı için zamanlayıcı da
|
|
590
|
-
// ölür; yalnızca sunucu bir süre sakin kalınca ısınır.
|
|
591
|
-
const delay = setting("PREWARM_DELAY_MS", "delayMs", isDev ? 3000 : 500);
|
|
592
|
-
setTimeout(() => void run(), delay).unref();
|
|
593
|
-
|
|
594
|
-
// Girdiler `revalidate` ile yaşlanır; stale-while-revalidate sayesinde
|
|
595
|
-
// ziyaretçi beklemez. Periyodik tur, hiç ziyaret edilmeyen sayfaları da
|
|
596
|
-
// sıcak tutmak isteyen kurulumlar için opsiyoneldir.
|
|
597
|
-
// `rotate` ile birlikte bu ayar "damla damla ısıtma"ya dönüşür: her tur
|
|
598
|
-
// kuyruğun bir dilimini alır, yeterli tur sonunda liste baştan sona ısınır.
|
|
599
|
-
const interval = setting("PREWARM_INTERVAL_SECONDS", "intervalSeconds", 0);
|
|
600
|
-
if (interval > 0) setInterval(() => void run(), interval * 1000).unref();
|
|
601
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Sunucu açılışında sayfaları önden render edip HTML cache'ini doldurur.
|
|
3
|
+
*
|
|
4
|
+
* Next'teki build-time prerender'ın karşılığı, ama çıktı diske yazılmaz:
|
|
5
|
+
* HTML cache süreç belleğinde yaşadığı için ısıtma da süreç ayağa kalkınca
|
|
6
|
+
* yapılır. Kazanç aynı — ilk ziyaretçi soğuk render'ı beklemez — fakat veri
|
|
7
|
+
* dondurulmaz: her girdi route'un `revalidate` süresiyle yaşlanır ve
|
|
8
|
+
* stale-while-revalidate ile arkada tazelenir.
|
|
9
|
+
*
|
|
10
|
+
* Isıtma gerçek HTTP istekleriyle yapılır: cache anahtarı, sıkıştırma ve
|
|
11
|
+
* middleware zinciri normal trafikle bire bir aynı olsun. Hangi yolların
|
|
12
|
+
* ısıtılacağını uygulama `hooks.prewarmPaths()` ile bildirir; genelde
|
|
13
|
+
* sitemap üreten fonksiyonun aynısıdır.
|
|
14
|
+
*
|
|
15
|
+
* On binlerce yolluk bir sitede tur bir "damla damla" tarayıcıya dönüşür:
|
|
16
|
+
* `priority` desenleri her turda başa alınır, geri kalan kuyruk turlar
|
|
17
|
+
* arasında kaldığı yerden devam eder (`rotate`) ve `rps` toplam hızı upstream
|
|
18
|
+
* kotasının altında tutar. Amaç, kimse gelmese bile hiçbir sayfanın soğuk
|
|
19
|
+
* kalmaması — ama bunu API'yi düşürmeden yapmak.
|
|
20
|
+
*/
|
|
21
|
+
import process from "node:process";
|
|
22
|
+
import { getConfig, hook } from "../config/index.js";
|
|
23
|
+
import { getRequestContext } from "../http/request-context.js";
|
|
24
|
+
import { takeInvalidatedPaths } from "./html-cache.js";
|
|
25
|
+
import { getDataCacheStats } from "./data-cache.js";
|
|
26
|
+
import { isTransientStatus } from "./upstream-tracking.js";
|
|
27
|
+
import { upstreamCooldownMs } from "./upstream-limiter.js";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Isıtmanın canlı durumu. Dev araçları bunu okuyup ilerlemeyi gösterir;
|
|
31
|
+
* üretimde kimse okumazsa da maliyeti bir nesnedir.
|
|
32
|
+
*/
|
|
33
|
+
export const prewarmProgress = {
|
|
34
|
+
active: false,
|
|
35
|
+
done: 0,
|
|
36
|
+
total: 0,
|
|
37
|
+
ok: 0,
|
|
38
|
+
failed: 0,
|
|
39
|
+
/** @type {number | null} */
|
|
40
|
+
startedAt: null,
|
|
41
|
+
/** @type {number | null} */
|
|
42
|
+
finishedAt: null,
|
|
43
|
+
/**
|
|
44
|
+
* Denenen her yolun sonucu; dev panelindeki Prewarming sekmesi bunu listeler.
|
|
45
|
+
* @type {{ path: string, status: number, ms: number, bytes: number,
|
|
46
|
+
* cache: string | null, error: string | null }[]}
|
|
47
|
+
*/
|
|
48
|
+
entries: [],
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Isıtma turu sırasında bastırılan uyarılar: mesaj → kaç kez görüldü.
|
|
53
|
+
*
|
|
54
|
+
* Yüzlerce yolu tarayan bir tur, upstream bir an için tıksırdığında yüzlerce
|
|
55
|
+
* satır loga döküyor ve asıl bilgi (kaç sayfa ısındı) kayboluyor. Tur boyunca
|
|
56
|
+
* mesajlar burada toplanır, tur bitince tek satırda özetlenir. Yol adı
|
|
57
|
+
* anahtara girmez: gruplanabilmesi için mesajın kendisi yeterli, tek bir yolun
|
|
58
|
+
* ayrıntısı zaten `entries` üzerinden dev panelinde görünüyor.
|
|
59
|
+
* @type {Map<string, number>}
|
|
60
|
+
*/
|
|
61
|
+
const suppressed = new Map();
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* İstek ısıtma turunun kendi isteği mi? Yalnızca tur çalışırken ve istek
|
|
65
|
+
* ısıtmanın user-agent'ıyla geldiğinde doğru; gerçek trafiğin hataları her
|
|
66
|
+
* zaman loglanmaya devam eder.
|
|
67
|
+
*
|
|
68
|
+
* @param {{ get?: (name: string) => string | undefined } | null | undefined} req
|
|
69
|
+
* @returns {boolean}
|
|
70
|
+
*/
|
|
71
|
+
export function isPrewarmRequest(req) {
|
|
72
|
+
if (!prewarmProgress.active || !req) return false;
|
|
73
|
+
const ua = req.get?.("user-agent");
|
|
74
|
+
return Boolean(ua) && ua === getConfig().brand.prewarmUserAgent;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Şu an işlenen istek ısıtma turuna mı ait? `req` elde olmayan derin
|
|
79
|
+
* katmanlar (render, cache) için: yanıt nesnesi istek bağlamında taşınıyor.
|
|
80
|
+
*
|
|
81
|
+
* @returns {boolean}
|
|
82
|
+
*/
|
|
83
|
+
function inPrewarmRequest() {
|
|
84
|
+
if (!prewarmProgress.active) return false;
|
|
85
|
+
return isPrewarmRequest(getRequestContext()?.res?.req);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Isıtma turuna ait bir uyarıyı loglamak yerine sayar.
|
|
90
|
+
*
|
|
91
|
+
* @param {string} message Gruplama anahtarı; yol adı içermemeli.
|
|
92
|
+
* @returns {boolean} `true` ise sayıldı, çağıran taraf loglamamalı.
|
|
93
|
+
*/
|
|
94
|
+
export function suppressForPrewarm(message) {
|
|
95
|
+
if (!inPrewarmRequest()) return false;
|
|
96
|
+
suppressed.set(message, (suppressed.get(message) ?? 0) + 1);
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Bastırılan bir istek hatasını sayaca ekler. Yığın izi saklanmaz: özet
|
|
102
|
+
* satırının amacı "neyin bozulduğunu" göstermek, hatayı ayıklamak değil.
|
|
103
|
+
*
|
|
104
|
+
* @param {number} status
|
|
105
|
+
* @param {unknown} error
|
|
106
|
+
* @returns {void}
|
|
107
|
+
*/
|
|
108
|
+
export function notePrewarmError(status, error) {
|
|
109
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
110
|
+
const key = `${status} ${message.split("\n")[0]}`;
|
|
111
|
+
suppressed.set(key, (suppressed.get(key) ?? 0) + 1);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** @param {unknown} value @param {number} fallback */
|
|
115
|
+
function num(value, fallback) {
|
|
116
|
+
const parsed = Number(value);
|
|
117
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Ayar sırası: ortam değişkeni → `jskelet.config.mjs` → kod varsayılanı.
|
|
122
|
+
* Env önde, çünkü tek seferlik deneyler config'i düzenlemeden yapılabilsin.
|
|
123
|
+
*
|
|
124
|
+
* @param {string} envKey
|
|
125
|
+
* @param {string} configKey
|
|
126
|
+
* @param {number} fallback
|
|
127
|
+
* @returns {number}
|
|
128
|
+
*/
|
|
129
|
+
function setting(envKey, configKey, fallback) {
|
|
130
|
+
return num(process.env[envKey], num(getConfig().prewarm?.[configKey], fallback));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* @returns {Promise<string[]>}
|
|
135
|
+
*/
|
|
136
|
+
async function collectPaths() {
|
|
137
|
+
const { prewarmSkip } = getConfig();
|
|
138
|
+
const paths = await hook("prewarmPaths", []);
|
|
139
|
+
|
|
140
|
+
if (!Array.isArray(paths)) {
|
|
141
|
+
console.warn("[prewarm] hooks.prewarmPaths() must return an array, ignoring it");
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Tekilleştirme sırayı korur: liste `PREWARM_MAX` ile budandığı için
|
|
146
|
+
// uygulamanın verdiği öncelik sırası anlamlıdır.
|
|
147
|
+
return [...new Set(paths)].filter(
|
|
148
|
+
(candidate) =>
|
|
149
|
+
typeof candidate === "string" &&
|
|
150
|
+
candidate.startsWith("/") &&
|
|
151
|
+
!prewarmSkip.some((prefix) => candidate.startsWith(prefix)),
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* `cache().prewarm.priority` desenlerine göre sıralar. Eşleşmeyen yollar
|
|
157
|
+
* listenin sonuna, kendi aralarındaki sırayı koruyarak gider — uygulamanın
|
|
158
|
+
* verdiği sıra hâlâ anlamlı olsun.
|
|
159
|
+
*
|
|
160
|
+
* @param {string[]} paths
|
|
161
|
+
* @returns {{ head: string[], tail: string[] }}
|
|
162
|
+
* `head` öncelikli yollar (her turda ısıtılır), `tail` geri kalan kuyruk
|
|
163
|
+
* (turlar arasında dolaşılır).
|
|
164
|
+
*/
|
|
165
|
+
function byPriority(paths) {
|
|
166
|
+
const rules = getConfig().prewarmPriority;
|
|
167
|
+
if (!rules.length) return { head: [], tail: paths };
|
|
168
|
+
|
|
169
|
+
/** @type {string[][]} */
|
|
170
|
+
const buckets = rules.map(() => []);
|
|
171
|
+
/** @type {string[]} */
|
|
172
|
+
const tail = [];
|
|
173
|
+
|
|
174
|
+
for (const candidate of paths) {
|
|
175
|
+
const rank = rules.findIndex((rule) => rule.test(candidate));
|
|
176
|
+
if (rank === -1) tail.push(candidate);
|
|
177
|
+
else buckets[rank].push(candidate);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return { head: buckets.flat(), tail };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Kuyruğun kaldığı yer. Periyodik turlar listeyi baştan ısıtıp aynı ilk
|
|
185
|
+
* `max` yolu tekrar tekrar tazelemesin: her tur bir sonraki dilimi alır ve
|
|
186
|
+
* yeterli tur sonunda liste baştan sona ısınır.
|
|
187
|
+
*/
|
|
188
|
+
let queueCursor = 0;
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Bir turda ısıtılacak dilimi seçer: önce `priority` eşleşenler, sonra
|
|
192
|
+
* kuyruğun sırası gelen parçası. Dışa açık olması bilinçli — sıralama ve
|
|
193
|
+
* rotasyon, tur çalışmadan doğrulanabilen tek davranış.
|
|
194
|
+
*
|
|
195
|
+
* @param {string[]} all
|
|
196
|
+
* @param {number} limit
|
|
197
|
+
* @param {boolean} rotate
|
|
198
|
+
* @returns {string[]}
|
|
199
|
+
*/
|
|
200
|
+
export function selectPrewarmPaths(all, limit, rotate = true) {
|
|
201
|
+
if (all.length <= limit) return all;
|
|
202
|
+
|
|
203
|
+
const { head, tail } = byPriority(all);
|
|
204
|
+
const selected = head.slice(0, limit);
|
|
205
|
+
const room = limit - selected.length;
|
|
206
|
+
if (room <= 0 || !tail.length) return selected;
|
|
207
|
+
|
|
208
|
+
if (!rotate) return [...selected, ...tail.slice(0, room)];
|
|
209
|
+
|
|
210
|
+
// Dilim listenin sonunu aşarsa başa sarar: kuyruk halkasal dolaşılır.
|
|
211
|
+
const start = queueCursor % tail.length;
|
|
212
|
+
const slice = tail.slice(start, start + room);
|
|
213
|
+
if (slice.length < room) slice.push(...tail.slice(0, room - slice.length));
|
|
214
|
+
queueCursor = (start + room) % tail.length;
|
|
215
|
+
|
|
216
|
+
return [...selected, ...slice];
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** @param {number} ms */
|
|
220
|
+
function sleep(ms) {
|
|
221
|
+
return new Promise((resolve) => {
|
|
222
|
+
setTimeout(resolve, ms).unref?.();
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Saniyedeki istek sayısını sınırlar. Fren `concurrency`'den bağımsız
|
|
228
|
+
* olmalı: paralellik gecikmeyi kapatmak için var, kotayı koruyan şey toplam
|
|
229
|
+
* hız. İşçiler aynı sayacı paylaştığı için sıra kimde olursa olsun tur
|
|
230
|
+
* verilen hızın üstüne çıkmaz.
|
|
231
|
+
*
|
|
232
|
+
* @param {number} rps 0 → sınırsız
|
|
233
|
+
* @returns {() => Promise<void>}
|
|
234
|
+
*/
|
|
235
|
+
function createPacer(rps) {
|
|
236
|
+
if (!rps) return async () => {};
|
|
237
|
+
|
|
238
|
+
const gap = 1000 / rps;
|
|
239
|
+
let nextSlot = 0;
|
|
240
|
+
|
|
241
|
+
return async () => {
|
|
242
|
+
const now = Date.now();
|
|
243
|
+
const slot = Math.max(now, nextSlot);
|
|
244
|
+
nextSlot = slot + gap;
|
|
245
|
+
if (slot > now) await sleep(slot - now);
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* `DEV_TOKEN` ayarlıyken `devGate` token taşımayan her isteğe 404 döner.
|
|
251
|
+
* Isıtma kendi sunucusuna istek attığı için token'ı çerez olarak taşımalı;
|
|
252
|
+
* yoksa tüm sayfalar 404 alır ve önbellek hiç dolmaz.
|
|
253
|
+
*
|
|
254
|
+
* @returns {Record<string, string>}
|
|
255
|
+
*/
|
|
256
|
+
function devGateHeader() {
|
|
257
|
+
const token = process.env.DEV_TOKEN;
|
|
258
|
+
if (!token) return {};
|
|
259
|
+
|
|
260
|
+
const cookie = getConfig().brand.devTokenCookie;
|
|
261
|
+
return { cookie: `${cookie}=${encodeURIComponent(token)}` };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* @param {string} origin
|
|
266
|
+
* @param {string[]} paths
|
|
267
|
+
* @param {number} concurrency
|
|
268
|
+
* @param {(ok: number, failed: number) => void} [report]
|
|
269
|
+
* Tur ilerlemesini `prewarmProgress`'e yazar. Tekrar turunda sayaçların
|
|
270
|
+
* anlamı değiştiği için çağıran taraf kendi formülünü verir.
|
|
271
|
+
* @param {() => Promise<void>} [pace] İstek başına beklenen hız freni.
|
|
272
|
+
* @returns {Promise<{ ok: number, failed: number,
|
|
273
|
+
* failures: { path: string, status: number }[] }>}
|
|
274
|
+
* `failures` durum koduyla birlikte döner: tekrar turuna yalnızca geçici
|
|
275
|
+
* hatalar alınıyor, kalıcı olanı yeniden denemek boşa çağrı.
|
|
276
|
+
*/
|
|
277
|
+
async function crawl(origin, paths, concurrency, report = undefined, pace = undefined) {
|
|
278
|
+
const { brand } = getConfig();
|
|
279
|
+
const cacheHeader = brand.cacheHeader.toLowerCase();
|
|
280
|
+
|
|
281
|
+
let index = 0;
|
|
282
|
+
let ok = 0;
|
|
283
|
+
let failed = 0;
|
|
284
|
+
/** @type {{ path: string, status: number }[]} */
|
|
285
|
+
const failures = [];
|
|
286
|
+
|
|
287
|
+
async function worker() {
|
|
288
|
+
while (index < paths.length) {
|
|
289
|
+
const target = paths[index];
|
|
290
|
+
index += 1;
|
|
291
|
+
|
|
292
|
+
if (pace) await pace();
|
|
293
|
+
|
|
294
|
+
const startedAt = Date.now();
|
|
295
|
+
|
|
296
|
+
try {
|
|
297
|
+
const response = await fetch(`${origin}${target}`, {
|
|
298
|
+
headers: {
|
|
299
|
+
// Sıkıştırılmış gövde de cache'lensin.
|
|
300
|
+
"accept-encoding": "br, gzip",
|
|
301
|
+
"user-agent": brand.prewarmUserAgent,
|
|
302
|
+
...devGateHeader(),
|
|
303
|
+
},
|
|
304
|
+
});
|
|
305
|
+
// Gövde okunmadan bağlantı açık kalır.
|
|
306
|
+
const body = await response.arrayBuffer();
|
|
307
|
+
if (response.ok) ok += 1;
|
|
308
|
+
else {
|
|
309
|
+
failed += 1;
|
|
310
|
+
failures.push({ path: target, status: response.status });
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
prewarmProgress.entries.push({
|
|
314
|
+
path: target,
|
|
315
|
+
status: response.status,
|
|
316
|
+
ms: Date.now() - startedAt,
|
|
317
|
+
bytes: body.byteLength,
|
|
318
|
+
cache: response.headers.get(cacheHeader),
|
|
319
|
+
error: response.ok ? null : `HTTP ${response.status}`,
|
|
320
|
+
});
|
|
321
|
+
} catch (error) {
|
|
322
|
+
failed += 1;
|
|
323
|
+
// Yanıt hiç gelmedi: ağ hatası her zaman geçici, tekrar denenir.
|
|
324
|
+
failures.push({ path: target, status: 0 });
|
|
325
|
+
prewarmProgress.entries.push({
|
|
326
|
+
path: target,
|
|
327
|
+
status: 0,
|
|
328
|
+
ms: Date.now() - startedAt,
|
|
329
|
+
bytes: 0,
|
|
330
|
+
cache: null,
|
|
331
|
+
error: error instanceof Error ? error.message : String(error),
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (report) {
|
|
336
|
+
report(ok, failed);
|
|
337
|
+
} else {
|
|
338
|
+
prewarmProgress.done = ok + failed;
|
|
339
|
+
prewarmProgress.ok = ok;
|
|
340
|
+
prewarmProgress.failed = failed;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
await Promise.all(
|
|
346
|
+
Array.from({ length: Math.min(concurrency, paths.length) }, worker),
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
return { ok, failed, failures };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Tekrar turuna girecek yollar. Yalnızca geçici hatalar: `400`/`403`/`404` gibi
|
|
354
|
+
* deterministik cevaplar tekrar denemekle düzelmez ve o çağrılar kotadan
|
|
355
|
+
* karşılıksız yiyor. Sınıflandırma render tarafıyla aynı listeden
|
|
356
|
+
* (`isTransientStatus`), yoksa iki yer birbirinden kayar.
|
|
357
|
+
*
|
|
358
|
+
* @param {{ path: string, status: number }[]} failures
|
|
359
|
+
* @returns {string[]}
|
|
360
|
+
*/
|
|
361
|
+
function retryablePaths(failures) {
|
|
362
|
+
return failures
|
|
363
|
+
.filter((failure) => isTransientStatus(failure.status))
|
|
364
|
+
.map((failure) => failure.path);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Tekrar turundan önce beklenecek süre.
|
|
369
|
+
*
|
|
370
|
+
* Sabit bekleme yanlış soruyu cevaplıyordu: doğru süreyi upstream biliyor.
|
|
371
|
+
* Hız freni açıkken `Retry-After` ya da devre kesicinin soğuma süresi zaten
|
|
372
|
+
* elimizde; 10 saniye kapalı kalacak bir kesiciden 2 saniye sonra tekrar
|
|
373
|
+
* denemek, aynı 429'u peşin peşin almak demek.
|
|
374
|
+
*
|
|
375
|
+
* @returns {number} ms
|
|
376
|
+
*/
|
|
377
|
+
function retryDelayMs() {
|
|
378
|
+
const configured = setting("PREWARM_RETRY_DELAY_MS", "retryDelayMs", 2000);
|
|
379
|
+
const cooldown = upstreamCooldownMs();
|
|
380
|
+
|
|
381
|
+
// Fren kapalıysa `cooldown` 0 olur ve davranış eskisi gibi kalır. Üst sınır
|
|
382
|
+
// turun sonsuza kadar açık kalmasını engelliyor.
|
|
383
|
+
return Math.min(Math.max(configured, cooldown), 60_000);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Turun veri önbelleği üzerinden upstream'e ne kadar dokunduğunu özetler.
|
|
388
|
+
*
|
|
389
|
+
* @param {ReturnType<typeof getDataCacheStats>} before Tur başındaki sayaçlar.
|
|
390
|
+
* @returns {string} Okunacak bir şey yoksa boş dize.
|
|
391
|
+
*/
|
|
392
|
+
function upstreamUsage(before) {
|
|
393
|
+
const after = getDataCacheStats();
|
|
394
|
+
const reads = after.reads - before.reads;
|
|
395
|
+
if (reads <= 0) return "";
|
|
396
|
+
|
|
397
|
+
const produced = after.produced - before.produced;
|
|
398
|
+
const coalesced = after.coalesced - before.coalesced;
|
|
399
|
+
const shared = after.shared - before.shared;
|
|
400
|
+
const served = reads - produced;
|
|
401
|
+
const ratio = Math.round((served / reads) * 100);
|
|
402
|
+
|
|
403
|
+
return (
|
|
404
|
+
`${produced} upstream call${produced === 1 ? "" : "s"} for ${reads} data read${reads === 1 ? "" : "s"} ` +
|
|
405
|
+
`(${ratio}% from the data cache` +
|
|
406
|
+
`${coalesced ? `, ${coalesced} coalesced` : ""}` +
|
|
407
|
+
`${shared ? `, ${shared} from the shared tier` : ""})`
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* @param {{ origin: string, quiet?: boolean, paths?: string[] }} options
|
|
413
|
+
* `paths` verilirse hook çağrılmaz, yalnızca o yollar ısıtılır (dev
|
|
414
|
+
* panelindeki "tekrar dene" bunu kullanır).
|
|
415
|
+
* @returns {Promise<{ ok: number, failed: number, total: number, elapsed: number }>}
|
|
416
|
+
*/
|
|
417
|
+
export async function prewarm({ origin, quiet = false, paths: only }) {
|
|
418
|
+
const started = Date.now();
|
|
419
|
+
const limit = setting("PREWARM_MAX", "max", 400);
|
|
420
|
+
const isDev = process.env.NODE_ENV === "development";
|
|
421
|
+
|
|
422
|
+
// Dev'de tek işçi: tarama, o an tarayıcıda açtığın sayfanın render'ıyla CPU
|
|
423
|
+
// için yarışmasın.
|
|
424
|
+
const concurrency = setting("PREWARM_CONCURRENCY", "concurrency", isDev ? 1 : 4);
|
|
425
|
+
|
|
426
|
+
// Render tek bir olay döngüsünde çalışıyor: aralıksız bir tur, geliştirme
|
|
427
|
+
// sırasında sayfa isteklerini ve dev panelinin kanalını arkasında bekletiyor.
|
|
428
|
+
// Dev'de varsayılan bir hız freni bu yüzden var; üretimde ısıtma bir kez
|
|
429
|
+
// olup bittiği için fren yalnızca istenirse (`prewarm.rps`) devreye girer.
|
|
430
|
+
const rps = num(process.env.PREWARM_RPS, num(getConfig().prewarm?.rps, isDev ? 4 : 0));
|
|
431
|
+
const pace = createPacer(rps);
|
|
432
|
+
|
|
433
|
+
const all = only?.length ? only : await collectPaths();
|
|
434
|
+
// Elle verilen liste budanmaz ve sıralanmaz: çağıran tam olarak neyi
|
|
435
|
+
// istediğini biliyor (dev panelindeki "tekrar dene" bunu kullanır).
|
|
436
|
+
const selected = only?.length
|
|
437
|
+
? all
|
|
438
|
+
: selectPrewarmPaths(all, limit, getConfig().prewarm?.rotate !== false);
|
|
439
|
+
|
|
440
|
+
// Invalidate edilmiş sayfalar kuyruğun önüne geçer: "içerik güncellendi"
|
|
441
|
+
// bilgisi geldiğinde sayfa, ziyaretçi gelmesini beklemeden tazelenir. Bu
|
|
442
|
+
// yollar `max` bütçesinin dışında tutulur — sayıları zaten gerçekleşen
|
|
443
|
+
// invalidation kadar ve rotasyonun sırasını bozmaları istenmez.
|
|
444
|
+
const pending = only?.length ? [] : takeInvalidatedPaths();
|
|
445
|
+
const paths = pending.length
|
|
446
|
+
? [...new Set([...pending, ...selected])]
|
|
447
|
+
: selected;
|
|
448
|
+
|
|
449
|
+
Object.assign(prewarmProgress, {
|
|
450
|
+
active: true,
|
|
451
|
+
done: 0,
|
|
452
|
+
total: paths.length,
|
|
453
|
+
ok: 0,
|
|
454
|
+
failed: 0,
|
|
455
|
+
startedAt: started,
|
|
456
|
+
finishedAt: null,
|
|
457
|
+
entries: [],
|
|
458
|
+
});
|
|
459
|
+
suppressed.clear();
|
|
460
|
+
|
|
461
|
+
// Kotanın gerçekten ne kadar harcandığı ancak veri önbelleğinden görülür:
|
|
462
|
+
// 400 sayfalık bir tur, ortak bir uç için tek çağrı da yapabilir dört yüz de.
|
|
463
|
+
const dataBefore = getDataCacheStats();
|
|
464
|
+
|
|
465
|
+
let ok = 0;
|
|
466
|
+
let failed = 0;
|
|
467
|
+
let recovered = 0;
|
|
468
|
+
let skippedRetry = 0;
|
|
469
|
+
try {
|
|
470
|
+
/** @type {{ path: string, status: number }[]} */
|
|
471
|
+
let failures;
|
|
472
|
+
({ ok, failed, failures } = await crawl(
|
|
473
|
+
origin,
|
|
474
|
+
paths,
|
|
475
|
+
concurrency,
|
|
476
|
+
undefined,
|
|
477
|
+
pace,
|
|
478
|
+
));
|
|
479
|
+
|
|
480
|
+
// Hatalar çoğunlukla upstream rate limit'i (429): ilk tur yüzlerce sayfayı
|
|
481
|
+
// aynı anda çekerken API'yi zorluyor. Tek seri tekrar turu bu sayfaların
|
|
482
|
+
// önbelleğe girmesini sağlıyor; aksi hâlde ziyaretçi soğuk render'ı öder.
|
|
483
|
+
const retryPaths = retryablePaths(failures);
|
|
484
|
+
skippedRetry = failures.length - retryPaths.length;
|
|
485
|
+
|
|
486
|
+
if (retryPaths.length) {
|
|
487
|
+
const firstOk = ok;
|
|
488
|
+
const firstFailed = failed;
|
|
489
|
+
|
|
490
|
+
await sleep(retryDelayMs());
|
|
491
|
+
|
|
492
|
+
const retry = await crawl(
|
|
493
|
+
origin,
|
|
494
|
+
retryPaths,
|
|
495
|
+
1,
|
|
496
|
+
(retriedOk) => {
|
|
497
|
+
// Tekrar turunda her başarı bir hatayı başarıya çevirir.
|
|
498
|
+
prewarmProgress.ok = firstOk + retriedOk;
|
|
499
|
+
prewarmProgress.failed = firstFailed - retriedOk;
|
|
500
|
+
},
|
|
501
|
+
pace,
|
|
502
|
+
);
|
|
503
|
+
recovered = retry.ok;
|
|
504
|
+
ok += retry.ok;
|
|
505
|
+
failed -= retry.ok;
|
|
506
|
+
}
|
|
507
|
+
} finally {
|
|
508
|
+
prewarmProgress.active = false;
|
|
509
|
+
prewarmProgress.finishedAt = Date.now();
|
|
510
|
+
}
|
|
511
|
+
const elapsed = Date.now() - started;
|
|
512
|
+
|
|
513
|
+
if (!quiet && paths.length) {
|
|
514
|
+
const skipped = all.length - selected.length;
|
|
515
|
+
// Rotasyon açıkken sınırın dışında kalan yollar kaybolmuyor, bir sonraki
|
|
516
|
+
// tura kalıyor; log bunu ayırt etmeli, yoksa "400 yol atlandı" satırı
|
|
517
|
+
// hatalı bir kurulum sanılıyor.
|
|
518
|
+
const rotate = !only?.length && getConfig().prewarm?.rotate !== false;
|
|
519
|
+
console.log(
|
|
520
|
+
`[prewarm] warmed ${ok}/${paths.length} pages` +
|
|
521
|
+
`${pending.length ? `, ${pending.length} invalidated` : ""}` +
|
|
522
|
+
`${failed ? `, ${failed} failed` : ""}` +
|
|
523
|
+
`${recovered ? `, ${recovered} recovered on the retry pass` : ""}` +
|
|
524
|
+
`${skippedRetry ? `, ${skippedRetry} not retried (permanent)` : ""}` +
|
|
525
|
+
`${skipped > 0 ? `, ${skipped} ${rotate ? "deferred to the next pass" : "over the limit"}` : ""}` +
|
|
526
|
+
` (${(elapsed / 1000).toFixed(1)}s)`,
|
|
527
|
+
);
|
|
528
|
+
|
|
529
|
+
// Turun upstream'e ne kadar dokunduğu. Asıl karar bu satıra bakılarak
|
|
530
|
+
// veriliyor: oran düşükse çözüm hız freni değil, `withDataCache` TTL'ini
|
|
531
|
+
// tur aralığından uzun tutmak — fren çağrıları yavaşlatır, sayısını
|
|
532
|
+
// azaltmaz.
|
|
533
|
+
const upstreamCalls = upstreamUsage(dataBefore);
|
|
534
|
+
if (upstreamCalls) console.log(`[prewarm] ${upstreamCalls}`);
|
|
535
|
+
|
|
536
|
+
// Tur boyunca bastırılan hatalar: en sık görülenler önce, liste uzarsa
|
|
537
|
+
// kalanı tek satırda toplanır. Amaç, logu şişirmeden "ne bozuldu"yu
|
|
538
|
+
// görünür tutmak.
|
|
539
|
+
if (suppressed.size) {
|
|
540
|
+
const ranked = [...suppressed.entries()].sort((a, b) => b[1] - a[1]);
|
|
541
|
+
const shown = ranked.slice(0, 5);
|
|
542
|
+
const rest = ranked.slice(shown.length).reduce((sum, [, n]) => sum + n, 0);
|
|
543
|
+
const total = ranked.reduce((sum, [, n]) => sum + n, 0);
|
|
544
|
+
|
|
545
|
+
console.warn(
|
|
546
|
+
`[prewarm] ${total} problem${total === 1 ? " was" : "s were"} not logged individually:\n` +
|
|
547
|
+
shown.map(([message, n]) => ` ${n}× ${message}`).join("\n") +
|
|
548
|
+
(rest ? `\n … ${rest} more in ${ranked.length - shown.length} other kinds` : ""),
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
return { ok, failed, total: paths.length, elapsed };
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Açılışta ısıtmayı tetikler. `listen` geri çağrısından çağrılır; isteğe
|
|
558
|
+
* bağlı olarak periyodik tekrarlar. Hiçbir hata süreci düşürmez.
|
|
559
|
+
*
|
|
560
|
+
* @param {{ port: number }} options
|
|
561
|
+
* @returns {void}
|
|
562
|
+
*/
|
|
563
|
+
export function startPrewarm({ port }) {
|
|
564
|
+
const config = getConfig();
|
|
565
|
+
if (process.env.PREWARM === "0") return;
|
|
566
|
+
if (process.env.PREWARM !== "1" && config.prewarm?.enabled === false) return;
|
|
567
|
+
// Isıtacak yol bildirmeyen bir projede zamanlayıcı kurmanın anlamı yok.
|
|
568
|
+
if (typeof config.hooks?.prewarmPaths !== "function") return;
|
|
569
|
+
|
|
570
|
+
const isDev = process.env.NODE_ENV === "development";
|
|
571
|
+
const origin = `http://127.0.0.1:${port}`;
|
|
572
|
+
|
|
573
|
+
// Hız frenli bir tur `intervalSeconds`'tan uzun sürebilir; üst üste binen
|
|
574
|
+
// turlar `prewarmProgress`'i bozar ve upstream'e iki kat yük bindirir.
|
|
575
|
+
let running = false;
|
|
576
|
+
const run = async () => {
|
|
577
|
+
if (running) return;
|
|
578
|
+
running = true;
|
|
579
|
+
try {
|
|
580
|
+
await prewarm({ origin });
|
|
581
|
+
} catch (error) {
|
|
582
|
+
console.error("[prewarm] failed", error);
|
|
583
|
+
} finally {
|
|
584
|
+
running = false;
|
|
585
|
+
}
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
// Isıtma ilk isteklerle yarışmasın diye gecikmeyle başlar. Dev'de gecikme
|
|
589
|
+
// daha uzun: dosya kaydı süreci yeniden başlattığı için zamanlayıcı da
|
|
590
|
+
// ölür; yalnızca sunucu bir süre sakin kalınca ısınır.
|
|
591
|
+
const delay = setting("PREWARM_DELAY_MS", "delayMs", isDev ? 3000 : 500);
|
|
592
|
+
setTimeout(() => void run(), delay).unref();
|
|
593
|
+
|
|
594
|
+
// Girdiler `revalidate` ile yaşlanır; stale-while-revalidate sayesinde
|
|
595
|
+
// ziyaretçi beklemez. Periyodik tur, hiç ziyaret edilmeyen sayfaları da
|
|
596
|
+
// sıcak tutmak isteyen kurulumlar için opsiyoneldir.
|
|
597
|
+
// `rotate` ile birlikte bu ayar "damla damla ısıtma"ya dönüşür: her tur
|
|
598
|
+
// kuyruğun bir dilimini alır, yeterli tur sonunda liste baştan sona ısınır.
|
|
599
|
+
const interval = setting("PREWARM_INTERVAL_SECONDS", "intervalSeconds", 0);
|
|
600
|
+
if (interval > 0) setInterval(() => void run(), interval * 1000).unref();
|
|
601
|
+
}
|