Skip to content

Commit

Permalink
Stop flowing and then abort if a stream is cancelled
Browse files Browse the repository at this point in the history
  • Loading branch information
sebmarkbage committed Sep 22, 2023
1 parent 56b1447 commit b92a159
Show file tree
Hide file tree
Showing 14 changed files with 128 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,8 @@ describe('ReactDOMFizzServerBrowser', () => {
expect(isComplete).toBe(false);

const reader = stream.getReader();
reader.cancel();
await reader.read();
await reader.cancel();

expect(errors).toEqual([
'The render was aborted by the server without a reason.',
Expand All @@ -355,6 +356,10 @@ describe('ReactDOMFizzServerBrowser', () => {

expect(rendered).toBe(false);
expect(isComplete).toBe(true);

expect(errors).toEqual([
'The render was aborted by the server without a reason.',
]);
});

it('should stream large contents that might overlow individual buffers', async () => {
Expand Down
3 changes: 3 additions & 0 deletions packages/react-dom/src/server/ReactDOMFizzServerBrowser.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
resumeRequest,
startWork,
startFlowing,
stopFlowing,
abort,
} from 'react-server/src/ReactFizzServer';

Expand Down Expand Up @@ -78,6 +79,7 @@ function renderToReadableStream(
startFlowing(request, controller);
},
cancel: (reason): ?Promise<void> => {
stopFlowing(request);
abort(request);
},
},
Expand Down Expand Up @@ -158,6 +160,7 @@ function resume(
startFlowing(request, controller);
},
cancel: (reason): ?Promise<void> => {
stopFlowing(request);
abort(request);
},
},
Expand Down
2 changes: 2 additions & 0 deletions packages/react-dom/src/server/ReactDOMFizzServerBun.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
createRequest,
startWork,
startFlowing,
stopFlowing,
abort,
} from 'react-server/src/ReactFizzServer';

Expand Down Expand Up @@ -68,6 +69,7 @@ function renderToReadableStream(
startFlowing(request, controller);
},
cancel: (reason): ?Promise<void> => {
stopFlowing(request);
abort(request);
},
},
Expand Down
3 changes: 3 additions & 0 deletions packages/react-dom/src/server/ReactDOMFizzServerEdge.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
resumeRequest,
startWork,
startFlowing,
stopFlowing,
abort,
} from 'react-server/src/ReactFizzServer';

Expand Down Expand Up @@ -78,6 +79,7 @@ function renderToReadableStream(
startFlowing(request, controller);
},
cancel: (reason): ?Promise<void> => {
stopFlowing(request);
abort(request);
},
},
Expand Down Expand Up @@ -158,6 +160,7 @@ function resume(
startFlowing(request, controller);
},
cancel: (reason): ?Promise<void> => {
stopFlowing(request);
abort(request);
},
},
Expand Down
19 changes: 15 additions & 4 deletions packages/react-dom/src/server/ReactDOMFizzServerNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
resumeRequest,
startWork,
startFlowing,
stopFlowing,
abort,
} from 'react-server/src/ReactFizzServer';

Expand All @@ -36,8 +37,18 @@ function createDrainHandler(destination: Destination, request: Request) {
}

function createAbortHandler(request: Request, reason: string) {
// eslint-disable-next-line react-internal/prod-error-codes
return () => abort(request, new Error(reason));
return () => {
// eslint-disable-next-line react-internal/prod-error-codes
abort(request, new Error(reason));
};
}

function createCancelHandler(request: Request, reason: string) {
return () => {
stopFlowing(request);
// eslint-disable-next-line react-internal/prod-error-codes
abort(request, new Error(reason));
};
}

type Options = {
Expand Down Expand Up @@ -122,14 +133,14 @@ function renderToPipeableStream(
destination.on('drain', createDrainHandler(destination, request));
destination.on(
'error',
createAbortHandler(
createCancelHandler(
request,
'The destination stream errored while writing data.',
),
);
destination.on(
'close',
createAbortHandler(request, 'The destination stream closed early.'),
createCancelHandler(request, 'The destination stream closed early.'),
);
return destination;
},
Expand Down
5 changes: 5 additions & 0 deletions packages/react-dom/src/server/ReactDOMFizzStaticBrowser.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
createPrerenderRequest,
startWork,
startFlowing,
stopFlowing,
abort,
getPostponedState,
} from 'react-server/src/ReactFizzServer';
Expand Down Expand Up @@ -61,6 +62,10 @@ function prerender(
pull: (controller): ?Promise<void> => {
startFlowing(request, controller);
},
cancel: (reason): ?Promise<void> => {
stopFlowing(request);
abort(request);
},
},
// $FlowFixMe[prop-missing] size() methods are not allowed on byte streams.
{highWaterMark: 0},
Expand Down
5 changes: 5 additions & 0 deletions packages/react-dom/src/server/ReactDOMFizzStaticEdge.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
createPrerenderRequest,
startWork,
startFlowing,
stopFlowing,
abort,
getPostponedState,
} from 'react-server/src/ReactFizzServer';
Expand Down Expand Up @@ -61,6 +62,10 @@ function prerender(
pull: (controller): ?Promise<void> => {
startFlowing(request, controller);
},
cancel: (reason): ?Promise<void> => {
stopFlowing(request);
abort(request);
},
},
// $FlowFixMe[prop-missing] size() methods are not allowed on byte streams.
{highWaterMark: 0},
Expand Down
2 changes: 2 additions & 0 deletions packages/react-server-dom-esm/src/ReactFlightDOMServerNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
createRequest,
startWork,
startFlowing,
stopFlowing,
abort,
} from 'react-server/src/ReactFlightServer';

