akanjs 2.3.0 → 2.3.1-rc.1

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