akanjs 2.3.0 → 2.3.1-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/csrTypes.ts +16 -0
- package/constant/fieldInfo.ts +11 -9
- package/constant/getDefault.ts +1 -1
- package/fetch/requestStorage.ts +5 -0
- package/package.json +4 -4
- package/server/akanApp.ts +26 -3
- package/server/akanServer.ts +5 -1
- package/server/cachePolicy.ts +99 -5
- package/server/imageOptimizer.ts +14 -1
- package/server/metadata.tsx +117 -33
- package/server/resolver/database.resolver.ts +4 -4
- package/server/routeElementComposer.tsx +46 -14
- package/server/routeState.ts +379 -0
- package/server/routeTreeBuilder.ts +3 -2
- package/server/rscClient.tsx +316 -46
- package/server/rscClientFetch.ts +57 -0
- package/server/rscClientPatch.ts +157 -0
- package/server/rscHeadPatch.ts +80 -0
- package/server/rscNavigationState.ts +315 -0
- package/server/rscPartialCommit.ts +3 -0
- package/server/rscPatchSafety.ts +57 -0
- package/server/rscSegmentOutlet.tsx +69 -0
- package/server/rscSegmentOutletReference.ts +24 -0
- package/server/rscWorker.tsx +380 -53
- package/server/rscWorkerCache.ts +180 -0
- package/server/rscWorkerHost.ts +40 -12
- package/server/rscWorkerReplay.ts +11 -2
- package/server/ssrFromRscRenderer.tsx +15 -10
- package/server/ssrTypes.ts +18 -0
- package/server/types.tsx +4 -0
- package/server/webRouter.ts +198 -42
- package/service/predefinedAdaptor/database.adaptor.ts +72 -25
- package/signal/signalContext.ts +1 -1
- package/types/client/csrTypes.d.ts +16 -0
- package/types/constant/fieldInfo.d.ts +8 -7
- package/types/fetch/requestStorage.d.ts +2 -0
- package/types/server/cachePolicy.d.ts +36 -0
- package/types/server/metadata.d.ts +10 -1
- package/types/server/routeElementComposer.d.ts +9 -1
- package/types/server/routeState.d.ts +94 -0
- package/types/server/rscClient.d.ts +1 -0
- package/types/server/rscClientFetch.d.ts +24 -0
- package/types/server/rscClientPatch.d.ts +21 -0
- package/types/server/rscHeadPatch.d.ts +12 -0
- package/types/server/rscNavigationState.d.ts +78 -0
- package/types/server/rscPartialCommit.d.ts +1 -0
- package/types/server/rscPatchSafety.d.ts +8 -0
- package/types/server/rscSegmentOutlet.d.ts +17 -0
- package/types/server/rscSegmentOutletReference.d.ts +2 -0
- package/types/server/rscWorker.d.ts +5 -0
- package/types/server/rscWorkerCache.d.ts +63 -0
- package/types/server/rscWorkerHost.d.ts +8 -4
- package/types/server/rscWorkerReplay.d.ts +3 -0
- package/types/server/ssrFromRscRenderer.d.ts +1 -0
- package/types/server/ssrTypes.d.ts +17 -0
- package/types/server/types.d.ts +4 -0
- package/types/server/webRouter.d.ts +7 -3
- package/types/service/predefinedAdaptor/database.adaptor.d.ts +6 -0
- package/types/ui/Button.d.ts +1 -1
- package/types/ui/ClientSide.d.ts +1 -1
- package/types/ui/Constant/Doc.d.ts +6 -6
- package/types/ui/Constant/Mermaid.d.ts +1 -1
- package/types/ui/Constant/index.d.ts +1 -1
- package/types/ui/Constant/schemaDoc.d.ts +1 -1
- package/types/ui/Copy.d.ts +1 -1
- package/types/ui/CsrImage.d.ts +1 -1
- package/types/ui/Data/CardList.d.ts +1 -1
- package/types/ui/Data/Dashboard.d.ts +1 -1
- package/types/ui/Data/Insight.d.ts +1 -1
- package/types/ui/Data/Item.d.ts +6 -6
- package/types/ui/Data/ListContainer.d.ts +1 -1
- package/types/ui/Data/Pagination.d.ts +1 -1
- package/types/ui/Data/TableList.d.ts +1 -1
- package/types/ui/DatePicker.d.ts +3 -3
- package/types/ui/Dialog/Close.d.ts +1 -1
- package/types/ui/Dialog/Content.d.ts +1 -1
- package/types/ui/Dialog/Provider.d.ts +1 -1
- package/types/ui/Dialog/Trigger.d.ts +1 -1
- package/types/ui/Dialog/index.d.ts +3 -3
- package/types/ui/DragAction.d.ts +4 -4
- package/types/ui/DraggableList.d.ts +3 -3
- package/types/ui/Dropdown.d.ts +1 -1
- package/types/ui/Empty.d.ts +1 -1
- package/types/ui/Field.d.ts +22 -22
- package/types/ui/Image.d.ts +1 -1
- package/types/ui/InfiniteScroll.d.ts +1 -1
- package/types/ui/Input.d.ts +6 -6
- package/types/ui/KeyboardAvoiding.d.ts +1 -1
- package/types/ui/Layout/BottomAction.d.ts +1 -1
- package/types/ui/Layout/BottomInset.d.ts +1 -1
- package/types/ui/Layout/BottomTab.d.ts +1 -1
- package/types/ui/Layout/Header.d.ts +1 -1
- package/types/ui/Layout/LeftSider.d.ts +1 -1
- package/types/ui/Layout/Navbar.d.ts +1 -1
- package/types/ui/Layout/RightSider.d.ts +1 -1
- package/types/ui/Layout/Sider.d.ts +1 -1
- package/types/ui/Layout/Template.d.ts +1 -1
- package/types/ui/Layout/TopLeftAction.d.ts +1 -1
- package/types/ui/Layout/Unit.d.ts +1 -1
- package/types/ui/Layout/View.d.ts +1 -1
- package/types/ui/Layout/Zone.d.ts +1 -1
- package/types/ui/Layout/index.d.ts +12 -12
- package/types/ui/Link/Back.d.ts +1 -1
- package/types/ui/Link/Close.d.ts +1 -1
- package/types/ui/Link/CsrLink.d.ts +1 -1
- package/types/ui/Link/Lang.d.ts +1 -1
- package/types/ui/Link/SsrLink.d.ts +1 -1
- package/types/ui/Link/index.d.ts +1 -1
- package/types/ui/Load/Edit.d.ts +1 -1
- package/types/ui/Load/Edit_Client.d.ts +1 -1
- package/types/ui/Load/PageCSR.d.ts +1 -1
- package/types/ui/Load/Pagination.d.ts +1 -1
- package/types/ui/Load/Units.d.ts +1 -1
- package/types/ui/Load/View.d.ts +1 -1
- package/types/ui/Loading/Area.d.ts +1 -1
- package/types/ui/Loading/Button.d.ts +1 -1
- package/types/ui/Loading/Input.d.ts +1 -1
- package/types/ui/Loading/ProgressBar.d.ts +1 -1
- package/types/ui/Loading/Skeleton.d.ts +1 -1
- package/types/ui/Loading/Spin.d.ts +1 -1
- package/types/ui/Loading/index.d.ts +6 -6
- package/types/ui/Menu.d.ts +1 -1
- package/types/ui/Modal.d.ts +1 -1
- package/types/ui/Model/AdminPanel.d.ts +1 -1
- package/types/ui/Model/Edit.d.ts +1 -1
- package/types/ui/Model/EditModal.d.ts +1 -1
- package/types/ui/Model/EditWrapper.d.ts +1 -1
- package/types/ui/Model/LoadInit.d.ts +1 -1
- package/types/ui/Model/New.d.ts +1 -1
- package/types/ui/Model/NewWrapper.d.ts +1 -1
- package/types/ui/Model/NewWrapper_Client.d.ts +1 -1
- package/types/ui/Model/Remove.d.ts +1 -1
- package/types/ui/Model/RemoveWrapper.d.ts +1 -1
- package/types/ui/Model/SureToRemove.d.ts +1 -1
- package/types/ui/Model/View.d.ts +1 -1
- package/types/ui/Model/ViewEditModal.d.ts +1 -1
- package/types/ui/Model/ViewModal.d.ts +1 -1
- package/types/ui/Model/ViewWrapper.d.ts +1 -1
- package/types/ui/More.d.ts +1 -1
- package/types/ui/ObjectId.d.ts +1 -1
- package/types/ui/Popconfirm.d.ts +1 -1
- package/types/ui/Radio.d.ts +2 -2
- package/types/ui/RecentTime.d.ts +1 -1
- package/types/ui/Refresh.d.ts +1 -1
- package/types/ui/ScreenNavigator.d.ts +3 -3
- package/types/ui/Select.d.ts +1 -1
- package/types/ui/Signal/Arg.d.ts +13 -13
- package/types/ui/Signal/Doc.d.ts +6 -6
- package/types/ui/Signal/Listener.d.ts +2 -2
- package/types/ui/Signal/Message.d.ts +4 -4
- package/types/ui/Signal/Object.d.ts +4 -4
- package/types/ui/Signal/PubSub.d.ts +4 -4
- package/types/ui/Signal/Request.d.ts +2 -2
- package/types/ui/Signal/Response.d.ts +3 -3
- package/types/ui/Signal/RestApi.d.ts +5 -5
- package/types/ui/Signal/WebSocket.d.ts +2 -2
- package/types/ui/System/CSR.d.ts +5 -5
- package/types/ui/System/Client.d.ts +8 -8
- package/types/ui/System/Common.d.ts +2 -2
- package/types/ui/System/DevModeToggle.d.ts +1 -1
- package/types/ui/System/Gtag.d.ts +1 -1
- package/types/ui/System/Messages.d.ts +1 -1
- package/types/ui/System/Reconnect.d.ts +1 -1
- package/types/ui/System/Root.d.ts +1 -1
- package/types/ui/System/SSR.d.ts +4 -4
- package/types/ui/System/SelectLanguage.d.ts +1 -1
- package/types/ui/System/ThemeToggle.d.ts +1 -1
- package/types/ui/System/index.d.ts +7 -7
- package/types/ui/Tab/Menu.d.ts +1 -1
- package/types/ui/Tab/Menus.d.ts +1 -1
- package/types/ui/Tab/Panel.d.ts +1 -1
- package/types/ui/Tab/Provider.d.ts +1 -1
- package/types/ui/Tab/index.d.ts +4 -4
- package/types/ui/Table.d.ts +1 -1
- package/types/ui/ToggleSelect.d.ts +2 -2
- package/types/ui/Unauthorized.d.ts +1 -1
- package/ui/Constant/schemaDoc.ts +1 -1
- package/server/resolver/resolver.contract.fixture.ts +0 -222
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import {
|
|
2
|
+
hasRouteCacheInvalidationScope,
|
|
3
|
+
type LruTtlCache,
|
|
4
|
+
type RouteCacheEntry,
|
|
5
|
+
type RouteCacheInvalidation,
|
|
6
|
+
type RouteCacheRenderState,
|
|
7
|
+
shouldInvalidateRouteCacheEntry,
|
|
8
|
+
} from "./cachePolicy";
|
|
9
|
+
import type { AkanRouterStateV1, AkanRscPatchDecision, AkanRscPatchMetadata } from "./routeState";
|
|
10
|
+
|
|
11
|
+
export interface CachedRscResult {
|
|
12
|
+
chunks: Uint8Array[];
|
|
13
|
+
bytes: number;
|
|
14
|
+
chunksCount: number;
|
|
15
|
+
pathname: string;
|
|
16
|
+
routeId?: string;
|
|
17
|
+
tags?: string[];
|
|
18
|
+
theme?: string;
|
|
19
|
+
cacheState: RouteCacheRenderState;
|
|
20
|
+
patch?: CachedRscPatchMetadata;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface CachedRscPatchMetadata {
|
|
24
|
+
targetRouterState: AkanRouterStateV1;
|
|
25
|
+
patch: AkanRscPatchMetadata;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface RscPatchCacheKeyInput {
|
|
29
|
+
baseEntry: RouteCacheEntry;
|
|
30
|
+
targetRouterState: AkanRouterStateV1;
|
|
31
|
+
patch: AkanRscPatchMetadata;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function createRscPatchCacheKey({ baseEntry, targetRouterState, patch }: RscPatchCacheKeyInput): string {
|
|
35
|
+
return [
|
|
36
|
+
"patch-v1",
|
|
37
|
+
baseEntry.key,
|
|
38
|
+
targetRouterState.buildId ?? "",
|
|
39
|
+
targetRouterState.href,
|
|
40
|
+
targetRouterState.routeId,
|
|
41
|
+
JSON.stringify(targetRouterState.segments),
|
|
42
|
+
patch.patchStartIndex,
|
|
43
|
+
patch.patchStartSegmentKey,
|
|
44
|
+
JSON.stringify(patch.segmentPath),
|
|
45
|
+
patch.headSafe === true ? "head-safe" : "head-unsafe",
|
|
46
|
+
].join("\n");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function createRscPatchCacheEntry(input: RscPatchCacheKeyInput): RouteCacheEntry {
|
|
50
|
+
return { key: createRscPatchCacheKey(input), ttl: input.baseEntry.ttl };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function isRscPatchResultCacheEligible(input: {
|
|
54
|
+
partialCommitEnabled: boolean;
|
|
55
|
+
patch?: AkanRscPatchMetadata;
|
|
56
|
+
}): boolean {
|
|
57
|
+
return (
|
|
58
|
+
input.partialCommitEnabled &&
|
|
59
|
+
input.patch?.headSafe === true &&
|
|
60
|
+
input.patch.headSnapshot !== undefined &&
|
|
61
|
+
!input.patch.headSnapshotFailure
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function createCachedRscPatchMetadata(input: {
|
|
66
|
+
targetRouterState: AkanRouterStateV1;
|
|
67
|
+
patch: AkanRscPatchMetadata;
|
|
68
|
+
}): CachedRscPatchMetadata {
|
|
69
|
+
return {
|
|
70
|
+
targetRouterState: input.targetRouterState,
|
|
71
|
+
patch: input.patch,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function isCachedRscPatchMetadataCompatible(input: {
|
|
76
|
+
cached?: CachedRscPatchMetadata;
|
|
77
|
+
targetRouterState: AkanRouterStateV1 | null;
|
|
78
|
+
safePatchDecision: AkanRscPatchDecision;
|
|
79
|
+
}): boolean {
|
|
80
|
+
if (!input.cached || !input.targetRouterState || input.safePatchDecision.status !== "patch") return false;
|
|
81
|
+
const currentPatch = input.safePatchDecision.patch;
|
|
82
|
+
if (!currentPatch) return false;
|
|
83
|
+
const cachedPatch = input.cached.patch;
|
|
84
|
+
return (
|
|
85
|
+
input.cached.targetRouterState.buildId === input.targetRouterState.buildId &&
|
|
86
|
+
input.cached.targetRouterState.href === input.targetRouterState.href &&
|
|
87
|
+
input.cached.targetRouterState.routeId === input.targetRouterState.routeId &&
|
|
88
|
+
JSON.stringify(input.cached.targetRouterState.segments) === JSON.stringify(input.targetRouterState.segments) &&
|
|
89
|
+
cachedPatch.patchStartIndex === currentPatch.patchStartIndex &&
|
|
90
|
+
cachedPatch.patchStartSegmentKey === currentPatch.patchStartSegmentKey &&
|
|
91
|
+
JSON.stringify(cachedPatch.segmentPath) === JSON.stringify(currentPatch.segmentPath) &&
|
|
92
|
+
cachedPatch.headSafe === true &&
|
|
93
|
+
cachedPatch.headSnapshot !== undefined &&
|
|
94
|
+
!cachedPatch.headSnapshotFailure
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function createRscWorkerCachedPatchReplayDecision(input: {
|
|
99
|
+
cached: CachedRscPatchMetadata;
|
|
100
|
+
safePatchDecision: AkanRscPatchDecision;
|
|
101
|
+
}): AkanRscPatchDecision {
|
|
102
|
+
return {
|
|
103
|
+
...input.safePatchDecision,
|
|
104
|
+
patch: input.cached.patch,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function resolveRscWorkerPatchCacheEntry(input: {
|
|
109
|
+
cacheEntry: RouteCacheEntry | null;
|
|
110
|
+
targetRouterState: AkanRouterStateV1 | null;
|
|
111
|
+
safePatchDecision: AkanRscPatchDecision;
|
|
112
|
+
partialCommitEnabled: boolean;
|
|
113
|
+
}): RouteCacheEntry | null {
|
|
114
|
+
const patch = input.safePatchDecision.status === "patch" ? input.safePatchDecision.patch : undefined;
|
|
115
|
+
if (
|
|
116
|
+
!input.cacheEntry ||
|
|
117
|
+
!input.targetRouterState ||
|
|
118
|
+
input.safePatchDecision.status !== "patch" ||
|
|
119
|
+
!patch ||
|
|
120
|
+
!isRscPatchResultCacheEligible({ partialCommitEnabled: input.partialCommitEnabled, patch })
|
|
121
|
+
) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
return createRscPatchCacheEntry({
|
|
125
|
+
baseEntry: input.cacheEntry,
|
|
126
|
+
targetRouterState: input.targetRouterState,
|
|
127
|
+
patch,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function shouldCollectRscWorkerRenderChunks(input: {
|
|
132
|
+
cacheEntry: RouteCacheEntry | null;
|
|
133
|
+
effectivePatchDecision: AkanRscPatchDecision;
|
|
134
|
+
patchCacheEntry: RouteCacheEntry | null;
|
|
135
|
+
}): boolean {
|
|
136
|
+
return (
|
|
137
|
+
input.cacheEntry !== null && (input.effectivePatchDecision.status !== "patch" || input.patchCacheEntry !== null)
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function shouldStoreRscWorkerPatchResult(input: {
|
|
142
|
+
cacheEntry: RouteCacheEntry | null;
|
|
143
|
+
patchCacheEntry: RouteCacheEntry | null;
|
|
144
|
+
effectivePatchDecision: AkanRscPatchDecision;
|
|
145
|
+
storeTtl: number | null;
|
|
146
|
+
}): boolean {
|
|
147
|
+
return (
|
|
148
|
+
input.cacheEntry !== null &&
|
|
149
|
+
input.patchCacheEntry !== null &&
|
|
150
|
+
input.storeTtl !== null &&
|
|
151
|
+
input.effectivePatchDecision.status === "patch"
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function shouldUseRscWorkerFullResultCache(input: {
|
|
156
|
+
cacheEntry: RouteCacheEntry | null;
|
|
157
|
+
patchCacheEntry: RouteCacheEntry | null;
|
|
158
|
+
}): boolean {
|
|
159
|
+
return input.cacheEntry !== null && input.patchCacheEntry === null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function invalidateCachedRscResults(
|
|
163
|
+
cache: LruTtlCache<CachedRscResult>,
|
|
164
|
+
invalidation: RouteCacheInvalidation,
|
|
165
|
+
): void {
|
|
166
|
+
if (!hasRouteCacheInvalidationScope(invalidation)) {
|
|
167
|
+
cache.clear();
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
cache.invalidate((_key, result) =>
|
|
171
|
+
shouldInvalidateRouteCacheEntry(
|
|
172
|
+
{
|
|
173
|
+
pathname: result.pathname,
|
|
174
|
+
routeId: result.routeId,
|
|
175
|
+
tags: result.tags,
|
|
176
|
+
},
|
|
177
|
+
invalidation,
|
|
178
|
+
),
|
|
179
|
+
);
|
|
180
|
+
}
|
package/server/rscWorkerHost.ts
CHANGED
|
@@ -5,8 +5,8 @@ import { type AkanI18nConfig, DEFAULT_AKAN_I18N, Logger } from "akanjs/common";
|
|
|
5
5
|
import type { AkanTheme } from "akanjs/fetch";
|
|
6
6
|
import type { AkanMetricsReport } from "akanjs/service";
|
|
7
7
|
import type { ClientManifest } from "./artifact";
|
|
8
|
-
import type { RouteCacheRenderState } from "./cachePolicy";
|
|
9
|
-
import type { SsrLateRedirect } from "./ssrTypes";
|
|
8
|
+
import type { RouteCacheInvalidation, RouteCacheRenderState } from "./cachePolicy";
|
|
9
|
+
import type { RscTraceMetadata, SsrLateRedirect } from "./ssrTypes";
|
|
10
10
|
import type { BaseBuildArtifact, CssAsset } from "./types";
|
|
11
11
|
|
|
12
12
|
const DEFAULT_RSC_HOST_MAX_PENDING_CHUNKS = 256;
|
|
@@ -15,7 +15,7 @@ export interface RscPending {
|
|
|
15
15
|
onChunk: (data: Uint8Array) => void;
|
|
16
16
|
onEnd: () => void;
|
|
17
17
|
onError: (message: string) => void;
|
|
18
|
-
onMeta?: (meta: { theme?: AkanTheme; status?: number }) => void;
|
|
18
|
+
onMeta?: (meta: { theme?: AkanTheme; status?: number; trace?: RscTraceMetadata }) => void;
|
|
19
19
|
onCacheState?: (state: RouteCacheRenderState) => void;
|
|
20
20
|
onRedirect?: (location: string, method: RscRedirectMethod, status: RscRedirectStatus) => void;
|
|
21
21
|
onLateRedirect?: (location: string, method: RscRedirectMethod, status: RscRedirectStatus) => void;
|
|
@@ -28,6 +28,8 @@ export type RscRedirectStatus = 303 | 307 | 308;
|
|
|
28
28
|
export interface RscWorkerInvalidateCacheMessage {
|
|
29
29
|
type: "invalidate-cache";
|
|
30
30
|
reason?: string;
|
|
31
|
+
tags?: string[];
|
|
32
|
+
paths?: string[];
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
export type RscRenderResult =
|
|
@@ -36,6 +38,7 @@ export type RscRenderResult =
|
|
|
36
38
|
stream: ReadableStream<Uint8Array>;
|
|
37
39
|
theme?: AkanTheme;
|
|
38
40
|
status?: number;
|
|
41
|
+
trace?: RscTraceMetadata;
|
|
39
42
|
lateControl: Promise<SsrLateRedirect | null>;
|
|
40
43
|
cacheState: Promise<RouteCacheRenderState>;
|
|
41
44
|
cancel: (reason?: unknown) => void;
|
|
@@ -72,8 +75,17 @@ export function createIdempotentRscRenderCancel(onCancel: (reason?: unknown) =>
|
|
|
72
75
|
};
|
|
73
76
|
}
|
|
74
77
|
|
|
75
|
-
export function createRscWorkerInvalidateCacheMessage(
|
|
76
|
-
|
|
78
|
+
export function createRscWorkerInvalidateCacheMessage(
|
|
79
|
+
invalidation?: string | RouteCacheInvalidation,
|
|
80
|
+
): RscWorkerInvalidateCacheMessage {
|
|
81
|
+
if (!invalidation) return { type: "invalidate-cache" };
|
|
82
|
+
if (typeof invalidation === "string") return { type: "invalidate-cache", reason: invalidation };
|
|
83
|
+
return {
|
|
84
|
+
type: "invalidate-cache",
|
|
85
|
+
reason: invalidation.reason,
|
|
86
|
+
tags: invalidation.tags,
|
|
87
|
+
paths: invalidation.paths,
|
|
88
|
+
};
|
|
77
89
|
}
|
|
78
90
|
|
|
79
91
|
export function createRscHostRenderStream(input: {
|
|
@@ -89,6 +101,7 @@ export function createRscHostRenderStream(input: {
|
|
|
89
101
|
let stream!: ReadableStream<Uint8Array>;
|
|
90
102
|
let theme: AkanTheme | undefined;
|
|
91
103
|
let status: number | undefined;
|
|
104
|
+
let trace: RscTraceMetadata | undefined;
|
|
92
105
|
let resolveLateControl!: (control: SsrLateRedirect | null) => void;
|
|
93
106
|
let resolveCacheState!: (state: RouteCacheRenderState) => void;
|
|
94
107
|
const lateControl = new Promise<SsrLateRedirect | null>((resolve) => {
|
|
@@ -140,12 +153,13 @@ export function createRscHostRenderStream(input: {
|
|
|
140
153
|
const settleStream = () => {
|
|
141
154
|
if (settled) return;
|
|
142
155
|
settled = true;
|
|
143
|
-
resolve({ type: "stream", stream, theme, status, lateControl, cacheState, cancel: cancelRender });
|
|
156
|
+
resolve({ type: "stream", stream, theme, status, trace, lateControl, cacheState, cancel: cancelRender });
|
|
144
157
|
};
|
|
145
158
|
input.setPending({
|
|
146
159
|
onMeta: (meta) => {
|
|
147
160
|
theme = meta.theme;
|
|
148
161
|
status = meta.status;
|
|
162
|
+
trace = meta.trace;
|
|
149
163
|
settleStream();
|
|
150
164
|
},
|
|
151
165
|
onChunk: (data) => {
|
|
@@ -234,7 +248,7 @@ type RscInMsg =
|
|
|
234
248
|
| { type: "hello" }
|
|
235
249
|
| { type: "ready" }
|
|
236
250
|
| { type: "reloaded"; buildId: number }
|
|
237
|
-
| { type: "meta"; requestId: string; theme?: AkanTheme; status?: number }
|
|
251
|
+
| { type: "meta"; requestId: string; theme?: AkanTheme; status?: number; trace?: RscTraceMetadata }
|
|
238
252
|
| { type: "cache-state"; requestId: string; state: RouteCacheRenderState }
|
|
239
253
|
| { type: "chunk"; requestId: string; data: Uint8Array }
|
|
240
254
|
| { type: "end"; requestId: string }
|
|
@@ -329,7 +343,7 @@ export class RscWorker {
|
|
|
329
343
|
};
|
|
330
344
|
|
|
331
345
|
constructor(artifact: BaseBuildArtifact) {
|
|
332
|
-
this.#clientManifest = {};
|
|
346
|
+
this.#clientManifest = artifact.rscRuntimeClientManifest ?? {};
|
|
333
347
|
this.#pagesBundlePath = artifact.pagesBundlePath;
|
|
334
348
|
this.#pagesBundleBuildId = artifact.pagesBundleBuildId;
|
|
335
349
|
this.#cssAssets = artifact.cssAssets ?? {};
|
|
@@ -417,10 +431,10 @@ export class RscWorker {
|
|
|
417
431
|
});
|
|
418
432
|
}
|
|
419
433
|
|
|
420
|
-
invalidateRouteResultCache(
|
|
434
|
+
invalidateRouteResultCache(invalidation?: string | RouteCacheInvalidation): void {
|
|
421
435
|
if (this.#status !== "ready") return;
|
|
422
436
|
try {
|
|
423
|
-
this.#proc.send(createRscWorkerInvalidateCacheMessage(
|
|
437
|
+
this.#proc.send(createRscWorkerInvalidateCacheMessage(invalidation));
|
|
424
438
|
} catch (error) {
|
|
425
439
|
this.#logger.warn(
|
|
426
440
|
`rsc worker cache invalidate send failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
@@ -544,12 +558,24 @@ export class RscWorker {
|
|
|
544
558
|
this.#status = "starting";
|
|
545
559
|
const workerPath = this.#resolveWorkerPath();
|
|
546
560
|
let proc!: Bun.Subprocess<"ignore", "inherit", "inherit">;
|
|
561
|
+
const earlyMessages: RscInMsg[] = [];
|
|
547
562
|
proc = Bun.spawn(["bun", "--conditions", "react-server", workerPath], {
|
|
548
|
-
ipc: (message: RscInMsg) =>
|
|
563
|
+
ipc: (message: RscInMsg) => {
|
|
564
|
+
if (!proc) {
|
|
565
|
+
earlyMessages.push(message);
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
this.#handleMessage(message, proc);
|
|
569
|
+
},
|
|
549
570
|
stdio: ["ignore", "inherit", "inherit"],
|
|
550
571
|
serialization: "advanced",
|
|
551
572
|
env: { ...process.env },
|
|
552
573
|
});
|
|
574
|
+
if (earlyMessages.length > 0) {
|
|
575
|
+
setTimeout(() => {
|
|
576
|
+
for (const message of earlyMessages.splice(0)) this.#handleMessage(message, proc);
|
|
577
|
+
}, 0);
|
|
578
|
+
}
|
|
553
579
|
proc.exited.then((code) => this.#handleExit(proc, code));
|
|
554
580
|
return proc;
|
|
555
581
|
}
|
|
@@ -603,7 +629,9 @@ export class RscWorker {
|
|
|
603
629
|
this.#pending.get(message.requestId)?.onChunk(message.data);
|
|
604
630
|
return;
|
|
605
631
|
case "meta":
|
|
606
|
-
this.#pending
|
|
632
|
+
this.#pending
|
|
633
|
+
.get(message.requestId)
|
|
634
|
+
?.onMeta?.({ theme: message.theme, status: message.status, trace: message.trace });
|
|
607
635
|
return;
|
|
608
636
|
case "end":
|
|
609
637
|
this.#resolvePending(message.requestId, (p) => p.onEnd());
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { RouteCacheRenderState } from "./cachePolicy";
|
|
2
|
+
import type { RscTraceMetadata } from "./ssrTypes";
|
|
2
3
|
|
|
3
4
|
export type CachedRscReplayMessage =
|
|
4
|
-
| { type: "meta"; requestId: string; theme?: string; status?: number }
|
|
5
|
+
| { type: "meta"; requestId: string; theme?: string; status?: number; trace?: RscTraceMetadata }
|
|
5
6
|
| { type: "cache-state"; requestId: string; state: RouteCacheRenderState }
|
|
6
7
|
| { type: "chunk"; requestId: string; data: Uint8Array }
|
|
7
8
|
| { type: "end"; requestId: string };
|
|
@@ -15,6 +16,7 @@ export async function replayCachedRscResult(input: {
|
|
|
15
16
|
chunks: readonly Uint8Array[];
|
|
16
17
|
theme?: string;
|
|
17
18
|
status?: number;
|
|
19
|
+
trace?: RscTraceMetadata;
|
|
18
20
|
cacheState?: RouteCacheRenderState;
|
|
19
21
|
send: (message: CachedRscReplayMessage) => void;
|
|
20
22
|
isCancelled: () => boolean;
|
|
@@ -27,7 +29,14 @@ export async function replayCachedRscResult(input: {
|
|
|
27
29
|
: 1;
|
|
28
30
|
const yieldToHost = input.yieldToHost ?? yieldToHostEventLoop;
|
|
29
31
|
if (input.isCancelled()) return false;
|
|
30
|
-
|
|
32
|
+
const metaMessage: CachedRscReplayMessage = {
|
|
33
|
+
type: "meta",
|
|
34
|
+
requestId: input.requestId,
|
|
35
|
+
theme: input.theme,
|
|
36
|
+
status: input.status,
|
|
37
|
+
};
|
|
38
|
+
if (input.trace) metaMessage.trace = input.trace;
|
|
39
|
+
input.send(metaMessage);
|
|
31
40
|
input.send({ type: "cache-state", requestId: input.requestId, state: input.cacheState ?? { cacheable: true } });
|
|
32
41
|
for (let index = 0; index < input.chunks.length; index += 1) {
|
|
33
42
|
if (input.isCancelled()) return false;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Readable } from "node:stream";
|
|
2
2
|
import { type AkanRequestStore, type AkanTheme, pushRequestFallback, requestStorage } from "akanjs/fetch";
|
|
3
|
-
import {
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
4
|
import { renderToReadableStream } from "react-dom/server.browser";
|
|
5
5
|
import { createFromNodeStream } from "react-server-dom-webpack/client.node";
|
|
6
6
|
import type { SsrChunkRegistryStats, SsrFromRscInput, SsrLateRedirect } from "./ssrTypes";
|
|
@@ -121,12 +121,13 @@ export function createSoftRedirectScript(redirect: SsrLateRedirect): string {
|
|
|
121
121
|
|
|
122
122
|
function sanitizeFlightRows(
|
|
123
123
|
stream: ReadableStream<Uint8Array>,
|
|
124
|
-
options: { rewriteStylesheetHints?: boolean } = {},
|
|
124
|
+
options: { rewriteStylesheetHints?: boolean; dropDebugInfoRows?: boolean } = {},
|
|
125
125
|
): ReadableStream<Uint8Array> {
|
|
126
126
|
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
127
127
|
const encoder = new TextEncoder();
|
|
128
128
|
const hlStylesheetRe = /(:HL\["[^"\\]*(?:\\.[^"\\]*)*",)"stylesheet"(\])/g;
|
|
129
129
|
const redirectErrorRowRe = /^([0-9a-z]+):E(\{[^\n]*"digest":"AKAN_REDIRECT(?:;[^"]*)?"[^\n]*\})(\n?)$/;
|
|
130
|
+
const debugInfoRowRe = /^[0-9a-z]+:D/;
|
|
130
131
|
let buffered: Uint8Array<ArrayBuffer> = new Uint8Array(0);
|
|
131
132
|
|
|
132
133
|
const concatBytes = (left: Uint8Array, right: Uint8Array): Uint8Array<ArrayBuffer> => {
|
|
@@ -143,6 +144,7 @@ function sanitizeFlightRows(
|
|
|
143
144
|
} catch {
|
|
144
145
|
return row;
|
|
145
146
|
}
|
|
147
|
+
if (options.dropDebugInfoRows && debugInfoRowRe.test(text)) return new Uint8Array(0);
|
|
146
148
|
const sanitized = (options.rewriteStylesheetHints ? text.replace(hlStylesheetRe, `$1"style"$2`) : text).replace(
|
|
147
149
|
redirectErrorRowRe,
|
|
148
150
|
"$1:null$3",
|
|
@@ -180,6 +182,10 @@ export function sanitizeFlightForClientStream(stream: ReadableStream<Uint8Array>
|
|
|
180
182
|
return sanitizeFlightRows(stream, { rewriteStylesheetHints: true });
|
|
181
183
|
}
|
|
182
184
|
|
|
185
|
+
export function sanitizeFlightForSsrStream(stream: ReadableStream<Uint8Array>): ReadableStream<Uint8Array> {
|
|
186
|
+
return sanitizeFlightRows(stream, { dropDebugInfoRows: true });
|
|
187
|
+
}
|
|
188
|
+
|
|
183
189
|
type StderrWrite = typeof process.stderr.write;
|
|
184
190
|
|
|
185
191
|
export class ExpectedLateRedirectStderrSuppressor {
|
|
@@ -445,7 +451,6 @@ export function interleaveRscScriptsWithHtml(
|
|
|
445
451
|
if (done || errored) break;
|
|
446
452
|
controller.enqueue(value);
|
|
447
453
|
bootstrapDetector.observe(value);
|
|
448
|
-
flushPendingRscScripts();
|
|
449
454
|
}
|
|
450
455
|
} finally {
|
|
451
456
|
htmlReader.releaseLock();
|
|
@@ -578,25 +583,25 @@ export class SsrFromRscRenderer {
|
|
|
578
583
|
|
|
579
584
|
const [rscForSsr, rscForClient] = input.rscStream.tee();
|
|
580
585
|
|
|
581
|
-
const ssrNodeStream = Readable.fromWeb(
|
|
586
|
+
const ssrNodeStream = Readable.fromWeb(sanitizeFlightForSsrStream(rscForSsr) as never);
|
|
582
587
|
const stderrSuppressor = ExpectedLateRedirectStderrSuppressor.start(input.lateControl);
|
|
583
588
|
const thenable = SsrFromRscRenderer.#suppressExpectedLateRedirectError(
|
|
584
589
|
createFromNodeStream(ssrNodeStream, input.ssrManifest),
|
|
585
590
|
input.lateControl,
|
|
586
591
|
);
|
|
587
592
|
|
|
588
|
-
function Root(): ReactNode {
|
|
589
|
-
return use(thenable);
|
|
590
|
-
}
|
|
591
|
-
|
|
592
593
|
const bootstrap = input.extraBootstrapInline
|
|
593
594
|
? `${SsrFromRscRenderer.#clientBootstrap}\n${input.extraBootstrapInline}`
|
|
594
595
|
: SsrFromRscRenderer.#clientBootstrap;
|
|
595
596
|
|
|
596
|
-
const renderHtml = () =>
|
|
597
|
-
|
|
597
|
+
const renderHtml = async () => {
|
|
598
|
+
const root = await thenable;
|
|
599
|
+
const stream = await renderToReadableStream(root, {
|
|
598
600
|
bootstrapScriptContent: bootstrap,
|
|
599
601
|
});
|
|
602
|
+
await stream.allReady;
|
|
603
|
+
return stream;
|
|
604
|
+
};
|
|
600
605
|
const requestContext = input.requestStore ?? input.request;
|
|
601
606
|
const htmlStream =
|
|
602
607
|
requestContext && requestStorage ? await requestStorage.run(requestContext, renderHtml) : await renderHtml();
|
package/server/ssrTypes.ts
CHANGED
|
@@ -26,6 +26,24 @@ export interface SsrLateRedirect {
|
|
|
26
26
|
status: 303 | 307 | 308;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
export interface RscTraceMetadata {
|
|
30
|
+
navId?: string;
|
|
31
|
+
pathname: string;
|
|
32
|
+
routeId: string;
|
|
33
|
+
cache: "hit" | "miss" | "bypass";
|
|
34
|
+
cacheReason?: string;
|
|
35
|
+
cacheKeyHash?: string;
|
|
36
|
+
partial?: "full" | "candidate" | "patch" | "fallback";
|
|
37
|
+
partialReason?: string;
|
|
38
|
+
partialCommonPrefixLength?: number;
|
|
39
|
+
patchStartIndex?: number;
|
|
40
|
+
patchSegmentPath?: string;
|
|
41
|
+
patchStartSegment?: string;
|
|
42
|
+
patchHeadSafe?: boolean;
|
|
43
|
+
patchHeadSnapshot?: string;
|
|
44
|
+
routeState?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
29
47
|
export interface SsrFromRscInput {
|
|
30
48
|
request?: Request;
|
|
31
49
|
requestStore?: AkanRequestStore;
|
package/server/types.tsx
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { PromiseOrObject } from "akanjs/base";
|
|
2
2
|
import type { AkanI18nConfig } from "akanjs/common";
|
|
3
|
+
import type { ClientManifest } from "./artifact";
|
|
4
|
+
import type { SsrManifest } from "./ssrTypes";
|
|
3
5
|
|
|
4
6
|
export type WebsocketRoute = (
|
|
5
7
|
ws: Bun.ServerWebSocket<unknown>,
|
|
@@ -85,6 +87,8 @@ export function getAkanImageWidths(config: Pick<AkanImageConfig, "deviceSizes" |
|
|
|
85
87
|
|
|
86
88
|
export type BaseBuildArtifact = {
|
|
87
89
|
rscClientUrl: string;
|
|
90
|
+
rscRuntimeClientManifest?: ClientManifest;
|
|
91
|
+
rscRuntimeSsrManifest?: SsrManifest;
|
|
88
92
|
vendorMap: Record<string, string>;
|
|
89
93
|
pagesBundlePath: string;
|
|
90
94
|
pagesBundleBuildId: number;
|