jskelet 0.2.4 → 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.
Files changed (91) hide show
  1. package/AGENTS.md +132 -132
  2. package/CHANGELOG.md +8 -0
  3. package/LICENSE +21 -21
  4. package/bin/jskelet.mjs +103 -103
  5. package/docs/01-baslangic.md +285 -285
  6. package/docs/02-mimari.md +287 -287
  7. package/docs/03-routing.md +480 -480
  8. package/docs/04-render-ve-sablonlar.md +490 -490
  9. package/docs/05-islands.md +482 -482
  10. package/docs/06-cache.md +1209 -1209
  11. package/docs/08-build.md +366 -366
  12. package/docs/09-dev-araclari.md +335 -335
  13. package/docs/10-dagitim.md +329 -329
  14. package/docs/12-panel-ve-oturum.md +384 -384
  15. package/docs/README.md +105 -105
  16. package/docs/en/01-getting-started.md +292 -292
  17. package/docs/en/02-architecture.md +305 -305
  18. package/docs/en/03-routing.md +497 -497
  19. package/docs/en/04-rendering.md +504 -504
  20. package/docs/en/05-islands.md +492 -492
  21. package/docs/en/06-caching.md +1239 -1239
  22. package/docs/en/07-configuration.md +986 -986
  23. package/docs/en/08-build.md +383 -383
  24. package/docs/en/09-dev-tools.md +342 -342
  25. package/docs/en/10-deployment.md +332 -332
  26. package/docs/en/11-migration.md +359 -359
  27. package/docs/en/12-dashboards-and-sessions.md +392 -392
  28. package/docs/en/README.md +112 -112
  29. package/package.json +102 -102
  30. package/src/build/ensure-build.mjs +15 -15
  31. package/src/build/paths.mjs +143 -143
  32. package/src/build/resolve-peer.mjs +36 -36
  33. package/src/build/tasks/client.mjs +268 -268
  34. package/src/build/tasks/css.mjs +124 -124
  35. package/src/build/tasks/fonts.mjs +146 -146
  36. package/src/build/tasks/icons.mjs +224 -224
  37. package/src/build/tasks/images.mjs +244 -244
  38. package/src/build/tasks/precompress.mjs +78 -78
  39. package/src/client/cache-panel/i18n.js +670 -670
  40. package/src/client/cache-panel/login.html +74 -74
  41. package/src/client/cache-panel/panel.css +756 -756
  42. package/src/client/cache-panel/panel.html +308 -308
  43. package/src/client/cache-panel/panel.js +915 -915
  44. package/src/client/devtools/report.html +185 -185
  45. package/src/client/devtools/report.js +725 -725
  46. package/src/client/dom.js +95 -95
  47. package/src/client/form.js +192 -192
  48. package/src/client/index.js +35 -35
  49. package/src/client/registry.js +297 -297
  50. package/src/client/safe-image.js +91 -91
  51. package/src/client/store.js +36 -36
  52. package/src/client/swap.js +188 -188
  53. package/src/config/pattern.js +107 -107
  54. package/src/http/control-flow.js +71 -71
  55. package/src/http/cookies.js +257 -257
  56. package/src/http/request-cache.js +46 -46
  57. package/src/http/request-context.js +162 -162
  58. package/src/index.js +83 -83
  59. package/src/init.mjs +221 -221
  60. package/src/runtime/alias-hooks.mjs +119 -119
  61. package/src/runtime/register.mjs +4 -4
  62. package/src/server/assets.js +147 -147
  63. package/src/server/cache-deps.js +42 -42
  64. package/src/server/cache-panel.js +759 -759
  65. package/src/server/cloudflare.js +607 -595
  66. package/src/server/create-app.js +291 -291
  67. package/src/server/data-cache.js +462 -462
  68. package/src/server/dev/report.js +369 -369
  69. package/src/server/dev/socket.js +170 -170
  70. package/src/server/dev/version-check.mjs +139 -139
  71. package/src/server/html-cache.js +817 -817
  72. package/src/server/metadata.js +102 -102
  73. package/src/server/middleware/compression.js +205 -205
  74. package/src/server/middleware/csrf.js +134 -134
  75. package/src/server/middleware/dev-gate.js +62 -62
  76. package/src/server/middleware/headers.js +37 -37
  77. package/src/server/middleware/redirects.js +32 -32
  78. package/src/server/middleware/static-precompressed.js +100 -100
  79. package/src/server/middleware/upstream-proxy.js +141 -141
  80. package/src/server/prewarm.js +601 -601
  81. package/src/server/redis.js +569 -569
  82. package/src/server/router.js +128 -128
  83. package/src/server/status-page.js +164 -164
  84. package/src/server/upstream-limiter.js +376 -376
  85. package/src/server/upstream-tracking.js +166 -166
  86. package/src/start.mjs +7 -7
  87. package/src/templates/layout.ejs +44 -44
  88. package/src/version.mjs +31 -31
  89. package/src/views/components/loader.js +85 -85
  90. package/src/views/helpers/html.js +102 -102
  91. package/src/views/helpers/tags.js +245 -245
