jskelet 0.2.0 → 0.2.2

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.
@@ -120,6 +120,42 @@ export const DEFAULT_DATA_CACHE = {
120
120
  staleFactor: 10,
121
121
  };
122
122
 
123
+ /**
124
+ * Upstream API'ye giden isteklerin host başına hız freni.
125
+ *
126
+ * Varsayılan **kapalı** (`rate: 0`): fren, kotasını bilen bir uygulamanın
127
+ * bilinçli kararı. Açıldığında `rate` bir tavan olur ve gerçek hız 429
128
+ * cevaplarına göre kendini aşağı çeker (bkz. `src/server/upstream-limiter.js`).
129
+ *
130
+ * `hosts` ile tek tek uçlar ayrılabilir; API'lerin kotası aynı olmak zorunda
131
+ * değil:
132
+ *
133
+ * upstream: { rate: 10, hosts: { "api.example.com": { rate: 3 } } }
134
+ */
135
+ export const DEFAULT_UPSTREAM_LIMIT = {
136
+ /** Saniyedeki en fazla çağrı. 0 → fren tamamen kapalı. */
137
+ rate: 0,
138
+ /** Kova boyu; verilmezse bir saniyelik bütçe kadar patlamaya izin verilir. */
139
+ burst: 0,
140
+ /** Aynı anda uçabilecek çağrı. Ortalama hızdan bağımsız: anlık baskıyı bağlar. */
141
+ concurrency: 8,
142
+ /** Azalmanın dibi: hız buranın altına inmez, yoksa site tamamen durur. */
143
+ minRate: 0.5,
144
+ /** Toplamsal artışın adımı (çağrı/saniye) ve periyodu. */
145
+ increaseStep: 1,
146
+ increaseIntervalMs: 5000,
147
+ /**
148
+ * İki azalma arasındaki en kısa süre. Aynı anda uçan on çağrının hepsi 429
149
+ * dönerse hız on kez yarılanıp dibe vurmasın.
150
+ */
151
+ decreaseIntervalMs: 1000,
152
+ /** Devre kesici: art arda kaç 429'dan sonra host'a hiç gidilmeyeceği. */
153
+ breakerFailures: 5,
154
+ breakerCooldownMs: 10_000,
155
+ /** @type {Record<string, Record<string, number>>} */
156
+ hosts: {},
157
+ };
158
+
123
159
  /**
124
160
  * Opsiyonel Redis ikinci kademesi (L2).
125
161
  *
@@ -155,8 +191,37 @@ export const DEFAULT_REDIS = {
155
191
  commandTimeoutMs: 200,
156
192
  };
157
193
 
194
+ /**
195
+ * Önbellek yönetim paneli.
196
+ *
197
+ * `enabled` varsayılan olarak **kapalı** ve ortama bakmaz: panel açıldığında
198
+ * production'da da çalışır, ama açılması bilinçli bir karar olmalı. Kapalıyken
199
+ * router hiç mount edilmez — yolun kendisi de yok, yani 404 dönen bir uç bile
200
+ * ortaya çıkmaz.
201
+ *
202
+ * Şifre her süreç başlangıcında yeniden üretilir (bkz.
203
+ * `src/server/cache-panel.js`): panelin ömrü sürecin ömrü kadardır ve bir
204
+ * deploy eski erişimi otomatik olarak iptal eder. Bu yüzden config'te şifre
205
+ * alanı yok.
206
+ */
207
+ export const DEFAULT_CACHE_PANEL = {
208
+ enabled: false,
209
+ basePath: "/_jskelet/cache",
210
+ /** Kaç başarısız denemeden sonra IP yasaklanır. */
211
+ banAttempts: 3,
212
+ /** Yasağın süresi. */
213
+ banHours: 24,
214
+ /** Oturumun ömrü; süreç yeniden başladığında zaten sıfırlanır. */
215
+ sessionHours: 12,
216
+ };
217
+
158
218
  /** Oturuma bağlı sayfalar ısıtılmaz; uygulama kendi listesini verebilir. */
159
- export const DEFAULT_PREWARM_SKIP = ["/api/", "/_fragment/", "/__jskelet/"];
219
+ export const DEFAULT_PREWARM_SKIP = [
220
+ "/api/",
221
+ "/_fragment/",
222
+ "/__jskelet/",
223
+ "/_jskelet/",
224
+ ];
160
225
 
161
226
  /**
162
227
  * Site içi gezinme ipuçları (Speculation Rules + view transition).
@@ -182,7 +247,7 @@ export const DEFAULT_NAVIGATION = {
182
247
  * hedefi olmayan uçların spekülatif istekle tetiklenmesi gerçek bir hata
183
248
  * kaynağı; uygulama kendi listesini `navigation.exclude` ile ekler.
184
249
  */
185
- export const DEFAULT_NAVIGATION_EXCLUDE = ["/api/*", "/_fragment/*"];
250
+ export const DEFAULT_NAVIGATION_EXCLUDE = ["/api/*", "/_fragment/*", "/_jskelet/*"];
186
251
 
