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
@@ -1,13 +1,38 @@
1
- import { createElement, type ReactNode, startTransition, use, useLayoutEffect, useState } from "react";
1
+ import { createElement, type ReactNode, startTransition, use, type Usable, useLayoutEffect, useState } from "react";
2
2
  import { hydrateRoot } from "react-dom/client";
3
3
  import { createFromReadableStream } from "react-server-dom-webpack/client.browser";
4
+ import {
5
+ type AkanHeadSnapshotV1,
6
+ type AkanRouterStateV1,
7
+ type AkanRscPatchMetadata,
8
+ decodeAkanRouterState,
9
+ readAkanRouterStateResponseHeader,
10
+ } from "./routeState";
11
+ import { fetchRscNavigationResponse } from "./rscClientFetch";
12
+ import { validateRscPatchForGuardedCommit } from "./rscClientPatch";
13
+ import {
14
+ commitPreparedAkanHeadSnapshotPatch,
15
+ getAkanHeadSnapshotPatchFailureReason,
16
+ prepareAkanHeadSnapshotPatch,
17
+ rollbackPreparedAkanHeadSnapshotPatch,
18
+ } from "./rscHeadPatch";
4
19
  import { getRscPayloadStream, guardRscRedirectRows, type RscRedirectRow } from "./rscHttp";
5
20
  import {
21
+ type AkanSegmentCacheNode,
6
22
  commitLatestRscNavigation,
23
+ createAkanSegmentCacheTree,
24
+ createRscNavigationCacheNode,
25
+ createRscPatchNavigationCacheNode,
7
26
  deleteRscCacheEntryIfCurrent,
8
- observeRscNavigation,
9
- rememberRscCacheEntry,
27
+ observeRscNavigationNode,
28
+ type RscNavigationCacheNode,
29
+ type RscPatchNavigationCacheNode,
30
+ rememberRscCacheNode,
31
+ rememberRscPatchCacheNode,
32
+ resolveCachedRscPatchNavigation,
10
33
  } from "./rscNavigationState";
34
+ import { isAkanRscPartialCommitEnabled } from "./rscPartialCommit";
35
+ import { commitAkanSegmentOutletPatch, resetAkanSegmentOutletPatches } from "./rscSegmentOutlet";
11
36
 
12
37
  type InlineRscChunk = [1, string] | [3, string];
13
38
 
@@ -16,6 +41,7 @@ declare global {
16
41
  var __RSC_CLOSED__: boolean | undefined;
17
42
  var __RSC_PUSH__: ((type: InlineRscChunk[0], data: string) => void) | undefined;
18
43
  var __RSC_CLOSE__: (() => void) | undefined;
44
+ var __AKAN_RSC_INITIAL_STATE__: string | undefined;
19
45
  var __AKAN_RSC_NAVIGATE__:
20
46
  | ((href: string, options?: { replace?: boolean; scrollToTop?: boolean }) => Promise<void>)
21
47
  | undefined;
@@ -35,8 +61,24 @@ function decodeInlineRscChunk([type, data]: InlineRscChunk): Uint8Array {
35
61
  return decodeBase64(data);
36
62
  }
37
63
 
38
- type RscThenable = Promise<ReactNode>;
39
- type RscFetchResult = { type: "rsc"; thenable: RscThenable } | { type: "redirected"; status?: number };
64
+ type RscThenable = Promise<ReactNode> & {
65
+ status?: "pending" | "fulfilled" | "rejected";
66
+ value?: ReactNode;
67
+ reason?: unknown;
68
+ };
69
+ type RscCacheNode = RscNavigationCacheNode<RscThenable>;
70
+ type RscSegmentCacheNode = AkanSegmentCacheNode<RscThenable>;
71
+ type RscFetchResult =
72
+ | { type: "rsc"; node: RscCacheNode }
73
+ | {
74
+ type: "patched";
75
+ tree: RscSegmentCacheNode;
76
+ patchedNode: RscSegmentCacheNode;
77
+ patch: AkanRscPatchMetadata;
78
+ outletKey: string;
79
+ headSnapshot: AkanHeadSnapshotV1;
80
+ }
81
+ | { type: "redirected"; status?: number };
40
82
  const MAX_RSC_CACHE_ENTRIES = 32;
41
83
  let documentNavigationFallbackInFlight = false;
42
84
 
@@ -69,8 +111,35 @@ function normalizeHref(href: string): string {
69
111
  return new URL(href, window.location.origin).href;
70
112
  }
71
113
 
114
+ /**
115
+ * Mirror React's thenable protocol (status/value/reason) onto the Flight thenable.
116
+ *
117
+ * Without this, `use(thenable)` cannot tell an already-resolved native Promise apart
118
+ * from a pending one: it suspends the root transition once and relies on React's
119
+ * ping -> retry -> re-commit path. That path intermittently lost the re-commit when
120
+ * sync store updates raced the suspended transition, leaving the previous page DOM
121
+ * visible even though the navigation pipeline completed. With the status tracked,
122
+ * `use()` returns the fulfilled payload synchronously and the committed transition
123
+ * renders the new tree in a single pass.
124
+ */
125
+ function trackRscThenable(thenable: RscThenable): RscThenable {
126
+ if (thenable.status !== undefined) return thenable;
127
+ thenable.status = "pending";
128
+ thenable.then(
129
+ (value) => {
130
+ thenable.status = "fulfilled";
131
+ thenable.value = value;
132
+ },
133
+ (reason) => {
134
+ thenable.status = "rejected";
135
+ thenable.reason = reason;
136
+ },
137
+ );
138
+ return thenable;
139
+ }
140
+
72
141
  function createRscThenable(stream: ReadableStream<Uint8Array>): RscThenable {
73
- return createFromReadableStream<ReactNode>(stream) as RscThenable;
142
+ return trackRscThenable(createFromReadableStream<ReactNode>(stream) as RscThenable);
74
143
  }
75
144
 
76
145
  function hardNavigateAfterRscFailure(target: string, replace = false, error?: unknown): void {
@@ -93,35 +162,105 @@ function navigateAfterRscRedirect(target: string, replace = true): void {
93
162
  });
94
163
  }
