jskelet 0.6.2 → 0.6.3

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 (155) hide show
  1. package/AGENTS.md +136 -136
  2. package/CHANGELOG.md +620 -596
  3. package/LICENSE +21 -21
  4. package/bin/jskelet.mjs +130 -130
  5. package/docs/01-baslangic.md +291 -291
  6. package/docs/02-mimari.md +310 -309
  7. package/docs/03-routing.md +515 -515
  8. package/docs/04-render-ve-sablonlar.md +661 -661
  9. package/docs/05-islands.md +486 -486
  10. package/docs/06-cache.md +1443 -1423
  11. package/docs/07-yapilandirma.md +12 -6
  12. package/docs/08-build.md +429 -428
  13. package/docs/09-dev-araclari.md +364 -364
  14. package/docs/10-dagitim.md +338 -338
  15. package/docs/12-panel-ve-oturum.md +478 -478
  16. package/docs/README.md +83 -83
  17. package/docs/en/01-getting-started.md +298 -298
  18. package/docs/en/02-architecture.md +329 -328
  19. package/docs/en/03-routing.md +531 -531
  20. package/docs/en/04-rendering.md +669 -669
  21. package/docs/en/05-islands.md +497 -497
  22. package/docs/en/06-caching.md +1453 -1431
  23. package/docs/en/07-configuration.md +1219 -1214
  24. package/docs/en/08-build.md +447 -446
  25. package/docs/en/09-dev-tools.md +373 -373
  26. package/docs/en/10-deployment.md +340 -340
  27. package/docs/en/11-migration.md +398 -398
  28. package/docs/en/12-dashboards-and-sessions.md +488 -488
  29. package/docs/en/README.md +87 -87
  30. package/package.json +137 -137
  31. package/src/build/ensure-build.mjs +19 -19
  32. package/src/build/paths.mjs +153 -153
  33. package/src/build/resolve-peer.mjs +36 -36
  34. package/src/build/tasks/client.mjs +349 -349
  35. package/src/build/tasks/css.mjs +235 -235
  36. package/src/build/tasks/fonts.mjs +146 -146
  37. package/src/build/tasks/icons.mjs +357 -357
  38. package/src/build/tasks/images.mjs +244 -244
  39. package/src/build/tasks/precompress.mjs +78 -78
  40. package/src/build/tasks/templates.mjs +20 -20
  41. package/src/client/admin/i18n.js +764 -764
  42. package/src/client/admin/login.html +74 -74
  43. package/src/client/admin/panel.css +809 -809
  44. package/src/client/admin/panel.html +495 -495
  45. package/src/client/admin/panel.js +1251 -1251
  46. package/src/client/devtools/report.html +185 -185
  47. package/src/client/devtools/report.js +745 -745
  48. package/src/client/devtools/seo.js +628 -628
  49. package/src/client/dom.js +95 -95
  50. package/src/client/form.js +192 -192
  51. package/src/client/index.js +45 -45
  52. package/src/client/registry.js +305 -305
  53. package/src/client/safe-image.js +91 -91
  54. package/src/client/shared-cookie.js +225 -225
  55. package/src/client/store.js +36 -36
  56. package/src/client/swap.js +188 -188
  57. package/src/compile/codegen.js +336 -336
  58. package/src/compile/compile-all.js +149 -149
  59. package/src/compile/errors.js +66 -66
  60. package/src/compile/expr.js +409 -409
  61. package/src/compile/index.js +17 -17
  62. package/src/compile/parse.js +541 -541
  63. package/src/compile/resolve.js +211 -211
  64. package/src/compile/scan-exports.js +51 -51
  65. package/src/config/defaults.js +17 -1
  66. package/src/config/index.js +13 -0
  67. package/src/config/pattern.js +107 -107
  68. package/src/generate.mjs +163 -163
  69. package/src/http/control-flow.js +71 -71
  70. package/src/http/cookies-entry.js +21 -21
  71. package/src/http/cookies.js +277 -277
  72. package/src/http/request-cache.js +46 -46
  73. package/src/http/request-context.js +165 -165
  74. package/src/http/shared-cookie.js +178 -178
  75. package/src/index.js +101 -101
  76. package/src/init.mjs +230 -230
  77. package/src/migrate/apply.mjs +262 -262
  78. package/src/migrate/babel.mjs +79 -79
  79. package/src/migrate/classify.mjs +155 -155
  80. package/src/migrate/config.mjs +126 -126
  81. package/src/migrate/fs-walk.mjs +191 -191
  82. package/src/migrate/parse.mjs +26 -26
  83. package/src/migrate/scan.mjs +177 -177
  84. package/src/migrate/transform/expr-source.mjs +168 -168
  85. package/src/migrate/transform/island.mjs +67 -67
  86. package/src/migrate/transform/jsx-to-component.mjs +302 -302
  87. package/src/migrate/transform/jsx-to-jsk.mjs +330 -330
  88. package/src/migrate/transform/page-split.mjs +435 -435
  89. package/src/migrate/write.mjs +81 -81
  90. package/src/migrate.mjs +171 -171
  91. package/src/runtime/alias-hooks.mjs +119 -119
  92. package/src/runtime/register.mjs +4 -4
  93. package/src/server/admin/actions.js +229 -229
  94. package/src/server/admin/auth.js +125 -125
  95. package/src/server/admin/event-log.js +151 -151
  96. package/src/server/admin/gate.js +209 -209
  97. package/src/server/admin/inventory.js +188 -188
  98. package/src/server/admin/mount.js +56 -56
  99. package/src/server/admin/router.js +216 -216
  100. package/src/server/admin/snapshot.js +241 -241
  101. package/src/server/assets.js +147 -147
  102. package/src/server/auth/handoff.js +309 -309
  103. package/src/server/cache-blob.js +70 -0
  104. package/src/server/cache-deps.js +42 -42
  105. package/src/server/cache-vary.js +113 -113
  106. package/src/server/cloudflare.js +607 -607
  107. package/src/server/create-app.js +366 -366
  108. package/src/server/data-cache.js +553 -462
  109. package/src/server/dev/report.js +485 -485
  110. package/src/server/dev/socket.js +170 -170
  111. package/src/server/dev/version-check.mjs +139 -139
  112. package/src/server/disk-cache.js +233 -0
  113. package/src/server/ejs-adapter.js +59 -59
  114. package/src/server/html-cache.js +1196 -1122
  115. package/src/server/image-optimizer.js +500 -407
  116. package/src/server/logs/access-middleware.js +66 -66
  117. package/src/server/logs/file-sink.js +193 -66
  118. package/src/server/logs/pipeline.js +165 -158
  119. package/src/server/logs/s3-put.js +214 -214
  120. package/src/server/logs/s3-sink.js +112 -112
  121. package/src/server/metadata.js +102 -102
  122. package/src/server/middleware/compression.js +205 -205
  123. package/src/server/middleware/csrf.js +134 -134
  124. package/src/server/middleware/dev-gate.js +75 -75
  125. package/src/server/middleware/headers.js +37 -37
  126. package/src/server/middleware/redirects.js +32 -32
  127. package/src/server/middleware/robots-txt.js +341 -341
  128. package/src/server/middleware/static-precompressed.js +121 -100
  129. package/src/server/middleware/trailing-slash.js +53 -53
  130. package/src/server/middleware/upstream-proxy.js +141 -141
  131. package/src/server/og-image.js +356 -356
  132. package/src/server/port-guard.js +255 -255
  133. package/src/server/prewarm.js +1082 -1058
  134. package/src/server/redis.js +588 -569
  135. package/src/server/render.js +4 -4
  136. package/src/server/router.js +157 -157
  137. package/src/server/status-page.js +265 -265
  138. package/src/server/upstream-limiter.js +376 -376
  139. package/src/server/upstream-tracking.js +166 -166
  140. package/src/shared/cookie-domain.js +66 -66
  141. package/src/start.mjs +22 -22
  142. package/src/templates/layout.ejs +30 -30
  143. package/src/templates/layout.jsk +30 -30
  144. package/src/version.mjs +31 -31
  145. package/src/views/components/loader.js +101 -101
  146. package/src/views/helpers/html.js +102 -102
  147. package/src/views/helpers/tags.js +375 -375
  148. package/types/config/defaults.d.ts +15 -1
  149. package/types/config/index.d.ts +8 -0
  150. package/types/server/cache-blob.d.ts +13 -0
  151. package/types/server/data-cache.d.ts +9 -0
  152. package/types/server/disk-cache.d.ts +36 -0
  153. package/types/server/html-cache.d.ts +26 -3
  154. package/types/server/logs/file-sink.d.ts +16 -5
  155. package/types/server/redis.d.ts +2 -1
@@ -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
+ }