187
252
  /**
188
253
  * Güvenlik ayarları.
@@ -16,7 +16,8 @@
16
16
  * redirects() → [{ source, destination, permanent?, statusCode? }]
17
17
  * rewrites() → [{ source, destination }] | { beforeFiles?, afterFiles? }
18
18
  * cache() → { html?: { [source]: saniye }, maxEntries?: number,
19
- * data?: {...}, redis?: {...}, prewarm?: {...} }
19
+ * data?: {...}, redis?: {...}, prewarm?: {...},
20
+ * panel?: {...} }
20
21
  *
21
22
  * Fonksiyon olmayan bölümler (`brand`, `security`, `static`, `navigation`…)
22
23
  * düz nesne olarak okunur.
@@ -28,6 +29,7 @@ import { pathToFileURL } from "node:url";
28
29
  import { compilePattern, matchPattern } from "./pattern.js";
29
30
  import {
30
31
  DEFAULT_BRAND,
32
+ DEFAULT_CACHE_PANEL,
31
33
  DEFAULT_DATA_CACHE,
32
34
  DEFAULT_DEV_GATE_BYPASS,
33
35
  DEFAULT_DIRS,
@@ -40,6 +42,7 @@ import {
40
42
  DEFAULT_SECURITY,
41
43
  DEFAULT_STATIC,
42
44
  DEFAULT_TRANSIENT_RETRY,
45
+ DEFAULT_UPSTREAM_LIMIT,
43
46
  } from "./defaults.js";
44
47
 
45
48
  /** Framework paketinin kökü — kendi şablonlarına ve varlıklarına erişir. */
@@ -87,6 +90,8 @@ const CONFIG_FILE = "jskelet.config.mjs";
87
90
  * @property {boolean} trackDependencies Render'ın okuduğu veri anahtarları kaydedilsin mi.
88
91
  * @property {{ attempts: number, delayMs: number }} transientRetry
89
92
  * @property {RedisConfig} redis Opsiyonel Redis ikinci kademesi.
93
+ * @property {typeof DEFAULT_UPSTREAM_LIMIT} upstream Upstream hız freni.
94
+ * @property {typeof DEFAULT_CACHE_PANEL} cachePanel Önbellek yönetim paneli.
90
95
  * @property {Record<string, unknown>} prewarm
91
96
  * @property {{ source: string, test: (pathname: string) => boolean }[]} prewarmPriority
92
97
  * @property {Record<string, unknown>} brand
@@ -249,6 +254,90 @@ function normalizeRedis(raw) {
249
254
  };
250
255
  }
251
256
 
257
+ /**
258
+ * Upstream hız freni. Sayısal alanlar tipine zorlanır; bozuk bir değer freni
259
+ * yanlış ayarlamak yerine varsayılana döner.
260
+ *
261
+ * @param {unknown} raw
262
+ * @returns {typeof DEFAULT_UPSTREAM_LIMIT}
263
+ */
264
+ function normalizeUpstream(raw) {
265
+ const source = /** @type {Record<string, any>} */ (raw ?? {});
266
+ const merged = { ...DEFAULT_UPSTREAM_LIMIT, ...source };
267
+
268
+ /** @param {string} key */
269
+ const positive = (key) => {
270
+ const value = Number(merged[key]);
271
+ return Number.isFinite(value) && value >= 0
272
+ ? value
273
+ : /** @type {any} */ (DEFAULT_UPSTREAM_LIMIT)[key];
274
+ };
275
+
276
+ /** @type {Record<string, Record<string, number>>} */
277
+ const hosts = {};
278
+ for (const [host, override] of Object.entries(merged.hosts ?? {})) {
279
+ if (override && typeof override === "object") hosts[host] = override;
280
+ }
281
+
282
+ return {
283
+ ...merged,
284
+ rate: positive("rate"),
285
+ burst: positive("burst"),
286
+ concurrency: Math.max(1, Math.floor(positive("concurrency"))),
287
+ minRate: positive("minRate"),
288
+ increaseStep: positive("increaseStep"),
289
+ increaseIntervalMs: positive("increaseIntervalMs"),
290
+ decreaseIntervalMs: positive("decreaseIntervalMs"),
291
+ breakerFailures: Math.floor(positive("breakerFailures")),
292
+ breakerCooldownMs: positive("breakerCooldownMs"),
293
+ hosts,
294
+ };
295
+ }
296
+
297
+ /**
298
+ * Önbellek panelinin bölümü.
299
+ *
300
+ * `enabled` yalnızca açıkça `true` verildiğinde ya da `JSKELET_CACHE_PANEL`
301
+ * ortam değişkeni ayarlandığında açılır: paneli yanlışlıkla açmanın bedeli,
302
+ * önbelleği boşaltabilen bir ucu internete koymak.
303
+ *
304
+ * Ortam değişkeni config'in **üstünde** duruyor, çünkü paneli genelde bir
305
+ * arıza sırasında tek seferlik açmak isteniyor ve o an config dosyasını
306
+ * değiştirip yeniden dağıtmak istenmiyor. `JSKELET_CACHE_PANEL=0` aynı
307
+ * mantıkla config'te açık olan paneli kapatır.
308
+ *
309
+ * @param {unknown} raw
310
+ * @returns {typeof DEFAULT_CACHE_PANEL}
311
+ */
312
+ function normalizeCachePanel(raw) {
313
+ const source = /** @type {Record<string, any>} */ (raw ?? {});
314
+ const env = process.env.JSKELET_CACHE_PANEL;
315
+
316
+ const basePath =
317
+ typeof source.basePath === "string" && source.basePath.startsWith("/")
318
+ ? source.basePath.replace(/\/+$/, "")
319
+ : DEFAULT_CACHE_PANEL.basePath;
320
+
321
+ /** @param {string} key @param {number} min */
322
+ const positive = (key, min) => {
323
+ const value = Number(source[key]);
324
+ return Number.isFinite(value) && value >= min
325
+ ? value
326
+ : /** @type {any} */ (DEFAULT_CACHE_PANEL)[key];
327
+ };
328
+
329
+ return {
330
+ enabled:
331
+ env === undefined
332
+ ? source.enabled === true
333
+ : env !== "0" && env !== "false" && env !== "",
334
+ basePath: basePath || DEFAULT_CACHE_PANEL.basePath,
335
+ banAttempts: Math.floor(positive("banAttempts", 1)),
336
+ banHours: positive("banHours", 0),
337
+ sessionHours: positive("sessionHours", 0),
338
+ };
339
+ }
340
+
252
341
  /**
253
342
  * @param {unknown} raw
254
343
  * @returns {{ html: ResolvedConfig["html"], htmlMaxEntries: number,
@@ -256,6 +345,8 @@ function normalizeRedis(raw) {
256
345
  * trackDependencies: boolean,
257
346
  * transientRetry: { attempts: number, delayMs: number },
258
347
  * redis: RedisConfig,
348
+ * upstream: typeof DEFAULT_UPSTREAM_LIMIT,
349
+ * cachePanel: typeof DEFAULT_CACHE_PANEL,
259
350
  * prewarm: Record<string, unknown>,
260
351
  * prewarmPriority: ResolvedConfig["prewarmPriority"] }}
261
352
  */
