jskelet 0.6.0 → 0.6.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.
- package/AGENTS.md +2 -3
- package/CHANGELOG.md +158 -103
- package/README.md +7 -14
- package/bin/jskelet.mjs +1 -1
- package/docs/02-mimari.md +6 -1
- package/docs/03-routing.md +3 -1
- package/docs/04-render-ve-sablonlar.md +25 -0
- package/docs/06-cache.md +18 -7
- package/docs/07-yapilandirma.md +29 -9
- package/docs/09-dev-araclari.md +17 -7
- package/docs/10-dagitim.md +8 -12
- package/docs/README.md +3 -28
- package/docs/en/02-architecture.md +7 -1
- package/docs/en/03-routing.md +3 -1
- package/docs/en/04-rendering.md +24 -0
- package/docs/en/06-caching.md +21 -7
- package/docs/en/07-configuration.md +29 -9
- package/docs/en/09-dev-tools.md +18 -7
- package/docs/en/10-deployment.md +8 -13
- package/docs/en/README.md +3 -30
- package/package.json +2 -2
- package/src/config/defaults.js +36 -3
- package/src/config/index.js +137 -26
- package/src/server/create-app.js +9 -0
- package/src/server/html-cache.js +178 -32
- package/src/server/middleware/dev-gate.js +21 -8
- package/src/server/middleware/robots-txt.js +341 -0
- package/src/server/prewarm.js +137 -51
- package/src/server/render.js +3 -1
- package/types/config/defaults.d.ts +31 -3
- package/types/config/index.d.ts +5 -0
- package/types/server/html-cache.d.ts +40 -6
- package/types/server/middleware/robots-txt.d.ts +33 -0
- package/types/server/prewarm.d.ts +6 -3
package/src/config/index.js
CHANGED
|
@@ -37,10 +37,15 @@ import {
|
|
|
37
37
|
DEFAULT_AUTH,
|
|
38
38
|
DEFAULT_BRAND,
|
|
39
39
|
DEFAULT_CLOUDFLARE,
|
|
40
|
+
DATA_CACHE_MAX_ENTRIES_CEILING,
|
|
40
41
|
DEFAULT_DATA_CACHE,
|
|
41
42
|
DEFAULT_DEV_GATE_BYPASS,
|
|
42
43
|
DEFAULT_DIRS,
|
|
43
44
|
DEFAULT_HTML_CACHE_MAX_ENTRIES,
|
|
45
|
+
HTML_CACHE_MAX_ENTRIES_CEILING,
|
|
46
|
+
ON_VISIT_CONCURRENCY_CEILING,
|
|
47
|
+
ON_VISIT_PER_PAGE_CEILING,
|
|
48
|
+
ON_VISIT_RPS_CEILING,
|
|
44
49
|
DEFAULT_IMAGES,
|
|
45
50
|
DEFAULT_LOGS,
|
|
46
51
|
DEFAULT_NAVIGATION,
|
|
@@ -134,6 +139,8 @@ const CONFIG_FILE = "jskelet.config.mjs";
|
|
|
134
139
|
* @property {string[] | null} routes Açık route modülü listesi.
|
|
135
140
|
* @property {boolean} trailingSlash URL'ler `/` ile bitsin mi (Next `trailingSlash`).
|
|
136
141
|
* @property {{ extensions: Set<string>, prefixes: string[] }} static
|
|
142
|
+
* @property {boolean} devGate `DEV_TOKEN` tek başına siteyi kilitlemez; gate
|
|
143
|
+
* ancak bu bayrak veya `DEV_GATE=1` ile açılır.
|
|
137
144
|
* @property {string[]} devGateBypass
|
|
138
145
|
* @property {string[]} preconnect
|
|
139
146
|
* @property {NavigationConfig} navigation
|
|
@@ -348,6 +355,22 @@ function normalizeUpstream(raw) {
|
|
|
348
355
|
};
|
|
349
356
|
}
|
|
350
357
|
|
|
358
|
+
/**
|
|
359
|
+
* Dev gate. `DEV_TOKEN` ortamda durması siteyi kilitlemez: paylaşılan bir
|
|
360
|
+
* task tanımı production'a da aynı değişkeni taşır ve herkese 404 olur.
|
|
361
|
+
* Gate ancak `devGate: true` ya da `DEV_GATE=1` ile açılır. `DEV_GATE=0`
|
|
362
|
+
* config'teki açığı da kapatır.
|
|
363
|
+
*
|
|
364
|
+
* @param {Record<string, any>} source
|
|
365
|
+
* @returns {boolean}
|
|
366
|
+
*/
|
|
367
|
+
function normalizeDevGate(source) {
|
|
368
|
+
const env = process.env.DEV_GATE;
|
|
369
|
+
if (env === "0" || env === "false") return false;
|
|
370
|
+
if (env === "1" || env === "true") return true;
|
|
371
|
+
return source.devGate === true;
|
|
372
|
+
}
|
|
373
|
+
|
|
351
374
|
/**
|
|
352
375
|
* Yönetim paneli. `enabled` yalnızca açıkça `true` verildiğinde ya da
|
|
353
376
|
* `JSKELET_ADMIN` ortam değişkeni ayarlandığında açılır: paneli yanlışlıkla
|
|
@@ -701,16 +724,24 @@ function normalizeCache(raw) {
|
|
|
701
724
|
const prewarm = normalizePrewarm(raw?.prewarm);
|
|
702
725
|
const queryRules = normalizeQueryRules(raw?.query);
|
|
703
726
|
const maxEntries = Number(raw?.maxEntries);
|
|
727
|
+
let htmlMaxEntries =
|
|
728
|
+
Number.isFinite(maxEntries) && maxEntries > 0
|
|
729
|
+
? Math.floor(maxEntries)
|
|
730
|
+
: DEFAULT_HTML_CACHE_MAX_ENTRIES;
|
|
731
|
+
if (htmlMaxEntries > HTML_CACHE_MAX_ENTRIES_CEILING) {
|
|
732
|
+
console.warn(
|
|
733
|
+
`[config] cache().maxEntries ${htmlMaxEntries} exceeds the ceiling of ` +
|
|
734
|
+
`${HTML_CACHE_MAX_ENTRIES_CEILING}; using ${HTML_CACHE_MAX_ENTRIES_CEILING}`,
|
|
735
|
+
);
|
|
736
|
+
htmlMaxEntries = HTML_CACHE_MAX_ENTRIES_CEILING;
|
|
737
|
+
}
|
|
704
738
|
|
|
705
739
|
return {
|
|
706
740
|
html,
|
|
707
741
|
cacheQuery: queryRules,
|
|
708
742
|
cacheVary: normalizeVary(raw?.vary),
|
|
709
|
-
htmlMaxEntries
|
|
710
|
-
|
|
711
|
-
? Math.floor(maxEntries)
|
|
712
|
-
: DEFAULT_HTML_CACHE_MAX_ENTRIES,
|
|
713
|
-
data: { ...DEFAULT_DATA_CACHE, ...(raw?.data ?? {}) },
|
|
743
|
+
htmlMaxEntries,
|
|
744
|
+
data: normalizeDataCache(raw?.data),
|
|
714
745
|
// Otomatik upstream izleme kapatılabilir olmalı: `fetch`i kendisi saran
|
|
715
746
|
// bir uygulama (ölçüm, retry, circuit breaker) çakışma yaşayabilir.
|
|
716
747
|
trackUpstream: raw?.trackUpstream !== false,
|
|
@@ -732,14 +763,110 @@ function normalizeCache(raw) {
|
|
|
732
763
|
};
|
|
733
764
|
}
|
|
734
765
|
|
|
766
|
+
/**
|
|
767
|
+
* İstenen pozitif tamsayı tavanı aşıyorsa uyarı basıp tavana çeker.
|
|
768
|
+
*
|
|
769
|
+
* @param {string} field
|
|
770
|
+
* @param {number} requested
|
|
771
|
+
* @param {number} ceiling
|
|
772
|
+
* @returns {number}
|
|
773
|
+
*/
|
|
774
|
+
function clampCeiling(field, requested, ceiling) {
|
|
775
|
+
if (requested <= ceiling) return requested;
|
|
776
|
+
console.warn(
|
|
777
|
+
`[config] ${field} ${requested} exceeds the ceiling of ${ceiling}; using ${ceiling}`,
|
|
778
|
+
);
|
|
779
|
+
return ceiling;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/**
|
|
783
|
+
* Veri önbelleği JSON tutar; sınır HTML'den yüksek olabilir ama sonsuz değil.
|
|
784
|
+
*
|
|
785
|
+
* @param {unknown} raw
|
|
786
|
+
* @returns {{ maxEntries: number, staleFactor: number }}
|
|
787
|
+
*/
|
|
788
|
+
function normalizeDataCache(raw) {
|
|
789
|
+
const source =
|
|
790
|
+
raw && typeof raw === "object" && !Array.isArray(raw)
|
|
791
|
+
? /** @type {Record<string, unknown>} */ (raw)
|
|
792
|
+
: {};
|
|
793
|
+
const requested = Number(source.maxEntries);
|
|
794
|
+
let maxEntries =
|
|
795
|
+
Number.isFinite(requested) && requested > 0
|
|
796
|
+
? Math.floor(requested)
|
|
797
|
+
: DEFAULT_DATA_CACHE.maxEntries;
|
|
798
|
+
maxEntries = clampCeiling(
|
|
799
|
+
"cache().data.maxEntries",
|
|
800
|
+
maxEntries,
|
|
801
|
+
DATA_CACHE_MAX_ENTRIES_CEILING,
|
|
802
|
+
);
|
|
803
|
+
|
|
804
|
+
const stale = Number(source.staleFactor);
|
|
805
|
+
return {
|
|
806
|
+
maxEntries,
|
|
807
|
+
staleFactor:
|
|
808
|
+
Number.isFinite(stale) && stale >= 0 ? stale : DEFAULT_DATA_CACHE.staleFactor,
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
/**
|
|
813
|
+
* onVisit sürekli çalışır. Klasik turdaki `rps: 0` (sınırsız) burada her
|
|
814
|
+
* ziyaretçide yeniden crawl demek; boş, `0` ve tavanın üstü 2'ye çekilir.
|
|
815
|
+
*
|
|
816
|
+
* @param {Record<string, unknown>} source
|
|
817
|
+
* @returns {{ enabled: boolean } & typeof DEFAULT_PREWARM_ON_VISIT}
|
|
818
|
+
*/
|
|
819
|
+
function resolveOnVisitLimits(source) {
|
|
820
|
+
const perPageRaw = Number(source.perPage);
|
|
821
|
+
const perPage = clampCeiling(
|
|
822
|
+
"cache().prewarm.onVisit.perPage",
|
|
823
|
+
Number.isFinite(perPageRaw) && perPageRaw > 0
|
|
824
|
+
? Math.floor(perPageRaw)
|
|
825
|
+
: DEFAULT_PREWARM_ON_VISIT.perPage,
|
|
826
|
+
ON_VISIT_PER_PAGE_CEILING,
|
|
827
|
+
);
|
|
828
|
+
|
|
829
|
+
const concurrencyRaw = Number(source.concurrency);
|
|
830
|
+
const concurrency = clampCeiling(
|
|
831
|
+
"cache().prewarm.onVisit.concurrency",
|
|
832
|
+
Number.isFinite(concurrencyRaw) && concurrencyRaw > 0
|
|
833
|
+
? Math.floor(concurrencyRaw)
|
|
834
|
+
: ON_VISIT_CONCURRENCY_CEILING,
|
|
835
|
+
ON_VISIT_CONCURRENCY_CEILING,
|
|
836
|
+
);
|
|
837
|
+
|
|
838
|
+
const rpsRaw = Number(source.rps);
|
|
839
|
+
let rps = ON_VISIT_RPS_CEILING;
|
|
840
|
+
if (source.rps != null && source.rps !== "") {
|
|
841
|
+
if (!Number.isFinite(rpsRaw) || rpsRaw <= 0) {
|
|
842
|
+
console.warn(
|
|
843
|
+
`[config] cache().prewarm.onVisit.rps ${source.rps} is not a positive rate; ` +
|
|
844
|
+
`using ${ON_VISIT_RPS_CEILING}`,
|
|
845
|
+
);
|
|
846
|
+
} else {
|
|
847
|
+
rps = clampCeiling(
|
|
848
|
+
"cache().prewarm.onVisit.rps",
|
|
849
|
+
rpsRaw,
|
|
850
|
+
ON_VISIT_RPS_CEILING,
|
|
851
|
+
);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
return {
|
|
856
|
+
...DEFAULT_PREWARM_ON_VISIT,
|
|
857
|
+
enabled: source.enabled !== false,
|
|
858
|
+
perPage,
|
|
859
|
+
concurrency,
|
|
860
|
+
rps,
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
|
|
735
864
|
/**
|
|
736
865
|
* @param {unknown} raw
|
|
737
866
|
* @returns {{ enabled: boolean } & typeof DEFAULT_PREWARM_ON_VISIT}
|
|
738
867
|
*/
|
|
739
868
|
function normalizeOnVisit(raw) {
|
|
740
|
-
if (raw === true) {
|
|
741
|
-
return { ...DEFAULT_PREWARM_ON_VISIT, enabled: true };
|
|
742
|
-
}
|
|
869
|
+
if (raw === true) return resolveOnVisitLimits({});
|
|
743
870
|
|
|
744
871
|
if (raw == null || raw === false) {
|
|
745
872
|
return { ...DEFAULT_PREWARM_ON_VISIT, enabled: false };
|
|
@@ -751,24 +878,7 @@ function normalizeOnVisit(raw) {
|
|
|
751
878
|
);
|
|
752
879
|
}
|
|
753
880
|
|
|
754
|
-
|
|
755
|
-
const perPage = Number(source.perPage);
|
|
756
|
-
const concurrency = Number(source.concurrency);
|
|
757
|
-
const rps = Number(source.rps);
|
|
758
|
-
|
|
759
|
-
return {
|
|
760
|
-
...DEFAULT_PREWARM_ON_VISIT,
|
|
761
|
-
enabled: source.enabled !== false,
|
|
762
|
-
perPage:
|
|
763
|
-
Number.isFinite(perPage) && perPage > 0
|
|
764
|
-
? Math.floor(perPage)
|
|
765
|
-
: DEFAULT_PREWARM_ON_VISIT.perPage,
|
|
766
|
-
concurrency:
|
|
767
|
-
Number.isFinite(concurrency) && concurrency > 0
|
|
768
|
-
? Math.floor(concurrency)
|
|
769
|
-
: null,
|
|
770
|
-
rps: Number.isFinite(rps) && rps >= 0 ? rps : null,
|
|
771
|
-
};
|
|
881
|
+
return resolveOnVisitLimits(/** @type {Record<string, unknown>} */ (raw));
|
|
772
882
|
}
|
|
773
883
|
|
|
774
884
|
/**
|
|
@@ -1255,6 +1365,7 @@ export async function loadConfig(options = {}) {
|
|
|
1255
1365
|
extensions: new Set(source.static?.extensions ?? DEFAULT_STATIC.extensions),
|
|
1256
1366
|
prefixes: source.static?.prefixes ?? DEFAULT_STATIC.prefixes,
|
|
1257
1367
|
},
|
|
1368
|
+
devGate: normalizeDevGate(source),
|
|
1258
1369
|
devGateBypass: source.devGateBypass ?? DEFAULT_DEV_GATE_BYPASS,
|
|
1259
1370
|
preconnect: source.preconnect ?? [],
|
|
1260
1371
|
navigation: normalizeNavigation(source.navigation, brand),
|
package/src/server/create-app.js
CHANGED
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
* açık kurallar istenen yolu önce görür.
|
|
13
13
|
* 3b. access log (açıksa) — tamamlanan yanıtların süresi; admin/prewarm
|
|
14
14
|
* içeride elenir.
|
|
15
|
+
* 3c. robots.txt — statikten önce, compression'ın içinde: kullanıcının
|
|
16
|
+
* gövdesine framework Disallow bloğu eklenir. Sıkıştırılmış kopyanın
|
|
17
|
+
* (Content-Encoding dolu) üzerine yazılmaz.
|
|
15
18
|
* 4. staticPrecompressed → express.static — build'de üretilmiş `.br`/`.gz`
|
|
16
19
|
* kopyalar varsa onlar servis edilir (kalite 11), yoksa istek altındaki
|
|
17
20
|
* static'e düşer ve middleware anında sıkıştırır (kalite 5).
|
|
@@ -32,6 +35,7 @@ import path from "node:path";
|
|
|
32
35
|
import process from "node:process";
|
|
33
36
|
import express from "express";
|
|
34
37
|
import { compression } from "./middleware/compression.js";
|
|
38
|
+
import { robotsTxtMiddleware } from "./middleware/robots-txt.js";
|
|
35
39
|
import { headersMiddleware } from "./middleware/headers.js";
|
|
36
40
|
import { csrf } from "./middleware/csrf.js";
|
|
37
41
|
import { staticPrecompressed } from "./middleware/static-precompressed.js";
|
|
@@ -109,6 +113,11 @@ export async function createApp(options = {}) {
|
|
|
109
113
|
app.use(accessLogMiddleware());
|
|
110
114
|
}
|
|
111
115
|
|
|
116
|
+
// Kullanıcının robots.txt'i statik dosya da olabilir, route da. İkisi de
|
|
117
|
+
// bu sarmalayıcıdan geçer. compression'dan sonra durur: ek, sıkıştırılmış
|
|
118
|
+
// baytların değil düz metnin sonuna yazılır.
|
|
119
|
+
app.use(robotsTxtMiddleware());
|
|
120
|
+
|
|
112
121
|
app.use(staticPrecompressed(config.dirs.public));
|
|
113
122
|
app.use(
|
|
114
123
|
express.static(config.dirs.public, {
|
package/src/server/html-cache.js
CHANGED
|
@@ -31,10 +31,13 @@
|
|
|
31
31
|
*/
|
|
32
32
|
|
|
33
33
|
import { getConfig } from "../config/index.js";
|
|
34
|
-
import {
|
|
34
|
+
import {
|
|
35
|
+
DEFAULT_HTML_CACHE_MAX_ENTRIES,
|
|
36
|
+
HTML_CACHE_BYTE_BUDGET,
|
|
37
|
+
} from "../config/defaults.js";
|
|
35
38
|
import { collectDependencies } from "./cache-deps.js";
|
|
36
39
|
import { compilePattern, matchPattern } from "../config/pattern.js";
|
|
37
|
-
import { pathOfCacheKey } from "./cache-vary.js";
|
|
40
|
+
import { pathOfCacheKey, publicHost } from "./cache-vary.js";
|
|
38
41
|
import {
|
|
39
42
|
cacheKey,
|
|
40
43
|
onCacheEvent,
|
|
@@ -118,6 +121,19 @@ const EARLY_SWEEP_INTERVAL_MS = 1000;
|
|
|
118
121
|
/** @type {Map<string, HtmlEntry>} */
|
|
119
122
|
const store = new Map();
|
|
120
123
|
|
|
124
|
+
/**
|
|
125
|
+
* `store` içindeki HTML string + sıkıştırılmış gövdelerin toplamı.
|
|
126
|
+
* Her `drop` / `install` bunu günceller; sıkıştırma sonradan eklendiğinde
|
|
127
|
+
* `noteHtmlCacheGrowth` artışı yazar. Tam tarama yapmamak için tutulur.
|
|
128
|
+
*/
|
|
129
|
+
let storedBytes = 0;
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Testler tahliyeyi küçük bir bütçeyle doğrular. `null` → üretim tavanı.
|
|
133
|
+
* @type {number | null}
|
|
134
|
+
*/
|
|
135
|
+
let byteBudgetOverride = null;
|
|
136
|
+
|
|
121
137
|
/** @type {Map<string, Promise<{ html: string, status: number }>>} */
|
|
122
138
|
const inflight = new Map();
|
|
123
139
|
|
|
@@ -237,8 +253,45 @@ function unlink(key, entry) {
|
|
|
237
253
|
}
|
|
238
254
|
|
|
239
255
|
/**
|
|
240
|
-
*
|
|
241
|
-
*
|
|
256
|
+
* Ham HTML + sıkıştırılmış gövdeler. Bayt bütçesi girdi sayısından bağımsız
|
|
257
|
+
* bu ağırlığa bakar.
|
|
258
|
+
*
|
|
259
|
+
* @param {HtmlEntry} entry
|
|
260
|
+
* @returns {number}
|
|
261
|
+
*/
|
|
262
|
+
function entryBytes(entry) {
|
|
263
|
+
let bytes = Buffer.byteLength(entry.html);
|
|
264
|
+
for (const buffer of entry.encoded.values()) bytes += buffer.length;
|
|
265
|
+
return bytes;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* @returns {number}
|
|
270
|
+
*/
|
|
271
|
+
function activeByteBudget() {
|
|
272
|
+
return byteBudgetOverride ?? HTML_CACHE_BYTE_BUDGET;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Sayı tavanı veya bayt bütçesi aşılınca en eski girdiden düşer. Tek başına
|
|
277
|
+
* bütçeyi aşan girdi de gider: yanıt o istekte zaten üretilmiştir, L1'de
|
|
278
|
+
* durması süreci şişirir.
|
|
279
|
+
*/
|
|
280
|
+
function evictOverflow() {
|
|
281
|
+
const limit = maxEntries();
|
|
282
|
+
const budget = activeByteBudget();
|
|
283
|
+
|
|
284
|
+
while (store.size > 0 && (store.size > limit || storedBytes > budget)) {
|
|
285
|
+
const oldest = store.keys().next().value;
|
|
286
|
+
if (oldest === undefined) break;
|
|
287
|
+
if (!drop(oldest)) break;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Store'dan silmenin **tek** yolu. Ters indeks ve bayt sayacı buraya bağlı;
|
|
293
|
+
* hiçbir yerde doğrudan `store.delete()` çağrılmaz. `read()` içindeki
|
|
294
|
+
* sil-yaz yalnızca LRU sırası içindir, sayacı değiştirmez.
|
|
242
295
|
*
|
|
243
296
|
* @param {string} key
|
|
244
297
|
* @returns {boolean} Girdi var mıydı.
|
|
@@ -248,6 +301,7 @@ function drop(key) {
|
|
|
248
301
|
if (!entry) return false;
|
|
249
302
|
|
|
250
303
|
unlink(key, entry);
|
|
304
|
+
storedBytes = Math.max(0, storedBytes - entryBytes(entry));
|
|
251
305
|
store.delete(key);
|
|
252
306
|
return true;
|
|
253
307
|
}
|
|
@@ -406,8 +460,9 @@ function write(key, value, ttlSeconds, deps = null, produceMs = DEFAULT_PRODUCE_
|
|
|
406
460
|
produceMs: normalizeProduceMs(produceMs),
|
|
407
461
|
};
|
|
408
462
|
|
|
409
|
-
|
|
410
|
-
|
|
463
|
+
// Bütçeye sığmayan sayfa Redis'e de yazılmaz: bir sonraki istek onu
|
|
464
|
+
// geri alıp yine reddeder, stringify ise o anki RSS'i şişirir.
|
|
465
|
+
if (install(key, entry)) share(key, entry);
|
|
411
466
|
}
|
|
412
467
|
|
|
413
468
|
/**
|
|
@@ -416,13 +471,20 @@ function write(key, value, ttlSeconds, deps = null, produceMs = DEFAULT_PRODUCE_
|
|
|
416
471
|
*
|
|
417
472
|
* @param {string} key
|
|
418
473
|
* @param {HtmlEntry} entry
|
|
474
|
+
* @returns {boolean} Girdi L1'e girdi mi.
|
|
419
475
|
*/
|
|
420
476
|
function install(key, entry) {
|
|
421
477
|
// Aynı anahtarın eski girdisi ters indekste kalmasın: bağımlılıklar
|
|
422
478
|
// tazelemeden tazelemeye değişebilir.
|
|
423
479
|
drop(key);
|
|
424
480
|
|
|
481
|
+
const weight = entryBytes(entry);
|
|
482
|
+
// Tek sayfa bütçeden büyükse saklama. Eski kopya da düştü; bu yanıt
|
|
483
|
+
// üreticinin döndürdüğü HTML ile gider, L1'e girmez.
|
|
484
|
+
if (weight > activeByteBudget()) return false;
|
|
485
|
+
|
|
425
486
|
store.set(key, entry);
|
|
487
|
+
storedBytes += weight;
|
|
426
488
|
|
|
427
489
|
for (const dep of entry.deps) {
|
|
428
490
|
let set = dependents.get(dep);
|
|
@@ -430,12 +492,35 @@ function install(key, entry) {
|
|
|
430
492
|
set.add(key);
|
|
431
493
|
}
|
|
432
494
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
495
|
+
evictOverflow();
|
|
496
|
+
return store.has(key);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Sıkıştırılmış gövde `install()`'dan sonra, ilk brotli/gzip yanıtında
|
|
501
|
+
* girdinin `encoded` haritasına eklenir. Sayacı delta ile büyütmek, o sıra
|
|
502
|
+
* LRU'dan düşmüş bir haritaya yazınca bir daha inmeyen bir artık bırakır;
|
|
503
|
+
* store'dan yeniden okumak o artığı taşımaz.
|
|
504
|
+
*
|
|
505
|
+
* @returns {void}
|
|
506
|
+
*/
|
|
507
|
+
export function noteHtmlCacheGrowth() {
|
|
508
|
+
let bytes = 0;
|
|
509
|
+
for (const entry of store.values()) bytes += entryBytes(entry);
|
|
510
|
+
storedBytes = bytes;
|
|
511
|
+
evictOverflow();
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Bellek freninin bayt tavanını geçici olarak değiştirir. Testler LRU
|
|
516
|
+
* tahliyesini küçük bir değerle doğrular; `null` üretim tavanına döner.
|
|
517
|
+
*
|
|
518
|
+
* @param {number | null} bytes
|
|
519
|
+
* @returns {void}
|
|
520
|
+
*/
|
|
521
|
+
export function setHtmlCacheByteBudget(bytes) {
|
|
522
|
+
byteBudgetOverride = bytes == null ? null : bytes;
|
|
523
|
+
evictOverflow();
|
|
439
524
|
}
|
|
440
525
|
|
|
441
526
|
/**
|
|
@@ -589,6 +674,7 @@ export function clearHtmlCache() {
|
|
|
589
674
|
*/
|
|
590
675
|
function clearLocal() {
|
|
591
676
|
store.clear();
|
|
677
|
+
storedBytes = 0;
|
|
592
678
|
dependents.clear();
|
|
593
679
|
tokens.clear();
|
|
594
680
|
invalidated.clear();
|
|
@@ -877,27 +963,65 @@ function dropLocalKey(key) {
|
|
|
877
963
|
}
|
|
878
964
|
|
|
879
965
|
/**
|
|
880
|
-
*
|
|
881
|
-
* boşaltır. Isıtma turu bunları başa alır; iki tur aynı yolu tekrar
|
|
882
|
-
* ısıtmasın diye okuma yıkıcıdır.
|
|
966
|
+
* Vary önekinden `h=` parçasını okur. Önek yoksa boş string.
|
|
883
967
|
*
|
|
884
|
-
*
|
|
885
|
-
*
|
|
968
|
+
* @param {string} key
|
|
969
|
+
* @returns {string}
|
|
970
|
+
*/
|
|
971
|
+
function hostOfCacheKey(key) {
|
|
972
|
+
const sep = key.indexOf("|/");
|
|
973
|
+
if (sep === -1) return "";
|
|
974
|
+
const prefix = key.slice(0, sep);
|
|
975
|
+
for (const part of prefix.split("&")) {
|
|
976
|
+
if (part.startsWith("h=")) return part.slice(2);
|
|
977
|
+
}
|
|
978
|
+
return "";
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
/**
|
|
982
|
+
* @param {string} key
|
|
983
|
+
* @returns {string}
|
|
984
|
+
*/
|
|
985
|
+
function pathWithQuery(key) {
|
|
986
|
+
const pathname = pathOfCacheKey(key);
|
|
987
|
+
const q = key.indexOf("?");
|
|
988
|
+
if (q === -1) return pathname;
|
|
989
|
+
const query = key.slice(q + 1);
|
|
990
|
+
return query ? `${pathname}?${query}` : pathname;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
/**
|
|
994
|
+
* Invalidate edilmiş yollar. Okuma yıkıcıdır; iki tur aynı yolu tekrar
|
|
995
|
+
* ısıtmasın. `onlyHost` verilirse başka host'ların anahtarları kuyrukta
|
|
996
|
+
* kalır — süre dolumu onları kendi host'uyla ısıtır, `127.0.0.1` anahtarı
|
|
997
|
+
* açılmaz.
|
|
886
998
|
*
|
|
887
|
-
* @
|
|
999
|
+
* @param {string} [onlyHost]
|
|
1000
|
+
* @returns {{ path: string, host: string }[]}
|
|
888
1001
|
*/
|
|
889
|
-
export function
|
|
1002
|
+
export function takeInvalidatedTargets(onlyHost) {
|
|
890
1003
|
if (!invalidated.size) return [];
|
|
891
1004
|
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
const
|
|
897
|
-
if (
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
}
|
|
1005
|
+
/** @type {{ path: string, host: string }[]} */
|
|
1006
|
+
const taken = [];
|
|
1007
|
+
|
|
1008
|
+
for (const key of [...invalidated]) {
|
|
1009
|
+
const host = hostOfCacheKey(key);
|
|
1010
|
+
if (onlyHost && host && host !== onlyHost) continue;
|
|
1011
|
+
invalidated.delete(key);
|
|
1012
|
+
taken.push({ path: pathWithQuery(key), host });
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
return taken;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
/**
|
|
1019
|
+
* Yol listesi. Vary öneki düşülür; host ayrımı `takeInvalidatedTargets`.
|
|
1020
|
+
*
|
|
1021
|
+
* @returns {string[]}
|
|
1022
|
+
*/
|
|
1023
|
+
export function takeInvalidatedPaths() {
|
|
1024
|
+
return takeInvalidatedTargets().map((target) => target.path);
|
|
901
1025
|
}
|
|
902
1026
|
|
|
903
1027
|
/**
|
|
@@ -926,14 +1050,36 @@ export function getHtmlCacheEntries() {
|
|
|
926
1050
|
* Yol (query'siz) için taze bir HTML girdisi var mı? Ziyaret ısıtması yalnızca
|
|
927
1051
|
* soğuk / bayat hedefleri kuyruğa alır; HIT'leri yeniden çekmez.
|
|
928
1052
|
*
|
|
1053
|
+
* Gerçek anahtar `h=host|/yol?` biçimindedir: düz `store.get(pathname)` hem
|
|
1054
|
+
* vary önekini hem sondaki `?` işaretini kaçırır ve sıcak sayfayı yeniden
|
|
1055
|
+
* ısıtır. `vary.host` açıkken yalnızca bu isteğin host'u sayılır; diğer
|
|
1056
|
+
* locale'in kopyası bu yolu sıcak yapmaz.
|
|
1057
|
+
*
|
|
929
1058
|
* @param {string} pathname
|
|
1059
|
+
* @param {{ headers?: Record<string, unknown>, get?: (name: string) => string | undefined }} [req]
|
|
930
1060
|
* @returns {boolean}
|
|
931
1061
|
*/
|
|
932
|
-
export function isHtmlCacheFresh(pathname) {
|
|
1062
|
+
export function isHtmlCacheFresh(pathname, req) {
|
|
933
1063
|
if (typeof pathname !== "string" || !pathname.startsWith("/")) return false;
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
1064
|
+
|
|
1065
|
+
let host = "";
|
|
1066
|
+
try {
|
|
1067
|
+
if (getConfig().cacheVary?.host && req) host = publicHost(req);
|
|
1068
|
+
} catch {
|
|
1069
|
+
// Config yokken (testler) önek aranmaz; yol eşleşmesi yeter.
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
const now = Date.now();
|
|
1073
|
+
for (const [key, entry] of store) {
|
|
1074
|
+
if (now >= entry.expiresAt) continue;
|
|
1075
|
+
if (pathOfCacheKey(key) !== pathname) continue;
|
|
1076
|
+
const q = key.indexOf("?");
|
|
1077
|
+
if (q !== -1 && key.slice(q + 1)) continue;
|
|
1078
|
+
if (host && hostOfCacheKey(key) !== host) continue;
|
|
1079
|
+
return true;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
return false;
|
|
937
1083
|
}
|
|
938
1084
|
|
|
939
1085
|
/**
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Yayına açılmamış bir ortamı gizler
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Yayına açılmamış bir ortamı gizler. Gate kapalıyken (varsayılan) `DEV_TOKEN`
|
|
3
|
+
* olsa bile istekler geçer: değişkenin task ortamında unutulması production'u
|
|
4
|
+
* kilitlemesin. Açmak için `devGate: true` ya da `DEV_GATE=1` gerekir; o zaman
|
|
5
|
+
* token taşımayan her isteğe 404 döner. 403 değil 404 — 403 ortamın var
|
|
6
|
+
* olduğunu doğrular, 404 hiç yokmuş gibi davranır.
|
|
5
7
|
*
|
|
6
8
|
* Token bir kez `?dev_token=…` ile gelirse çereze yazılır, böylece link
|
|
7
|
-
* paylaşımı yeterli olur.
|
|
8
|
-
* ve üretimde hiçbir maliyeti olmaz.
|
|
9
|
+
* paylaşımı yeterli olur. Token boşsa gate açık olsa da kimse kilitlenmez.
|
|
9
10
|
*/
|
|
10
11
|
import process from "node:process";
|
|
11
12
|
import { getConfig } from "../../config/index.js";
|
|
@@ -32,13 +33,25 @@ function readCookie(req, name) {
|
|
|
32
33
|
|
|
33
34
|
/** @returns {import('express').RequestHandler} */
|
|
34
35
|
export function devGate() {
|
|
35
|
-
const { devGateBypass, brand } = getConfig();
|
|
36
|
+
const { devGate: enabled, devGateBypass, brand } = getConfig();
|
|
36
37
|
const bypass = new Set(devGateBypass);
|
|
37
38
|
const cookieName = brand.devTokenCookie;
|
|
39
|
+
const devToken = process.env.DEV_TOKEN;
|
|
40
|
+
|
|
41
|
+
// Unutulmuş bir değişken sessizce herkese 404 kesmesin; operatör görsün.
|
|
42
|
+
if (!enabled && devToken) {
|
|
43
|
+
console.warn(
|
|
44
|
+
"[dev-gate] DEV_TOKEN is set but the gate is off, so the site stays public. " +
|
|
45
|
+
"Set devGate: true or DEV_GATE=1 to require the token.",
|
|
46
|
+
);
|
|
47
|
+
} else if (enabled && !devToken) {
|
|
48
|
+
console.warn(
|
|
49
|
+
"[dev-gate] the gate is on but DEV_TOKEN is empty; requests are not blocked.",
|
|
50
|
+
);
|
|
51
|
+
}
|
|
38
52
|
|
|
39
53
|
return (req, res, next) => {
|
|
40
|
-
|
|
41
|
-
if (!devToken) return next();
|
|
54
|
+
if (!enabled || !devToken) return next();
|
|
42
55
|
if (bypass.has(req.path)) return next();
|
|
43
56
|
|
|
44
57
|
const fromQuery = req.query?.[cookieName];
|