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.
Files changed (38) hide show
  1. package/client/csrTypes.ts +37 -6
  2. package/client/makePageProto.tsx +8 -8
  3. package/client/router.ts +5 -2
  4. package/fetch/requestStorage.ts +41 -11
  5. package/package.json +1 -1
  6. package/server/akanApp.ts +55 -0
  7. package/server/cachePolicy.ts +192 -0
  8. package/server/metadata.tsx +114 -0
  9. package/server/routeElementComposer.tsx +21 -1
  10. package/server/routeTreeBuilder.ts +44 -5
  11. package/server/rscClient.tsx +127 -50
  12. package/server/rscHttp.ts +120 -0
  13. package/server/rscNavigationState.ts +95 -0
  14. package/server/rscWorker.tsx +318 -121
  15. package/server/rscWorkerHost.ts +281 -66
  16. package/server/rscWorkerReplay.ts +40 -0
  17. package/server/ssrFromRscRenderer.tsx +462 -77
  18. package/server/ssrTypes.ts +11 -1
  19. package/server/webRouter.ts +173 -88
  20. package/service/ipcTypes.ts +1 -0
  21. package/types/client/csrTypes.d.ts +37 -6
  22. package/types/dictionary/base.dictionary.d.ts +1 -1
  23. package/types/dictionary/dictionary.d.ts +8 -8
  24. package/types/fetch/requestStorage.d.ts +16 -6
  25. package/types/server/cachePolicy.d.ts +55 -0
  26. package/types/server/metadata.d.ts +13 -0
  27. package/types/server/routeElementComposer.d.ts +6 -1
  28. package/types/server/rscHttp.d.ts +16 -0
  29. package/types/server/rscNavigationState.d.ts +35 -0
  30. package/types/server/rscWorkerHost.d.ts +38 -0
  31. package/types/server/rscWorkerReplay.d.ts +29 -0
  32. package/types/server/ssrFromRscRenderer.d.ts +20 -1
  33. package/types/server/ssrTypes.d.ts +10 -1
  34. package/types/server/webRouter.d.ts +27 -1
  35. package/types/service/ipcTypes.d.ts +1 -0
  36. package/types/webkit/useCsrValues.d.ts +1 -1
  37. package/ui/Link/SsrLink.tsx +0 -2
  38. package/webkit/bootCsr.tsx +16 -2
@@ -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 Map<string, CachedHtmlResult>();
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 createRscStreamResponse(result.stream, result.status ?? 200);
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 htmlCacheKey = this.#getHtmlCacheKey(req, url);
381
- const cachedHtml = htmlCacheKey ? this.#getCachedHtml(htmlCacheKey) : null;
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 (htmlCacheKey && responseStatus === 200) {
409
- const html = await new Response(htmlStream).text();
410
- this.#setCachedHtml(htmlCacheKey, html);
411
- return new Response(html, {
412
- headers: {
413
- "Content-Type": "text/html; charset=utf-8",
414
- "X-Akan-Cache": "MISS",
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(req.method === "HEAD" ? null : htmlStream, {
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
- const parsed = new URL(req.url);
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
- #getHtmlCacheKey(req: Request, url: URL): string | null {
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 (!WebRouter.#isPublicCacheableRequest(req)) {
639
+ if (!isPublicRouteCacheableRequest(req)) {
504
640
  this.#htmlCacheBypass += 1;
505
641
  return null;
506
642
  }
507
- if (!WebRouter.#isHtmlCachePathAllowed(url.pathname)) {
643
+ if (!isHtmlRouteCachePathAllowed(url.pathname)) {
508
644
  this.#htmlCacheBypass += 1;
509
645
  return null;
510
646
  }
511
- const ttl = WebRouter.#htmlCacheTtlSeconds();
512
- if (ttl <= 0) {
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
- const ttl = Number.parseInt(cacheKey.split("\n").at(-1) ?? "30", 10);
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 new Response("Not Found", {
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({
@@ -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, gesture, and cache behavior. */
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 type ResolveHead = (props: PageProps) => PromiseOrObject<Head | null | undefined>;
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?: () => any;
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: any[]) => ReactDOMAttributes;
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" | "cancel" | "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
+ 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" | "cancel" | "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>;
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" | "cancel" | "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" | "success" | "edit" | "view" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "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" | "success" | "edit" | "view" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "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" | "success" | "edit" | "view" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "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" | "success" | "edit" | "view" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "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" | "success" | "edit" | "view" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "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" | "success" | "edit" | "view" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "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;
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(req: Request): () => void;
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(): Request | undefined;
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(): Map<string, string>;
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(): Map<string, CookieEntry>;
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>;