95
164
 
165
+ function commitRscPatchNavigation({
166
+ target,
167
+ patch,
168
+ replace,
169
+ scrollToTop,
170
+ bumpScrollToTop,
171
+ }: {
172
+ target: string;
173
+ patch: Extract<RscFetchResult, { type: "patched" }>;
174
+ replace?: boolean;
175
+ scrollToTop?: boolean;
176
+ bumpScrollToTop?: () => void;
177
+ }): boolean {
178
+ const patchThenable = patch.patchedNode.thenable;
179
+ if (!patchThenable) throw new Error("[rscClient] validated RSC patch is missing a thenable");
180
+ const preparedHeadPatch = prepareAkanHeadSnapshotPatch(patch.headSnapshot);
181
+ if (!preparedHeadPatch) return false;
182
+
183
+ let outletCommitted = false;
184
+ let headApplied = false;
185
+ startTransition(() => {
186
+ try {
187
+ headApplied = commitPreparedAkanHeadSnapshotPatch(preparedHeadPatch);
188
+ if (!headApplied) {
189
+ return;
190
+ }
191
+ outletCommitted = commitAkanSegmentOutletPatch(patch.outletKey, patchThenable);
192
+ if (!outletCommitted) {
193
+ rollbackPreparedAkanHeadSnapshotPatch(preparedHeadPatch);
194
+ return;
195
+ }
196
+ if (replace) window.history.replaceState(null, "", target);
197
+ else window.history.pushState(null, "", target);
198
+ if (scrollToTop) bumpScrollToTop?.();
199
+ } catch {
200
+ if (headApplied) rollbackPreparedAkanHeadSnapshotPatch(preparedHeadPatch);
201
+ if (outletCommitted) resetAkanSegmentOutletPatches();
202
+ headApplied = false;
203
+ outletCommitted = false;
204
+ }
205
+ });
206
+ return outletCommitted && headApplied;
207
+ }
208
+
96
209
  async function fetchRsc(
97
210
  href: string,
98
- options: { buildId?: number; replaceOnRedirect?: boolean; shouldApplyNavigation?: () => boolean } = {},
211
+ options: {
212
+ buildId?: number;
213
+ replaceOnRedirect?: boolean;
214
+ shouldApplyNavigation?: () => boolean;
215
+ sendRouterState?: boolean;
216
+ navId?: number;
217
+ } = {},
99
218
  ): Promise<RscFetchResult> {
100
219
  const shouldApplyNavigation = options.shouldApplyNavigation ?? (() => true);
101
- const endpoint = new URL("/__rsc", window.location.origin);
102
- endpoint.searchParams.set("url", href);
103
- if (options.buildId !== undefined) endpoint.searchParams.set("buildId", String(options.buildId));
104
- const res = await fetch(endpoint, {
105
- headers: { Accept: "text/x-component", "Cache-Control": "no-cache" },
106
- credentials: "same-origin",
107
- cache: "no-store",
220
+ const responseResult = await fetchRscNavigationResponse(href, {
221
+ buildId: options.buildId,
222
+ currentRouterState,
223
+ navigate: globalThis.__AKAN_RSC_NAVIGATE__,
224
+ sendRouterState: options.sendRouterState,
225
+ shouldApplyNavigation,
108
226
  });
109
- const redirect = res.headers.get("X-Akan-Redirect");
110
- if (redirect) {
111
- const method = res.headers.get("X-Akan-Redirect-Method");
112
- const statusHeader = res.headers.get("X-Akan-Redirect-Status");
113
- const status = statusHeader ? Number(statusHeader) : undefined;
114
- if (shouldApplyNavigation())
115
- await globalThis.__AKAN_RSC_NAVIGATE__?.(redirect, { replace: method !== "push", scrollToTop: true });
116
- return { type: "redirected", status };
227
+ if (responseResult.type === "redirected") return responseResult;
228
+ if (responseResult.type === "patch") {
229
+ const patchResult = await validateRscPatchForGuardedCommit({
230
+ partialCommitEnabled: isAkanRscPartialCommitEnabled(),
231
+ currentTree: currentSegmentTree,
232
+ response: responseResult.response,
233
+ patch: responseResult.patch,
234
+ href,
235
+ createThenable: createRscThenable,
236
+ navId: options.navId,
237
+ getCurrentNavId: () => navigationSeq,
238
+ getHeadSnapshotPatchFailureReason: getAkanHeadSnapshotPatchFailureReason,
239
+ });
240
+ if (patchResult.status === "patched") {
241
+ if (!patchResult.headSnapshot) throw new Error("[rscClient] validated RSC patch is missing a head snapshot");
242
+ return {
243
+ type: "patched",
244
+ tree: patchResult.tree,
245
+ patchedNode: patchResult.patchedNode,
246
+ patch: responseResult.patch,
247
+ outletKey: patchResult.outletKey,
248
+ headSnapshot: patchResult.headSnapshot,
249
+ };
250
+ }
251
+ return fetchRsc(href, {
252
+ ...options,
253
+ sendRouterState: false,
254
+ });
117
255
  }
256
+ const res = responseResult.response;
118
257
  const stream = getRscPayloadStream(res);
119
258
  if (!stream) throw new Error(`[rscClient] RSC fetch failed ${res.status} ${res.statusText}`);
120
- let thenable: RscThenable | undefined;
259
+ const nodeRef: { current?: RscCacheNode } = {};
121
260
  const handleRedirect = (redirect: RscRedirectRow) => {
122
261
  if (!shouldApplyNavigation()) return;
123
262
  const location = redirect.location ? normalizeHref(redirect.location) : href;
124
- if (thenable) deleteRscCacheEntryIfCurrent(rscCache, href, thenable);
263
+ if (nodeRef.current) deleteRscCacheEntryIfCurrent(rscCache, href, nodeRef.current);
125
264
  navigateAfterRscRedirect(
126
265
  location,
127
266
  redirect.method ? redirect.method !== "push" : (options.replaceOnRedirect ?? true),
@@ -130,18 +269,50 @@ async function fetchRsc(
130
269
  const guardedStream = guardRscRedirectRows(stream, {
131
270
  onRedirect: handleRedirect,
132
271
  });
133
- thenable = createRscThenable(guardedStream);
272
+ const thenable = createRscThenable(guardedStream);
273
+ const node = createRscNavigationCacheNode({
274
+ href,
275
+ thenable,
276
+ routerState: readAkanRouterStateResponseHeader(res.headers),
277
+ });
278
+ nodeRef.current = node;
134
279
  return {
135
280
  type: "rsc",
136
- thenable,
281
+ node,
137
282
  };
138
283
  }
139
284
 
140
- const rscCache = new Map<string, RscThenable>();
285
+ const rscCache = new Map<string, RscCacheNode>();
286
+ const rscPatchCache = new Map<string, RscPatchNavigationCacheNode<RscThenable>>();
141
287
  const initialThenable = createRscThenable(createInitialRscStream());
142
- rscCache.set(normalizeHref(window.location.href), initialThenable);
288
+ const initialRouterState = decodeAkanRouterState(globalThis.__AKAN_RSC_INITIAL_STATE__);
289
+ const initialNode = createRscNavigationCacheNode({
290
+ href: normalizeHref(window.location.href),
291
+ thenable: initialThenable,
292
+ routerState: initialRouterState,
293
+ });
294
+ rscCache.set(initialNode.href, initialNode);
295
+ let currentRouterState: AkanRouterStateV1 | null = initialRouterState;
296
+ let currentSegmentTree: RscSegmentCacheNode | null = createAkanSegmentCacheTree(initialNode);
297
+ let currentFullNode: RscCacheNode = initialNode;
298
+ let currentCommitKind: "full" | "patch" = "full";
143
299
  let navigationSeq = 0;
144
300
 
301
+ function rememberCommittedRouteState(node: RscCacheNode): void {
302
+ rscPatchCache.clear();
303
+ if (!node.routerState) return;
304
+ currentRouterState = node.routerState;
305
+ currentSegmentTree = createAkanSegmentCacheTree(node);
306
+ currentFullNode = node;
307
+ currentCommitKind = "full";
308
+ }
309
+
310
+ function rememberPatchedRouteState(tree: RscSegmentCacheNode, patchedNode: RscSegmentCacheNode): void {
311
+ currentRouterState = patchedNode.routerState;
312
+ currentSegmentTree = tree;
313
+ currentCommitKind = "patch";
314
+ }
315
+
145
316
  function Root(): ReactNode {
146
317
  const [thenable, setThenable] = useState<RscThenable>(initialThenable);
147
318
  const [scrollToTopTick, setScrollToTopTick] = useState(0);
@@ -153,39 +324,64 @@ function Root(): ReactNode {
153
324
 
154
325
  globalThis.__AKAN_RSC_CLEAR_CACHE__ = () => {
155
326
  rscCache.clear();
156
- rscCache.set(normalizeHref(window.location.href), thenable);
327
+ rscPatchCache.clear();
328
+ if (currentCommitKind === "patch") {
329
+ void globalThis.__AKAN_RSC_REFRESH__?.();
330
+ return;
331
+ }
332
+ const href = normalizeHref(window.location.href);
333
+ const currentFullState = currentFullNode.routerState;
334
+ const canRestoreFullNode =
335
+ currentFullNode.href === href &&
336
+ ((!currentFullState && !currentRouterState) ||
337
+ (currentFullState !== null &&
338
+ currentRouterState !== null &&
339
+ currentFullState.routeId === currentRouterState.routeId));
340
+ if (canRestoreFullNode) {
341
+ resetAkanSegmentOutletPatches();
342
+ rscCache.set(href, currentFullNode);
343
+ }
157
344
  };
158
345
 
159
346
  globalThis.__AKAN_RSC_REFRESH__ = async (options = {}) => {
160
347
  const navId = ++navigationSeq;
161
348
  const target = normalizeHref(window.location.href);
162
349
  rscCache.delete(target);
350
+ rscPatchCache.clear();
163
351
  try {
164
352
  const next = await fetchRsc(target, {
165
353
  ...options,
166
354
  replaceOnRedirect: true,
355
+ sendRouterState: false,
356
+ navId,
167
357
  shouldApplyNavigation: () => navId === navigationSeq,
168
358
  });
169
359
  if (next.type === "redirected") return;
170
- observeRscNavigation({
360
+ if (next.type === "patched") return;
361
+ observeRscNavigationNode({
171
362
  cache: rscCache,
172
- href: target,
173
- thenable: next.thenable,
363
+ node: next.node,
174
364
  navId,
175
365
  getCurrentNavId: () => navigationSeq,
176
366
  isExpectedNavigationError: (error) => error instanceof RscRedirectNavigationStarted,
177
367
  onLatestError: (error) => hardNavigateAfterRscFailure(target, true, error),
178
368
  });
179
- commitLatestRscNavigation({
369
+
370
+ await next.node.thenable;
371
+ const committed = commitLatestRscNavigation({
180
372
  cache: rscCache,
181
373
  href: target,
182
- thenable: next.thenable,
374
+ thenable: next.node,
183
375
  maxEntries: MAX_RSC_CACHE_ENTRIES,
184
376
  startTransition,
185
- commitThenable: setThenable,
377
+ commitThenable: (node) => {
378
+ resetAkanSegmentOutletPatches();
379
+ setThenable(node.thenable);
380
+ },
186
381
  navId,
187
382
  getCurrentNavId: () => navigationSeq,
188
383
  });
384
+ if (committed) rememberCommittedRouteState(next.node);
189
385
  } catch (error) {
190
386
  if (error instanceof RscRedirectNavigationStarted) return;
191
387
  if (navId === navigationSeq) hardNavigateAfterRscFailure(target, true, error);
@@ -197,33 +393,106 @@ function Root(): ReactNode {
197
393
  const target = normalizeHref(href);
198
394
  const scrollToTop = options.scrollToTop ?? true;
199
395
  try {
200
- let next = rscCache.get(target);
201
- if (!next) {
396
+ let nextNode = rscCache.get(target);
397
+ if (!nextNode) {
398
+ const cachedPatch = rscPatchCache.get(target);
399
+ if (cachedPatch) {
400
+ const patchResult = resolveCachedRscPatchNavigation({
401
+ currentTree: currentSegmentTree,
402
+ node: cachedPatch,
403
+ partialCommitEnabled: isAkanRscPartialCommitEnabled(),
404
+ navId,
405
+ getCurrentNavId: () => navigationSeq,
406
+ });
407
+ if (patchResult.status === "patched") {
408
+ const replayedPatch = {
409
+ type: "patched" as const,
410
+ tree: patchResult.tree,
411
+ patchedNode: patchResult.patchedNode,
412
+ patch: cachedPatch.patch,
413
+ outletKey: patchResult.outletKey,
414
+ headSnapshot: patchResult.headSnapshot,
415
+ };
416
+ if (
417
+ commitRscPatchNavigation({
418
+ target,
419
+ patch: replayedPatch,
420
+ replace: options.replace,
421
+ scrollToTop,
422
+ bumpScrollToTop: () => setScrollToTopTick((tick) => tick + 1),
423
+ })
424
+ ) {
425
+ rememberPatchedRouteState(patchResult.tree, patchResult.patchedNode);
426
+ rememberRscPatchCacheNode(rscPatchCache, cachedPatch, MAX_RSC_CACHE_ENTRIES);
427
+ return;
428
+ }
429
+ }
430
+ rscPatchCache.delete(target);
431
+ }
202
432
  const fetched = await fetchRsc(target, {
203
433
  replaceOnRedirect: options.replace,
434
+ navId,
204
435
  shouldApplyNavigation: () => navId === navigationSeq,
205
436
  });
206
437
  if (fetched.type === "redirected") return;
207
- next = fetched.thenable;
438
+ if (fetched.type === "patched") {
439
+ if (navId !== navigationSeq) return;
440
+ if (
441
+ commitRscPatchNavigation({
442
+ target,
443
+ patch: fetched,
444
+ replace: options.replace,
445
+ scrollToTop,
446
+ bumpScrollToTop: () => setScrollToTopTick((tick) => tick + 1),
447
+ })
448
+ ) {
449
+ rememberPatchedRouteState(fetched.tree, fetched.patchedNode);
450
+ const patchCacheNode = createRscPatchNavigationCacheNode({
451
+ href: target,
452
+ patch: fetched.patch,
453
+ patchedNode: fetched.patchedNode,
454
+ outletKey: fetched.outletKey,
455
+ headSnapshot: fetched.headSnapshot,
456
+ });
457
+ if (patchCacheNode) rememberRscPatchCacheNode(rscPatchCache, patchCacheNode, MAX_RSC_CACHE_ENTRIES);
458
+ return;
459
+ }
460
+ rscPatchCache.delete(target);
461
+ const fallback = await fetchRsc(target, {
462
+ replaceOnRedirect: options.replace,
463
+ sendRouterState: false,
464
+ navId,
465
+ shouldApplyNavigation: () => navId === navigationSeq,
466
+ });
467
+ if (fallback.type === "redirected") return;
468
+ if (fallback.type === "patched") throw new Error("[rscClient] full fallback unexpectedly returned a patch");
469
+ nextNode = fallback.node;
470
+ } else {
471
+ nextNode = fetched.node;
472
+ }
208
473
  } else {
209
- rememberRscCacheEntry(rscCache, target, next, MAX_RSC_CACHE_ENTRIES);
474
+ rememberRscCacheNode(rscCache, nextNode, MAX_RSC_CACHE_ENTRIES);
210
475
  }
211
- observeRscNavigation({
476
+ observeRscNavigationNode({
212
477
  cache: rscCache,
213
- href: target,
214
- thenable: next,
478
+ node: nextNode,
215
479
  navId,
216
480
  getCurrentNavId: () => navigationSeq,
217
481
  isExpectedNavigationError: (error) => error instanceof RscRedirectNavigationStarted,
218
482
  onLatestError: (error) => hardNavigateAfterRscFailure(target, options.replace, error),
219
483
  });
220
- commitLatestRscNavigation({
484
+
485
+ await nextNode.thenable;
486
+ const committed = commitLatestRscNavigation({
221
487
  cache: rscCache,
222
488
  href: target,
223
- thenable: next,
489
+ thenable: nextNode,
224
490
  maxEntries: MAX_RSC_CACHE_ENTRIES,
225
491
  startTransition,
226
- commitThenable: setThenable,
492
+ commitThenable: (node) => {
493
+ resetAkanSegmentOutletPatches();
494
+ setThenable(node.thenable);
495
+ },
227
496
  updateHistory: () => {
228
497
  if (options.replace) window.history.replaceState(null, "", target);
229
498
  else window.history.pushState(null, "", target);
@@ -233,13 +502,14 @@ function Root(): ReactNode {
233
502
  navId,
234
503
  getCurrentNavId: () => navigationSeq,
235
504
  });
505
+ if (committed) rememberCommittedRouteState(nextNode);
236
506
  } catch (error) {
237
507
  if (error instanceof RscRedirectNavigationStarted) return;
238
508
  if (navId === navigationSeq) hardNavigateAfterRscFailure(target, options.replace, error);
239
509
  }
240
510
  };
241
511
 
242
- return use(thenable);
512
+ return use(thenable as Usable<ReactNode>);
243
513
  }
244
514
 
245
515
  window.addEventListener("popstate", () => {
@@ -0,0 +1,57 @@
1
+ import {
2
+ type AkanRouterStateV1,
3
+ type AkanRscPatchMetadata,
4
+ appendAkanRouterStateRequestHeaders,
5
+ readAkanRscPatchMetadataResponseHeaders,
6
+ } from "./routeState";
7
+
8
+ type RscNavigate = (href: string, options?: { replace?: boolean; scrollToTop?: boolean }) => Promise<void> | void;
9
+
10
+ export type RscClientFetchResponseResult =
11
+ | { type: "response"; response: Response }
12
+ | { type: "patch"; response: Response; patch: AkanRscPatchMetadata }
13
+ | { type: "redirected"; status?: number };
14
+
15
+ export async function fetchRscNavigationResponse(
16
+ href: string,
17
+ options: {
18
+ buildId?: number;
19
+ currentRouterState: AkanRouterStateV1 | null;
20
+ navigate?: RscNavigate;
21
+ sendRouterState?: boolean;
22
+ shouldApplyNavigation?: () => boolean;
23
+ },
24
+ ): Promise<RscClientFetchResponseResult> {
25
+ const shouldApplyNavigation = options.shouldApplyNavigation ?? (() => true);
26
+ const endpoint = new URL("/__rsc", window.location.origin);
27
+ endpoint.searchParams.set("url", href);
28
+ if (options.buildId !== undefined) endpoint.searchParams.set("buildId", String(options.buildId));
29
+ const headers = new Headers({ Accept: "text/x-component", "Cache-Control": "no-cache" });
30
+ if (options.sendRouterState !== false) appendAkanRouterStateRequestHeaders(headers, options.currentRouterState);
31
+ const response = await fetch(endpoint, {
32
+ headers,
33
+ credentials: "same-origin",
34
+ cache: "no-store",
35
+ });
36
+ const redirect = response.headers.get("X-Akan-Redirect");
37
+ if (redirect) {
38
+ const method = response.headers.get("X-Akan-Redirect-Method");
39
+ const statusHeader = response.headers.get("X-Akan-Redirect-Status");
40
+ const status = statusHeader ? Number(statusHeader) : undefined;
41
+ if (shouldApplyNavigation()) await options.navigate?.(redirect, { replace: method !== "push", scrollToTop: true });
42
+ return { type: "redirected", status };
43
+ }
44
+ if (response.headers.get("X-Akan-Rsc-Partial") === "patch") {
45
+ const patch = readAkanRscPatchMetadataResponseHeaders(response.headers);
46
+ if (options.sendRouterState === false) throw new Error("[rscClient] RSC full fallback returned a patch response");
47
+ if (!patch) {
48
+ await response.body?.cancel();
49
+ return fetchRscNavigationResponse(href, {
50
+ ...options,
51
+ sendRouterState: false,
52
+ });
53
+ }
54
+ return { type: "patch", response, patch };
55
+ }
56
+ return { type: "response", response };
57
+ }