Skip to content

Commit 709f948

Browse files
authored
[Fizz] Add FB specific streaming API and build (#21337)
Add FB specific streaming API and build
1 parent af5037a commit 709f948

File tree

8 files changed

+342
-14
lines changed

8 files changed

+342
-14
lines changed

packages/react-server-dom-relay/package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
"scheduler": "^0.11.0"
1313
},
1414
"peerDependencies": {
15-
"react": "^17.0.0",
16-
"react-dom": "^17.0.0"
15+
"react": "^17.0.0"
1716
}
1817
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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};
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
}
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ const BUFFERING = 0;
161161
const FLOWING = 1;
162162
const CLOSED = 2;
163163

164-
type Request = {
164+
export opaque type Request = {
165165
+destination: Destination,
166166
+responseState: ResponseState,
167167
+progressiveChunkSize: number,
@@ -1361,7 +1361,7 @@ function retryTask(request: Request, task: Task): void {
13611361
}
13621362
}
13631363

1364-
function performWork(request: Request): void {
1364+
export function performWork(request: Request): void {
13651365
if (request.status === CLOSED) {
13661366
return;
13671367
}

packages/react-server/src/forks/ReactServerStreamConfig.dom-relay.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@
77
* @flow
88
*/
99

10-
export * from '../ReactServerStreamConfigNode';
10+
export * from 'react-server-dom-relay/src/ReactServerStreamConfigFB';

scripts/rollup/bundles.js

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -238,14 +238,9 @@ const bundles = [
238238

239239
/******* React DOM Server *******/
240240
{
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],
249244
moduleType: NON_FIBER_RENDERER,
250245
entry: 'react-dom/server.browser',
251246
global: 'ReactDOMServer',
@@ -287,6 +282,13 @@ const bundles = [
287282
global: 'ReactDOMFizzServer',
288283
externals: ['react', 'react-dom/server'],
289284
},
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+
},
290292

291293
/******* React Server DOM Webpack Writer *******/
292294
{

0 commit comments

Comments
 (0)