jskelet 0.1.2 → 0.1.4
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/CHANGELOG.md +87 -4
- package/docs/06-cache.md +255 -20
- package/docs/07-yapilandirma.md +75 -6
- package/docs/en/06-caching.md +264 -23
- package/docs/en/07-configuration.md +78 -6
- package/package.json +1 -1
- package/src/config/defaults.js +63 -0
- package/src/config/index.js +82 -5
- package/src/index.js +7 -0
- package/src/server/create-app.js +6 -0
- package/src/server/data-cache.js +244 -0
- package/src/server/dev/report.js +8 -1
- package/src/server/html-cache.js +22 -2
- package/src/server/prewarm.js +159 -14
- package/src/server/render.js +155 -29
- package/src/server/upstream-tracking.js +103 -13
package/src/config/index.js
CHANGED
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
* headers() → [{ source, headers: [{ key, value }] }]
|
|
16
16
|
* redirects() → [{ source, destination, permanent?, statusCode? }]
|
|
17
17
|
* rewrites() → [{ source, destination }] | { beforeFiles?, afterFiles? }
|
|
18
|
-
* cache() → { html?: { [source]: saniye },
|
|
18
|
+
* cache() → { html?: { [source]: saniye }, maxEntries?: number,
|
|
19
|
+
* data?: {...}, prewarm?: {...} }
|
|
19
20
|
*
|
|
20
21
|
* Fonksiyon olmayan bölümler (`brand`, `security`, `static`, `navigation`…)
|
|
21
22
|
* düz nesne olarak okunur.
|
|
@@ -24,17 +25,20 @@ import fs from "node:fs";
|
|
|
24
25
|
import path from "node:path";
|
|
25
26
|
import process from "node:process";
|
|
26
27
|
import { pathToFileURL } from "node:url";
|
|
27
|
-
import { compilePattern } from "./pattern.js";
|
|
28
|
+
import { compilePattern, matchPattern } from "./pattern.js";
|
|
28
29
|
import {
|
|
29
30
|
DEFAULT_BRAND,
|
|
31
|
+
DEFAULT_DATA_CACHE,
|
|
30
32
|
DEFAULT_DEV_GATE_BYPASS,
|
|
31
33
|
DEFAULT_DIRS,
|
|
34
|
+
DEFAULT_HTML_CACHE_MAX_ENTRIES,
|
|
32
35
|
DEFAULT_NAVIGATION,
|
|
33
36
|
DEFAULT_NAVIGATION_EXCLUDE,
|
|
34
37
|
DEFAULT_PREWARM,
|
|
35
38
|
DEFAULT_PREWARM_SKIP,
|
|
36
39
|
DEFAULT_SECURITY,
|
|
37
40
|
DEFAULT_STATIC,
|
|
41
|
+
DEFAULT_TRANSIENT_RETRY,
|
|
38
42
|
} from "./defaults.js";
|
|
39
43
|
|
|
40
44
|
/** Framework paketinin kökü — kendi şablonlarına ve varlıklarına erişir. */
|
|
@@ -63,7 +67,12 @@ const CONFIG_FILE = "jskelet.config.mjs";
|
|
|
63
67
|
* @property {{ pattern: CompiledPattern, destination: string, statusCode: number }[]} redirects
|
|
64
68
|
* @property {{ phase: "beforeFiles" | "afterFiles", pattern: CompiledPattern, destination: string }[]} rewrites
|
|
65
69
|
* @property {{ pattern: CompiledPattern, seconds: number }[]} html
|
|
70
|
+
* @property {number} htmlMaxEntries HTML önbelleğinin girdi sınırı.
|
|
71
|
+
* @property {Record<string, unknown>} data Upstream veri önbelleği ayarları.
|
|
72
|
+
* @property {boolean} trackUpstream `fetch` sarılıp geçici hatalar otomatik bildirilsin mi.
|
|
73
|
+
* @property {{ attempts: number, delayMs: number }} transientRetry
|
|
66
74
|
* @property {Record<string, unknown>} prewarm
|
|
75
|
+
* @property {{ source: string, test: (pathname: string) => boolean }[]} prewarmPriority
|
|
67
76
|
* @property {Record<string, unknown>} brand
|
|
68
77
|
* @property {Record<string, Function>} hooks
|
|
69
78
|
* @property {string} layout Layout `.ejs` dosyasının mutlak yolu.
|
|
@@ -168,8 +177,42 @@ function normalizeRewrites(raw) {
|
|
|
168
177
|
}
|
|
169
178
|
|
|
170
179
|
/**
|
|
180
|
+
* Isıtma sırası desenleri. İki biçim kabul edilir: config'in her yerinde
|
|
181
|
+
* geçerli olan `/haber/:slug` sözdizimi ve doğrudan `RegExp` — ikincisi
|
|
182
|
+
* "sonu `-yorumlar` ile bitenler" gibi desen sözdiziminin karşılamadığı
|
|
183
|
+
* kuralları yazabilmek için.
|
|
184
|
+
*
|
|
171
185
|
* @param {unknown} raw
|
|
172
|
-
* @returns {
|
|
186
|
+
* @returns {ResolvedConfig["prewarmPriority"]}
|
|
187
|
+
*/
|
|
188
|
+
function normalizePriority(raw) {
|
|
189
|
+
/** @type {ResolvedConfig["prewarmPriority"]} */
|
|
190
|
+
const out = [];
|
|
191
|
+
|
|
192
|
+
for (const entry of asArray(raw, "cache().prewarm.priority")) {
|
|
193
|
+
if (entry instanceof RegExp) {
|
|
194
|
+
out.push({ source: String(entry), test: (pathname) => entry.test(pathname) });
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const pattern = compilePattern(entry);
|
|
199
|
+
if (!pattern) continue;
|
|
200
|
+
out.push({
|
|
201
|
+
source: pattern.source,
|
|
202
|
+
test: (pathname) => matchPattern(pattern, pathname) !== null,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return out;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* @param {unknown} raw
|
|
211
|
+
* @returns {{ html: ResolvedConfig["html"], htmlMaxEntries: number,
|
|
212
|
+
* data: Record<string, unknown>, trackUpstream: boolean,
|
|
213
|
+
* transientRetry: { attempts: number, delayMs: number },
|
|
214
|
+
* prewarm: Record<string, unknown>,
|
|
215
|
+
* prewarmPriority: ResolvedConfig["prewarmPriority"] }}
|
|
173
216
|
*/
|
|
174
217
|
function normalizeCache(raw) {
|
|
175
218
|
/** @type {ResolvedConfig["html"]} */
|
|
@@ -182,7 +225,28 @@ function normalizeCache(raw) {
|
|
|
182
225
|
html.push({ pattern, seconds: value });
|
|
183
226
|
}
|
|
184
227
|
|
|
185
|
-
|
|
228
|
+
const prewarm = { ...DEFAULT_PREWARM, ...(raw?.prewarm ?? {}) };
|
|
229
|
+
const maxEntries = Number(raw?.maxEntries);
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
html,
|
|
233
|
+
htmlMaxEntries:
|
|
234
|
+
Number.isFinite(maxEntries) && maxEntries > 0
|
|
235
|
+
? Math.floor(maxEntries)
|
|
236
|
+
: DEFAULT_HTML_CACHE_MAX_ENTRIES,
|
|
237
|
+
data: { ...DEFAULT_DATA_CACHE, ...(raw?.data ?? {}) },
|
|
238
|
+
// Otomatik upstream izleme kapatılabilir olmalı: `fetch`i kendisi saran
|
|
239
|
+
// bir uygulama (ölçüm, retry, circuit breaker) çakışma yaşayabilir.
|
|
240
|
+
trackUpstream: raw?.trackUpstream !== false,
|
|
241
|
+
transientRetry:
|
|
242
|
+
raw?.transientRetry === false
|
|
243
|
+
? { attempts: 0, delayMs: 0 }
|
|
244
|
+
: { ...DEFAULT_TRANSIENT_RETRY, ...(raw?.transientRetry ?? {}) },
|
|
245
|
+
// Desenler derlenmiş hâlde ayrı alanda tutulur: `prewarm` sayısal
|
|
246
|
+
// ayarların düz torbası olarak kalsın, her turda yeniden derlenmesin.
|
|
247
|
+
prewarm,
|
|
248
|
+
prewarmPriority: normalizePriority(prewarm.priority),
|
|
249
|
+
};
|
|
186
250
|
}
|
|
187
251
|
|
|
188
252
|
/** Speculation Rules'un tanıdığı eagerness değerleri. */
|
|
@@ -388,7 +452,15 @@ export async function loadConfig(options = {}) {
|
|
|
388
452
|
section("cache"),
|
|
389
453
|
]);
|
|
390
454
|
|
|
391
|
-
const {
|
|
455
|
+
const {
|
|
456
|
+
html,
|
|
457
|
+
htmlMaxEntries,
|
|
458
|
+
data,
|
|
459
|
+
trackUpstream,
|
|
460
|
+
transientRetry,
|
|
461
|
+
prewarm,
|
|
462
|
+
prewarmPriority,
|
|
463
|
+
} = normalizeCache(cache);
|
|
392
464
|
const dirs = resolveDirs(root, source.paths);
|
|
393
465
|
const brand = { ...DEFAULT_BRAND, ...(source.brand ?? {}) };
|
|
394
466
|
|
|
@@ -400,7 +472,12 @@ export async function loadConfig(options = {}) {
|
|
|
400
472
|
redirects: normalizeRedirects(redirects),
|
|
401
473
|
rewrites: normalizeRewrites(rewrites),
|
|
402
474
|
html,
|
|
475
|
+
htmlMaxEntries,
|
|
476
|
+
data,
|
|
477
|
+
trackUpstream,
|
|
478
|
+
transientRetry,
|
|
403
479
|
prewarm,
|
|
480
|
+
prewarmPriority,
|
|
404
481
|
brand,
|
|
405
482
|
hooks: source.hooks ?? {},
|
|
406
483
|
layout: resolveLayout(dirs, source.layout),
|
package/src/index.js
CHANGED
|
@@ -45,6 +45,13 @@ export {
|
|
|
45
45
|
getHtmlCacheSize,
|
|
46
46
|
withHtmlCache,
|
|
47
47
|
} from "./server/html-cache.js";
|
|
48
|
+
export {
|
|
49
|
+
clearDataCache,
|
|
50
|
+
dataCache,
|
|
51
|
+
getDataCacheEntries,
|
|
52
|
+
getDataCacheSize,
|
|
53
|
+
withDataCache,
|
|
54
|
+
} from "./server/data-cache.js";
|
|
48
55
|
export { prewarm, prewarmProgress } from "./server/prewarm.js";
|
|
49
56
|
export { createProxy } from "./server/middleware/upstream-proxy.js";
|
|
50
57
|
export { getConfig, loadConfig } from "./config/index.js";
|
package/src/server/create-app.js
CHANGED
|
@@ -34,6 +34,7 @@ import { registerRoutes } from "./router.js";
|
|
|
34
34
|
import { renderNotFound } from "./render.js";
|
|
35
35
|
import { renderStatusPage, statusFromError } from "./status-page.js";
|
|
36
36
|
import { startPrewarm } from "./prewarm.js";
|
|
37
|
+
import { trackUpstreamFetch } from "./upstream-tracking.js";
|
|
37
38
|
import { isNotFoundError, isRedirectError } from "../http/control-flow.js";
|
|
38
39
|
|
|
39
40
|
/**
|
|
@@ -45,6 +46,11 @@ export async function createApp(options = {}) {
|
|
|
45
46
|
await loadConfig(options);
|
|
46
47
|
const config = getConfig();
|
|
47
48
|
|
|
49
|
+
// Upstream hatalarının izlenmesi route'lardan önce kurulmalı: sarmalayıcı
|
|
50
|
+
// yalnızca render bağlamı içindeki `fetch` çağrılarına bakar, ama bağlamın
|
|
51
|
+
// ilk kurulduğu istek de kapsanmalı.
|
|
52
|
+
if (config.trackUpstream) trackUpstreamFetch();
|
|
53
|
+
|
|
48
54
|
const app = express();
|
|
49
55
|
|
|
50
56
|
app.disable("x-powered-by");
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Upstream veri için TTL + stale-while-revalidate LRU önbelleği.
|
|
3
|
+
*
|
|
4
|
+
* HTML önbelleği (`html-cache.js`) yalnızca **trafiği olan** sayfaları tutar:
|
|
5
|
+
* girdi başına yüz kilobayt düştüğü için sınırı 500 civarındadır ve on binlerce
|
|
6
|
+
* yolluk bir site onu ısıtmaya çalıştığında kendi ısıttığını siler. Uzun kuyruk
|
|
7
|
+
* için doğru katman bu modül: aynı sayfanın JSON'u HTML'inden onlarca kat
|
|
8
|
+
* küçük olduğu için on binlerce girdi bellekte durur.
|
|
9
|
+
*
|
|
10
|
+
* Kazanç iki taraflı:
|
|
11
|
+
* - Hiç ısıtılmamış bir uzun kuyruk sayfası ilk ziyaretçide render edilir ama
|
|
12
|
+
* upstream'e gitmez; gecikme yüzlerce ms değil, şablon render'ı kadardır.
|
|
13
|
+
* - Periyodik ısıtma turları API kotası harcamaz, veri katmanından okur.
|
|
14
|
+
*
|
|
15
|
+
* `null`/`undefined` **saklanmaz**: uygulamaların HTTP istemcisi hatada
|
|
16
|
+
* genellikle `null` döner ve bunu saklamak, geçici bir 429'u TTL boyunca "veri
|
|
17
|
+
* yok" hâline dondurmak olurdu. Boş cevabı bilinçli olarak saklamak isteyen
|
|
18
|
+
* `storeEmpty: true` verir.
|
|
19
|
+
*/
|
|
20
|
+
import { getConfig } from "../config/index.js";
|
|
21
|
+
import { DEFAULT_DATA_CACHE } from "../config/defaults.js";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @typedef {{ value: unknown, expiresAt: number, staleUntil: number }} DataEntry
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/** @type {Map<string, DataEntry>} */
|
|
28
|
+
const store = new Map();
|
|
29
|
+
|
|
30
|
+
/** @type {Map<string, Promise<unknown>>} */
|
|
31
|
+
const inflight = new Map();
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Ayarlar config'ten okunur ama config yüklenmemiş olabilir: bu modül
|
|
35
|
+
* script'lerden ve testlerden de çağrılabiliyor. `getConfig()` fırlatırsa
|
|
36
|
+
* kod varsayılanına düşülür.
|
|
37
|
+
*
|
|
38
|
+
* @returns {{ maxEntries: number, staleFactor: number }}
|
|
39
|
+
*/
|
|
40
|
+
function settings() {
|
|
41
|
+
try {
|
|
42
|
+
const { data } = getConfig();
|
|
43
|
+
return {
|
|
44
|
+
maxEntries: Number(data?.maxEntries) || DEFAULT_DATA_CACHE.maxEntries,
|
|
45
|
+
staleFactor: Number.isFinite(Number(data?.staleFactor))
|
|
46
|
+
? Number(data.staleFactor)
|
|
47
|
+
: DEFAULT_DATA_CACHE.staleFactor,
|
|
48
|
+
};
|
|
49
|
+
} catch {
|
|
50
|
+
return { ...DEFAULT_DATA_CACHE };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @param {string} key
|
|
56
|
+
* @returns {{ value: unknown, stale: boolean } | null}
|
|
57
|
+
*/
|
|
58
|
+
function read(key) {
|
|
59
|
+
const entry = store.get(key);
|
|
60
|
+
if (!entry) return null;
|
|
61
|
+
|
|
62
|
+
const now = Date.now();
|
|
63
|
+
if (now >= entry.staleUntil) {
|
|
64
|
+
store.delete(key);
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// LRU: erişilen girdiyi sona taşı.
|
|
69
|
+
store.delete(key);
|
|
70
|
+
store.set(key, entry);
|
|
71
|
+
|
|
72
|
+
return { value: entry.value, stale: now >= entry.expiresAt };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* @param {string} key
|
|
77
|
+
* @param {unknown} value
|
|
78
|
+
* @param {number} ttlSeconds
|
|
79
|
+
* @param {number} staleFactor
|
|
80
|
+
*/
|
|
81
|
+
function write(key, value, ttlSeconds, staleFactor) {
|
|
82
|
+
const now = Date.now();
|
|
83
|
+
const ttl = ttlSeconds * 1000;
|
|
84
|
+
|
|
85
|
+
store.set(key, {
|
|
86
|
+
value,
|
|
87
|
+
expiresAt: now + ttl,
|
|
88
|
+
staleUntil: now + ttl + ttl * staleFactor,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const { maxEntries } = settings();
|
|
92
|
+
while (store.size > maxEntries) {
|
|
93
|
+
const oldest = store.keys().next().value;
|
|
94
|
+
if (oldest === undefined) break;
|
|
95
|
+
store.delete(oldest);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* @param {string} key
|
|
101
|
+
* @param {number} ttlSeconds
|
|
102
|
+
* @param {() => Promise<unknown>} producer
|
|
103
|
+
* @param {{ storeEmpty?: boolean, staleFactor?: number }} options
|
|
104
|
+
* @returns {Promise<unknown>}
|
|
105
|
+
*/
|
|
106
|
+
function refresh(key, ttlSeconds, producer, options) {
|
|
107
|
+
// Aynı anahtarı eşzamanlı isteyen yüz sayfa tek upstream isteğine düşer.
|
|
108
|
+
// Isıtma turlarında bu tek başına kotanın büyük kısmını kurtarıyor.
|
|
109
|
+
const pending = inflight.get(key);
|
|
110
|
+
if (pending) return pending;
|
|
111
|
+
|
|
112
|
+
const staleFactor = options.staleFactor ?? settings().staleFactor;
|
|
113
|
+
|
|
114
|
+
const task = Promise.resolve()
|
|
115
|
+
.then(producer)
|
|
116
|
+
.then((value) => {
|
|
117
|
+
const empty = value === undefined || value === null;
|
|
118
|
+
if (!empty || options.storeEmpty === true) {
|
|
119
|
+
write(key, value, ttlSeconds, staleFactor);
|
|
120
|
+
}
|
|
121
|
+
return value;
|
|
122
|
+
})
|
|
123
|
+
.finally(() => {
|
|
124
|
+
inflight.delete(key);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
inflight.set(key, task);
|
|
128
|
+
return task;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Veriyi önbellekten döner, gerekiyorsa `producer` ile üretir.
|
|
133
|
+
*
|
|
134
|
+
* @param {string} key Anahtar tamamen uygulamanın; sürüm/dil gibi ayrımlar
|
|
135
|
+
* anahtara yazılır (`quote:v2:${symbol}`).
|
|
136
|
+
* @param {number} ttlSeconds 0 → önbellek yok, `producer` her çağrıda çalışır.
|
|
137
|
+
* @param {() => Promise<T>} producer
|
|
138
|
+
* @param {{ storeEmpty?: boolean, staleFactor?: number }} [options]
|
|
139
|
+
* `storeEmpty` boş cevabı da saklar, `staleFactor` bu anahtar için bayat
|
|
140
|
+
* penceresini ayarlar (0 → bayat servis yok).
|
|
141
|
+
* @returns {Promise<T>}
|
|
142
|
+
* @template T
|
|
143
|
+
*/
|
|
144
|
+
export async function withDataCache(key, ttlSeconds, producer, options = {}) {
|
|
145
|
+
if (!ttlSeconds) return producer();
|
|
146
|
+
|
|
147
|
+
const hit = read(key);
|
|
148
|
+
|
|
149
|
+
if (hit) {
|
|
150
|
+
// Bayat girdi anında döner; tazeleme arkada yürür ve hatası bu isteği
|
|
151
|
+
// etkilemez — çağıran taraf bir şey beklemediği için upstream'in yavaş
|
|
152
|
+
// olması sayfaya yansımaz.
|
|
153
|
+
if (hit.stale) {
|
|
154
|
+
void refresh(key, ttlSeconds, producer, options).catch((error) => {
|
|
155
|
+
console.error(`[data-cache] background refresh failed: ${key}`, error);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
return /** @type {T} */ (hit.value);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
return /** @type {T} */ (await refresh(key, ttlSeconds, producer, options));
|
|
163
|
+
} catch (error) {
|
|
164
|
+
// Girdi yoksa hata çağırana gider; asıl kazanç bayat girdinin olduğu
|
|
165
|
+
// durumda: upstream düşmüşken sayfayı eski veriyle ayakta tutmak,
|
|
166
|
+
// ziyaretçiye hata sayfası göstermekten iyidir.
|
|
167
|
+
const stale = read(key);
|
|
168
|
+
if (!stale) throw error;
|
|
169
|
+
|
|
170
|
+
console.warn(
|
|
171
|
+
`[data-cache] producer failed, serving stale value: ${key}`,
|
|
172
|
+
error instanceof Error ? error.message : error,
|
|
173
|
+
);
|
|
174
|
+
return /** @type {T} */ (stale.value);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* `withDataCache`'in fonksiyon sarmalayıcısı: argümanlardan anahtar üretir.
|
|
180
|
+
* `cache()` (istek içi memoizasyon) ile aynı kullanım biçimi, ama istekler
|
|
181
|
+
* arasında ve TTL'li.
|
|
182
|
+
*
|
|
183
|
+
* @param {F} fn
|
|
184
|
+
* @param {{ key: string, revalidate: number, storeEmpty?: boolean,
|
|
185
|
+
* staleFactor?: number }} options `key` önektir; argümanlar sonuna eklenir.
|
|
186
|
+
* @returns {F}
|
|
187
|
+
* @template {(...args: any[]) => Promise<any>} F
|
|
188
|
+
*/
|
|
189
|
+
export function dataCache(fn, options) {
|
|
190
|
+
const wrapped = (...args) =>
|
|
191
|
+
withDataCache(
|
|
192
|
+
args.length ? `${options.key}:${JSON.stringify(args)}` : options.key,
|
|
193
|
+
options.revalidate,
|
|
194
|
+
() => fn(...args),
|
|
195
|
+
options,
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
return /** @type {F} */ (wrapped);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Bir anahtarı ya da önek eşleşen tüm anahtarları düşürür. Webhook ile
|
|
203
|
+
* "bu haber güncellendi" bilgisi geldiğinde kullanılır.
|
|
204
|
+
*
|
|
205
|
+
* @param {string} [prefix] Verilmezse tüm önbellek boşaltılır.
|
|
206
|
+
* @returns {number} Silinen girdi sayısı.
|
|
207
|
+
*/
|
|
208
|
+
export function clearDataCache(prefix) {
|
|
209
|
+
if (prefix === undefined) {
|
|
210
|
+
const size = store.size;
|
|
211
|
+
store.clear();
|
|
212
|
+
return size;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
let removed = 0;
|
|
216
|
+
for (const key of store.keys()) {
|
|
217
|
+
if (key.startsWith(prefix)) {
|
|
218
|
+
store.delete(key);
|
|
219
|
+
removed += 1;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return removed;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** @returns {number} */
|
|
226
|
+
export function getDataCacheSize() {
|
|
227
|
+
return store.size;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Dev raporu ve yönetim uçları için döküm. Değerin kendisi dönmez: JSON'un
|
|
232
|
+
* tamamını bir teşhis ucundan dışa vermek istenmez.
|
|
233
|
+
*
|
|
234
|
+
* @returns {{ key: string, stale: boolean, expiresIn: number }[]}
|
|
235
|
+
*/
|
|
236
|
+
export function getDataCacheEntries() {
|
|
237
|
+
const now = Date.now();
|
|
238
|
+
|
|
239
|
+
return [...store.entries()].map(([key, entry]) => ({
|
|
240
|
+
key,
|
|
241
|
+
stale: now >= entry.expiresAt,
|
|
242
|
+
expiresIn: Math.round((entry.expiresAt - now) / 1000),
|
|
243
|
+
}));
|
|
244
|
+
}
|
package/src/server/dev/report.js
CHANGED
|
@@ -13,6 +13,7 @@ import fs from "node:fs";
|
|
|
13
13
|
import path from "node:path";
|
|
14
14
|
import zlib from "node:zlib";
|
|
15
15
|
import { getHtmlCacheEntries, getHtmlCacheSize } from "../html-cache.js";
|
|
16
|
+
import { getDataCacheSize } from "../data-cache.js";
|
|
16
17
|
import { prewarmProgress } from "../prewarm.js";
|
|
17
18
|
import { getConfig } from "../../config/index.js";
|
|
18
19
|
|
|
@@ -343,7 +344,13 @@ export function buildReport(devtools) {
|
|
|
343
344
|
pages: pageList.sort((a, b) => (b.visits - a.visits) || b.at - a.at),
|
|
344
345
|
serverApi: serverApiCalls.slice().reverse(),
|
|
345
346
|
build: { assets: assets(), ...chunks() },
|
|
346
|
-
cache: {
|
|
347
|
+
cache: {
|
|
348
|
+
size: getHtmlCacheSize(),
|
|
349
|
+
entries: getHtmlCacheEntries(),
|
|
350
|
+
// Veri önbelleğinden yalnızca sayaç: uzun kuyruklu bir sitede on
|
|
351
|
+
// binlerce anahtar oluyor ve dökümü rapora koymak faydasız bir yük.
|
|
352
|
+
data: getDataCacheSize(),
|
|
353
|
+
},
|
|
347
354
|
prewarm: { ...prewarmProgress },
|
|
348
355
|
requests: devtools.requests,
|
|
349
356
|
errors: devtools.errors,
|
package/src/server/html-cache.js
CHANGED
|
@@ -8,12 +8,31 @@
|
|
|
8
8
|
* WebSocket'ten güncellendiği için bu gecikme ekranda görünmez.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { getConfig } from "../config/index.js";
|
|
12
|
+
import { DEFAULT_HTML_CACHE_MAX_ENTRIES } from "../config/defaults.js";
|
|
13
|
+
|
|
11
14
|
/**
|
|
12
15
|
* @typedef {{ html: string, status: number, expiresAt: number,
|
|
13
16
|
* staleUntil: number, encoded: Map<string, Buffer> }} HtmlEntry
|
|
14
17
|
*/
|
|
15
18
|
|
|
16
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Girdi sınırı `cache().maxEntries` ile yükseltilebilir ama uzun kuyruklu bir
|
|
21
|
+
* siteyi buradan çözmeye çalışmak yanlış katman: girdi başına yüz kilobayt
|
|
22
|
+
* düşüyor. On binlerce yol için `withDataCache` kullanılır.
|
|
23
|
+
*
|
|
24
|
+
* Config yüklenmemiş olabilir (testler bu modülü doğrudan çağırıyor); o
|
|
25
|
+
* durumda kod varsayılanı geçerli.
|
|
26
|
+
*
|
|
27
|
+
* @returns {number}
|
|
28
|
+
*/
|
|
29
|
+
function maxEntries() {
|
|
30
|
+
try {
|
|
31
|
+
return getConfig().htmlMaxEntries;
|
|
32
|
+
} catch {
|
|
33
|
+
return DEFAULT_HTML_CACHE_MAX_ENTRIES;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
17
36
|
|
|
18
37
|
/**
|
|
19
38
|
* TTL dolduktan sonra eski HTML'in kaç TTL boyunca daha servis edilebileceği.
|
|
@@ -73,7 +92,8 @@ function write(key, value, ttlSeconds) {
|
|
|
73
92
|
staleUntil: now + ttlSeconds * 1000 * (1 + STALE_FACTOR),
|
|
74
93
|
});
|
|
75
94
|
|
|
76
|
-
|
|
95
|
+
const limit = maxEntries();
|
|
96
|
+
while (store.size > limit) {
|
|
77
97
|
const oldest = store.keys().next().value;
|
|
78
98
|
if (oldest === undefined) break;
|
|
79
99
|
store.delete(oldest);
|