Skip to content

Commit

Permalink
[Flight][Static] When prerendering serialize infinite promise when ab…
Browse files Browse the repository at this point in the history
…orting with no reason

When prerendering if you abort the prerender without a reason instead of erroring each remaining task complete it with a promise that never resolve

Unfortunately when you abort with an AbortSignal without a value the aborted reason is defaulted to an AbortError DOMexception. We test for this and basically just say that if you abort with an AbortError DOMException that is equivalent to aborting with nothing. Practically this should be fine because usually you abort with a specific reason.
  • Loading branch information
gnoff committed Aug 15, 2024
1 parent 0a55ade commit f953493
Show file tree
Hide file tree
Showing 9 changed files with 247 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import {
startFlowing,
stopFlowing,
abort,
suspend,
isDefaultAbortError,
} from 'react-server/src/ReactFlightServer';

import {
Expand Down Expand Up @@ -187,10 +189,20 @@ function prerenderToNodeStream(
if (options && options.signal) {
const signal = options.signal;
if (signal.aborted) {
abort(request, (signal: any).reason);
const reason = (signal: any).reason;
if (isDefaultAbortError(reason)) {
suspend(request);
} else {
abort(request, reason);
}
} else {
const listener = () => {
abort(request, (signal: any).reason);
const reason = (signal: any).reason;
if (isDefaultAbortError(reason)) {
suspend(request);
} else {
abort(request, reason);
}
signal.removeEventListener('abort', listener);
};
signal.addEventListener('abort', listener);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import {
startFlowing,
stopFlowing,
abort,
suspend,
isDefaultAbortError,
} from 'react-server/src/ReactFlightServer';

import {
Expand Down Expand Up @@ -146,10 +148,20 @@ function prerender(
if (options && options.signal) {
const signal = options.signal;
if (signal.aborted) {
abort(request, (signal: any).reason);
const reason = (signal: any).reason;
if (isDefaultAbortError(reason)) {
suspend(request);
} else {
abort(request, reason);
}
} else {
const listener = () => {
abort(request, (signal: any).reason);
const reason = (signal: any).reason;
if (isDefaultAbortError(reason)) {
suspend(request);
} else {
abort(request, reason);
}
signal.removeEventListener('abort', listener);
};
signal.addEventListener('abort', listener);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import {
startFlowing,
stopFlowing,
abort,
suspend,
isDefaultAbortError,
} from 'react-server/src/ReactFlightServer';

import {
Expand Down Expand Up @@ -146,10 +148,20 @@ function prerender(
if (options && options.signal) {
const signal = options.signal;
if (signal.aborted) {
abort(request, (signal: any).reason);
const reason = (signal: any).reason;
if (isDefaultAbortError(reason)) {
suspend(request);
} else {
abort(request, reason);
}
} else {
const listener = () => {
abort(request, (signal: any).reason);
const reason = (signal: any).reason;
if (isDefaultAbortError(reason)) {
suspend(request);
} else {
abort(request, reason);
}
signal.removeEventListener('abort', listener);
};
signal.addEventListener('abort', listener);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import {
startFlowing,
stopFlowing,
abort,
suspend,
isDefaultAbortError,
} from 'react-server/src/ReactFlightServer';

import {
Expand Down Expand Up @@ -189,10 +191,20 @@ function prerenderToNodeStream(
if (options && options.signal) {
const signal = options.signal;
if (signal.aborted) {
abort(request, (signal: any).reason);
const reason = (signal: any).reason;
if (isDefaultAbortError(reason)) {
suspend(request);
} else {
abort(request, reason);
}
} else {
const listener = () => {
abort(request, (signal: any).reason);
const reason = (signal: any).reason;
if (isDefaultAbortError(reason)) {
suspend(request);
} else {
abort(request, reason);
}
signal.removeEventListener('abort', listener);
};
signal.addEventListener('abort', listener);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2722,4 +2722,83 @@ describe('ReactFlightDOM', () => {
await readInto(container, fizzReadable);
expect(getMeaningfulChildren(container)).toEqual(<div>hello world</div>);
});

// @gate experimental
it('serializes unfinished tasks with infinite promises when aborting a prerender without a reason', async () => {
let resolveGreeting;
const greetingPromise = new Promise(resolve => {
resolveGreeting = resolve;
});

function App() {
return (
<div>
<Suspense fallback="loading...">
<Greeting />
</Suspense>
</div>
);
}

async function Greeting() {
await greetingPromise;
return 'hello world';
}

const controller = new AbortController();
const {pendingResult} = await serverAct(async () => {
// destructure trick to avoid the act scope from awaiting the returned value
return {
pendingResult: ReactServerDOMStaticServer.prerenderToNodeStream(
<App />,
webpackMap,
{
signal: controller.signal,
},
),
};
});

controller.abort();
resolveGreeting();
const {prelude} = await pendingResult;

const preludeWeb = Readable.toWeb(prelude);
const response = ReactServerDOMClient.createFromReadableStream(preludeWeb);

const {writable: fizzWritable, readable: fizzReadable} = getTestStream();

function ClientApp() {
return use(response);
}

const shellErrors = [];
let abortFizz;
await serverAct(async () => {
const {pipe, abort} = ReactDOMFizzServer.renderToPipeableStream(
React.createElement(ClientApp),
{
onShellError(error) {
shellErrors.push(error.message);
},
},
);
pipe(fizzWritable);
abortFizz = abort;
});

await serverAct(() => {
try {
React.unstable_postpone('abort reason');
} catch (reason) {
abortFizz(reason);
}
});

expect(shellErrors).toEqual([]);

const container = document.createElement('div');
await readInto(container, fizzReadable);
expect(getMeaningfulChildren(container)).toEqual(<div>loading...</div>);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import {
startFlowing,
stopFlowing,
abort,
suspend,
isDefaultAbortError,
} from 'react-server/src/ReactFlightServer';

import {
Expand Down Expand Up @@ -146,10 +148,20 @@ function prerender(
if (options && options.signal) {
const signal = options.signal;
if (signal.aborted) {
abort(request, (signal: any).reason);
const reason = (signal: any).reason;
if (isDefaultAbortError(reason)) {
suspend(request);
} else {
abort(request, reason);
}
} else {
const listener = () => {
abort(request, (signal: any).reason);
const reason = (signal: any).reason;
if (isDefaultAbortError(reason)) {
suspend(request);
} else {
abort(request, reason);
}
signal.removeEventListener('abort', listener);
};
signal.addEventListener('abort', listener);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import {
startFlowing,
stopFlowing,
abort,
suspend,
isDefaultAbortError,
} from 'react-server/src/ReactFlightServer';

import {
Expand Down Expand Up @@ -146,10 +148,20 @@ function prerender(
if (options && options.signal) {
const signal = options.signal;
if (signal.aborted) {
abort(request, (signal: any).reason);
const reason = (signal: any).reason;
if (isDefaultAbortError(reason)) {
suspend(request);
} else {
abort(request, reason);
}
} else {
const listener = () => {
abort(request, (signal: any).reason);
const reason = (signal: any).reason;
if (isDefaultAbortError(reason)) {
suspend(request);
} else {
abort(request, reason);
}
signal.removeEventListener('abort', listener);
};
signal.addEventListener('abort', listener);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import {
startFlowing,
stopFlowing,
abort,
suspend,
isDefaultAbortError,
} from 'react-server/src/ReactFlightServer';

import {
Expand Down Expand Up @@ -189,10 +191,20 @@ function prerenderToNodeStream(
if (options && options.signal) {
const signal = options.signal;
if (signal.aborted) {
abort(request, (signal: any).reason);
const reason = (signal: any).reason;
if (isDefaultAbortError(reason)) {
suspend(request);
} else {
abort(request, reason);
}
} else {
const listener = () => {
abort(request, (signal: any).reason);
const reason = (signal: any).reason;
if (isDefaultAbortError(reason)) {
suspend(request);
} else {
abort(request, reason);
}
signal.removeEventListener('abort', listener);
};
signal.addEventListener('abort', listener);
Expand Down
Loading

0 comments on commit f953493

Please sign in to comment.