@rangojs/router 0.0.0-experimental.e9c0b2f2 → 0.0.0-experimental.ea9f40f2
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/AGENTS.md +6 -10
- package/README.md +289 -938
- package/dist/bin/rango.js +271 -46
- package/dist/vite/index.js +673 -193
- package/package.json +10 -8
- package/skills/api-client/SKILL.md +1 -1
- package/skills/breadcrumbs/SKILL.md +31 -14
- package/skills/cache-guide/SKILL.md +5 -2
- package/skills/caching/SKILL.md +59 -4
- package/skills/catalog.json +271 -0
- package/skills/comparison/SKILL.md +50 -0
- package/skills/comparison/agents/openai.yaml +4 -0
- package/skills/comparison/references/framework-comparison.md +837 -0
- package/skills/composability/SKILL.md +83 -2
- package/skills/debug-manifest/SKILL.md +1 -1
- package/skills/defer-hydration/SKILL.md +235 -0
- package/skills/document-cache/SKILL.md +9 -1
- package/skills/fonts/SKILL.md +1 -1
- package/skills/handler-use/SKILL.md +8 -8
- package/skills/hooks/SKILL.md +54 -892
- package/skills/hooks/data.md +273 -0
- package/skills/hooks/handle-and-actions.md +103 -0
- package/skills/hooks/navigation.md +110 -0
- package/skills/hooks/outlets.md +41 -0
- package/skills/hooks/state.md +228 -0
- package/skills/hooks/urls.md +135 -0
- package/skills/host-router/SKILL.md +4 -4
- package/skills/i18n/SKILL.md +1 -1
- package/skills/intercept/SKILL.md +46 -14
- package/skills/layout/SKILL.md +27 -10
- package/skills/links/SKILL.md +1 -1
- package/skills/loader/SKILL.md +23 -1
- package/skills/middleware/SKILL.md +7 -3
- package/skills/migrate-nextjs/SKILL.md +167 -6
- package/skills/migrate-react-router/SKILL.md +59 -677
- package/skills/migrate-react-router/cloudflare-workers.md +129 -0
- package/skills/migrate-react-router/component-migration.md +196 -0
- package/skills/migrate-react-router/data-and-actions.md +225 -0
- package/skills/migrate-react-router/route-mapping.md +271 -0
- package/skills/mime-routes/SKILL.md +1 -1
- package/skills/observability/SKILL.md +9 -1
- package/skills/parallel/SKILL.md +23 -4
- package/skills/ppr/SKILL.md +622 -0
- package/skills/prerender/SKILL.md +28 -18
- package/skills/rango/SKILL.md +84 -25
- package/skills/response-routes/SKILL.md +15 -1
- package/skills/route/SKILL.md +71 -4
- package/skills/router-setup/SKILL.md +14 -3
- package/skills/scripts/SKILL.md +1 -1
- package/skills/server-actions/SKILL.md +3 -2
- package/skills/shell-manifest/SKILL.md +185 -0
- package/skills/streams-and-websockets/SKILL.md +1 -1
- package/skills/tailwind/SKILL.md +1 -1
- package/skills/testing/SKILL.md +2 -1
- package/skills/testing/handles.md +4 -2
- package/skills/testing/render-handler.md +15 -14
- package/skills/testing/reverse-and-types.md +8 -7
- package/skills/theme/SKILL.md +1 -1
- package/skills/typesafety/SKILL.md +45 -919
- package/skills/typesafety/env-and-bindings.md +254 -0
- package/skills/typesafety/generated-files-and-cli.md +335 -0
- package/skills/typesafety/params-and-search.md +153 -0
- package/skills/typesafety/route-types.md +209 -0
- package/skills/use-cache/SKILL.md +30 -3
- package/skills/vercel/SKILL.md +1 -1
- package/skills/view-transitions/SKILL.md +44 -1
- package/src/browser/event-controller.ts +62 -10
- package/src/browser/logging.ts +28 -0
- package/src/browser/merge-segment-loaders.ts +6 -4
- package/src/browser/navigation-bridge.ts +65 -16
- package/src/browser/navigation-client.ts +32 -2
- package/src/browser/navigation-store.ts +128 -14
- package/src/browser/network-error-handler.ts +34 -7
- package/src/browser/partial-update.ts +76 -17
- package/src/browser/prefetch/cache.ts +51 -11
- package/src/browser/prefetch/fetch.ts +59 -21
- package/src/browser/prefetch/queue.ts +19 -4
- package/src/browser/react/Link.tsx +13 -3
- package/src/browser/react/NavigationProvider.tsx +108 -4
- package/src/browser/response-adapter.ts +38 -9
- package/src/browser/rsc-router.tsx +54 -4
- package/src/browser/scroll-restoration.ts +7 -5
- package/src/browser/segment-reconciler.ts +31 -21
- package/src/browser/server-action-bridge.ts +22 -10
- package/src/browser/types.ts +54 -1
- package/src/build/generate-manifest.ts +155 -131
- package/src/build/index.ts +3 -1
- package/src/build/route-trie.ts +35 -7
- package/src/build/route-types/include-resolution.ts +347 -47
- package/src/build/runtime-discovery.ts +4 -1
- package/src/cache/cache-key-utils.ts +29 -0
- package/src/cache/cache-runtime.ts +262 -71
- package/src/cache/cache-scope.ts +2 -17
- package/src/cache/cache-tag.ts +60 -14
- package/src/cache/cf/cf-cache-store.ts +243 -20
- package/src/cache/document-cache.ts +54 -21
- package/src/cache/index.ts +1 -0
- package/src/cache/memory-segment-store.ts +110 -3
- package/src/cache/profile-registry.ts +15 -0
- package/src/cache/read-through-swr.ts +15 -1
- package/src/cache/segment-codec.ts +4 -4
- package/src/cache/shell-snapshot.ts +417 -0
- package/src/cache/types.ts +158 -0
- package/src/cache/vercel/vercel-cache-store.ts +401 -124
- package/src/client.rsc.tsx +0 -3
- package/src/client.tsx +0 -3
- package/src/cloudflare/tracing.ts +7 -8
- package/src/defer.ts +11 -22
- package/src/handle.ts +37 -15
- package/src/handles/MetaTags.tsx +16 -82
- package/src/handles/breadcrumbs.ts +12 -14
- package/src/handles/deferred-resolution.ts +127 -0
- package/src/handles/is-thenable.ts +7 -8
- package/src/handles/meta.ts +7 -44
- package/src/host/errors.ts +15 -0
- package/src/host/index.ts +1 -0
- package/src/index.rsc.ts +8 -2
- package/src/index.ts +19 -13
- package/src/internal-debug.ts +11 -8
- package/src/prerender.ts +17 -4
- package/src/redirect-origin.ts +14 -0
- package/src/render-error-thrower.tsx +20 -0
- package/src/route-content-wrapper.tsx +12 -5
- package/src/route-definition/dsl-helpers.ts +21 -32
- package/src/route-definition/helper-factories.ts +0 -2
- package/src/route-definition/helpers-types.ts +43 -43
- package/src/route-definition/index.ts +1 -2
- package/src/route-definition/resolve-handler-use.ts +0 -1
- package/src/route-definition/use-item-types.ts +3 -6
- package/src/route-map-builder.ts +41 -4
- package/src/route-types.ts +0 -5
- package/src/router/find-match.ts +86 -8
- package/src/router/instrument.ts +9 -4
- package/src/router/lazy-includes.ts +72 -12
- package/src/router/loader-resolution.ts +14 -2
- package/src/router/manifest.ts +56 -11
- package/src/router/match-api.ts +76 -32
- package/src/router/match-handlers.ts +181 -135
- package/src/router/match-middleware/background-revalidation.ts +40 -23
- package/src/router/match-middleware/cache-store.ts +39 -24
- package/src/router/match-result.ts +35 -15
- package/src/router/middleware.ts +64 -38
- package/src/router/navigation-snapshot.ts +7 -5
- package/src/router/parse-pattern.ts +115 -0
- package/src/router/pattern-matching.ts +53 -64
- package/src/router/prefetch-limits.ts +37 -0
- package/src/router/prerender-match.ts +11 -5
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +23 -8
- package/src/router/route-snapshot.ts +14 -2
- package/src/router/router-context.ts +3 -1
- package/src/router/router-interfaces.ts +32 -1
- package/src/router/router-options.ts +30 -0
- package/src/router/segment-resolution/fresh.ts +39 -3
- package/src/router/segment-resolution/loader-cache.ts +93 -2
- package/src/router/segment-resolution/loader-mask.ts +60 -0
- package/src/router/segment-resolution/loader-snapshot.ts +259 -0
- package/src/router/segment-resolution/mask-nested.ts +83 -0
- package/src/router/segment-resolution/revalidation.ts +3 -0
- package/src/router/segment-resolution/view-transition-default.ts +35 -15
- package/src/router/substitute-pattern-params.ts +54 -35
- package/src/router/telemetry-otel.ts +6 -8
- package/src/router/telemetry.ts +9 -1
- package/src/router/tracing.ts +14 -5
- package/src/router/trie-matching.ts +19 -11
- package/src/router/url-params.ts +13 -0
- package/src/router.ts +47 -16
- package/src/rsc/full-payload.ts +70 -0
- package/src/rsc/handler.ts +60 -33
- package/src/rsc/manifest-init.ts +1 -1
- package/src/rsc/nonce.ts +10 -1
- package/src/rsc/progressive-enhancement.ts +61 -4
- package/src/rsc/redirect-guard.ts +2 -1
- package/src/rsc/rsc-rendering.ts +429 -37
- package/src/rsc/server-action.ts +25 -2
- package/src/rsc/shell-capture.ts +1190 -0
- package/src/rsc/shell-serve.ts +181 -0
- package/src/rsc/transition-gate.ts +89 -0
- package/src/rsc/types.ts +30 -0
- package/src/segment-loader-promise.ts +18 -0
- package/src/segment-system.tsx +149 -14
- package/src/server/context.ts +67 -9
- package/src/server/cookie-store.ts +73 -1
- package/src/server/loader-registry.ts +13 -1
- package/src/server/request-context.ts +169 -10
- package/src/ssr/index.tsx +462 -178
- package/src/ssr/inject-rsc-eager.ts +167 -0
- package/src/ssr/ssr-root.tsx +228 -0
- package/src/testing/collect-handle.ts +14 -8
- package/src/testing/dispatch.ts +152 -40
- package/src/testing/generated-routes.ts +27 -11
- package/src/testing/index.ts +6 -0
- package/src/testing/render-handler.ts +14 -0
- package/src/testing/render-route.tsx +13 -10
- package/src/testing/run-transition-when.ts +164 -0
- package/src/theme/ThemeProvider.tsx +36 -26
- package/src/types/handler-context.ts +1 -1
- package/src/types/index.ts +2 -0
- package/src/types/route-config.ts +19 -7
- package/src/types/segments.ts +100 -0
- package/src/urls/include-helper.ts +10 -8
- package/src/urls/include-provider.ts +71 -0
- package/src/urls/index.ts +1 -0
- package/src/urls/path-helper-types.ts +44 -12
- package/src/urls/path-helper.ts +5 -0
- package/src/urls/pattern-types.ts +36 -0
- package/src/urls/type-extraction.ts +43 -18
- package/src/urls/urls-function.ts +0 -1
- package/src/vercel/tracing.ts +7 -7
- package/src/vite/discovery/dev-prerender-cache.ts +117 -0
- package/src/vite/discovery/discover-routers.ts +1 -1
- package/src/vite/discovery/discovery-errors.ts +61 -0
- package/src/vite/index.ts +7 -0
- package/src/vite/inject-client-debug.ts +88 -0
- package/src/vite/plugins/vercel-output.ts +114 -25
- package/src/vite/plugins/version-injector.ts +22 -7
- package/src/vite/plugins/virtual-entries.ts +80 -22
- package/src/vite/rango.ts +29 -19
- package/src/vite/router-discovery.ts +171 -43
- package/src/vite/utils/prerender-utils.ts +17 -4
- package/src/vite/utils/shared-utils.ts +47 -0
- package/src/network-error-thrower.tsx +0 -18
package/src/ssr/index.tsx
CHANGED
|
@@ -1,21 +1,7 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
filterSegmentOrder,
|
|
5
|
-
filterRouteSegmentIds,
|
|
6
|
-
} from "../browser/react/filter-segment-order.js";
|
|
7
|
-
import { ThemeProvider } from "../theme/ThemeProvider.js";
|
|
8
|
-
import { NonceContext } from "../browser/react/nonce-context.js";
|
|
9
|
-
import { NavigationStoreContext } from "../browser/react/context.js";
|
|
10
|
-
import type { NavigationStoreContextValue } from "../browser/react/context.js";
|
|
11
|
-
import type { HandleData } from "../browser/types.js";
|
|
2
|
+
import { createSsrRootComponent } from "./ssr-root.js";
|
|
3
|
+
import { injectRSCPayloadEager } from "./inject-rsc-eager.js";
|
|
12
4
|
import type { ErrorPhase } from "../types.js";
|
|
13
|
-
import type { ResolvedSegment } from "../types.js";
|
|
14
|
-
import type { ResolvedThemeConfig, Theme } from "../theme/types.js";
|
|
15
|
-
import type {
|
|
16
|
-
EventController,
|
|
17
|
-
DerivedNavigationState,
|
|
18
|
-
} from "../browser/event-controller.js";
|
|
19
5
|
|
|
20
6
|
/**
|
|
21
7
|
* Options for injectRSCPayload
|
|
@@ -43,6 +29,51 @@ interface ReactDOMReadableStream extends ReadableStream<Uint8Array> {
|
|
|
43
29
|
allReady: Promise<void>;
|
|
44
30
|
}
|
|
45
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Options for prerender from react-dom/static.edge
|
|
34
|
+
*/
|
|
35
|
+
interface PrerenderOptions {
|
|
36
|
+
signal?: AbortSignal;
|
|
37
|
+
bootstrapScriptContent?: string;
|
|
38
|
+
onError?: (error: unknown) => void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Result of prerender from react-dom/static.edge. `postponed` is React's
|
|
43
|
+
* opaque resume state — non-null when the render was aborted with pending
|
|
44
|
+
* holes, null when the shell completed with nothing left to stream.
|
|
45
|
+
*/
|
|
46
|
+
interface PrerenderResult {
|
|
47
|
+
prelude: ReadableStream<Uint8Array>;
|
|
48
|
+
postponed: unknown;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* prerender from react-dom/static.edge
|
|
53
|
+
*/
|
|
54
|
+
type PrerenderFn = (
|
|
55
|
+
element: React.ReactNode,
|
|
56
|
+
options?: PrerenderOptions,
|
|
57
|
+
) => Promise<PrerenderResult>;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Options for resume from react-dom/server.edge
|
|
61
|
+
*/
|
|
62
|
+
interface ResumeOptions {
|
|
63
|
+
onError?: (error: unknown) => void;
|
|
64
|
+
nonce?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* resume from react-dom/server.edge — continues a prerendered render, emitting
|
|
69
|
+
* only the postponed holes.
|
|
70
|
+
*/
|
|
71
|
+
type ResumeFn = (
|
|
72
|
+
element: React.ReactNode,
|
|
73
|
+
postponedState: unknown,
|
|
74
|
+
options?: ResumeOptions,
|
|
75
|
+
) => Promise<ReactDOMReadableStream>;
|
|
76
|
+
|
|
46
77
|
/**
|
|
47
78
|
* Options for the renderHTML function
|
|
48
79
|
*/
|
|
@@ -102,6 +133,18 @@ export interface SSRDependencies<TEnv = unknown> {
|
|
|
102
133
|
*/
|
|
103
134
|
loadBootstrapScriptContent: () => Promise<string>;
|
|
104
135
|
|
|
136
|
+
/**
|
|
137
|
+
* prerender from react-dom/static.edge. Optional; required only by
|
|
138
|
+
* {@link createShellCaptureHandler} for PPR shell capture.
|
|
139
|
+
*/
|
|
140
|
+
prerender?: PrerenderFn;
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* resume from react-dom/server.edge. Optional; required only by
|
|
144
|
+
* {@link createShellResumeHandler} for resuming a postponed shell.
|
|
145
|
+
*/
|
|
146
|
+
resume?: ResumeFn;
|
|
147
|
+
|
|
105
148
|
/**
|
|
106
149
|
* Optional callback invoked when an error occurs during SSR rendering.
|
|
107
150
|
*
|
|
@@ -122,96 +165,165 @@ export interface SSRDependencies<TEnv = unknown> {
|
|
|
122
165
|
}
|
|
123
166
|
|
|
124
167
|
/**
|
|
125
|
-
*
|
|
168
|
+
* Default guard for how long capture waits on the caller's `quiesce` signal
|
|
169
|
+
* before forcing the abort that freezes the shell. This is the ONLY wall-clock
|
|
170
|
+
* on the capture path and it is a pathological guard — it should never fire once
|
|
171
|
+
* the caller's `quiesce` is a task-quantized, frozen-byte signal (the capture
|
|
172
|
+
* gate in shell-capture.ts). See docs/design/ppr-shell-resume.md.
|
|
126
173
|
*/
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
174
|
+
const DEFAULT_SHELL_CAPTURE_MAX_WAIT_MS = 5000;
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Fixed number of macrotask hops between `quiesce` resolving and the abort. These
|
|
178
|
+
* give React's fizz worker turns to flush the settled shell into the prelude and
|
|
179
|
+
* mark still-pending boundaries as POSTPONED (rather than errored) before
|
|
180
|
+
* controller.abort() lands. Not a wall-clock wait.
|
|
181
|
+
*
|
|
182
|
+
* Why 16 and not the original 2: under the REPLAY-ONLY capture model
|
|
183
|
+
* (docs/design/ppr-shell-resume.md), the capture Flight render serializes ring-3
|
|
184
|
+
* cached segments that are ALREADY serialized, so it emits the whole shell payload
|
|
185
|
+
* in the first tick and the gate declares quiesce almost immediately (~a few ms).
|
|
186
|
+
* On the old fresh-execution path the Flight dribbled out as handlers ran, so
|
|
187
|
+
* Flight-quiet effectively meant "the shell has rendered" and 2 hops sufficed. Under
|
|
188
|
+
* replay, Flight-quiet fires BEFORE the fizz side has consumed the instant payload
|
|
189
|
+
* and rendered the shell to `<body>`, so the fizz needs a real buffer of turns after
|
|
190
|
+
* quiesce — otherwise the abort lands on an unrendered tree (empty prelude, root
|
|
191
|
+
* postpone) and the sanity gate refuses. Still task-based (masked loaders never
|
|
192
|
+
* emit, so more hops never lets a hole settle); a cold worker whose first
|
|
193
|
+
* attempt still under-renders heals on the in-place retry. Bounded by maxWaitMs.
|
|
194
|
+
*/
|
|
195
|
+
const POST_QUIESCE_TASK_HOPS = 16;
|
|
141
196
|
|
|
142
197
|
/**
|
|
143
|
-
*
|
|
144
|
-
*
|
|
198
|
+
* Route an SSR error through the deps.onError notification callback with the
|
|
199
|
+
* "rendering" phase. Swallows callback failures so a broken reporter never
|
|
200
|
+
* masks the original error. Shared by renderHTML, capture, and resume so the
|
|
201
|
+
* onError contract is identical across all three handlers.
|
|
145
202
|
*/
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
203
|
+
function reportRenderError(
|
|
204
|
+
onError: SSRDependencies["onError"],
|
|
205
|
+
error: unknown,
|
|
206
|
+
): void {
|
|
207
|
+
if (onError) {
|
|
208
|
+
const errorObj = error instanceof Error ? error : new Error(String(error));
|
|
209
|
+
try {
|
|
210
|
+
onError(errorObj, { phase: "rendering" });
|
|
211
|
+
} catch (callbackError) {
|
|
212
|
+
console.error("[SSRHandler.onError] Callback error:", callbackError);
|
|
213
|
+
}
|
|
152
214
|
}
|
|
153
|
-
return lastData;
|
|
154
215
|
}
|
|
155
216
|
|
|
156
217
|
/**
|
|
157
|
-
*
|
|
158
|
-
*
|
|
218
|
+
* Yield one macrotask. Used by capture to let React's fizz worker flush the
|
|
219
|
+
* shell and mark still-pending boundaries as postponed before the abort.
|
|
159
220
|
*/
|
|
160
|
-
function
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
handleData?: HandleData;
|
|
164
|
-
matched?: string[];
|
|
165
|
-
}): EventController {
|
|
166
|
-
const location = new URL(opts.pathname, "http://localhost");
|
|
167
|
-
let params = opts.params ?? {};
|
|
168
|
-
const rawMatched = opts.matched ?? [];
|
|
169
|
-
const handleState = {
|
|
170
|
-
data: opts.handleData ?? {},
|
|
171
|
-
segmentOrder: filterSegmentOrder(rawMatched),
|
|
172
|
-
routeSegmentIds: filterRouteSegmentIds(rawMatched),
|
|
173
|
-
};
|
|
174
|
-
const state: DerivedNavigationState = {
|
|
175
|
-
state: "idle",
|
|
176
|
-
isStreaming: false,
|
|
177
|
-
isNavigating: false,
|
|
178
|
-
location,
|
|
179
|
-
pendingUrl: null,
|
|
180
|
-
inflightActions: [],
|
|
181
|
-
};
|
|
221
|
+
function macrotask(): Promise<void> {
|
|
222
|
+
return new Promise((resolve) => setTimeout(resolve, 0));
|
|
223
|
+
}
|
|
182
224
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
225
|
+
/**
|
|
226
|
+
* A timeout promise paired with a cancel() so the pending timer is cleared once
|
|
227
|
+
* the race is decided — otherwise the maxWait timer keeps the event loop alive
|
|
228
|
+
* for the full duration even after `quiesce` won.
|
|
229
|
+
*/
|
|
230
|
+
function createCancelableTimeout(ms: number): {
|
|
231
|
+
promise: Promise<void>;
|
|
232
|
+
cancel: () => void;
|
|
233
|
+
} {
|
|
234
|
+
let id: ReturnType<typeof setTimeout> | undefined;
|
|
235
|
+
const promise = new Promise<void>((resolve) => {
|
|
236
|
+
id = setTimeout(resolve, ms);
|
|
237
|
+
});
|
|
238
|
+
return { promise, cancel: () => clearTimeout(id) };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Drain a ReadableStream fully into a single Uint8Array. Capture buffers the
|
|
243
|
+
* whole prelude so it can be stored and later prepended byte-for-byte.
|
|
244
|
+
*/
|
|
245
|
+
async function readStreamToUint8Array(
|
|
246
|
+
stream: ReadableStream<Uint8Array>,
|
|
247
|
+
): Promise<Uint8Array> {
|
|
248
|
+
const reader = stream.getReader();
|
|
249
|
+
const chunks: Uint8Array[] = [];
|
|
250
|
+
let total = 0;
|
|
251
|
+
try {
|
|
252
|
+
while (true) {
|
|
253
|
+
const { done, value } = await reader.read();
|
|
254
|
+
if (done) break;
|
|
255
|
+
chunks.push(value);
|
|
256
|
+
total += value.length;
|
|
257
|
+
}
|
|
258
|
+
} catch (error) {
|
|
259
|
+
// Mid-read abort path (documented): the prelude stream errors with our
|
|
260
|
+
// abort reason while we are still reading it. Cancel the source before
|
|
261
|
+
// rethrowing so it is not left uncancelled; releaseLock always runs in
|
|
262
|
+
// finally. Mirrors src/rsc/rsc-rendering.ts's serve-side reader cleanup.
|
|
263
|
+
reader.cancel(error).catch(() => {});
|
|
264
|
+
throw error;
|
|
265
|
+
} finally {
|
|
266
|
+
reader.releaseLock();
|
|
267
|
+
}
|
|
268
|
+
const out = new Uint8Array(total);
|
|
269
|
+
let offset = 0;
|
|
270
|
+
for (const chunk of chunks) {
|
|
271
|
+
out.set(chunk, offset);
|
|
272
|
+
offset += chunk.length;
|
|
273
|
+
}
|
|
274
|
+
return out;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* A minimal HTML stream for the resume DATA variant: one empty chunk, then
|
|
279
|
+
* close.
|
|
280
|
+
*
|
|
281
|
+
* injectRSCPayload only resolves its internal flight-data promise (and thus
|
|
282
|
+
* only writes the Flight payload <script> pushes) from inside transform()'s
|
|
283
|
+
* scheduled callback. A stream that closes without ever emitting a chunk never
|
|
284
|
+
* runs transform, so its flush() awaits a promise that is never resolved and
|
|
285
|
+
* the output deadlocks. Emitting a single empty chunk runs transform once,
|
|
286
|
+
* which is enough for the payload to be written and the trailer appended.
|
|
287
|
+
*/
|
|
288
|
+
function createDataVariantHtmlStream(): ReadableStream<Uint8Array> {
|
|
289
|
+
return new ReadableStream({
|
|
290
|
+
start(controller) {
|
|
291
|
+
controller.enqueue(new Uint8Array(0));
|
|
292
|
+
controller.close();
|
|
209
293
|
},
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Options for the captureShellHTML function returned by
|
|
299
|
+
* {@link createShellCaptureHandler}.
|
|
300
|
+
*/
|
|
301
|
+
interface ShellCaptureOptions {
|
|
302
|
+
/** Caller-provided promise that resolves once the cached content settled. */
|
|
303
|
+
quiesce: Promise<void>;
|
|
304
|
+
/** Upper bound on how long to wait for `quiesce`. Default 5000ms. */
|
|
305
|
+
maxWaitMs?: number;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Result of a successful shell capture. `prelude` is the raw prelude bytes;
|
|
310
|
+
* `postponed` is React's resume state serialized to JSON, or null when the
|
|
311
|
+
* shell completed with no holes (the DATA variant).
|
|
312
|
+
*/
|
|
313
|
+
interface ShellCaptureResult {
|
|
314
|
+
prelude: Uint8Array;
|
|
315
|
+
postponed: string | null;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Options for the resumeShellHTML function returned by
|
|
320
|
+
* {@link createShellResumeHandler}.
|
|
321
|
+
*/
|
|
322
|
+
interface ShellResumeOptions {
|
|
323
|
+
/** JSON from capture; null selects the DATA variant (no fizz). */
|
|
324
|
+
postponed: string | null;
|
|
325
|
+
/** Nonce for CSP. */
|
|
326
|
+
nonce?: string;
|
|
215
327
|
}
|
|
216
328
|
|
|
217
329
|
/**
|
|
@@ -260,81 +372,11 @@ export function createSSRHandler<TEnv = unknown>(deps: SSRDependencies<TEnv>) {
|
|
|
260
372
|
// - rscStream2: For browser hydration (inject as __FLIGHT_DATA__)
|
|
261
373
|
const [rscStream1, rscStream2] = rscStream.tee();
|
|
262
374
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
function SsrRoot() {
|
|
269
|
-
payload ??= createFromReadableStream<RscPayload>(rscStream1);
|
|
270
|
-
const resolved = React.use(payload);
|
|
271
|
-
|
|
272
|
-
const themeConfig = resolved.metadata?.themeConfig ?? null;
|
|
273
|
-
const pathname = resolved.metadata?.pathname ?? "/";
|
|
274
|
-
|
|
275
|
-
// Await handles before creating SSR event controller so hooks can
|
|
276
|
-
// read request-local handle data via NavigationStoreContext.
|
|
277
|
-
// The handles property is an async generator that yields on each push
|
|
278
|
-
// Memoize the promise since async generators can only be iterated once
|
|
279
|
-
let handleData: HandleData = {};
|
|
280
|
-
if (resolved.metadata?.handles) {
|
|
281
|
-
handlesPromise ??= consumeAsyncGenerator(resolved.metadata.handles);
|
|
282
|
-
handleData = React.use(handlesPromise);
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// Create SSR context with request-local pathname/params/handles.
|
|
286
|
-
ssrContextValue ??= {
|
|
287
|
-
store: null as any,
|
|
288
|
-
eventController: createSsrEventController({
|
|
289
|
-
pathname,
|
|
290
|
-
params: resolved.metadata?.params,
|
|
291
|
-
handleData,
|
|
292
|
-
matched: resolved.metadata?.matched,
|
|
293
|
-
}),
|
|
294
|
-
navigate: async () => {},
|
|
295
|
-
refresh: async () => {},
|
|
296
|
-
version: resolved.metadata?.version,
|
|
297
|
-
basename: resolved.metadata?.basename,
|
|
298
|
-
};
|
|
299
|
-
|
|
300
|
-
// Build content tree from segments.
|
|
301
|
-
// Order must match NavigationProvider: NavigationStoreContext > NonceContext > ThemeProvider > content
|
|
302
|
-
// Memoize like payload/handles above: renderSegments is async, so
|
|
303
|
-
// React.use() on a fresh promise suspends and replays SsrRoot, which
|
|
304
|
-
// would re-run the entire segment-tree build on every initial render.
|
|
305
|
-
rootPromise ??= Promise.resolve(
|
|
306
|
-
renderSegments(resolved.metadata?.segments ?? [], {
|
|
307
|
-
rootLayout: resolved.metadata?.rootLayout,
|
|
308
|
-
}),
|
|
309
|
-
);
|
|
310
|
-
let content: React.ReactNode = React.use(rootPromise);
|
|
311
|
-
|
|
312
|
-
// Wrap content with ThemeProvider if theme is enabled
|
|
313
|
-
if (themeConfig) {
|
|
314
|
-
content = (
|
|
315
|
-
<ThemeProvider
|
|
316
|
-
config={themeConfig}
|
|
317
|
-
initialTheme={resolved.metadata?.initialTheme}
|
|
318
|
-
>
|
|
319
|
-
{content}
|
|
320
|
-
</ThemeProvider>
|
|
321
|
-
);
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// Wrap with NonceContext so client components (e.g. MetaTags) can
|
|
325
|
-
// apply CSP nonces to inline scripts during SSR. Always present to
|
|
326
|
-
// match the browser-side NavigationProvider tree shape for hydration.
|
|
327
|
-
content = (
|
|
328
|
-
<NonceContext.Provider value={nonce}>{content}</NonceContext.Provider>
|
|
329
|
-
);
|
|
330
|
-
|
|
331
|
-
// Wrap with NavigationStoreContext for useNavigation hook
|
|
332
|
-
return (
|
|
333
|
-
<NavigationStoreContext.Provider value={ssrContextValue!}>
|
|
334
|
-
{content}
|
|
335
|
-
</NavigationStoreContext.Provider>
|
|
336
|
-
);
|
|
337
|
-
}
|
|
375
|
+
const SsrRoot = createSsrRootComponent({
|
|
376
|
+
createFromReadableStream,
|
|
377
|
+
rscStream: rscStream1,
|
|
378
|
+
nonce,
|
|
379
|
+
});
|
|
338
380
|
|
|
339
381
|
// Get bootstrap script content
|
|
340
382
|
const bootstrapScriptContent = await loadBootstrapScriptContent();
|
|
@@ -358,16 +400,258 @@ export function createSSRHandler<TEnv = unknown>(deps: SSRDependencies<TEnv>) {
|
|
|
358
400
|
// Inject RSC payload into HTML as <script nonce="...">__FLIGHT_DATA__</script>
|
|
359
401
|
return htmlStream.pipeThrough(injectRSCPayload(rscStream2, { nonce }));
|
|
360
402
|
} catch (error) {
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
403
|
+
reportRenderError(onError, error);
|
|
404
|
+
throw error;
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Create the PPR shell capture handler.
|
|
411
|
+
*
|
|
412
|
+
* captureShellHTML prerenders the shell over the (cached, loader-masked) Flight
|
|
413
|
+
* stream, aborts once the shell settles, and returns the prelude bytes plus the
|
|
414
|
+
* postponed resume state for storage. The stored pair is later served by
|
|
415
|
+
* {@link createShellResumeHandler}. See docs/design/ppr-shell-resume.md.
|
|
416
|
+
*
|
|
417
|
+
* Throws at creation if `deps.prerender` is missing — capture cannot run
|
|
418
|
+
* without react-dom/static.edge's prerender.
|
|
419
|
+
*/
|
|
420
|
+
export function createShellCaptureHandler<TEnv = unknown>(
|
|
421
|
+
deps: SSRDependencies<TEnv>,
|
|
422
|
+
) {
|
|
423
|
+
const { createFromReadableStream, loadBootstrapScriptContent, prerender } =
|
|
424
|
+
deps;
|
|
425
|
+
const onError = deps.onError;
|
|
426
|
+
|
|
427
|
+
if (!prerender) {
|
|
428
|
+
throw new Error(
|
|
429
|
+
"[createShellCaptureHandler] Missing `prerender` dependency (react-dom/static.edge). " +
|
|
430
|
+
"PPR shell capture requires the prerender export; wire it in the SSR virtual entry.",
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Prerender the shell and return the stored artifacts, or null when the
|
|
436
|
+
* shell degraded (root postpone / hung handles) and must not be cached.
|
|
437
|
+
*
|
|
438
|
+
* @param rscStream - Flight stream to render the shell over. Not teed and not
|
|
439
|
+
* piped through injectRSCPayload: the hydration payload is produced fresh
|
|
440
|
+
* per request by the resume/serve pass.
|
|
441
|
+
* @param opts - quiesce signal and maxWait guard.
|
|
442
|
+
*/
|
|
443
|
+
return async function captureShellHTML(
|
|
444
|
+
rscStream: ReadableStream<Uint8Array>,
|
|
445
|
+
opts: ShellCaptureOptions,
|
|
446
|
+
): Promise<ShellCaptureResult | null> {
|
|
447
|
+
const maxWaitMs = opts.maxWaitMs ?? DEFAULT_SHELL_CAPTURE_MAX_WAIT_MS;
|
|
448
|
+
|
|
449
|
+
// Arm the maxWaitMs deadline BEFORE the first await so it bounds the ENTIRE
|
|
450
|
+
// capture, the bootstrap-script load included. loadBootstrapScriptContent()
|
|
451
|
+
// used to run before the timer, so a hung/slow bootstrap load hung
|
|
452
|
+
// captureShellHTML with no upper bound and held the background capture task
|
|
453
|
+
// open. One deadline, shared by the bootstrap race below and the quiesce
|
|
454
|
+
// race, keeps the whole path "bounded by maxWaitMs like every quiesce input".
|
|
455
|
+
const deadline = createCancelableTimeout(maxWaitMs);
|
|
456
|
+
try {
|
|
457
|
+
// No nonce (nonce'd requests never reach capture); no formState.
|
|
458
|
+
const SsrRoot = createSsrRootComponent({
|
|
459
|
+
createFromReadableStream,
|
|
460
|
+
rscStream,
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
// Bootstrap load raced against the deadline. A load that never resolves
|
|
464
|
+
// within maxWaitMs is the same bounded no-shell degrade as a shell that
|
|
465
|
+
// never goes quiet: return null, do not hang. A load that REJECTS is a
|
|
466
|
+
// genuine error and still propagates (it is not the deadline). `null` is
|
|
467
|
+
// the deadline sentinel — disjoint from the load's `Promise<string>`, so
|
|
468
|
+
// the race narrows to `string | null` with no wrapper. The no-op catch
|
|
469
|
+
// keeps a late rejection off the unhandledRejection path when the deadline
|
|
470
|
+
// already won; a rejection that lands first still propagates out.
|
|
471
|
+
const load = loadBootstrapScriptContent();
|
|
472
|
+
load.catch(() => {});
|
|
473
|
+
const bootstrapScriptContent = await Promise.race([
|
|
474
|
+
load,
|
|
475
|
+
deadline.promise.then(() => null),
|
|
476
|
+
]);
|
|
477
|
+
if (bootstrapScriptContent === null) {
|
|
478
|
+
return null;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Start prerender first, then run the abort schedule concurrently. When
|
|
482
|
+
// holes are pending, prerender's promise settles only after abort(); when
|
|
483
|
+
// the shell completes with no holes it settles on its own and the later
|
|
484
|
+
// abort() is a harmless no-op (the DATA variant).
|
|
485
|
+
const controller = new AbortController();
|
|
486
|
+
// Private reason object: the deliberate abort is identified by object
|
|
487
|
+
// IDENTITY in both the onError below and the post-await catch. React
|
|
488
|
+
// propagates this EXACT object to onError for every still-pending boundary
|
|
489
|
+
// (verified identity-preserving), and rejects/errors the prelude with it.
|
|
490
|
+
const abortReason = { rangoShellCaptureAbort: true };
|
|
491
|
+
const prerenderPromise = prerender(<SsrRoot />, {
|
|
492
|
+
signal: controller.signal,
|
|
493
|
+
bootstrapScriptContent,
|
|
494
|
+
// Abort is how capture WORKS: once the shell is quiet we abort() to
|
|
495
|
+
// freeze the prelude and let the still-pending holes postpone. React
|
|
496
|
+
// reports the abort reason for each pending boundary through onError.
|
|
497
|
+
// Without an onError here React falls back to console.error, so every
|
|
498
|
+
// capture that still has a live hole at abort time (the normal case)
|
|
499
|
+
// dumps a stack once per pending boundary. That is EXPECTED degradation,
|
|
500
|
+
// so swallow OUR abort — matched by IDENTITY (error === abortReason).
|
|
501
|
+
// Discriminate by identity, NOT error.name: capture aborts before
|
|
502
|
+
// awaiting, so signal.aborted is unconditionally true and a name check
|
|
503
|
+
// swallowed genuine AbortError-named throws (a component's own
|
|
504
|
+
// fetch/AbortController cancellation) as if they were our abort. Genuine
|
|
505
|
+
// render errors are NOT our sentinel and still surface through
|
|
506
|
+
// deps.onError, the same channel renderHTML uses. See
|
|
507
|
+
// docs/design/ppr-shell-resume.md.
|
|
508
|
+
onError: (error: unknown) => {
|
|
509
|
+
if (error === abortReason) {
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
reportRenderError(onError, error);
|
|
513
|
+
},
|
|
514
|
+
});
|
|
515
|
+
// Pre-attach a no-op catch: the real await sits AFTER quiesce + the
|
|
516
|
+
// post-quiesce hops, so an early prerender rejection (e.g. a bake-lane
|
|
517
|
+
// loader tripping the identity guard within milliseconds) would otherwise
|
|
518
|
+
// spend several turns handler-less and crash the worker as an unhandled
|
|
519
|
+
// rejection. The actual rejection handling still happens at the await
|
|
520
|
+
// below; this parallel handler only keeps the gap crash-free.
|
|
521
|
+
prerenderPromise.catch(() => {});
|
|
522
|
+
|
|
523
|
+
// Wait for the caller's quiesce signal, bounded by the SAME deadline. By
|
|
524
|
+
// the time it resolves the Flight input is byte-quiet and FROZEN by the
|
|
525
|
+
// capture gate (shell-capture.ts gateFlightForCapture), so there is no
|
|
526
|
+
// wall-clock debounce here — maxWaitMs is only the pathological guard for a
|
|
527
|
+
// shell that never goes quiet (a root postpone / hung handle).
|
|
528
|
+
await Promise.race([opts.quiesce, deadline.promise]);
|
|
529
|
+
// Fixed task hops before the abort: give React's fizz worker turns to flush
|
|
530
|
+
// the now-complete shell and mark the still-pending boundaries as POSTPONED
|
|
531
|
+
// rather than errored. Deterministic (the byte set is already frozen), so a
|
|
532
|
+
// fixed count of turns suffices — no wall-clock.
|
|
533
|
+
for (let i = 0; i < POST_QUIESCE_TASK_HOPS; i++) {
|
|
534
|
+
await macrotask();
|
|
535
|
+
}
|
|
536
|
+
controller.abort(abortReason);
|
|
537
|
+
|
|
538
|
+
// A hard prerender rejection (fatal shell error) propagates. Expected
|
|
539
|
+
// degradation surfaces three ways and all return null: a trivial prelude
|
|
540
|
+
// (sanity gate below), the prerender REJECTING with our abort reason, or
|
|
541
|
+
// the prelude STREAM erroring with our abort reason mid-read — both abort
|
|
542
|
+
// shapes happen when our own abort lands before the shell completed (seen
|
|
543
|
+
// on dev cold paths, where module transform / first-render latency
|
|
544
|
+
// outlasts flight quiesce; a later request re-captures against warm
|
|
545
|
+
// modules and succeeds).
|
|
546
|
+
let prelude: Uint8Array;
|
|
547
|
+
let postponed: unknown;
|
|
548
|
+
try {
|
|
549
|
+
const result = await prerenderPromise;
|
|
550
|
+
prelude = await readStreamToUint8Array(result.prelude);
|
|
551
|
+
postponed = result.postponed;
|
|
552
|
+
} catch (error) {
|
|
553
|
+
// Identity match: swallow ONLY our own deliberate abort
|
|
554
|
+
// (error === abortReason). Not error.name — capture aborts before this
|
|
555
|
+
// await, so signal.aborted is always true, and a name check let a
|
|
556
|
+
// genuine AbortError-named throw masquerade as our abort and degrade
|
|
557
|
+
// into a retryable no-shell, hiding real failures from reportCacheError.
|
|
558
|
+
if (error === abortReason) {
|
|
559
|
+
return null;
|
|
369
560
|
}
|
|
561
|
+
throw error;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Sanity gate: a prelude with no `<body` is the no-shell failure mode.
|
|
565
|
+
// Return null and store nothing; the request falls back to axis 1 and a
|
|
566
|
+
// later request re-captures. The dominant real-world cause is a loader
|
|
567
|
+
// route WITHOUT a route-level loading() boundary: renderSegments' loading-
|
|
568
|
+
// less branch awaits loader data at TREE-BUILD, so the masked loader pins
|
|
569
|
+
// the whole tree above <body> (root postpone). Root-postponing layouts and
|
|
570
|
+
// hung handles degrade the same way. shell-capture.ts logs a once-per-key
|
|
571
|
+
// warning so the eternal-MISS shape is diagnosable.
|
|
572
|
+
if (!new TextDecoder().decode(prelude).includes("<body")) {
|
|
573
|
+
return null;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
return {
|
|
577
|
+
prelude,
|
|
578
|
+
postponed: postponed == null ? null : JSON.stringify(postponed),
|
|
579
|
+
};
|
|
580
|
+
} finally {
|
|
581
|
+
deadline.cancel();
|
|
582
|
+
}
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Create the PPR shell resume handler.
|
|
588
|
+
*
|
|
589
|
+
* resumeShellHTML produces the per-request live portion of the document: for a
|
|
590
|
+
* postponed shell it resumes fizz over a fresh SsrRoot to emit only the holes;
|
|
591
|
+
* for the DATA variant it emits only the fresh Flight payload scripts. The
|
|
592
|
+
* caller (shell-cache middleware) prepends the stored prelude bytes to form the
|
|
593
|
+
* composite response. See docs/design/ppr-shell-resume.md.
|
|
594
|
+
*/
|
|
595
|
+
export function createShellResumeHandler<TEnv = unknown>(
|
|
596
|
+
deps: SSRDependencies<TEnv>,
|
|
597
|
+
) {
|
|
598
|
+
const { createFromReadableStream, resume, onError } = deps;
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* @param rscStream - Fresh full Flight stream for this request.
|
|
602
|
+
* @param opts - postponed state (null = DATA variant) and optional nonce.
|
|
603
|
+
*/
|
|
604
|
+
return async function resumeShellHTML(
|
|
605
|
+
rscStream: ReadableStream<Uint8Array>,
|
|
606
|
+
opts: ShellResumeOptions,
|
|
607
|
+
): Promise<ReadableStream<Uint8Array>> {
|
|
608
|
+
const { postponed, nonce } = opts;
|
|
609
|
+
|
|
610
|
+
try {
|
|
611
|
+
if (postponed === null) {
|
|
612
|
+
// DATA variant: the stored prelude is the complete shell. No fizz runs;
|
|
613
|
+
// the eager injector pumps the fresh Flight payload scripts without
|
|
614
|
+
// needing an HTML chunk to trigger it (the stock injector deadlocked on
|
|
615
|
+
// a chunkless stream — see createDataVariantHtmlStream, kept for the
|
|
616
|
+
// batching invariant's sake).
|
|
617
|
+
return createDataVariantHtmlStream().pipeThrough(
|
|
618
|
+
injectRSCPayloadEager(rscStream, { nonce }),
|
|
619
|
+
);
|
|
370
620
|
}
|
|
621
|
+
|
|
622
|
+
if (!resume) {
|
|
623
|
+
throw new Error(
|
|
624
|
+
"[createShellResumeHandler] Missing `resume` dependency (react-dom/server.edge). " +
|
|
625
|
+
"Resuming a postponed shell requires the resume export; wire it in the SSR virtual entry.",
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// Tee: one branch deserializes into the SsrRoot VDOM that resume() replays
|
|
630
|
+
// (a fresh instance is fine — replay matches structure, not identity), the
|
|
631
|
+
// other feeds the fresh hydration payload to injectRSCPayload.
|
|
632
|
+
const [rscStream1, rscStream2] = rscStream.tee();
|
|
633
|
+
|
|
634
|
+
const SsrRoot = createSsrRootComponent({
|
|
635
|
+
createFromReadableStream,
|
|
636
|
+
rscStream: rscStream1,
|
|
637
|
+
nonce,
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
const resumed = await resume(<SsrRoot />, JSON.parse(postponed), {
|
|
641
|
+
onError: (error) => reportRenderError(onError, error),
|
|
642
|
+
nonce,
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
// EAGER injection (resume-only): the stored prelude — a complete document
|
|
646
|
+
// through </body></html> — is already on the wire ahead of this stream,
|
|
647
|
+
// so a Flight <script> is valid as the first tail byte. The stock
|
|
648
|
+
// injector waits for the first fizz chunk, which only appears when the
|
|
649
|
+
// first hole's loaders resolve — parking the whole hydration payload
|
|
650
|
+
// (root row included) behind the slowest live loader. See
|
|
651
|
+
// inject-rsc-eager.ts for the measured failure mode.
|
|
652
|
+
return resumed.pipeThrough(injectRSCPayloadEager(rscStream2, { nonce }));
|
|
653
|
+
} catch (error) {
|
|
654
|
+
reportRenderError(onError, error);
|
|
371
655
|
throw error;
|
|
372
656
|
}
|
|
373
657
|
};
|