@@ -292,6 +383,8 @@ function normalizeCache(raw) {
292
383
  ? { attempts: 0, delayMs: 0 }
293
384
  : { ...DEFAULT_TRANSIENT_RETRY, ...(raw?.transientRetry ?? {}) },
294
385
  redis: normalizeRedis(raw?.redis),
386
+ upstream: normalizeUpstream(raw?.upstream),
387
+ cachePanel: normalizeCachePanel(raw?.panel),
295
388
  // Desenler derlenmiş hâlde ayrı alanda tutulur: `prewarm` sayısal
296
389
  // ayarların düz torbası olarak kalsın, her turda yeniden derlenmesin.
297
390
  prewarm,
@@ -510,6 +603,8 @@ export async function loadConfig(options = {}) {
510
603
  trackDependencies,
511
604
  transientRetry,
512
605
  redis,
606
+ upstream,
607
+ cachePanel,
513
608
  prewarm,
514
609
  prewarmPriority,
515
610
  } = normalizeCache(cache);
@@ -530,6 +625,8 @@ export async function loadConfig(options = {}) {
530
625
  trackDependencies,
531
626
  transientRetry,
532
627
  redis,
628
+ upstream,
629
+ cachePanel,
533
630
  prewarm,
534
631
  prewarmPriority,
535
632
  brand,
package/src/index.js CHANGED
@@ -41,6 +41,7 @@ export { headHints } from "./server/head-hints.js";
41
41
  export { renderHeadMeta } from "./server/metadata.js";
42
42
  export {
43
43
  clearHtmlCache,
44
+ dropHtmlCacheKey,
44
45
  getHtmlCacheEntries,
45
46
  getHtmlCacheSize,
46
47
  invalidateHtmlCache,
@@ -49,13 +50,17 @@ export {
49
50
  export {
50
51
  clearDataCache,
51
52
  dataCache,
53
+ dropDataCacheKey,
52
54
  getDataCacheEntries,
53
55
  getDataCacheSize,
56
+ getDataCacheStats,
54
57
  withDataCache,
55
58
  } from "./server/data-cache.js";
56
59
  // Paylaşımlı önbellek kademesinin durumu. Bağlantı kurulmamışken de güvenle
57
60
  // çağrılır; healthcheck uçları bunu okuyor.
58
61
  export { getRedisStatus } from "./server/redis.js";
62
+ // Upstream hız freninin host başına durumu: healthcheck ve teşhis uçları için.
63
+ export { getUpstreamLimiterStatus } from "./server/upstream-limiter.js";
59
64
  export { prewarm, prewarmProgress } from "./server/prewarm.js";
60
65
  export { createProxy } from "./server/middleware/upstream-proxy.js";
61
66
  export { getConfig, loadConfig } from "./config/index.js";