akanjs 2.2.12 → 2.2.13-rc.1
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/client/csrTypes.ts +37 -6
- package/client/makePageProto.tsx +8 -8
- package/client/router.ts +5 -2
- package/fetch/requestStorage.ts +41 -11
- package/package.json +1 -1
- package/server/akanApp.ts +55 -0
- package/server/cachePolicy.ts +192 -0
- package/server/metadata.tsx +114 -0
- package/server/routeElementComposer.tsx +21 -1
- package/server/routeTreeBuilder.ts +44 -5
- package/server/rscClient.tsx +127 -50
- package/server/rscHttp.ts +120 -0
- package/server/rscNavigationState.ts +95 -0
- package/server/rscWorker.tsx +318 -121
- package/server/rscWorkerHost.ts +281 -66
- package/server/rscWorkerReplay.ts +40 -0
- package/server/ssrFromRscRenderer.tsx +462 -77
- package/server/ssrTypes.ts +11 -1
- package/server/webRouter.ts +173 -88
- package/service/ipcTypes.ts +1 -0
- package/types/client/csrTypes.d.ts +37 -6
- package/types/dictionary/base.dictionary.d.ts +1 -1
- package/types/dictionary/dictionary.d.ts +8 -8
- package/types/fetch/requestStorage.d.ts +16 -6
- package/types/server/cachePolicy.d.ts +55 -0
- package/types/server/metadata.d.ts +13 -0
- package/types/server/routeElementComposer.d.ts +6 -1
- package/types/server/rscHttp.d.ts +16 -0
- package/types/server/rscNavigationState.d.ts +35 -0
- package/types/server/rscWorkerHost.d.ts +38 -0
- package/types/server/rscWorkerReplay.d.ts +29 -0
- package/types/server/ssrFromRscRenderer.d.ts +20 -1
- package/types/server/ssrTypes.d.ts +10 -1
- package/types/server/webRouter.d.ts +27 -1
- package/types/service/ipcTypes.d.ts +1 -0
- package/types/webkit/useCsrValues.d.ts +1 -1
- package/ui/Link/SsrLink.tsx +0 -2
- package/webkit/bootCsr.tsx +16 -2
package/server/webRouter.ts
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
Logger,
|
|
9
9
|
parseAkanI18nEnv,
|
|
10
10
|
} from "akanjs/common";
|
|
11
|
-
import { parseCookieHeader } from "akanjs/fetch";
|
|
11
|
+
import { type AkanRequestStore, createRequestStore, parseCookieHeader } from "akanjs/fetch";
|
|
12
12
|
import type { AkanMetricsReport } from "akanjs/service";
|
|
13
13
|
import {
|
|
14
14
|
type BuilderRpc,
|
|
@@ -17,12 +17,25 @@ import {
|
|
|
17
17
|
RouteSeedIndexStore,
|
|
18
18
|
RoutesManifestStore,
|
|
19
19
|
} from "./artifact";
|
|
20
|
+
import {
|
|
21
|
+
createRouteCacheEntry,
|
|
22
|
+
getClientFacingOrigin,
|
|
23
|
+
isPublicRouteCacheableRequest,
|
|
24
|
+
isRouteCachePathAllowed,
|
|
25
|
+
LruTtlCache,
|
|
26
|
+
normalizeRouteCacheTtl,
|
|
27
|
+
parsePositiveInt,
|
|
28
|
+
type RouteCacheEntry,
|
|
29
|
+
type RouteCacheRenderState,
|
|
30
|
+
resolveRouteCacheStoreTtl,
|
|
31
|
+
shouldStoreRouteCache,
|
|
32
|
+
} from "./cachePolicy";
|
|
20
33
|
import { DevHmrController } from "./hmr";
|
|
21
34
|
import { HMR_CLIENT_SCRIPT } from "./hmr/clientScript";
|
|
22
35
|
import type { HmrWsData, HmrWsHub } from "./hmr/wsHub";
|
|
23
36
|
import { ImageOptimizer } from "./imageOptimizer";
|
|
24
37
|
import { createDefaultRobotsTxt } from "./robots";
|
|
25
|
-
import { type RscRedirectMethod, type RscRedirectStatus, RscWorker } from "./rscWorkerHost";
|
|
38
|
+
import { type RscRedirectMethod, type RscRedirectStatus, type RscRenderResult, RscWorker } from "./rscWorkerHost";
|
|
26
39
|
import { createDefaultSitemapXml, getSitemapBasePath } from "./sitemap";
|
|
27
40
|
import { SsrFromRscRenderer } from "./ssrFromRscRenderer";
|
|
28
41
|
import { createSystemPageResponse, getSystemPageHomeHref } from "./systemPages";
|
|
@@ -57,6 +70,94 @@ export function createRscStreamResponse(stream: BodyInit, status = 200): Respons
|
|
|
57
70
|
});
|
|
58
71
|
}
|
|
59
72
|
|
|
73
|
+
export function createRscNotFoundFallbackResponse(): Response {
|
|
74
|
+
return createRscStreamResponse("0:null\n", 404);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function cacheHtmlWhileStreaming(
|
|
78
|
+
stream: ReadableStream<Uint8Array>,
|
|
79
|
+
onComplete: (html: string) => void,
|
|
80
|
+
options: { shouldCache?: () => boolean | Promise<boolean>; maxBodyBytes?: number | null } = {},
|
|
81
|
+
): ReadableStream<Uint8Array> {
|
|
82
|
+
const chunks: Uint8Array[] = [];
|
|
83
|
+
let byteLength = 0;
|
|
84
|
+
let exceededMaxBodyBytes = false;
|
|
85
|
+
const decoder = new TextDecoder();
|
|
86
|
+
|
|
87
|
+
return stream.pipeThrough(
|
|
88
|
+
new TransformStream<Uint8Array, Uint8Array>({
|
|
89
|
+
transform(chunk, controller) {
|
|
90
|
+
if (!exceededMaxBodyBytes) {
|
|
91
|
+
byteLength += chunk.byteLength;
|
|
92
|
+
if (options.maxBodyBytes && byteLength > options.maxBodyBytes) {
|
|
93
|
+
exceededMaxBodyBytes = true;
|
|
94
|
+
chunks.length = 0;
|
|
95
|
+
} else {
|
|
96
|
+
chunks.push(chunk.slice());
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
controller.enqueue(chunk);
|
|
100
|
+
},
|
|
101
|
+
async flush() {
|
|
102
|
+
if (exceededMaxBodyBytes) return;
|
|
103
|
+
const body = new Uint8Array(byteLength);
|
|
104
|
+
let offset = 0;
|
|
105
|
+
for (const chunk of chunks) {
|
|
106
|
+
body.set(chunk, offset);
|
|
107
|
+
offset += chunk.byteLength;
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
if (options.shouldCache && !(await options.shouldCache())) return;
|
|
111
|
+
onComplete(decoder.decode(body));
|
|
112
|
+
} catch {
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
}),
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function cancelStreamForHeadResponse(stream: ReadableStream<Uint8Array>, reason: unknown): void {
|
|
120
|
+
void stream.cancel(reason).catch(() => {
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function resolveHtmlRouteCacheStoreTtl(input: {
|
|
125
|
+
baseTtl: number;
|
|
126
|
+
workerCacheState: RouteCacheRenderState;
|
|
127
|
+
hostRequestStore: AkanRequestStore;
|
|
128
|
+
lateControl?: { type: "redirect" } | null;
|
|
129
|
+
}): number | null {
|
|
130
|
+
if (input.lateControl?.type === "redirect") return null;
|
|
131
|
+
const workerTtl = resolveRouteCacheStoreTtl(input.baseTtl, input.workerCacheState);
|
|
132
|
+
if (workerTtl === null) return null;
|
|
133
|
+
const hostCacheState = shouldStoreRouteCache({
|
|
134
|
+
policy: input.hostRequestStore.policy,
|
|
135
|
+
dynamicUsage: input.hostRequestStore.dynamicUsage,
|
|
136
|
+
});
|
|
137
|
+
return resolveRouteCacheStoreTtl(workerTtl, hostCacheState);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function isHtmlRouteCachePathAllowed(
|
|
141
|
+
pathname: string,
|
|
142
|
+
env: {
|
|
143
|
+
[key: string]: string | undefined;
|
|
144
|
+
AKAN_HTML_RESULT_CACHE_PATHS?: string;
|
|
145
|
+
AKAN_HTML_RESULT_CACHE_EXCLUDE_PATHS?: string;
|
|
146
|
+
} = process.env as Record<string, string | undefined>,
|
|
147
|
+
): boolean {
|
|
148
|
+
return isRouteCachePathAllowed(pathname, {
|
|
149
|
+
allow: env.AKAN_HTML_RESULT_CACHE_PATHS,
|
|
150
|
+
deny: env.AKAN_HTML_RESULT_CACHE_EXCLUDE_PATHS,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export async function createRscNavigationStreamResponse(
|
|
155
|
+
result: Extract<RscRenderResult, { type: "stream" }>,
|
|
156
|
+
): Promise<Response> {
|
|
157
|
+
|
|
158
|
+
return createRscStreamResponse(result.stream, result.status ?? 200);
|
|
159
|
+
}
|
|
160
|
+
|
|
60
161
|
export function normalizeRscTargetUrlForHostBasePath(
|
|
61
162
|
targetUrl: URL,
|
|
62
163
|
options: {
|
|
@@ -107,7 +208,6 @@ interface WebRouterOptions {
|
|
|
107
208
|
}
|
|
108
209
|
|
|
109
210
|
interface CachedHtmlResult {
|
|
110
|
-
expiresAt: number;
|
|
111
211
|
html: string;
|
|
112
212
|
}
|
|
113
213
|
|
|
@@ -128,7 +228,9 @@ export class WebRouter {
|
|
|
128
228
|
csr: 0,
|
|
129
229
|
image: 0,
|
|
130
230
|
};
|
|
131
|
-
readonly #htmlCache = new
|
|
231
|
+
readonly #htmlCache = new LruTtlCache<CachedHtmlResult>(
|
|
232
|
+
parsePositiveInt(process.env.AKAN_HTML_RESULT_CACHE_MAX_ENTRIES) ?? 100,
|
|
233
|
+
);
|
|
132
234
|
#htmlCacheHits = 0;
|
|
133
235
|
#htmlCacheMisses = 0;
|
|
134
236
|
#htmlCacheBypass = 0;
|
|
@@ -288,13 +390,14 @@ export class WebRouter {
|
|
|
288
390
|
});
|
|
289
391
|
const result = await this.#rsc.renderWithMeta(rscReq, {
|
|
290
392
|
clientManifest: manifest.clientManifest,
|
|
393
|
+
signal: req.signal,
|
|
291
394
|
});
|
|
292
395
|
if (result.type === "redirect")
|
|
293
396
|
return createRscRedirectResponse(result.location, result.method, result.status);
|
|
294
397
|
if (result.type === "not-found") return WebRouter.#rscNotFoundResponse();
|
|
295
398
|
if (result.status && result.status >= 500)
|
|
296
399
|
return this.#renderRscErrorResponse("__rsc", "Internal Server Error");
|
|
297
|
-
return
|
|
400
|
+
return createRscNavigationStreamResponse(result);
|
|
298
401
|
} catch (err) {
|
|
299
402
|
return this.#renderRscErrorResponse("__rsc", err);
|
|
300
403
|
}
|
|
@@ -377,8 +480,8 @@ export class WebRouter {
|
|
|
377
480
|
try {
|
|
378
481
|
this.#requestStats.fullSsr += 1;
|
|
379
482
|
const manifest = await this.#ensureRoute(url);
|
|
380
|
-
const
|
|
381
|
-
const cachedHtml =
|
|
483
|
+
const htmlCacheEntry = this.#getHtmlCacheEntry(req, url);
|
|
484
|
+
const cachedHtml = htmlCacheEntry ? this.#getCachedHtml(htmlCacheEntry.key) : null;
|
|
382
485
|
if (cachedHtml) {
|
|
383
486
|
return new Response(cachedHtml, {
|
|
384
487
|
headers: {
|
|
@@ -389,33 +492,70 @@ export class WebRouter {
|
|
|
389
492
|
}
|
|
390
493
|
const rscResult = await this.#rsc.renderWithMeta(req, {
|
|
391
494
|
clientManifest: manifest.clientManifest,
|
|
495
|
+
signal: req.signal,
|
|
392
496
|
});
|
|
393
497
|
if (rscResult.type === "redirect")
|
|
394
498
|
return Response.redirect(new URL(rscResult.location, url.origin), rscResult.status);
|
|
395
499
|
if (rscResult.type === "not-found") return this.#renderNotFoundResponse(req, url);
|
|
396
500
|
const themeCookieExists = WebRouter.#hasCookie(req, "theme");
|
|
501
|
+
const hostRequestStore = createRequestStore(req);
|
|
397
502
|
const htmlStream = await new SsrFromRscRenderer().render({
|
|
398
503
|
request: req,
|
|
504
|
+
requestStore: hostRequestStore,
|
|
399
505
|
rscStream: rscResult.stream,
|
|
400
506
|
ssrManifest: manifest.ssrManifest,
|
|
401
507
|
bootstrapModules: [this.#artifact.rscClientUrl],
|
|
402
508
|
extraBootstrapInline: !this.#prodMode ? HMR_CLIENT_SCRIPT : undefined,
|
|
403
509
|
importmap: this.#artifact.vendorMap,
|
|
404
510
|
theme: themeCookieExists ? undefined : (rscResult.theme ?? "system"),
|
|
511
|
+
lateControl: rscResult.lateControl,
|
|
512
|
+
onCancel: (reason: unknown) => {
|
|
513
|
+
rscResult.cancel(reason);
|
|
514
|
+
},
|
|
405
515
|
});
|
|
406
516
|
const responseStatus = rscResult.status ?? 200;
|
|
407
517
|
const responseHeaders = WebRouter.#htmlResponseHeaders(responseStatus);
|
|
408
|
-
if (
|
|
409
|
-
const
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
518
|
+
if (req.method === "HEAD") {
|
|
519
|
+
const headers = new Headers(responseHeaders);
|
|
520
|
+
if (htmlCacheEntry && responseStatus === 200) headers.set("X-Akan-Cache", "MISS");
|
|
521
|
+
cancelStreamForHeadResponse(htmlStream, new Error("HEAD response does not consume body"));
|
|
522
|
+
return new Response(null, { status: responseStatus, headers });
|
|
523
|
+
}
|
|
524
|
+
if (htmlCacheEntry && responseStatus === 200) {
|
|
525
|
+
const headers = new Headers(responseHeaders);
|
|
526
|
+
headers.set("X-Akan-Cache", "MISS");
|
|
527
|
+
let htmlStoreTtl = htmlCacheEntry.ttl;
|
|
528
|
+
const shouldCacheHtml = Promise.all([rscResult.lateControl, rscResult.cacheState]).then(
|
|
529
|
+
([control, cacheState]) => {
|
|
530
|
+
const storeTtl = resolveHtmlRouteCacheStoreTtl({
|
|
531
|
+
baseTtl: htmlCacheEntry.ttl,
|
|
532
|
+
workerCacheState: cacheState,
|
|
533
|
+
hostRequestStore,
|
|
534
|
+
lateControl: control,
|
|
535
|
+
});
|
|
536
|
+
if (storeTtl === null) return false;
|
|
537
|
+
htmlStoreTtl = storeTtl;
|
|
538
|
+
return true;
|
|
415
539
|
},
|
|
416
|
-
|
|
540
|
+
);
|
|
541
|
+
return new Response(
|
|
542
|
+
cacheHtmlWhileStreaming(
|
|
543
|
+
htmlStream,
|
|
544
|
+
(html) => {
|
|
545
|
+
this.#setCachedHtml(htmlCacheEntry.key, html, htmlStoreTtl);
|
|
546
|
+
},
|
|
547
|
+
{
|
|
548
|
+
shouldCache: () => shouldCacheHtml,
|
|
549
|
+
maxBodyBytes: parsePositiveInt(process.env.AKAN_HTML_RESULT_CACHE_MAX_BODY_BYTES),
|
|
550
|
+
},
|
|
551
|
+
),
|
|
552
|
+
{
|
|
553
|
+
status: responseStatus,
|
|
554
|
+
headers,
|
|
555
|
+
},
|
|
556
|
+
);
|
|
417
557
|
}
|
|
418
|
-
return new Response(
|
|
558
|
+
return new Response(htmlStream, {
|
|
419
559
|
status: responseStatus,
|
|
420
560
|
headers: responseHeaders,
|
|
421
561
|
});
|
|
@@ -452,23 +592,19 @@ export class WebRouter {
|
|
|
452
592
|
httpHtmlCacheBypass: this.#htmlCacheBypass,
|
|
453
593
|
};
|
|
454
594
|
}
|
|
595
|
+
|
|
596
|
+
/** @internal Clears local route result caches owned by the host and RSC worker. */
|
|
597
|
+
invalidateRouteCaches(reason?: string): void {
|
|
598
|
+
this.#htmlCache.clear();
|
|
599
|
+
this.#rsc.invalidateRouteResultCache(reason);
|
|
600
|
+
}
|
|
601
|
+
|
|
455
602
|
/**
|
|
456
603
|
* Reconstruct origin as the browser saw it when behind Ingress / reverse proxies
|
|
457
604
|
* (prevents `/__rsc` same-origin rejecting because `req.url` is internal).
|
|
458
605
|
*/
|
|
459
606
|
static #clientFacingOrigin(req: Request): string {
|
|
460
|
-
|
|
461
|
-
const fwdProto = req.headers.get("x-forwarded-proto")?.split(",")[0]?.trim();
|
|
462
|
-
const fwdHost = req.headers.get("x-forwarded-host")?.split(",")[0]?.trim();
|
|
463
|
-
const hostFallback = fwdHost ?? req.headers.get("host");
|
|
464
|
-
const protoFallback = fwdProto ?? parsed.protocol.slice(0, -1);
|
|
465
|
-
if (hostFallback && protoFallback) {
|
|
466
|
-
try {
|
|
467
|
-
return new URL(`${protoFallback}://${hostFallback}`).origin;
|
|
468
|
-
} catch {
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
return parsed.origin;
|
|
607
|
+
return getClientFacingOrigin(req);
|
|
472
608
|
}
|
|
473
609
|
|
|
474
610
|
static #basePathForRequestHost(req: Request, subRoutes: Record<string, string[]>): string | null {
|
|
@@ -495,33 +631,25 @@ export class WebRouter {
|
|
|
495
631
|
static #hasCookie(req: Request, name: string): boolean {
|
|
496
632
|
return parseCookieHeader(req.headers.get("cookie") ?? "").has(name);
|
|
497
633
|
}
|
|
498
|
-
#
|
|
634
|
+
#getHtmlCacheEntry(req: Request, url: URL): RouteCacheEntry | null {
|
|
499
635
|
if (!this.#prodMode || process.env.AKAN_HTML_RESULT_CACHE !== "1") {
|
|
500
636
|
this.#htmlCacheBypass += 1;
|
|
501
637
|
return null;
|
|
502
638
|
}
|
|
503
|
-
if (!
|
|
639
|
+
if (!isPublicRouteCacheableRequest(req)) {
|
|
504
640
|
this.#htmlCacheBypass += 1;
|
|
505
641
|
return null;
|
|
506
642
|
}
|
|
507
|
-
if (!
|
|
643
|
+
if (!isHtmlRouteCachePathAllowed(url.pathname)) {
|
|
508
644
|
this.#htmlCacheBypass += 1;
|
|
509
645
|
return null;
|
|
510
646
|
}
|
|
511
|
-
const ttl =
|
|
512
|
-
if (ttl
|
|
647
|
+
const ttl = normalizeRouteCacheTtl(process.env.AKAN_HTML_RESULT_CACHE_TTL);
|
|
648
|
+
if (ttl === null) {
|
|
513
649
|
this.#htmlCacheBypass += 1;
|
|
514
650
|
return null;
|
|
515
651
|
}
|
|
516
|
-
return
|
|
517
|
-
WebRouter.#clientFacingOrigin(req),
|
|
518
|
-
req.headers.get("x-base-path") ?? "",
|
|
519
|
-
url.pathname,
|
|
520
|
-
url.search,
|
|
521
|
-
req.headers.get("accept-language") ?? "",
|
|
522
|
-
WebRouter.#cookieValue(req, "theme") ?? "",
|
|
523
|
-
ttl,
|
|
524
|
-
].join("\n");
|
|
652
|
+
return createRouteCacheEntry({ request: req, url, theme: WebRouter.#cookieValue(req, "theme"), ttl });
|
|
525
653
|
}
|
|
526
654
|
|
|
527
655
|
#getCachedHtml(cacheKey: string): string | null {
|
|
@@ -530,58 +658,18 @@ export class WebRouter {
|
|
|
530
658
|
this.#htmlCacheMisses += 1;
|
|
531
659
|
return null;
|
|
532
660
|
}
|
|
533
|
-
if (cached.expiresAt <= Date.now()) {
|
|
534
|
-
this.#htmlCache.delete(cacheKey);
|
|
535
|
-
this.#htmlCacheMisses += 1;
|
|
536
|
-
return null;
|
|
537
|
-
}
|
|
538
661
|
this.#htmlCacheHits += 1;
|
|
539
662
|
return cached.html;
|
|
540
663
|
}
|
|
541
664
|
|
|
542
|
-
#setCachedHtml(cacheKey: string, html: string): void {
|
|
543
|
-
|
|
544
|
-
const maxEntries = WebRouter.#positiveIntEnv("AKAN_HTML_RESULT_CACHE_MAX_ENTRIES") ?? 100;
|
|
545
|
-
while (this.#htmlCache.size >= maxEntries) {
|
|
546
|
-
const firstKey = this.#htmlCache.keys().next().value;
|
|
547
|
-
if (!firstKey) break;
|
|
548
|
-
this.#htmlCache.delete(firstKey);
|
|
549
|
-
}
|
|
550
|
-
this.#htmlCache.set(cacheKey, { html, expiresAt: Date.now() + ttl * 1000 });
|
|
665
|
+
#setCachedHtml(cacheKey: string, html: string, ttl: number): void {
|
|
666
|
+
this.#htmlCache.set(cacheKey, { html }, ttl);
|
|
551
667
|
}
|
|
552
668
|
|
|
553
669
|
static #cookieValue(req: Request, name: string): string | undefined {
|
|
554
670
|
return parseCookieHeader(req.headers.get("cookie") ?? "").get(name)?.value;
|
|
555
671
|
}
|
|
556
672
|
|
|
557
|
-
static #isPublicCacheableRequest(req: Request): boolean {
|
|
558
|
-
if (req.method !== "GET") return false;
|
|
559
|
-
if (req.headers.has("authorization")) return false;
|
|
560
|
-
const cookie = req.headers.get("cookie");
|
|
561
|
-
if (!cookie) return true;
|
|
562
|
-
return [...parseCookieHeader(cookie).keys()].every((name) => name === "theme" || name.startsWith("akan_public_"));
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
static #htmlCacheTtlSeconds(): number {
|
|
566
|
-
return WebRouter.#positiveIntEnv("AKAN_HTML_RESULT_CACHE_TTL") ?? 30;
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
static #isHtmlCachePathAllowed(pathname: string): boolean {
|
|
570
|
-
const prefixes = (process.env.AKAN_HTML_RESULT_CACHE_PATHS ?? "")
|
|
571
|
-
.split(",")
|
|
572
|
-
.map((prefix) => prefix.trim())
|
|
573
|
-
.filter(Boolean);
|
|
574
|
-
if (prefixes.length === 0) return false;
|
|
575
|
-
return prefixes.some(
|
|
576
|
-
(prefix) => pathname === prefix || pathname.startsWith(prefix.endsWith("/") ? prefix : `${prefix}/`),
|
|
577
|
-
);
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
static #positiveIntEnv(name: string): number | null {
|
|
581
|
-
const parsed = Number.parseInt(process.env[name] ?? "", 10);
|
|
582
|
-
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
|
|
583
|
-
}
|
|
584
|
-
|
|
585
673
|
static #isImageOptimizerPath(pathname: string): boolean {
|
|
586
674
|
return pathname === "/_akan/image" || pathname.endsWith("/_akan/image");
|
|
587
675
|
}
|
|
@@ -675,10 +763,7 @@ export class WebRouter {
|
|
|
675
763
|
return `${html.slice(0, last.index)}${snippet}\n${html.slice(last.index)}`;
|
|
676
764
|
}
|
|
677
765
|
static #rscNotFoundResponse(): Response {
|
|
678
|
-
return
|
|
679
|
-
status: 404,
|
|
680
|
-
headers: { "Content-Type": "text/plain; charset=utf-8", "Cache-Control": "no-store" },
|
|
681
|
-
});
|
|
766
|
+
return createRscNotFoundFallbackResponse();
|
|
682
767
|
}
|
|
683
768
|
#getProductionRouteCache() {
|
|
684
769
|
return new RouteClientCache({
|
package/service/ipcTypes.ts
CHANGED
|
@@ -57,6 +57,7 @@ export interface AkanMetricsReport {
|
|
|
57
57
|
rscWorkerLastRecycleReason?: string;
|
|
58
58
|
rscPendingRenderCount?: number;
|
|
59
59
|
rscQueuedSendCount?: number;
|
|
60
|
+
rscHostPendingChunkOverflowCount?: number;
|
|
60
61
|
rscRenderCount?: number;
|
|
61
62
|
rscInFlightRenderCount?: number;
|
|
62
63
|
rscLastRenderedPath?: string;
|
|
@@ -5,7 +5,7 @@ import type { AnimatedComponent, AnimatedProps, Interpolation, SpringValue } fro
|
|
|
5
5
|
import type { RouterInstance } from "./router.d.ts";
|
|
6
6
|
import type { ReactFont } from "./types.d.ts";
|
|
7
7
|
export type TransitionType = "none" | "fade" | "bottomUp" | "stack" | "scaleOut";
|
|
8
|
-
/** Per-page CSR configuration for transition, safe-area,
|
|
8
|
+
/** Per-page CSR configuration for transition, safe-area, and gesture behavior. */
|
|
9
9
|
export interface PageConfig {
|
|
10
10
|
transition?: TransitionType;
|
|
11
11
|
safeArea?: boolean | "top" | "bottom";
|
|
@@ -16,8 +16,6 @@ export interface PageConfig {
|
|
|
16
16
|
bottomInset?: boolean | number;
|
|
17
17
|
gesture?: boolean;
|
|
18
18
|
cache?: boolean;
|
|
19
|
-
rscCache?: "public" | false;
|
|
20
|
-
rscCacheTtl?: number;
|
|
21
19
|
topSafeAreaColor?: string;
|
|
22
20
|
bottomSafeAreaColor?: string;
|
|
23
21
|
}
|
|
@@ -63,7 +61,12 @@ export interface LayoutErrorProps extends LayoutNotFoundProps {
|
|
|
63
61
|
}
|
|
64
62
|
export type Head = ReactNode;
|
|
65
63
|
export type GenerateHead = (props: PageProps) => PromiseOrObject<Head | null | undefined>;
|
|
66
|
-
export
|
|
64
|
+
export interface ResolvedHead {
|
|
65
|
+
node: Head | null | undefined;
|
|
66
|
+
hasExplicitLanguageAlternates: boolean;
|
|
67
|
+
}
|
|
68
|
+
export type ResolveHeadResult = Head | ResolvedHead | null | undefined;
|
|
69
|
+
export type ResolveHead = (props: PageProps) => PromiseOrObject<ResolveHeadResult>;
|
|
67
70
|
export type HeadProps = PageProps;
|
|
68
71
|
export type PageRender = (props: PageProps) => PromiseOrObject<ReactNode>;
|
|
69
72
|
export type LayoutRender = (props: LayoutProps) => PromiseOrObject<ReactNode>;
|
|
@@ -107,17 +110,45 @@ export interface WebAppManifest {
|
|
|
107
110
|
screenshots?: WebAppManifestIcon[];
|
|
108
111
|
[key: string]: unknown;
|
|
109
112
|
}
|
|
113
|
+
export interface AkanMetadata {
|
|
114
|
+
title?: string;
|
|
115
|
+
description?: string;
|
|
116
|
+
robots?: string;
|
|
117
|
+
openGraph?: {
|
|
118
|
+
title?: string;
|
|
119
|
+
description?: string;
|
|
120
|
+
type?: string;
|
|
121
|
+
url?: string;
|
|
122
|
+
siteName?: string;
|
|
123
|
+
images?: string | string[];
|
|
124
|
+
};
|
|
125
|
+
twitter?: {
|
|
126
|
+
card?: "summary" | "summary_large_image" | "app" | "player" | (string & {});
|
|
127
|
+
title?: string;
|
|
128
|
+
description?: string;
|
|
129
|
+
images?: string | string[];
|
|
130
|
+
};
|
|
131
|
+
alternates?: {
|
|
132
|
+
canonical?: string;
|
|
133
|
+
languages?: Record<string, string>;
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
export type GenerateMetadata = (props: PageProps) => PromiseOrObject<AkanMetadata | null | undefined>;
|
|
110
137
|
export interface PageModule {
|
|
111
138
|
default?: PageRender;
|
|
112
139
|
pageConfig?: PageConfig;
|
|
113
140
|
head?: Head;
|
|
141
|
+
metadata?: AkanMetadata;
|
|
114
142
|
generateHead?: GenerateHead;
|
|
143
|
+
generateMetadata?: GenerateMetadata;
|
|
115
144
|
Loading?: PageLoadingRender;
|
|
116
145
|
}
|
|
117
146
|
export interface LayoutModule {
|
|
118
147
|
default?: LayoutRender;
|
|
119
148
|
head?: Head;
|
|
149
|
+
metadata?: AkanMetadata;
|
|
120
150
|
generateHead?: GenerateHead;
|
|
151
|
+
generateMetadata?: GenerateMetadata;
|
|
121
152
|
fonts?: ReactFont[];
|
|
122
153
|
manifest?: WebAppManifest;
|
|
123
154
|
theme?: string;
|
|
@@ -137,7 +168,7 @@ export interface Route {
|
|
|
137
168
|
renderLayout?: RouteRender;
|
|
138
169
|
pageIncludesOwnLayout?: boolean;
|
|
139
170
|
isSpecialRoute?: boolean;
|
|
140
|
-
loader?: () =>
|
|
171
|
+
loader?: () => unknown;
|
|
141
172
|
pageState?: PageState;
|
|
142
173
|
children: Map<string, Route>;
|
|
143
174
|
}
|
|
@@ -218,7 +249,7 @@ export interface RouteState {
|
|
|
218
249
|
pathRoutes: PathRoute[];
|
|
219
250
|
}
|
|
220
251
|
export type UseCsrTransition = CsrTransitionStyles & {
|
|
221
|
-
pageBind: (...args:
|
|
252
|
+
pageBind: (...args: unknown[]) => ReactDOMAttributes;
|
|
222
253
|
pageClassName: string;
|
|
223
254
|
transDirection: "vertical" | "horizontal" | "none";
|
|
224
255
|
transUnitRange: number[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const baseDictionary: import("./dictInfo.d.ts").ServiceDictInfo<[string, string], string, never, "error" | "save" | "password" | "remove" | "ok" | "connecting" | "failed" | "success" | "edit" | "view" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "
|
|
1
|
+
export declare const baseDictionary: import("./dictInfo.d.ts").ServiceDictInfo<[string, string], string, never, "error" | "save" | "password" | "remove" | "ok" | "connecting" | "failed" | "cancel" | "success" | "edit" | "view" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "unauthorized" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "actions" | "new">;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { BaseEndpoint } from "akanjs/signal";
|
|
2
2
|
export declare const dictionary: {
|
|
3
|
-
base: import("./locale.d.ts").DictModule<import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "save" | "password" | "remove" | "ok" | "connecting" | "failed" | "success" | "edit" | "view" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "
|
|
3
|
+
base: import("./locale.d.ts").DictModule<import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "save" | "password" | "remove" | "ok" | "connecting" | "failed" | "cancel" | "success" | "edit" | "view" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "unauthorized" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "actions" | "new">, never>;
|
|
4
4
|
};
|
|
5
|
-
export declare const Err: import("./trans.d.ts").ErrConstructor<never>, translate: (lang: "en" | "ko" | "zhChs" | "zhCht" | "ja", key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "save" | "password" | "remove" | "ok" | "connecting" | "failed" | "success" | "edit" | "view" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "
|
|
6
|
-
info: (key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "save" | "password" | "remove" | "ok" | "connecting" | "failed" | "success" | "edit" | "view" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "
|
|
7
|
-
success: (key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "save" | "password" | "remove" | "ok" | "connecting" | "failed" | "success" | "edit" | "view" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "
|
|
8
|
-
error: (key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "save" | "password" | "remove" | "ok" | "connecting" | "failed" | "success" | "edit" | "view" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "
|
|
9
|
-
warning: (key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "save" | "password" | "remove" | "ok" | "connecting" | "failed" | "success" | "edit" | "view" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "
|
|
10
|
-
loading: (key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "save" | "password" | "remove" | "ok" | "connecting" | "failed" | "success" | "edit" | "view" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "
|
|
11
|
-
}, getDictionary: (lang: "en" | "ko" | "zhChs" | "zhCht" | "ja") => object, getAllDictionary: () => import("./trans.d.ts").RootDictionary, __Dict_Key__: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "save" | "password" | "remove" | "ok" | "connecting" | "failed" | "success" | "edit" | "view" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "
|
|
5
|
+
export declare const Err: import("./trans.d.ts").ErrConstructor<never>, translate: (lang: "en" | "ko" | "zhChs" | "zhCht" | "ja", key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "save" | "password" | "remove" | "ok" | "connecting" | "failed" | "cancel" | "success" | "edit" | "view" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "unauthorized" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "actions" | "new">, data?: import("./trans.d.ts").TranslationData) => string, msg: {
|
|
6
|
+
info: (key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "save" | "password" | "remove" | "ok" | "connecting" | "failed" | "cancel" | "success" | "edit" | "view" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "unauthorized" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "actions" | "new">, option?: import("./trans.d.ts").TransMessageOption) => void;
|
|
7
|
+
success: (key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "save" | "password" | "remove" | "ok" | "connecting" | "failed" | "cancel" | "success" | "edit" | "view" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "unauthorized" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "actions" | "new">, option?: import("./trans.d.ts").TransMessageOption) => void;
|
|
8
|
+
error: (key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "save" | "password" | "remove" | "ok" | "connecting" | "failed" | "cancel" | "success" | "edit" | "view" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "unauthorized" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "actions" | "new">, option?: import("./trans.d.ts").TransMessageOption) => void;
|
|
9
|
+
warning: (key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "save" | "password" | "remove" | "ok" | "connecting" | "failed" | "cancel" | "success" | "edit" | "view" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "unauthorized" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "actions" | "new">, option?: import("./trans.d.ts").TransMessageOption) => void;
|
|
10
|
+
loading: (key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "save" | "password" | "remove" | "ok" | "connecting" | "failed" | "cancel" | "success" | "edit" | "view" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "unauthorized" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "actions" | "new">, option?: import("./trans.d.ts").TransMessageOption) => void;
|
|
11
|
+
}, getDictionary: (lang: "en" | "ko" | "zhChs" | "zhCht" | "ja") => object, getAllDictionary: () => import("./trans.d.ts").RootDictionary, __Dict_Key__: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "save" | "password" | "remove" | "ok" | "connecting" | "failed" | "cancel" | "success" | "edit" | "view" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "unauthorized" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "actions" | "new">, __Error_Key__: never;
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
export type AkanTheme = "css" | "system" | (string & {});
|
|
2
2
|
export interface AkanRequestPolicy {
|
|
3
3
|
routeId?: string;
|
|
4
|
-
rscCache?: "public" | false;
|
|
5
|
-
rscCacheTtl?: number;
|
|
6
4
|
cacheable?: boolean;
|
|
7
5
|
revalidate?: number | false;
|
|
8
6
|
tags: Set<string>;
|
|
@@ -31,11 +29,15 @@ export declare function createRequestStore(request: Request, policy?: Partial<Om
|
|
|
31
29
|
/** Stores theme preference on the active request when server rendering. */
|
|
32
30
|
export declare function setRequestTheme(theme: AkanTheme | undefined): void;
|
|
33
31
|
export declare function getRequestTheme(): AkanTheme | undefined;
|
|
34
|
-
export declare function pushRequestFallback(
|
|
32
|
+
export declare function pushRequestFallback(storeOrRequest: Request | AkanRequestStore): () => void;
|
|
35
33
|
/** Returns the active server request store from AsyncLocalStorage or the fallback stack. */
|
|
36
34
|
export declare function getRequestStore(): AkanRequestStore | undefined;
|
|
37
35
|
/** Returns the active server request from AsyncLocalStorage or the fallback stack. */
|
|
38
|
-
export declare function getRequest(
|
|
36
|
+
export declare function getRequest(options?: {
|
|
37
|
+
trackDynamic?: boolean;
|
|
38
|
+
}): Request | undefined;
|
|
39
|
+
/** Reads the framework's active server request without marking the user route dynamic. */
|
|
40
|
+
export declare function untrackedRequest(): Request | undefined;
|
|
39
41
|
export declare function getRequestPolicy(): AkanRequestPolicy | undefined;
|
|
40
42
|
export declare function updateRequestPolicy(patch: Partial<Omit<AkanRequestPolicy, "tags">> & {
|
|
41
43
|
tags?: Iterable<string>;
|
|
@@ -44,11 +46,19 @@ export declare function getRequestDynamicUsage(): AkanDynamicUsage | undefined;
|
|
|
44
46
|
/** Deduplicates a promise-producing query within the active request. */
|
|
45
47
|
export declare function memoizeRequestQuery<T>(key: string, factory: () => Promise<T>): Promise<T>;
|
|
46
48
|
/** Returns current request headers as a Map, or an empty Map outside a request. */
|
|
47
|
-
export declare function headers(
|
|
49
|
+
export declare function headers(options?: {
|
|
50
|
+
trackDynamic?: boolean;
|
|
51
|
+
}): Map<string, string>;
|
|
52
|
+
/** Reads headers for framework internals without marking the user route dynamic. */
|
|
53
|
+
export declare function untrackedHeaders(): Map<string, string>;
|
|
48
54
|
export interface CookieEntry {
|
|
49
55
|
name: string;
|
|
50
56
|
value: string;
|
|
51
57
|
}
|
|
52
58
|
export declare function parseCookieHeader(cookieHeader: string): Map<string, CookieEntry>;
|
|
53
59
|
/** Returns parsed cookies from the current request, or an empty Map outside a request. */
|
|
54
|
-
export declare function cookies(
|
|
60
|
+
export declare function cookies(options?: {
|
|
61
|
+
trackDynamic?: boolean;
|
|
62
|
+
}): Map<string, CookieEntry>;
|
|
63
|
+
/** Reads cookies for framework internals without marking the user route dynamic. */
|
|
64
|
+
export declare function untrackedCookies(): Map<string, CookieEntry>;
|