jskelet 0.2.5 → 0.3.0

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 (106) hide show
  1. package/AGENTS.md +132 -132
  2. package/CHANGELOG.md +403 -383
  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 +293 -287
  7. package/docs/03-routing.md +486 -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 +1231 -1209
  11. package/docs/07-yapilandirma.md +44 -21
  12. package/docs/08-build.md +366 -366
  13. package/docs/09-dev-araclari.md +335 -335
  14. package/docs/10-dagitim.md +329 -329
  15. package/docs/11-tasima.md +1 -0
  16. package/docs/12-panel-ve-oturum.md +384 -384
  17. package/docs/README.md +105 -105
  18. package/docs/en/01-getting-started.md +292 -292
  19. package/docs/en/02-architecture.md +311 -305
  20. package/docs/en/03-routing.md +503 -497
  21. package/docs/en/04-rendering.md +504 -504
  22. package/docs/en/05-islands.md +492 -492
  23. package/docs/en/06-caching.md +1197 -1198
  24. package/docs/en/07-configuration.md +1009 -986
  25. package/docs/en/08-build.md +383 -383
  26. package/docs/en/09-dev-tools.md +342 -342
  27. package/docs/en/10-deployment.md +332 -332
  28. package/docs/en/11-migration.md +360 -359
  29. package/docs/en/12-dashboards-and-sessions.md +392 -392
  30. package/docs/en/README.md +112 -112
  31. package/package.json +102 -102
  32. package/src/build/ensure-build.mjs +15 -15
  33. package/src/build/paths.mjs +143 -143
  34. package/src/build/resolve-peer.mjs +36 -36
  35. package/src/build/tasks/client.mjs +268 -268
  36. package/src/build/tasks/css.mjs +124 -124
  37. package/src/build/tasks/fonts.mjs +146 -146
  38. package/src/build/tasks/icons.mjs +224 -224
  39. package/src/build/tasks/images.mjs +244 -244
  40. package/src/build/tasks/precompress.mjs +78 -78
  41. package/src/client/{cache-panel → admin}/i18n.js +756 -670
  42. package/src/client/{cache-panel → admin}/login.html +74 -74
  43. package/src/client/{cache-panel → admin}/panel.css +804 -756
  44. package/src/client/admin/panel.html +486 -0
  45. package/src/client/{cache-panel → admin}/panel.js +1242 -915
  46. package/src/client/devtools/report.html +185 -185
  47. package/src/client/devtools/report.js +725 -725
  48. package/src/client/dom.js +95 -95
  49. package/src/client/form.js +192 -192
  50. package/src/client/index.js +35 -35
  51. package/src/client/registry.js +297 -297
  52. package/src/client/safe-image.js +91 -91
  53. package/src/client/store.js +36 -36
  54. package/src/client/swap.js +188 -188
  55. package/src/config/defaults.js +16 -7
  56. package/src/config/index.js +38 -22
  57. package/src/config/pattern.js +107 -107
  58. package/src/http/control-flow.js +71 -71
  59. package/src/http/cookies.js +257 -257
  60. package/src/http/request-cache.js +46 -46
  61. package/src/http/request-context.js +162 -162
  62. package/src/index.js +83 -83
  63. package/src/init.mjs +221 -221
  64. package/src/log.mjs +58 -0
  65. package/src/runtime/alias-hooks.mjs +119 -119
  66. package/src/runtime/register.mjs +4 -4
  67. package/src/server/admin/actions.js +229 -0
  68. package/src/server/admin/auth.js +125 -0
  69. package/src/server/admin/event-log.js +151 -0
  70. package/src/server/admin/gate.js +209 -0
  71. package/src/server/admin/inventory.js +188 -0
  72. package/src/server/admin/mount.js +56 -0
  73. package/src/server/admin/router.js +216 -0
  74. package/src/server/admin/snapshot.js +126 -0
  75. package/src/server/assets.js +147 -147
  76. package/src/server/cache-deps.js +42 -42
  77. package/src/server/cloudflare.js +607 -607
  78. package/src/server/create-app.js +295 -291
  79. package/src/server/data-cache.js +462 -462
  80. package/src/server/dev/report.js +369 -369
  81. package/src/server/dev/socket.js +170 -170
  82. package/src/server/dev/version-check.mjs +139 -139
  83. package/src/server/html-cache.js +817 -817
  84. package/src/server/metadata.js +102 -102
  85. package/src/server/middleware/compression.js +205 -205
  86. package/src/server/middleware/csrf.js +134 -134
  87. package/src/server/middleware/dev-gate.js +62 -62
  88. package/src/server/middleware/headers.js +37 -37
  89. package/src/server/middleware/redirects.js +32 -32
  90. package/src/server/middleware/static-precompressed.js +100 -100
  91. package/src/server/middleware/trailing-slash.js +53 -0
  92. package/src/server/middleware/upstream-proxy.js +141 -141
  93. package/src/server/prewarm.js +601 -601
  94. package/src/server/redis.js +569 -569
  95. package/src/server/router.js +128 -128
  96. package/src/server/status-page.js +164 -164
  97. package/src/server/upstream-limiter.js +376 -376
  98. package/src/server/upstream-tracking.js +166 -166
  99. package/src/start.mjs +7 -7
  100. package/src/templates/layout.ejs +44 -44
  101. package/src/version.mjs +31 -31
  102. package/src/views/components/loader.js +85 -85
  103. package/src/views/helpers/html.js +102 -102
  104. package/src/views/helpers/tags.js +245 -245
  105. package/src/client/cache-panel/panel.html +0 -308
  106. package/src/server/cache-panel.js +0 -759
@@ -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
+ }