jskelet 0.4.6 → 0.4.8
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 +10 -0
- package/docs/06-cache.md +64 -10
- package/docs/07-yapilandirma.md +24 -1
- package/docs/09-dev-araclari.md +11 -5
- package/docs/en/06-caching.md +65 -13
- package/docs/en/07-configuration.md +24 -1
- package/docs/en/09-dev-tools.md +11 -6
- package/package.json +1 -1
- package/src/client/devtools/overlay.js +194 -5
- package/src/client/devtools/report.js +20 -0
- package/src/client/registry.js +8 -0
- package/src/config/defaults.js +40 -1
- package/src/config/index.js +106 -1
- package/src/http/request-context.js +4 -1
- package/src/index.js +1 -1
- package/src/server/dev/devtools.js +118 -5
- package/src/server/dev/report.js +131 -15
- package/src/server/html-cache.js +14 -0
- package/src/server/prewarm.js +268 -19
- package/src/server/render.js +22 -3
package/src/server/prewarm.js
CHANGED
|
@@ -1,31 +1,43 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Sunucu açılışında sayfaları önden render edip HTML
|
|
2
|
+
* Sunucu açılışında veya ziyaret sırasında sayfaları önden render edip HTML
|
|
3
|
+
* cache'ini doldurur.
|
|
4
|
+
*
|
|
5
|
+
* İki karşılıklı dışlayan mod:
|
|
6
|
+
*
|
|
7
|
+
* 1. **Klasik** — `hooks.prewarmPaths()` listesi, açılış/`intervalSeconds`
|
|
8
|
+
* turları, `priority` / `rotate` / `max`.
|
|
9
|
+
* 2. **onVisit** — herkese açık bir sayfa servis edilince HTML'deki
|
|
10
|
+
* aynı-origin linkler kuyruğa alınır; tıklanabilir komşular ısınır.
|
|
3
11
|
*
|
|
4
12
|
* Next'teki build-time prerender'ın karşılığı, ama çıktı diske yazılmaz:
|
|
5
13
|
* HTML cache süreç belleğinde yaşadığı için ısıtma da süreç ayağa kalkınca
|
|
6
|
-
*
|
|
7
|
-
* dondurulmaz: her girdi route'un `revalidate` süresiyle yaşlanır ve
|
|
8
|
-
* stale-while-revalidate ile arkada tazelenir.
|
|
14
|
+
* (veya trafik geldikçe) yapılır.
|
|
9
15
|
*
|
|
10
16
|
* Isıtma gerçek HTTP istekleriyle yapılır: cache anahtarı, sıkıştırma ve
|
|
11
|
-
* middleware zinciri normal trafikle bire bir aynı olsun.
|
|
12
|
-
* ısıtılacağını uygulama `hooks.prewarmPaths()` ile bildirir; genelde
|
|
13
|
-
* sitemap üreten fonksiyonun aynısıdır.
|
|
14
|
-
*
|
|
15
|
-
* On binlerce yolluk bir sitede tur bir "damla damla" tarayıcıya dönüşür:
|
|
16
|
-
* `priority` desenleri her turda başa alınır, geri kalan kuyruk turlar
|
|
17
|
-
* arasında kaldığı yerden devam eder (`rotate`) ve `rps` toplam hızı upstream
|
|
18
|
-
* kotasının altında tutar. Amaç, kimse gelmese bile hiçbir sayfanın soğuk
|
|
19
|
-
* kalmaması — ama bunu API'yi düşürmeden yapmak.
|
|
17
|
+
* middleware zinciri normal trafikle bire bir aynı olsun.
|
|
20
18
|
*/
|
|
21
19
|
import process from "node:process";
|
|
22
20
|
import { getConfig, hook } from "../config/index.js";
|
|
23
21
|
import { getRequestContext } from "../http/request-context.js";
|
|
24
|
-
import { takeInvalidatedPaths } from "./html-cache.js";
|
|
22
|
+
import { isHtmlCacheFresh, takeInvalidatedPaths } from "./html-cache.js";
|
|
25
23
|
import { getDataCacheStats } from "./data-cache.js";
|
|
26
24
|
import { isTransientStatus } from "./upstream-tracking.js";
|
|
27
25
|
import { upstreamCooldownMs } from "./upstream-limiter.js";
|
|
28
26
|
|
|
27
|
+
/** Klasik turu yöneten env'ler; `onVisit` ile birlikte yasak. */
|
|
28
|
+
const CLASSIC_PREWARM_ENV = [
|
|
29
|
+
"PREWARM_MAX",
|
|
30
|
+
"PREWARM_INTERVAL_SECONDS",
|
|
31
|
+
"PREWARM_DELAY_MS",
|
|
32
|
+
"PREWARM_RETRY_DELAY_MS",
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* onVisit turu sürerken concurrency/rps. Klasik moda dokunmaz (`null`).
|
|
37
|
+
* @type {{ concurrency: number | null, rps: number | null }}
|
|
38
|
+
*/
|
|
39
|
+
const visitWarmSettings = { concurrency: null, rps: null };
|
|
40
|
+
|
|
29
41
|
/**
|
|
30
42
|
* Isıtmanın canlı durumu. Dev araçları bunu okuyup ilerlemeyi gösterir;
|
|
31
43
|
* üretimde kimse okumazsa da maliyeti bir nesnedir.
|
|
@@ -127,6 +139,15 @@ function num(value, fallback) {
|
|
|
127
139
|
* @returns {number}
|
|
128
140
|
*/
|
|
129
141
|
function setting(envKey, configKey, fallback) {
|
|
142
|
+
if (
|
|
143
|
+
visitWarmSettings.concurrency != null &&
|
|
144
|
+
configKey === "concurrency"
|
|
145
|
+
) {
|
|
146
|
+
return visitWarmSettings.concurrency;
|
|
147
|
+
}
|
|
148
|
+
if (visitWarmSettings.rps != null && configKey === "rps") {
|
|
149
|
+
return visitWarmSettings.rps;
|
|
150
|
+
}
|
|
130
151
|
return num(process.env[envKey], num(getConfig().prewarm?.[configKey], fallback));
|
|
131
152
|
}
|
|
132
153
|
|
|
@@ -421,13 +442,18 @@ export async function prewarm({ origin, quiet = false, paths: only }) {
|
|
|
421
442
|
|
|
422
443
|
// Dev'de tek işçi: tarama, o an tarayıcıda açtığın sayfanın render'ıyla CPU
|
|
423
444
|
// için yarışmasın.
|
|
424
|
-
const concurrency = setting(
|
|
445
|
+
const concurrency = setting(
|
|
446
|
+
"PREWARM_CONCURRENCY",
|
|
447
|
+
"concurrency",
|
|
448
|
+
isDev ? 1 : 4,
|
|
449
|
+
);
|
|
425
450
|
|
|
426
451
|
// Render tek bir olay döngüsünde çalışıyor: aralıksız bir tur, geliştirme
|
|
427
452
|
// sırasında sayfa isteklerini ve dev panelinin kanalını arkasında bekletiyor.
|
|
428
453
|
// Dev'de varsayılan bir hız freni bu yüzden var; üretimde ısıtma bir kez
|
|
429
454
|
// olup bittiği için fren yalnızca istenirse (`prewarm.rps`) devreye girer.
|
|
430
|
-
|
|
455
|
+
// onVisit turlarında `visitWarmSettings.rps` `setting()` üzerinden iner.
|
|
456
|
+
const rps = setting("PREWARM_RPS", "rps", isDev ? 4 : 0);
|
|
431
457
|
const pace = createPacer(rps);
|
|
432
458
|
|
|
433
459
|
const all = only?.length ? only : await collectPaths();
|
|
@@ -554,8 +580,213 @@ export async function prewarm({ origin, quiet = false, paths: only }) {
|
|
|
554
580
|
}
|
|
555
581
|
|
|
556
582
|
/**
|
|
557
|
-
*
|
|
558
|
-
*
|
|
583
|
+
* Speculation Rules `href_matches` benzeri dışlama: tam yol veya `/*` öneki.
|
|
584
|
+
*
|
|
585
|
+
* @param {string} pathname
|
|
586
|
+
* @param {string[]} patterns
|
|
587
|
+
* @returns {boolean}
|
|
588
|
+
*/
|
|
589
|
+
function matchesHrefExclude(pathname, patterns) {
|
|
590
|
+
for (const pattern of patterns) {
|
|
591
|
+
if (typeof pattern !== "string" || !pattern) continue;
|
|
592
|
+
if (pattern.endsWith("/*")) {
|
|
593
|
+
const base = pattern.slice(0, -2);
|
|
594
|
+
if (pathname === base || pathname.startsWith(`${base}/`)) return true;
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
if (pathname === pattern) return true;
|
|
598
|
+
}
|
|
599
|
+
return false;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* HTML içindeki aynı-origin `<a href>` yollarını DOM sırasıyla (üstten alta)
|
|
604
|
+
* toplar. Speculation Rules ile aynı muafiyetler: `nofollow`, `_blank`,
|
|
605
|
+
* `data-no-prefetch`, `prewarmSkip`, `navigation.exclude`.
|
|
606
|
+
*
|
|
607
|
+
* @param {string} html
|
|
608
|
+
* @param {{ limit?: number, basePath?: string }} [options]
|
|
609
|
+
* @returns {string[]}
|
|
610
|
+
*/
|
|
611
|
+
export function extractSameOriginLinks(html, options = {}) {
|
|
612
|
+
if (typeof html !== "string" || !html) return [];
|
|
613
|
+
|
|
614
|
+
const limit = Math.max(1, Math.floor(Number(options.limit) || 20));
|
|
615
|
+
const { prewarmSkip, navigation } = getConfig();
|
|
616
|
+
const exclude = navigation?.exclude ?? [];
|
|
617
|
+
|
|
618
|
+
/** @type {string[]} */
|
|
619
|
+
const links = [];
|
|
620
|
+
const seen = new Set();
|
|
621
|
+
const tagRe = /<a\b([^>]*)>/gi;
|
|
622
|
+
let match;
|
|
623
|
+
|
|
624
|
+
while ((match = tagRe.exec(html)) !== null && links.length < limit) {
|
|
625
|
+
const attrs = match[1];
|
|
626
|
+
if (/\btarget\s*=\s*(?:"_blank"|'_blank'|_blank)(?=[\s>]|$)/i.test(attrs)) {
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
629
|
+
if (/\bdata-no-prefetch\b/i.test(attrs)) continue;
|
|
630
|
+
|
|
631
|
+
const relMatch = attrs.match(
|
|
632
|
+
/\brel\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/i,
|
|
633
|
+
);
|
|
634
|
+
const rel = relMatch?.[1] ?? relMatch?.[2] ?? relMatch?.[3] ?? "";
|
|
635
|
+
if (/\bnofollow\b/i.test(rel)) continue;
|
|
636
|
+
|
|
637
|
+
const hrefMatch = attrs.match(
|
|
638
|
+
/\bhref\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/i,
|
|
639
|
+
);
|
|
640
|
+
const href = hrefMatch?.[1] ?? hrefMatch?.[2] ?? hrefMatch?.[3];
|
|
641
|
+
if (!href) continue;
|
|
642
|
+
|
|
643
|
+
const path = normalizeWarmPath(href, options.basePath);
|
|
644
|
+
if (!path) continue;
|
|
645
|
+
if (prewarmSkip.some((prefix) => path.startsWith(prefix))) continue;
|
|
646
|
+
if (matchesHrefExclude(path, exclude)) continue;
|
|
647
|
+
if (seen.has(path)) continue;
|
|
648
|
+
|
|
649
|
+
seen.add(path);
|
|
650
|
+
links.push(path);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
return links;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* @param {string} href
|
|
658
|
+
* @param {string} [basePath] Ziyaret edilen sayfa; göreli href çözümü için.
|
|
659
|
+
* @returns {string | null}
|
|
660
|
+
*/
|
|
661
|
+
function normalizeWarmPath(href, basePath = "/") {
|
|
662
|
+
const trimmed = href.trim();
|
|
663
|
+
if (!trimmed || trimmed.startsWith("#")) return null;
|
|
664
|
+
if (/^(mailto|tel|javascript|data):/i.test(trimmed)) return null;
|
|
665
|
+
|
|
666
|
+
try {
|
|
667
|
+
const basePathname = basePath.startsWith("/") ? basePath : `/${basePath}`;
|
|
668
|
+
const url = new URL(trimmed, `https://warm.invalid${basePathname}`);
|
|
669
|
+
// Dış origin veya `https://…` mutlak linkler elenir; yalnızca site-içi
|
|
670
|
+
// path / göreli href ısınır.
|
|
671
|
+
if (url.origin !== "https://warm.invalid") return null;
|
|
672
|
+
return url.pathname || "/";
|
|
673
|
+
} catch {
|
|
674
|
+
return null;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/** @type {string | null} */
|
|
679
|
+
let visitOrigin = null;
|
|
680
|
+
/** @type {string[]} */
|
|
681
|
+
const visitPending = [];
|
|
682
|
+
/** @type {Set<string>} */
|
|
683
|
+
const visitQueued = new Set();
|
|
684
|
+
let visitDraining = false;
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Ziyaret ısıtması açık mı? `PREWARM=0` her iki modu da keser.
|
|
688
|
+
*
|
|
689
|
+
* @returns {boolean}
|
|
690
|
+
*/
|
|
691
|
+
export function isOnVisitPrewarm() {
|
|
692
|
+
if (process.env.PREWARM === "0") return false;
|
|
693
|
+
try {
|
|
694
|
+
return getConfig().prewarm?.onVisit?.enabled === true;
|
|
695
|
+
} catch {
|
|
696
|
+
return false;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* Yanıt gövdesindeki linkleri soğuksa kuyruğa alır. İstek yolunu bloklamamak
|
|
702
|
+
* için `route()` bunu `queueMicrotask` ile çağırır.
|
|
703
|
+
*
|
|
704
|
+
* @param {string} html
|
|
705
|
+
* @param {{ path: string, req?: { get?: (name: string) => string | undefined,
|
|
706
|
+
* headers?: Record<string, unknown> } }} context
|
|
707
|
+
* @returns {void}
|
|
708
|
+
*/
|
|
709
|
+
export function noteVisitWarm(html, context) {
|
|
710
|
+
if (!isOnVisitPrewarm() || !visitOrigin) return;
|
|
711
|
+
if (!context?.path || typeof html !== "string") return;
|
|
712
|
+
|
|
713
|
+
const ua =
|
|
714
|
+
context.req?.get?.("user-agent") ??
|
|
715
|
+
/** @type {string | undefined} */ (context.req?.headers?.["user-agent"]);
|
|
716
|
+
if (ua && ua === getConfig().brand.prewarmUserAgent) return;
|
|
717
|
+
|
|
718
|
+
const perPage = Number(getConfig().prewarm?.onVisit?.perPage) || 20;
|
|
719
|
+
const links = extractSameOriginLinks(html, {
|
|
720
|
+
limit: perPage,
|
|
721
|
+
basePath: context.path,
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
for (const path of links) {
|
|
725
|
+
if (path === context.path) continue;
|
|
726
|
+
if (isHtmlCacheFresh(path)) continue;
|
|
727
|
+
if (visitQueued.has(path)) continue;
|
|
728
|
+
visitQueued.add(path);
|
|
729
|
+
visitPending.push(path);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
enqueueInvalidatedForVisit();
|
|
733
|
+
if (visitPending.length) void drainVisitWarm();
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/**
|
|
737
|
+
* Invalidate edilmiş yolları da kuyruğa alır (klasik turdaki
|
|
738
|
+
* `takeInvalidatedPaths` karşılığı).
|
|
739
|
+
*
|
|
740
|
+
* @returns {void}
|
|
741
|
+
*/
|
|
742
|
+
function enqueueInvalidatedForVisit() {
|
|
743
|
+
for (const path of takeInvalidatedPaths()) {
|
|
744
|
+
if (visitQueued.has(path)) continue;
|
|
745
|
+
visitQueued.add(path);
|
|
746
|
+
visitPending.push(path);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
async function drainVisitWarm() {
|
|
751
|
+
if (visitDraining || !visitOrigin) return;
|
|
752
|
+
visitDraining = true;
|
|
753
|
+
|
|
754
|
+
const onVisit = getConfig().prewarm?.onVisit ?? {};
|
|
755
|
+
|
|
756
|
+
try {
|
|
757
|
+
while (visitPending.length) {
|
|
758
|
+
enqueueInvalidatedForVisit();
|
|
759
|
+
|
|
760
|
+
const batch = visitPending.splice(0, 32);
|
|
761
|
+
for (const path of batch) visitQueued.delete(path);
|
|
762
|
+
|
|
763
|
+
const cold = batch.filter((path) => !isHtmlCacheFresh(path));
|
|
764
|
+
if (!cold.length) continue;
|
|
765
|
+
|
|
766
|
+
// Klasik `prewarm()` turunu yeniden kullan: retry, progress, UA aynı.
|
|
767
|
+
// onVisit.concurrency / rps verilmişse `visitWarmSettings` ile iner;
|
|
768
|
+
// yoksa `prewarm()` kendi (env → config → isDev) zincirine düşer.
|
|
769
|
+
visitWarmSettings.concurrency = onVisit.concurrency;
|
|
770
|
+
visitWarmSettings.rps = onVisit.rps;
|
|
771
|
+
|
|
772
|
+
try {
|
|
773
|
+
await prewarm({ origin: visitOrigin, paths: cold, quiet: true });
|
|
774
|
+
} catch (error) {
|
|
775
|
+
console.error("[prewarm] onVisit warm failed", error);
|
|
776
|
+
} finally {
|
|
777
|
+
visitWarmSettings.concurrency = null;
|
|
778
|
+
visitWarmSettings.rps = null;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
} finally {
|
|
782
|
+
visitDraining = false;
|
|
783
|
+
if (visitPending.length) void drainVisitWarm();
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/**
|
|
788
|
+
* Açılışta ısıtmayı tetikler. `listen` geri çağrısından çağrılır.
|
|
789
|
+
* `onVisit` modunda zamanlayıcı yok: yalnızca origin kaydı ve kuyruk.
|
|
559
790
|
*
|
|
560
791
|
* @param {{ port: number }} options
|
|
561
792
|
* @returns {void}
|
|
@@ -563,12 +794,30 @@ export async function prewarm({ origin, quiet = false, paths: only }) {
|
|
|
563
794
|
export function startPrewarm({ port }) {
|
|
564
795
|
const config = getConfig();
|
|
565
796
|
if (process.env.PREWARM === "0") return;
|
|
797
|
+
|
|
798
|
+
const origin = `http://127.0.0.1:${port}`;
|
|
799
|
+
|
|
800
|
+
if (config.prewarm?.onVisit?.enabled) {
|
|
801
|
+
const classicEnv = CLASSIC_PREWARM_ENV.filter((key) => process.env[key]);
|
|
802
|
+
if (classicEnv.length) {
|
|
803
|
+
throw new Error(
|
|
804
|
+
"[prewarm] onVisit mode cannot be used with " +
|
|
805
|
+
`${classicEnv.join(", ")}. Those env vars belong to classic prewarm.`,
|
|
806
|
+
);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
visitOrigin = origin;
|
|
810
|
+
console.log(
|
|
811
|
+
`[prewarm] onVisit mode — warming links from each public page response`,
|
|
812
|
+
);
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
|
|
566
816
|
if (process.env.PREWARM !== "1" && config.prewarm?.enabled === false) return;
|
|
567
817
|
// Isıtacak yol bildirmeyen bir projede zamanlayıcı kurmanın anlamı yok.
|
|
568
818
|
if (typeof config.hooks?.prewarmPaths !== "function") return;
|
|
569
819
|
|
|
570
820
|
const isDev = process.env.NODE_ENV === "development";
|
|
571
|
-
const origin = `http://127.0.0.1:${port}`;
|
|
572
821
|
|
|
573
822
|
// Hız frenli bir tur `intervalSeconds`'tan uzun sürebilir; üst üste binen
|
|
574
823
|
// turlar `prewarmProgress`'i bozar ve upstream'e iki kat yük bindirir.
|
package/src/server/render.js
CHANGED
|
@@ -41,7 +41,7 @@ import * as html from "../views/helpers/html.js";
|
|
|
41
41
|
import * as tags from "../views/helpers/tags.js";
|
|
42
42
|
import { loadComponents } from "../views/components/loader.js";
|
|
43
43
|
import { renderStatusPage } from "./status-page.js";
|
|
44
|
-
import { suppressForPrewarm } from "./prewarm.js";
|
|
44
|
+
import { suppressForPrewarm, noteVisitWarm } from "./prewarm.js";
|
|
45
45
|
import {
|
|
46
46
|
ensureTemplatesCompiled,
|
|
47
47
|
getComponentDirs,
|
|
@@ -304,7 +304,11 @@ export function route(controller, options = {}) {
|
|
|
304
304
|
const isPrivate = options.private === true;
|
|
305
305
|
|
|
306
306
|
return async (req, res, next) => {
|
|
307
|
-
const context = createRequestContext({
|
|
307
|
+
const context = createRequestContext({
|
|
308
|
+
private: isPrivate,
|
|
309
|
+
res,
|
|
310
|
+
pathname: req.path,
|
|
311
|
+
});
|
|
308
312
|
const ctx = {
|
|
309
313
|
params: req.params ?? {},
|
|
310
314
|
query: req.query ?? {},
|
|
@@ -395,6 +399,17 @@ export function route(controller, options = {}) {
|
|
|
395
399
|
// ETag kişiye özel HTML için kullanıcıya özgü bir doğrulayıcıdır ve
|
|
396
400
|
// `no-store` ile birlikte hiçbir işe yaramaz; üretilmesi engellenir.
|
|
397
401
|
await sendHtml(req, res, result.html, result.encoded, { etag: publicCache });
|
|
402
|
+
|
|
403
|
+
// onVisit: yanıt gittikten sonra linkleri kuyruğa al — TTFB'yi şişirmez.
|
|
404
|
+
// Yalnızca herkese açık, önbelleklenebilir 200 HTML; private / degraded
|
|
405
|
+
// sayfadaki linkler kişiye özel veya eksik olabilir.
|
|
406
|
+
if (publicCache && result.status === 200 && result.html) {
|
|
407
|
+
const pagePath = req.path;
|
|
408
|
+
const pageHtml = result.html;
|
|
409
|
+
queueMicrotask(() => {
|
|
410
|
+
noteVisitWarm(pageHtml, { path: pagePath, req });
|
|
411
|
+
});
|
|
412
|
+
}
|
|
398
413
|
} catch (error) {
|
|
399
414
|
if (isRedirectError(error)) {
|
|
400
415
|
// Oturuma bağlı bir yönlendirme de kişiye özeldir: "giriş yapmalısın"
|
|
@@ -431,7 +446,11 @@ export function route(controller, options = {}) {
|
|
|
431
446
|
*/
|
|
432
447
|
export function fragment(controller) {
|
|
433
448
|
return async (req, res, next) => {
|
|
434
|
-
const context = createRequestContext({
|
|
449
|
+
const context = createRequestContext({
|
|
450
|
+
private: true,
|
|
451
|
+
res,
|
|
452
|
+
pathname: req.path,
|
|
453
|
+
});
|
|
435
454
|
const ctx = {
|
|
436
455
|
params: req.params ?? {},
|
|
437
456
|
query: req.query ?? {},
|