@samitouri / QOS-React / commits / 709f948412

[Fizz] Add FB specific streaming API and build (#21337)

Add FB specific streaming API and build

Sebastian Markbåge committed Apr 22, 2021 at 19:54 UTC 709f9484122f5913fcdf2520eab230dbb0ae6bd2
8 files changed +342 -14
packages/react-server-dom-relay/package.json
+1 -2
@@ -12,7 +12,6 @@
12 "scheduler": "^0.11.0"
13 },
14 "peerDependencies": {
15 - "react": "^17.0.0",
16 - "react-dom": "^17.0.0"
15 + "react": "^17.0.0"
16 }
17 }
packages/react-server-dom-relay/src/ReactDOMServerFB.js new
+87
@@ -0,0 +1,87 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +import type {ReactNodeList} from 'shared/ReactTypes';
11 +
12 +import type {Request} from 'react-server/src/ReactFizzServer';
13 +
14 +import type {Destination} from 'react-server/src/ReactServerStreamConfig';
15 +
16 +import {
17 + createRequest,
18 + startWork,
19 + performWork,
20 + startFlowing,
21 + abort,
22 +} from 'react-server/src/ReactFizzServer';
23 +
24 +import {
25 + createResponseState,
26 + createRootFormatContext,
27 +} from 'react-server/src/ReactServerFormatConfig';
28 +
29 +type Options = {
30 + identifierPrefix?: string,
31 + progressiveChunkSize?: number,
32 + onError: (error: mixed) => void,
33 +};
34 +
35 +opaque type Stream = {
36 + destination: Destination,
37 + request: Request,
38 +};
39 +
40 +function renderToStream(children: ReactNodeList, options: Options): Stream {
41 + const destination = {
42 + buffer: '',
43 + done: false,
44 + fatal: false,
45 + error: null,
46 + };
47 + const request = createRequest(
48 + children,
49 + destination,
50 + createResponseState(options ? options.identifierPrefix : undefined),
51 + createRootFormatContext(undefined),
52 + options ? options.progressiveChunkSize : undefined,
53 + options.onError,
54 + undefined,
55 + undefined,
56 + );
57 + startWork(request);
58 + if (destination.fatal) {
59 + throw destination.error;
60 + }
61 + return {
62 + destination,
63 + request,
64 + };
65 +}
66 +
67 +function abortStream(stream: Stream): void {
68 + abort(stream.request);
69 +}
70 +
71 +function renderNextChunk(stream: Stream): string {
72 + const {request, destination} = stream;
73 + performWork(request);
74 + startFlowing(request);
75 + if (destination.fatal) {
76 + throw destination.error;
77 + }
78 + const chunk = destination.buffer;
79 + destination.buffer = '';
80 + return chunk;
81 +}
82 +
83 +function hasFinished(stream: Stream): boolean {
84 + return stream.destination.done;
85 +}
86 +
87 +export {renderToStream, renderNextChunk, hasFinished, abortStream};
packages/react-server-dom-relay/src/ReactServerStreamConfigFB.js new
+54
@@ -0,0 +1,54 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +export type Destination = {
11 + buffer: string,
12 + done: boolean,
13 + fatal: boolean,
14 + error: mixed,
15 +};
16 +
17 +export type PrecomputedChunk = string;
18 +export type Chunk = string;
19 +
20 +export function scheduleWork(callback: () => void) {
21 + // We don't schedule work in this model, and instead expect performWork to always be called repeatedly.
22 +}
23 +
24 +export function flushBuffered(destination: Destination) {}
25 +
26 +export function beginWriting(destination: Destination) {}
27 +
28 +export function writeChunk(
29 + destination: Destination,
30 + chunk: Chunk | PrecomputedChunk,
31 +): boolean {
32 + destination.buffer += chunk;
33 + return true;
34 +}
35 +
36 +export function completeWriting(destination: Destination) {}
37 +
38 +export function close(destination: Destination) {
39 + destination.done = true;
40 +}
41 +
42 +export function stringToChunk(content: string): Chunk {
43 + return content;
44 +}
45 +
46 +export function stringToPrecomputedChunk(content: string): PrecomputedChunk {
47 + return content;
48 +}
49 +
50 +export function closeWithError(destination: Destination, error: mixed): void {
51 + destination.done = true;
52 + destination.fatal = true;
53 + destination.error = error;
54 +}
packages/react-server-dom-relay/src/__tests__/ReactDOMServerFB-test.internal.js new
+182
@@ -0,0 +1,182 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @emails react-core
8 + */
9 +
10 +'use strict';
11 +
12 +let React;
13 +let ReactDOMServer;
14 +let Suspense;
15 +
16 +describe('ReactDOMServerFB', () => {
17 + beforeEach(() => {
18 + jest.resetModules();
19 + React = require('react');
20 + ReactDOMServer = require('../ReactDOMServerFB');
21 + Suspense = React.Suspense;
22 + });
23 +
24 + const theError = new Error('This is an error');
25 + function Throw() {
26 + throw theError;
27 + }
28 + const theInfinitePromise = new Promise(() => {});
29 + function InfiniteSuspend() {
30 + throw theInfinitePromise;
31 + }
32 +
33 + function readResult(stream) {
34 + let result = '';
35 + while (!ReactDOMServer.hasFinished(stream)) {
36 + result += ReactDOMServer.renderNextChunk(stream);
37 + }
38 + return result;
39 + }
40 +
41 + it('should be able to render basic HTML', async () => {
42 + const stream = ReactDOMServer.renderToStream(<div>hello world</div>, {
43 + onError(x) {
44 + console.error(x);
45 + },
46 + });
47 + const result = readResult(stream);
48 + expect(result).toMatchInlineSnapshot(
49 + `"<div data-reactroot=\\"\\">hello world</div>"`,
50 + );
51 + });
52 +
53 + it('emits all HTML as one unit if we wait until the end to start', async () => {
54 + let hasLoaded = false;
55 + let resolve;
56 + const promise = new Promise(r => (resolve = r));
57 + function Wait() {
58 + if (!hasLoaded) {
59 + throw promise;
60 + }
61 + return 'Done';
62 + }
63 + const stream = ReactDOMServer.renderToStream(
64 + <div>
65 + <Suspense fallback="Loading">
66 + <Wait />
67 + </Suspense>
68 + </div>,
69 + {
70 + onError(x) {
71 + console.error(x);
72 + },
73 + },
74 + );
75 + await jest.runAllTimers();
76 + // Resolve the loading.
77 + hasLoaded = true;
78 + await resolve();
79 +
80 + await jest.runAllTimers();
81 +
82 + const result = readResult(stream);
83 + expect(result).toMatchInlineSnapshot(
84 + `"<div data-reactroot=\\"\\"><!--$-->Done<!-- --><!--/$--></div>"`,
85 + );
86 + });
87 +
88 + it('should throw an error when an error is thrown at the root', () => {
89 + const reportedErrors = [];
90 + const stream = ReactDOMServer.renderToStream(
91 + <div>
92 + <Throw />
93 + </div>,
94 + {
95 + onError(x) {
96 + reportedErrors.push(x);
97 + },
98 + },
99 + );
100 +
101 + let caughtError = null;
102 + let result = '';
103 + try {
104 + result = readResult(stream);
105 + } catch (x) {
106 + caughtError = x;
107 + }
108 + expect(caughtError).toBe(theError);
109 + expect(result).toBe('');
110 + expect(reportedErrors).toEqual([theError]);
111 + });
112 +
113 + it('should throw an error when an error is thrown inside a fallback', () => {
114 + const reportedErrors = [];
115 + const stream = ReactDOMServer.renderToStream(
116 + <div>
117 + <Suspense fallback={<Throw />}>
118 + <InfiniteSuspend />
119 + </Suspense>
120 + </div>,
121 + {
122 + onError(x) {
123 + reportedErrors.push(x);
124 + },
125 + },
126 + );
127 +
128 + let caughtError = null;
129 + let result = '';
130 + try {
131 + result = readResult(stream);
132 + } catch (x) {
133 + caughtError = x;
134 + }
135 + expect(caughtError).toBe(theError);
136 + expect(result).toBe('');
137 + expect(reportedErrors).toEqual([theError]);
138 + });
139 +
140 + it('should not throw an error when an error is thrown inside suspense boundary', async () => {
141 + const reportedErrors = [];
142 + const stream = ReactDOMServer.renderToStream(
143 + <div>
144 + <Suspense fallback={<div>Loading</div>}>
145 + <Throw />
146 + </Suspense>
147 + </div>,
148 + {
149 + onError(x) {
150 + reportedErrors.push(x);
151 + },
152 + },
153 + );
154 +
155 + const result = readResult(stream);
156 + expect(result).toContain('Loading');
157 + expect(reportedErrors).toEqual([theError]);
158 + });
159 +
160 + it('should be able to complete by aborting even if the promise never resolves', () => {
161 + const stream = ReactDOMServer.renderToStream(
162 + <div>
163 + <Suspense fallback={<div>Loading</div>}>
164 + <InfiniteSuspend />
165 + </Suspense>
166 + </div>,
167 + {
168 + onError(x) {
169 + console.error(x);
170 + },
171 + },
172 + );
173 +
174 + const partial = ReactDOMServer.renderNextChunk(stream);
175 + expect(partial).toContain('Loading');
176 +
177 + ReactDOMServer.abortStream(stream);
178 +
179 + const remaining = readResult(stream);
180 + expect(remaining).toEqual('');
181 + });
182 +});
packages/react-server/src/ReactFizzServer.js
+2 -2
@@ -161,7 +161,7 @@ const BUFFERING = 0;
161 const FLOWING = 1;
162 const CLOSED = 2;
163
164 -type Request = {
164 +export opaque type Request = {
165 +destination: Destination,
166 +responseState: ResponseState,
167 +progressiveChunkSize: number,
@@ -1361,7 +1361,7 @@ function retryTask(request: Request, task: Task): void {
1361 }
1362 }
1363
1364 -function performWork(request: Request): void {
1364 +export function performWork(request: Request): void {
1365 if (request.status === CLOSED) {
1366 return;
1367 }
packages/react-server/src/forks/ReactServerStreamConfig.dom-relay.js
+1 -1
@@ -7,4 +7,4 @@
7 * @flow
8 */
9
10 -export * from '../ReactServerStreamConfigNode';
10 +export * from 'react-server-dom-relay/src/ReactServerStreamConfigFB';
scripts/rollup/bundles.js
+10 -8
@@ -238,14 +238,9 @@ const bundles = [
238
239 /******* React DOM Server *******/
240 {
241 - bundleTypes: [
242 - UMD_DEV,
243 - UMD_PROD,
244 - NODE_DEV,
245 - NODE_PROD,
246 - FB_WWW_DEV,
247 - FB_WWW_PROD,
248 - ],
241 + bundleTypes: __EXPERIMENTAL__
242 + ? [UMD_DEV, UMD_PROD, NODE_DEV, NODE_PROD]
243 + : [UMD_DEV, UMD_PROD, NODE_DEV, NODE_PROD, FB_WWW_DEV, FB_WWW_PROD],
244 moduleType: NON_FIBER_RENDERER,
245 entry: 'react-dom/server.browser',
246 global: 'ReactDOMServer',
@@ -287,6 +282,13 @@ const bundles = [
282 global: 'ReactDOMFizzServer',
283 externals: ['react', 'react-dom/server'],
284 },
285 + {
286 + bundleTypes: __EXPERIMENTAL__ ? [FB_WWW_DEV, FB_WWW_PROD] : [],
287 + moduleType: RENDERER,
288 + entry: 'react-server-dom-relay/src/ReactDOMServerFB',
289 + global: 'ReactDOMServer',
290 + externals: ['react', 'react-dom/server'],
291 + },
292
293 /******* React Server DOM Webpack Writer *******/
294 {
scripts/shared/inlinedHostConfigs.js
+5 -1
@@ -83,7 +83,11 @@ module.exports = [
83 },
84 {
85 shortName: 'dom-relay',
86 - entryPoints: ['react-server-dom-relay', 'react-server-dom-relay/server'],
86 + entryPoints: [
87 + 'react-server-dom-relay',
88 + 'react-server-dom-relay/server',
89 + 'react-server-dom-relay/src/ReactDOMServerFB',
90 + ],
91 paths: ['react-dom', 'react-server-dom-relay'],
92 isFlowTyped: true,
93 isServerSupported: true,