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
|
@@ -246,16 +246,16 @@ export class DatabaseResolver {
|
|
|
246
246
|
return await timedQuery(() => this.__store.findIds(find, { sort, skip, limit, sample }));
|
|
247
247
|
}
|
|
248
248
|
async __find(query?: QueryOf<any>, queryOption?: FindQueryOption): Promise<any | null> {
|
|
249
|
-
const { find, sort, skip, sample } = getFindQuery(query, queryOption);
|
|
250
|
-
return await timedQuery(() => this.__store.findOne(find, { sort, skip, sample }));
|
|
249
|
+
const { find, sort, skip, sample, select } = getFindQuery(query, queryOption);
|
|
250
|
+
return await timedQuery(() => this.__store.findOne(find, { sort, skip, sample, select }));
|
|
251
251
|
}
|
|
252
252
|
async __findId(query?: QueryOf<any>, queryOption?: FindQueryOption): Promise<string | null> {
|
|
253
253
|
const { find, sort, skip, sample } = getFindQuery(query, queryOption);
|
|
254
254
|
return await timedQuery(() => this.__store.findId(find, { sort, skip, sample }));
|
|
255
255
|
}
|
|
256
256
|
async __pick(query?: QueryOf<any>, queryOption?: FindQueryOption): Promise<any> {
|
|
257
|
-
const { find, sort, skip, sample } = getFindQuery(query, queryOption);
|
|
258
|
-
return await this.__store.pickOne(find, { sort, skip, sample });
|
|
257
|
+
const { find, sort, skip, sample, select } = getFindQuery(query, queryOption);
|
|
258
|
+
return await this.__store.pickOne(find, { sort, skip, sample, select });
|
|
259
259
|
}
|
|
260
260
|
async __pickId(query?: QueryOf<any>, queryOption?: FindQueryOption): Promise<string> {
|
|
261
261
|
const { find, sort, skip, sample } = getFindQuery(query, queryOption);
|
|
@@ -9,6 +9,9 @@ import type {
|
|
|
9
9
|
} from "akanjs/client";
|
|
10
10
|
import { Children, cloneElement, isValidElement, type ReactElement, type ReactNode, Suspense } from "react";
|
|
11
11
|
import { resolveHeadResult } from "./metadata";
|
|
12
|
+
import { type AkanRouteSegmentState, createAkanRouteSegments, createAkanSegmentOutletKey } from "./routeState";
|
|
13
|
+
import { isAkanRscPartialCommitEnabled } from "./rscPartialCommit";
|
|
14
|
+
import { AkanSegmentOutletReference } from "./rscSegmentOutletReference";
|
|
12
15
|
|
|
13
16
|
export class RouteElementComposer {
|
|
14
17
|
static compose({
|
|
@@ -20,20 +23,32 @@ export class RouteElementComposer {
|
|
|
20
23
|
params: Record<string, string>;
|
|
21
24
|
searchParams: Record<string, string | string[]>;
|
|
22
25
|
}): ReactNode {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
26
|
+
return RouteElementComposer.composeRenders({
|
|
27
|
+
renders: RouteElementComposer.#getRenderStack(pathRoute),
|
|
28
|
+
segments: isAkanRscPartialCommitEnabled() ? createAkanRouteSegments(pathRoute) : undefined,
|
|
29
|
+
params,
|
|
30
|
+
searchParams,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
static composeSuffix({
|
|
35
|
+
pathRoute,
|
|
36
|
+
params,
|
|
37
|
+
searchParams,
|
|
38
|
+
patchStartIndex,
|
|
39
|
+
}: {
|
|
40
|
+
pathRoute: PathRoute;
|
|
41
|
+
params: Record<string, string>;
|
|
42
|
+
searchParams: Record<string, string | string[]>;
|
|
43
|
+
patchStartIndex: number;
|
|
44
|
+
}): ReactNode | null {
|
|
45
|
+
const renders = RouteElementComposer.#getRenderStack(pathRoute);
|
|
46
|
+
if (!Number.isInteger(patchStartIndex) || patchStartIndex < 0 || patchStartIndex >= renders.length) return null;
|
|
47
|
+
return RouteElementComposer.composeRenders({
|
|
48
|
+
renders: renders.slice(patchStartIndex),
|
|
49
|
+
params,
|
|
50
|
+
searchParams,
|
|
51
|
+
});
|
|
37
52
|
}
|
|
38
53
|
|
|
39
54
|
static async resolveHead({
|
|
@@ -109,10 +124,12 @@ export class RouteElementComposer {
|
|
|
109
124
|
|
|
110
125
|
static composeRenders({
|
|
111
126
|
renders,
|
|
127
|
+
segments,
|
|
112
128
|
params,
|
|
113
129
|
searchParams,
|
|
114
130
|
}: {
|
|
115
131
|
renders: RouteRender[];
|
|
132
|
+
segments?: AkanRouteSegmentState[];
|
|
116
133
|
params: Record<string, string>;
|
|
117
134
|
searchParams: Record<string, string | string[]>;
|
|
118
135
|
}): ReactNode {
|
|
@@ -127,6 +144,17 @@ export class RouteElementComposer {
|
|
|
127
144
|
</RouteElementComposer.AsyncRender>
|
|
128
145
|
</Suspense>
|
|
129
146
|
);
|
|
147
|
+
const segment = segments?.[i];
|
|
148
|
+
if (segment?.kind === "page") {
|
|
149
|
+
const routeSegments = segments;
|
|
150
|
+
if (!routeSegments) continue;
|
|
151
|
+
const outletKey =
|
|
152
|
+
createAkanSegmentOutletKey(
|
|
153
|
+
routeSegments.slice(0, i + 1).map((item) => item.key),
|
|
154
|
+
i,
|
|
155
|
+
) ?? segment.key;
|
|
156
|
+
element = <AkanSegmentOutletReference segmentKey={outletKey}>{element}</AkanSegmentOutletReference>;
|
|
157
|
+
}
|
|
130
158
|
}
|
|
131
159
|
return element;
|
|
132
160
|
}
|
|
@@ -199,6 +227,10 @@ export class RouteElementComposer {
|
|
|
199
227
|
return RouteElementComposer.#normalizeReactNode(children);
|
|
200
228
|
}
|
|
201
229
|
|
|
230
|
+
static #getRenderStack(pathRoute: PathRoute): RouteRender[] {
|
|
231
|
+
return [...pathRoute.renderRootLayouts, ...pathRoute.renderLayouts, pathRoute.renderPage];
|
|
232
|
+
}
|
|
233
|
+
|
|
202
234
|
static #composeLoadingFallback(renders: RouteRender[], params: Record<string, string>): ReactNode {
|
|
203
235
|
let element: ReactNode = null;
|
|
204
236
|
for (let i = renders.length - 1; i >= 0; i--) {
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import type { PathRoute } from "akanjs/client";
|
|
2
|
+
|
|
3
|
+
export const AKAN_RSC_STATE_VERSION = 1;
|
|
4
|
+
export const AKAN_RSC_STATE_VERSION_HEADER = "X-Akan-Rsc-State-Version";
|
|
5
|
+
export const AKAN_RSC_CURRENT_ROUTE_HEADER = "X-Akan-Rsc-Current-Route";
|
|
6
|
+
export const AKAN_RSC_CURRENT_STATE_HEADER = "X-Akan-Rsc-Current-State";
|
|
7
|
+
export const AKAN_RSC_RESPONSE_STATE_HEADER = "X-Akan-Rsc-State";
|
|
8
|
+
export const AKAN_RSC_PATCH_START_INDEX_HEADER = "X-Akan-Rsc-Patch-Start-Index";
|
|
9
|
+
export const AKAN_RSC_PATCH_SEGMENT_PATH_HEADER = "X-Akan-Rsc-Patch-Segment-Path";
|
|
10
|
+
export const AKAN_RSC_PATCH_START_SEGMENT_HEADER = "X-Akan-Rsc-Patch-Start-Segment";
|
|
11
|
+
export const AKAN_RSC_PATCH_HEAD_SAFE_HEADER = "X-Akan-Rsc-Patch-Head-Safe";
|
|
12
|
+
export const AKAN_RSC_PATCH_HEAD_SNAPSHOT_HEADER = "X-Akan-Rsc-Patch-Head-Snapshot";
|
|
13
|
+
export const AKAN_RSC_HEAD_SNAPSHOT_VERSION = 1;
|
|
14
|
+
export const AKAN_RSC_HEAD_SNAPSHOT_MAX_HEADER_BYTES = 12 * 1024;
|
|
15
|
+
|
|
16
|
+
export type AkanRscPartialStatus = "full" | "candidate" | "patch" | "fallback";
|
|
17
|
+
|
|
18
|
+
export interface AkanRouteSegmentState {
|
|
19
|
+
key: string;
|
|
20
|
+
path: string;
|
|
21
|
+
kind: "root-layout" | "layout" | "page";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface AkanRouterStateV1 {
|
|
25
|
+
version: typeof AKAN_RSC_STATE_VERSION;
|
|
26
|
+
buildId?: number;
|
|
27
|
+
href: string;
|
|
28
|
+
routeId: string;
|
|
29
|
+
segments: AkanRouteSegmentState[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type AkanHeadSnapshotTag = "title" | "meta" | "link";
|
|
33
|
+
|
|
34
|
+
export interface AkanHeadSnapshotNode {
|
|
35
|
+
tag: AkanHeadSnapshotTag;
|
|
36
|
+
attrs?: Record<string, string>;
|
|
37
|
+
text?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface AkanHeadSnapshotV1 {
|
|
41
|
+
version: typeof AKAN_RSC_HEAD_SNAPSHOT_VERSION;
|
|
42
|
+
nodes: AkanHeadSnapshotNode[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type AkanHeadSnapshotDecodeResult =
|
|
46
|
+
| { status: "ok"; snapshot: AkanHeadSnapshotV1 }
|
|
47
|
+
| { status: "missing" | "invalid" | "too-large" };
|
|
48
|
+
|
|
49
|
+
export interface AkanRscPartialDecision {
|
|
50
|
+
status: AkanRscPartialStatus;
|
|
51
|
+
reason?: string;
|
|
52
|
+
commonPrefixLength: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface AkanRscPatchMetadata {
|
|
56
|
+
patchStartIndex: number;
|
|
57
|
+
patchStartSegmentKey: string;
|
|
58
|
+
segmentPath: string[];
|
|
59
|
+
headSafe?: boolean;
|
|
60
|
+
headSnapshot?: AkanHeadSnapshotV1;
|
|
61
|
+
headSnapshotFailure?: "head-invalid" | "head-too-large";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface AkanRscPatchDecision extends AkanRscPartialDecision {
|
|
65
|
+
status: "full" | "patch" | "fallback";
|
|
66
|
+
patch?: AkanRscPatchMetadata;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function encodeBase64Url(value: string): string {
|
|
70
|
+
const bytes = new TextEncoder().encode(value);
|
|
71
|
+
let binary = "";
|
|
72
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
73
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function decodeBase64Url(value: string): string | null {
|
|
77
|
+
try {
|
|
78
|
+
const padded = value
|
|
79
|
+
.replace(/-/g, "+")
|
|
80
|
+
.replace(/_/g, "/")
|
|
81
|
+
.padEnd(Math.ceil(value.length / 4) * 4, "=");
|
|
82
|
+
const binary = atob(padded);
|
|
83
|
+
const bytes = new Uint8Array(binary.length);
|
|
84
|
+
for (let index = 0; index < binary.length; index++) bytes[index] = binary.charCodeAt(index);
|
|
85
|
+
return new TextDecoder().decode(bytes);
|
|
86
|
+
} catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function isSegmentState(value: unknown): value is AkanRouteSegmentState {
|
|
92
|
+
if (!value || typeof value !== "object") return false;
|
|
93
|
+
const segment = value as Partial<AkanRouteSegmentState>;
|
|
94
|
+
return (
|
|
95
|
+
typeof segment.key === "string" &&
|
|
96
|
+
typeof segment.path === "string" &&
|
|
97
|
+
(segment.kind === "root-layout" || segment.kind === "layout" || segment.kind === "page")
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function isHeadSnapshotNode(value: unknown): value is AkanHeadSnapshotNode {
|
|
102
|
+
if (!value || typeof value !== "object") return false;
|
|
103
|
+
const node = value as Partial<AkanHeadSnapshotNode>;
|
|
104
|
+
if (node.tag !== "title" && node.tag !== "meta" && node.tag !== "link") return false;
|
|
105
|
+
if (node.text !== undefined && typeof node.text !== "string") return false;
|
|
106
|
+
if (node.attrs !== undefined) {
|
|
107
|
+
if (!node.attrs || typeof node.attrs !== "object" || Array.isArray(node.attrs)) return false;
|
|
108
|
+
for (const [key, attrValue] of Object.entries(node.attrs)) {
|
|
109
|
+
if (!key || typeof attrValue !== "string") return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function isAkanHeadSnapshotV1(value: unknown): value is AkanHeadSnapshotV1 {
|
|
116
|
+
if (!value || typeof value !== "object") return false;
|
|
117
|
+
const snapshot = value as Partial<AkanHeadSnapshotV1>;
|
|
118
|
+
return (
|
|
119
|
+
snapshot.version === AKAN_RSC_HEAD_SNAPSHOT_VERSION &&
|
|
120
|
+
Array.isArray(snapshot.nodes) &&
|
|
121
|
+
snapshot.nodes.length <= 64 &&
|
|
122
|
+
snapshot.nodes.every(isHeadSnapshotNode)
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function isAkanRouterStateV1(value: unknown): value is AkanRouterStateV1 {
|
|
127
|
+
if (!value || typeof value !== "object") return false;
|
|
128
|
+
const state = value as Partial<AkanRouterStateV1>;
|
|
129
|
+
return (
|
|
130
|
+
state.version === AKAN_RSC_STATE_VERSION &&
|
|
131
|
+
(state.buildId === undefined || typeof state.buildId === "number") &&
|
|
132
|
+
typeof state.href === "string" &&
|
|
133
|
+
typeof state.routeId === "string" &&
|
|
134
|
+
Array.isArray(state.segments) &&
|
|
135
|
+
state.segments.every(isSegmentState)
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function encodeAkanRouterState(state: AkanRouterStateV1): string {
|
|
140
|
+
return encodeBase64Url(JSON.stringify(state));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function encodeAkanHeadSnapshot(snapshot: AkanHeadSnapshotV1): string | null {
|
|
144
|
+
const encoded = encodeBase64Url(JSON.stringify(snapshot));
|
|
145
|
+
return new TextEncoder().encode(encoded).byteLength <= AKAN_RSC_HEAD_SNAPSHOT_MAX_HEADER_BYTES ? encoded : null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function decodeAkanHeadSnapshot(value: string | null | undefined): AkanHeadSnapshotDecodeResult {
|
|
149
|
+
if (!value) return { status: "missing" };
|
|
150
|
+
if (new TextEncoder().encode(value).byteLength > AKAN_RSC_HEAD_SNAPSHOT_MAX_HEADER_BYTES) {
|
|
151
|
+
return { status: "too-large" };
|
|
152
|
+
}
|
|
153
|
+
const json = decodeBase64Url(value);
|
|
154
|
+
if (!json) return { status: "invalid" };
|
|
155
|
+
try {
|
|
156
|
+
const parsed = JSON.parse(json) as unknown;
|
|
157
|
+
return isAkanHeadSnapshotV1(parsed) ? { status: "ok", snapshot: parsed } : { status: "invalid" };
|
|
158
|
+
} catch {
|
|
159
|
+
return { status: "invalid" };
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function readAkanHeadSnapshotResponseHeader(headers: Headers): AkanHeadSnapshotDecodeResult {
|
|
164
|
+
return decodeAkanHeadSnapshot(headers.get(AKAN_RSC_PATCH_HEAD_SNAPSHOT_HEADER));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function decodeAkanRouterState(value: string | null | undefined): AkanRouterStateV1 | null {
|
|
168
|
+
if (!value) return null;
|
|
169
|
+
const json = decodeBase64Url(value);
|
|
170
|
+
if (!json) return null;
|
|
171
|
+
try {
|
|
172
|
+
const parsed = JSON.parse(json) as unknown;
|
|
173
|
+
return isAkanRouterStateV1(parsed) ? parsed : null;
|
|
174
|
+
} catch {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function appendAkanRouterStateRequestHeaders(
|
|
180
|
+
headers: Headers,
|
|
181
|
+
state: AkanRouterStateV1 | null | undefined,
|
|
182
|
+
): void {
|
|
183
|
+
if (!state) return;
|
|
184
|
+
headers.set(AKAN_RSC_STATE_VERSION_HEADER, String(state.version));
|
|
185
|
+
headers.set(AKAN_RSC_CURRENT_ROUTE_HEADER, state.routeId);
|
|
186
|
+
headers.set(AKAN_RSC_CURRENT_STATE_HEADER, encodeAkanRouterState(state));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function readAkanRouterStateResponseHeader(headers: Headers): AkanRouterStateV1 | null {
|
|
190
|
+
return decodeAkanRouterState(headers.get(AKAN_RSC_RESPONSE_STATE_HEADER));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function encodeAkanRscPatchSegmentPath(segmentPath: string[]): string {
|
|
194
|
+
return encodeBase64Url(JSON.stringify(segmentPath));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function decodeAkanRscPatchSegmentPath(value: string | null | undefined): string[] | null {
|
|
198
|
+
if (!value) return null;
|
|
199
|
+
const json = decodeBase64Url(value);
|
|
200
|
+
if (!json) return null;
|
|
201
|
+
try {
|
|
202
|
+
const parsed = JSON.parse(json) as unknown;
|
|
203
|
+
return Array.isArray(parsed) && parsed.every((segment) => typeof segment === "string") ? parsed : null;
|
|
204
|
+
} catch {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function readAkanRscPatchMetadataResponseHeaders(headers: Headers): AkanRscPatchMetadata | null {
|
|
210
|
+
const patchStartIndexHeader = headers.get(AKAN_RSC_PATCH_START_INDEX_HEADER);
|
|
211
|
+
if (patchStartIndexHeader === null) return null;
|
|
212
|
+
const patchStartIndex = Number(patchStartIndexHeader);
|
|
213
|
+
const patchStartSegmentKey = headers.get(AKAN_RSC_PATCH_START_SEGMENT_HEADER);
|
|
214
|
+
const segmentPath = decodeAkanRscPatchSegmentPath(headers.get(AKAN_RSC_PATCH_SEGMENT_PATH_HEADER));
|
|
215
|
+
if (!Number.isInteger(patchStartIndex) || patchStartIndex < 0 || !patchStartSegmentKey || !segmentPath) return null;
|
|
216
|
+
if (segmentPath[patchStartIndex] !== patchStartSegmentKey) return null;
|
|
217
|
+
const headSnapshotResult = readAkanHeadSnapshotResponseHeader(headers);
|
|
218
|
+
return {
|
|
219
|
+
patchStartIndex,
|
|
220
|
+
patchStartSegmentKey,
|
|
221
|
+
segmentPath,
|
|
222
|
+
...(headers.get(AKAN_RSC_PATCH_HEAD_SAFE_HEADER) === "1" ? { headSafe: true } : {}),
|
|
223
|
+
...(headSnapshotResult.status === "ok" ? { headSnapshot: headSnapshotResult.snapshot } : {}),
|
|
224
|
+
...(headSnapshotResult.status === "invalid" ? { headSnapshotFailure: "head-invalid" as const } : {}),
|
|
225
|
+
...(headSnapshotResult.status === "too-large" ? { headSnapshotFailure: "head-too-large" as const } : {}),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function createAkanRouterState({
|
|
230
|
+
pathRoute,
|
|
231
|
+
href,
|
|
232
|
+
buildId,
|
|
233
|
+
}: {
|
|
234
|
+
pathRoute: PathRoute;
|
|
235
|
+
href: string;
|
|
236
|
+
buildId?: number;
|
|
237
|
+
}): AkanRouterStateV1 {
|
|
238
|
+
return {
|
|
239
|
+
version: AKAN_RSC_STATE_VERSION,
|
|
240
|
+
buildId,
|
|
241
|
+
href,
|
|
242
|
+
routeId: pathRoute.path,
|
|
243
|
+
segments: createAkanRouteSegments(pathRoute),
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function createAkanRouteSegments(pathRoute: PathRoute): AkanRouteSegmentState[] {
|
|
248
|
+
const segments: AkanRouteSegmentState[] = [];
|
|
249
|
+
const routePaths = pathRoute.pathSegments.length ? pathRoute.pathSegments : [pathRoute.path || "/"];
|
|
250
|
+
const segmentPathAt = (index: number) => routePaths[Math.min(index, routePaths.length - 1)] ?? "/";
|
|
251
|
+
|
|
252
|
+
for (let index = 0; index < pathRoute.renderRootLayouts.length; index++) {
|
|
253
|
+
const path = segmentPathAt(index);
|
|
254
|
+
segments.push({ kind: "root-layout", path, key: `root:${path}:${index}` });
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
for (let index = 0; index < pathRoute.renderLayouts.length; index++) {
|
|
258
|
+
const stackIndex = pathRoute.renderRootLayouts.length + index;
|
|
259
|
+
const path = segmentPathAt(stackIndex);
|
|
260
|
+
segments.push({ kind: "layout", path, key: `layout:${path}:${stackIndex}` });
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const pageIndex = pathRoute.renderRootLayouts.length + pathRoute.renderLayouts.length;
|
|
264
|
+
segments.push({ kind: "page", path: pathRoute.path, key: `page:${pathRoute.path}:${pageIndex}` });
|
|
265
|
+
return segments;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function createAkanSegmentOutletKey(segmentPath: string[], segmentIndex: number): string | null {
|
|
269
|
+
if (!Number.isInteger(segmentIndex) || segmentIndex < 0) return null;
|
|
270
|
+
const parentKey = segmentPath[segmentIndex - 1] ?? "root";
|
|
271
|
+
return `slot:${parentKey}:${segmentIndex}`;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function readAkanRouterStateRequest(headers: Headers): {
|
|
275
|
+
state: AkanRouterStateV1 | null;
|
|
276
|
+
currentRoute?: string;
|
|
277
|
+
reason?: string;
|
|
278
|
+
} {
|
|
279
|
+
const encoded = headers.get(AKAN_RSC_CURRENT_STATE_HEADER);
|
|
280
|
+
if (!encoded) return { state: null, reason: "missing-state" };
|
|
281
|
+
|
|
282
|
+
const version = headers.get(AKAN_RSC_STATE_VERSION_HEADER);
|
|
283
|
+
if (version !== String(AKAN_RSC_STATE_VERSION)) return { state: null, reason: "version-mismatch" };
|
|
284
|
+
|
|
285
|
+
const state = decodeAkanRouterState(encoded);
|
|
286
|
+
if (!state) return { state: null, reason: "invalid-state" };
|
|
287
|
+
|
|
288
|
+
return { state, currentRoute: headers.get(AKAN_RSC_CURRENT_ROUTE_HEADER) ?? undefined };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export function resolveAkanRscPartialDecision({
|
|
292
|
+
currentState,
|
|
293
|
+
currentRoute,
|
|
294
|
+
targetState,
|
|
295
|
+
}: {
|
|
296
|
+
currentState: AkanRouterStateV1 | null;
|
|
297
|
+
currentRoute?: string;
|
|
298
|
+
targetState: AkanRouterStateV1;
|
|
299
|
+
}): AkanRscPartialDecision {
|
|
300
|
+
if (!currentState) return { status: "full", reason: "missing-state", commonPrefixLength: 0 };
|
|
301
|
+
if (currentRoute && currentRoute !== currentState.routeId) {
|
|
302
|
+
return { status: "fallback", reason: "current-route-mismatch", commonPrefixLength: 0 };
|
|
303
|
+
}
|
|
304
|
+
if (
|
|
305
|
+
currentState.buildId !== undefined &&
|
|
306
|
+
targetState.buildId !== undefined &&
|
|
307
|
+
currentState.buildId !== targetState.buildId
|
|
308
|
+
) {
|
|
309
|
+
return { status: "fallback", reason: "build-mismatch", commonPrefixLength: 0 };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const commonPrefixLength = countCommonRouteSegments(currentState.segments, targetState.segments);
|
|
313
|
+
if (commonPrefixLength === 0) return { status: "full", reason: "root-mismatch", commonPrefixLength };
|
|
314
|
+
if (currentState.href === targetState.href && currentState.routeId === targetState.routeId) {
|
|
315
|
+
return { status: "full", reason: "same-route", commonPrefixLength };
|
|
316
|
+
}
|
|
317
|
+
return { status: "candidate", reason: "common-prefix", commonPrefixLength };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export function resolveAkanRscPatchDecision({
|
|
321
|
+
currentState,
|
|
322
|
+
targetState,
|
|
323
|
+
partialDecision,
|
|
324
|
+
}: {
|
|
325
|
+
currentState: AkanRouterStateV1 | null;
|
|
326
|
+
targetState: AkanRouterStateV1;
|
|
327
|
+
partialDecision: AkanRscPartialDecision;
|
|
328
|
+
}): AkanRscPatchDecision {
|
|
329
|
+
if (partialDecision.status === "fallback") return { ...partialDecision, status: "fallback" };
|
|
330
|
+
if (partialDecision.status !== "candidate" || !currentState) {
|
|
331
|
+
return { status: "full", reason: partialDecision.reason, commonPrefixLength: partialDecision.commonPrefixLength };
|
|
332
|
+
}
|
|
333
|
+
if (currentState.routeId === targetState.routeId) {
|
|
334
|
+
const patchStartIndex = targetState.segments.length - 1;
|
|
335
|
+
const targetPageSegment = targetState.segments[patchStartIndex];
|
|
336
|
+
if (targetPageSegment?.kind !== "page") {
|
|
337
|
+
return { status: "full", reason: "unsupported-suffix", commonPrefixLength: partialDecision.commonPrefixLength };
|
|
338
|
+
}
|
|
339
|
+
return {
|
|
340
|
+
status: "patch",
|
|
341
|
+
reason: "same-route-search-params",
|
|
342
|
+
commonPrefixLength: partialDecision.commonPrefixLength,
|
|
343
|
+
patch: {
|
|
344
|
+
patchStartIndex,
|
|
345
|
+
patchStartSegmentKey: targetPageSegment.key,
|
|
346
|
+
segmentPath: targetState.segments.map((segment) => segment.key),
|
|
347
|
+
},
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const patchStartIndex = partialDecision.commonPrefixLength;
|
|
352
|
+
const targetSuffix = targetState.segments.slice(patchStartIndex);
|
|
353
|
+
if (targetSuffix.length !== 1 || targetSuffix[0]?.kind !== "page") {
|
|
354
|
+
return { status: "full", reason: "unsupported-suffix", commonPrefixLength: partialDecision.commonPrefixLength };
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const patchStartSegmentKey = targetSuffix[0].key;
|
|
358
|
+
return {
|
|
359
|
+
status: "patch",
|
|
360
|
+
reason: "sibling-page",
|
|
361
|
+
commonPrefixLength: partialDecision.commonPrefixLength,
|
|
362
|
+
patch: {
|
|
363
|
+
patchStartIndex,
|
|
364
|
+
patchStartSegmentKey,
|
|
365
|
+
segmentPath: targetState.segments.slice(0, patchStartIndex + 1).map((segment) => segment.key),
|
|
366
|
+
},
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export function countCommonRouteSegments(
|
|
371
|
+
currentSegments: AkanRouteSegmentState[],
|
|
372
|
+
targetSegments: AkanRouteSegmentState[],
|
|
373
|
+
): number {
|
|
374
|
+
const length = Math.min(currentSegments.length, targetSegments.length);
|
|
375
|
+
for (let index = 0; index < length; index++) {
|
|
376
|
+
if (currentSegments[index]?.key !== targetSegments[index]?.key) return index;
|
|
377
|
+
}
|
|
378
|
+
return length;
|
|
379
|
+
}
|
|
@@ -337,13 +337,14 @@ export class RouteTreeBuilder {
|
|
|
337
337
|
}
|
|
338
338
|
if (mod.generateHead) {
|
|
339
339
|
const head = await mod.generateHead(props);
|
|
340
|
-
if (head !== null && head !== undefined) return resolveHeadExport(head);
|
|
340
|
+
if (head !== null && head !== undefined) return resolveHeadExport(head, { includeHeadSnapshot: false });
|
|
341
341
|
}
|
|
342
342
|
if (mod.generateMetadata) {
|
|
343
343
|
const metadata = await mod.generateMetadata(props);
|
|
344
344
|
return metadata === null || metadata === undefined ? metadata : resolveMetadataHead(metadata);
|
|
345
345
|
}
|
|
346
|
-
if (mod.head !== undefined)
|
|
346
|
+
if (mod.head !== undefined)
|
|
347
|
+
return mod.head === null ? null : resolveHeadExport(mod.head, { includeHeadSnapshot: false });
|
|
347
348
|
return mod.metadata === undefined ? undefined : resolveMetadataHead(mod.metadata);
|
|
348
349
|
},
|
|
349
350
|
};
|