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