@samitouri / QOS-React / commits / 7b41cdc093

[Flight][Static] Implement halting a prerender behind enableHalt (#30705)

enableHalt turns on a mode for flight prerenders where aborts are treated like infinitely stalled outcomes while still completing the prerender. For regular tasks we simply serialize the slot as a promise that never settles. For ReadableStream, Blob, and Async Iterators we just never advance the serialization so they remain unfinished when consumed on the client. When enableHalt is turned on aborts of prerenders will halt rather than error. The abort reason is forwarded to the upstream produces of the aforementioned async iterators, blobs, and ReadableStreams. In the future if we expose a signal that you can consume from within a render to cancel additional work the abort reason will also be forwarded there

Josh Story committed Aug 16, 2024 at 14:21 UTC 7b41cdc093c7a28a089e2c402cbe98cac68de509
19 files changed +855 -120
packages/react-server-dom-esm/src/server/ReactFlightDOMServerNode.js
+15 -2
@@ -20,12 +20,15 @@ import type {Thenable} from 'shared/ReactTypes';
20
21 import {Readable} from 'stream';
22
23 +import {enableHalt} from 'shared/ReactFeatureFlags';
24 +
25 import {
26 createRequest,
27 startWork,
28 startFlowing,
29 stopFlowing,
30 abort,
31 + halt,
32 } from 'react-server/src/ReactFlightServer';
33
34 import {
@@ -187,10 +190,20 @@ function prerenderToNodeStream(
190 if (options && options.signal) {
191 const signal = options.signal;
192 if (signal.aborted) {
190 - abort(request, (signal: any).reason);
193 + const reason = (signal: any).reason;
194 + if (enableHalt) {
195 + halt(request, reason);
196 + } else {
197 + abort(request, reason);
198 + }
199 } else {
200 const listener = () => {
193 - abort(request, (signal: any).reason);
201 + const reason = (signal: any).reason;
202 + if (enableHalt) {
203 + halt(request, reason);
204 + } else {
205 + abort(request, reason);
206 + }
207 signal.removeEventListener('abort', listener);
208 };
209 signal.addEventListener('abort', listener);
packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerBrowser.js
+15 -2
@@ -12,12 +12,15 @@ 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 +
17 import {
18 createRequest,
19 startWork,
20 startFlowing,
21 stopFlowing,
22 abort,
23 + halt,
24 } from 'react-server/src/ReactFlightServer';
25
26 import {
@@ -146,10 +149,20 @@ function prerender(
149 if (options && options.signal) {
150 const signal = options.signal;
151 if (signal.aborted) {
149 - abort(request, (signal: any).reason);
152 + const reason = (signal: any).reason;
153 + if (enableHalt) {
154 + halt(request, reason);
155 + } else {
156 + abort(request, reason);
157 + }
158 } else {
159 const listener = () => {
152 - abort(request, (signal: any).reason);
160 + const reason = (signal: any).reason;
161 + if (enableHalt) {
162 + halt(request, reason);
163 + } else {
164 + abort(request, reason);
165 + }
166 signal.removeEventListener('abort', listener);
167 };
168 signal.addEventListener('abort', listener);
packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerEdge.js
+15 -2
@@ -12,12 +12,15 @@ 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 +
17 import {
18 createRequest,
19 startWork,
20 startFlowing,
21 stopFlowing,
22 abort,
23 + halt,
24 } from 'react-server/src/ReactFlightServer';
25
26 import {
@@ -146,10 +149,20 @@ function prerender(
149 if (options && options.signal) {
150 const signal = options.signal;
151 if (signal.aborted) {
149 - abort(request, (signal: any).reason);
152 + const reason = (signal: any).reason;
153 + if (enableHalt) {
154 + halt(request, reason);
155 + } else {
156 + abort(request, reason);
157 + }
158 } else {
159 const listener = () => {
152 - abort(request, (signal: any).reason);
160 + const reason = (signal: any).reason;
161 + if (enableHalt) {
162 + halt(request, reason);
163 + } else {
164 + abort(request, reason);
165 + }
166 signal.removeEventListener('abort', listener);
167 };
168 signal.addEventListener('abort', listener);
packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerNode.js
+15 -2
@@ -20,12 +20,15 @@ import type {Thenable} from 'shared/ReactTypes';
20
21 import {Readable} from 'stream';
22
23 +import {enableHalt} from 'shared/ReactFeatureFlags';
24 +
25 import {
26 createRequest,
27 startWork,
28 startFlowing,
29 stopFlowing,
30 abort,
31 + halt,
32 } from 'react-server/src/ReactFlightServer';
33
34 import {
@@ -189,10 +192,20 @@ function prerenderToNodeStream(
192 if (options && options.signal) {
193 const signal = options.signal;
194 if (signal.aborted) {
192 - abort(request, (signal: any).reason);
195 + const reason = (signal: any).reason;
196 + if (enableHalt) {
197 + halt(request, reason);
198 + } else {
199 + abort(request, reason);
200 + }
201 } else {
202 const listener = () => {
195 - abort(request, (signal: any).reason);
203 + const reason = (signal: any).reason;
204 + if (enableHalt) {
205 + halt(request, reason);
206 + } else {
207 + abort(request, reason);
208 + }
209 signal.removeEventListener('abort', listener);
210 };
211 signal.addEventListener('abort', listener);
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js
+134
@@ -2722,4 +2722,138 @@ describe('ReactFlightDOM', () => {
2722 await readInto(container, fizzReadable);
2723 expect(getMeaningfulChildren(container)).toEqual(<div>hello world</div>);
2724 });
2725 +
2726 + // @gate enableHalt
2727 + it('serializes unfinished tasks with infinite promises when aborting a prerender', async () => {
2728 + let resolveGreeting;
2729 + const greetingPromise = new Promise(resolve => {
2730 + resolveGreeting = resolve;
2731 + });
2732 +
2733 + function App() {
2734 + return (
2735 + <div>
2736 + <Suspense fallback="loading...">
2737 + <Greeting />
2738 + </Suspense>
2739 + </div>
2740 + );
2741 + }
2742 +
2743 + async function Greeting() {
2744 + await greetingPromise;
2745 + return 'hello world';
2746 + }
2747 +
2748 + const controller = new AbortController();
2749 + const {pendingResult} = await serverAct(async () => {
2750 + // destructure trick to avoid the act scope from awaiting the returned value
2751 + return {
2752 + pendingResult: ReactServerDOMStaticServer.prerenderToNodeStream(
2753 + <App />,
2754 + webpackMap,
2755 + {
2756 + signal: controller.signal,
2757 + },
2758 + ),
2759 + };
2760 + });
2761 +
2762 + controller.abort();
2763 + resolveGreeting();
2764 + const {prelude} = await pendingResult;
2765 +
2766 + const preludeWeb = Readable.toWeb(prelude);
2767 + const response = ReactServerDOMClient.createFromReadableStream(preludeWeb);
2768 +
2769 + const {writable: fizzWritable, readable: fizzReadable} = getTestStream();
2770 +
2771 + function ClientApp() {
2772 + return use(response);
2773 + }
2774 +
2775 + const errors = [];
2776 + let abortFizz;
2777 + await serverAct(async () => {
2778 + const {pipe, abort} = ReactDOMFizzServer.renderToPipeableStream(
2779 + React.createElement(ClientApp),
2780 + {
2781 + onError(error) {
2782 + errors.push(error);
2783 + },
2784 + },
2785 + );
2786 + pipe(fizzWritable);
2787 + abortFizz = abort;
2788 + });
2789 +
2790 + await serverAct(() => {
2791 + abortFizz('boom');
2792 + });
2793 +
2794 + expect(errors).toEqual(['boom']);
2795 +
2796 + const container = document.createElement('div');
2797 + await readInto(container, fizzReadable);
2798 + expect(getMeaningfulChildren(container)).toEqual(<div>loading...</div>);
2799 + });
2800 +
2801 + // @gate enableHalt
2802 + it('will leave async iterables in an incomplete state when halting', async () => {
2803 + let resolve;
2804 + const wait = new Promise(r => (resolve = r));
2805 + const errors = [];
2806 +
2807 + const multiShotIterable = {
2808 + async *[Symbol.asyncIterator]() {
2809 + yield {hello: 'A'};
2810 + await wait;
2811 + yield {hi: 'B'};
2812 + return 'C';
2813 + },
2814 + };
2815 +
2816 + const controller = new AbortController();
2817 + const {pendingResult} = await serverAct(() => {
2818 + return {
2819 + pendingResult: ReactServerDOMStaticServer.prerenderToNodeStream(
2820 + {
2821 + multiShotIterable,
2822 + },
2823 + {},
2824 + {
2825 + onError(x) {
2826 + errors.push(x);
2827 + },
2828 + signal: controller.signal,
2829 + },
2830 + ),
2831 + };
2832 + });
2833 +
2834 + controller.abort();
2835 + await serverAct(() => resolve());
2836 +
2837 + const {prelude} = await pendingResult;
2838 +
2839 + const result = await ReactServerDOMClient.createFromReadableStream(
2840 + Readable.toWeb(prelude),
2841 + );
2842 +
2843 + const iterator = result.multiShotIterable[Symbol.asyncIterator]();
2844 +
2845 + expect(await iterator.next()).toEqual({
2846 + value: {hello: 'A'},
2847 + done: false,
2848 + });
2849 +
2850 + const race = Promise.race([
2851 + iterator.next(),
2852 + new Promise(r => setTimeout(() => r('timeout'), 10)),
2853 + ]);
2854 +
2855 + await 1;
2856 + jest.advanceTimersByTime('100');
2857 + expect(await race).toBe('timeout');
2858 + });
2859 });
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js
+114 -1
@@ -29,6 +29,7 @@ let ReactDOM;
29 let ReactDOMClient;
30 let ReactDOMFizzServer;
31 let ReactServerDOMServer;
32 +let ReactServerDOMStaticServer;
33 let ReactServerDOMClient;
34 let Suspense;
35 let use;
@@ -60,7 +61,13 @@ describe('ReactFlightDOMBrowser', () => {
61 serverExports = WebpackMock.serverExports;
62 webpackMap = WebpackMock.webpackMap;
63 webpackServerMap = WebpackMock.webpackServerMap;
63 - ReactServerDOMServer = require('react-server-dom-webpack/server.browser');
64 + ReactServerDOMServer = require('react-server-dom-webpack/server');
65 + if (__EXPERIMENTAL__) {
66 + jest.mock('react-server-dom-webpack/static', () =>
67 + require('react-server-dom-webpack/static.browser'),
68 + );
69 + ReactServerDOMStaticServer = require('react-server-dom-webpack/static');
70 + }
71
72 __unmockReact();
73 jest.resetModules();
@@ -2332,4 +2339,110 @@ describe('ReactFlightDOMBrowser', () => {
2339 expect(error.digest).toBe('aborted');
2340 expect(errors).toEqual([reason]);
2341 });
2342 +
2343 + // @gate experimental
2344 + it('can prerender', async () => {
2345 + let resolveGreeting;
2346 + const greetingPromise = new Promise(resolve => {
2347 + resolveGreeting = resolve;
2348 + });
2349 +
2350 + function App() {
2351 + return (
2352 + <div>
2353 + <Greeting />
2354 + </div>
2355 + );
2356 + }
2357 +
2358 + async function Greeting() {
2359 + await greetingPromise;
2360 + return 'hello world';
2361 + }
2362 +
2363 + const {pendingResult} = await serverAct(async () => {
2364 + // destructure trick to avoid the act scope from awaiting the returned value
2365 + return {
2366 + pendingResult: ReactServerDOMStaticServer.prerender(
2367 + <App />,
2368 + webpackMap,
2369 + ),
2370 + };
2371 + });
2372 +
2373 + resolveGreeting();
2374 + const {prelude} = await pendingResult;
2375 +
2376 + function ClientRoot({response}) {
2377 + return use(response);
2378 + }
2379 +
2380 + const response = ReactServerDOMClient.createFromReadableStream(
2381 + passThrough(prelude),
2382 + );
2383 + const container = document.createElement('div');
2384 + const root = ReactDOMClient.createRoot(container);
2385 +
2386 + await act(() => {
2387 + root.render(<ClientRoot response={response} />);
2388 + });
2389 + expect(container.innerHTML).toBe('<div>hello world</div>');
2390 + });
2391 +
2392 + // @gate enableHalt
2393 + it('serializes unfinished tasks with infinite promises when aborting a prerender', async () => {
2394 + let resolveGreeting;
2395 + const greetingPromise = new Promise(resolve => {
2396 + resolveGreeting = resolve;
2397 + });
2398 +
2399 + function App() {
2400 + return (
2401 + <div>
2402 + <Suspense fallback="loading...">
2403 + <Greeting />
2404 + </Suspense>
2405 + </div>
2406 + );
2407 + }
2408 +
2409 + async function Greeting() {
2410 + await greetingPromise;
2411 + return 'hello world';
2412 + }
2413 +
2414 + const controller = new AbortController();
2415 + const {pendingResult} = await serverAct(async () => {
2416 + // destructure trick to avoid the act scope from awaiting the returned value
2417 + return {
2418 + pendingResult: ReactServerDOMStaticServer.prerender(
2419 + <App />,
2420 + webpackMap,
2421 + {
2422 + signal: controller.signal,
2423 + },
2424 + ),
2425 + };
2426 + });
2427 +
2428 + controller.abort();
2429 + resolveGreeting();
2430 + const {prelude} = await pendingResult;
2431 +
2432 + function ClientRoot({response}) {
2433 + return use(response);
2434 + }
2435 +
2436 + const response = ReactServerDOMClient.createFromReadableStream(
2437 + passThrough(prelude),
2438 + );
2439 + const container = document.createElement('div');
2440 + const root = ReactDOMClient.createRoot(container);
2441 +
2442 + await act(() => {
2443 + root.render(<ClientRoot response={response} />);
2444 + });
2445 +
2446 + expect(container.innerHTML).toBe('<div>loading...</div>');
2447 + });
2448 });
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js
+257 -84
@@ -23,9 +23,9 @@ if (typeof File === 'undefined' || typeof FormData === 'undefined') {
23 // Patch for Edge environments for global scope
24 global.AsyncLocalStorage = require('async_hooks').AsyncLocalStorage;
25
26 -// Don't wait before processing work on the server.
27 -// TODO: we can replace this with FlightServer.act().
28 -global.setTimeout = cb => cb();
26 +const {
27 + patchMessageChannel,
28 +} = require('../../../../scripts/jest/patchMessageChannel');
29
30 let serverExports;
31 let clientExports;
@@ -36,8 +36,11 @@ let React;
36 let ReactServer;
37 let ReactDOMServer;
38 let ReactServerDOMServer;
39 +let ReactServerDOMStaticServer;
40 let ReactServerDOMClient;
41 let use;
42 +let ReactServerScheduler;
43 +let reactServerAct;
44
45 function normalizeCodeLocInfo(str) {
46 return (
@@ -52,6 +55,10 @@ describe('ReactFlightDOMEdge', () => {
55 beforeEach(() => {
56 jest.resetModules();
57
58 + ReactServerScheduler = require('scheduler');
59 + patchMessageChannel(ReactServerScheduler);
60 + reactServerAct = require('internal-test-utils').act;
61 +
62 // Simulate the condition resolution
63 jest.mock('react', () => require('react/react.react-server'));
64 jest.mock('react-server-dom-webpack/server', () =>
@@ -68,6 +75,12 @@ describe('ReactFlightDOMEdge', () => {
75
76 ReactServer = require('react');
77 ReactServerDOMServer = require('react-server-dom-webpack/server');
78 + if (__EXPERIMENTAL__) {
79 + jest.mock('react-server-dom-webpack/static', () =>
80 + require('react-server-dom-webpack/static.edge'),
81 + );
82 + ReactServerDOMStaticServer = require('react-server-dom-webpack/static');
83 + }
84
85 jest.resetModules();
86 __unmockReact();
@@ -81,6 +94,17 @@ describe('ReactFlightDOMEdge', () => {
94 use = React.use;
95 });
96
97 + async function serverAct(callback) {
98 + let maybePromise;
99 + await reactServerAct(() => {
100 + maybePromise = callback();
101 + if (maybePromise && typeof maybePromise.catch === 'function') {
102 + maybePromise.catch(() => {});
103 + }
104 + });
105 + return maybePromise;
106 + }
107 +
108 function passThrough(stream) {
109 // Simulate more realistic network by splitting up and rejoining some chunks.
110 // This lets us test that we don't accidentally rely on particular bounds of the chunks.
@@ -174,9 +198,8 @@ describe('ReactFlightDOMEdge', () => {
198 return <ClientComponentOnTheClient />;
199 }
200
177 - const stream = ReactServerDOMServer.renderToReadableStream(
178 - <App />,
179 - webpackMap,
201 + const stream = await serverAct(() =>
202 + ReactServerDOMServer.renderToReadableStream(<App />, webpackMap),
203 );
204 const response = ReactServerDOMClient.createFromReadableStream(stream, {
205 ssrManifest: {
@@ -189,8 +212,8 @@ describe('ReactFlightDOMEdge', () => {
212 return use(response);
213 }
214
192 - const ssrStream = await ReactDOMServer.renderToReadableStream(
193 - <ClientRoot />,
215 + const ssrStream = await serverAct(() =>
216 + ReactDOMServer.renderToReadableStream(<ClientRoot />),
217 );
218 const result = await readResult(ssrStream);
219 expect(result).toEqual('<span>Client Component</span>');
@@ -200,10 +223,12 @@ describe('ReactFlightDOMEdge', () => {
223 const testString = '"\n\t'.repeat(500) + '🙃';
224 const testString2 = 'hello'.repeat(400);
225
203 - const stream = ReactServerDOMServer.renderToReadableStream({
204 - text: testString,
205 - text2: testString2,
206 - });
226 + const stream = await serverAct(() =>
227 + ReactServerDOMServer.renderToReadableStream({
228 + text: testString,
229 + text2: testString2,
230 + }),
231 + );
232 const [stream1, stream2] = passThrough(stream).tee();
233
234 const serializedContent = await readResult(stream1);
@@ -234,7 +259,9 @@ describe('ReactFlightDOMEdge', () => {
259 with: {many: 'properties in it'},
260 };
261 const props = {root: <div>{new Array(30).fill(obj)}</div>};
237 - const stream = ReactServerDOMServer.renderToReadableStream(props);
262 + const stream = await serverAct(() =>
263 + ReactServerDOMServer.renderToReadableStream(props),
264 + );
265 const [stream1, stream2] = passThrough(stream).tee();
266
267 const serializedContent = await readResult(stream1);
@@ -302,7 +329,9 @@ describe('ReactFlightDOMEdge', () => {
329 </>
330 );
331 const resolvedChildren = new Array(30).fill(str);
305 - const stream = ReactServerDOMServer.renderToReadableStream(children);
332 + const stream = await serverAct(() =>
333 + ReactServerDOMServer.renderToReadableStream(children),
334 + );
335 const [stream1, stream2] = passThrough(stream).tee();
336
337 const serializedContent = await readResult(stream1);
@@ -318,7 +347,9 @@ describe('ReactFlightDOMEdge', () => {
347 });
348
349 // Use the SSR render to resolve any lazy elements
321 - const ssrStream = await ReactDOMServer.renderToReadableStream(model);
350 + const ssrStream = await serverAct(() =>
351 + ReactDOMServer.renderToReadableStream(model),
352 + );
353 // Should still match the result when parsed
354 const result = await readResult(ssrStream);
355 expect(result).toEqual(resolvedChildren.join('<!-- -->'));
@@ -370,22 +401,28 @@ describe('ReactFlightDOMEdge', () => {
401 const resolvedChildren = new Array(30).fill(
402 '<div>this is a long return value</div>',
403 );
373 - const stream = ReactServerDOMServer.renderToReadableStream(children);
404 + const stream = await serverAct(() =>
405 + ReactServerDOMServer.renderToReadableStream(children),
406 + );
407 const [stream1, stream2] = passThrough(stream).tee();
408
409 const serializedContent = await readResult(stream1);
410 expect(serializedContent.length).toBeLessThan(__DEV__ ? 605 : 400);
411 expect(timesRendered).toBeLessThan(5);
412
380 - const model = await ReactServerDOMClient.createFromReadableStream(stream2, {
381 - ssrManifest: {
382 - moduleMap: null,
383 - moduleLoading: null,
384 - },
385 - });
413 + const model = await serverAct(() =>
414 + ReactServerDOMClient.createFromReadableStream(stream2, {
415 + ssrManifest: {
416 + moduleMap: null,
417 + moduleLoading: null,
418 + },
419 + }),
420 + );
421
422 // Use the SSR render to resolve any lazy elements
388 - const ssrStream = await ReactDOMServer.renderToReadableStream(model);
423 + const ssrStream = await serverAct(() =>
424 + ReactDOMServer.renderToReadableStream(model),
425 + );
426 // Should still match the result when parsed
427 const result = await readResult(ssrStream);
428 expect(result).toEqual(resolvedChildren.join(''));
@@ -398,8 +435,10 @@ describe('ReactFlightDOMEdge', () => {
435 }
436 return <div>Fin</div>;
437 }
401 - const stream = ReactServerDOMServer.renderToReadableStream(
402 - <ServerComponent recurse={20} />,
438 + const stream = await serverAct(() =>
439 + ReactServerDOMServer.renderToReadableStream(
440 + <ServerComponent recurse={20} />,
441 + ),
442 );
443 const serializedContent = await readResult(stream);
444 const expectedDebugInfoSize = __DEV__ ? 300 * 20 : 0;
@@ -426,8 +465,8 @@ describe('ReactFlightDOMEdge', () => {
465 new BigUint64Array(buffer, 0),
466 new DataView(buffer, 3),
467 ];
429 - const stream = passThrough(
430 - ReactServerDOMServer.renderToReadableStream(buffers),
468 + const stream = await serverAct(() =>
469 + passThrough(ReactServerDOMServer.renderToReadableStream(buffers)),
470 );
471 const result = await ReactServerDOMClient.createFromReadableStream(stream, {
472 ssrManifest: {
@@ -446,8 +485,8 @@ describe('ReactFlightDOMEdge', () => {
485 const blob = new Blob([bytes, bytes], {
486 type: 'application/x-test',
487 });
449 - const stream = passThrough(
450 - ReactServerDOMServer.renderToReadableStream(blob),
488 + const stream = await serverAct(() =>
489 + passThrough(ReactServerDOMServer.renderToReadableStream(blob)),
490 );
491 const result = await ReactServerDOMClient.createFromReadableStream(stream, {
492 ssrManifest: {
@@ -476,8 +515,8 @@ describe('ReactFlightDOMEdge', () => {
515 expect(formData.get('file') instanceof File).toBe(true);
516 expect(formData.get('file').name).toBe('filename.test');
517
479 - const stream = passThrough(
480 - ReactServerDOMServer.renderToReadableStream(formData),
518 + const stream = await serverAct(() =>
519 + passThrough(ReactServerDOMServer.renderToReadableStream(formData)),
520 );
521 const result = await ReactServerDOMClient.createFromReadableStream(stream, {
522 ssrManifest: {
@@ -507,8 +546,8 @@ describe('ReactFlightDOMEdge', () => {
546 const map = new Map();
547 map.set('value', awaitedValue);
548
510 - const stream = passThrough(
511 - ReactServerDOMServer.renderToReadableStream(map, webpackMap),
549 + const stream = await serverAct(() =>
550 + passThrough(ReactServerDOMServer.renderToReadableStream(map, webpackMap)),
551 );
552
553 // Parsing the root blocks because the module hasn't loaded yet
@@ -549,16 +588,18 @@ describe('ReactFlightDOMEdge', () => {
588 },
589 });
590
552 - const stream = passThrough(
553 - ReactServerDOMServer.renderToReadableStream(s, webpackMap),
591 + const stream = await serverAct(() =>
592 + passThrough(ReactServerDOMServer.renderToReadableStream(s, webpackMap)),
593 );
594
556 - const result = await ReactServerDOMClient.createFromReadableStream(stream, {
557 - ssrManifest: {
558 - moduleMap: null,
559 - moduleLoading: null,
560 - },
561 - });
595 + const result = await serverAct(() =>
596 + ReactServerDOMClient.createFromReadableStream(stream, {
597 + ssrManifest: {
598 + moduleMap: null,
599 + moduleLoading: null,
600 + },
601 + }),
602 + );
603
604 const reader = result.getReader();
605
@@ -589,20 +630,24 @@ describe('ReactFlightDOMEdge', () => {
630 },
631 };
632
592 - const stream = passThrough(
593 - ReactServerDOMServer.renderToReadableStream(
594 - multiShotIterable,
595 - webpackMap,
633 + const stream = await serverAct(() =>
634 + passThrough(
635 + ReactServerDOMServer.renderToReadableStream(
636 + multiShotIterable,
637 + webpackMap,
638 + ),
639 ),
640 );
641
642 // Parsing the root blocks because the module hasn't loaded yet
600 - const result = await ReactServerDOMClient.createFromReadableStream(stream, {
601 - ssrManifest: {
602 - moduleMap: null,
603 - moduleLoading: null,
604 - },
605 - });
643 + const result = await serverAct(() =>
644 + ReactServerDOMClient.createFromReadableStream(stream, {
645 + ssrManifest: {
646 + moduleMap: null,
647 + moduleLoading: null,
648 + },
649 + }),
650 + );
651
652 const iterator = result[Symbol.asyncIterator]();
653
@@ -635,9 +680,11 @@ describe('ReactFlightDOMEdge', () => {
680 },
681 };
682
638 - const stream = ReactServerDOMServer.renderToReadableStream({
639 - iterable,
640 - });
683 + const stream = await serverAct(() =>
684 + ReactServerDOMServer.renderToReadableStream({
685 + iterable,
686 + }),
687 + );
688 const [stream1, stream2] = passThrough(stream).tee();
689
690 const serializedContent = await readResult(stream1);
@@ -728,7 +775,9 @@ describe('ReactFlightDOMEdge', () => {
775 },
776 });
777
731 - const stream = ReactServerDOMServer.renderToReadableStream(s, {});
778 + const stream = await serverAct(() =>
779 + ReactServerDOMServer.renderToReadableStream(s, {}),
780 + );
781
782 const [stream1, stream2] = passThrough(stream).tee();
783
@@ -785,7 +834,9 @@ describe('ReactFlightDOMEdge', () => {
834 },
835 });
836
788 - const stream = ReactServerDOMServer.renderToReadableStream(s, {});
837 + const stream = await serverAct(() =>
838 + ReactServerDOMServer.renderToReadableStream(s, {}),
839 + );
840
841 const [stream1, stream2] = passThrough(stream).tee();
842
@@ -841,23 +892,21 @@ describe('ReactFlightDOMEdge', () => {
892 greeting: ReactServer.createElement(Greeting, {firstName: 'Seb'}),
893 };
894
844 - const stream = ReactServerDOMServer.renderToReadableStream(
845 - model,
846 - webpackMap,
895 + const stream = await serverAct(() =>
896 + ReactServerDOMServer.renderToReadableStream(model, webpackMap),
897 );
898
849 - const rootModel = await ReactServerDOMClient.createFromReadableStream(
850 - stream,
851 - {
899 + const rootModel = await serverAct(() =>
900 + ReactServerDOMClient.createFromReadableStream(stream, {
901 ssrManifest: {
902 moduleMap: null,
903 moduleLoading: null,
904 },
856 - },
905 + }),
906 );
907
859 - const ssrStream = await ReactDOMServer.renderToReadableStream(
860 - rootModel.greeting,
908 + const ssrStream = await serverAct(() =>
909 + ReactDOMServer.renderToReadableStream(rootModel.greeting),
910 );
911 const result = await readResult(ssrStream);
912 expect(result).toEqual('<span>Hello, Seb</span>');
@@ -916,13 +965,15 @@ describe('ReactFlightDOMEdge', () => {
965 return ReactServer.createElement('span', null, 'hi');
966 }
967
919 - const stream = ReactServerDOMServer.renderToReadableStream(
920 - ReactServer.createElement(
921 - 'div',
922 - null,
923 - ReactServer.createElement(Foo, null),
968 + const stream = await serverAct(() =>
969 + ReactServerDOMServer.renderToReadableStream(
970 + ReactServer.createElement(
971 + 'div',
972 + null,
973 + ReactServer.createElement(Foo, null),
974 + ),
975 + webpackMap,
976 ),
925 - webpackMap,
977 );
978 await readResult(stream);
979
@@ -943,35 +994,31 @@ describe('ReactFlightDOMEdge', () => {
994 root: ReactServer.createElement(Erroring),
995 };
996
946 - const stream = ReactServerDOMServer.renderToReadableStream(
947 - model,
948 - webpackMap,
949 - {
997 + const stream = await serverAct(() =>
998 + ReactServerDOMServer.renderToReadableStream(model, webpackMap, {
999 onError() {},
951 - },
1000 + }),
1001 );
1002
954 - const rootModel = await ReactServerDOMClient.createFromReadableStream(
955 - stream,
956 - {
1003 + const rootModel = await serverAct(() =>
1004 + ReactServerDOMClient.createFromReadableStream(stream, {
1005 ssrManifest: {
1006 moduleMap: null,
1007 moduleLoading: null,
1008 },
961 - },
1009 + }),
1010 );
1011
1012 const errors = [];
965 - const result = ReactDOMServer.renderToReadableStream(
966 - <div>{rootModel.root}</div>,
967 - {
1013 + const result = serverAct(() =>
1014 + ReactDOMServer.renderToReadableStream(<div>{rootModel.root}</div>, {
1015 onError(error, {componentStack}) {
1016 errors.push({
1017 error,
1018 componentStack: normalizeCodeLocInfo(componentStack),
1019 });
1020 },
974 - },
1021 + }),
1022 );
1023
1024 const theError = new Error('my error');
@@ -1000,4 +1047,130 @@ describe('ReactFlightDOMEdge', () => {
1047 },
1048 ]);
1049 });
1050 +
1051 + // @gate experimental
1052 + it('can prerender', async () => {
1053 + let resolveGreeting;
1054 + const greetingPromise = new Promise(resolve => {
1055 + resolveGreeting = resolve;
1056 + });
1057 +
1058 + function App() {
1059 + return (
1060 + <div>
1061 + <Greeting />
1062 + </div>
1063 + );
1064 + }
1065 +
1066 + async function Greeting() {
1067 + await greetingPromise;
1068 + return 'hello world';
1069 + }
1070 +
1071 + const {pendingResult} = await serverAct(async () => {
1072 + // destructure trick to avoid the act scope from awaiting the returned value
1073 + return {
1074 + pendingResult: ReactServerDOMStaticServer.prerender(
1075 + <App />,
1076 + webpackMap,
1077 + ),
1078 + };
1079 + });
1080 +
1081 + resolveGreeting();
1082 + const {prelude} = await pendingResult;
1083 +
1084 + function ClientRoot({response}) {
1085 + return use(response);
1086 + }
1087 +
1088 + const response = ReactServerDOMClient.createFromReadableStream(prelude, {
1089 + ssrManifest: {
1090 + moduleMap: null,
1091 + moduleLoading: null,
1092 + },
1093 + });
1094 + // Use the SSR render to resolve any lazy elements
1095 + const ssrStream = await serverAct(() =>
1096 + ReactDOMServer.renderToReadableStream(
1097 + React.createElement(ClientRoot, {response}),
1098 + ),
1099 + );
1100 + // Should still match the result when parsed
1101 + const result = await readResult(ssrStream);
1102 + expect(result).toBe('<div>hello world</div>');
1103 + });
1104 +
1105 + // @gate enableHalt
1106 + it('serializes unfinished tasks with infinite promises when aborting a prerender', async () => {
1107 + let resolveGreeting;
1108 + const greetingPromise = new Promise(resolve => {
1109 + resolveGreeting = resolve;
1110 + });
1111 +
1112 + function App() {
1113 + return (
1114 + <div>
1115 + <ReactServer.Suspense fallback="loading...">
1116 + <Greeting />
1117 + </ReactServer.Suspense>
1118 + </div>
1119 + );
1120 + }
1121 +
1122 + async function Greeting() {
1123 + await greetingPromise;
1124 + return 'hello world';
1125 + }
1126 +
1127 + const controller = new AbortController();
1128 + const {pendingResult} = await serverAct(async () => {
1129 + // destructure trick to avoid the act scope from awaiting the returned value
1130 + return {
1131 + pendingResult: ReactServerDOMStaticServer.prerender(
1132 + <App />,
1133 + webpackMap,
1134 + {
1135 + signal: controller.signal,
1136 + },
1137 + ),
1138 + };
1139 + });
1140 +
1141 + controller.abort();
1142 + resolveGreeting();
1143 + const {prelude} = await pendingResult;
1144 +
1145 + function ClientRoot({response}) {
1146 + return use(response);
1147 + }
1148 +
1149 + const response = ReactServerDOMClient.createFromReadableStream(prelude, {
1150 + ssrManifest: {
1151 + moduleMap: null,
1152 + moduleLoading: null,
1153 + },
1154 + });
1155 + const fizzController = new AbortController();
1156 + const errors = [];
1157 + const ssrStream = await serverAct(() =>
1158 + ReactDOMServer.renderToReadableStream(
1159 + React.createElement(ClientRoot, {response}),
1160 + {
1161 + signal: fizzController.signal,
1162 + onError(error) {
1163 + errors.push(error);
1164 + },
1165 + },
1166 + ),
1167 + );
1168 + fizzController.abort('boom');
1169 + expect(errors).toEqual(['boom']);
1170 + // Should still match the result when parsed
1171 + const result = await readResult(ssrStream);
1172 + const div = document.createElement('div');
1173 + div.innerHTML = result;
1174 + expect(div.textContent).toBe('loading...');
1175 + });
1176 });
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js
+133
@@ -20,7 +20,9 @@ let webpackModules;
20 let webpackModuleLoading;
21 let React;
22 let ReactDOMServer;
23 +let ReactServer;
24 let ReactServerDOMServer;
25 +let ReactServerDOMStaticServer;
26 let ReactServerDOMClient;
27 let Stream;
28 let use;
@@ -45,7 +47,14 @@ describe('ReactFlightDOMNode', () => {
47 jest.mock('react-server-dom-webpack/server', () =>
48 require('react-server-dom-webpack/server.node'),
49 );
50 + ReactServer = require('react');
51 ReactServerDOMServer = require('react-server-dom-webpack/server');
52 + if (__EXPERIMENTAL__) {
53 + jest.mock('react-server-dom-webpack/static', () =>
54 + require('react-server-dom-webpack/static.node'),
55 + );
56 + ReactServerDOMStaticServer = require('react-server-dom-webpack/static');
57 + }
58
59 const WebpackMock = require('./utils/WebpackMock');
60 clientExports = WebpackMock.clientExports;
@@ -378,4 +387,128 @@ describe('ReactFlightDOMNode', () => {
387 expect(error.digest).toBe('aborted');
388 expect(errors).toEqual([reason]);
389 });
390 +
391 + // @gate experimental
392 + it('can prerender', async () => {
393 + let resolveGreeting;
394 + const greetingPromise = new Promise(resolve => {
395 + resolveGreeting = resolve;
396 + });
397 +
398 + function App() {
399 + return (
400 + <div>
401 + <Greeting />
402 + </div>
403 + );
404 + }
405 +
406 + async function Greeting() {
407 + await greetingPromise;
408 + return 'hello world';
409 + }
410 +
411 + const {pendingResult} = await serverAct(async () => {
412 + // destructure trick to avoid the act scope from awaiting the returned value
413 + return {
414 + pendingResult: ReactServerDOMStaticServer.prerenderToNodeStream(
415 + <App />,
416 + webpackMap,
417 + ),
418 + };
419 + });
420 +
421 + resolveGreeting();
422 + const {prelude} = await pendingResult;
423 +
424 + function ClientRoot({response}) {
425 + return use(response);
426 + }
427 +
428 + const response = ReactServerDOMClient.createFromNodeStream(prelude, {
429 + ssrManifest: {
430 + moduleMap: null,
431 + moduleLoading: null,
432 + },
433 + });
434 + // Use the SSR render to resolve any lazy elements
435 + const ssrStream = await serverAct(() =>
436 + ReactDOMServer.renderToPipeableStream(
437 + React.createElement(ClientRoot, {response}),
438 + ),
439 + );
440 + // Should still match the result when parsed
441 + const result = await readResult(ssrStream);
442 + expect(result).toBe('<div>hello world</div>');
443 + });
444 +
445 + // @gate enableHalt
446 + it('serializes unfinished tasks with infinite promises when aborting a prerender', async () => {
447 + let resolveGreeting;
448 + const greetingPromise = new Promise(resolve => {
449 + resolveGreeting = resolve;
450 + });
451 +
452 + function App() {
453 + return (
454 + <div>
455 + <ReactServer.Suspense fallback="loading...">
456 + <Greeting />
457 + </ReactServer.Suspense>
458 + </div>
459 + );
460 + }
461 +
462 + async function Greeting() {
463 + await greetingPromise;
464 + return 'hello world';
465 + }
466 +
467 + const controller = new AbortController();
468 + const {pendingResult} = await serverAct(async () => {
469 + // destructure trick to avoid the act scope from awaiting the returned value
470 + return {
471 + pendingResult: ReactServerDOMStaticServer.prerenderToNodeStream(
472 + <App />,
473 + webpackMap,
474 + {
475 + signal: controller.signal,
476 + },
477 + ),
478 + };
479 + });
480 +
481 + controller.abort();
482 + resolveGreeting();
483 + const {prelude} = await pendingResult;
484 +
485 + function ClientRoot({response}) {
486 + return use(response);
487 + }
488 +
489 + const response = ReactServerDOMClient.createFromNodeStream(prelude, {
490 + ssrManifest: {
491 + moduleMap: null,
492 + moduleLoading: null,
493 + },
494 + });
495 + const errors = [];
496 + const ssrStream = await serverAct(() =>
497 + ReactDOMServer.renderToPipeableStream(
498 + React.createElement(ClientRoot, {response}),
499 + {
500 + onError(error) {
501 + errors.push(error);
502 + },
503 + },
504 + ),
505 + );
506 + ssrStream.abort('boom');
507 + expect(errors).toEqual(['boom']);
508 + // Should still match the result when parsed
509 + const result = await readResult(ssrStream);
510 + const div = document.createElement('div');
511 + div.innerHTML = result;
512 + expect(div.textContent).toBe('loading...');
513 + });
514 });
packages/react-server-dom-webpack/src/server/ReactFlightDOMServerBrowser.js
+15 -2
@@ -12,12 +12,15 @@ 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 +
17 import {
18 createRequest,
19 startWork,
20 startFlowing,
21 stopFlowing,
22 abort,
23 + halt,
24 } from 'react-server/src/ReactFlightServer';
25
26 import {
@@ -146,10 +149,20 @@ function prerender(
149 if (options && options.signal) {
150 const signal = options.signal;
151 if (signal.aborted) {
149 - abort(request, (signal: any).reason);
152 + const reason = (signal: any).reason;
153 + if (enableHalt) {
154 + halt(request, reason);
155 + } else {
156 + abort(request, reason);
157 + }
158 } else {
159 const listener = () => {
152 - abort(request, (signal: any).reason);
160 + const reason = (signal: any).reason;
161 + if (enableHalt) {
162 + halt(request, reason);
163 + } else {
164 + abort(request, reason);
165 + }
166 signal.removeEventListener('abort', listener);
167 };
168 signal.addEventListener('abort', listener);
packages/react-server-dom-webpack/src/server/ReactFlightDOMServerEdge.js
+15 -2
@@ -12,12 +12,15 @@ 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 +
17 import {
18 createRequest,
19 startWork,
20 startFlowing,
21 stopFlowing,
22 abort,
23 + halt,
24 } from 'react-server/src/ReactFlightServer';
25
26 import {
@@ -146,10 +149,20 @@ function prerender(
149 if (options && options.signal) {
150 const signal = options.signal;
151 if (signal.aborted) {
149 - abort(request, (signal: any).reason);
152 + const reason = (signal: any).reason;
153 + if (enableHalt) {
154 + halt(request, reason);
155 + } else {
156 + abort(request, reason);
157 + }
158 } else {
159 const listener = () => {
152 - abort(request, (signal: any).reason);
160 + const reason = (signal: any).reason;
161 + if (enableHalt) {
162 + halt(request, reason);
163 + } else {
164 + abort(request, reason);
165 + }
166 signal.removeEventListener('abort', listener);
167 };
168 signal.addEventListener('abort', listener);
packages/react-server-dom-webpack/src/server/ReactFlightDOMServerNode.js
+15 -2
@@ -20,12 +20,15 @@ import type {Thenable} from 'shared/ReactTypes';
20
21 import {Readable} from 'stream';
22
23 +import {enableHalt} from 'shared/ReactFeatureFlags';
24 +
25 import {
26 createRequest,
27 startWork,
28 startFlowing,
29 stopFlowing,
30 abort,
31 + halt,
32 } from 'react-server/src/ReactFlightServer';
33
34 import {
@@ -189,10 +192,20 @@ function prerenderToNodeStream(
192 if (options && options.signal) {
193 const signal = options.signal;
194 if (signal.aborted) {
192 - abort(request, (signal: any).reason);
195 + const reason = (signal: any).reason;
196 + if (enableHalt) {
197 + halt(request, reason);
198 + } else {
199 + abort(request, reason);
200 + }
201 } else {
202 const listener = () => {
195 - abort(request, (signal: any).reason);
203 + const reason = (signal: any).reason;
204 + if (enableHalt) {
205 + halt(request, reason);
206 + } else {
207 + abort(request, reason);
208 + }
209 signal.removeEventListener('abort', listener);
210 };
211 signal.addEventListener('abort', listener);
packages/react-server/src/ReactFlightServer.js
+103 -21
@@ -16,6 +16,7 @@ import type {TemporaryReferenceSet} from './ReactFlightServerTemporaryReferences
16 import {
17 enableBinaryFlight,
18 enablePostpone,
19 + enableHalt,
20 enableTaint,
21 enableRefAsProp,
22 enableServerComponentLogs,
@@ -611,11 +612,15 @@ function serializeThenable(
612 default: {
613 if (request.status === ABORTING) {
614 // We can no longer accept any resolved values
614 - newTask.status = ABORTED;
615 - const errorId: number = (request.fatalError: any);
616 - const model = stringify(serializeByValueID(errorId));
617 - emitModelChunk(request, newTask.id, model);
615 request.abortableTasks.delete(newTask);
616 + newTask.status = ABORTED;
617 + if (enableHalt && request.fatalError === haltSymbol) {
618 + emitModelChunk(request, newTask.id, reusableInfinitePromiseModel);
619 + } else {
620 + const errorId: number = (request.fatalError: any);
621 + const model = stringify(serializeByValueID(errorId));
622 + emitModelChunk(request, newTask.id, model);
623 + }
624 return newTask.id;
625 }
626 if (typeof thenable.status === 'string') {
@@ -748,23 +753,32 @@ function serializeReadableStream(
753 }
754 aborted = true;
755 request.abortListeners.delete(error);
751 - if (
756 +
757 + let cancelWith: mixed;
758 + if (enableHalt && request.fatalError === haltSymbol) {
759 + cancelWith = reason;
760 + } else if (
761 enablePostpone &&
762 typeof reason === 'object' &&
763 reason !== null &&
764 (reason: any).$$typeof === REACT_POSTPONE_TYPE
765 ) {
766 + cancelWith = reason;
767 const postponeInstance: Postpone = (reason: any);
768 logPostpone(request, postponeInstance.message, streamTask);
769 emitPostponeChunk(request, streamTask.id, postponeInstance);
770 + enqueueFlush(request);
771 } else {
772 + cancelWith = reason;
773 const digest = logRecoverableError(request, reason, streamTask);
774 emitErrorChunk(request, streamTask.id, digest, reason);
775 + enqueueFlush(request);
776 }
764 - enqueueFlush(request);
777 +
778 // $FlowFixMe should be able to pass mixed
766 - reader.cancel(reason).then(error, error);
779 + reader.cancel(cancelWith).then(error, error);
780 }
781 +
782 request.abortListeners.add(error);
783 reader.read().then(progress, error);
784 return serializeByValueID(streamTask.id);
@@ -866,24 +880,30 @@ function serializeAsyncIterable(
880 }
881 aborted = true;
882 request.abortListeners.delete(error);
869 - if (
883 + let throwWith: mixed;
884 + if (enableHalt && request.fatalError === haltSymbol) {
885 + throwWith = reason;
886 + } else if (
887 enablePostpone &&
888 typeof reason === 'object' &&
889 reason !== null &&
890 (reason: any).$$typeof === REACT_POSTPONE_TYPE
891 ) {
892 + throwWith = reason;
893 const postponeInstance: Postpone = (reason: any);
894 logPostpone(request, postponeInstance.message, streamTask);
895 emitPostponeChunk(request, streamTask.id, postponeInstance);
896 + enqueueFlush(request);
897 } else {
898 + throwWith = reason;
899 const digest = logRecoverableError(request, reason, streamTask);
900 emitErrorChunk(request, streamTask.id, digest, reason);
901 + enqueueFlush(request);
902 }
882 - enqueueFlush(request);
903 if (typeof (iterator: any).throw === 'function') {
904 // The iterator protocol doesn't necessarily include this but a generator do.
905 // $FlowFixMe should be able to pass mixed
886 - iterator.throw(reason).then(error, error);
906 + iterator.throw(throwWith).then(error, error);
907 }
908 }
909 request.abortListeners.add(error);
@@ -1798,6 +1818,7 @@ function serializeLazyID(id: number): string {
1818 function serializeInfinitePromise(): string {
1819 return '$@';
1820 }
1821 +const reusableInfinitePromiseModel = stringify(serializeInfinitePromise());
1822
1823 function serializePromiseID(id: number): string {
1824 return '$@' + id.toString(16);
@@ -2066,12 +2087,18 @@ function serializeBlob(request: Request, blob: Blob): string {
2087 }
2088 aborted = true;
2089 request.abortListeners.delete(error);
2069 - const digest = logRecoverableError(request, reason, newTask);
2070 - emitErrorChunk(request, newTask.id, digest, reason);
2071 - request.abortableTasks.delete(newTask);
2072 - enqueueFlush(request);
2090 + let cancelWith: mixed;
2091 + if (enableHalt && request.fatalError === haltSymbol) {
2092 + cancelWith = reason;
2093 + } else {
2094 + cancelWith = reason;
2095 + const digest = logRecoverableError(request, reason, newTask);
2096 + emitErrorChunk(request, newTask.id, digest, reason);
2097 + request.abortableTasks.delete(newTask);
2098 + enqueueFlush(request);
2099 + }
2100 // $FlowFixMe should be able to pass mixed
2074 - reader.cancel(reason).then(error, error);
2101 + reader.cancel(cancelWith).then(error, error);
2102 }
2103
2104 request.abortListeners.add(error);
@@ -2149,6 +2176,9 @@ function renderModel(
2176 if (typeof x.then === 'function') {
2177 if (request.status === ABORTING) {
2178 task.status = ABORTED;
2179 + if (enableHalt && request.fatalError === haltSymbol) {
2180 + return serializeInfinitePromise();
2181 + }
2182 const errorId: number = (request.fatalError: any);
2183 if (wasReactNode) {
2184 return serializeLazyID(errorId);
@@ -2202,6 +2232,9 @@ function renderModel(
2232
2233 if (request.status === ABORTING) {
2234 task.status = ABORTED;
2235 + if (enableHalt && request.fatalError === haltSymbol) {
2236 + return serializeInfinitePromise();
2237 + }
2238 const errorId: number = (request.fatalError: any);
2239 if (wasReactNode) {
2240 return serializeLazyID(errorId);
@@ -3691,9 +3724,13 @@ function retryTask(request: Request, task: Task): void {
3724 if (request.status === ABORTING) {
3725 request.abortableTasks.delete(task);
3726 task.status = ABORTED;
3694 - const errorId: number = (request.fatalError: any);
3695 - const model = stringify(serializeByValueID(errorId));
3696 - emitModelChunk(request, task.id, model);
3727 + if (enableHalt && request.fatalError === haltSymbol) {
3728 + emitModelChunk(request, task.id, reusableInfinitePromiseModel);
3729 + } else {
3730 + const errorId: number = (request.fatalError: any);
3731 + const model = stringify(serializeByValueID(errorId));
3732 + emitModelChunk(request, task.id, model);
3733 + }
3734 return;
3735 }
3736 // Something suspended again, let's pick it back up later.
@@ -3715,9 +3752,13 @@ function retryTask(request: Request, task: Task): void {
3752 if (request.status === ABORTING) {
3753 request.abortableTasks.delete(task);
3754 task.status = ABORTED;
3718 - const errorId: number = (request.fatalError: any);
3719 - const model = stringify(serializeByValueID(errorId));
3720 - emitModelChunk(request, task.id, model);
3755 + if (enableHalt && request.fatalError === haltSymbol) {
3756 + emitModelChunk(request, task.id, reusableInfinitePromiseModel);
3757 + } else {
3758 + const errorId: number = (request.fatalError: any);
3759 + const model = stringify(serializeByValueID(errorId));
3760 + emitModelChunk(request, task.id, model);
3761 + }
3762 return;
3763 }
3764
@@ -3795,6 +3836,15 @@ function abortTask(task: Task, request: Request, errorId: number): void {
3836 request.completedErrorChunks.push(processedChunk);
3837 }
3838
3839 +function haltTask(task: Task, request: Request): void {
3840 + if (task.status === RENDERING) {
3841 + // This task will be aborted by the render
3842 + return;
3843 + }
3844 + task.status = ABORTED;
3845 + emitModelChunk(request, task.id, reusableInfinitePromiseModel);
3846 +}
3847 +
3848 function flushCompletedChunks(
3849 request: Request,
3850 destination: Destination,
@@ -4012,3 +4062,35 @@ export function abort(request: Request, reason: mixed): void {
4062 fatalError(request, error);
4063 }
4064 }
4065 +
4066 +const haltSymbol = Symbol('halt');
4067 +
4068 +// This is called to stop rendering without erroring. All unfinished work is represented Promises
4069 +// that never resolve.
4070 +export function halt(request: Request, reason: mixed): void {
4071 + try {
4072 + if (request.status === OPEN) {
4073 + request.status = ABORTING;
4074 + }
4075 + request.fatalError = haltSymbol;
4076 + const abortableTasks = request.abortableTasks;
4077 + // We have tasks to abort. We'll emit one error row and then emit a reference
4078 + // to that row from every row that's still remaining.
4079 + if (abortableTasks.size > 0) {
4080 + request.pendingChunks++;
4081 + abortableTasks.forEach(task => haltTask(task, request));
4082 + abortableTasks.clear();
4083 + }
4084 + const abortListeners = request.abortListeners;
4085 + if (abortListeners.size > 0) {
4086 + abortListeners.forEach(callback => callback(reason));
4087 + abortListeners.clear();
4088 + }
4089 + if (request.destination !== null) {
4090 + flushCompletedChunks(request, request.destination);
4091 + }
4092 + } catch (error) {
4093 + logRecoverableError(request, error, null);
4094 + fatalError(request, error);
4095 + }
4096 +}
packages/shared/ReactFeatureFlags.js
+2
@@ -87,6 +87,8 @@ export const enableTaint = __EXPERIMENTAL__;
87
88 export const enablePostpone = __EXPERIMENTAL__;
89
90 +export const enableHalt = __EXPERIMENTAL__;
91 +
92 /**
93 * Switches the Fabric API from doing layout in commit work instead of complete work.
94 */
packages/shared/forks/ReactFeatureFlags.native-fb.js
+1
@@ -58,6 +58,7 @@ export const enableFilterEmptyStringAttributesDOM = true;
58 export const enableFizzExternalRuntime = true;
59 export const enableFlightReadableStream = true;
60 export const enableGetInspectorDataForInstanceInProduction = true;
61 +export const enableHalt = false;
62 export const enableInfiniteRenderLoopDetection = true;
63 export const enableContextProfiling = false;
64 export const enableLegacyCache = false;
packages/shared/forks/ReactFeatureFlags.native-oss.js
+1
@@ -49,6 +49,7 @@ export const enableFilterEmptyStringAttributesDOM = true;
49 export const enableFizzExternalRuntime = true;
50 export const enableFlightReadableStream = true;
51 export const enableGetInspectorDataForInstanceInProduction = false;
52 +export const enableHalt = false;
53 export const enableInfiniteRenderLoopDetection = true;
54 export const enableLazyContextPropagation = false;
55 export const enableContextProfiling = false;
packages/shared/forks/ReactFeatureFlags.test-renderer.js
+1
@@ -25,6 +25,7 @@ export const enableFlightReadableStream = true;
25 export const enableAsyncIterableChildren = false;
26 export const enableTaint = true;
27 export const enablePostpone = false;
28 +export const enableHalt = false;
29 export const disableCommentsAsDOMContainers = true;
30 export const disableInputAttributeSyncing = false;
31 export const disableIEWorkarounds = true;
packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js
+1
@@ -40,6 +40,7 @@ export const enableFilterEmptyStringAttributesDOM = true;
40 export const enableFizzExternalRuntime = true;
41 export const enableFlightReadableStream = true;
42 export const enableGetInspectorDataForInstanceInProduction = false;
43 +export const enableHalt = false;
44 export const enableInfiniteRenderLoopDetection = true;
45 export const enableLazyContextPropagation = false;
46 export const enableContextProfiling = false;
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+1
@@ -25,6 +25,7 @@ export const enableFlightReadableStream = true;
25 export const enableAsyncIterableChildren = false;
26 export const enableTaint = true;
27 export const enablePostpone = false;
28 +export const enableHalt = false;
29 export const disableCommentsAsDOMContainers = true;
30 export const disableInputAttributeSyncing = false;
31 export const disableIEWorkarounds = true;
packages/shared/forks/ReactFeatureFlags.www.js
+2
@@ -78,6 +78,8 @@ export const enableTaint = false;
78
79 export const enablePostpone = false;
80
81 +export const enableHalt = false;
82 +
83 export const enableContextProfiling = true;
84
85 // TODO: www currently relies on this feature. It's disabled in open source.