akanjs 2.2.12 → 2.2.13-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/client/csrTypes.ts +37 -6
- package/client/makePageProto.tsx +8 -8
- package/client/router.ts +5 -2
- package/fetch/requestStorage.ts +41 -11
- package/package.json +1 -1
- package/server/akanApp.ts +55 -0
- package/server/cachePolicy.ts +192 -0
- package/server/metadata.tsx +114 -0
- package/server/routeElementComposer.tsx +21 -1
- package/server/routeTreeBuilder.ts +44 -5
- package/server/rscClient.tsx +127 -50
- package/server/rscHttp.ts +120 -0
- package/server/rscNavigationState.ts +95 -0
- package/server/rscWorker.tsx +318 -121
- package/server/rscWorkerHost.ts +281 -66
- package/server/rscWorkerReplay.ts +40 -0
- package/server/ssrFromRscRenderer.tsx +462 -77
- package/server/ssrTypes.ts +11 -1
- package/server/webRouter.ts +173 -88
- package/service/ipcTypes.ts +1 -0
- package/types/client/csrTypes.d.ts +37 -6
- package/types/dictionary/base.dictionary.d.ts +1 -1
- package/types/dictionary/dictionary.d.ts +8 -8
- package/types/fetch/requestStorage.d.ts +16 -6
- package/types/server/cachePolicy.d.ts +55 -0
- package/types/server/metadata.d.ts +13 -0
- package/types/server/routeElementComposer.d.ts +6 -1
- package/types/server/rscHttp.d.ts +16 -0
- package/types/server/rscNavigationState.d.ts +35 -0
- package/types/server/rscWorkerHost.d.ts +38 -0
- package/types/server/rscWorkerReplay.d.ts +29 -0
- package/types/server/ssrFromRscRenderer.d.ts +20 -1
- package/types/server/ssrTypes.d.ts +10 -1
- package/types/server/webRouter.d.ts +27 -1
- package/types/service/ipcTypes.d.ts +1 -0
- package/types/webkit/useCsrValues.d.ts +1 -1
- package/ui/Link/SsrLink.tsx +0 -2
- package/webkit/bootCsr.tsx +16 -2
package/server/rscWorkerHost.ts
CHANGED
|
@@ -5,33 +5,247 @@ 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
10
|
import type { BaseBuildArtifact, CssAsset } from "./types";
|
|
9
11
|
|
|
10
|
-
|
|
12
|
+
const DEFAULT_RSC_HOST_MAX_PENDING_CHUNKS = 256;
|
|
13
|
+
|
|
14
|
+
export interface RscPending {
|
|
11
15
|
onChunk: (data: Uint8Array) => void;
|
|
12
16
|
onEnd: () => void;
|
|
13
17
|
onError: (message: string) => void;
|
|
14
18
|
onMeta?: (meta: { theme?: AkanTheme; status?: number }) => void;
|
|
19
|
+
onCacheState?: (state: RouteCacheRenderState) => void;
|
|
15
20
|
onRedirect?: (location: string, method: RscRedirectMethod, status: RscRedirectStatus) => void;
|
|
21
|
+
onLateRedirect?: (location: string, method: RscRedirectMethod, status: RscRedirectStatus) => void;
|
|
16
22
|
onNotFound?: () => void;
|
|
17
23
|
}
|
|
18
24
|
|
|
19
25
|
export type RscRedirectMethod = "replace" | "push";
|
|
20
26
|
export type RscRedirectStatus = 303 | 307 | 308;
|
|
21
27
|
|
|
28
|
+
export interface RscWorkerInvalidateCacheMessage {
|
|
29
|
+
type: "invalidate-cache";
|
|
30
|
+
reason?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
22
33
|
export type RscRenderResult =
|
|
23
|
-
| {
|
|
34
|
+
| {
|
|
35
|
+
type: "stream";
|
|
36
|
+
stream: ReadableStream<Uint8Array>;
|
|
37
|
+
theme?: AkanTheme;
|
|
38
|
+
status?: number;
|
|
39
|
+
lateControl: Promise<SsrLateRedirect | null>;
|
|
40
|
+
cacheState: Promise<RouteCacheRenderState>;
|
|
41
|
+
cancel: (reason?: unknown) => void;
|
|
42
|
+
}
|
|
24
43
|
| { type: "redirect"; location: string; method: RscRedirectMethod; status: RscRedirectStatus }
|
|
25
44
|
| { type: "not-found" };
|
|
26
45
|
|
|
46
|
+
export function getRscHostMaxPendingChunks(value = process.env.AKAN_RSC_HOST_MAX_PENDING_CHUNKS): number {
|
|
47
|
+
const parsed = Number.parseInt(value ?? "", 10);
|
|
48
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_RSC_HOST_MAX_PENDING_CHUNKS;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function nextRscHostPendingChunkCount(currentPendingChunks: number, desiredSize: number | null): number {
|
|
52
|
+
return desiredSize !== null && desiredSize <= 0 ? currentPendingChunks + 1 : 0;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function isRscHostPendingChunkOverflow(pendingChunks: number, maxPendingChunks: number): boolean {
|
|
56
|
+
return pendingChunks > maxPendingChunks;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function createRscRenderAbortError(reason?: unknown): Error {
|
|
60
|
+
if (reason instanceof Error) return reason;
|
|
61
|
+
const error = new Error(reason === undefined ? "rsc render aborted" : String(reason));
|
|
62
|
+
error.name = "AbortError";
|
|
63
|
+
return error;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function createIdempotentRscRenderCancel(onCancel: (reason?: unknown) => void): (reason?: unknown) => void {
|
|
67
|
+
let cancelled = false;
|
|
68
|
+
return (reason?: unknown) => {
|
|
69
|
+
if (cancelled) return;
|
|
70
|
+
cancelled = true;
|
|
71
|
+
onCancel(reason);
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function createRscWorkerInvalidateCacheMessage(reason?: string): RscWorkerInvalidateCacheMessage {
|
|
76
|
+
return reason ? { type: "invalidate-cache", reason } : { type: "invalidate-cache" };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function createRscHostRenderStream(input: {
|
|
80
|
+
setPending: (pending: RscPending) => void;
|
|
81
|
+
deletePending: () => void;
|
|
82
|
+
sendRenderOrQueue: () => void;
|
|
83
|
+
cancelRender: (reason?: unknown) => void;
|
|
84
|
+
maxPendingChunks?: number;
|
|
85
|
+
signal?: AbortSignal;
|
|
86
|
+
onPendingChunkOverflow?: () => void;
|
|
87
|
+
}): Promise<RscRenderResult> {
|
|
88
|
+
let settled = false;
|
|
89
|
+
let stream!: ReadableStream<Uint8Array>;
|
|
90
|
+
let theme: AkanTheme | undefined;
|
|
91
|
+
let status: number | undefined;
|
|
92
|
+
let resolveLateControl!: (control: SsrLateRedirect | null) => void;
|
|
93
|
+
let resolveCacheState!: (state: RouteCacheRenderState) => void;
|
|
94
|
+
const lateControl = new Promise<SsrLateRedirect | null>((resolve) => {
|
|
95
|
+
resolveLateControl = resolve;
|
|
96
|
+
});
|
|
97
|
+
const cacheState = new Promise<RouteCacheRenderState>((resolve) => {
|
|
98
|
+
resolveCacheState = resolve;
|
|
99
|
+
});
|
|
100
|
+
let lateControlSettled = false;
|
|
101
|
+
let cacheStateSettled = false;
|
|
102
|
+
const settleLateControl = (control: SsrLateRedirect | null) => {
|
|
103
|
+
if (lateControlSettled) return;
|
|
104
|
+
lateControlSettled = true;
|
|
105
|
+
resolveLateControl(control);
|
|
106
|
+
};
|
|
107
|
+
const settleCacheState = (state: RouteCacheRenderState) => {
|
|
108
|
+
if (cacheStateSettled) return;
|
|
109
|
+
cacheStateSettled = true;
|
|
110
|
+
resolveCacheState(state);
|
|
111
|
+
};
|
|
112
|
+
const maxPendingChunks = input.maxPendingChunks ?? getRscHostMaxPendingChunks();
|
|
113
|
+
let pendingChunks = 0;
|
|
114
|
+
let removeAbortListener: (() => void) | undefined;
|
|
115
|
+
const cleanupAbortListener = () => {
|
|
116
|
+
removeAbortListener?.();
|
|
117
|
+
removeAbortListener = undefined;
|
|
118
|
+
};
|
|
119
|
+
const cancelRender = createIdempotentRscRenderCancel((reason) => {
|
|
120
|
+
input.deletePending();
|
|
121
|
+
settleLateControl(null);
|
|
122
|
+
settleCacheState({ cacheable: false, reason: "cancelled" });
|
|
123
|
+
input.cancelRender(reason);
|
|
124
|
+
cleanupAbortListener();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
return new Promise<RscRenderResult>((resolve, reject) => {
|
|
128
|
+
stream = new ReadableStream<Uint8Array>({
|
|
129
|
+
start: (controller) => {
|
|
130
|
+
const abortRender = () => {
|
|
131
|
+
const error = createRscRenderAbortError(input.signal?.reason);
|
|
132
|
+
cancelRender(error);
|
|
133
|
+
if (!settled) {
|
|
134
|
+
settled = true;
|
|
135
|
+
reject(error);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
controller.error(error);
|
|
139
|
+
};
|
|
140
|
+
const settleStream = () => {
|
|
141
|
+
if (settled) return;
|
|
142
|
+
settled = true;
|
|
143
|
+
resolve({ type: "stream", stream, theme, status, lateControl, cacheState, cancel: cancelRender });
|
|
144
|
+
};
|
|
145
|
+
input.setPending({
|
|
146
|
+
onMeta: (meta) => {
|
|
147
|
+
theme = meta.theme;
|
|
148
|
+
status = meta.status;
|
|
149
|
+
settleStream();
|
|
150
|
+
},
|
|
151
|
+
onChunk: (data) => {
|
|
152
|
+
settleStream();
|
|
153
|
+
pendingChunks = nextRscHostPendingChunkCount(pendingChunks, controller.desiredSize);
|
|
154
|
+
if (isRscHostPendingChunkOverflow(pendingChunks, maxPendingChunks)) {
|
|
155
|
+
const msg = `rsc worker host queue exceeded ${maxPendingChunks} pending chunks`;
|
|
156
|
+
const error = new Error(msg);
|
|
157
|
+
input.onPendingChunkOverflow?.();
|
|
158
|
+
cancelRender(error);
|
|
159
|
+
controller.error(error);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
controller.enqueue(data);
|
|
163
|
+
},
|
|
164
|
+
onEnd: () => {
|
|
165
|
+
settleLateControl(null);
|
|
166
|
+
settleCacheState({ cacheable: false, reason: "missing-cache-state" });
|
|
167
|
+
cleanupAbortListener();
|
|
168
|
+
settleStream();
|
|
169
|
+
controller.close();
|
|
170
|
+
},
|
|
171
|
+
onError: (msg) => {
|
|
172
|
+
settleLateControl(null);
|
|
173
|
+
settleCacheState({ cacheable: false, reason: "error" });
|
|
174
|
+
cleanupAbortListener();
|
|
175
|
+
if (!settled) {
|
|
176
|
+
settled = true;
|
|
177
|
+
reject(new Error(msg));
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
controller.error(new Error(msg));
|
|
181
|
+
},
|
|
182
|
+
onRedirect: (location, method, status) => {
|
|
183
|
+
settleLateControl(null);
|
|
184
|
+
settleCacheState({ cacheable: false, reason: "redirect" });
|
|
185
|
+
cleanupAbortListener();
|
|
186
|
+
if (!settled) {
|
|
187
|
+
settled = true;
|
|
188
|
+
resolve({ type: "redirect", location, method, status });
|
|
189
|
+
controller.close();
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
controller.error(new Error(`redirect after stream started: ${location}`));
|
|
193
|
+
},
|
|
194
|
+
onLateRedirect: (location, method, status) => {
|
|
195
|
+
settleLateControl({ type: "redirect", location, method, status });
|
|
196
|
+
},
|
|
197
|
+
onCacheState: (state) => {
|
|
198
|
+
settleCacheState(state);
|
|
199
|
+
},
|
|
200
|
+
onNotFound: () => {
|
|
201
|
+
settleLateControl(null);
|
|
202
|
+
settleCacheState({ cacheable: false, reason: "not-found" });
|
|
203
|
+
cleanupAbortListener();
|
|
204
|
+
if (!settled) {
|
|
205
|
+
settled = true;
|
|
206
|
+
resolve({ type: "not-found" });
|
|
207
|
+
controller.close();
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
controller.error(new Error("not-found after stream started"));
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
if (input.signal) {
|
|
214
|
+
if (input.signal.aborted) {
|
|
215
|
+
abortRender();
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
input.signal.addEventListener("abort", abortRender, { once: true });
|
|
219
|
+
removeAbortListener = () => input.signal?.removeEventListener("abort", abortRender);
|
|
220
|
+
}
|
|
221
|
+
input.sendRenderOrQueue();
|
|
222
|
+
},
|
|
223
|
+
cancel: (reason) => {
|
|
224
|
+
cancelRender(reason);
|
|
225
|
+
},
|
|
226
|
+
pull: () => {
|
|
227
|
+
pendingChunks = Math.max(0, pendingChunks - 1);
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
27
233
|
type RscInMsg =
|
|
28
234
|
| { type: "hello" }
|
|
29
235
|
| { type: "ready" }
|
|
30
236
|
| { type: "reloaded"; buildId: number }
|
|
31
237
|
| { type: "meta"; requestId: string; theme?: AkanTheme; status?: number }
|
|
238
|
+
| { type: "cache-state"; requestId: string; state: RouteCacheRenderState }
|
|
32
239
|
| { type: "chunk"; requestId: string; data: Uint8Array }
|
|
33
240
|
| { type: "end"; requestId: string }
|
|
34
241
|
| { type: "redirect"; requestId: string; location: string; method?: RscRedirectMethod; status?: RscRedirectStatus }
|
|
242
|
+
| {
|
|
243
|
+
type: "late-redirect";
|
|
244
|
+
requestId: string;
|
|
245
|
+
location: string;
|
|
246
|
+
method?: RscRedirectMethod;
|
|
247
|
+
status?: RscRedirectStatus;
|
|
248
|
+
}
|
|
35
249
|
| { type: "not-found"; requestId: string }
|
|
36
250
|
| { type: "metrics"; metrics: AkanMetricsReport }
|
|
37
251
|
| { type: "error"; requestId: string; message: string; buildId?: number };
|
|
@@ -106,6 +320,7 @@ export class RscWorker {
|
|
|
106
320
|
#recycleCount = 0;
|
|
107
321
|
#lastRecycleReason: string | undefined;
|
|
108
322
|
#lastWorkerMetrics: AkanMetricsReport = {};
|
|
323
|
+
#hostPendingChunkOverflowCount = 0;
|
|
109
324
|
#restartTimer: ReturnType<typeof setTimeout> | null = null;
|
|
110
325
|
#recycleTimer: ReturnType<typeof setTimeout> | null = null;
|
|
111
326
|
#rollingRecycle: { oldProc: Bun.Subprocess<"ignore", "inherit", "inherit">; reason: string } | null = null;
|
|
@@ -172,73 +387,45 @@ export class RscWorker {
|
|
|
172
387
|
},
|
|
173
388
|
cancel: () => {
|
|
174
389
|
this.#pending.delete(requestId);
|
|
390
|
+
this.#cancelRender(requestId);
|
|
175
391
|
},
|
|
176
392
|
});
|
|
177
393
|
}
|
|
178
394
|
|
|
179
|
-
renderWithMeta(
|
|
395
|
+
renderWithMeta(
|
|
396
|
+
req: Request,
|
|
397
|
+
options: { clientManifest?: ClientManifest; signal?: AbortSignal } = {},
|
|
398
|
+
): Promise<RscRenderResult> {
|
|
180
399
|
const requestId = crypto.randomUUID();
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
},
|
|
199
|
-
onChunk: (data) => {
|
|
200
|
-
settleStream();
|
|
201
|
-
controller.enqueue(data);
|
|
202
|
-
},
|
|
203
|
-
onEnd: () => {
|
|
204
|
-
settleStream();
|
|
205
|
-
controller.close();
|
|
206
|
-
},
|
|
207
|
-
onError: (msg) => {
|
|
208
|
-
if (!settled) {
|
|
209
|
-
settled = true;
|
|
210
|
-
reject(new Error(msg));
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
213
|
-
controller.error(new Error(msg));
|
|
214
|
-
},
|
|
215
|
-
onRedirect: (location, method, status) => {
|
|
216
|
-
if (!settled) {
|
|
217
|
-
settled = true;
|
|
218
|
-
resolve({ type: "redirect", location, method, status });
|
|
219
|
-
controller.close();
|
|
220
|
-
return;
|
|
221
|
-
}
|
|
222
|
-
controller.error(new Error(`redirect after stream started: ${location}`));
|
|
223
|
-
},
|
|
224
|
-
onNotFound: () => {
|
|
225
|
-
if (!settled) {
|
|
226
|
-
settled = true;
|
|
227
|
-
resolve({ type: "not-found" });
|
|
228
|
-
controller.close();
|
|
229
|
-
return;
|
|
230
|
-
}
|
|
231
|
-
controller.error(new Error("not-found after stream started"));
|
|
232
|
-
},
|
|
233
|
-
});
|
|
234
|
-
this.#sendRenderOrQueue(requestId, req, options.clientManifest);
|
|
235
|
-
},
|
|
236
|
-
cancel: () => {
|
|
237
|
-
this.#pending.delete(requestId);
|
|
238
|
-
},
|
|
239
|
-
});
|
|
400
|
+
return createRscHostRenderStream({
|
|
401
|
+
setPending: (pending) => {
|
|
402
|
+
this.#pending.set(requestId, pending);
|
|
403
|
+
},
|
|
404
|
+
deletePending: () => {
|
|
405
|
+
this.#pending.delete(requestId);
|
|
406
|
+
},
|
|
407
|
+
sendRenderOrQueue: () => {
|
|
408
|
+
this.#sendRenderOrQueue(requestId, req, options.clientManifest);
|
|
409
|
+
},
|
|
410
|
+
cancelRender: () => {
|
|
411
|
+
this.#cancelRender(requestId);
|
|
412
|
+
},
|
|
413
|
+
signal: options.signal,
|
|
414
|
+
onPendingChunkOverflow: () => {
|
|
415
|
+
this.#hostPendingChunkOverflowCount += 1;
|
|
416
|
+
},
|
|
240
417
|
});
|
|
241
|
-
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
invalidateRouteResultCache(reason?: string): void {
|
|
421
|
+
if (this.#status !== "ready") return;
|
|
422
|
+
try {
|
|
423
|
+
this.#proc.send(createRscWorkerInvalidateCacheMessage(reason));
|
|
424
|
+
} catch (error) {
|
|
425
|
+
this.#logger.warn(
|
|
426
|
+
`rsc worker cache invalidate send failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
427
|
+
);
|
|
428
|
+
}
|
|
242
429
|
}
|
|
243
430
|
|
|
244
431
|
kill(): void {
|
|
@@ -258,8 +445,18 @@ export class RscWorker {
|
|
|
258
445
|
}
|
|
259
446
|
|
|
260
447
|
getMetrics(): AkanMetricsReport {
|
|
261
|
-
|
|
262
|
-
|
|
448
|
+
const {
|
|
449
|
+
rscWorkerPid: _rscWorkerPid,
|
|
450
|
+
rscWorkerStatus: _rscWorkerStatus,
|
|
451
|
+
rscWorkerRestartCount: _rscWorkerRestartCount,
|
|
452
|
+
rscWorkerRecycleCount: _rscWorkerRecycleCount,
|
|
453
|
+
rscWorkerLastRecycleReason: _rscWorkerLastRecycleReason,
|
|
454
|
+
rscPendingRenderCount: _rscPendingRenderCount,
|
|
455
|
+
rscQueuedSendCount: _rscQueuedSendCount,
|
|
456
|
+
rscHostPendingChunkOverflowCount: _rscHostPendingChunkOverflowCount,
|
|
457
|
+
...workerMetrics
|
|
458
|
+
} = this.#lastWorkerMetrics;
|
|
459
|
+
return Object.assign(workerMetrics, {
|
|
263
460
|
rscWorkerPid: this.#proc.pid,
|
|
264
461
|
rscWorkerStatus: this.#status,
|
|
265
462
|
rscWorkerRestartCount: this.#restartCount,
|
|
@@ -267,7 +464,8 @@ export class RscWorker {
|
|
|
267
464
|
rscWorkerLastRecycleReason: this.#lastRecycleReason,
|
|
268
465
|
rscPendingRenderCount: this.#pending.size,
|
|
269
466
|
rscQueuedSendCount: this.#queuedSends.length,
|
|
270
|
-
|
|
467
|
+
rscHostPendingChunkOverflowCount: this.#hostPendingChunkOverflowCount,
|
|
468
|
+
});
|
|
271
469
|
}
|
|
272
470
|
|
|
273
471
|
restartWhenIdle(reason: string): boolean {
|
|
@@ -371,6 +569,10 @@ export class RscWorker {
|
|
|
371
569
|
|
|
372
570
|
#handleMessage(message: RscInMsg, proc: Bun.Subprocess<"ignore", "inherit", "inherit">): void {
|
|
373
571
|
if (proc !== this.#proc) return;
|
|
572
|
+
if (message.type === "cache-state") {
|
|
573
|
+
this.#pending.get(message.requestId)?.onCacheState?.(message.state);
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
374
576
|
switch (message.type) {
|
|
375
577
|
case "hello":
|
|
376
578
|
|
|
@@ -411,6 +613,11 @@ export class RscWorker {
|
|
|
411
613
|
p.onRedirect?.(message.location, message.method ?? "replace", message.status ?? 307),
|
|
412
614
|
);
|
|
413
615
|
return;
|
|
616
|
+
case "late-redirect":
|
|
617
|
+
this.#pending
|
|
618
|
+
.get(message.requestId)
|
|
619
|
+
?.onLateRedirect?.(message.location, message.method ?? "replace", message.status ?? 307);
|
|
620
|
+
return;
|
|
414
621
|
case "not-found":
|
|
415
622
|
this.#resolvePending(message.requestId, (p) => p.onNotFound?.());
|
|
416
623
|
return;
|
|
@@ -470,6 +677,14 @@ export class RscWorker {
|
|
|
470
677
|
}
|
|
471
678
|
}
|
|
472
679
|
|
|
680
|
+
#cancelRender(requestId: string): void {
|
|
681
|
+
if (this.#status !== "ready") return;
|
|
682
|
+
try {
|
|
683
|
+
this.#proc.send({ type: "cancel", requestId });
|
|
684
|
+
} catch {
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
473
688
|
#flushQueuedSends(): void {
|
|
474
689
|
const queue = this.#queuedSends;
|
|
475
690
|
this.#queuedSends = [];
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { RouteCacheRenderState } from "./cachePolicy";
|
|
2
|
+
|
|
3
|
+
export type CachedRscReplayMessage =
|
|
4
|
+
| { type: "meta"; requestId: string; theme?: string; status?: number }
|
|
5
|
+
| { type: "cache-state"; requestId: string; state: RouteCacheRenderState }
|
|
6
|
+
| { type: "chunk"; requestId: string; data: Uint8Array }
|
|
7
|
+
| { type: "end"; requestId: string };
|
|
8
|
+
|
|
9
|
+
function yieldToHostEventLoop(): Promise<void> {
|
|
10
|
+
return new Promise((resolve) => setTimeout(resolve, 0));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function replayCachedRscResult(input: {
|
|
14
|
+
requestId: string;
|
|
15
|
+
chunks: readonly Uint8Array[];
|
|
16
|
+
theme?: string;
|
|
17
|
+
status?: number;
|
|
18
|
+
cacheState?: RouteCacheRenderState;
|
|
19
|
+
send: (message: CachedRscReplayMessage) => void;
|
|
20
|
+
isCancelled: () => boolean;
|
|
21
|
+
yieldEveryChunks?: number;
|
|
22
|
+
yieldToHost?: () => Promise<void>;
|
|
23
|
+
}): Promise<boolean> {
|
|
24
|
+
const yieldEveryChunks =
|
|
25
|
+
input.yieldEveryChunks !== undefined && Number.isFinite(input.yieldEveryChunks) && input.yieldEveryChunks > 0
|
|
26
|
+
? Math.floor(input.yieldEveryChunks)
|
|
27
|
+
: 1;
|
|
28
|
+
const yieldToHost = input.yieldToHost ?? yieldToHostEventLoop;
|
|
29
|
+
if (input.isCancelled()) return false;
|
|
30
|
+
input.send({ type: "meta", requestId: input.requestId, theme: input.theme, status: input.status });
|
|
31
|
+
input.send({ type: "cache-state", requestId: input.requestId, state: input.cacheState ?? { cacheable: true } });
|
|
32
|
+
for (let index = 0; index < input.chunks.length; index += 1) {
|
|
33
|
+
if (input.isCancelled()) return false;
|
|
34
|
+
input.send({ type: "chunk", requestId: input.requestId, data: input.chunks[index] });
|
|
35
|
+
if ((index + 1) % yieldEveryChunks === 0) await yieldToHost();
|
|
36
|
+
}
|
|
37
|
+
if (input.isCancelled()) return false;
|
|
38
|
+
input.send({ type: "end", requestId: input.requestId });
|
|
39
|
+
return true;
|
|
40
|
+
}
|