@@ -1,376 +1,376 @@
1
- /**
2
- * Upstream API'ye giden isteklerin host başına hız freni.
3
- *
4
- * Isıtma turundaki `rps` ayarı bu işi yapamıyordu: o, kendi sunucumuza atılan
5
- * **sayfa** isteklerini sayıyor. Bir sayfa render'ı bir API çağrısı da yapabilir
6
- * yirmi tane de; kotayı bağlayan şey sayfa sayısı değil, çağrı sayısı. Bu
7
- * yüzden fren `trackUpstreamFetch()` sarmalayıcısına, yani gerçek çağrının
8
- * geçtiği yere konuyor — ısıtma da, gerçek trafik de aynı bütçeden harcar.
9
- *
10
- * Üç mekanizma birlikte çalışıyor, çünkü üç farklı şeyi sınırlıyorlar:
11
- *
12
- * token bucket → ortalama hız (saniyedeki çağrı)
13
- * concurrency → anlık baskı (aynı anda uçan çağrı)
14
- * AIMD → doğru hızın ne olduğu
15
- *
16
- * Sabit bir hız her zaman ya çok yavaş ya çok hızlıdır: kotanın gerçek sınırını
17
- * kimse config'e doğru yazamaz, üstelik gün içinde değişir. 429 geldiğinde hızı
18
- * yarıya indirip (çarpımsal azalma) temiz geçen her pencerede bir adım geri
19
- * çıkmak (toplamsal artış), sınırı elle ayar yapılmadan bulur.
20
- *
21
- * Devre kesici de aynı gerekçeyle var: 429 geçici sayıldığı için o çağrıyla
22
- * üretilen HTML önbelleğe **yazılmıyor**. Yani rate limit'e girmiş bir turda
23
- * kota harcanır ve karşılığında hiçbir şey saklanmaz. Art arda 429 alan bir
24
- * host'a bir süre hiç gitmemek, o boşa yanmayı kesiyor.
25
- *
26
- * Varsayılan **kapalı**: `rate` verilmedikçe hiçbir istek beklemez. Framework
27
- * mevcut kurulumların davranışını sessizce değiştirmez.
28
- */
29
- import { DEFAULT_UPSTREAM_LIMIT } from "../config/defaults.js";
30
-
31
- /**
32
- * @typedef {object} HostState
33
- * @property {string} host
34
- * @property {number} maxRate Config'te verilen tavan; AIMD bunun üstüne çıkmaz.
35
- * @property {number} minRate Azalmanın dibi; 0'a inip tamamen kilitlenmesin.
36
- * @property {number} rate Saniyedeki izin — AIMD bunu oynatır.
37
- * @property {number} burst Kovanın boyu: kısa patlamalara verilen tolerans.
38
- * @property {number} concurrency Aynı anda uçabilecek çağrı sayısı.
39
- * @property {number} tokens
40
- * @property {number} refilledAt
41
- * @property {number} active Uçuştaki çağrı.
42
- * @property {(() => void)[]} waiters Boş yuva bekleyenler.
43
- * @property {Promise<void>} chain Admission sırası (FIFO).
44
- * @property {number} blockedUntil `Retry-After` boyunca kova tamamen durur.
45
- * @property {number} consecutiveFailures
46
- * @property {number} bypassUntil Devre kesicinin açık kaldığı an.
47
- * @property {number} adjustedAt Son AIMD kararının zamanı.
48
- * @property {number} throttled Kaç kez 429/503 görüldü (teşhis için).
49
- * @property {number} rejected Devre kesici kaç çağrıyı hiç göndermedi.
50
- */
51
-
52
- /** @type {Map<string, HostState>} */
53
- const hosts = new Map();
54
-
55
- /** @type {typeof DEFAULT_UPSTREAM_LIMIT} */
56
- let settings = { ...DEFAULT_UPSTREAM_LIMIT };
57
-
58
- /**
59
- * `createApp()` config yüklendikten sonra bir kez çağırır. Ayar değiştiğinde
60
- * host durumları sıfırlanır: eski `maxRate`'e göre ayarlanmış bir `rate`
61
- * yeni tavanın üstünde kalabilir.
62
- *
63
- * @param {Record<string, unknown> | undefined} config `cache().upstream`
64
- * @returns {void}
65
- */
66
- export function configureUpstreamLimiter(config) {
67
- settings = { ...DEFAULT_UPSTREAM_LIMIT, ...(config ?? {}) };
68
- hosts.clear();
69
- }
70
-
71
- /** @returns {boolean} */
72
- function enabled() {
73
- return settings.rate > 0 || hostOverrides().length > 0;
74
- }
75
-
76
- /** @returns {[string, Record<string, number>][]} */
77
- function hostOverrides() {
78
- const raw = /** @type {Record<string, any>} */ (settings.hosts ?? {});
79
- return Object.entries(raw);
80
- }
81
-
82
- /**
83
- * @param {string} url
84
- * @returns {string | null} Host, ya da URL çözülemediyse `null`.
85
- */
86
- function hostOf(url) {
87
- try {
88
- return new URL(url).host;
89
- } catch {
90
- return null;
91
- }
92
- }
93
-
94
- /**
95
- * @param {string} host
96
- * @returns {HostState}
97
- */
98
- function stateFor(host) {
99
- const existing = hosts.get(host);
100
- if (existing) return existing;
101
-
102
- const override = /** @type {Record<string, any>} */ (settings.hosts ?? {})[host] ?? {};
103
- const merged = { ...settings, ...override };
104
- const rate = num(merged.rate, 0);
105
-
106
- /** @type {HostState} */
107
- const created = {
108
- host,
109
- maxRate: rate,
110
- minRate: Math.min(num(merged.minRate, DEFAULT_UPSTREAM_LIMIT.minRate), rate || 1),
111
- rate: rate,
112
- // Kova boyu verilmezse bir saniyelik bütçe: hız 4/s ise dört çağrılık bir
113
- // patlama tolere edilir, beşincisi bekler.
114
- burst: num(merged.burst, 0) || Math.max(1, Math.ceil(rate)),
115
- concurrency: Math.floor(num(merged.concurrency, DEFAULT_UPSTREAM_LIMIT.concurrency)),
116
- tokens: num(merged.burst, 0) || Math.max(1, Math.ceil(rate)),
117
- refilledAt: Date.now(),
118
- active: 0,
119
- waiters: [],
120
- chain: Promise.resolve(),
121
- blockedUntil: 0,
122
- consecutiveFailures: 0,
123
- bypassUntil: 0,
124
- // 0, `Date.now()` değil: ilk saniye içinde gelen bir 429 de hızı
125
- // düşürmeli. Aksi hâlde ısıtma turunun ilk patlaması cezasız kalıyor.
126
- adjustedAt: 0,
127
- throttled: 0,
128
- rejected: 0,
129
- };
130
-
131
- hosts.set(host, created);
132
- return created;
133
- }
134
-
135
- /** @param {unknown} value @param {number} fallback */
136
- function num(value, fallback) {
137
- const parsed = Number(value);
138
- return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
139
- }
140
-
141
- /** @param {number} ms */
142
- function sleep(ms) {
143
- return new Promise((resolve) => {
144
- setTimeout(resolve, ms).unref?.();
145
- });
146
- }
147
-
148
- /**
149
- * Geçen süreye göre kovayı doldurur ve hata görülmeyen pencereler için hızı
150
- * bir adım yukarı çeker. İkisi aynı yerde: her ikisi de "zaman geçti" bilgisine
151
- * dayanıyor ve zamanlayıcı kurmadan, çağrı anında hesaplanıyorlar — boşta duran
152
- * bir süreç için sayaç işletmenin anlamı yok.
153
- *
154
- * @param {HostState} state
155
- * @returns {void}
156
- */
157
- function refill(state) {
158
- const now = Date.now();
159
- const elapsed = now - state.refilledAt;
160
-
161
- if (elapsed > 0) {
162
- state.tokens = Math.min(state.burst, state.tokens + (elapsed * state.rate) / 1000);
163
- state.refilledAt = now;
164
- }
165
-
166
- if (state.rate >= state.maxRate) return;
167
- if (now - state.adjustedAt < settings.increaseIntervalMs) return;
168
-
169
- // Toplamsal artış: azalma yarıya indiriyor, geri çıkış adım adım. Ters
170
- // olsaydı (hızlı çık, yavaş in) her pencerede yeniden 429 yerdik.
171
- state.rate = Math.min(state.maxRate, state.rate + num(settings.increaseStep, 1));
172
- state.adjustedAt = now;
173
- }
174
-
175
- /**
176
- * @param {HostState} state
177
- * @returns {number} Kaç ms sonra tekrar denenmeli; 0 → yuva hazır.
178
- */
179
- function waitFor(state) {
180
- const now = Date.now();
181
- if (state.blockedUntil > now) return state.blockedUntil - now;
182
-
183
- refill(state);
184
- if (state.tokens >= 1) return 0;
185
-
186
- // Bir tokenlik eksiğin dolması için gereken süre. `rate` düşükken bu
187
- // saniyeler olabilir; beklemek doğru davranış — alternatifi 429.
188
- return Math.max(10, Math.ceil(((1 - state.tokens) / state.rate) * 1000));
189
- }
190
-
191
- /**
192
- * Boş bir eşzamanlılık yuvası bekler.
193
- *
194
- * @param {HostState} state
195
- * @returns {Promise<void>}
196
- */
197
- function waitForSlot(state) {
198
- if (state.active < state.concurrency) return Promise.resolve();
199
- return new Promise((resolve) => state.waiters.push(resolve));
200
- }
201
-
202
- /**
203
- * Çağrı için izin alır. `null` dönerse fren kapalı ya da host çözülemedi;
204
- * `blocked` dönerse devre kesici açık ve çağrı hiç yapılmamalı.
205
- *
206
- * @param {string} url
207
- * @returns {Promise<{ blocked: boolean, host: string, release: () => void } | null>}
208
- */
209
- export async function limitUpstream(url) {
210
- if (!enabled()) return null;
211
-
212
- const host = hostOf(url);
213
- if (!host) return null;
214
-
215
- const state = stateFor(host);
216
- if (!state.maxRate) return null;
217
-
218
- if (state.bypassUntil > Date.now()) {
219
- state.rejected += 1;
220
- return { blocked: true, host, release: () => {} };
221
- }
222
-
223
- // Admission FIFO: her çağrı kendinden öncekinin izin almasını bekler. Sıra
224
- // olmadan, uyanan çağrılar rastgele yarışır ve ilk gelen en son geçebilir.
225
- const previous = state.chain;
226
- /** @type {() => void} */
227
- let releaseChain = () => {};
228
- state.chain = new Promise((resolve) => {
229
- releaseChain = resolve;
230
- });
231
-
232
- try {
233
- await previous;
234
-
235
- for (;;) {
236
- await waitForSlot(state);
237
- const wait = waitFor(state);
238
- if (wait === 0) break;
239
- await sleep(wait);
240
- }
241
-
242
- state.tokens -= 1;
243
- state.active += 1;
244
- } finally {
245
- releaseChain();
246
- }
247
-
248
- let released = false;
249
- return {
250
- blocked: false,
251
- host,
252
- release: () => {
253
- if (released) return;
254
- released = true;
255
- state.active -= 1;
256
- state.waiters.shift()?.();
257
- },
258
- };
259
- }
260
-
261
- /**
262
- * Yanıtın hıza etkisini işler. 429/503 hızı yarıya indirir ve `Retry-After`
263
- * varsa kovayı o süre boyunca tamamen durdurur; başarı sayaçları sıfırlar.
264
- *
265
- * @param {string} host
266
- * @param {number} status `0` → ağ hatası (yanıt gelmedi).
267
- * @param {string | null} [retryAfter] `Retry-After` başlığı.
268
- * @returns {void}
269
- */
270
- export function noteUpstreamResponse(host, status, retryAfter = null) {
271
- const state = hosts.get(host);
272
- if (!state) return;
273
-
274
- // Yalnızca "yavaşla" anlamına gelen cevaplar hızı cezalandırır. 400/404 bir
275
- // kota sorunu değil, 500 de öyle: onlar için yavaşlamak arızayı düzeltmez,
276
- // sadece siteyi yavaşlatır.
277
- if (status !== 429 && status !== 503) {
278
- state.consecutiveFailures = 0;
279
- return;
280
- }
281
-
282
- state.throttled += 1;
283
- state.consecutiveFailures += 1;
284
-
285
- const now = Date.now();
286
- const cooldown = retryAfterMs(retryAfter);
287
- if (cooldown) state.blockedUntil = Math.max(state.blockedUntil, now + cooldown);
288
-
289
- // Çarpımsal azalma, ama pencere başına bir kez: aynı anda uçan on çağrı
290
- // hep 429 dönerse hız on kez yarılanıp dibe vururdu.
291
- if (now - state.adjustedAt >= settings.decreaseIntervalMs) {
292
- state.rate = Math.max(state.minRate, state.rate / 2);
293
- state.adjustedAt = now;
294
- }
295
-
296
- if (
297
- state.consecutiveFailures >= settings.breakerFailures &&
298
- state.bypassUntil <= now
299
- ) {
300
- state.bypassUntil = now + settings.breakerCooldownMs;
301
- console.warn(
302
- `[upstream] ${state.host}: ${state.consecutiveFailures} consecutive rate limits — ` +
303
- `bypassing for ${settings.breakerCooldownMs}ms (rate is now ${state.rate.toFixed(1)}/s)`,
304
- );
305
- }
306
- }
307
-
308
- /**
309
- * `Retry-After` iki biçimde gelir: saniye ya da HTTP tarihi.
310
- *
311
- * @param {string | null | undefined} value
312
- * @returns {number} ms; okunamazsa 0.
313
- */
314
- function retryAfterMs(value) {
315
- if (!value) return 0;
316
-
317
- const seconds = Number(value);
318
- if (Number.isFinite(seconds) && seconds >= 0) return Math.min(seconds * 1000, 300_000);
319
-
320
- const date = Date.parse(value);
321
- if (Number.isNaN(date)) return 0;
322
-
323
- return Math.min(Math.max(0, date - Date.now()), 300_000);
324
- }
325
-
326
- /**
327
- * Dev paneli ve teşhis için host başına durum. Fren kapalıysa boş dizi.
328
- *
329
- * @returns {{ host: string, rate: number, maxRate: number, concurrency: number,
330
- * active: number, throttled: number, rejected: number, bypassed: boolean,
331
- * blockedMs: number, bypassedMs: number }[]}
332
- */
333
- export function getUpstreamLimiterStatus() {
334
- const now = Date.now();
335
-
336
- return [...hosts.values()].map((state) => ({
337
- host: state.host,
338
- rate: Number(state.rate.toFixed(2)),
339
- maxRate: state.maxRate,
340
- concurrency: state.concurrency,
341
- active: state.active,
342
- throttled: state.throttled,
343
- rejected: state.rejected,
344
- bypassed: state.bypassUntil > now,
345
- blockedMs: Math.max(0, state.blockedUntil - now),
346
- bypassedMs: Math.max(0, state.bypassUntil - now),
347
- }));
348
- }
349
-
350
- /**
351
- * Freni bekleten en uzun süre. Isıtma turunun tekrar denemesi bunu kullanıyor:
352
- * sabit bir bekleme, kesici 10 saniye açıkken 2 saniye sonra tekrar denemek
353
- * demekti — yani aynı 429'u peşin peşin almak.
354
- *
355
- * @returns {number} ms; fren kapalıysa ya da bekleyen bir şey yoksa 0.
356
- */
357
- export function upstreamCooldownMs() {
358
- const now = Date.now();
359
- let longest = 0;
360
-
361
- for (const state of hosts.values()) {
362
- longest = Math.max(longest, state.bypassUntil - now, state.blockedUntil - now);
363
- }
364
-
365
- return Math.max(0, longest);
366
- }
367
-
368
- /**
369
- * Testler için: ayarları verip tüm host durumlarını sıfırlar.
370
- *
371
- * @param {Record<string, unknown>} [config]
372
- * @returns {void}
373
- */
374
- export function resetUpstreamLimiterForTests(config = {}) {
375
- configureUpstreamLimiter(config);
376
- }
1
+ /**
2
+ * Upstream API'ye giden isteklerin host başına hız freni.
3
+ *
4
+ * Isıtma turundaki `rps` ayarı bu işi yapamıyordu: o, kendi sunucumuza atılan
5
+ * **sayfa** isteklerini sayıyor. Bir sayfa render'ı bir API çağrısı da yapabilir
6
+ * yirmi tane de; kotayı bağlayan şey sayfa sayısı değil, çağrı sayısı. Bu
7
+ * yüzden fren `trackUpstreamFetch()` sarmalayıcısına, yani gerçek çağrının
8
+ * geçtiği yere konuyor — ısıtma da, gerçek trafik de aynı bütçeden harcar.
9
+ *
10
+ * Üç mekanizma birlikte çalışıyor, çünkü üç farklı şeyi sınırlıyorlar:
11
+ *
12
+ * token bucket → ortalama hız (saniyedeki çağrı)
13
+ * concurrency → anlık baskı (aynı anda uçan çağrı)
14
+ * AIMD → doğru hızın ne olduğu
15
+ *
16
+ * Sabit bir hız her zaman ya çok yavaş ya çok hızlıdır: kotanın gerçek sınırını
17
+ * kimse config'e doğru yazamaz, üstelik gün içinde değişir. 429 geldiğinde hızı
18
+ * yarıya indirip (çarpımsal azalma) temiz geçen her pencerede bir adım geri
19
+ * çıkmak (toplamsal artış), sınırı elle ayar yapılmadan bulur.
20
+ *
21
+ * Devre kesici de aynı gerekçeyle var: 429 geçici sayıldığı için o çağrıyla
22
+ * üretilen HTML önbelleğe **yazılmıyor**. Yani rate limit'e girmiş bir turda
23
+ * kota harcanır ve karşılığında hiçbir şey saklanmaz. Art arda 429 alan bir
24
+ * host'a bir süre hiç gitmemek, o boşa yanmayı kesiyor.
25
+ *
26
+ * Varsayılan **kapalı**: `rate` verilmedikçe hiçbir istek beklemez. Framework
27
+ * mevcut kurulumların davranışını sessizce değiştirmez.
28
+ */
29
+ import { DEFAULT_UPSTREAM_LIMIT } from "../config/defaults.js";
30
+
31
+ /**
32
+ * @typedef {object} HostState
33
+ * @property {string} host
34
+ * @property {number} maxRate Config'te verilen tavan; AIMD bunun üstüne çıkmaz.
35
+ * @property {number} minRate Azalmanın dibi; 0'a inip tamamen kilitlenmesin.
36
+ * @property {number} rate Saniyedeki izin — AIMD bunu oynatır.
37
+ * @property {number} burst Kovanın boyu: kısa patlamalara verilen tolerans.
38
+ * @property {number} concurrency Aynı anda uçabilecek çağrı sayısı.
39
+ * @property {number} tokens
40
+ * @property {number} refilledAt
41
+ * @property {number} active Uçuştaki çağrı.
42
+ * @property {(() => void)[]} waiters Boş yuva bekleyenler.
43
+ * @property {Promise<void>} chain Admission sırası (FIFO).
44
+ * @property {number} blockedUntil `Retry-After` boyunca kova tamamen durur.
45
+ * @property {number} consecutiveFailures
46
+ * @property {number} bypassUntil Devre kesicinin açık kaldığı an.
47
+ * @property {number} adjustedAt Son AIMD kararının zamanı.
48
+ * @property {number} throttled Kaç kez 429/503 görüldü (teşhis için).
49
+ * @property {number} rejected Devre kesici kaç çağrıyı hiç göndermedi.
50
+ */
51
+
52
+ /** @type {Map<string, HostState>} */
53
+ const hosts = new Map();
54
+
55
+ /** @type {typeof DEFAULT_UPSTREAM_LIMIT} */
56
+ let settings = { ...DEFAULT_UPSTREAM_LIMIT };
57
+
58
+ /**
59
+ * `createApp()` config yüklendikten sonra bir kez çağırır. Ayar değiştiğinde
60
+ * host durumları sıfırlanır: eski `maxRate`'e göre ayarlanmış bir `rate`
61
+ * yeni tavanın üstünde kalabilir.
62
+ *
63
+ * @param {Record<string, unknown> | undefined} config `cache().upstream`
64
+ * @returns {void}
65
+ */
66
+ export function configureUpstreamLimiter(config) {
67
+ settings = { ...DEFAULT_UPSTREAM_LIMIT, ...(config ?? {}) };
68
+ hosts.clear();
69
+ }
70
+
71
+ /** @returns {boolean} */
72
+ function enabled() {
73
+ return settings.rate > 0 || hostOverrides().length > 0;
74
+ }
75
+
76
+ /** @returns {[string, Record<string, number>][]} */
77
+ function hostOverrides() {
78
+ const raw = /** @type {Record<string, any>} */ (settings.hosts ?? {});
79
+ return Object.entries(raw);
80
+ }
81
+
82
+ /**
83
+ * @param {string} url
84
+ * @returns {string | null} Host, ya da URL çözülemediyse `null`.
85
+ */
86
+ function hostOf(url) {
87
+ try {
88
+ return new URL(url).host;
89
+ } catch {
90
+ return null;
91
+ }
92
+ }
93
+
94
+ /**
95
+ * @param {string} host
96
+ * @returns {HostState}
97
+ */
98
+ function stateFor(host) {
99
+ const existing = hosts.get(host);
100
+ if (existing) return existing;
101
+
102
+ const override = /** @type {Record<string, any>} */ (settings.hosts ?? {})[host] ?? {};
103
+ const merged = { ...settings, ...override };
104
+ const rate = num(merged.rate, 0);
105
+
106
+ /** @type {HostState} */
107
+ const created = {
108
+ host,
109
+ maxRate: rate,
110
+ minRate: Math.min(num(merged.minRate, DEFAULT_UPSTREAM_LIMIT.minRate), rate || 1),
111
+ rate: rate,
112
+ // Kova boyu verilmezse bir saniyelik bütçe: hız 4/s ise dört çağrılık bir
113
+ // patlama tolere edilir, beşincisi bekler.
114
+ burst: num(merged.burst, 0) || Math.max(1, Math.ceil(rate)),
115
+ concurrency: Math.floor(num(merged.concurrency, DEFAULT_UPSTREAM_LIMIT.concurrency)),
116
+ tokens: num(merged.burst, 0) || Math.max(1, Math.ceil(rate)),
117
+ refilledAt: Date.now(),
118
+ active: 0,
119
+ waiters: [],
120
+ chain: Promise.resolve(),
121
+ blockedUntil: 0,
122
+ consecutiveFailures: 0,
123
+ bypassUntil: 0,
124
+ // 0, `Date.now()` değil: ilk saniye içinde gelen bir 429 de hızı
125
+ // düşürmeli. Aksi hâlde ısıtma turunun ilk patlaması cezasız kalıyor.
126
+ adjustedAt: 0,
127
+ throttled: 0,
128
+ rejected: 0,
129
+ };
130
+
131
+ hosts.set(host, created);
132
+ return created;
133
+ }
134
+
135
+ /** @param {unknown} value @param {number} fallback */
136
+ function num(value, fallback) {
137
+ const parsed = Number(value);
138
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
139
+ }
140
+
141
+ /** @param {number} ms */
142
+ function sleep(ms) {
143
+ return new Promise((resolve) => {
144
+ setTimeout(resolve, ms).unref?.();
145
+ });
146
+ }
147
+
148
+ /**
149
+ * Geçen süreye göre kovayı doldurur ve hata görülmeyen pencereler için hızı
150
+ * bir adım yukarı çeker. İkisi aynı yerde: her ikisi de "zaman geçti" bilgisine
151
+ * dayanıyor ve zamanlayıcı kurmadan, çağrı anında hesaplanıyorlar — boşta duran
152
+ * bir süreç için sayaç işletmenin anlamı yok.
153
+ *
154
+ * @param {HostState} state
155
+ * @returns {void}
156
+ */
157
+ function refill(state) {
158
+ const now = Date.now();
159
+ const elapsed = now - state.refilledAt;
160
+
161
+ if (elapsed > 0) {
162
+ state.tokens = Math.min(state.burst, state.tokens + (elapsed * state.rate) / 1000);
163
+ state.refilledAt = now;
164
+ }
165
+
166
+ if (state.rate >= state.maxRate) return;
167
+ if (now - state.adjustedAt < settings.increaseIntervalMs) return;
168
+
169
+ // Toplamsal artış: azalma yarıya indiriyor, geri çıkış adım adım. Ters
170
+ // olsaydı (hızlı çık, yavaş in) her pencerede yeniden 429 yerdik.
171
+ state.rate = Math.min(state.maxRate, state.rate + num(settings.increaseStep, 1));
172
+ state.adjustedAt = now;
173
+ }
174
+
175
+ /**
176
+ * @param {HostState} state
177
+ * @returns {number} Kaç ms sonra tekrar denenmeli; 0 → yuva hazır.
178
+ */
179
+ function waitFor(state) {
180
+ const now = Date.now();
181
+ if (state.blockedUntil > now) return state.blockedUntil - now;
182
+
183
+ refill(state);
184
+ if (state.tokens >= 1) return 0;
185
+
186
+ // Bir tokenlik eksiğin dolması için gereken süre. `rate` düşükken bu
187
+ // saniyeler olabilir; beklemek doğru davranış — alternatifi 429.
188
+ return Math.max(10, Math.ceil(((1 - state.tokens) / state.rate) * 1000));
189
+ }
190
+
191
+ /**
192
+ * Boş bir eşzamanlılık yuvası bekler.
193
+ *
194
+ * @param {HostState} state
195
+ * @returns {Promise<void>}
196
+ */
197
+ function waitForSlot(state) {
198
+ if (state.active < state.concurrency) return Promise.resolve();
199
+ return new Promise((resolve) => state.waiters.push(resolve));
200
+ }
201
+
202
+ /**
203
+ * Çağrı için izin alır. `null` dönerse fren kapalı ya da host çözülemedi;
204
+ * `blocked` dönerse devre kesici açık ve çağrı hiç yapılmamalı.
205
+ *
206
+ * @param {string} url
207
+ * @returns {Promise<{ blocked: boolean, host: string, release: () => void } | null>}
208
+ */
209
+ export async function limitUpstream(url) {
210
+ if (!enabled()) return null;
211
+
212
+ const host = hostOf(url);
213
+ if (!host) return null;
214
+
215
+ const state = stateFor(host);
216
+ if (!state.maxRate) return null;
217
+
218
+ if (state.bypassUntil > Date.now()) {
219
+ state.rejected += 1;
220
+ return { blocked: true, host, release: () => {} };
221
+ }
222
+
223
+ // Admission FIFO: her çağrı kendinden öncekinin izin almasını bekler. Sıra
224
+ // olmadan, uyanan çağrılar rastgele yarışır ve ilk gelen en son geçebilir.
225
+ const previous = state.chain;
226
+ /** @type {() => void} */
227
+ let releaseChain = () => {};
228
+ state.chain = new Promise((resolve) => {
229
+ releaseChain = resolve;
230
+ });
231
+
232
+ try {
233
+ await previous;
234
+
235
+ for (;;) {
236
+ await waitForSlot(state);
237
+ const wait = waitFor(state);
238
+ if (wait === 0) break;
239
+ await sleep(wait);
240
+ }
241
+
242
+ state.tokens -= 1;
243
+ state.active += 1;
244
+ } finally {
245
+ releaseChain();
246
+ }
247
+
248
+ let released = false;
249
+ return {
250
+ blocked: false,
251
+ host,
252
+ release: () => {
253
+ if (released) return;
254
+ released = true;
255
+ state.active -= 1;
256
+ state.waiters.shift()?.();
257
+ },
258
+ };
259
+ }
260
+
261
+ /**
262
+ * Yanıtın hıza etkisini işler. 429/503 hızı yarıya indirir ve `Retry-After`
263
+ * varsa kovayı o süre boyunca tamamen durdurur; başarı sayaçları sıfırlar.
264
+ *
265
+ * @param {string} host
266
+ * @param {number} status `0` → ağ hatası (yanıt gelmedi).
267
+ * @param {string | null} [retryAfter] `Retry-After` başlığı.
268
+ * @returns {void}
269
+ */
270
+ export function noteUpstreamResponse(host, status, retryAfter = null) {
271
+ const state = hosts.get(host);
272
+ if (!state) return;
273
+
274
+ // Yalnızca "yavaşla" anlamına gelen cevaplar hızı cezalandırır. 400/404 bir
275
+ // kota sorunu değil, 500 de öyle: onlar için yavaşlamak arızayı düzeltmez,
276
+ // sadece siteyi yavaşlatır.
277
+ if (status !== 429 && status !== 503) {
278
+ state.consecutiveFailures = 0;
279
+ return;
280
+ }
281
+
282
+ state.throttled += 1;
283
+ state.consecutiveFailures += 1;
284
+
285
+ const now = Date.now();
286
+ const cooldown = retryAfterMs(retryAfter);
287
+ if (cooldown) state.blockedUntil = Math.max(state.blockedUntil, now + cooldown);
288
+
289
+ // Çarpımsal azalma, ama pencere başına bir kez: aynı anda uçan on çağrı
290
+ // hep 429 dönerse hız on kez yarılanıp dibe vururdu.
291
+ if (now - state.adjustedAt >= settings.decreaseIntervalMs) {
292
+ state.rate = Math.max(state.minRate, state.rate / 2);
293
+ state.adjustedAt = now;
294
+ }
295
+
296
+ if (
297
+ state.consecutiveFailures >= settings.breakerFailures &&
298
+ state.bypassUntil <= now
299
+ ) {
300
+ state.bypassUntil = now + settings.breakerCooldownMs;
301
+ console.warn(
302
+ `[upstream] ${state.host}: ${state.consecutiveFailures} consecutive rate limits — ` +
303
+ `bypassing for ${settings.breakerCooldownMs}ms (rate is now ${state.rate.toFixed(1)}/s)`,
304
+ );
305
+ }
306
+ }
307
+
308
+ /**
309
+ * `Retry-After` iki biçimde gelir: saniye ya da HTTP tarihi.
310
+ *
311
+ * @param {string | null | undefined} value
312
+ * @returns {number} ms; okunamazsa 0.
313
+ */
314
+ function retryAfterMs(value) {
315
+ if (!value) return 0;
316
+
317
+ const seconds = Number(value);
318
+ if (Number.isFinite(seconds) && seconds >= 0) return Math.min(seconds * 1000, 300_000);
319
+
320
+ const date = Date.parse(value);
321
+ if (Number.isNaN(date)) return 0;
322
+
323
+ return Math.min(Math.max(0, date - Date.now()), 300_000);
324
+ }
325
+
326
+ /**
327
+ * Dev paneli ve teşhis için host başına durum. Fren kapalıysa boş dizi.
328
+ *
329
+ * @returns {{ host: string, rate: number, maxRate: number, concurrency: number,
330
+ * active: number, throttled: number, rejected: number, bypassed: boolean,
331
+ * blockedMs: number, bypassedMs: number }[]}
332
+ */
333
+ export function getUpstreamLimiterStatus() {
334
+ const now = Date.now();
335
+
336
+ return [...hosts.values()].map((state) => ({
337
+ host: state.host,
338
+ rate: Number(state.rate.toFixed(2)),
339
+ maxRate: state.maxRate,
340
+ concurrency: state.concurrency,
341
+ active: state.active,
342
+ throttled: state.throttled,
343
+ rejected: state.rejected,
344
+ bypassed: state.bypassUntil > now,
345
+ blockedMs: Math.max(0, state.blockedUntil - now),
346
+ bypassedMs: Math.max(0, state.bypassUntil - now),
347
+ }));
348
+ }
349
+
350
+ /**
351
+ * Freni bekleten en uzun süre. Isıtma turunun tekrar denemesi bunu kullanıyor:
352
+ * sabit bir bekleme, kesici 10 saniye açıkken 2 saniye sonra tekrar denemek
353
+ * demekti — yani aynı 429'u peşin peşin almak.
354
+ *
355
+ * @returns {number} ms; fren kapalıysa ya da bekleyen bir şey yoksa 0.
356
+ */
357
+ export function upstreamCooldownMs() {
358
+ const now = Date.now();
359
+ let longest = 0;
360
+
361
+ for (const state of hosts.values()) {
362
+ longest = Math.max(longest, state.bypassUntil - now, state.blockedUntil - now);
363
+ }
364
+
365
+ return Math.max(0, longest);
366
+ }
367
+
368
+ /**
369
+ * Testler için: ayarları verip tüm host durumlarını sıfırlar.
370
+ *
371
+ * @param {Record<string, unknown>} [config]
372
+ * @returns {void}
373
+ */
374
+ export function resetUpstreamLimiterForTests(config = {}) {
375
+ configureUpstreamLimiter(config);
376
+ }