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.
Files changed (178) hide show
  1. package/client/csrTypes.ts +16 -0
  2. package/constant/fieldInfo.ts +11 -9
  3. package/constant/getDefault.ts +1 -1
  4. package/fetch/requestStorage.ts +5 -0
  5. package/package.json +4 -4
  6. package/server/akanApp.ts +26 -3
  7. package/server/akanServer.ts +5 -1
  8. package/server/cachePolicy.ts +99 -5
  9. package/server/imageOptimizer.ts +14 -1
  10. package/server/metadata.tsx +117 -33
  11. package/server/resolver/database.resolver.ts +4 -4
  12. package/server/routeElementComposer.tsx +46 -14
  13. package/server/routeState.ts +379 -0
  14. package/server/routeTreeBuilder.ts +3 -2
  15. package/server/rscClient.tsx +316 -46
  16. package/server/rscClientFetch.ts +57 -0
  17. package/server/rscClientPatch.ts +157 -0
  18. package/server/rscHeadPatch.ts +80 -0
  19. package/server/rscNavigationState.ts +315 -0
  20. package/server/rscPartialCommit.ts +3 -0
  21. package/server/rscPatchSafety.ts +57 -0
  22. package/server/rscSegmentOutlet.tsx +69 -0
  23. package/server/rscSegmentOutletReference.ts +24 -0
  24. package/server/rscWorker.tsx +380 -53
  25. package/server/rscWorkerCache.ts +180 -0
  26. package/server/rscWorkerHost.ts +40 -12
  27. package/server/rscWorkerReplay.ts +11 -2
  28. package/server/ssrFromRscRenderer.tsx +15 -10
  29. package/server/ssrTypes.ts +18 -0
  30. package/server/types.tsx +4 -0
  31. package/server/webRouter.ts +198 -42
  32. package/service/predefinedAdaptor/database.adaptor.ts +72 -25
  33. package/signal/signalContext.ts +1 -1
  34. package/types/client/csrTypes.d.ts +16 -0
  35. package/types/constant/fieldInfo.d.ts +8 -7
  36. package/types/fetch/requestStorage.d.ts +2 -0
  37. package/types/server/cachePolicy.d.ts +36 -0
  38. package/types/server/metadata.d.ts +10 -1
  39. package/types/server/routeElementComposer.d.ts +9 -1
  40. package/types/server/routeState.d.ts +94 -0
  41. package/types/server/rscClient.d.ts +1 -0
  42. package/types/server/rscClientFetch.d.ts +24 -0
  43. package/types/server/rscClientPatch.d.ts +21 -0
  44. package/types/server/rscHeadPatch.d.ts +12 -0
  45. package/types/server/rscNavigationState.d.ts +78 -0
  46. package/types/server/rscPartialCommit.d.ts +1 -0
  47. package/types/server/rscPatchSafety.d.ts +8 -0
  48. package/types/server/rscSegmentOutlet.d.ts +17 -0
  49. package/types/server/rscSegmentOutletReference.d.ts +2 -0
  50. package/types/server/rscWorker.d.ts +5 -0
  51. package/types/server/rscWorkerCache.d.ts +63 -0
  52. package/types/server/rscWorkerHost.d.ts +8 -4
  53. package/types/server/rscWorkerReplay.d.ts +3 -0
  54. package/types/server/ssrFromRscRenderer.d.ts +1 -0
  55. package/types/server/ssrTypes.d.ts +17 -0
  56. package/types/server/types.d.ts +4 -0
  57. package/types/server/webRouter.d.ts +7 -3
  58. package/types/service/predefinedAdaptor/database.adaptor.d.ts +6 -0
  59. package/types/ui/Button.d.ts +1 -1
  60. package/types/ui/ClientSide.d.ts +1 -1
  61. package/types/ui/Constant/Doc.d.ts +6 -6
  62. package/types/ui/Constant/Mermaid.d.ts +1 -1
  63. package/types/ui/Constant/index.d.ts +1 -1
  64. package/types/ui/Constant/schemaDoc.d.ts +1 -1
  65. package/types/ui/Copy.d.ts +1 -1
  66. package/types/ui/CsrImage.d.ts +1 -1
  67. package/types/ui/Data/CardList.d.ts +1 -1
  68. package/types/ui/Data/Dashboard.d.ts +1 -1
  69. package/types/ui/Data/Insight.d.ts +1 -1
  70. package/types/ui/Data/Item.d.ts +6 -6
  71. package/types/ui/Data/ListContainer.d.ts +1 -1
  72. package/types/ui/Data/Pagination.d.ts +1 -1
  73. package/types/ui/Data/TableList.d.ts +1 -1
  74. package/types/ui/DatePicker.d.ts +3 -3
  75. package/types/ui/Dialog/Close.d.ts +1 -1
  76. package/types/ui/Dialog/Content.d.ts +1 -1
  77. package/types/ui/Dialog/Provider.d.ts +1 -1
  78. package/types/ui/Dialog/Trigger.d.ts +1 -1
  79. package/types/ui/Dialog/index.d.ts +3 -3
  80. package/types/ui/DragAction.d.ts +4 -4
  81. package/types/ui/DraggableList.d.ts +3 -3
  82. package/types/ui/Dropdown.d.ts +1 -1
  83. package/types/ui/Empty.d.ts +1 -1
  84. package/types/ui/Field.d.ts +22 -22
  85. package/types/ui/Image.d.ts +1 -1
  86. package/types/ui/InfiniteScroll.d.ts +1 -1
  87. package/types/ui/Input.d.ts +6 -6
  88. package/types/ui/KeyboardAvoiding.d.ts +1 -1
  89. package/types/ui/Layout/BottomAction.d.ts +1 -1
  90. package/types/ui/Layout/BottomInset.d.ts +1 -1
  91. package/types/ui/Layout/BottomTab.d.ts +1 -1
  92. package/types/ui/Layout/Header.d.ts +1 -1
  93. package/types/ui/Layout/LeftSider.d.ts +1 -1
  94. package/types/ui/Layout/Navbar.d.ts +1 -1
  95. package/types/ui/Layout/RightSider.d.ts +1 -1
  96. package/types/ui/Layout/Sider.d.ts +1 -1
  97. package/types/ui/Layout/Template.d.ts +1 -1
  98. package/types/ui/Layout/TopLeftAction.d.ts +1 -1
  99. package/types/ui/Layout/Unit.d.ts +1 -1
  100. package/types/ui/Layout/View.d.ts +1 -1
  101. package/types/ui/Layout/Zone.d.ts +1 -1
  102. package/types/ui/Layout/index.d.ts +12 -12
  103. package/types/ui/Link/Back.d.ts +1 -1
  104. package/types/ui/Link/Close.d.ts +1 -1
  105. package/types/ui/Link/CsrLink.d.ts +1 -1
  106. package/types/ui/Link/Lang.d.ts +1 -1
  107. package/types/ui/Link/SsrLink.d.ts +1 -1
  108. package/types/ui/Link/index.d.ts +1 -1
  109. package/types/ui/Load/Edit.d.ts +1 -1
  110. package/types/ui/Load/Edit_Client.d.ts +1 -1
  111. package/types/ui/Load/PageCSR.d.ts +1 -1
  112. package/types/ui/Load/Pagination.d.ts +1 -1
  113. package/types/ui/Load/Units.d.ts +1 -1
  114. package/types/ui/Load/View.d.ts +1 -1
  115. package/types/ui/Loading/Area.d.ts +1 -1
  116. package/types/ui/Loading/Button.d.ts +1 -1
  117. package/types/ui/Loading/Input.d.ts +1 -1
  118. package/types/ui/Loading/ProgressBar.d.ts +1 -1
  119. package/types/ui/Loading/Skeleton.d.ts +1 -1
  120. package/types/ui/Loading/Spin.d.ts +1 -1
  121. package/types/ui/Loading/index.d.ts +6 -6
  122. package/types/ui/Menu.d.ts +1 -1
  123. package/types/ui/Modal.d.ts +1 -1
  124. package/types/ui/Model/AdminPanel.d.ts +1 -1
  125. package/types/ui/Model/Edit.d.ts +1 -1
  126. package/types/ui/Model/EditModal.d.ts +1 -1
  127. package/types/ui/Model/EditWrapper.d.ts +1 -1
  128. package/types/ui/Model/LoadInit.d.ts +1 -1
  129. package/types/ui/Model/New.d.ts +1 -1
  130. package/types/ui/Model/NewWrapper.d.ts +1 -1
  131. package/types/ui/Model/NewWrapper_Client.d.ts +1 -1
  132. package/types/ui/Model/Remove.d.ts +1 -1
  133. package/types/ui/Model/RemoveWrapper.d.ts +1 -1
  134. package/types/ui/Model/SureToRemove.d.ts +1 -1
  135. package/types/ui/Model/View.d.ts +1 -1
  136. package/types/ui/Model/ViewEditModal.d.ts +1 -1
  137. package/types/ui/Model/ViewModal.d.ts +1 -1
  138. package/types/ui/Model/ViewWrapper.d.ts +1 -1
  139. package/types/ui/More.d.ts +1 -1
  140. package/types/ui/ObjectId.d.ts +1 -1
  141. package/types/ui/Popconfirm.d.ts +1 -1
  142. package/types/ui/Radio.d.ts +2 -2
  143. package/types/ui/RecentTime.d.ts +1 -1
  144. package/types/ui/Refresh.d.ts +1 -1
  145. package/types/ui/ScreenNavigator.d.ts +3 -3
  146. package/types/ui/Select.d.ts +1 -1
  147. package/types/ui/Signal/Arg.d.ts +13 -13
  148. package/types/ui/Signal/Doc.d.ts +6 -6
  149. package/types/ui/Signal/Listener.d.ts +2 -2
  150. package/types/ui/Signal/Message.d.ts +4 -4
  151. package/types/ui/Signal/Object.d.ts +4 -4
  152. package/types/ui/Signal/PubSub.d.ts +4 -4
  153. package/types/ui/Signal/Request.d.ts +2 -2
  154. package/types/ui/Signal/Response.d.ts +3 -3
  155. package/types/ui/Signal/RestApi.d.ts +5 -5
  156. package/types/ui/Signal/WebSocket.d.ts +2 -2
  157. package/types/ui/System/CSR.d.ts +5 -5
  158. package/types/ui/System/Client.d.ts +8 -8
  159. package/types/ui/System/Common.d.ts +2 -2
  160. package/types/ui/System/DevModeToggle.d.ts +1 -1
  161. package/types/ui/System/Gtag.d.ts +1 -1
  162. package/types/ui/System/Messages.d.ts +1 -1
  163. package/types/ui/System/Reconnect.d.ts +1 -1
  164. package/types/ui/System/Root.d.ts +1 -1
  165. package/types/ui/System/SSR.d.ts +4 -4
  166. package/types/ui/System/SelectLanguage.d.ts +1 -1
  167. package/types/ui/System/ThemeToggle.d.ts +1 -1
  168. package/types/ui/System/index.d.ts +7 -7
  169. package/types/ui/Tab/Menu.d.ts +1 -1
  170. package/types/ui/Tab/Menus.d.ts +1 -1
  171. package/types/ui/Tab/Panel.d.ts +1 -1
  172. package/types/ui/Tab/Provider.d.ts +1 -1
  173. package/types/ui/Tab/index.d.ts +4 -4
  174. package/types/ui/Table.d.ts +1 -1
  175. package/types/ui/ToggleSelect.d.ts +2 -2
  176. package/types/ui/Unauthorized.d.ts +1 -1
  177. package/ui/Constant/schemaDoc.ts +1 -1
  178. 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
- const renders = [...pathRoute.renderRootLayouts, ...pathRoute.renderLayouts, pathRoute.renderPage];
24
- let element: ReactNode = null;
25
- for (let i = renders.length - 1; i >= 0; i--) {
26
- const routeRender = renders[i];
27
- if (!routeRender) continue;
28
- element = (
29
- <Suspense fallback={RouteElementComposer.#composeLoadingFallback(renders.slice(i), params)}>
30
- <RouteElementComposer.AsyncRender routeRender={routeRender} params={params} searchParams={searchParams}>
31
- {element}
32
- </RouteElementComposer.AsyncRender>
33
- </Suspense>
34
- );
35
- }
36
- return element;
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) return mod.head === null ? null : resolveHeadExport(mod.head);
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
  };