[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.