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
package/server/rscWorker.tsx
CHANGED
|
@@ -6,13 +6,35 @@ import type {
|
|
|
6
6
|
RedirectStatus,
|
|
7
7
|
} from "akanjs/client";
|
|
8
8
|
import { type AkanI18nConfig, DEFAULT_AKAN_I18N, getBasePathFromPathname, Logger } from "akanjs/common";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
getRequestDynamicUsage,
|
|
11
|
+
getRequestPolicy,
|
|
12
|
+
getRequestTheme,
|
|
13
|
+
requestStorage,
|
|
14
|
+
untrackedCookies,
|
|
15
|
+
untrackedRequest,
|
|
16
|
+
updateRequestPolicy,
|
|
17
|
+
} from "akanjs/fetch";
|
|
10
18
|
import type { ReactNode } from "react";
|
|
11
19
|
import { renderToReadableStream } from "react-server-dom-webpack/server.node";
|
|
12
20
|
import type { ClientManifest } from "./artifact";
|
|
21
|
+
import {
|
|
22
|
+
createRouteCacheEntry,
|
|
23
|
+
isPublicRouteCacheableRequest,
|
|
24
|
+
isRouteCachePathAllowed,
|
|
25
|
+
LruTtlCache,
|
|
26
|
+
parsePositiveInt,
|
|
27
|
+
type RouteCacheEntry,
|
|
28
|
+
type RouteCacheRenderState,
|
|
29
|
+
resolveAutoRouteCacheTtl,
|
|
30
|
+
resolveRouteCacheStoreTtl,
|
|
31
|
+
shouldStoreRouteCache,
|
|
32
|
+
} from "./cachePolicy";
|
|
33
|
+
import { shouldRenderLocaleAlternates } from "./metadata";
|
|
13
34
|
import { ProcessMetricsCollector } from "./processMetricsCollector";
|
|
14
35
|
import { RouteElementComposer } from "./routeElementComposer";
|
|
15
36
|
import { type PagesContext, RouteTreeBuilder } from "./routeTreeBuilder";
|
|
37
|
+
import { encodeAkanRedirectDigest } from "./rscHttp";
|
|
16
38
|
import { replayCachedRscResult } from "./rscWorkerReplay";
|
|
17
39
|
import { createSystemPageDocument, getSystemPageHomeHref } from "./systemPages";
|
|
18
40
|
|
|
@@ -49,7 +71,11 @@ interface UpdateCssAssetsMsg {
|
|
|
49
71
|
type: "updateCssAssets";
|
|
50
72
|
cssAssets: Record<string, { cssUrl: string; cssRelPath: string }>;
|
|
51
73
|
}
|
|
52
|
-
|
|
74
|
+
interface InvalidateCacheMsg {
|
|
75
|
+
type: "invalidate-cache";
|
|
76
|
+
reason?: string;
|
|
77
|
+
}
|
|
78
|
+
type InMsg = InitMsg | RenderMsg | CancelMsg | ReloadMsg | UpdateCssAssetsMsg | InvalidateCacheMsg;
|
|
53
79
|
type RenderControl =
|
|
54
80
|
| { type: "redirect"; location: string; method: "replace" | "push"; status: RedirectStatus }
|
|
55
81
|
| { type: "not-found" }
|
|
@@ -87,11 +113,11 @@ interface RouteRenderStats {
|
|
|
87
113
|
}
|
|
88
114
|
|
|
89
115
|
interface CachedRscResult {
|
|
90
|
-
expiresAt: number;
|
|
91
116
|
chunks: Uint8Array[];
|
|
92
117
|
bytes: number;
|
|
93
118
|
chunksCount: number;
|
|
94
119
|
theme?: string;
|
|
120
|
+
cacheState: RouteCacheRenderState;
|
|
95
121
|
}
|
|
96
122
|
|
|
97
123
|
export function isAkanRedirectError(error: unknown): error is AkanRedirectError {
|
|
@@ -140,7 +166,9 @@ class RscRenderer {
|
|
|
140
166
|
pagesBundleBuildId: 0,
|
|
141
167
|
};
|
|
142
168
|
readonly #routeStats = new Map<string, RouteRenderStats>();
|
|
143
|
-
|
|
169
|
+
#resultCache = new LruTtlCache<CachedRscResult>(
|
|
170
|
+
parsePositiveInt(process.env.AKAN_RSC_RESULT_CACHE_MAX_ENTRIES) ?? 100,
|
|
171
|
+
);
|
|
144
172
|
readonly #activeRenderReaders = new Map<string, ReadableStreamDefaultReader<Uint8Array>>();
|
|
145
173
|
readonly #cancelledRenderRequests = new Set<string>();
|
|
146
174
|
#resultCacheHits = 0;
|
|
@@ -185,6 +213,10 @@ class RscRenderer {
|
|
|
185
213
|
this.#logger.verbose(`received updateCssAssets count=${Object.keys(msg.cssAssets).length}`);
|
|
186
214
|
this.#cssAssets = msg.cssAssets;
|
|
187
215
|
return;
|
|
216
|
+
case "invalidate-cache":
|
|
217
|
+
this.#logger.verbose(`received invalidate-cache reason=${msg.reason ?? "(none)"}`);
|
|
218
|
+
this.#resultCache.clear();
|
|
219
|
+
return;
|
|
188
220
|
}
|
|
189
221
|
}
|
|
190
222
|
|
|
@@ -316,8 +348,8 @@ class RscRenderer {
|
|
|
316
348
|
);
|
|
317
349
|
else this.#logger.verbose(`render[${requestId}] no route matched pathname=${urlObj.pathname} — rendering 404`);
|
|
318
350
|
const beforeLoadedKeys = RouteTreeBuilder.getCacheStats().loadedModuleKeys;
|
|
319
|
-
const
|
|
320
|
-
const cached =
|
|
351
|
+
const cacheEntry = match ? this.#getResultCacheEntry(request, urlObj) : null;
|
|
352
|
+
const cached = cacheEntry ? this.#getCachedResult(cacheEntry.key) : null;
|
|
321
353
|
if (cached) {
|
|
322
354
|
this.#stats.lastRenderDurationMs = Date.now() - startedAt;
|
|
323
355
|
this.#stats.lastRenderLoadedModuleDelta = 0;
|
|
@@ -331,12 +363,13 @@ class RscRenderer {
|
|
|
331
363
|
requestId,
|
|
332
364
|
chunks: cached.chunks,
|
|
333
365
|
theme: cached.theme,
|
|
366
|
+
cacheState: cached.cacheState,
|
|
334
367
|
send: (message) => this.#send(message),
|
|
335
368
|
isCancelled: () => this.#cancelledRenderRequests.has(requestId),
|
|
336
369
|
});
|
|
337
370
|
return;
|
|
338
371
|
}
|
|
339
|
-
const theme =
|
|
372
|
+
const theme = untrackedCookies().get("theme")?.value;
|
|
340
373
|
const searchParams = RouteTreeBuilder.parseSearchParams(urlObj.search);
|
|
341
374
|
let element: ReactNode;
|
|
342
375
|
if (match) element = await this.#renderMatched(urlObj, match, theme, searchParams);
|
|
@@ -344,8 +377,31 @@ class RscRenderer {
|
|
|
344
377
|
this.#logger.verbose(`render[${requestId}] starting Flight stream`);
|
|
345
378
|
const result = await this.#renderFlightElement(element, msg.clientManifest ?? this.#clientManifest, {
|
|
346
379
|
requestId,
|
|
347
|
-
collectChunks:
|
|
380
|
+
collectChunks: cacheEntry !== null,
|
|
348
381
|
status: match ? undefined : 404,
|
|
382
|
+
onComplete: ({ chunks, bytes, chunksCount, control, lateControlSent }) => {
|
|
383
|
+
const cacheState = shouldStoreRouteCache({
|
|
384
|
+
policy: getRequestPolicy(),
|
|
385
|
+
dynamicUsage: getRequestDynamicUsage(),
|
|
386
|
+
renderControlType: control?.type,
|
|
387
|
+
lateRedirect: control?.type === "redirect" && lateControlSent,
|
|
388
|
+
});
|
|
389
|
+
const storeTtl = cacheEntry ? resolveRouteCacheStoreTtl(cacheEntry.ttl, cacheState) : null;
|
|
390
|
+
if (cacheEntry && storeTtl !== null) {
|
|
391
|
+
this.#setCachedResult(
|
|
392
|
+
cacheEntry.key,
|
|
393
|
+
{
|
|
394
|
+
chunks,
|
|
395
|
+
bytes,
|
|
396
|
+
chunksCount,
|
|
397
|
+
theme: getRequestTheme(),
|
|
398
|
+
cacheState,
|
|
399
|
+
},
|
|
400
|
+
storeTtl,
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
return cacheState;
|
|
404
|
+
},
|
|
349
405
|
});
|
|
350
406
|
if (result.cancelled) return;
|
|
351
407
|
const control = result.control;
|
|
@@ -383,6 +439,16 @@ class RscRenderer {
|
|
|
383
439
|
) {
|
|
384
440
|
return;
|
|
385
441
|
}
|
|
442
|
+
if (
|
|
443
|
+
control.type === "not-found" &&
|
|
444
|
+
(await this.#trySendSystemNotFoundRender({
|
|
445
|
+
requestId,
|
|
446
|
+
url: urlObj,
|
|
447
|
+
clientManifest: msg.clientManifest ?? this.#clientManifest,
|
|
448
|
+
}))
|
|
449
|
+
) {
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
386
452
|
this.#sendRenderControl(requestId, control);
|
|
387
453
|
return;
|
|
388
454
|
}
|
|
@@ -396,15 +462,10 @@ class RscRenderer {
|
|
|
396
462
|
this.#stats.lastRenderLoadedModuleDelta = this.#stats.lastRenderLoadedModules.length;
|
|
397
463
|
this.#recordRouteStats(routeId, result.bytes, this.#stats.lastRenderDurationMs);
|
|
398
464
|
const responseTheme = getRequestTheme();
|
|
399
|
-
if (cacheKey)
|
|
400
|
-
this.#setCachedResult(cacheKey, {
|
|
401
|
-
chunks: result.chunks,
|
|
402
|
-
bytes: result.bytes,
|
|
403
|
-
chunksCount: result.chunksCount,
|
|
404
|
-
theme: responseTheme,
|
|
405
|
-
});
|
|
406
465
|
this.#logger.verbose(
|
|
407
|
-
`render[${requestId}] done chunks=${result.chunksCount} bytes=${result.bytes}
|
|
466
|
+
`render[${requestId}] done chunks=${result.chunksCount} bytes=${result.bytes} theme=${responseTheme ?? "(none)"} in ${
|
|
467
|
+
Date.now() - startedAt
|
|
468
|
+
}ms`,
|
|
408
469
|
);
|
|
409
470
|
});
|
|
410
471
|
} catch (error) {
|
|
@@ -441,6 +502,16 @@ class RscRenderer {
|
|
|
441
502
|
) {
|
|
442
503
|
return;
|
|
443
504
|
}
|
|
505
|
+
if (
|
|
506
|
+
fallbackUrl &&
|
|
507
|
+
(await this.#trySendSystemNotFoundRender({
|
|
508
|
+
requestId,
|
|
509
|
+
url: fallbackUrl,
|
|
510
|
+
clientManifest: msg.clientManifest ?? this.#clientManifest,
|
|
511
|
+
}))
|
|
512
|
+
) {
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
444
515
|
this.#send({ type: "not-found", requestId });
|
|
445
516
|
return;
|
|
446
517
|
}
|
|
@@ -527,6 +598,13 @@ class RscRenderer {
|
|
|
527
598
|
requestId?: string;
|
|
528
599
|
collectChunks?: boolean;
|
|
529
600
|
status?: number;
|
|
601
|
+
onComplete?: (result: {
|
|
602
|
+
chunks: Uint8Array[];
|
|
603
|
+
bytes: number;
|
|
604
|
+
chunksCount: number;
|
|
605
|
+
control: RenderControl | null;
|
|
606
|
+
lateControlSent: boolean;
|
|
607
|
+
}) => Promise<RouteCacheRenderState> | RouteCacheRenderState;
|
|
530
608
|
} = {},
|
|
531
609
|
): Promise<FlightRenderResult> {
|
|
532
610
|
const controlRef: { current: RenderControl | null } = { current: null };
|
|
@@ -539,7 +617,11 @@ class RscRenderer {
|
|
|
539
617
|
method: error.method,
|
|
540
618
|
status: error.status,
|
|
541
619
|
};
|
|
542
|
-
return
|
|
620
|
+
return encodeAkanRedirectDigest({
|
|
621
|
+
location: error.location,
|
|
622
|
+
method: error.method,
|
|
623
|
+
status: error.status,
|
|
624
|
+
});
|
|
543
625
|
}
|
|
544
626
|
if (isAkanNotFoundError(error)) {
|
|
545
627
|
controlRef.current = { type: "not-found" };
|
|
@@ -611,6 +693,14 @@ class RscRenderer {
|
|
|
611
693
|
return { chunks, bytes, chunksCount, control: controlRef.current, lateControlSent, cancelled: false };
|
|
612
694
|
if (options.requestId) {
|
|
613
695
|
sendMeta();
|
|
696
|
+
const cacheState = (await options.onComplete?.({
|
|
697
|
+
chunks,
|
|
698
|
+
bytes,
|
|
699
|
+
chunksCount,
|
|
700
|
+
control: controlRef.current,
|
|
701
|
+
lateControlSent,
|
|
702
|
+
})) ?? { cacheable: false, reason: "uncacheable-render" };
|
|
703
|
+
this.#send({ type: "cache-state", requestId: options.requestId, state: cacheState });
|
|
614
704
|
this.#send({ type: "end", requestId: options.requestId });
|
|
615
705
|
}
|
|
616
706
|
return {
|
|
@@ -677,6 +767,37 @@ class RscRenderer {
|
|
|
677
767
|
}
|
|
678
768
|
}
|
|
679
769
|
|
|
770
|
+
async #trySendSystemNotFoundRender({
|
|
771
|
+
requestId,
|
|
772
|
+
url,
|
|
773
|
+
clientManifest,
|
|
774
|
+
}: {
|
|
775
|
+
requestId: string;
|
|
776
|
+
url: URL;
|
|
777
|
+
clientManifest: ClientManifest;
|
|
778
|
+
}): Promise<boolean> {
|
|
779
|
+
try {
|
|
780
|
+
const result = await this.#renderFlightElement(this.#renderSystemNotFound(url), clientManifest, {
|
|
781
|
+
requestId,
|
|
782
|
+
status: 404,
|
|
783
|
+
});
|
|
784
|
+
if (result.cancelled) return true;
|
|
785
|
+
if (result.control) return false;
|
|
786
|
+
this.#stats.lastFlightBytes = result.bytes;
|
|
787
|
+
this.#stats.lastFlightChunks = result.chunksCount;
|
|
788
|
+
this.#stats.totalFlightBytes += result.bytes;
|
|
789
|
+
this.#stats.totalFlightChunks += result.chunksCount;
|
|
790
|
+
return true;
|
|
791
|
+
} catch (error) {
|
|
792
|
+
this.#logger.error(
|
|
793
|
+
`render[${requestId}] system not-found fallback failed: ${
|
|
794
|
+
error instanceof Error ? (error.stack ?? error.message) : String(error)
|
|
795
|
+
}`,
|
|
796
|
+
);
|
|
797
|
+
return false;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
680
801
|
#sendRenderControl(requestId: string, control: RenderControl): void {
|
|
681
802
|
if (control.type === "redirect") {
|
|
682
803
|
this.#logger.verbose(`render[${requestId}] redirect ${control.location}`);
|
|
@@ -719,35 +840,29 @@ class RscRenderer {
|
|
|
719
840
|
}));
|
|
720
841
|
}
|
|
721
842
|
|
|
722
|
-
|
|
723
|
-
const
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
rscCache: config?.rscCache,
|
|
727
|
-
rscCacheTtl: config?.rscCacheTtl,
|
|
728
|
-
cacheable: config?.rscCache === "public" || ttl !== null,
|
|
843
|
+
#getResultCacheEntry(request: Request, url: URL): RouteCacheEntry | null {
|
|
844
|
+
const ttl = resolveAutoRouteCacheTtl({
|
|
845
|
+
enabled: process.env.AKAN_RSC_RESULT_CACHE,
|
|
846
|
+
ttl: process.env.AKAN_RSC_RESULT_CACHE_TTL,
|
|
729
847
|
});
|
|
730
|
-
if (
|
|
848
|
+
if (ttl === null) {
|
|
731
849
|
this.#resultCacheBypass += 1;
|
|
732
850
|
return null;
|
|
733
851
|
}
|
|
734
|
-
if (
|
|
852
|
+
if (
|
|
853
|
+
!isRouteCachePathAllowed(url.pathname, {
|
|
854
|
+
allow: process.env.AKAN_RSC_RESULT_CACHE_PATHS,
|
|
855
|
+
deny: process.env.AKAN_RSC_RESULT_CACHE_EXCLUDE_PATHS,
|
|
856
|
+
})
|
|
857
|
+
) {
|
|
735
858
|
this.#resultCacheBypass += 1;
|
|
736
859
|
return null;
|
|
737
860
|
}
|
|
738
|
-
if (!
|
|
861
|
+
if (!isPublicRouteCacheableRequest(request)) {
|
|
739
862
|
this.#resultCacheBypass += 1;
|
|
740
863
|
return null;
|
|
741
864
|
}
|
|
742
|
-
return
|
|
743
|
-
request.headers.get("x-forwarded-host") ?? request.headers.get("host") ?? url.host,
|
|
744
|
-
request.headers.get("x-base-path") ?? "",
|
|
745
|
-
url.pathname,
|
|
746
|
-
url.search,
|
|
747
|
-
request.headers.get("accept-language") ?? "",
|
|
748
|
-
cookies().get("theme")?.value ?? "",
|
|
749
|
-
ttl ?? 30,
|
|
750
|
-
].join("\n");
|
|
865
|
+
return createRouteCacheEntry({ request, url, theme: untrackedCookies().get("theme")?.value, ttl });
|
|
751
866
|
}
|
|
752
867
|
|
|
753
868
|
#getCachedResult(cacheKey: string): CachedRscResult | null {
|
|
@@ -756,27 +871,12 @@ class RscRenderer {
|
|
|
756
871
|
this.#resultCacheMisses += 1;
|
|
757
872
|
return null;
|
|
758
873
|
}
|
|
759
|
-
if (cached.expiresAt <= Date.now()) {
|
|
760
|
-
this.#resultCache.delete(cacheKey);
|
|
761
|
-
this.#resultCacheMisses += 1;
|
|
762
|
-
return null;
|
|
763
|
-
}
|
|
764
874
|
this.#resultCacheHits += 1;
|
|
765
875
|
return cached;
|
|
766
876
|
}
|
|
767
877
|
|
|
768
|
-
#setCachedResult(
|
|
769
|
-
cacheKey
|
|
770
|
-
result: { chunks: Uint8Array[]; bytes: number; chunksCount: number; theme?: string },
|
|
771
|
-
): void {
|
|
772
|
-
const ttl = Number.parseInt(cacheKey.split("\n").at(-1) ?? "30", 10);
|
|
773
|
-
const maxEntries = RscRenderer.#parsePositiveIntEnv("AKAN_RSC_RESULT_CACHE_MAX_ENTRIES") ?? 100;
|
|
774
|
-
while (this.#resultCache.size >= maxEntries) {
|
|
775
|
-
const firstKey = this.#resultCache.keys().next().value;
|
|
776
|
-
if (!firstKey) break;
|
|
777
|
-
this.#resultCache.delete(firstKey);
|
|
778
|
-
}
|
|
779
|
-
this.#resultCache.set(cacheKey, { ...result, expiresAt: Date.now() + ttl * 1000 });
|
|
878
|
+
#setCachedResult(cacheKey: string, result: CachedRscResult, ttl: number): void {
|
|
879
|
+
this.#resultCache.set(cacheKey, result, ttl);
|
|
780
880
|
}
|
|
781
881
|
|
|
782
882
|
#runWithRequest<T>(request: Request, fn: () => Promise<T>): Promise<T> {
|
|
@@ -813,8 +913,18 @@ class RscRenderer {
|
|
|
813
913
|
digest,
|
|
814
914
|
});
|
|
815
915
|
if (!body) return null;
|
|
816
|
-
const routeHead =
|
|
817
|
-
|
|
916
|
+
const routeHead =
|
|
917
|
+
"resolveHead" in route
|
|
918
|
+
? await RouteElementComposer.resolveHeadWithMetadata({
|
|
919
|
+
pathRoute: route,
|
|
920
|
+
params,
|
|
921
|
+
searchParams,
|
|
922
|
+
})
|
|
923
|
+
: { node: undefined, hasExplicitLanguageAlternates: false };
|
|
924
|
+
const renderLocaleAlternates = shouldRenderLocaleAlternates({
|
|
925
|
+
hasExplicitLanguageAlternates: routeHead.hasExplicitLanguageAlternates,
|
|
926
|
+
});
|
|
927
|
+
const theme = untrackedCookies().get("theme")?.value;
|
|
818
928
|
return (
|
|
819
929
|
<html
|
|
820
930
|
lang={params.lang ?? RscRenderer.#getLocale(pathname, this.#i18n)}
|
|
@@ -824,8 +934,8 @@ class RscRenderer {
|
|
|
824
934
|
<meta key="charset" charSet="utf-8" />
|
|
825
935
|
<meta key="viewport" name="viewport" content="width=device-width, initial-scale=1" />
|
|
826
936
|
<meta key="robots" name="robots" content="noindex" />
|
|
827
|
-
{routeHead ?? this.#renderDefaultHead()}
|
|
828
|
-
{this.#renderLocaleAlternates(url)}
|
|
937
|
+
{routeHead.node ?? this.#renderDefaultHead()}
|
|
938
|
+
{renderLocaleAlternates ? this.#renderLocaleAlternates(url) : null}
|
|
829
939
|
{this.#renderStylesheet(pathname)}
|
|
830
940
|
</head>
|
|
831
941
|
<body key="body">{body}</body>
|
|
@@ -842,11 +952,15 @@ class RscRenderer {
|
|
|
842
952
|
this.#logger.verbose(
|
|
843
953
|
`composing route element pathname=${url.pathname} search=${url.search || "(none)"} params=${JSON.stringify(match.params)}`,
|
|
844
954
|
);
|
|
845
|
-
const routeHead = await RouteElementComposer.
|
|
955
|
+
const routeHead = await RouteElementComposer.resolveHeadWithMetadata({
|
|
846
956
|
pathRoute: match.pathRoute,
|
|
847
957
|
params: match.params,
|
|
848
958
|
searchParams,
|
|
849
959
|
});
|
|
960
|
+
const renderLocaleAlternates = shouldRenderLocaleAlternates({
|
|
961
|
+
isSpecialRoute: match.pathRoute.isSpecialRoute,
|
|
962
|
+
hasExplicitLanguageAlternates: routeHead.hasExplicitLanguageAlternates,
|
|
963
|
+
});
|
|
850
964
|
const body = RouteElementComposer.compose({
|
|
851
965
|
pathRoute: match.pathRoute,
|
|
852
966
|
params: match.params,
|
|
@@ -860,8 +974,8 @@ class RscRenderer {
|
|
|
860
974
|
<head key="head">
|
|
861
975
|
<meta key="charset" charSet="utf-8" />
|
|
862
976
|
<meta key="viewport" name="viewport" content="width=device-width, initial-scale=1" />
|
|
863
|
-
{routeHead ?? this.#renderDefaultHead()}
|
|
864
|
-
{
|
|
977
|
+
{routeHead.node ?? this.#renderDefaultHead()}
|
|
978
|
+
{renderLocaleAlternates ? this.#renderLocaleAlternates(url) : null}
|
|
865
979
|
{this.#renderStylesheet(url.pathname)}
|
|
866
980
|
</head>
|
|
867
981
|
<body key="body">{body}</body>
|
|
@@ -900,7 +1014,7 @@ class RscRenderer {
|
|
|
900
1014
|
pathname: url.pathname,
|
|
901
1015
|
i18n: this.#i18n,
|
|
902
1016
|
basePaths: this.#basePaths,
|
|
903
|
-
headerBasePath:
|
|
1017
|
+
headerBasePath: untrackedRequest()?.headers.get("x-base-path"),
|
|
904
1018
|
}),
|
|
905
1019
|
stylesheetHref: this.#getStylesheetHref(url.pathname),
|
|
906
1020
|
});
|
|
@@ -938,7 +1052,7 @@ class RscRenderer {
|
|
|
938
1052
|
const basePath = getBasePathFromPathname(pathname, {
|
|
939
1053
|
basePaths: Object.keys(this.#cssAssets),
|
|
940
1054
|
i18n: this.#i18n,
|
|
941
|
-
headerBasePath:
|
|
1055
|
+
headerBasePath: untrackedRequest()?.headers.get("x-base-path"),
|
|
942
1056
|
});
|
|
943
1057
|
return this.#cssAssets[basePath ?? ""]?.cssUrl ?? null;
|
|
944
1058
|
}
|
|
@@ -948,29 +1062,6 @@ class RscRenderer {
|
|
|
948
1062
|
return segment && i18n.locales.includes(segment) ? segment : i18n.defaultLocale;
|
|
949
1063
|
}
|
|
950
1064
|
|
|
951
|
-
static #parsePositiveIntEnv(name: string): number | null {
|
|
952
|
-
const parsed = Number.parseInt(process.env[name] ?? "", 10);
|
|
953
|
-
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
static #normalizeCacheTtl(value: unknown): number | null {
|
|
957
|
-
if (value === undefined || value === null) return null;
|
|
958
|
-
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return null;
|
|
959
|
-
return Math.floor(value);
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
static #isPublicCacheableRequest(request: Request): boolean {
|
|
963
|
-
if (request.method !== "GET") return false;
|
|
964
|
-
if (request.headers.has("authorization")) return false;
|
|
965
|
-
const cookie = request.headers.get("cookie");
|
|
966
|
-
if (!cookie) return true;
|
|
967
|
-
return cookie
|
|
968
|
-
.split(";")
|
|
969
|
-
.map((part) => part.trim().split("=")[0])
|
|
970
|
-
.filter(Boolean)
|
|
971
|
-
.every((name) => name === "theme" || name.startsWith("akan_public_"));
|
|
972
|
-
}
|
|
973
|
-
|
|
974
1065
|
static #errorForFallback(error: unknown): unknown {
|
|
975
1066
|
if (process.env.NODE_ENV !== "production") return error;
|
|
976
1067
|
return undefined;
|
|
@@ -978,7 +1069,7 @@ class RscRenderer {
|
|
|
978
1069
|
|
|
979
1070
|
static #getPublicRequestUrl(url: URL): URL {
|
|
980
1071
|
const publicUrl = new URL(url);
|
|
981
|
-
const req =
|
|
1072
|
+
const req = untrackedRequest();
|
|
982
1073
|
const headers = req?.headers;
|
|
983
1074
|
const host = headers?.get("x-forwarded-host") ?? headers?.get("host");
|
|
984
1075
|
const proto = headers?.get("x-forwarded-proto");
|
package/server/rscWorkerHost.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { type AkanI18nConfig, DEFAULT_AKAN_I18N, Logger } from "akanjs/common";
|
|
|
5
5
|
import type { AkanTheme } from "akanjs/fetch";
|
|
6
6
|
import type { AkanMetricsReport } from "akanjs/service";
|
|
7
7
|
import type { ClientManifest } from "./artifact";
|
|
8
|
+
import type { RouteCacheRenderState } from "./cachePolicy";
|
|
8
9
|
import type { SsrLateRedirect } from "./ssrTypes";
|
|
9
10
|
import type { BaseBuildArtifact, CssAsset } from "./types";
|
|
10
11
|
|
|
@@ -15,6 +16,7 @@ export interface RscPending {
|
|
|
15
16
|
onEnd: () => void;
|
|
16
17
|
onError: (message: string) => void;
|
|
17
18
|
onMeta?: (meta: { theme?: AkanTheme; status?: number }) => void;
|
|
19
|
+
onCacheState?: (state: RouteCacheRenderState) => void;
|
|
18
20
|
onRedirect?: (location: string, method: RscRedirectMethod, status: RscRedirectStatus) => void;
|
|
19
21
|
onLateRedirect?: (location: string, method: RscRedirectMethod, status: RscRedirectStatus) => void;
|
|
20
22
|
onNotFound?: () => void;
|
|
@@ -23,6 +25,11 @@ export interface RscPending {
|
|
|
23
25
|
export type RscRedirectMethod = "replace" | "push";
|
|
24
26
|
export type RscRedirectStatus = 303 | 307 | 308;
|
|
25
27
|
|
|
28
|
+
export interface RscWorkerInvalidateCacheMessage {
|
|
29
|
+
type: "invalidate-cache";
|
|
30
|
+
reason?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
26
33
|
export type RscRenderResult =
|
|
27
34
|
| {
|
|
28
35
|
type: "stream";
|
|
@@ -30,6 +37,7 @@ export type RscRenderResult =
|
|
|
30
37
|
theme?: AkanTheme;
|
|
31
38
|
status?: number;
|
|
32
39
|
lateControl: Promise<SsrLateRedirect | null>;
|
|
40
|
+
cacheState: Promise<RouteCacheRenderState>;
|
|
33
41
|
cancel: (reason?: unknown) => void;
|
|
34
42
|
}
|
|
35
43
|
| { type: "redirect"; location: string; method: RscRedirectMethod; status: RscRedirectStatus }
|
|
@@ -64,6 +72,10 @@ export function createIdempotentRscRenderCancel(onCancel: (reason?: unknown) =>
|
|
|
64
72
|
};
|
|
65
73
|
}
|
|
66
74
|
|
|
75
|
+
export function createRscWorkerInvalidateCacheMessage(reason?: string): RscWorkerInvalidateCacheMessage {
|
|
76
|
+
return reason ? { type: "invalidate-cache", reason } : { type: "invalidate-cache" };
|
|
77
|
+
}
|
|
78
|
+
|
|
67
79
|
export function createRscHostRenderStream(input: {
|
|
68
80
|
setPending: (pending: RscPending) => void;
|
|
69
81
|
deletePending: () => void;
|
|
@@ -78,15 +90,25 @@ export function createRscHostRenderStream(input: {
|
|
|
78
90
|
let theme: AkanTheme | undefined;
|
|
79
91
|
let status: number | undefined;
|
|
80
92
|
let resolveLateControl!: (control: SsrLateRedirect | null) => void;
|
|
93
|
+
let resolveCacheState!: (state: RouteCacheRenderState) => void;
|
|
81
94
|
const lateControl = new Promise<SsrLateRedirect | null>((resolve) => {
|
|
82
95
|
resolveLateControl = resolve;
|
|
83
96
|
});
|
|
97
|
+
const cacheState = new Promise<RouteCacheRenderState>((resolve) => {
|
|
98
|
+
resolveCacheState = resolve;
|
|
99
|
+
});
|
|
84
100
|
let lateControlSettled = false;
|
|
101
|
+
let cacheStateSettled = false;
|
|
85
102
|
const settleLateControl = (control: SsrLateRedirect | null) => {
|
|
86
103
|
if (lateControlSettled) return;
|
|
87
104
|
lateControlSettled = true;
|
|
88
105
|
resolveLateControl(control);
|
|
89
106
|
};
|
|
107
|
+
const settleCacheState = (state: RouteCacheRenderState) => {
|
|
108
|
+
if (cacheStateSettled) return;
|
|
109
|
+
cacheStateSettled = true;
|
|
110
|
+
resolveCacheState(state);
|
|
111
|
+
};
|
|
90
112
|
const maxPendingChunks = input.maxPendingChunks ?? getRscHostMaxPendingChunks();
|
|
91
113
|
let pendingChunks = 0;
|
|
92
114
|
let removeAbortListener: (() => void) | undefined;
|
|
@@ -97,6 +119,7 @@ export function createRscHostRenderStream(input: {
|
|
|
97
119
|
const cancelRender = createIdempotentRscRenderCancel((reason) => {
|
|
98
120
|
input.deletePending();
|
|
99
121
|
settleLateControl(null);
|
|
122
|
+
settleCacheState({ cacheable: false, reason: "cancelled" });
|
|
100
123
|
input.cancelRender(reason);
|
|
101
124
|
cleanupAbortListener();
|
|
102
125
|
});
|
|
@@ -117,7 +140,7 @@ export function createRscHostRenderStream(input: {
|
|
|
117
140
|
const settleStream = () => {
|
|
118
141
|
if (settled) return;
|
|
119
142
|
settled = true;
|
|
120
|
-
resolve({ type: "stream", stream, theme, status, lateControl, cancel: cancelRender });
|
|
143
|
+
resolve({ type: "stream", stream, theme, status, lateControl, cacheState, cancel: cancelRender });
|
|
121
144
|
};
|
|
122
145
|
input.setPending({
|
|
123
146
|
onMeta: (meta) => {
|
|
@@ -140,12 +163,14 @@ export function createRscHostRenderStream(input: {
|
|
|
140
163
|
},
|
|
141
164
|
onEnd: () => {
|
|
142
165
|
settleLateControl(null);
|
|
166
|
+
settleCacheState({ cacheable: false, reason: "missing-cache-state" });
|
|
143
167
|
cleanupAbortListener();
|
|
144
168
|
settleStream();
|
|
145
169
|
controller.close();
|
|
146
170
|
},
|
|
147
171
|
onError: (msg) => {
|
|
148
172
|
settleLateControl(null);
|
|
173
|
+
settleCacheState({ cacheable: false, reason: "error" });
|
|
149
174
|
cleanupAbortListener();
|
|
150
175
|
if (!settled) {
|
|
151
176
|
settled = true;
|
|
@@ -156,6 +181,7 @@ export function createRscHostRenderStream(input: {
|
|
|
156
181
|
},
|
|
157
182
|
onRedirect: (location, method, status) => {
|
|
158
183
|
settleLateControl(null);
|
|
184
|
+
settleCacheState({ cacheable: false, reason: "redirect" });
|
|
159
185
|
cleanupAbortListener();
|
|
160
186
|
if (!settled) {
|
|
161
187
|
settled = true;
|
|
@@ -168,8 +194,12 @@ export function createRscHostRenderStream(input: {
|
|
|
168
194
|
onLateRedirect: (location, method, status) => {
|
|
169
195
|
settleLateControl({ type: "redirect", location, method, status });
|
|
170
196
|
},
|
|
197
|
+
onCacheState: (state) => {
|
|
198
|
+
settleCacheState(state);
|
|
199
|
+
},
|
|
171
200
|
onNotFound: () => {
|
|
172
201
|
settleLateControl(null);
|
|
202
|
+
settleCacheState({ cacheable: false, reason: "not-found" });
|
|
173
203
|
cleanupAbortListener();
|
|
174
204
|
if (!settled) {
|
|
175
205
|
settled = true;
|
|
@@ -205,6 +235,7 @@ type RscInMsg =
|
|
|
205
235
|
| { type: "ready" }
|
|
206
236
|
| { type: "reloaded"; buildId: number }
|
|
207
237
|
| { type: "meta"; requestId: string; theme?: AkanTheme; status?: number }
|
|
238
|
+
| { type: "cache-state"; requestId: string; state: RouteCacheRenderState }
|
|
208
239
|
| { type: "chunk"; requestId: string; data: Uint8Array }
|
|
209
240
|
| { type: "end"; requestId: string }
|
|
210
241
|
| { type: "redirect"; requestId: string; location: string; method?: RscRedirectMethod; status?: RscRedirectStatus }
|
|
@@ -386,6 +417,17 @@ export class RscWorker {
|
|
|
386
417
|
});
|
|
387
418
|
}
|
|
388
419
|
|
|
420
|
+
invalidateRouteResultCache(reason?: string): void {
|
|
421
|
+
if (this.#status !== "ready") return;
|
|
422
|
+
try {
|
|
423
|
+
this.#proc.send(createRscWorkerInvalidateCacheMessage(reason));
|
|
424
|
+
} catch (error) {
|
|
425
|
+
this.#logger.warn(
|
|
426
|
+
`rsc worker cache invalidate send failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
389
431
|
kill(): void {
|
|
390
432
|
this.#killed = true;
|
|
391
433
|
this.#status = "stopped";
|
|
@@ -527,6 +569,10 @@ export class RscWorker {
|
|
|
527
569
|
|
|
528
570
|
#handleMessage(message: RscInMsg, proc: Bun.Subprocess<"ignore", "inherit", "inherit">): void {
|
|
529
571
|
if (proc !== this.#proc) return;
|
|
572
|
+
if (message.type === "cache-state") {
|
|
573
|
+
this.#pending.get(message.requestId)?.onCacheState?.(message.state);
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
530
576
|
switch (message.type) {
|
|
531
577
|
case "hello":
|
|
532
578
|
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import type { RouteCacheRenderState } from "./cachePolicy";
|
|
2
|
+
|
|
1
3
|
export type CachedRscReplayMessage =
|
|
2
4
|
| { type: "meta"; requestId: string; theme?: string; status?: number }
|
|
5
|
+
| { type: "cache-state"; requestId: string; state: RouteCacheRenderState }
|
|
3
6
|
| { type: "chunk"; requestId: string; data: Uint8Array }
|
|
4
7
|
| { type: "end"; requestId: string };
|
|
5
8
|
|
|
@@ -12,6 +15,7 @@ export async function replayCachedRscResult(input: {
|
|
|
12
15
|
chunks: readonly Uint8Array[];
|
|
13
16
|
theme?: string;
|
|
14
17
|
status?: number;
|
|
18
|
+
cacheState?: RouteCacheRenderState;
|
|
15
19
|
send: (message: CachedRscReplayMessage) => void;
|
|
16
20
|
isCancelled: () => boolean;
|
|
17
21
|
yieldEveryChunks?: number;
|
|
@@ -24,6 +28,7 @@ export async function replayCachedRscResult(input: {
|
|
|
24
28
|
const yieldToHost = input.yieldToHost ?? yieldToHostEventLoop;
|
|
25
29
|
if (input.isCancelled()) return false;
|
|
26
30
|
input.send({ type: "meta", requestId: input.requestId, theme: input.theme, status: input.status });
|
|
31
|
+
input.send({ type: "cache-state", requestId: input.requestId, state: input.cacheState ?? { cacheable: true } });
|
|
27
32
|
for (let index = 0; index < input.chunks.length; index += 1) {
|
|
28
33
|
if (input.isCancelled()) return false;
|
|
29
34
|
input.send({ type: "chunk", requestId: input.requestId, data: input.chunks[index] });
|