@samitouri / QOS-React / commits / a960b92cb9

[Flight] model halting as never delivered chunks (#30740)

stacked on: #30731 We've refined the model of halting a prerender. Now when you abort during a prerender we simply omit the rows that would complete the flight render. This is analagous to prerendering in Fizz where you must resume the prerender to actually result in errors propagating in the postponed holes. We don't have a resume yet for flight and it's not entirely clear how that will work however the key insight here is that deciding whether the never resolving rows are an error or not should really be done on the consuming side rather than in the producer. This PR also reintroduces the logs for the abort error/postpone when prerendering which will give you some indication that something wasn't finished when the prerender was aborted.

Josh Story committed Aug 19, 2024 at 19:34 UTC a960b92cb93e7d006e5e8de850f9b8b51f655c90
13 files changed +253 -283
packages/react-client/src/ReactFlightClient.js
-22
@@ -46,7 +46,6 @@ import {
46 enableRefAsProp,
47 enableFlightReadableStream,
48 enableOwnerStacks,
49 - enableHalt,
49 } from 'shared/ReactFeatureFlags';
50
51 import {
@@ -1997,20 +1996,6 @@ function resolvePostponeDev(
1996 }
1997 }
1998
2000 -function resolveBlocked(response: Response, id: number): void {
2001 - const chunks = response._chunks;
2002 - const chunk = chunks.get(id);
2003 - if (!chunk) {
2004 - chunks.set(id, createBlockedChunk(response));
2005 - } else if (chunk.status === PENDING) {
2006 - // This chunk as contructed via other means but it is actually a blocked chunk
2007 - // so we update it here. We check the status because it might have been aborted
2008 - // before we attempted to resolve it.
2009 - const blockedChunk: BlockedChunk<mixed> = (chunk: any);
2010 - blockedChunk.status = BLOCKED;
2011 - }
2012 -}
2013 -
1999 function resolveHint<Code: HintCode>(
2000 response: Response,
2001 code: Code,
@@ -2637,13 +2622,6 @@ function processFullStringRow(
2622 }
2623 }
2624 // Fallthrough
2640 - case 35 /* "#" */: {
2641 - if (enableHalt) {
2642 - resolveBlocked(response, id);
2643 - return;
2644 - }
2645 - }
2646 - // Fallthrough
2625 default: /* """ "{" "[" "t" "f" "n" "0" - "9" */ {
2626 // We assume anything else is JSON.
2627 resolveModel(response, id, row);
packages/react-server-dom-esm/src/server/ReactFlightDOMServerNode.js
+6 -16
@@ -20,15 +20,13 @@ import type {Thenable} from 'shared/ReactTypes';
20
21 import {Readable} from 'stream';
22
23 -import {enableHalt} from 'shared/ReactFeatureFlags';
24 -
23 import {
24 createRequest,
25 + createPrerenderRequest,
26 startWork,
27 startFlowing,
28 stopFlowing,
29 abort,
31 - halt,
30 } from 'react-server/src/ReactFlightServer';
31
32 import {
@@ -175,35 +173,27 @@ function prerenderToNodeStream(
173 resolve({prelude: readable});
174 }
175
178 - const request = createRequest(
176 + const request = createPrerenderRequest(
177 model,
178 moduleBasePath,
179 + onAllReady,
180 + onFatalError,
181 options ? options.onError : undefined,
182 options ? options.identifierPrefix : undefined,
183 options ? options.onPostpone : undefined,
184 options ? options.temporaryReferences : undefined,
185 __DEV__ && options ? options.environmentName : undefined,
186 __DEV__ && options ? options.filterStackFrame : undefined,
187 - onAllReady,
188 - onFatalError,
187 );
188 if (options && options.signal) {
189 const signal = options.signal;
190 if (signal.aborted) {
191 const reason = (signal: any).reason;
194 - if (enableHalt) {
195 - halt(request, reason);
196 - } else {
197 - abort(request, reason);
198 - }
192 + abort(request, reason);
193 } else {
194 const listener = () => {
195 const reason = (signal: any).reason;
202 - if (enableHalt) {
203 - halt(request, reason);
204 - } else {
205 - abort(request, reason);
206 - }
196 + abort(request, reason);
197 signal.removeEventListener('abort', listener);
198 };
199 signal.addEventListener('abort', listener);
packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerBrowser.js
+6 -16
@@ -12,15 +12,13 @@ import type {Thenable} from 'shared/ReactTypes';
12 import type {ClientManifest} from './ReactFlightServerConfigTurbopackBundler';
13 import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig';
14
15 -import {enableHalt} from 'shared/ReactFeatureFlags';
16 -
15 import {
16 createRequest,
17 + createPrerenderRequest,
18 startWork,
19 startFlowing,
20 stopFlowing,
21 abort,
23 - halt,
22 } from 'react-server/src/ReactFlightServer';
23
24 import {
@@ -134,35 +132,27 @@ function prerender(
132 );
133 resolve({prelude: stream});
134 }
137 - const request = createRequest(
135 + const request = createPrerenderRequest(
136 model,
137 turbopackMap,
138 + onAllReady,
139 + onFatalError,
140 options ? options.onError : undefined,
141 options ? options.identifierPrefix : undefined,
142 options ? options.onPostpone : undefined,
143 options ? options.temporaryReferences : undefined,
144 __DEV__ && options ? options.environmentName : undefined,
145 __DEV__ && options ? options.filterStackFrame : undefined,
146 - onAllReady,
147 - onFatalError,
146 );
147 if (options && options.signal) {
148 const signal = options.signal;
149 if (signal.aborted) {
150 const reason = (signal: any).reason;
153 - if (enableHalt) {
154 - halt(request, reason);
155 - } else {
156 - abort(request, reason);
157 - }
151 + abort(request, reason);
152 } else {
153 const listener = () => {
154 const reason = (signal: any).reason;
161 - if (enableHalt) {
162 - halt(request, reason);
163 - } else {
164 - abort(request, reason);
165 - }
155 + abort(request, reason);
156 signal.removeEventListener('abort', listener);
157 };
158 signal.addEventListener('abort', listener);
packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerEdge.js
+6 -16
@@ -12,15 +12,13 @@ import type {Thenable} from 'shared/ReactTypes';
12 import type {ClientManifest} from './ReactFlightServerConfigTurbopackBundler';
13 import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig';
14
15 -import {enableHalt} from 'shared/ReactFeatureFlags';
16 -
15 import {
16 createRequest,
17 + createPrerenderRequest,
18 startWork,
19 startFlowing,
20 stopFlowing,
21 abort,
23 - halt,
22 } from 'react-server/src/ReactFlightServer';
23
24 import {
@@ -134,35 +132,27 @@ function prerender(
132 );
133 resolve({prelude: stream});
134 }
137 - const request = createRequest(
135 + const request = createPrerenderRequest(
136 model,
137 turbopackMap,
138 + onAllReady,
139 + onFatalError,
140 options ? options.onError : undefined,
141 options ? options.identifierPrefix : undefined,
142 options ? options.onPostpone : undefined,
143 options ? options.temporaryReferences : undefined,
144 __DEV__ && options ? options.environmentName : undefined,
145 __DEV__ && options ? options.filterStackFrame : undefined,
146 - onAllReady,
147 - onFatalError,
146 );
147 if (options && options.signal) {
148 const signal = options.signal;
149 if (signal.aborted) {
150 const reason = (signal: any).reason;
153 - if (enableHalt) {
154 - halt(request, reason);
155 - } else {
156 - abort(request, reason);
157 - }
151 + abort(request, reason);
152 } else {
153 const listener = () => {
154 const reason = (signal: any).reason;
161 - if (enableHalt) {
162 - halt(request, reason);
163 - } else {
164 - abort(request, reason);
165 - }
155 + abort(request, reason);
156 signal.removeEventListener('abort', listener);
157 };
158 signal.addEventListener('abort', listener);
packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerNode.js
+6 -16
@@ -20,15 +20,13 @@ import type {Thenable} from 'shared/ReactTypes';
20
21 import {Readable} from 'stream';
22
23 -import {enableHalt} from 'shared/ReactFeatureFlags';
24 -
23 import {
24 createRequest,
25 + createPrerenderRequest,
26 startWork,
27 startFlowing,
28 stopFlowing,
29 abort,
31 - halt,
30 } from 'react-server/src/ReactFlightServer';
31
32 import {
@@ -177,35 +175,27 @@ function prerenderToNodeStream(
175 resolve({prelude: readable});
176 }
177
180 - const request = createRequest(
178 + const request = createPrerenderRequest(
179 model,
180 turbopackMap,
181 + onAllReady,
182 + onFatalError,
183 options ? options.onError : undefined,
184 options ? options.identifierPrefix : undefined,
185 options ? options.onPostpone : undefined,
186 options ? options.temporaryReferences : undefined,
187 __DEV__ && options ? options.environmentName : undefined,
188 __DEV__ && options ? options.filterStackFrame : undefined,
189 - onAllReady,
190 - onFatalError,
189 );
190 if (options && options.signal) {
191 const signal = options.signal;
192 if (signal.aborted) {
193 const reason = (signal: any).reason;
196 - if (enableHalt) {
197 - halt(request, reason);
198 - } else {
199 - abort(request, reason);
200 - }
194 + abort(request, reason);
195 } else {
196 const listener = () => {
197 const reason = (signal: any).reason;
204 - if (enableHalt) {
205 - halt(request, reason);
206 - } else {
207 - abort(request, reason);
208 - }
198 + abort(request, reason);
199 signal.removeEventListener('abort', listener);
200 };
201 signal.addEventListener('abort', listener);
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js
+14 -10
@@ -2724,7 +2724,7 @@ describe('ReactFlightDOM', () => {
2724 });
2725
2726 // @gate enableHalt
2727 - it('serializes unfinished tasks with infinite promises when aborting a prerender', async () => {
2727 + it('does not propagate abort reasons errors when aborting a prerender', async () => {
2728 let resolveGreeting;
2729 const greetingPromise = new Promise(resolve => {
2730 resolveGreeting = resolve;
@@ -2746,6 +2746,7 @@ describe('ReactFlightDOM', () => {
2746 }
2747
2748 const controller = new AbortController();
2749 + const errors = [];
2750 const {pendingResult} = await serverAct(async () => {
2751 // destructure trick to avoid the act scope from awaiting the returned value
2752 return {
@@ -2754,15 +2755,20 @@ describe('ReactFlightDOM', () => {
2755 webpackMap,
2756 {
2757 signal: controller.signal,
2758 + onError(err) {
2759 + errors.push(err);
2760 + },
2761 },
2762 ),
2763 };
2764 });
2765
2762 - controller.abort();
2766 + controller.abort('boom');
2767 resolveGreeting();
2768 const {prelude} = await pendingResult;
2769
2770 + expect(errors).toEqual(['boom']);
2771 +
2772 const preludeWeb = Readable.toWeb(prelude);
2773 const response = ReactServerDOMClient.createFromReadableStream(preludeWeb);
2774
@@ -2772,7 +2778,7 @@ describe('ReactFlightDOM', () => {
2778 return use(response);
2779 }
2780
2775 - const errors = [];
2781 + errors.length = 0;
2782 let abortFizz;
2783 await serverAct(async () => {
2784 const {pipe, abort} = ReactDOMFizzServer.renderToPipeableStream(
@@ -2788,10 +2794,10 @@ describe('ReactFlightDOM', () => {
2794 });
2795
2796 await serverAct(() => {
2791 - abortFizz('boom');
2797 + abortFizz('bam');
2798 });
2799
2794 - expect(errors).toEqual(['boom']);
2800 + expect(errors).toEqual(['bam']);
2801
2802 const container = document.createElement('div');
2803 await readInto(container, fizzReadable);
@@ -2861,7 +2867,7 @@ describe('ReactFlightDOM', () => {
2867 it('will halt unfinished chunks inside Suspense when aborting a prerender', async () => {
2868 const controller = new AbortController();
2869 function ComponentThatAborts() {
2864 - controller.abort();
2870 + controller.abort('boom');
2871 return null;
2872 }
2873
@@ -2912,11 +2918,8 @@ describe('ReactFlightDOM', () => {
2918 };
2919 });
2920
2915 - controller.abort();
2916 -
2921 const {prelude} = await pendingResult;
2918 - expect(errors).toEqual([]);
2919 -
2922 + expect(errors).toEqual(['boom']);
2923 const response = ReactServerDOMClient.createFromReadableStream(
2924 Readable.toWeb(prelude),
2925 );
@@ -2926,6 +2929,7 @@ describe('ReactFlightDOM', () => {
2929 function ClientApp() {
2930 return use(response);
2931 }
2932 + errors.length = 0;
2933 let abortFizz;
2934 await serverAct(async () => {
2935 const {pipe, abort} = ReactDOMFizzServer.renderToPipeableStream(
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js
+7 -2
@@ -2402,7 +2402,7 @@ describe('ReactFlightDOMBrowser', () => {
2402 });
2403
2404 // @gate enableHalt
2405 - it('serializes unfinished tasks with infinite promises when aborting a prerender', async () => {
2405 + it('does not propagate abort reasons errors when aborting a prerender', async () => {
2406 let resolveGreeting;
2407 const greetingPromise = new Promise(resolve => {
2408 resolveGreeting = resolve;
@@ -2424,6 +2424,7 @@ describe('ReactFlightDOMBrowser', () => {
2424 }
2425
2426 const controller = new AbortController();
2427 + const errors = [];
2428 const {pendingResult} = await serverAct(async () => {
2429 // destructure trick to avoid the act scope from awaiting the returned value
2430 return {
@@ -2432,14 +2433,18 @@ describe('ReactFlightDOMBrowser', () => {
2433 webpackMap,
2434 {
2435 signal: controller.signal,
2436 + onError(err) {
2437 + errors.push(err);
2438 + },
2439 },
2440 ),
2441 };
2442 });
2443
2440 - controller.abort();
2444 + controller.abort('boom');
2445 resolveGreeting();
2446 const {prelude} = await pendingResult;
2447 + expect(errors).toEqual(['boom']);
2448
2449 function ClientRoot({response}) {
2450 return use(response);
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js
+11 -5
@@ -1103,7 +1103,7 @@ describe('ReactFlightDOMEdge', () => {
1103 });
1104
1105 // @gate enableHalt
1106 - it('serializes unfinished tasks with infinite promises when aborting a prerender', async () => {
1106 + it('does not propagate abort reasons errors when aborting a prerender', async () => {
1107 let resolveGreeting;
1108 const greetingPromise = new Promise(resolve => {
1109 resolveGreeting = resolve;
@@ -1125,6 +1125,7 @@ describe('ReactFlightDOMEdge', () => {
1125 }
1126
1127 const controller = new AbortController();
1128 + const errors = [];
1129 const {pendingResult} = await serverAct(async () => {
1130 // destructure trick to avoid the act scope from awaiting the returned value
1131 return {
@@ -1133,15 +1134,20 @@ describe('ReactFlightDOMEdge', () => {
1134 webpackMap,
1135 {
1136 signal: controller.signal,
1137 + onError(err) {
1138 + errors.push(err);
1139 + },
1140 },
1141 ),
1142 };
1143 });
1144
1141 - controller.abort();
1145 + controller.abort('boom');
1146 resolveGreeting();
1147 const {prelude} = await pendingResult;
1148
1149 + expect(errors).toEqual(['boom']);
1150 +
1151 function ClientRoot({response}) {
1152 return use(response);
1153 }
@@ -1153,7 +1159,7 @@ describe('ReactFlightDOMEdge', () => {
1159 },
1160 });
1161 const fizzController = new AbortController();
1156 - const errors = [];
1162 + errors.length = 0;
1163 const ssrStream = await serverAct(() =>
1164 ReactDOMServer.renderToReadableStream(
1165 React.createElement(ClientRoot, {response}),
@@ -1165,8 +1171,8 @@ describe('ReactFlightDOMEdge', () => {
1171 },
1172 ),
1173 );
1168 - fizzController.abort('boom');
1169 - expect(errors).toEqual(['boom']);
1174 + fizzController.abort('bam');
1175 + expect(errors).toEqual(['bam']);
1176 // Should still match the result when parsed
1177 const result = await readResult(ssrStream);
1178 const div = document.createElement('div');
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js
+10 -5
@@ -443,7 +443,7 @@ describe('ReactFlightDOMNode', () => {
443 });
444
445 // @gate enableHalt
446 - it('serializes unfinished tasks with infinite promises when aborting a prerender', async () => {
446 + it('does not propagate abort reasons errors when aborting a prerender', async () => {
447 let resolveGreeting;
448 const greetingPromise = new Promise(resolve => {
449 resolveGreeting = resolve;
@@ -465,6 +465,7 @@ describe('ReactFlightDOMNode', () => {
465 }
466
467 const controller = new AbortController();
468 + const errors = [];
469 const {pendingResult} = await serverAct(async () => {
470 // destructure trick to avoid the act scope from awaiting the returned value
471 return {
@@ -473,14 +474,18 @@ describe('ReactFlightDOMNode', () => {
474 webpackMap,
475 {
476 signal: controller.signal,
477 + onError(err) {
478 + errors.push(err);
479 + },
480 },
481 ),
482 };
483 });
484
481 - controller.abort();
485 + controller.abort('boom');
486 resolveGreeting();
487 const {prelude} = await pendingResult;
488 + expect(errors).toEqual(['boom']);
489
490 function ClientRoot({response}) {
491 return use(response);
@@ -492,7 +497,7 @@ describe('ReactFlightDOMNode', () => {
497 moduleLoading: null,
498 },
499 });
495 - const errors = [];
500 + errors.length = 0;
501 const ssrStream = await serverAct(() =>
502 ReactDOMServer.renderToPipeableStream(
503 React.createElement(ClientRoot, {response}),
@@ -503,8 +508,8 @@ describe('ReactFlightDOMNode', () => {
508 },
509 ),
510 );
506 - ssrStream.abort('boom');
507 - expect(errors).toEqual(['boom']);
511 + ssrStream.abort('bam');
512 + expect(errors).toEqual(['bam']);
513 // Should still match the result when parsed
514 const result = await readResult(ssrStream);
515 const div = document.createElement('div');
packages/react-server-dom-webpack/src/server/ReactFlightDOMServerBrowser.js
+6 -16
@@ -12,15 +12,13 @@ import type {Thenable} from 'shared/ReactTypes';
12 import type {ClientManifest} from './ReactFlightServerConfigWebpackBundler';
13 import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig';
14
15 -import {enableHalt} from 'shared/ReactFeatureFlags';
16 -
15 import {
16 createRequest,
17 + createPrerenderRequest,
18 startWork,
19 startFlowing,
20 stopFlowing,
21 abort,
23 - halt,
22 } from 'react-server/src/ReactFlightServer';
23
24 import {
@@ -134,35 +132,27 @@ function prerender(
132 );
133 resolve({prelude: stream});
134 }
137 - const request = createRequest(
135 + const request = createPrerenderRequest(
136 model,
137 webpackMap,
138 + onAllReady,
139 + onFatalError,
140 options ? options.onError : undefined,
141 options ? options.identifierPrefix : undefined,
142 options ? options.onPostpone : undefined,
143 options ? options.temporaryReferences : undefined,
144 __DEV__ && options ? options.environmentName : undefined,
145 __DEV__ && options ? options.filterStackFrame : undefined,
146 - onAllReady,
147 - onFatalError,
146 );
147 if (options && options.signal) {
148 const signal = options.signal;
149 if (signal.aborted) {
150 const reason = (signal: any).reason;
153 - if (enableHalt) {
154 - halt(request, reason);
155 - } else {
156 - abort(request, reason);
157 - }
151 + abort(request, reason);
152 } else {
153 const listener = () => {
154 const reason = (signal: any).reason;
161 - if (enableHalt) {
162 - halt(request, reason);
163 - } else {
164 - abort(request, reason);
165 - }
155 + abort(request, reason);
156 signal.removeEventListener('abort', listener);
157 };
158 signal.addEventListener('abort', listener);
packages/react-server-dom-webpack/src/server/ReactFlightDOMServerEdge.js
+6 -16
@@ -12,15 +12,13 @@ import type {Thenable} from 'shared/ReactTypes';
12 import type {ClientManifest} from './ReactFlightServerConfigWebpackBundler';
13 import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig';
14
15 -import {enableHalt} from 'shared/ReactFeatureFlags';
16 -
15 import {
16 createRequest,
17 + createPrerenderRequest,
18 startWork,
19 startFlowing,
20 stopFlowing,
21 abort,
23 - halt,
22 } from 'react-server/src/ReactFlightServer';
23
24 import {
@@ -134,35 +132,27 @@ function prerender(
132 );
133 resolve({prelude: stream});
134 }
137 - const request = createRequest(
135 + const request = createPrerenderRequest(
136 model,
137 webpackMap,
138 + onAllReady,
139 + onFatalError,
140 options ? options.onError : undefined,
141 options ? options.identifierPrefix : undefined,
142 options ? options.onPostpone : undefined,
143 options ? options.temporaryReferences : undefined,
144 __DEV__ && options ? options.environmentName : undefined,
145 __DEV__ && options ? options.filterStackFrame : undefined,
146 - onAllReady,
147 - onFatalError,
146 );
147 if (options && options.signal) {
148 const signal = options.signal;
149 if (signal.aborted) {
150 const reason = (signal: any).reason;
153 - if (enableHalt) {
154 - halt(request, reason);
155 - } else {
156 - abort(request, reason);
157 - }
151 + abort(request, reason);
152 } else {
153 const listener = () => {
154 const reason = (signal: any).reason;
161 - if (enableHalt) {
162 - halt(request, reason);
163 - } else {
164 - abort(request, reason);
165 - }
155 + abort(request, reason);
156 signal.removeEventListener('abort', listener);
157 };
158 signal.addEventListener('abort', listener);
packages/react-server-dom-webpack/src/server/ReactFlightDOMServerNode.js
+6 -16
@@ -20,15 +20,13 @@ import type {Thenable} from 'shared/ReactTypes';
20
21 import {Readable} from 'stream';
22
23 -import {enableHalt} from 'shared/ReactFeatureFlags';
24 -
23 import {
24 createRequest,
25 + createPrerenderRequest,
26 startWork,
27 startFlowing,
28 stopFlowing,
29 abort,
31 - halt,
30 } from 'react-server/src/ReactFlightServer';
31
32 import {
@@ -177,35 +175,27 @@ function prerenderToNodeStream(
175 resolve({prelude: readable});
176 }
177
180 - const request = createRequest(
178 + const request = createPrerenderRequest(
179 model,
180 webpackMap,
181 + onAllReady,
182 + onFatalError,
183 options ? options.onError : undefined,
184 options ? options.identifierPrefix : undefined,
185 options ? options.onPostpone : undefined,
186 options ? options.temporaryReferences : undefined,
187 __DEV__ && options ? options.environmentName : undefined,
188 __DEV__ && options ? options.filterStackFrame : undefined,
189 - onAllReady,
190 - onFatalError,
189 );
190 if (options && options.signal) {
191 const signal = options.signal;
192 if (signal.aborted) {
193 const reason = (signal: any).reason;
196 - if (enableHalt) {
197 - halt(request, reason);
198 - } else {
199 - abort(request, reason);
200 - }
194 + abort(request, reason);
195 } else {
196 const listener = () => {
197 const reason = (signal: any).reason;
204 - if (enableHalt) {
205 - halt(request, reason);
206 - } else {
207 - abort(request, reason);
208 - }
198 + abort(request, reason);
199 signal.removeEventListener('abort', listener);
200 };
201 signal.addEventListener('abort', listener);
packages/react-server/src/ReactFlightServer.js
+169 -127
@@ -353,7 +353,8 @@ type Task = {
353 interface Reference {}
354
355 export type Request = {
356 - status: 0 | 1 | 2 | 3,
356 + status: 10 | 11 | 12 | 13,
357 + type: 20 | 21,
358 flushScheduled: boolean,
359 fatalError: mixed,
360 destination: null | Destination,
@@ -425,13 +426,17 @@ function defaultPostponeHandler(reason: string) {
426 // Noop
427 }
428
428 -const OPEN = 0;
429 -const ABORTING = 1;
430 -const CLOSING = 2;
431 -const CLOSED = 3;
429 +const OPEN = 10;
430 +const ABORTING = 11;
431 +const CLOSING = 12;
432 +const CLOSED = 13;
433 +
434 +const RENDER = 20;
435 +const PRERENDER = 21;
436
437 function RequestInstance(
438 this: $FlowFixMe,
439 + type: 20 | 21,
440 model: ReactClientValue,
441 bundlerConfig: ClientManifest,
442 onError: void | ((error: mixed) => ?string),
@@ -440,8 +445,8 @@ function RequestInstance(
445 temporaryReferences: void | TemporaryReferenceSet,
446 environmentName: void | string | (() => string), // DEV-only
447 filterStackFrame: void | ((url: string, functionName: string) => boolean), // DEV-only
443 - onAllReady: void | (() => void),
444 - onFatalError: void | ((error: mixed) => void),
448 + onAllReady: () => void,
449 + onFatalError: (error: mixed) => void,
450 ) {
451 if (
452 ReactSharedInternals.A !== null &&
@@ -466,6 +471,7 @@ function RequestInstance(
471 TaintRegistryPendingRequests.add(cleanupQueue);
472 }
473 const hints = createHints();
474 + this.type = type;
475 this.status = OPEN;
476 this.flushScheduled = false;
477 this.fatalError = null;
@@ -493,8 +499,8 @@ function RequestInstance(
499 this.onError = onError === undefined ? defaultErrorHandler : onError;
500 this.onPostpone =
501 onPostpone === undefined ? defaultPostponeHandler : onPostpone;
496 - this.onAllReady = onAllReady === undefined ? noop : onAllReady;
497 - this.onFatalError = onFatalError === undefined ? noop : onFatalError;
502 + this.onAllReady = onAllReady;
503 + this.onFatalError = onFatalError;
504
505 if (__DEV__) {
506 this.environmentName =
@@ -522,7 +528,7 @@ function RequestInstance(
528 pingedTasks.push(rootTask);
529 }
530
525 -function noop(): void {}
531 +function noop() {}
532
533 export function createRequest(
534 model: ReactClientValue,
@@ -533,11 +539,38 @@ export function createRequest(
539 temporaryReferences: void | TemporaryReferenceSet,
540 environmentName: void | string | (() => string), // DEV-only
541 filterStackFrame: void | ((url: string, functionName: string) => boolean), // DEV-only
536 - onAllReady: void | (() => void),
537 - onFatalError: void | (() => void),
542 ): Request {
543 // $FlowFixMe[invalid-constructor]: the shapes are exact here but Flow doesn't like constructors
544 return new RequestInstance(
545 + RENDER,
546 + model,
547 + bundlerConfig,
548 + onError,
549 + identifierPrefix,
550 + onPostpone,
551 + temporaryReferences,
552 + environmentName,
553 + filterStackFrame,
554 + noop,
555 + noop,
556 + );
557 +}
558 +
559 +export function createPrerenderRequest(
560 + model: ReactClientValue,
561 + bundlerConfig: ClientManifest,
562 + onAllReady: () => void,
563 + onFatalError: () => void,
564 + onError: void | ((error: mixed) => ?string),
565 + identifierPrefix?: string,
566 + onPostpone: void | ((reason: string) => void),
567 + temporaryReferences: void | TemporaryReferenceSet,
568 + environmentName: void | string | (() => string), // DEV-only
569 + filterStackFrame: void | ((url: string, functionName: string) => boolean), // DEV-only
570 +): Request {
571 + // $FlowFixMe[invalid-constructor]: the shapes are exact here but Flow doesn't like constructors
572 + return new RequestInstance(
573 + PRERENDER,
574 model,
575 bundlerConfig,
576 onError,
@@ -616,13 +649,9 @@ function serializeThenable(
649 // We can no longer accept any resolved values
650 request.abortableTasks.delete(newTask);
651 newTask.status = ABORTED;
619 - if (enableHalt && request.fatalError === haltSymbol) {
620 - emitBlockedChunk(request, newTask.id);
621 - } else {
622 - const errorId: number = (request.fatalError: any);
623 - const model = stringify(serializeByValueID(errorId));
624 - emitModelChunk(request, newTask.id, model);
625 - }
652 + const errorId: number = (request.fatalError: any);
653 + const model = stringify(serializeByValueID(errorId));
654 + emitModelChunk(request, newTask.id, model);
655 return newTask.id;
656 }
657 if (typeof thenable.status === 'string') {
@@ -732,7 +761,7 @@ function serializeReadableStream(
761 }
762
763 if (entry.done) {
735 - request.abortListeners.delete(error);
764 + request.abortListeners.delete(abortStream);
765 const endStreamRow = streamTask.id.toString(16) + ':C\n';
766 request.completedRegularChunks.push(stringToChunk(endStreamRow));
767 enqueueFlush(request);
@@ -754,34 +783,49 @@ function serializeReadableStream(
783 return;
784 }
785 aborted = true;
757 - request.abortListeners.delete(error);
786 + request.abortListeners.delete(abortStream);
787 + const digest = logRecoverableError(request, reason, streamTask);
788 + emitErrorChunk(request, streamTask.id, digest, reason);
789 + enqueueFlush(request);
790
759 - let cancelWith: mixed;
760 - if (enableHalt && request.fatalError === haltSymbol) {
761 - cancelWith = reason;
762 - } else if (
791 + // $FlowFixMe should be able to pass mixed
792 + reader.cancel(reason).then(error, error);
793 + }
794 + function abortStream(reason: mixed) {
795 + if (aborted) {
796 + return;
797 + }
798 + aborted = true;
799 + request.abortListeners.delete(abortStream);
800 + if (
801 enablePostpone &&
802 typeof reason === 'object' &&
803 reason !== null &&
804 (reason: any).$$typeof === REACT_POSTPONE_TYPE
805 ) {
768 - cancelWith = reason;
806 const postponeInstance: Postpone = (reason: any);
807 logPostpone(request, postponeInstance.message, streamTask);
771 - emitPostponeChunk(request, streamTask.id, postponeInstance);
772 - enqueueFlush(request);
808 + if (enableHalt && request.type === PRERENDER) {
809 + request.pendingChunks--;
810 + } else {
811 + emitPostponeChunk(request, streamTask.id, postponeInstance);
812 + enqueueFlush(request);
813 + }
814 } else {
774 - cancelWith = reason;
815 const digest = logRecoverableError(request, reason, streamTask);
776 - emitErrorChunk(request, streamTask.id, digest, reason);
777 - enqueueFlush(request);
816 + if (enableHalt && request.type === PRERENDER) {
817 + request.pendingChunks--;
818 + } else {
819 + emitErrorChunk(request, streamTask.id, digest, reason);
820 + enqueueFlush(request);
821 + }
822 }
823
824 // $FlowFixMe should be able to pass mixed
781 - reader.cancel(cancelWith).then(error, error);
825 + reader.cancel(reason).then(error, error);
826 }
827
784 - request.abortListeners.add(error);
828 + request.abortListeners.add(abortStream);
829 reader.read().then(progress, error);
830 return serializeByValueID(streamTask.id);
831 }
@@ -837,7 +881,7 @@ function serializeAsyncIterable(
881 }
882
883 if (entry.done) {
840 - request.abortListeners.delete(error);
884 + request.abortListeners.delete(abortIterable);
885 let endStreamRow;
886 if (entry.value === undefined) {
887 endStreamRow = streamTask.id.toString(16) + ':C\n';
@@ -881,34 +925,52 @@ function serializeAsyncIterable(
925 return;
926 }
927 aborted = true;
884 - request.abortListeners.delete(error);
885 - let throwWith: mixed;
886 - if (enableHalt && request.fatalError === haltSymbol) {
887 - throwWith = reason;
888 - } else if (
928 + request.abortListeners.delete(abortIterable);
929 + const digest = logRecoverableError(request, reason, streamTask);
930 + emitErrorChunk(request, streamTask.id, digest, reason);
931 + enqueueFlush(request);
932 + if (typeof (iterator: any).throw === 'function') {
933 + // The iterator protocol doesn't necessarily include this but a generator do.
934 + // $FlowFixMe should be able to pass mixed
935 + iterator.throw(reason).then(error, error);
936 + }
937 + }
938 + function abortIterable(reason: mixed) {
939 + if (aborted) {
940 + return;
941 + }
942 + aborted = true;
943 + request.abortListeners.delete(abortIterable);
944 + if (
945 enablePostpone &&
946 typeof reason === 'object' &&
947 reason !== null &&
948 (reason: any).$$typeof === REACT_POSTPONE_TYPE
949 ) {
894 - throwWith = reason;
950 const postponeInstance: Postpone = (reason: any);
951 logPostpone(request, postponeInstance.message, streamTask);
897 - emitPostponeChunk(request, streamTask.id, postponeInstance);
898 - enqueueFlush(request);
952 + if (enableHalt && request.type === PRERENDER) {
953 + request.pendingChunks--;
954 + } else {
955 + emitPostponeChunk(request, streamTask.id, postponeInstance);
956 + enqueueFlush(request);
957 + }
958 } else {
900 - throwWith = reason;
959 const digest = logRecoverableError(request, reason, streamTask);
902 - emitErrorChunk(request, streamTask.id, digest, reason);
903 - enqueueFlush(request);
960 + if (enableHalt && request.type === PRERENDER) {
961 + request.pendingChunks--;
962 + } else {
963 + emitErrorChunk(request, streamTask.id, digest, reason);
964 + enqueueFlush(request);
965 + }
966 }
967 if (typeof (iterator: any).throw === 'function') {
968 // The iterator protocol doesn't necessarily include this but a generator do.
969 // $FlowFixMe should be able to pass mixed
908 - iterator.throw(throwWith).then(error, error);
970 + iterator.throw(reason).then(error, error);
971 }
972 }
911 - request.abortListeners.add(error);
973 + request.abortListeners.add(abortIterable);
974 if (__DEV__) {
975 callIteratorInDEV(iterator, progress, error);
976 } else {
@@ -2101,7 +2163,7 @@ function serializeBlob(request: Request, blob: Blob): string {
2163 return;
2164 }
2165 if (entry.done) {
2104 - request.abortListeners.delete(error);
2166 + request.abortListeners.delete(abortBlob);
2167 aborted = true;
2168 pingTask(request, newTask);
2169 return;
@@ -2111,28 +2173,52 @@ function serializeBlob(request: Request, blob: Blob): string {
2173 // $FlowFixMe[incompatible-call]
2174 return reader.read().then(progress).catch(error);
2175 }
2114 -
2176 function error(reason: mixed) {
2177 if (aborted) {
2178 return;
2179 }
2180 aborted = true;
2120 - request.abortListeners.delete(error);
2121 - let cancelWith: mixed;
2122 - if (enableHalt && request.fatalError === haltSymbol) {
2123 - cancelWith = reason;
2181 + request.abortListeners.delete(abortBlob);
2182 + const digest = logRecoverableError(request, reason, newTask);
2183 + emitErrorChunk(request, newTask.id, digest, reason);
2184 + enqueueFlush(request);
2185 + // $FlowFixMe should be able to pass mixed
2186 + reader.cancel(reason).then(error, error);
2187 + }
2188 + function abortBlob(reason: mixed) {
2189 + if (aborted) {
2190 + return;
2191 + }
2192 + aborted = true;
2193 + request.abortListeners.delete(abortBlob);
2194 + if (
2195 + enablePostpone &&
2196 + typeof reason === 'object' &&
2197 + reason !== null &&
2198 + (reason: any).$$typeof === REACT_POSTPONE_TYPE
2199 + ) {
2200 + const postponeInstance: Postpone = (reason: any);
2201 + logPostpone(request, postponeInstance.message, newTask);
2202 + if (enableHalt && request.type === PRERENDER) {
2203 + request.pendingChunks--;
2204 + } else {
2205 + emitPostponeChunk(request, newTask.id, postponeInstance);
2206 + enqueueFlush(request);
2207 + }
2208 } else {
2125 - cancelWith = reason;
2209 const digest = logRecoverableError(request, reason, newTask);
2127 - emitErrorChunk(request, newTask.id, digest, reason);
2128 - request.abortableTasks.delete(newTask);
2129 - enqueueFlush(request);
2210 + if (enableHalt && request.type === PRERENDER) {
2211 + request.pendingChunks--;
2212 + } else {
2213 + emitErrorChunk(request, newTask.id, digest, reason);
2214 + enqueueFlush(request);
2215 + }
2216 }
2217 // $FlowFixMe should be able to pass mixed
2132 - reader.cancel(cancelWith).then(error, error);
2218 + reader.cancel(reason).then(error, error);
2219 }
2220
2135 - request.abortListeners.add(error);
2221 + request.abortListeners.add(abortBlob);
2222
2223 // $FlowFixMe[incompatible-call]
2224 reader.read().then(progress).catch(error);
@@ -3001,12 +3087,6 @@ function emitPostponeChunk(
3087 request.completedErrorChunks.push(processedChunk);
3088 }
3089
3004 -function emitBlockedChunk(request: Request, id: number): void {
3005 - const row = serializeRowHeader('#', id) + '\n';
3006 - const processedChunk = stringToChunk(row);
3007 - request.completedErrorChunks.push(processedChunk);
3008 -}
3009 -
3090 function emitErrorChunk(
3091 request: Request,
3092 id: number,
@@ -3755,13 +3835,9 @@ function retryTask(request: Request, task: Task): void {
3835 if (request.status === ABORTING) {
3836 request.abortableTasks.delete(task);
3837 task.status = ABORTED;
3758 - if (enableHalt && request.fatalError === haltSymbol) {
3759 - emitBlockedChunk(request, task.id);
3760 - } else {
3761 - const errorId: number = (request.fatalError: any);
3762 - const model = stringify(serializeByValueID(errorId));
3763 - emitModelChunk(request, task.id, model);
3764 - }
3838 + const errorId: number = (request.fatalError: any);
3839 + const model = stringify(serializeByValueID(errorId));
3840 + emitModelChunk(request, task.id, model);
3841 return;
3842 }
3843 // Something suspended again, let's pick it back up later.
@@ -3783,13 +3859,9 @@ function retryTask(request: Request, task: Task): void {
3859 if (request.status === ABORTING) {
3860 request.abortableTasks.delete(task);
3861 task.status = ABORTED;
3786 - if (enableHalt && request.fatalError === haltSymbol) {
3787 - emitBlockedChunk(request, task.id);
3788 - } else {
3789 - const errorId: number = (request.fatalError: any);
3790 - const model = stringify(serializeByValueID(errorId));
3791 - emitModelChunk(request, task.id, model);
3792 - }
3862 + const errorId: number = (request.fatalError: any);
3863 + const model = stringify(serializeByValueID(errorId));
3864 + emitModelChunk(request, task.id, model);
3865 return;
3866 }
3867
@@ -3844,7 +3916,8 @@ function performWork(request: Request): void {
3916 // We can ping after completing but if this happens there already
3917 // wouldn't be any abortable tasks. So we only call allReady after
3918 // the work which actually completed the last pending task
3847 - allReady(request);
3919 + const onAllReady = request.onAllReady;
3920 + onAllReady();
3921 }
3922 } catch (error) {
3923 logRecoverableError(request, error, null);
@@ -4007,17 +4080,17 @@ export function stopFlowing(request: Request): void {
4080 request.destination = null;
4081 }
4082
4010 -// This is called to early terminate a request. It creates an error at all pending tasks.
4083 export function abort(request: Request, reason: mixed): void {
4084 try {
4085 if (request.status === OPEN) {
4086 request.status = ABORTING;
4087 }
4088 const abortableTasks = request.abortableTasks;
4017 - // We have tasks to abort. We'll emit one error row and then emit a reference
4018 - // to that row from every row that's still remaining.
4089 if (abortableTasks.size > 0) {
4020 - request.pendingChunks++;
4090 + // We have tasks to abort. We'll emit one error row and then emit a reference
4091 + // to that row from every row that's still remaining if we are rendering. If we
4092 + // are prerendering (and halt semantics are enabled) we will refer to an error row
4093 + // but not actually emit it so the reciever can at that point rather than error.
4094 const errorId = request.nextChunkId++;
4095 request.fatalError = errorId;
4096 if (
@@ -4028,7 +4101,11 @@ export function abort(request: Request, reason: mixed): void {
4101 ) {
4102 const postponeInstance: Postpone = (reason: any);
4103 logPostpone(request, postponeInstance.message, null);
4031 - emitPostponeChunk(request, errorId, postponeInstance);
4104 + if (!enableHalt || request.type === PRERENDER) {
4105 + // When prerendering with halt semantics we omit the referred to postpone.
4106 + request.pendingChunks++;
4107 + emitPostponeChunk(request, errorId, postponeInstance);
4108 + }
4109 } else {
4110 const error =
4111 reason === undefined
@@ -4043,11 +4120,16 @@ export function abort(request: Request, reason: mixed): void {
4120 )
4121 : reason;
4122 const digest = logRecoverableError(request, error, null);
4046 - emitErrorChunk(request, errorId, digest, error);
4123 + if (!enableHalt || request.type === RENDER) {
4124 + // When prerendering with halt semantics we omit the referred to error.
4125 + request.pendingChunks++;
4126 + emitErrorChunk(request, errorId, digest, error);
4127 + }
4128 }
4129 abortableTasks.forEach(task => abortTask(task, request, errorId));
4130 abortableTasks.clear();
4050 - allReady(request);
4131 + const onAllReady = request.onAllReady;
4132 + onAllReady();
4133 }
4134 const abortListeners = request.abortListeners;
4135 if (abortListeners.size > 0) {
@@ -4087,43 +4169,3 @@ export function abort(request: Request, reason: mixed): void {
4169 fatalError(request, error);
4170 }
4171 }
4090 -
4091 -const haltSymbol = Symbol('halt');
4092 -
4093 -// This is called to stop rendering without erroring. All unfinished work is represented Promises
4094 -// that never resolve.
4095 -export function halt(request: Request, reason: mixed): void {
4096 - try {
4097 - if (request.status === OPEN) {
4098 - request.status = ABORTING;
4099 - }
4100 - request.fatalError = haltSymbol;
4101 - const abortableTasks = request.abortableTasks;
4102 - // We have tasks to abort. We'll emit one error row and then emit a reference
4103 - // to that row from every row that's still remaining.
4104 - if (abortableTasks.size > 0) {
4105 - request.pendingChunks++;
4106 - const errorId = request.nextChunkId++;
4107 - emitBlockedChunk(request, errorId);
4108 - abortableTasks.forEach(task => abortTask(task, request, errorId));
4109 - abortableTasks.clear();
4110 - allReady(request);
4111 - }
4112 - const abortListeners = request.abortListeners;
4113 - if (abortListeners.size > 0) {
4114 - abortListeners.forEach(callback => callback(reason));
4115 - abortListeners.clear();
4116 - }
4117 - if (request.destination !== null) {
4118 - flushCompletedChunks(request, request.destination);
4119 - }
4120 - } catch (error) {
4121 - logRecoverableError(request, error, null);
4122 - fatalError(request, error);
4123 - }
4124 -}
4125 -
4126 -function allReady(request: Request) {
4127 - const onAllReady = request.onAllReady;
4128 - onAllReady();
4129 -}