akanjs 2.2.13-rc.0 → 2.3.0
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/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 +177 -86
- package/server/rscWorkerHost.ts +47 -1
- package/server/rscWorkerReplay.ts +5 -0
- package/server/ssrFromRscRenderer.tsx +18 -6
- package/server/ssrTypes.ts +2 -1
- package/server/webRouter.ts +114 -110
- package/types/client/csrTypes.d.ts +37 -6
- 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 +9 -0
- package/types/server/rscWorkerReplay.d.ts +6 -0
- package/types/server/ssrFromRscRenderer.d.ts +2 -0
- package/types/server/ssrTypes.d.ts +2 -1
- package/types/server/webRouter.d.ts +22 -1
- package/types/ui/Button.d.ts +1 -1
- package/types/ui/ClientSide.d.ts +1 -1
- package/types/ui/Constant/Doc.d.ts +6 -6
- package/types/ui/Constant/Mermaid.d.ts +1 -1
- package/types/ui/Constant/index.d.ts +1 -1
- package/types/ui/Copy.d.ts +1 -1
- package/types/ui/CsrImage.d.ts +1 -1
- package/types/ui/Data/CardList.d.ts +1 -1
- package/types/ui/Data/Dashboard.d.ts +1 -1
- package/types/ui/Data/Insight.d.ts +1 -1
- package/types/ui/Data/Item.d.ts +6 -6
- package/types/ui/Data/ListContainer.d.ts +1 -1
- package/types/ui/Data/Pagination.d.ts +1 -1
- package/types/ui/Data/TableList.d.ts +1 -1
- package/types/ui/DatePicker.d.ts +3 -3
- package/types/ui/Dialog/Close.d.ts +1 -1
- package/types/ui/Dialog/Content.d.ts +1 -1
- package/types/ui/Dialog/Provider.d.ts +1 -1
- package/types/ui/Dialog/Trigger.d.ts +1 -1
- package/types/ui/Dialog/index.d.ts +3 -3
- package/types/ui/DragAction.d.ts +4 -4
- package/types/ui/DraggableList.d.ts +3 -3
- package/types/ui/Dropdown.d.ts +1 -1
- package/types/ui/Empty.d.ts +1 -1
- package/types/ui/Field.d.ts +22 -22
- package/types/ui/Image.d.ts +1 -1
- package/types/ui/InfiniteScroll.d.ts +1 -1
- package/types/ui/Input.d.ts +6 -6
- package/types/ui/KeyboardAvoiding.d.ts +1 -1
- package/types/ui/Layout/BottomAction.d.ts +1 -1
- package/types/ui/Layout/BottomInset.d.ts +1 -1
- package/types/ui/Layout/BottomTab.d.ts +1 -1
- package/types/ui/Layout/Header.d.ts +1 -1
- package/types/ui/Layout/LeftSider.d.ts +1 -1
- package/types/ui/Layout/Navbar.d.ts +1 -1
- package/types/ui/Layout/RightSider.d.ts +1 -1
- package/types/ui/Layout/Sider.d.ts +1 -1
- package/types/ui/Layout/Template.d.ts +1 -1
- package/types/ui/Layout/TopLeftAction.d.ts +1 -1
- package/types/ui/Layout/Unit.d.ts +1 -1
- package/types/ui/Layout/View.d.ts +1 -1
- package/types/ui/Layout/Zone.d.ts +1 -1
- package/types/ui/Layout/index.d.ts +12 -12
- package/types/ui/Link/Back.d.ts +1 -1
- package/types/ui/Link/Close.d.ts +1 -1
- package/types/ui/Link/CsrLink.d.ts +1 -1
- package/types/ui/Link/Lang.d.ts +1 -1
- package/types/ui/Link/SsrLink.d.ts +1 -1
- package/types/ui/Link/index.d.ts +1 -1
- package/types/ui/Load/Edit.d.ts +1 -1
- package/types/ui/Load/Edit_Client.d.ts +1 -1
- package/types/ui/Load/PageCSR.d.ts +1 -1
- package/types/ui/Load/Pagination.d.ts +1 -1
- package/types/ui/Load/Units.d.ts +1 -1
- package/types/ui/Load/View.d.ts +1 -1
- package/types/ui/Loading/Area.d.ts +1 -1
- package/types/ui/Loading/Button.d.ts +1 -1
- package/types/ui/Loading/Input.d.ts +1 -1
- package/types/ui/Loading/ProgressBar.d.ts +1 -1
- package/types/ui/Loading/Skeleton.d.ts +1 -1
- package/types/ui/Loading/Spin.d.ts +1 -1
- package/types/ui/Loading/index.d.ts +6 -6
- package/types/ui/Menu.d.ts +1 -1
- package/types/ui/Modal.d.ts +1 -1
- package/types/ui/Model/AdminPanel.d.ts +1 -1
- package/types/ui/Model/Edit.d.ts +1 -1
- package/types/ui/Model/EditModal.d.ts +1 -1
- package/types/ui/Model/EditWrapper.d.ts +1 -1
- package/types/ui/Model/LoadInit.d.ts +1 -1
- package/types/ui/Model/New.d.ts +1 -1
- package/types/ui/Model/NewWrapper.d.ts +1 -1
- package/types/ui/Model/NewWrapper_Client.d.ts +1 -1
- package/types/ui/Model/Remove.d.ts +1 -1
- package/types/ui/Model/RemoveWrapper.d.ts +1 -1
- package/types/ui/Model/SureToRemove.d.ts +1 -1
- package/types/ui/Model/View.d.ts +1 -1
- package/types/ui/Model/ViewEditModal.d.ts +1 -1
- package/types/ui/Model/ViewModal.d.ts +1 -1
- package/types/ui/Model/ViewWrapper.d.ts +1 -1
- package/types/ui/More.d.ts +1 -1
- package/types/ui/ObjectId.d.ts +1 -1
- package/types/ui/Popconfirm.d.ts +1 -1
- package/types/ui/Radio.d.ts +2 -2
- package/types/ui/RecentTime.d.ts +1 -1
- package/types/ui/Refresh.d.ts +1 -1
- package/types/ui/ScreenNavigator.d.ts +3 -3
- package/types/ui/Select.d.ts +1 -1
- package/types/ui/Signal/Arg.d.ts +13 -13
- package/types/ui/Signal/Doc.d.ts +6 -6
- package/types/ui/Signal/Listener.d.ts +2 -2
- package/types/ui/Signal/Message.d.ts +4 -4
- package/types/ui/Signal/Object.d.ts +4 -4
- package/types/ui/Signal/PubSub.d.ts +4 -4
- package/types/ui/Signal/Request.d.ts +2 -2
- package/types/ui/Signal/Response.d.ts +3 -3
- package/types/ui/Signal/RestApi.d.ts +5 -5
- package/types/ui/Signal/WebSocket.d.ts +2 -2
- package/types/ui/System/CSR.d.ts +5 -5
- package/types/ui/System/Client.d.ts +8 -8
- package/types/ui/System/Common.d.ts +2 -2
- package/types/ui/System/DevModeToggle.d.ts +1 -1
- package/types/ui/System/Gtag.d.ts +1 -1
- package/types/ui/System/Messages.d.ts +1 -1
- package/types/ui/System/Reconnect.d.ts +1 -1
- package/types/ui/System/Root.d.ts +1 -1
- package/types/ui/System/SSR.d.ts +4 -4
- package/types/ui/System/SelectLanguage.d.ts +1 -1
- package/types/ui/System/ThemeToggle.d.ts +1 -1
- package/types/ui/System/index.d.ts +7 -7
- package/types/ui/Tab/Menu.d.ts +1 -1
- package/types/ui/Tab/Menus.d.ts +1 -1
- package/types/ui/Tab/Panel.d.ts +1 -1
- package/types/ui/Tab/Provider.d.ts +1 -1
- package/types/ui/Tab/index.d.ts +4 -4
- package/types/ui/Table.d.ts +1 -1
- package/types/ui/ToggleSelect.d.ts +2 -2
- package/types/ui/Unauthorized.d.ts +1 -1
- package/types/webkit/useCsrValues.d.ts +1 -1
- package/webkit/bootCsr.tsx +16 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Readable } from "node:stream";
|
|
2
|
-
import { type AkanTheme, pushRequestFallback, requestStorage } from "akanjs/fetch";
|
|
2
|
+
import { type AkanRequestStore, type AkanTheme, pushRequestFallback, requestStorage } from "akanjs/fetch";
|
|
3
3
|
import { type ReactNode, use } from "react";
|
|
4
4
|
import { renderToReadableStream } from "react-dom/server.browser";
|
|
5
5
|
import { createFromNodeStream } from "react-server-dom-webpack/client.node";
|
|
@@ -109,9 +109,14 @@ export function createInlineRscScript(chunk: Uint8Array): string {
|
|
|
109
109
|
return `<script>self.__RSC_PUSH__(${type},${htmlEscapeJsonString(data)})</script>`;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
function escapeHtmlAttr(value: string): string {
|
|
113
|
+
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
114
|
+
}
|
|
115
|
+
|
|
112
116
|
export function createSoftRedirectScript(redirect: SsrLateRedirect): string {
|
|
113
117
|
const method = redirect.method === "push" ? "assign" : "replace";
|
|
114
|
-
|
|
118
|
+
const fallback = `<noscript><meta http-equiv="refresh" content="0;url=${escapeHtmlAttr(redirect.location)}"></noscript>`;
|
|
119
|
+
return `${fallback}<script>window.location.${method}(${htmlEscapeJsonString(redirect.location)})</script>`;
|
|
115
120
|
}
|
|
116
121
|
|
|
117
122
|
function sanitizeFlightRows(
|
|
@@ -121,7 +126,7 @@ function sanitizeFlightRows(
|
|
|
121
126
|
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
122
127
|
const encoder = new TextEncoder();
|
|
123
128
|
const hlStylesheetRe = /(:HL\["[^"\\]*(?:\\.[^"\\]*)*",)"stylesheet"(\])/g;
|
|
124
|
-
const redirectErrorRowRe = /^([0-9a-z]+):E(\{[^\n]*"digest":"AKAN_REDIRECT"[^\n]*\})(\n?)$/;
|
|
129
|
+
const redirectErrorRowRe = /^([0-9a-z]+):E(\{[^\n]*"digest":"AKAN_REDIRECT(?:;[^"]*)?"[^\n]*\})(\n?)$/;
|
|
125
130
|
let buffered: Uint8Array<ArrayBuffer> = new Uint8Array(0);
|
|
126
131
|
|
|
127
132
|
const concatBytes = (left: Uint8Array, right: Uint8Array): Uint8Array<ArrayBuffer> => {
|
|
@@ -335,6 +340,7 @@ export function interleaveRscScriptsWithHtml(
|
|
|
335
340
|
onComplete?: () => void;
|
|
336
341
|
onCancel?: (reason?: unknown) => void;
|
|
337
342
|
request?: Request;
|
|
343
|
+
requestStore?: AkanRequestStore;
|
|
338
344
|
} = {},
|
|
339
345
|
): ReadableStream<Uint8Array> {
|
|
340
346
|
const encoder = new TextEncoder();
|
|
@@ -464,7 +470,8 @@ export function interleaveRscScriptsWithHtml(
|
|
|
464
470
|
};
|
|
465
471
|
|
|
466
472
|
const runPump = () => {
|
|
467
|
-
const
|
|
473
|
+
const requestContext = options.requestStore ?? options.request;
|
|
474
|
+
const cleanup = requestContext ? pushRequestFallback(requestContext) : undefined;
|
|
468
475
|
return pump()
|
|
469
476
|
.catch(fail)
|
|
470
477
|
.finally(() => {
|
|
@@ -472,7 +479,8 @@ export function interleaveRscScriptsWithHtml(
|
|
|
472
479
|
options.onComplete?.();
|
|
473
480
|
});
|
|
474
481
|
};
|
|
475
|
-
|
|
482
|
+
const requestContext = options.requestStore ?? options.request;
|
|
483
|
+
if (requestContext && requestStorage) void requestStorage.run(requestContext, runPump);
|
|
476
484
|
else void runPump();
|
|
477
485
|
},
|
|
478
486
|
cancel(reason) {
|
|
@@ -589,8 +597,9 @@ export class SsrFromRscRenderer {
|
|
|
589
597
|
renderToReadableStream(<Root />, {
|
|
590
598
|
bootstrapScriptContent: bootstrap,
|
|
591
599
|
});
|
|
600
|
+
const requestContext = input.requestStore ?? input.request;
|
|
592
601
|
const htmlStream =
|
|
593
|
-
|
|
602
|
+
requestContext && requestStorage ? await requestStorage.run(requestContext, renderHtml) : await renderHtml();
|
|
594
603
|
|
|
595
604
|
const withHeadScripts = SsrFromRscRenderer.#injectHeadScriptsIntoHead(htmlStream, {
|
|
596
605
|
importmap: input.importmap,
|
|
@@ -604,6 +613,7 @@ export class SsrFromRscRenderer {
|
|
|
604
613
|
SsrFromRscRenderer.#sanitizeFlightForClient(rscForClient),
|
|
605
614
|
input.bootstrapModules,
|
|
606
615
|
input.request,
|
|
616
|
+
input.requestStore,
|
|
607
617
|
input.lateControl,
|
|
608
618
|
() => stderrSuppressor?.stop(),
|
|
609
619
|
input.onCancel,
|
|
@@ -771,6 +781,7 @@ export class SsrFromRscRenderer {
|
|
|
771
781
|
rscClientStream: ReadableStream<Uint8Array>,
|
|
772
782
|
bootstrapModules?: string[],
|
|
773
783
|
request?: Request,
|
|
784
|
+
requestStore?: AkanRequestStore,
|
|
774
785
|
lateControl?: Promise<SsrLateRedirect | null>,
|
|
775
786
|
onComplete?: () => void,
|
|
776
787
|
onCancel?: (reason?: unknown) => void,
|
|
@@ -783,6 +794,7 @@ export class SsrFromRscRenderer {
|
|
|
783
794
|
onComplete,
|
|
784
795
|
onCancel,
|
|
785
796
|
request,
|
|
797
|
+
requestStore,
|
|
786
798
|
});
|
|
787
799
|
}
|
|
788
800
|
}
|
package/server/ssrTypes.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AkanTheme } from "akanjs/fetch";
|
|
1
|
+
import type { AkanRequestStore, AkanTheme } from "akanjs/fetch";
|
|
2
2
|
|
|
3
3
|
export interface SsrManifestEntry {
|
|
4
4
|
id: string;
|
|
@@ -28,6 +28,7 @@ export interface SsrLateRedirect {
|
|
|
28
28
|
|
|
29
29
|
export interface SsrFromRscInput {
|
|
30
30
|
request?: Request;
|
|
31
|
+
requestStore?: AkanRequestStore;
|
|
31
32
|
rscStream: ReadableStream<Uint8Array>;
|
|
32
33
|
ssrManifest: SsrManifest;
|
|
33
34
|
bootstrapModules?: string[];
|
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,6 +17,19 @@ 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";
|
|
@@ -57,22 +70,36 @@ 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
|
+
|
|
60
77
|
export function cacheHtmlWhileStreaming(
|
|
61
78
|
stream: ReadableStream<Uint8Array>,
|
|
62
79
|
onComplete: (html: string) => void,
|
|
80
|
+
options: { shouldCache?: () => boolean | Promise<boolean>; maxBodyBytes?: number | null } = {},
|
|
63
81
|
): ReadableStream<Uint8Array> {
|
|
64
82
|
const chunks: Uint8Array[] = [];
|
|
65
83
|
let byteLength = 0;
|
|
84
|
+
let exceededMaxBodyBytes = false;
|
|
66
85
|
const decoder = new TextDecoder();
|
|
67
86
|
|
|
68
87
|
return stream.pipeThrough(
|
|
69
88
|
new TransformStream<Uint8Array, Uint8Array>({
|
|
70
89
|
transform(chunk, controller) {
|
|
71
|
-
|
|
72
|
-
|
|
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
|
+
}
|
|
73
99
|
controller.enqueue(chunk);
|
|
74
100
|
},
|
|
75
|
-
flush() {
|
|
101
|
+
async flush() {
|
|
102
|
+
if (exceededMaxBodyBytes) return;
|
|
76
103
|
const body = new Uint8Array(byteLength);
|
|
77
104
|
let offset = 0;
|
|
78
105
|
for (const chunk of chunks) {
|
|
@@ -80,6 +107,7 @@ export function cacheHtmlWhileStreaming(
|
|
|
80
107
|
offset += chunk.byteLength;
|
|
81
108
|
}
|
|
82
109
|
try {
|
|
110
|
+
if (options.shouldCache && !(await options.shouldCache())) return;
|
|
83
111
|
onComplete(decoder.decode(body));
|
|
84
112
|
} catch {
|
|
85
113
|
}
|
|
@@ -93,34 +121,41 @@ export function cancelStreamForHeadResponse(stream: ReadableStream<Uint8Array>,
|
|
|
93
121
|
});
|
|
94
122
|
}
|
|
95
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
|
+
|
|
96
154
|
export async function createRscNavigationStreamResponse(
|
|
97
155
|
result: Extract<RscRenderResult, { type: "stream" }>,
|
|
98
156
|
): Promise<Response> {
|
|
99
157
|
|
|
100
|
-
|
|
101
|
-
let byteLength = 0;
|
|
102
|
-
const reader = result.stream.getReader();
|
|
103
|
-
try {
|
|
104
|
-
while (true) {
|
|
105
|
-
const { value, done } = await reader.read();
|
|
106
|
-
if (done) break;
|
|
107
|
-
chunks.push(value);
|
|
108
|
-
byteLength += value.byteLength;
|
|
109
|
-
}
|
|
110
|
-
} finally {
|
|
111
|
-
reader.releaseLock();
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const lateControl = await result.lateControl;
|
|
115
|
-
if (lateControl?.type === "redirect")
|
|
116
|
-
return createRscRedirectResponse(lateControl.location, lateControl.method, lateControl.status);
|
|
117
|
-
const body = new Uint8Array(byteLength);
|
|
118
|
-
let offset = 0;
|
|
119
|
-
for (const chunk of chunks) {
|
|
120
|
-
body.set(chunk, offset);
|
|
121
|
-
offset += chunk.byteLength;
|
|
122
|
-
}
|
|
123
|
-
return createRscStreamResponse(body, result.status ?? 200);
|
|
158
|
+
return createRscStreamResponse(result.stream, result.status ?? 200);
|
|
124
159
|
}
|
|
125
160
|
|
|
126
161
|
export function normalizeRscTargetUrlForHostBasePath(
|
|
@@ -173,7 +208,6 @@ interface WebRouterOptions {
|
|
|
173
208
|
}
|
|
174
209
|
|
|
175
210
|
interface CachedHtmlResult {
|
|
176
|
-
expiresAt: number;
|
|
177
211
|
html: string;
|
|
178
212
|
}
|
|
179
213
|
|
|
@@ -194,7 +228,9 @@ export class WebRouter {
|
|
|
194
228
|
csr: 0,
|
|
195
229
|
image: 0,
|
|
196
230
|
};
|
|
197
|
-
readonly #htmlCache = new
|
|
231
|
+
readonly #htmlCache = new LruTtlCache<CachedHtmlResult>(
|
|
232
|
+
parsePositiveInt(process.env.AKAN_HTML_RESULT_CACHE_MAX_ENTRIES) ?? 100,
|
|
233
|
+
);
|
|
198
234
|
#htmlCacheHits = 0;
|
|
199
235
|
#htmlCacheMisses = 0;
|
|
200
236
|
#htmlCacheBypass = 0;
|
|
@@ -444,8 +480,8 @@ export class WebRouter {
|
|
|
444
480
|
try {
|
|
445
481
|
this.#requestStats.fullSsr += 1;
|
|
446
482
|
const manifest = await this.#ensureRoute(url);
|
|
447
|
-
const
|
|
448
|
-
const cachedHtml =
|
|
483
|
+
const htmlCacheEntry = this.#getHtmlCacheEntry(req, url);
|
|
484
|
+
const cachedHtml = htmlCacheEntry ? this.#getCachedHtml(htmlCacheEntry.key) : null;
|
|
449
485
|
if (cachedHtml) {
|
|
450
486
|
return new Response(cachedHtml, {
|
|
451
487
|
headers: {
|
|
@@ -462,8 +498,10 @@ export class WebRouter {
|
|
|
462
498
|
return Response.redirect(new URL(rscResult.location, url.origin), rscResult.status);
|
|
463
499
|
if (rscResult.type === "not-found") return this.#renderNotFoundResponse(req, url);
|
|
464
500
|
const themeCookieExists = WebRouter.#hasCookie(req, "theme");
|
|
501
|
+
const hostRequestStore = createRequestStore(req);
|
|
465
502
|
const htmlStream = await new SsrFromRscRenderer().render({
|
|
466
503
|
request: req,
|
|
504
|
+
requestStore: hostRequestStore,
|
|
467
505
|
rscStream: rscResult.stream,
|
|
468
506
|
ssrManifest: manifest.ssrManifest,
|
|
469
507
|
bootstrapModules: [this.#artifact.rscClientUrl],
|
|
@@ -471,7 +509,7 @@ export class WebRouter {
|
|
|
471
509
|
importmap: this.#artifact.vendorMap,
|
|
472
510
|
theme: themeCookieExists ? undefined : (rscResult.theme ?? "system"),
|
|
473
511
|
lateControl: rscResult.lateControl,
|
|
474
|
-
onCancel: (reason) => {
|
|
512
|
+
onCancel: (reason: unknown) => {
|
|
475
513
|
rscResult.cancel(reason);
|
|
476
514
|
},
|
|
477
515
|
});
|
|
@@ -479,17 +517,38 @@ export class WebRouter {
|
|
|
479
517
|
const responseHeaders = WebRouter.#htmlResponseHeaders(responseStatus);
|
|
480
518
|
if (req.method === "HEAD") {
|
|
481
519
|
const headers = new Headers(responseHeaders);
|
|
482
|
-
if (
|
|
520
|
+
if (htmlCacheEntry && responseStatus === 200) headers.set("X-Akan-Cache", "MISS");
|
|
483
521
|
cancelStreamForHeadResponse(htmlStream, new Error("HEAD response does not consume body"));
|
|
484
522
|
return new Response(null, { status: responseStatus, headers });
|
|
485
523
|
}
|
|
486
|
-
if (
|
|
524
|
+
if (htmlCacheEntry && responseStatus === 200) {
|
|
487
525
|
const headers = new Headers(responseHeaders);
|
|
488
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;
|
|
539
|
+
},
|
|
540
|
+
);
|
|
489
541
|
return new Response(
|
|
490
|
-
cacheHtmlWhileStreaming(
|
|
491
|
-
|
|
492
|
-
|
|
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
|
+
),
|
|
493
552
|
{
|
|
494
553
|
status: responseStatus,
|
|
495
554
|
headers,
|
|
@@ -533,23 +592,19 @@ export class WebRouter {
|
|
|
533
592
|
httpHtmlCacheBypass: this.#htmlCacheBypass,
|
|
534
593
|
};
|
|
535
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
|
+
|
|
536
602
|
/**
|
|
537
603
|
* Reconstruct origin as the browser saw it when behind Ingress / reverse proxies
|
|
538
604
|
* (prevents `/__rsc` same-origin rejecting because `req.url` is internal).
|
|
539
605
|
*/
|
|
540
606
|
static #clientFacingOrigin(req: Request): string {
|
|
541
|
-
|
|
542
|
-
const fwdProto = req.headers.get("x-forwarded-proto")?.split(",")[0]?.trim();
|
|
543
|
-
const fwdHost = req.headers.get("x-forwarded-host")?.split(",")[0]?.trim();
|
|
544
|
-
const hostFallback = fwdHost ?? req.headers.get("host");
|
|
545
|
-
const protoFallback = fwdProto ?? parsed.protocol.slice(0, -1);
|
|
546
|
-
if (hostFallback && protoFallback) {
|
|
547
|
-
try {
|
|
548
|
-
return new URL(`${protoFallback}://${hostFallback}`).origin;
|
|
549
|
-
} catch {
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
return parsed.origin;
|
|
607
|
+
return getClientFacingOrigin(req);
|
|
553
608
|
}
|
|
554
609
|
|
|
555
610
|
static #basePathForRequestHost(req: Request, subRoutes: Record<string, string[]>): string | null {
|
|
@@ -576,33 +631,25 @@ export class WebRouter {
|
|
|
576
631
|
static #hasCookie(req: Request, name: string): boolean {
|
|
577
632
|
return parseCookieHeader(req.headers.get("cookie") ?? "").has(name);
|
|
578
633
|
}
|
|
579
|
-
#
|
|
634
|
+
#getHtmlCacheEntry(req: Request, url: URL): RouteCacheEntry | null {
|
|
580
635
|
if (!this.#prodMode || process.env.AKAN_HTML_RESULT_CACHE !== "1") {
|
|
581
636
|
this.#htmlCacheBypass += 1;
|
|
582
637
|
return null;
|
|
583
638
|
}
|
|
584
|
-
if (!
|
|
639
|
+
if (!isPublicRouteCacheableRequest(req)) {
|
|
585
640
|
this.#htmlCacheBypass += 1;
|
|
586
641
|
return null;
|
|
587
642
|
}
|
|
588
|
-
if (!
|
|
643
|
+
if (!isHtmlRouteCachePathAllowed(url.pathname)) {
|
|
589
644
|
this.#htmlCacheBypass += 1;
|
|
590
645
|
return null;
|
|
591
646
|
}
|
|
592
|
-
const ttl =
|
|
593
|
-
if (ttl
|
|
647
|
+
const ttl = normalizeRouteCacheTtl(process.env.AKAN_HTML_RESULT_CACHE_TTL);
|
|
648
|
+
if (ttl === null) {
|
|
594
649
|
this.#htmlCacheBypass += 1;
|
|
595
650
|
return null;
|
|
596
651
|
}
|
|
597
|
-
return
|
|
598
|
-
WebRouter.#clientFacingOrigin(req),
|
|
599
|
-
req.headers.get("x-base-path") ?? "",
|
|
600
|
-
url.pathname,
|
|
601
|
-
url.search,
|
|
602
|
-
req.headers.get("accept-language") ?? "",
|
|
603
|
-
WebRouter.#cookieValue(req, "theme") ?? "",
|
|
604
|
-
ttl,
|
|
605
|
-
].join("\n");
|
|
652
|
+
return createRouteCacheEntry({ request: req, url, theme: WebRouter.#cookieValue(req, "theme"), ttl });
|
|
606
653
|
}
|
|
607
654
|
|
|
608
655
|
#getCachedHtml(cacheKey: string): string | null {
|
|
@@ -611,58 +658,18 @@ export class WebRouter {
|
|
|
611
658
|
this.#htmlCacheMisses += 1;
|
|
612
659
|
return null;
|
|
613
660
|
}
|
|
614
|
-
if (cached.expiresAt <= Date.now()) {
|
|
615
|
-
this.#htmlCache.delete(cacheKey);
|
|
616
|
-
this.#htmlCacheMisses += 1;
|
|
617
|
-
return null;
|
|
618
|
-
}
|
|
619
661
|
this.#htmlCacheHits += 1;
|
|
620
662
|
return cached.html;
|
|
621
663
|
}
|
|
622
664
|
|
|
623
|
-
#setCachedHtml(cacheKey: string, html: string): void {
|
|
624
|
-
|
|
625
|
-
const maxEntries = WebRouter.#positiveIntEnv("AKAN_HTML_RESULT_CACHE_MAX_ENTRIES") ?? 100;
|
|
626
|
-
while (this.#htmlCache.size >= maxEntries) {
|
|
627
|
-
const firstKey = this.#htmlCache.keys().next().value;
|
|
628
|
-
if (!firstKey) break;
|
|
629
|
-
this.#htmlCache.delete(firstKey);
|
|
630
|
-
}
|
|
631
|
-
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);
|
|
632
667
|
}
|
|
633
668
|
|
|
634
669
|
static #cookieValue(req: Request, name: string): string | undefined {
|
|
635
670
|
return parseCookieHeader(req.headers.get("cookie") ?? "").get(name)?.value;
|
|
636
671
|
}
|
|
637
672
|
|
|
638
|
-
static #isPublicCacheableRequest(req: Request): boolean {
|
|
639
|
-
if (req.method !== "GET") return false;
|
|
640
|
-
if (req.headers.has("authorization")) return false;
|
|
641
|
-
const cookie = req.headers.get("cookie");
|
|
642
|
-
if (!cookie) return true;
|
|
643
|
-
return [...parseCookieHeader(cookie).keys()].every((name) => name === "theme" || name.startsWith("akan_public_"));
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
static #htmlCacheTtlSeconds(): number {
|
|
647
|
-
return WebRouter.#positiveIntEnv("AKAN_HTML_RESULT_CACHE_TTL") ?? 30;
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
static #isHtmlCachePathAllowed(pathname: string): boolean {
|
|
651
|
-
const prefixes = (process.env.AKAN_HTML_RESULT_CACHE_PATHS ?? "")
|
|
652
|
-
.split(",")
|
|
653
|
-
.map((prefix) => prefix.trim())
|
|
654
|
-
.filter(Boolean);
|
|
655
|
-
if (prefixes.length === 0) return false;
|
|
656
|
-
return prefixes.some(
|
|
657
|
-
(prefix) => pathname === prefix || pathname.startsWith(prefix.endsWith("/") ? prefix : `${prefix}/`),
|
|
658
|
-
);
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
static #positiveIntEnv(name: string): number | null {
|
|
662
|
-
const parsed = Number.parseInt(process.env[name] ?? "", 10);
|
|
663
|
-
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
|
|
664
|
-
}
|
|
665
|
-
|
|
666
673
|
static #isImageOptimizerPath(pathname: string): boolean {
|
|
667
674
|
return pathname === "/_akan/image" || pathname.endsWith("/_akan/image");
|
|
668
675
|
}
|
|
@@ -756,10 +763,7 @@ export class WebRouter {
|
|
|
756
763
|
return `${html.slice(0, last.index)}${snippet}\n${html.slice(last.index)}`;
|
|
757
764
|
}
|
|
758
765
|
static #rscNotFoundResponse(): Response {
|
|
759
|
-
return
|
|
760
|
-
status: 404,
|
|
761
|
-
headers: { "Content-Type": "text/plain; charset=utf-8", "Cache-Control": "no-store" },
|
|
762
|
-
});
|
|
766
|
+
return createRscNotFoundFallbackResponse();
|
|
763
767
|
}
|
|
764
768
|
#getProductionRouteCache() {
|
|
765
769
|
return new RouteClientCache({
|
|
@@ -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,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>;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { type AkanDynamicUsage, type AkanRequestPolicy } from "akanjs/fetch";
|
|
2
|
+
export declare const DEFAULT_ROUTE_CACHE_TTL_SECONDS = 30;
|
|
3
|
+
export interface RouteCacheKeyInput {
|
|
4
|
+
request: Request;
|
|
5
|
+
url: URL;
|
|
6
|
+
theme?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface RouteCacheRenderState {
|
|
9
|
+
cacheable: boolean;
|
|
10
|
+
revalidate?: number | false;
|
|
11
|
+
tags?: string[];
|
|
12
|
+
dynamicUsage?: AkanDynamicUsage;
|
|
13
|
+
reason?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface RouteCacheEntry {
|
|
16
|
+
key: string;
|
|
17
|
+
ttl: number;
|
|
18
|
+
}
|
|
19
|
+
export type RouteCacheRenderControlType = "redirect" | "not-found" | "error";
|
|
20
|
+
export declare function parsePositiveInt(value: string | undefined | null): number | null;
|
|
21
|
+
export declare function normalizeRouteCacheTtl(value: unknown, fallback?: number): number | null;
|
|
22
|
+
export declare function resolveAutoRouteCacheTtl(input: {
|
|
23
|
+
enabled?: string | null;
|
|
24
|
+
ttl?: string | null;
|
|
25
|
+
defaultTtl?: number;
|
|
26
|
+
}): number | null;
|
|
27
|
+
export declare function combineMinRevalidate(...values: Array<number | false | null | undefined>): number | false | undefined;
|
|
28
|
+
export declare function getClientFacingOrigin(request: Request, url?: URL): string;
|
|
29
|
+
export declare function isPublicRouteCacheableRequest(request: Request): boolean;
|
|
30
|
+
export declare function isRouteCachePathAllowed(pathname: string, options?: {
|
|
31
|
+
allow?: string | null;
|
|
32
|
+
deny?: string | null;
|
|
33
|
+
}): boolean;
|
|
34
|
+
export declare function createRouteCacheKey({ request, url, theme }: RouteCacheKeyInput): string;
|
|
35
|
+
export declare function createRouteCacheEntry(input: RouteCacheKeyInput & {
|
|
36
|
+
ttl: number;
|
|
37
|
+
}): RouteCacheEntry;
|
|
38
|
+
export declare function resolveRouteCacheStoreTtl(baseTtl: number, state: RouteCacheRenderState): number | null;
|
|
39
|
+
export declare function shouldStoreRouteCache(input: {
|
|
40
|
+
policy?: AkanRequestPolicy;
|
|
41
|
+
dynamicUsage?: AkanDynamicUsage;
|
|
42
|
+
renderControlType?: RouteCacheRenderControlType;
|
|
43
|
+
lateRedirect?: boolean;
|
|
44
|
+
}): RouteCacheRenderState;
|
|
45
|
+
export declare class LruTtlCache<T> {
|
|
46
|
+
#private;
|
|
47
|
+
readonly maxEntries: number;
|
|
48
|
+
constructor(maxEntries?: number);
|
|
49
|
+
get size(): number;
|
|
50
|
+
get(key: string): T | null;
|
|
51
|
+
set(key: string, value: T, ttlSeconds: number): void;
|
|
52
|
+
delete(key: string): boolean;
|
|
53
|
+
invalidate(predicate: (key: string, value: T) => boolean): number;
|
|
54
|
+
clear(): void;
|
|
55
|
+
}
|