Expand Down Expand Up @@ -90,6 +91,7 @@ function renderToPipeableStream(
return destination;
},
abort(reason: mixed) {
stopFlowing(request);
abort(request, reason);
},
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
createRequest,
startWork,
startFlowing,
stopFlowing,
abort,
} from 'react-server/src/ReactFlightServer';

Expand Down Expand Up @@ -78,7 +79,10 @@ function renderToReadableStream(
pull: (controller): ?Promise<void> => {
startFlowing(request, controller);
},
cancel: (reason): ?Promise<void> => {},
cancel: (reason): ?Promise<void> => {
stopFlowing(request);
abort(request, reason);
},
},
// $FlowFixMe[prop-missing] size() methods are not allowed on byte streams.
{highWaterMark: 0},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
createRequest,
startWork,
startFlowing,
stopFlowing,
abort,
} from 'react-server/src/ReactFlightServer';

Expand Down Expand Up @@ -78,7 +79,10 @@ function renderToReadableStream(
pull: (controller): ?Promise<void> => {
startFlowing(request, controller);
},
cancel: (reason): ?Promise<void> => {},
cancel: (reason): ?Promise<void> => {
stopFlowing(request);
abort(request, reason);
},
},
// $FlowFixMe[prop-missing] size() methods are not allowed on byte streams.
{highWaterMark: 0},
Expand Down
20 changes: 20 additions & 0 deletions packages/react-server-dom-webpack/src/ReactFlightDOMServerNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
createRequest,
startWork,
startFlowing,
stopFlowing,
abort,
} from 'react-server/src/ReactFlightServer';

Expand Down Expand Up @@ -51,6 +52,14 @@ function createDrainHandler(destination: Destination, request: Request) {
return () => startFlowing(request, destination);
}

function createCancelHandler(request: Request, reason: string) {
return () => {
stopFlowing(request);
// eslint-disable-next-line react-internal/prod-error-codes
abort(request, new Error(reason));
};
}

type Options = {
onError?: (error: mixed) => void,
onPostpone?: (reason: string) => void,
Expand Down Expand Up @@ -88,6 +97,17 @@ function renderToPipeableStream(
hasStartedFlowing = true;
startFlowing(request, destination);
destination.on('drain', createDrainHandler(destination, request));
destination.on(
'error',
createCancelHandler(
request,
'The destination stream errored while writing data.',
),
);
destination.on(
'close',
createCancelHandler(request, 'The destination stream closed early.'),
);
return destination;
},
abort(reason: mixed) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1222,4 +1222,53 @@ describe('ReactFlightDOMBrowser', () => {

expect(postponed).toBe('testing postpone');
});

it('should not continue rendering after the reader cancels', async () => {
let hasLoaded = false;
let resolve;
let rendered = false;
const promise = new Promise(r => (resolve = r));
function Wait() {
if (!hasLoaded) {
throw promise;
}
rendered = true;
return 'Done';
}
const errors = [];
const stream = await ReactServerDOMServer.renderToReadableStream(
<div>
<Suspense fallback={<div>Loading</div>}>
<Wait />
</Suspense>
</div>,
null,
{
onError(x) {
errors.push(x.message);
},
},
);

expect(rendered).toBe(false);

const reader = stream.getReader();
await reader.read();
await reader.cancel();

expect(errors).toEqual([
'The render was aborted by the server without a reason.',
]);

hasLoaded = true;
resolve();

await jest.runAllTimers();

expect(rendered).toBe(false);

expect(errors).toEqual([
'The render was aborted by the server without a reason.',
]);
});
});
4 changes: 4 additions & 0 deletions packages/react-server/src/ReactFizzServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -3998,6 +3998,10 @@ export function startFlowing(request: Request, destination: Destination): void {
}
}

export function stopFlowing(request: Request): void {
request.destination = null;
}

// This is called to early terminate a request. It puts all pending boundaries in client rendered state.
export function abort(request: Request, reason: mixed): void {
try {
Expand Down
4 changes: 4 additions & 0 deletions packages/react-server/src/ReactFlightServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -1570,6 +1570,10 @@ export function startFlowing(request: Request, destination: Destination): void {
}
}

export function stopFlowing(request: Request): void {
request.destination = null;
}

// This is called to early terminate a request. It creates an error at all pending tasks.
export function abort(request: Request, reason: mixed): void {
try {
Expand Down

0 comments on commit b92a159

Please sign in to comment.