akanjs 2.3.0 → 2.3.1-rc.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 +16 -0
- package/constant/fieldInfo.ts +11 -9
- package/constant/getDefault.ts +1 -1
- package/fetch/requestStorage.ts +5 -0
- package/package.json +4 -4
- package/server/akanApp.ts +26 -3
- package/server/akanServer.ts +5 -1
- package/server/cachePolicy.ts +99 -5
- package/server/imageOptimizer.ts +14 -1
- package/server/metadata.tsx +117 -33
- package/server/resolver/database.resolver.ts +4 -4
- package/server/routeElementComposer.tsx +46 -14
- package/server/routeState.ts +379 -0
- package/server/routeTreeBuilder.ts +3 -2
- package/server/rscClient.tsx +316 -46
- package/server/rscClientFetch.ts +57 -0
- package/server/rscClientPatch.ts +157 -0
- package/server/rscHeadPatch.ts +80 -0
- package/server/rscNavigationState.ts +315 -0
- package/server/rscPartialCommit.ts +3 -0
- package/server/rscPatchSafety.ts +57 -0
- package/server/rscSegmentOutlet.tsx +69 -0
- package/server/rscSegmentOutletReference.ts +24 -0
- package/server/rscWorker.tsx +380 -53
- package/server/rscWorkerCache.ts +180 -0
- package/server/rscWorkerHost.ts +40 -12
- package/server/rscWorkerReplay.ts +11 -2
- package/server/ssrFromRscRenderer.tsx +15 -10
- package/server/ssrTypes.ts +18 -0
- package/server/types.tsx +4 -0
- package/server/webRouter.ts +198 -42
- package/service/predefinedAdaptor/database.adaptor.ts +72 -25
- package/signal/signalContext.ts +1 -1
- package/types/client/csrTypes.d.ts +16 -0
- package/types/constant/fieldInfo.d.ts +8 -7
- package/types/fetch/requestStorage.d.ts +2 -0
- package/types/server/cachePolicy.d.ts +36 -0
- package/types/server/metadata.d.ts +10 -1
- package/types/server/routeElementComposer.d.ts +9 -1
- package/types/server/routeState.d.ts +94 -0
- package/types/server/rscClient.d.ts +1 -0
- package/types/server/rscClientFetch.d.ts +24 -0
- package/types/server/rscClientPatch.d.ts +21 -0
- package/types/server/rscHeadPatch.d.ts +12 -0
- package/types/server/rscNavigationState.d.ts +78 -0
- package/types/server/rscPartialCommit.d.ts +1 -0
- package/types/server/rscPatchSafety.d.ts +8 -0
- package/types/server/rscSegmentOutlet.d.ts +17 -0
- package/types/server/rscSegmentOutletReference.d.ts +2 -0
- package/types/server/rscWorker.d.ts +5 -0
- package/types/server/rscWorkerCache.d.ts +63 -0
- package/types/server/rscWorkerHost.d.ts +8 -4
- package/types/server/rscWorkerReplay.d.ts +3 -0
- package/types/server/ssrFromRscRenderer.d.ts +1 -0
- package/types/server/ssrTypes.d.ts +17 -0
- package/types/server/types.d.ts +4 -0
- package/types/server/webRouter.d.ts +7 -3
- package/types/service/predefinedAdaptor/database.adaptor.d.ts +6 -0
- 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/Constant/schemaDoc.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/ui/Constant/schemaDoc.ts +1 -1
- package/server/resolver/resolver.contract.fixture.ts +0 -222
package/server/rscWorker.tsx
CHANGED
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
LayoutFallbackRoute,
|
|
5
5
|
PathRoute,
|
|
6
6
|
RedirectStatus,
|
|
7
|
+
ResolvedHead,
|
|
7
8
|
} from "akanjs/client";
|
|
8
9
|
import { type AkanI18nConfig, DEFAULT_AKAN_I18N, getBasePathFromPathname, Logger } from "akanjs/common";
|
|
9
10
|
import {
|
|
@@ -19,23 +20,50 @@ import type { ReactNode } from "react";
|
|
|
19
20
|
import { renderToReadableStream } from "react-server-dom-webpack/server.node";
|
|
20
21
|
import type { ClientManifest } from "./artifact";
|
|
21
22
|
import {
|
|
22
|
-
createRouteCacheEntry,
|
|
23
|
-
isPublicRouteCacheableRequest,
|
|
24
|
-
isRouteCachePathAllowed,
|
|
25
23
|
LruTtlCache,
|
|
26
24
|
parsePositiveInt,
|
|
27
25
|
type RouteCacheEntry,
|
|
26
|
+
type RouteCacheInvalidation,
|
|
28
27
|
type RouteCacheRenderState,
|
|
29
|
-
|
|
28
|
+
resolvePublicRouteCacheEntryDecision,
|
|
30
29
|
resolveRouteCacheStoreTtl,
|
|
31
30
|
shouldStoreRouteCache,
|
|
32
31
|
} from "./cachePolicy";
|
|
33
|
-
import {
|
|
32
|
+
import {
|
|
33
|
+
createAkanLocaleAlternateHeadSnapshot,
|
|
34
|
+
mergeAkanHeadSnapshots,
|
|
35
|
+
renderAkanHeadSnapshot,
|
|
36
|
+
shouldRenderLocaleAlternates,
|
|
37
|
+
} from "./metadata";
|
|
34
38
|
import { ProcessMetricsCollector } from "./processMetricsCollector";
|
|
35
39
|
import { RouteElementComposer } from "./routeElementComposer";
|
|
40
|
+
import {
|
|
41
|
+
type AkanRscPatchDecision,
|
|
42
|
+
createAkanRouterState,
|
|
43
|
+
encodeAkanHeadSnapshot,
|
|
44
|
+
encodeAkanRouterState,
|
|
45
|
+
encodeAkanRscPatchSegmentPath,
|
|
46
|
+
readAkanRouterStateRequest,
|
|
47
|
+
resolveAkanRscPartialDecision,
|
|
48
|
+
resolveAkanRscPatchDecision,
|
|
49
|
+
} from "./routeState";
|
|
36
50
|
import { type PagesContext, RouteTreeBuilder } from "./routeTreeBuilder";
|
|
37
51
|
import { encodeAkanRedirectDigest } from "./rscHttp";
|
|
52
|
+
import { isAkanRscPartialCommitEnabled } from "./rscPartialCommit";
|
|
53
|
+
import { resolveAkanRscHeadSafePatchDecision } from "./rscPatchSafety";
|
|
54
|
+
import {
|
|
55
|
+
type CachedRscResult,
|
|
56
|
+
createCachedRscPatchMetadata,
|
|
57
|
+
createRscWorkerCachedPatchReplayDecision,
|
|
58
|
+
invalidateCachedRscResults,
|
|
59
|
+
isCachedRscPatchMetadataCompatible,
|
|
60
|
+
resolveRscWorkerPatchCacheEntry,
|
|
61
|
+
shouldCollectRscWorkerRenderChunks,
|
|
62
|
+
shouldStoreRscWorkerPatchResult,
|
|
63
|
+
shouldUseRscWorkerFullResultCache,
|
|
64
|
+
} from "./rscWorkerCache";
|
|
38
65
|
import { replayCachedRscResult } from "./rscWorkerReplay";
|
|
66
|
+
import type { RscTraceMetadata } from "./ssrTypes";
|
|
39
67
|
import { createSystemPageDocument, getSystemPageHomeHref } from "./systemPages";
|
|
40
68
|
|
|
41
69
|
interface InitMsg {
|
|
@@ -74,6 +102,8 @@ interface UpdateCssAssetsMsg {
|
|
|
74
102
|
interface InvalidateCacheMsg {
|
|
75
103
|
type: "invalidate-cache";
|
|
76
104
|
reason?: string;
|
|
105
|
+
tags?: string[];
|
|
106
|
+
paths?: string[];
|
|
77
107
|
}
|
|
78
108
|
type InMsg = InitMsg | RenderMsg | CancelMsg | ReloadMsg | UpdateCssAssetsMsg | InvalidateCacheMsg;
|
|
79
109
|
type RenderControl =
|
|
@@ -89,6 +119,12 @@ interface FlightRenderResult {
|
|
|
89
119
|
cancelled: boolean;
|
|
90
120
|
}
|
|
91
121
|
|
|
122
|
+
function hashRscTraceCacheKey(cacheKey: string): string {
|
|
123
|
+
let hash = 5381;
|
|
124
|
+
for (let index = 0; index < cacheKey.length; index += 1) hash = (hash * 33) ^ cacheKey.charCodeAt(index);
|
|
125
|
+
return (hash >>> 0).toString(36);
|
|
126
|
+
}
|
|
127
|
+
|
|
92
128
|
interface RscRendererStats {
|
|
93
129
|
renderCount: number;
|
|
94
130
|
inFlightRenderCount: number;
|
|
@@ -112,14 +148,6 @@ interface RouteRenderStats {
|
|
|
112
148
|
totalDurationMs: number;
|
|
113
149
|
}
|
|
114
150
|
|
|
115
|
-
interface CachedRscResult {
|
|
116
|
-
chunks: Uint8Array[];
|
|
117
|
-
bytes: number;
|
|
118
|
-
chunksCount: number;
|
|
119
|
-
theme?: string;
|
|
120
|
-
cacheState: RouteCacheRenderState;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
151
|
export function isAkanRedirectError(error: unknown): error is AkanRedirectError {
|
|
124
152
|
return (
|
|
125
153
|
typeof error === "object" &&
|
|
@@ -142,7 +170,7 @@ export function isAkanNotFoundError(error: unknown): error is AkanNotFoundError
|
|
|
142
170
|
);
|
|
143
171
|
}
|
|
144
172
|
|
|
145
|
-
class RscRenderer {
|
|
173
|
+
export class RscRenderer {
|
|
146
174
|
readonly #logger = new Logger("scWorker");
|
|
147
175
|
#clientManifest: ClientManifest = {};
|
|
148
176
|
#pathRoutes: PathRoute[] = [];
|
|
@@ -169,6 +197,9 @@ class RscRenderer {
|
|
|
169
197
|
#resultCache = new LruTtlCache<CachedRscResult>(
|
|
170
198
|
parsePositiveInt(process.env.AKAN_RSC_RESULT_CACHE_MAX_ENTRIES) ?? 100,
|
|
171
199
|
);
|
|
200
|
+
#patchResultCache = new LruTtlCache<CachedRscResult>(
|
|
201
|
+
parsePositiveInt(process.env.AKAN_RSC_RESULT_CACHE_MAX_ENTRIES) ?? 100,
|
|
202
|
+
);
|
|
172
203
|
readonly #activeRenderReaders = new Map<string, ReadableStreamDefaultReader<Uint8Array>>();
|
|
173
204
|
readonly #cancelledRenderRequests = new Set<string>();
|
|
174
205
|
#resultCacheHits = 0;
|
|
@@ -215,7 +246,7 @@ class RscRenderer {
|
|
|
215
246
|
return;
|
|
216
247
|
case "invalidate-cache":
|
|
217
248
|
this.#logger.verbose(`received invalidate-cache reason=${msg.reason ?? "(none)"}`);
|
|
218
|
-
this.#
|
|
249
|
+
this.#invalidateResultCache(msg);
|
|
219
250
|
return;
|
|
220
251
|
}
|
|
221
252
|
}
|
|
@@ -228,6 +259,11 @@ class RscRenderer {
|
|
|
228
259
|
});
|
|
229
260
|
}
|
|
230
261
|
|
|
262
|
+
#invalidateResultCache(invalidation: RouteCacheInvalidation): void {
|
|
263
|
+
invalidateCachedRscResults(this.#resultCache, invalidation);
|
|
264
|
+
invalidateCachedRscResults(this.#patchResultCache, invalidation);
|
|
265
|
+
}
|
|
266
|
+
|
|
231
267
|
async #handleInit(msg: InitMsg): Promise<void> {
|
|
232
268
|
const startedAt = Date.now();
|
|
233
269
|
try {
|
|
@@ -240,6 +276,7 @@ class RscRenderer {
|
|
|
240
276
|
this.#stats.pagesBundleBuildId = msg.pagesBundleBuildId;
|
|
241
277
|
this.#routeStats.clear();
|
|
242
278
|
this.#resultCache.clear();
|
|
279
|
+
this.#patchResultCache.clear();
|
|
243
280
|
this.#logger.verbose(
|
|
244
281
|
`init state pagesBundlePath=${msg.pagesBundlePath} buildId=${msg.pagesBundleBuildId} cssAssets=${Object.keys(this.#cssAssets).length} clientEntries=${Object.keys(msg.clientManifest).length}`,
|
|
245
282
|
);
|
|
@@ -284,6 +321,7 @@ class RscRenderer {
|
|
|
284
321
|
this.#fallbackRoutes = routes.fallbackRoutes;
|
|
285
322
|
this.#routeStats.clear();
|
|
286
323
|
this.#resultCache.clear();
|
|
324
|
+
this.#patchResultCache.clear();
|
|
287
325
|
this.#logger.verbose(`reload complete buildId=${msg.buildId} in ${Date.now() - startedAt}ms`);
|
|
288
326
|
this.#send({ type: "reloaded", buildId: msg.buildId });
|
|
289
327
|
} catch (error) {
|
|
@@ -348,8 +386,119 @@ class RscRenderer {
|
|
|
348
386
|
);
|
|
349
387
|
else this.#logger.verbose(`render[${requestId}] no route matched pathname=${urlObj.pathname} — rendering 404`);
|
|
350
388
|
const beforeLoadedKeys = RouteTreeBuilder.getCacheStats().loadedModuleKeys;
|
|
351
|
-
const
|
|
352
|
-
const
|
|
389
|
+
const cacheDecision = match ? this.#getResultCacheEntry(request, urlObj) : { entry: null };
|
|
390
|
+
const cacheEntry = cacheDecision.entry;
|
|
391
|
+
const targetRouterState = match
|
|
392
|
+
? createAkanRouterState({
|
|
393
|
+
pathRoute: match.pathRoute,
|
|
394
|
+
href: urlObj.href,
|
|
395
|
+
buildId: this.#pagesBundleBuildId,
|
|
396
|
+
})
|
|
397
|
+
: null;
|
|
398
|
+
const searchParams = RouteTreeBuilder.parseSearchParams(urlObj.search);
|
|
399
|
+
const currentRouterState = readAkanRouterStateRequest(request.headers);
|
|
400
|
+
const partialDecision = targetRouterState
|
|
401
|
+
? resolveAkanRscPartialDecision({
|
|
402
|
+
currentState: currentRouterState.state,
|
|
403
|
+
currentRoute: currentRouterState.currentRoute,
|
|
404
|
+
targetState: targetRouterState,
|
|
405
|
+
})
|
|
406
|
+
: { status: "full" as const, reason: "missing-route", commonPrefixLength: 0 };
|
|
407
|
+
const patchDecision: AkanRscPatchDecision =
|
|
408
|
+
targetRouterState && match
|
|
409
|
+
? resolveAkanRscPatchDecision({
|
|
410
|
+
currentState: currentRouterState.state,
|
|
411
|
+
targetState: targetRouterState,
|
|
412
|
+
partialDecision,
|
|
413
|
+
})
|
|
414
|
+
: { status: "full" as const, reason: partialDecision.reason, commonPrefixLength: 0 };
|
|
415
|
+
const safePatchDecision = match
|
|
416
|
+
? await this.#resolveHeadSafePatchDecision(
|
|
417
|
+
match.pathRoute,
|
|
418
|
+
patchDecision,
|
|
419
|
+
await this.#resolveRouteHeadSnapshot(urlObj, match, searchParams),
|
|
420
|
+
)
|
|
421
|
+
: patchDecision;
|
|
422
|
+
const patchCacheEntry = resolveRscWorkerPatchCacheEntry({
|
|
423
|
+
cacheEntry,
|
|
424
|
+
targetRouterState,
|
|
425
|
+
safePatchDecision,
|
|
426
|
+
partialCommitEnabled: isAkanRscPartialCommitEnabled(),
|
|
427
|
+
});
|
|
428
|
+
const createTraceBase = (
|
|
429
|
+
decision: AkanRscPatchDecision,
|
|
430
|
+
cacheKey = cacheEntry?.key,
|
|
431
|
+
routeState = targetRouterState,
|
|
432
|
+
) => ({
|
|
433
|
+
navId: requestId,
|
|
434
|
+
pathname: urlObj.pathname,
|
|
435
|
+
routeId,
|
|
436
|
+
partial: decision.status,
|
|
437
|
+
partialReason: decision.reason ?? currentRouterState.reason,
|
|
438
|
+
partialCommonPrefixLength: decision.commonPrefixLength,
|
|
439
|
+
...(decision.patch
|
|
440
|
+
? {
|
|
441
|
+
patchStartIndex: decision.patch.patchStartIndex,
|
|
442
|
+
patchSegmentPath: encodeAkanRscPatchSegmentPath(decision.patch.segmentPath),
|
|
443
|
+
patchStartSegment: decision.patch.patchStartSegmentKey,
|
|
444
|
+
patchHeadSafe: decision.patch.headSafe,
|
|
445
|
+
patchHeadSnapshot: decision.patch.headSnapshot
|
|
446
|
+
? (encodeAkanHeadSnapshot(decision.patch.headSnapshot) ?? undefined)
|
|
447
|
+
: undefined,
|
|
448
|
+
}
|
|
449
|
+
: {}),
|
|
450
|
+
...(routeState ? { routeState: encodeAkanRouterState(routeState) } : {}),
|
|
451
|
+
...(cacheKey ? { cacheKeyHash: hashRscTraceCacheKey(cacheKey) } : {}),
|
|
452
|
+
...(cacheDecision.reason ? { cacheReason: cacheDecision.reason } : {}),
|
|
453
|
+
});
|
|
454
|
+
const traceBase = createTraceBase(safePatchDecision, patchCacheEntry?.key ?? cacheEntry?.key);
|
|
455
|
+
const cachedPatch = patchCacheEntry ? this.#getCachedPatchResult(patchCacheEntry.key) : null;
|
|
456
|
+
if (
|
|
457
|
+
cachedPatch?.patch &&
|
|
458
|
+
patchCacheEntry &&
|
|
459
|
+
isCachedRscPatchMetadataCompatible({
|
|
460
|
+
cached: cachedPatch.patch,
|
|
461
|
+
targetRouterState,
|
|
462
|
+
safePatchDecision,
|
|
463
|
+
})
|
|
464
|
+
) {
|
|
465
|
+
const cachedPatchDecision = createRscWorkerCachedPatchReplayDecision({
|
|
466
|
+
cached: cachedPatch.patch,
|
|
467
|
+
safePatchDecision,
|
|
468
|
+
});
|
|
469
|
+
const cachedTraceBase = createTraceBase(
|
|
470
|
+
cachedPatchDecision,
|
|
471
|
+
patchCacheEntry.key,
|
|
472
|
+
cachedPatch.patch.targetRouterState,
|
|
473
|
+
);
|
|
474
|
+
this.#stats.lastRenderDurationMs = Date.now() - startedAt;
|
|
475
|
+
this.#stats.lastRenderLoadedModuleDelta = 0;
|
|
476
|
+
this.#stats.lastRenderLoadedModules = [];
|
|
477
|
+
this.#stats.lastFlightBytes = cachedPatch.bytes;
|
|
478
|
+
this.#stats.lastFlightChunks = cachedPatch.chunksCount;
|
|
479
|
+
this.#stats.totalFlightBytes += cachedPatch.bytes;
|
|
480
|
+
this.#stats.totalFlightChunks += cachedPatch.chunksCount;
|
|
481
|
+
this.#recordRouteStats(routeId, cachedPatch.bytes, this.#stats.lastRenderDurationMs);
|
|
482
|
+
await replayCachedRscResult({
|
|
483
|
+
requestId,
|
|
484
|
+
chunks: cachedPatch.chunks,
|
|
485
|
+
theme: cachedPatch.theme,
|
|
486
|
+
cacheState: cachedPatch.cacheState,
|
|
487
|
+
trace: {
|
|
488
|
+
...cachedTraceBase,
|
|
489
|
+
cache: "hit",
|
|
490
|
+
partial: "patch",
|
|
491
|
+
partialReason: "cache-hit-patch-replay",
|
|
492
|
+
},
|
|
493
|
+
send: (message) => this.#send(message),
|
|
494
|
+
isCancelled: () => this.#cancelledRenderRequests.has(requestId),
|
|
495
|
+
});
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
const cached =
|
|
499
|
+
shouldUseRscWorkerFullResultCache({ cacheEntry, patchCacheEntry }) && cacheEntry
|
|
500
|
+
? this.#getCachedResult(cacheEntry.key)
|
|
501
|
+
: null;
|
|
353
502
|
if (cached) {
|
|
354
503
|
this.#stats.lastRenderDurationMs = Date.now() - startedAt;
|
|
355
504
|
this.#stats.lastRenderLoadedModuleDelta = 0;
|
|
@@ -364,21 +513,59 @@ class RscRenderer {
|
|
|
364
513
|
chunks: cached.chunks,
|
|
365
514
|
theme: cached.theme,
|
|
366
515
|
cacheState: cached.cacheState,
|
|
516
|
+
trace: {
|
|
517
|
+
...traceBase,
|
|
518
|
+
cache: "hit",
|
|
519
|
+
partial: "full",
|
|
520
|
+
partialReason: "cache-hit-full-replay",
|
|
521
|
+
partialCommonPrefixLength: 0,
|
|
522
|
+
patchStartIndex: undefined,
|
|
523
|
+
patchSegmentPath: undefined,
|
|
524
|
+
patchStartSegment: undefined,
|
|
525
|
+
patchHeadSafe: undefined,
|
|
526
|
+
patchHeadSnapshot: undefined,
|
|
527
|
+
},
|
|
367
528
|
send: (message) => this.#send(message),
|
|
368
529
|
isCancelled: () => this.#cancelledRenderRequests.has(requestId),
|
|
369
530
|
});
|
|
370
531
|
return;
|
|
371
532
|
}
|
|
372
533
|
const theme = untrackedCookies().get("theme")?.value;
|
|
373
|
-
const searchParams = RouteTreeBuilder.parseSearchParams(urlObj.search);
|
|
374
534
|
let element: ReactNode;
|
|
375
|
-
|
|
535
|
+
let effectivePatchDecision = safePatchDecision;
|
|
536
|
+
if (match && safePatchDecision.status === "patch" && safePatchDecision.patch) {
|
|
537
|
+
const suffixElement = await this.#renderMatchedSuffix(
|
|
538
|
+
urlObj,
|
|
539
|
+
match,
|
|
540
|
+
safePatchDecision.patch.patchStartIndex,
|
|
541
|
+
searchParams,
|
|
542
|
+
);
|
|
543
|
+
if (suffixElement === null) {
|
|
544
|
+
effectivePatchDecision = {
|
|
545
|
+
status: "full",
|
|
546
|
+
reason: "suffix-compose-fallback",
|
|
547
|
+
commonPrefixLength: safePatchDecision.commonPrefixLength,
|
|
548
|
+
};
|
|
549
|
+
element = await this.#renderMatched(urlObj, match, theme, searchParams);
|
|
550
|
+
} else element = suffixElement;
|
|
551
|
+
} else if (match) element = await this.#renderMatched(urlObj, match, theme, searchParams);
|
|
376
552
|
else element = await this.#renderNotFound(urlObj);
|
|
553
|
+
const traceCacheKey =
|
|
554
|
+
effectivePatchDecision.status === "patch" ? (patchCacheEntry?.key ?? cacheEntry?.key) : cacheEntry?.key;
|
|
555
|
+
const trace: RscTraceMetadata = {
|
|
556
|
+
...createTraceBase(effectivePatchDecision, traceCacheKey),
|
|
557
|
+
cache: cacheEntry ? "miss" : "bypass",
|
|
558
|
+
};
|
|
377
559
|
this.#logger.verbose(`render[${requestId}] starting Flight stream`);
|
|
378
560
|
const result = await this.#renderFlightElement(element, msg.clientManifest ?? this.#clientManifest, {
|
|
379
561
|
requestId,
|
|
380
|
-
collectChunks:
|
|
562
|
+
collectChunks: shouldCollectRscWorkerRenderChunks({
|
|
563
|
+
cacheEntry,
|
|
564
|
+
effectivePatchDecision,
|
|
565
|
+
patchCacheEntry,
|
|
566
|
+
}),
|
|
381
567
|
status: match ? undefined : 404,
|
|
568
|
+
trace,
|
|
382
569
|
onComplete: ({ chunks, bytes, chunksCount, control, lateControlSent }) => {
|
|
383
570
|
const cacheState = shouldStoreRouteCache({
|
|
384
571
|
policy: getRequestPolicy(),
|
|
@@ -387,13 +574,46 @@ class RscRenderer {
|
|
|
387
574
|
lateRedirect: control?.type === "redirect" && lateControlSent,
|
|
388
575
|
});
|
|
389
576
|
const storeTtl = cacheEntry ? resolveRouteCacheStoreTtl(cacheEntry.ttl, cacheState) : null;
|
|
390
|
-
if (
|
|
577
|
+
if (
|
|
578
|
+
shouldStoreRscWorkerPatchResult({
|
|
579
|
+
cacheEntry,
|
|
580
|
+
patchCacheEntry,
|
|
581
|
+
effectivePatchDecision,
|
|
582
|
+
storeTtl,
|
|
583
|
+
}) &&
|
|
584
|
+
patchCacheEntry &&
|
|
585
|
+
targetRouterState &&
|
|
586
|
+
effectivePatchDecision.patch &&
|
|
587
|
+
storeTtl !== null
|
|
588
|
+
) {
|
|
589
|
+
this.#setCachedPatchResult(
|
|
590
|
+
patchCacheEntry.key,
|
|
591
|
+
{
|
|
592
|
+
chunks,
|
|
593
|
+
bytes,
|
|
594
|
+
chunksCount,
|
|
595
|
+
pathname: urlObj.pathname,
|
|
596
|
+
routeId,
|
|
597
|
+
tags: cacheState.tags,
|
|
598
|
+
theme: getRequestTheme(),
|
|
599
|
+
cacheState,
|
|
600
|
+
patch: createCachedRscPatchMetadata({
|
|
601
|
+
targetRouterState,
|
|
602
|
+
patch: effectivePatchDecision.patch,
|
|
603
|
+
}),
|
|
604
|
+
},
|
|
605
|
+
storeTtl,
|
|
606
|
+
);
|
|
607
|
+
} else if (cacheEntry && storeTtl !== null && effectivePatchDecision.status !== "patch") {
|
|
391
608
|
this.#setCachedResult(
|
|
392
609
|
cacheEntry.key,
|
|
393
610
|
{
|
|
394
611
|
chunks,
|
|
395
612
|
bytes,
|
|
396
613
|
chunksCount,
|
|
614
|
+
pathname: urlObj.pathname,
|
|
615
|
+
routeId,
|
|
616
|
+
tags: cacheState.tags,
|
|
397
617
|
theme: getRequestTheme(),
|
|
398
618
|
cacheState,
|
|
399
619
|
},
|
|
@@ -415,7 +635,7 @@ class RscRenderer {
|
|
|
415
635
|
const systemResult = await this.#renderFlightElement(
|
|
416
636
|
this.#renderSystemNotFound(urlObj),
|
|
417
637
|
msg.clientManifest ?? this.#clientManifest,
|
|
418
|
-
{ requestId, status: 404 },
|
|
638
|
+
{ requestId, status: 404, trace },
|
|
419
639
|
);
|
|
420
640
|
if (systemResult.cancelled) return;
|
|
421
641
|
if (!systemResult.control) {
|
|
@@ -435,6 +655,7 @@ class RscRenderer {
|
|
|
435
655
|
url: urlObj,
|
|
436
656
|
error: control.type === "error" ? control.error : undefined,
|
|
437
657
|
clientManifest: msg.clientManifest ?? this.#clientManifest,
|
|
658
|
+
trace,
|
|
438
659
|
}))
|
|
439
660
|
) {
|
|
440
661
|
return;
|
|
@@ -445,6 +666,7 @@ class RscRenderer {
|
|
|
445
666
|
requestId,
|
|
446
667
|
url: urlObj,
|
|
447
668
|
clientManifest: msg.clientManifest ?? this.#clientManifest,
|
|
669
|
+
trace,
|
|
448
670
|
}))
|
|
449
671
|
) {
|
|
450
672
|
return;
|
|
@@ -583,7 +805,7 @@ class RscRenderer {
|
|
|
583
805
|
rscLoadedRouteModuleKeys: routeStats.loadedModuleKeys,
|
|
584
806
|
rscTopRoutesByRenderCount: this.#topRoutes((route) => route.count),
|
|
585
807
|
rscTopRoutesByFlightBytes: this.#topRoutes((route) => route.flightBytes),
|
|
586
|
-
rscResultCacheEntries: this.#resultCache.size,
|
|
808
|
+
rscResultCacheEntries: this.#resultCache.size + this.#patchResultCache.size,
|
|
587
809
|
rscResultCacheHits: this.#resultCacheHits,
|
|
588
810
|
rscResultCacheMisses: this.#resultCacheMisses,
|
|
589
811
|
rscResultCacheBypass: this.#resultCacheBypass,
|
|
@@ -598,6 +820,7 @@ class RscRenderer {
|
|
|
598
820
|
requestId?: string;
|
|
599
821
|
collectChunks?: boolean;
|
|
600
822
|
status?: number;
|
|
823
|
+
trace?: RscTraceMetadata;
|
|
601
824
|
onComplete?: (result: {
|
|
602
825
|
chunks: Uint8Array[];
|
|
603
826
|
bytes: number;
|
|
@@ -642,7 +865,13 @@ class RscRenderer {
|
|
|
642
865
|
const sendMeta = () => {
|
|
643
866
|
if (!options.requestId || sentMeta) return;
|
|
644
867
|
sentMeta = true;
|
|
645
|
-
this.#send({
|
|
868
|
+
this.#send({
|
|
869
|
+
type: "meta",
|
|
870
|
+
requestId: options.requestId,
|
|
871
|
+
theme: getRequestTheme(),
|
|
872
|
+
status: options.status,
|
|
873
|
+
trace: options.trace,
|
|
874
|
+
});
|
|
646
875
|
};
|
|
647
876
|
const sendLateRedirect = () => {
|
|
648
877
|
if (!options.requestId || lateControlSent || controlRef.current?.type !== "redirect") return;
|
|
@@ -723,6 +952,7 @@ class RscRenderer {
|
|
|
723
952
|
url,
|
|
724
953
|
error,
|
|
725
954
|
clientManifest,
|
|
955
|
+
trace,
|
|
726
956
|
}: {
|
|
727
957
|
requestId: string;
|
|
728
958
|
kind: "not-found" | "error";
|
|
@@ -733,6 +963,7 @@ class RscRenderer {
|
|
|
733
963
|
url: URL;
|
|
734
964
|
error?: unknown;
|
|
735
965
|
clientManifest: ClientManifest;
|
|
966
|
+
trace?: RscTraceMetadata;
|
|
736
967
|
}): Promise<boolean> {
|
|
737
968
|
try {
|
|
738
969
|
const element = await this.#renderFallbackDocument({
|
|
@@ -749,6 +980,7 @@ class RscRenderer {
|
|
|
749
980
|
const result = await this.#renderFlightElement(element, clientManifest, {
|
|
750
981
|
requestId,
|
|
751
982
|
status: kind === "not-found" ? 404 : 500,
|
|
983
|
+
trace,
|
|
752
984
|
});
|
|
753
985
|
if (result.cancelled) return true;
|
|
754
986
|
if (result.control) return false;
|
|
@@ -771,15 +1003,18 @@ class RscRenderer {
|
|
|
771
1003
|
requestId,
|
|
772
1004
|
url,
|
|
773
1005
|
clientManifest,
|
|
1006
|
+
trace,
|
|
774
1007
|
}: {
|
|
775
1008
|
requestId: string;
|
|
776
1009
|
url: URL;
|
|
777
1010
|
clientManifest: ClientManifest;
|
|
1011
|
+
trace?: RscTraceMetadata;
|
|
778
1012
|
}): Promise<boolean> {
|
|
779
1013
|
try {
|
|
780
1014
|
const result = await this.#renderFlightElement(this.#renderSystemNotFound(url), clientManifest, {
|
|
781
1015
|
requestId,
|
|
782
1016
|
status: 404,
|
|
1017
|
+
trace,
|
|
783
1018
|
});
|
|
784
1019
|
if (result.cancelled) return true;
|
|
785
1020
|
if (result.control) return false;
|
|
@@ -820,6 +1055,25 @@ class RscRenderer {
|
|
|
820
1055
|
this.#send({ type: "not-found", requestId });
|
|
821
1056
|
}
|
|
822
1057
|
|
|
1058
|
+
async #resolveHeadSafePatchDecision(
|
|
1059
|
+
pathRoute: PathRoute,
|
|
1060
|
+
patchDecision: AkanRscPatchDecision,
|
|
1061
|
+
headSnapshot: ResolvedHead["headSnapshot"],
|
|
1062
|
+
): Promise<AkanRscPatchDecision> {
|
|
1063
|
+
if (patchDecision.status !== "patch" || !patchDecision.patch) {
|
|
1064
|
+
return patchDecision;
|
|
1065
|
+
}
|
|
1066
|
+
if (!isAkanRscPartialCommitEnabled()) {
|
|
1067
|
+
return { status: "full", reason: "guard-disabled", commonPrefixLength: patchDecision.commonPrefixLength };
|
|
1068
|
+
}
|
|
1069
|
+
return resolveAkanRscHeadSafePatchDecision({
|
|
1070
|
+
partialCommitEnabled: true,
|
|
1071
|
+
patchDecision,
|
|
1072
|
+
pageConfig: await pathRoute.renderPage.getPageConfig?.(),
|
|
1073
|
+
headSnapshot,
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
|
|
823
1077
|
#recordRouteStats(routeId: string, flightBytes: number, durationMs: number): void {
|
|
824
1078
|
const current = this.#routeStats.get(routeId) ?? { routeId, count: 0, flightBytes: 0, totalDurationMs: 0 };
|
|
825
1079
|
current.count += 1;
|
|
@@ -840,29 +1094,22 @@ class RscRenderer {
|
|
|
840
1094
|
}));
|
|
841
1095
|
}
|
|
842
1096
|
|
|
843
|
-
#getResultCacheEntry(request: Request, url: URL): RouteCacheEntry | null {
|
|
844
|
-
const
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
!isRouteCachePathAllowed(url.pathname, {
|
|
1097
|
+
#getResultCacheEntry(request: Request, url: URL): { entry: RouteCacheEntry | null; reason?: string } {
|
|
1098
|
+
const decision = resolvePublicRouteCacheEntryDecision({
|
|
1099
|
+
request,
|
|
1100
|
+
url,
|
|
1101
|
+
theme: untrackedCookies().get("theme")?.value,
|
|
1102
|
+
defaultEnabled: process.env.NODE_ENV === "production",
|
|
1103
|
+
defaultAllow: process.env.NODE_ENV === "production",
|
|
1104
|
+
env: {
|
|
1105
|
+
enabled: process.env.AKAN_RSC_RESULT_CACHE,
|
|
1106
|
+
ttl: process.env.AKAN_RSC_RESULT_CACHE_TTL,
|
|
854
1107
|
allow: process.env.AKAN_RSC_RESULT_CACHE_PATHS,
|
|
855
1108
|
deny: process.env.AKAN_RSC_RESULT_CACHE_EXCLUDE_PATHS,
|
|
856
|
-
}
|
|
857
|
-
)
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
}
|
|
861
|
-
if (!isPublicRouteCacheableRequest(request)) {
|
|
862
|
-
this.#resultCacheBypass += 1;
|
|
863
|
-
return null;
|
|
864
|
-
}
|
|
865
|
-
return createRouteCacheEntry({ request, url, theme: untrackedCookies().get("theme")?.value, ttl });
|
|
1109
|
+
},
|
|
1110
|
+
});
|
|
1111
|
+
if (!decision.entry) this.#resultCacheBypass += 1;
|
|
1112
|
+
return decision;
|
|
866
1113
|
}
|
|
867
1114
|
|
|
868
1115
|
#getCachedResult(cacheKey: string): CachedRscResult | null {
|
|
@@ -875,10 +1122,24 @@ class RscRenderer {
|
|
|
875
1122
|
return cached;
|
|
876
1123
|
}
|
|
877
1124
|
|
|
1125
|
+
#getCachedPatchResult(cacheKey: string): CachedRscResult | null {
|
|
1126
|
+
const cached = this.#patchResultCache.get(cacheKey);
|
|
1127
|
+
if (!cached) {
|
|
1128
|
+
this.#resultCacheMisses += 1;
|
|
1129
|
+
return null;
|
|
1130
|
+
}
|
|
1131
|
+
this.#resultCacheHits += 1;
|
|
1132
|
+
return cached;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
878
1135
|
#setCachedResult(cacheKey: string, result: CachedRscResult, ttl: number): void {
|
|
879
1136
|
this.#resultCache.set(cacheKey, result, ttl);
|
|
880
1137
|
}
|
|
881
1138
|
|
|
1139
|
+
#setCachedPatchResult(cacheKey: string, result: CachedRscResult, ttl: number): void {
|
|
1140
|
+
this.#patchResultCache.set(cacheKey, result, ttl);
|
|
1141
|
+
}
|
|
1142
|
+
|
|
882
1143
|
#runWithRequest<T>(request: Request, fn: () => Promise<T>): Promise<T> {
|
|
883
1144
|
if (requestStorage) return Promise.resolve(requestStorage.run(request, fn));
|
|
884
1145
|
return fn();
|
|
@@ -921,7 +1182,7 @@ class RscRenderer {
|
|
|
921
1182
|
searchParams,
|
|
922
1183
|
})
|
|
923
1184
|
: { node: undefined, hasExplicitLanguageAlternates: false };
|
|
924
|
-
const
|
|
1185
|
+
const routeHeadSnapshot = this.#createRouteHeadSnapshot(url, routeHead, {
|
|
925
1186
|
hasExplicitLanguageAlternates: routeHead.hasExplicitLanguageAlternates,
|
|
926
1187
|
});
|
|
927
1188
|
const theme = untrackedCookies().get("theme")?.value;
|
|
@@ -934,8 +1195,13 @@ class RscRenderer {
|
|
|
934
1195
|
<meta key="charset" charSet="utf-8" />
|
|
935
1196
|
<meta key="viewport" name="viewport" content="width=device-width, initial-scale=1" />
|
|
936
1197
|
<meta key="robots" name="robots" content="noindex" />
|
|
937
|
-
{
|
|
938
|
-
|
|
1198
|
+
{routeHeadSnapshot
|
|
1199
|
+
? renderAkanHeadSnapshot(routeHeadSnapshot)
|
|
1200
|
+
: (routeHead.node ?? this.#renderDefaultHead())}
|
|
1201
|
+
{!routeHeadSnapshot &&
|
|
1202
|
+
shouldRenderLocaleAlternates({ hasExplicitLanguageAlternates: routeHead.hasExplicitLanguageAlternates })
|
|
1203
|
+
? this.#renderLocaleAlternates(url)
|
|
1204
|
+
: null}
|
|
939
1205
|
{this.#renderStylesheet(pathname)}
|
|
940
1206
|
</head>
|
|
941
1207
|
<body key="body">{body}</body>
|
|
@@ -957,7 +1223,7 @@ class RscRenderer {
|
|
|
957
1223
|
params: match.params,
|
|
958
1224
|
searchParams,
|
|
959
1225
|
});
|
|
960
|
-
const
|
|
1226
|
+
const routeHeadSnapshot = this.#createRouteHeadSnapshot(url, routeHead, {
|
|
961
1227
|
isSpecialRoute: match.pathRoute.isSpecialRoute,
|
|
962
1228
|
hasExplicitLanguageAlternates: routeHead.hasExplicitLanguageAlternates,
|
|
963
1229
|
});
|
|
@@ -974,8 +1240,16 @@ class RscRenderer {
|
|
|
974
1240
|
<head key="head">
|
|
975
1241
|
<meta key="charset" charSet="utf-8" />
|
|
976
1242
|
<meta key="viewport" name="viewport" content="width=device-width, initial-scale=1" />
|
|
977
|
-
{
|
|
978
|
-
|
|
1243
|
+
{routeHeadSnapshot
|
|
1244
|
+
? renderAkanHeadSnapshot(routeHeadSnapshot)
|
|
1245
|
+
: (routeHead.node ?? this.#renderDefaultHead())}
|
|
1246
|
+
{!routeHeadSnapshot &&
|
|
1247
|
+
shouldRenderLocaleAlternates({
|
|
1248
|
+
isSpecialRoute: match.pathRoute.isSpecialRoute,
|
|
1249
|
+
hasExplicitLanguageAlternates: routeHead.hasExplicitLanguageAlternates,
|
|
1250
|
+
})
|
|
1251
|
+
? this.#renderLocaleAlternates(url)
|
|
1252
|
+
: null}
|
|
979
1253
|
{this.#renderStylesheet(url.pathname)}
|
|
980
1254
|
</head>
|
|
981
1255
|
<body key="body">{body}</body>
|
|
@@ -983,6 +1257,23 @@ class RscRenderer {
|
|
|
983
1257
|
);
|
|
984
1258
|
}
|
|
985
1259
|
|
|
1260
|
+
async #renderMatchedSuffix(
|
|
1261
|
+
url: URL,
|
|
1262
|
+
match: { pathRoute: PathRoute; params: Record<string, string> },
|
|
1263
|
+
patchStartIndex: number,
|
|
1264
|
+
searchParams = RouteTreeBuilder.parseSearchParams(url.search),
|
|
1265
|
+
): Promise<ReactNode | null> {
|
|
1266
|
+
this.#logger.verbose(
|
|
1267
|
+
`composing route suffix pathname=${url.pathname} start=${patchStartIndex} params=${JSON.stringify(match.params)}`,
|
|
1268
|
+
);
|
|
1269
|
+
return RouteElementComposer.composeSuffix({
|
|
1270
|
+
pathRoute: match.pathRoute,
|
|
1271
|
+
params: match.params,
|
|
1272
|
+
searchParams,
|
|
1273
|
+
patchStartIndex,
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
|
|
986
1277
|
async #renderNotFound(url: URL): Promise<ReactNode> {
|
|
987
1278
|
const matchedFallback = RouteTreeBuilder.matchFallback(url.pathname, this.#fallbackRoutes);
|
|
988
1279
|
if (matchedFallback) {
|
|
@@ -1024,7 +1315,39 @@ class RscRenderer {
|
|
|
1024
1315
|
return <title key="title">{process.env.AKAN_PUBLIC_APP_NAME ?? "Akan App"}</title>;
|
|
1025
1316
|
}
|
|
1026
1317
|
|
|
1027
|
-
#
|
|
1318
|
+
async #resolveRouteHeadSnapshot(
|
|
1319
|
+
url: URL,
|
|
1320
|
+
match: { pathRoute: PathRoute; params: Record<string, string> },
|
|
1321
|
+
searchParams: Record<string, string | string[]>,
|
|
1322
|
+
): Promise<ResolvedHead["headSnapshot"]> {
|
|
1323
|
+
const routeHead = await RouteElementComposer.resolveHeadWithMetadata({
|
|
1324
|
+
pathRoute: match.pathRoute,
|
|
1325
|
+
params: match.params,
|
|
1326
|
+
searchParams,
|
|
1327
|
+
});
|
|
1328
|
+
return this.#createRouteHeadSnapshot(url, routeHead, {
|
|
1329
|
+
isSpecialRoute: match.pathRoute.isSpecialRoute,
|
|
1330
|
+
hasExplicitLanguageAlternates: routeHead.hasExplicitLanguageAlternates,
|
|
1331
|
+
});
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
#createRouteHeadSnapshot(
|
|
1335
|
+
url: URL,
|
|
1336
|
+
routeHead: ResolvedHead,
|
|
1337
|
+
options: { isSpecialRoute?: boolean; hasExplicitLanguageAlternates?: boolean },
|
|
1338
|
+
): ResolvedHead["headSnapshot"] {
|
|
1339
|
+
if (!routeHead.headSnapshot) return undefined;
|
|
1340
|
+
return mergeAkanHeadSnapshots(
|
|
1341
|
+
routeHead.headSnapshot,
|
|
1342
|
+
shouldRenderLocaleAlternates(options) ? this.#createLocaleAlternateHeadSnapshot(url) : undefined,
|
|
1343
|
+
);
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
#createLocaleAlternateHeadSnapshot(url: URL): ResolvedHead["headSnapshot"] {
|
|
1347
|
+
return createAkanLocaleAlternateHeadSnapshot(this.#getLocaleAlternateLanguages(url));
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
#getLocaleAlternateLanguages(url: URL): Record<string, string> {
|
|
1028
1351
|
const languages: Record<string, string> = {};
|
|
1029
1352
|
const publicUrl = RscRenderer.#getPublicRequestUrl(url);
|
|
1030
1353
|
for (const lang of this.#i18n.locales) {
|
|
@@ -1037,7 +1360,11 @@ class RscRenderer {
|
|
|
1037
1360
|
xDefaultUrl.search = "";
|
|
1038
1361
|
xDefaultUrl.hash = "";
|
|
1039
1362
|
languages["x-default"] = xDefaultUrl.href;
|
|
1040
|
-
return
|
|
1363
|
+
return languages;
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
#renderLocaleAlternates(url: URL): ReactNode {
|
|
1367
|
+
return Object.entries(this.#getLocaleAlternateLanguages(url)).map(([lang, href]) => (
|
|
1041
1368
|
<link key={`alternate:${lang}`} rel="alternate" hrefLang={lang} href={href} />
|
|
1042
1369
|
));
|
|
1043
1370
|
}
|