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.
- package/base/primitiveRegistry.ts +19 -12
- 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/base/primitiveRegistry.d.ts +6 -6
- package/types/client/csrTypes.d.ts +16 -0
- package/types/constant/fieldInfo.d.ts +8 -7
- package/types/fetch/requestStorage.d.ts +2 -0
- package/types/server/cachePolicy.d.ts +36 -0
- package/types/server/metadata.d.ts +10 -1
- package/types/server/routeElementComposer.d.ts +9 -1
- package/types/server/routeState.d.ts +94 -0
- package/types/server/rscClient.d.ts +1 -0
- package/types/server/rscClientFetch.d.ts +24 -0
- package/types/server/rscClientPatch.d.ts +21 -0
- package/types/server/rscHeadPatch.d.ts +12 -0
- package/types/server/rscNavigationState.d.ts +78 -0
- package/types/server/rscPartialCommit.d.ts +1 -0
- package/types/server/rscPatchSafety.d.ts +8 -0
- package/types/server/rscSegmentOutlet.d.ts +17 -0
- package/types/server/rscSegmentOutletReference.d.ts +2 -0
- package/types/server/rscWorker.d.ts +5 -0
- package/types/server/rscWorkerCache.d.ts +63 -0
- package/types/server/rscWorkerHost.d.ts +8 -4
- package/types/server/rscWorkerReplay.d.ts +3 -0
- package/types/server/ssrFromRscRenderer.d.ts +1 -0
- package/types/server/ssrTypes.d.ts +17 -0
- package/types/server/types.d.ts +4 -0
- package/types/server/webRouter.d.ts +7 -3
- package/types/service/predefinedAdaptor/database.adaptor.d.ts +6 -0
- package/types/ui/Button.d.ts +1 -1
- package/types/ui/ClientSide.d.ts +1 -1
- package/types/ui/Constant/Doc.d.ts +6 -6
- package/types/ui/Constant/Mermaid.d.ts +1 -1
- package/types/ui/Constant/index.d.ts +1 -1
- package/types/ui/Constant/schemaDoc.d.ts +1 -1
- package/types/ui/Copy.d.ts +1 -1
- package/types/ui/CsrImage.d.ts +1 -1
- package/types/ui/Data/CardList.d.ts +1 -1
- package/types/ui/Data/Dashboard.d.ts +1 -1
- package/types/ui/Data/Insight.d.ts +1 -1
- package/types/ui/Data/Item.d.ts +6 -6
- package/types/ui/Data/ListContainer.d.ts +1 -1
- package/types/ui/Data/Pagination.d.ts +1 -1
- package/types/ui/Data/TableList.d.ts +1 -1
- package/types/ui/DatePicker.d.ts +3 -3
- package/types/ui/Dialog/Close.d.ts +1 -1
- package/types/ui/Dialog/Content.d.ts +1 -1
- package/types/ui/Dialog/Provider.d.ts +1 -1
- package/types/ui/Dialog/Trigger.d.ts +1 -1
- package/types/ui/Dialog/index.d.ts +3 -3
- package/types/ui/DragAction.d.ts +4 -4
- package/types/ui/DraggableList.d.ts +3 -3
- package/types/ui/Dropdown.d.ts +1 -1
- package/types/ui/Empty.d.ts +1 -1
- package/types/ui/Field.d.ts +22 -22
- package/types/ui/Image.d.ts +1 -1
- package/types/ui/InfiniteScroll.d.ts +1 -1
- package/types/ui/Input.d.ts +6 -6
- package/types/ui/KeyboardAvoiding.d.ts +1 -1
- package/types/ui/Layout/BottomAction.d.ts +1 -1
- package/types/ui/Layout/BottomInset.d.ts +1 -1
- package/types/ui/Layout/BottomTab.d.ts +1 -1
- package/types/ui/Layout/Header.d.ts +1 -1
- package/types/ui/Layout/LeftSider.d.ts +1 -1
- package/types/ui/Layout/Navbar.d.ts +1 -1
- package/types/ui/Layout/RightSider.d.ts +1 -1
- package/types/ui/Layout/Sider.d.ts +1 -1
- package/types/ui/Layout/Template.d.ts +1 -1
- package/types/ui/Layout/TopLeftAction.d.ts +1 -1
- package/types/ui/Layout/Unit.d.ts +1 -1
- package/types/ui/Layout/View.d.ts +1 -1
- package/types/ui/Layout/Zone.d.ts +1 -1
- package/types/ui/Layout/index.d.ts +12 -12
- package/types/ui/Link/Back.d.ts +1 -1
- package/types/ui/Link/Close.d.ts +1 -1
- package/types/ui/Link/CsrLink.d.ts +1 -1
- package/types/ui/Link/Lang.d.ts +1 -1
- package/types/ui/Link/SsrLink.d.ts +1 -1
- package/types/ui/Link/index.d.ts +1 -1
- package/types/ui/Load/Edit.d.ts +1 -1
- package/types/ui/Load/Edit_Client.d.ts +1 -1
- package/types/ui/Load/PageCSR.d.ts +1 -1
- package/types/ui/Load/Pagination.d.ts +1 -1
- package/types/ui/Load/Units.d.ts +1 -1
- package/types/ui/Load/View.d.ts +1 -1
- package/types/ui/Loading/Area.d.ts +1 -1
- package/types/ui/Loading/Button.d.ts +1 -1
- package/types/ui/Loading/Input.d.ts +1 -1
- package/types/ui/Loading/ProgressBar.d.ts +1 -1
- package/types/ui/Loading/Skeleton.d.ts +1 -1
- package/types/ui/Loading/Spin.d.ts +1 -1
- package/types/ui/Loading/index.d.ts +6 -6
- package/types/ui/Menu.d.ts +1 -1
- package/types/ui/Modal.d.ts +1 -1
- package/types/ui/Model/AdminPanel.d.ts +1 -1
- package/types/ui/Model/Edit.d.ts +1 -1
- package/types/ui/Model/EditModal.d.ts +1 -1
- package/types/ui/Model/EditWrapper.d.ts +1 -1
- package/types/ui/Model/LoadInit.d.ts +1 -1
- package/types/ui/Model/New.d.ts +1 -1
- package/types/ui/Model/NewWrapper.d.ts +1 -1
- package/types/ui/Model/NewWrapper_Client.d.ts +1 -1
- package/types/ui/Model/Remove.d.ts +1 -1
- package/types/ui/Model/RemoveWrapper.d.ts +1 -1
- package/types/ui/Model/SureToRemove.d.ts +1 -1
- package/types/ui/Model/View.d.ts +1 -1
- package/types/ui/Model/ViewEditModal.d.ts +1 -1
- package/types/ui/Model/ViewModal.d.ts +1 -1
- package/types/ui/Model/ViewWrapper.d.ts +1 -1
- package/types/ui/More.d.ts +1 -1
- package/types/ui/ObjectId.d.ts +1 -1
- package/types/ui/Popconfirm.d.ts +1 -1
- package/types/ui/Radio.d.ts +2 -2
- package/types/ui/RecentTime.d.ts +1 -1
- package/types/ui/Refresh.d.ts +1 -1
- package/types/ui/ScreenNavigator.d.ts +3 -3
- package/types/ui/Select.d.ts +1 -1
- package/types/ui/Signal/Arg.d.ts +13 -13
- package/types/ui/Signal/Doc.d.ts +6 -6
- package/types/ui/Signal/Listener.d.ts +2 -2
- package/types/ui/Signal/Message.d.ts +4 -4
- package/types/ui/Signal/Object.d.ts +4 -4
- package/types/ui/Signal/PubSub.d.ts +4 -4
- package/types/ui/Signal/Request.d.ts +2 -2
- package/types/ui/Signal/Response.d.ts +3 -3
- package/types/ui/Signal/RestApi.d.ts +5 -5
- package/types/ui/Signal/WebSocket.d.ts +2 -2
- package/types/ui/System/CSR.d.ts +5 -5
- package/types/ui/System/Client.d.ts +8 -8
- package/types/ui/System/Common.d.ts +2 -2
- package/types/ui/System/DevModeToggle.d.ts +1 -1
- package/types/ui/System/Gtag.d.ts +1 -1
- package/types/ui/System/Messages.d.ts +1 -1
- package/types/ui/System/Reconnect.d.ts +1 -1
- package/types/ui/System/Root.d.ts +1 -1
- package/types/ui/System/SSR.d.ts +4 -4
- package/types/ui/System/SelectLanguage.d.ts +1 -1
- package/types/ui/System/ThemeToggle.d.ts +1 -1
- package/types/ui/System/index.d.ts +7 -7
- package/types/ui/Tab/Menu.d.ts +1 -1
- package/types/ui/Tab/Menus.d.ts +1 -1
- package/types/ui/Tab/Panel.d.ts +1 -1
- package/types/ui/Tab/Provider.d.ts +1 -1
- package/types/ui/Tab/index.d.ts +4 -4
- package/types/ui/Table.d.ts +1 -1
- package/types/ui/ToggleSelect.d.ts +2 -2
- package/types/ui/Unauthorized.d.ts +1 -1
- package/ui/Constant/schemaDoc.ts +1 -1
- package/server/resolver/resolver.contract.fixture.ts +0 -222
package/server/rscClient.tsx
CHANGED
|
@@ -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
|
-
|
|
9
|
-
|
|
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
|
-
|
|
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: {
|
|
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
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
110
|
-
if (
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
281
|
+
node,
|
|
137
282
|
};
|
|
138
283
|
}
|
|
139
284
|
|
|
140
|
-
const rscCache = new Map<string,
|
|
285
|
+
const rscCache = new Map<string, RscCacheNode>();
|
|
286
|
+
const rscPatchCache = new Map<string, RscPatchNavigationCacheNode<RscThenable>>();
|
|
141
287
|
const initialThenable = createRscThenable(createInitialRscStream());
|
|
142
|
-
|
|
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
|
-
|
|
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
|
-
|
|
360
|
+
if (next.type === "patched") return;
|
|
361
|
+
observeRscNavigationNode({
|
|
171
362
|
cache: rscCache,
|
|
172
|
-
|
|
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
|
-
|
|
369
|
+
|
|
370
|
+
await next.node.thenable;
|
|
371
|
+
const committed = commitLatestRscNavigation({
|
|
180
372
|
cache: rscCache,
|
|
181
373
|
href: target,
|
|
182
|
-
thenable: next.
|
|
374
|
+
thenable: next.node,
|
|
183
375
|
maxEntries: MAX_RSC_CACHE_ENTRIES,
|
|
184
376
|
startTransition,
|
|
185
|
-
commitThenable:
|
|
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
|
|
201
|
-
if (!
|
|
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
|
-
|
|
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
|
-
|
|
474
|
+
rememberRscCacheNode(rscCache, nextNode, MAX_RSC_CACHE_ENTRIES);
|
|
210
475
|
}
|
|
211
|
-
|
|
476
|
+
observeRscNavigationNode({
|
|
212
477
|
cache: rscCache,
|
|
213
|
-
|
|
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
|
-
|
|
484
|
+
|
|
485
|
+
await nextNode.thenable;
|
|
486
|
+
const committed = commitLatestRscNavigation({
|
|
221
487
|
cache: rscCache,
|
|
222
488
|
href: target,
|
|
223
|
-
thenable:
|
|
489
|
+
thenable: nextNode,
|
|
224
490
|
maxEntries: MAX_RSC_CACHE_ENTRIES,
|
|
225
491
|
startTransition,
|
|
226
|
-
commitThenable:
|
|
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
|
+
}
|