Skip to content

Commit 14c2be8

Browse files
authored
Rename Node SSR Callbacks to onShellReady/onAllReady and Other Fixes (#24030)
* I forgot to call onFatalError I can't figure out how to write a test for this because it only happens when there is a bug in React itself which would then be fixed if we found it. We're also covered by the protection of ReadableStream which doesn't leak other errors to us. * Abort requests if the reader cancels No need to continue computing at this point. * Abort requests if node streams get destroyed This is if the downstream cancels is for example. * Rename Node APIs for Parity with allReady The "Complete" terminology is a little misleading because not everything has been written yet. It's just "Ready" to be written now. onShellReady onShellError onAllReady * 'close' should be enough
1 parent cb1e7b1 commit 14c2be8

12 files changed

Lines changed: 151 additions & 58 deletions

‎fixtures/ssr/server/render.js‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,13 @@ export default function render(url, res) {
2222
letdidError=false;
2323
const{pipe, abort}=renderToPipeableStream(<Appassets={assets}/>,{
2424
bootstrapScripts: [assets['main.js']],
25-
onCompleteShell(){
25+
onShellReady(){
2626
// If something errored before we started streaming, we set the error code appropriately.
2727
res.statusCode=didError ? 500 : 200;
2828
res.setHeader('Content-type','text/html');
2929
pipe(res);
3030
},
31-
onErrorShell(x){
31+
onShellError(x){
3232
// Something errored before we could complete the shell so we emit an alternative shell.
3333
res.statusCode=500;
3434
res.send('<!doctype><p>Error</p>');

‎fixtures/ssr2/server/render.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ module.exports = function render(url, res) {
4949
res.setHeader('Content-type','text/html');
5050
pipe(res);
5151
},
52-
onErrorShell(x){
52+
onShellError(x){
5353
// Something errored before we could complete the shell so we emit an alternative shell.
5454
res.statusCode=500;
5555
res.send('<!doctype><p>Error</p>');

‎packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -914,7 +914,7 @@ describe('ReactDOMFizzServer', () => {
914914
</Suspense>,
915915
{
916916
identifierPrefix: 'A_',
917-
onCompleteShell(){
917+
onShellReady(){
918918
writableA.write('<div id="container-A">');
919919
pipe(writableA);
920920
writableA.write('</div>');
@@ -933,7 +933,7 @@ describe('ReactDOMFizzServer', () => {
933933
</Suspense>,
934934
{
935935
identifierPrefix: 'B_',
936-
onCompleteShell(){
936+
onShellReady(){
937937
writableB.write('<div id="container-B">');
938938
pipe(writableB);
939939
writableB.write('</div>');
@@ -1168,7 +1168,7 @@ describe('ReactDOMFizzServer', () => {
11681168

11691169
{
11701170
namespaceURI: 'http://www.w3.org/2000/svg',
1171-
onCompleteShell(){
1171+
onShellReady(){
11721172
writable.write('<svg>');
11731173
pipe(writable);
11741174
writable.write('</svg>');

‎packages/react-dom/src/__tests__/ReactDOMFizzServerBrowser-test.js‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,4 +209,43 @@ describe('ReactDOMFizzServer', () => {
209209
constresult=awaitreadResult(stream);
210210
expect(result).toContain('Loading');
211211
});
212+
213+
// @gate experimental
214+
it('should not continue rendering after the reader cancels',async()=>{
215+
lethasLoaded=false;
216+
letresolve;
217+
letisComplete=false;
218+
letrendered=false;
219+
constpromise=newPromise(r=>(resolve=r));
220+
functionWait(){
221+
if(!hasLoaded){
222+
throwpromise;
223+
}
224+
rendered=true;
225+
return'Done';
226+
}
227+
conststream=awaitReactDOMFizzServer.renderToReadableStream(
228+
<div>
229+
<Suspensefallback={<div>Loading</div>}>
230+
<Wait/> />
231+
</Suspense>
232+
</div>,
233+
);
234+
235+
stream.allReady.then(()=>(isComplete=true));
236+
237+
expect(rendered).toBe(false);
238+
expect(isComplete).toBe(false);
239+
240+
constreader=stream.getReader();
241+
reader.cancel();
242+
243+
hasLoaded=true;
244+
resolve();
245+
246+
awaitjest.runAllTimers();
247+
248+
expect(rendered).toBe(false);
249+
expect(isComplete).toBe(true);
250+
});
212251
});

‎packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js‎

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ describe('ReactDOMFizzServer', () => {
138138
</div>,
139139

140140
{
141-
onCompleteAll(){
141+
onAllReady(){
142142
isCompleteCalls++;
143143
},
144144
},
@@ -179,7 +179,7 @@ describe('ReactDOMFizzServer', () => {
179179
onError(x){
180180
reportedErrors.push(x);
181181
},
182-
onErrorShell(x){
182+
onShellError(x){
183183
reportedShellErrors.push(x);
184184
},
185185
},
@@ -213,7 +213,7 @@ describe('ReactDOMFizzServer', () => {
213213
onError(x){
214214
reportedErrors.push(x);
215215
},
216-
onErrorShell(x){
216+
onShellError(x){
217217
reportedShellErrors.push(x);
218218
},
219219
},
@@ -244,7 +244,7 @@ describe('ReactDOMFizzServer', () => {
244244
onError(x){
245245
reportedErrors.push(x);
246246
},
247-
onErrorShell(x){
247+
onShellError(x){
248248
reportedShellErrors.push(x);
249249
},
250250
},
@@ -298,7 +298,7 @@ describe('ReactDOMFizzServer', () => {
298298
</div>,
299299

300300
{
301-
onCompleteAll(){
301+
onAllReady(){
302302
isCompleteCalls++;
303303
},
304304
},
@@ -333,7 +333,7 @@ describe('ReactDOMFizzServer', () => {
333333
</div>,
334334

335335
{
336-
onCompleteAll(){
336+
onAllReady(){
337337
isCompleteCalls++;
338338
},
339339
},
@@ -537,4 +537,49 @@ describe('ReactDOMFizzServer', () => {
537537
expect(output.result).not.toContain('context never found');
538538
expect(output.result).toContain('OK');
539539
});
540+
541+
// @gate experimental
542+
it('should not continue rendering after the writable ends unexpectedly',async()=>{
543+
lethasLoaded=false;
544+
letresolve;
545+
letisComplete=false;
546+
letrendered=false;
547+
constpromise=newPromise(r=>(resolve=r));
548+
functionWait(){
549+
if(!hasLoaded){
550+
throwpromise;
551+
}
552+
rendered=true;
553+
return'Done';
554+
}
555+
const{writable, completed}=getTestWritable();
556+
const{pipe}=ReactDOMFizzServer.renderToPipeableStream(
557+
<div>
558+
<Suspensefallback={<div>Loading</div>}>
559+
<Wait/>
560+
</Suspense>
561+
</div>,
562+
{
563+
onAllReady(){
564+
isComplete=true;
565+
},
566+
},
567+
);
568+
pipe(writable);
569+
570+
expect(rendered).toBe(false);
571+
expect(isComplete).toBe(false);
572+
573+
writable.end();
574+
575+
awaitjest.runAllTimers();
576+
577+
hasLoaded=true;
578+
resolve();
579+
580+
awaitcompleted;
581+
582+
expect(rendered).toBe(false);
583+
expect(isComplete).toBe(true);
584+
});
540585
});

‎packages/react-dom/src/__tests__/utils/ReactDOMServerIntegrationTestUtils.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ module.exports = function(initModules) {
155155
newPromise((resolve,reject)=>{
156156
constwritable=newDrainWritable();
157157
consts=ReactDOMServer.renderToPipeableStream(reactElement,{
158-
onErrorShell(e){
158+
onShellError(e){
159159
reject(e);
160160
},
161161
});

‎packages/react-dom/src/server/ReactDOMFizzServerBrowser.js‎

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -46,25 +46,27 @@ function renderToReadableStream(
4646
): Promise<ReactDOMServerReadableStream>{
4747
returnnewPromise((resolve,reject)=>{
4848
letonFatalError;
49-
letonCompleteAll;
49+
letonAllReady;
5050
constallReady=newPromise((res,rej)=>{
51-
onCompleteAll=res;
51+
onAllReady=res;
5252
onFatalError=rej;
5353
});
5454

55-
functiononCompleteShell(){
55+
functiononShellReady(){
5656
conststream: ReactDOMServerReadableStream=(newReadableStream({
5757
type: 'bytes',
5858
pull(controller){
5959
startFlowing(request,controller);
6060
},
61-
cancel(reason){},
61+
cancel(reason){
62+
abort(request);
63+
},
6264
}): any);
6365
// TODO: Move to sub-classing ReadableStream.
6466
stream.allReady=allReady;
6567
resolve(stream);
6668
}
67-
functiononErrorShell(error: mixed){
69+
functiononShellError(error: mixed){
6870
reject(error);
6971
}
7072
constrequest=createRequest(
@@ -79,9 +81,9 @@ function renderToReadableStream(
7981
createRootFormatContext(options ? options.namespaceURI : undefined),
8082
options ? options.progressiveChunkSize : undefined,
8183
options ? options.onError : undefined,
82-
onCompleteAll,
83-
onCompleteShell,
84-
onErrorShell,
84+
onAllReady,
85+
onShellReady,
86+
onShellError,
8587
onFatalError,
8688
);
8789
if(options&&options.signal){

‎packages/react-dom/src/server/ReactDOMFizzServerNode.js‎

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ function createDrainHandler(destination, request) {
2828
return()=>startFlowing(request,destination);
2929
}
3030

31+
functioncreateAbortHandler(request){
32+
return()=>abort(request);
33+
}
34+
3135
typeOptions={|
3236
identifierPrefix?: string,
3337
namespaceURI?: string,
@@ -36,9 +40,9 @@ type Options = {|
3640
bootstrapScripts?: Array<string>,
3741
bootstrapModules?: Array<string>,
3842
progressiveChunkSize?: number,
39-
onCompleteShell?: ()=>void,
40-
onErrorShell?: ()=>void,
41-
onCompleteAll?: ()=>void,
43+
onShellReady?: ()=>void,
44+
onShellError?: ()=>void,
45+
onAllReady?: ()=>void,
4246
onError?: (error: mixed)=>void,
4347
|};
4448

@@ -62,9 +66,9 @@ function createRequestImpl(children: ReactNodeList, options: void | Options) {
6266
createRootFormatContext(options ? options.namespaceURI : undefined),
6367
options ? options.progressiveChunkSize : undefined,
6468
options ? options.onError : undefined,
65-
options ? options.onCompleteAll : undefined,
66-
options ? options.onCompleteShell : undefined,
67-
options ? options.onErrorShell : undefined,
69+
options ? options.onAllReady : undefined,
70+
options ? options.onShellReady : undefined,
71+
options ? options.onShellError : undefined,
6872
undefined,
6973
);
7074
}
@@ -86,6 +90,7 @@ function renderToPipeableStream(
8690
hasStartedFlowing = true;
8791
startFlowing(request, destination);
8892
destination.on('drain', createDrainHandler(destination, request));
93+
destination.on('close', createAbortHandler(request));
8994
return destination;
9095
},
9196
abort(){

‎packages/react-dom/src/server/ReactDOMLegacyServerBrowser.js‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ function renderToStringImpl(
5353
};
5454

5555
letreadyToStream=false;
56-
functiononCompleteShell(){
56+
functiononShellReady(){
5757
readyToStream=true;
5858
}
5959
constrequest=createRequest(
@@ -66,7 +66,7 @@ function renderToStringImpl(
6666
Infinity,
6767
onError,
6868
undefined,
69-
onCompleteShell,
69+
onShellReady,
7070
undefined,
7171
undefined,
7272
);

‎packages/react-dom/src/server/ReactDOMLegacyServerNode.js‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ function renderToNodeStreamImpl(
6868
options: void|ServerOptions,
6969
generateStaticMarkup: boolean,
7070
): Readable{
71-
functiononCompleteAll(){
71+
functiononAllReady(){
7272
// We wait until everything has loaded before starting to write.
7373
// That way we only end up with fully resolved HTML even if we suspend.
7474
destination.startedFlowing=true;
@@ -81,7 +81,7 @@ function renderToNodeStreamImpl(
8181
createRootFormatContext(),
8282
Infinity,
8383
onError,
84-
onCompleteAll,
84+
onAllReady,
8585
undefined,
8686
undefined,
8787
);

0 commit comments

Comments
 (0)