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.
- package/AGENTS.md +136 -136
- package/CHANGELOG.md +620 -596
- package/LICENSE +21 -21
- package/bin/jskelet.mjs +130 -130
- package/docs/01-baslangic.md +291 -291
- package/docs/02-mimari.md +310 -309
- package/docs/03-routing.md +515 -515
- package/docs/04-render-ve-sablonlar.md +661 -661
- package/docs/05-islands.md +486 -486
- package/docs/06-cache.md +1443 -1423
- package/docs/07-yapilandirma.md +12 -6
- package/docs/08-build.md +429 -428
- package/docs/09-dev-araclari.md +364 -364
- package/docs/10-dagitim.md +338 -338
- package/docs/12-panel-ve-oturum.md +478 -478
- package/docs/README.md +83 -83
- package/docs/en/01-getting-started.md +298 -298
- package/docs/en/02-architecture.md +329 -328
- package/docs/en/03-routing.md +531 -531
- package/docs/en/04-rendering.md +669 -669
- package/docs/en/05-islands.md +497 -497
- package/docs/en/06-caching.md +1453 -1431
- package/docs/en/07-configuration.md +1219 -1214
- package/docs/en/08-build.md +447 -446
- package/docs/en/09-dev-tools.md +373 -373
- package/docs/en/10-deployment.md +340 -340
- package/docs/en/11-migration.md +398 -398
- package/docs/en/12-dashboards-and-sessions.md +488 -488
- package/docs/en/README.md +87 -87
- package/package.json +137 -137
- package/src/build/ensure-build.mjs +19 -19
- package/src/build/paths.mjs +153 -153
- package/src/build/resolve-peer.mjs +36 -36
- package/src/build/tasks/client.mjs +349 -349
- package/src/build/tasks/css.mjs +235 -235
- package/src/build/tasks/fonts.mjs +146 -146
- package/src/build/tasks/icons.mjs +357 -357
- package/src/build/tasks/images.mjs +244 -244
- package/src/build/tasks/precompress.mjs +78 -78
- package/src/build/tasks/templates.mjs +20 -20
- package/src/client/admin/i18n.js +764 -764
- package/src/client/admin/login.html +74 -74
- package/src/client/admin/panel.css +809 -809
- package/src/client/admin/panel.html +495 -495
- package/src/client/admin/panel.js +1251 -1251
- package/src/client/devtools/report.html +185 -185
- package/src/client/devtools/report.js +745 -745
- package/src/client/devtools/seo.js +628 -628
- package/src/client/dom.js +95 -95
- package/src/client/form.js +192 -192
- package/src/client/index.js +45 -45
- package/src/client/registry.js +305 -305
- package/src/client/safe-image.js +91 -91
- package/src/client/shared-cookie.js +225 -225
- package/src/client/store.js +36 -36
- package/src/client/swap.js +188 -188
- package/src/compile/codegen.js +336 -336
- package/src/compile/compile-all.js +149 -149
- package/src/compile/errors.js +66 -66
- package/src/compile/expr.js +409 -409
- package/src/compile/index.js +17 -17
- package/src/compile/parse.js +541 -541
- package/src/compile/resolve.js +211 -211
- package/src/compile/scan-exports.js +51 -51
- package/src/config/defaults.js +17 -1
- package/src/config/index.js +13 -0
- package/src/config/pattern.js +107 -107
- package/src/generate.mjs +163 -163
- package/src/http/control-flow.js +71 -71
- package/src/http/cookies-entry.js +21 -21
- package/src/http/cookies.js +277 -277
- package/src/http/request-cache.js +46 -46
- package/src/http/request-context.js +165 -165
- package/src/http/shared-cookie.js +178 -178
- package/src/index.js +101 -101
- package/src/init.mjs +230 -230
- package/src/migrate/apply.mjs +262 -262
- package/src/migrate/babel.mjs +79 -79
- package/src/migrate/classify.mjs +155 -155
- package/src/migrate/config.mjs +126 -126
- package/src/migrate/fs-walk.mjs +191 -191
- package/src/migrate/parse.mjs +26 -26
- package/src/migrate/scan.mjs +177 -177
- package/src/migrate/transform/expr-source.mjs +168 -168
- package/src/migrate/transform/island.mjs +67 -67
- package/src/migrate/transform/jsx-to-component.mjs +302 -302
- package/src/migrate/transform/jsx-to-jsk.mjs +330 -330
- package/src/migrate/transform/page-split.mjs +435 -435
- package/src/migrate/write.mjs +81 -81
- package/src/migrate.mjs +171 -171
- package/src/runtime/alias-hooks.mjs +119 -119
- package/src/runtime/register.mjs +4 -4
- package/src/server/admin/actions.js +229 -229
- package/src/server/admin/auth.js +125 -125
- package/src/server/admin/event-log.js +151 -151
- package/src/server/admin/gate.js +209 -209
- package/src/server/admin/inventory.js +188 -188
- package/src/server/admin/mount.js +56 -56
- package/src/server/admin/router.js +216 -216
- package/src/server/admin/snapshot.js +241 -241
- package/src/server/assets.js +147 -147
- package/src/server/auth/handoff.js +309 -309
- package/src/server/cache-blob.js +70 -0
- package/src/server/cache-deps.js +42 -42
- package/src/server/cache-vary.js +113 -113
- package/src/server/cloudflare.js +607 -607
- package/src/server/create-app.js +366 -366
- package/src/server/data-cache.js +553 -462
- package/src/server/dev/report.js +485 -485
- package/src/server/dev/socket.js +170 -170
- package/src/server/dev/version-check.mjs +139 -139
- package/src/server/disk-cache.js +233 -0
- package/src/server/ejs-adapter.js +59 -59
- package/src/server/html-cache.js +1196 -1122
- package/src/server/image-optimizer.js +500 -407
- package/src/server/logs/access-middleware.js +66 -66
- package/src/server/logs/file-sink.js +193 -66
- package/src/server/logs/pipeline.js +165 -158
- package/src/server/logs/s3-put.js +214 -214
- package/src/server/logs/s3-sink.js +112 -112
- package/src/server/metadata.js +102 -102
- package/src/server/middleware/compression.js +205 -205
- package/src/server/middleware/csrf.js +134 -134
- package/src/server/middleware/dev-gate.js +75 -75
- package/src/server/middleware/headers.js +37 -37
- package/src/server/middleware/redirects.js +32 -32
- package/src/server/middleware/robots-txt.js +341 -341
- package/src/server/middleware/static-precompressed.js +121 -100
- package/src/server/middleware/trailing-slash.js +53 -53
- package/src/server/middleware/upstream-proxy.js +141 -141
- package/src/server/og-image.js +356 -356
- package/src/server/port-guard.js +255 -255
- package/src/server/prewarm.js +1082 -1058
- package/src/server/redis.js +588 -569
- package/src/server/render.js +4 -4
- package/src/server/router.js +157 -157
- package/src/server/status-page.js +265 -265
- package/src/server/upstream-limiter.js +376 -376
- package/src/server/upstream-tracking.js +166 -166
- package/src/shared/cookie-domain.js +66 -66
- package/src/start.mjs +22 -22
- package/src/templates/layout.ejs +30 -30
- package/src/templates/layout.jsk +30 -30
- package/src/version.mjs +31 -31
- package/src/views/components/loader.js +101 -101
- package/src/views/helpers/html.js +102 -102
- package/src/views/helpers/tags.js +375 -375
- package/types/config/defaults.d.ts +15 -1
- package/types/config/index.d.ts +8 -0
- package/types/server/cache-blob.d.ts +13 -0
- package/types/server/data-cache.d.ts +9 -0
- package/types/server/disk-cache.d.ts +36 -0
- package/types/server/html-cache.d.ts +26 -3
- package/types/server/logs/file-sink.d.ts +16 -5
- 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
|
+
}
|