[Flight] model halting as never delivered chunks (#30740)

stacked on: #30731

We've refined the model of halting a prerender. Now when you abort
during a prerender we simply omit the rows that would complete the
flight render. This is analagous to prerendering in Fizz where you must
resume the prerender to actually result in errors propagating in the
postponed holes. We don't have a resume yet for flight and it's not
entirely clear how that will work however the key insight here is that
deciding whether the never resolving rows are an error or not should
really be done on the consuming side rather than in the producer.

This PR also reintroduces the logs for the abort error/postpone when
prerendering which will give you some indication that something wasn't
finished when the prerender was aborted.
This commit is contained in:
Josh Story 2024-08-19 19:34:20 -07:00 committed by GitHub
parent 0fa9476b9b
commit a960b92cb9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 253 additions and 283 deletions

View File

@ -46,7 +46,6 @@ import {
enableRefAsProp, enableRefAsProp,
enableFlightReadableStream, enableFlightReadableStream,
enableOwnerStacks, enableOwnerStacks,
enableHalt,
} from 'shared/ReactFeatureFlags'; } from 'shared/ReactFeatureFlags';
import { import {
@ -1997,20 +1996,6 @@ function resolvePostponeDev(
} }
} }
function resolveBlocked(response: Response, id: number): void {
const chunks = response._chunks;
const chunk = chunks.get(id);
if (!chunk) {
chunks.set(id, createBlockedChunk(response));
} else if (chunk.status === PENDING) {
// This chunk as contructed via other means but it is actually a blocked chunk
// so we update it here. We check the status because it might have been aborted
// before we attempted to resolve it.
const blockedChunk: BlockedChunk<mixed> = (chunk: any);
blockedChunk.status = BLOCKED;
}
}
function resolveHint<Code: HintCode>( function resolveHint<Code: HintCode>(
response: Response, response: Response,
code: Code, code: Code,
@ -2637,13 +2622,6 @@ function processFullStringRow(
} }
} }
// Fallthrough // Fallthrough
case 35 /* "#" */: {
if (enableHalt) {
resolveBlocked(response, id);
return;
}
}
// Fallthrough
default: /* """ "{" "[" "t" "f" "n" "0" - "9" */ { default: /* """ "{" "[" "t" "f" "n" "0" - "9" */ {
// We assume anything else is JSON. // We assume anything else is JSON.
resolveModel(response, id, row); resolveModel(response, id, row);

View File

@ -20,15 +20,13 @@ import type {Thenable} from 'shared/ReactTypes';
import {Readable} from 'stream'; import {Readable} from 'stream';
import {enableHalt} from 'shared/ReactFeatureFlags';
import { import {
createRequest, createRequest,
createPrerenderRequest,
startWork, startWork,
startFlowing, startFlowing,
stopFlowing, stopFlowing,
abort, abort,
halt,
} from 'react-server/src/ReactFlightServer'; } from 'react-server/src/ReactFlightServer';
import { import {
@ -175,35 +173,27 @@ function prerenderToNodeStream(
resolve({prelude: readable}); resolve({prelude: readable});
} }
const request = createRequest( const request = createPrerenderRequest(
model, model,
moduleBasePath, moduleBasePath,
onAllReady,
onFatalError,
options ? options.onError : undefined, options ? options.onError : undefined,
options ? options.identifierPrefix : undefined, options ? options.identifierPrefix : undefined,
options ? options.onPostpone : undefined, options ? options.onPostpone : undefined,
options ? options.temporaryReferences : undefined, options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined, __DEV__ && options ? options.filterStackFrame : undefined,
onAllReady,
onFatalError,
); );
if (options && options.signal) { if (options && options.signal) {
const signal = options.signal; const signal = options.signal;
if (signal.aborted) { if (signal.aborted) {
const reason = (signal: any).reason; const reason = (signal: any).reason;
if (enableHalt) { abort(request, reason);
halt(request, reason);
} else {
abort(request, reason);
}
} else { } else {
const listener = () => { const listener = () => {
const reason = (signal: any).reason; const reason = (signal: any).reason;
if (enableHalt) { abort(request, reason);
halt(request, reason);
} else {
abort(request, reason);
}
signal.removeEventListener('abort', listener); signal.removeEventListener('abort', listener);
}; };
signal.addEventListener('abort', listener); signal.addEventListener('abort', listener);

View File

@ -12,15 +12,13 @@ import type {Thenable} from 'shared/ReactTypes';
import type {ClientManifest} from './ReactFlightServerConfigTurbopackBundler'; import type {ClientManifest} from './ReactFlightServerConfigTurbopackBundler';
import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig'; import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig';
import {enableHalt} from 'shared/ReactFeatureFlags';
import { import {
createRequest, createRequest,
createPrerenderRequest,
startWork, startWork,
startFlowing, startFlowing,
stopFlowing, stopFlowing,
abort, abort,
halt,
} from 'react-server/src/ReactFlightServer'; } from 'react-server/src/ReactFlightServer';
import { import {
@ -134,35 +132,27 @@ function prerender(
); );
resolve({prelude: stream}); resolve({prelude: stream});
} }
const request = createRequest( const request = createPrerenderRequest(
model, model,
turbopackMap, turbopackMap,
onAllReady,
onFatalError,
options ? options.onError : undefined, options ? options.onError : undefined,
options ? options.identifierPrefix : undefined, options ? options.identifierPrefix : undefined,
options ? options.onPostpone : undefined, options ? options.onPostpone : undefined,
options ? options.temporaryReferences : undefined, options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined, __DEV__ && options ? options.filterStackFrame : undefined,
onAllReady,
onFatalError,
); );
if (options && options.signal) { if (options && options.signal) {
const signal = options.signal; const signal = options.signal;
if (signal.aborted) { if (signal.aborted) {
const reason = (signal: any).reason; const reason = (signal: any).reason;
if (enableHalt) { abort(request, reason);
halt(request, reason);
} else {
abort(request, reason);
}
} else { } else {
const listener = () => { const listener = () => {
const reason = (signal: any).reason; const reason = (signal: any).reason;
if (enableHalt) { abort(request, reason);
halt(request, reason);
} else {
abort(request, reason);
}
signal.removeEventListener('abort', listener); signal.removeEventListener('abort', listener);
}; };
signal.addEventListener('abort', listener); signal.addEventListener('abort', listener);

View File

@ -12,15 +12,13 @@ import type {Thenable} from 'shared/ReactTypes';
import type {ClientManifest} from './ReactFlightServerConfigTurbopackBundler'; import type {ClientManifest} from './ReactFlightServerConfigTurbopackBundler';
import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig'; import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig';
import {enableHalt} from 'shared/ReactFeatureFlags';
import { import {
createRequest, createRequest,
createPrerenderRequest,
startWork, startWork,
startFlowing, startFlowing,
stopFlowing, stopFlowing,
abort, abort,
halt,
} from 'react-server/src/ReactFlightServer'; } from 'react-server/src/ReactFlightServer';
import { import {
@ -134,35 +132,27 @@ function prerender(
); );
resolve({prelude: stream}); resolve({prelude: stream});
} }
const request = createRequest( const request = createPrerenderRequest(
model, model,
turbopackMap, turbopackMap,
onAllReady,
onFatalError,
options ? options.onError : undefined, options ? options.onError : undefined,
options ? options.identifierPrefix : undefined, options ? options.identifierPrefix : undefined,
options ? options.onPostpone : undefined, options ? options.onPostpone : undefined,
options ? options.temporaryReferences : undefined, options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined, __DEV__ && options ? options.filterStackFrame : undefined,
onAllReady,
onFatalError,
); );
if (options && options.signal) { if (options && options.signal) {
const signal = options.signal; const signal = options.signal;
if (signal.aborted) { if (signal.aborted) {
const reason = (signal: any).reason; const reason = (signal: any).reason;
if (enableHalt) { abort(request, reason);
halt(request, reason);
} else {
abort(request, reason);
}
} else { } else {
const listener = () => { const listener = () => {
const reason = (signal: any).reason; const reason = (signal: any).reason;
if (enableHalt) { abort(request, reason);
halt(request, reason);
} else {
abort(request, reason);
}
signal.removeEventListener('abort', listener); signal.removeEventListener('abort', listener);
}; };
signal.addEventListener('abort', listener); signal.addEventListener('abort', listener);

View File

@ -20,15 +20,13 @@ import type {Thenable} from 'shared/ReactTypes';
import {Readable} from 'stream'; import {Readable} from 'stream';
import {enableHalt} from 'shared/ReactFeatureFlags';
import { import {
createRequest, createRequest,
createPrerenderRequest,
startWork, startWork,
startFlowing, startFlowing,
stopFlowing, stopFlowing,
abort, abort,
halt,
} from 'react-server/src/ReactFlightServer'; } from 'react-server/src/ReactFlightServer';
import { import {
@ -177,35 +175,27 @@ function prerenderToNodeStream(
resolve({prelude: readable}); resolve({prelude: readable});
} }
const request = createRequest( const request = createPrerenderRequest(
model, model,
turbopackMap, turbopackMap,
onAllReady,
onFatalError,
options ? options.onError : undefined, options ? options.onError : undefined,
options ? options.identifierPrefix : undefined, options ? options.identifierPrefix : undefined,
options ? options.onPostpone : undefined, options ? options.onPostpone : undefined,
options ? options.temporaryReferences : undefined, options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined, __DEV__ && options ? options.filterStackFrame : undefined,
onAllReady,
onFatalError,
); );
if (options && options.signal) { if (options && options.signal) {
const signal = options.signal; const signal = options.signal;
if (signal.aborted) { if (signal.aborted) {
const reason = (signal: any).reason; const reason = (signal: any).reason;
if (enableHalt) { abort(request, reason);
halt(request, reason);
} else {
abort(request, reason);
}
} else { } else {
const listener = () => { const listener = () => {
const reason = (signal: any).reason; const reason = (signal: any).reason;
if (enableHalt) { abort(request, reason);
halt(request, reason);
} else {
abort(request, reason);
}
signal.removeEventListener('abort', listener); signal.removeEventListener('abort', listener);
}; };
signal.addEventListener('abort', listener); signal.addEventListener('abort', listener);

View File

@ -2724,7 +2724,7 @@ describe('ReactFlightDOM', () => {
}); });
// @gate enableHalt // @gate enableHalt
it('serializes unfinished tasks with infinite promises when aborting a prerender', async () => { it('does not propagate abort reasons errors when aborting a prerender', async () => {
let resolveGreeting; let resolveGreeting;
const greetingPromise = new Promise(resolve => { const greetingPromise = new Promise(resolve => {
resolveGreeting = resolve; resolveGreeting = resolve;
@ -2746,6 +2746,7 @@ describe('ReactFlightDOM', () => {
} }
const controller = new AbortController(); const controller = new AbortController();
const errors = [];
const {pendingResult} = await serverAct(async () => { const {pendingResult} = await serverAct(async () => {
// destructure trick to avoid the act scope from awaiting the returned value // destructure trick to avoid the act scope from awaiting the returned value
return { return {
@ -2754,15 +2755,20 @@ describe('ReactFlightDOM', () => {
webpackMap, webpackMap,
{ {
signal: controller.signal, signal: controller.signal,
onError(err) {
errors.push(err);
},
}, },
), ),
}; };
}); });
controller.abort(); controller.abort('boom');
resolveGreeting(); resolveGreeting();
const {prelude} = await pendingResult; const {prelude} = await pendingResult;
expect(errors).toEqual(['boom']);
const preludeWeb = Readable.toWeb(prelude); const preludeWeb = Readable.toWeb(prelude);
const response = ReactServerDOMClient.createFromReadableStream(preludeWeb); const response = ReactServerDOMClient.createFromReadableStream(preludeWeb);
@ -2772,7 +2778,7 @@ describe('ReactFlightDOM', () => {
return use(response); return use(response);
} }
const errors = []; errors.length = 0;
let abortFizz; let abortFizz;
await serverAct(async () => { await serverAct(async () => {
const {pipe, abort} = ReactDOMFizzServer.renderToPipeableStream( const {pipe, abort} = ReactDOMFizzServer.renderToPipeableStream(
@ -2788,10 +2794,10 @@ describe('ReactFlightDOM', () => {
}); });
await serverAct(() => { await serverAct(() => {
abortFizz('boom'); abortFizz('bam');
}); });
expect(errors).toEqual(['boom']); expect(errors).toEqual(['bam']);
const container = document.createElement('div'); const container = document.createElement('div');
await readInto(container, fizzReadable); await readInto(container, fizzReadable);
@ -2861,7 +2867,7 @@ describe('ReactFlightDOM', () => {
it('will halt unfinished chunks inside Suspense when aborting a prerender', async () => { it('will halt unfinished chunks inside Suspense when aborting a prerender', async () => {
const controller = new AbortController(); const controller = new AbortController();
function ComponentThatAborts() { function ComponentThatAborts() {
controller.abort(); controller.abort('boom');
return null; return null;
} }
@ -2912,11 +2918,8 @@ describe('ReactFlightDOM', () => {
}; };
}); });
controller.abort();
const {prelude} = await pendingResult; const {prelude} = await pendingResult;
expect(errors).toEqual([]); expect(errors).toEqual(['boom']);
const response = ReactServerDOMClient.createFromReadableStream( const response = ReactServerDOMClient.createFromReadableStream(
Readable.toWeb(prelude), Readable.toWeb(prelude),
); );
@ -2926,6 +2929,7 @@ describe('ReactFlightDOM', () => {
function ClientApp() { function ClientApp() {
return use(response); return use(response);
} }
errors.length = 0;
let abortFizz; let abortFizz;
await serverAct(async () => { await serverAct(async () => {
const {pipe, abort} = ReactDOMFizzServer.renderToPipeableStream( const {pipe, abort} = ReactDOMFizzServer.renderToPipeableStream(

View File

@ -2402,7 +2402,7 @@ describe('ReactFlightDOMBrowser', () => {
}); });
// @gate enableHalt // @gate enableHalt
it('serializes unfinished tasks with infinite promises when aborting a prerender', async () => { it('does not propagate abort reasons errors when aborting a prerender', async () => {
let resolveGreeting; let resolveGreeting;
const greetingPromise = new Promise(resolve => { const greetingPromise = new Promise(resolve => {
resolveGreeting = resolve; resolveGreeting = resolve;
@ -2424,6 +2424,7 @@ describe('ReactFlightDOMBrowser', () => {
} }
const controller = new AbortController(); const controller = new AbortController();
const errors = [];
const {pendingResult} = await serverAct(async () => { const {pendingResult} = await serverAct(async () => {
// destructure trick to avoid the act scope from awaiting the returned value // destructure trick to avoid the act scope from awaiting the returned value
return { return {
@ -2432,14 +2433,18 @@ describe('ReactFlightDOMBrowser', () => {
webpackMap, webpackMap,
{ {
signal: controller.signal, signal: controller.signal,
onError(err) {
errors.push(err);
},
}, },
), ),
}; };
}); });
controller.abort(); controller.abort('boom');
resolveGreeting(); resolveGreeting();
const {prelude} = await pendingResult; const {prelude} = await pendingResult;
expect(errors).toEqual(['boom']);
function ClientRoot({response}) { function ClientRoot({response}) {
return use(response); return use(response);

View File

@ -1103,7 +1103,7 @@ describe('ReactFlightDOMEdge', () => {
}); });
// @gate enableHalt // @gate enableHalt
it('serializes unfinished tasks with infinite promises when aborting a prerender', async () => { it('does not propagate abort reasons errors when aborting a prerender', async () => {
let resolveGreeting; let resolveGreeting;
const greetingPromise = new Promise(resolve => { const greetingPromise = new Promise(resolve => {
resolveGreeting = resolve; resolveGreeting = resolve;
@ -1125,6 +1125,7 @@ describe('ReactFlightDOMEdge', () => {
} }
const controller = new AbortController(); const controller = new AbortController();
const errors = [];
const {pendingResult} = await serverAct(async () => { const {pendingResult} = await serverAct(async () => {
// destructure trick to avoid the act scope from awaiting the returned value // destructure trick to avoid the act scope from awaiting the returned value
return { return {
@ -1133,15 +1134,20 @@ describe('ReactFlightDOMEdge', () => {
webpackMap, webpackMap,
{ {
signal: controller.signal, signal: controller.signal,
onError(err) {
errors.push(err);
},
}, },
), ),
}; };
}); });
controller.abort(); controller.abort('boom');
resolveGreeting(); resolveGreeting();
const {prelude} = await pendingResult; const {prelude} = await pendingResult;
expect(errors).toEqual(['boom']);
function ClientRoot({response}) { function ClientRoot({response}) {
return use(response); return use(response);
} }
@ -1153,7 +1159,7 @@ describe('ReactFlightDOMEdge', () => {
}, },
}); });
const fizzController = new AbortController(); const fizzController = new AbortController();
const errors = []; errors.length = 0;
const ssrStream = await serverAct(() => const ssrStream = await serverAct(() =>
ReactDOMServer.renderToReadableStream( ReactDOMServer.renderToReadableStream(
React.createElement(ClientRoot, {response}), React.createElement(ClientRoot, {response}),
@ -1165,8 +1171,8 @@ describe('ReactFlightDOMEdge', () => {
}, },
), ),
); );
fizzController.abort('boom'); fizzController.abort('bam');
expect(errors).toEqual(['boom']); expect(errors).toEqual(['bam']);
// Should still match the result when parsed // Should still match the result when parsed
const result = await readResult(ssrStream); const result = await readResult(ssrStream);
const div = document.createElement('div'); const div = document.createElement('div');

View File

@ -443,7 +443,7 @@ describe('ReactFlightDOMNode', () => {
}); });
// @gate enableHalt // @gate enableHalt
it('serializes unfinished tasks with infinite promises when aborting a prerender', async () => { it('does not propagate abort reasons errors when aborting a prerender', async () => {
let resolveGreeting; let resolveGreeting;
const greetingPromise = new Promise(resolve => { const greetingPromise = new Promise(resolve => {
resolveGreeting = resolve; resolveGreeting = resolve;
@ -465,6 +465,7 @@ describe('ReactFlightDOMNode', () => {
} }
const controller = new AbortController(); const controller = new AbortController();
const errors = [];
const {pendingResult} = await serverAct(async () => { const {pendingResult} = await serverAct(async () => {
// destructure trick to avoid the act scope from awaiting the returned value // destructure trick to avoid the act scope from awaiting the returned value
return { return {
@ -473,14 +474,18 @@ describe('ReactFlightDOMNode', () => {
webpackMap, webpackMap,
{ {
signal: controller.signal, signal: controller.signal,
onError(err) {
errors.push(err);
},
}, },
), ),
}; };
}); });
controller.abort(); controller.abort('boom');
resolveGreeting(); resolveGreeting();
const {prelude} = await pendingResult; const {prelude} = await pendingResult;
expect(errors).toEqual(['boom']);
function ClientRoot({response}) { function ClientRoot({response}) {
return use(response); return use(response);
@ -492,7 +497,7 @@ describe('ReactFlightDOMNode', () => {
moduleLoading: null, moduleLoading: null,
}, },
}); });
const errors = []; errors.length = 0;
const ssrStream = await serverAct(() => const ssrStream = await serverAct(() =>
ReactDOMServer.renderToPipeableStream( ReactDOMServer.renderToPipeableStream(
React.createElement(ClientRoot, {response}), React.createElement(ClientRoot, {response}),
@ -503,8 +508,8 @@ describe('ReactFlightDOMNode', () => {
}, },
), ),
); );
ssrStream.abort('boom'); ssrStream.abort('bam');
expect(errors).toEqual(['boom']); expect(errors).toEqual(['bam']);
// Should still match the result when parsed // Should still match the result when parsed
const result = await readResult(ssrStream); const result = await readResult(ssrStream);
const div = document.createElement('div'); const div = document.createElement('div');

View File

@ -12,15 +12,13 @@ import type {Thenable} from 'shared/ReactTypes';
import type {ClientManifest} from './ReactFlightServerConfigWebpackBundler'; import type {ClientManifest} from './ReactFlightServerConfigWebpackBundler';
import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig'; import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig';
import {enableHalt} from 'shared/ReactFeatureFlags';
import { import {
createRequest, createRequest,
createPrerenderRequest,
startWork, startWork,
startFlowing, startFlowing,
stopFlowing, stopFlowing,
abort, abort,
halt,
} from 'react-server/src/ReactFlightServer'; } from 'react-server/src/ReactFlightServer';
import { import {
@ -134,35 +132,27 @@ function prerender(
); );
resolve({prelude: stream}); resolve({prelude: stream});
} }
const request = createRequest( const request = createPrerenderRequest(
model, model,
webpackMap, webpackMap,
onAllReady,
onFatalError,
options ? options.onError : undefined, options ? options.onError : undefined,
options ? options.identifierPrefix : undefined, options ? options.identifierPrefix : undefined,
options ? options.onPostpone : undefined, options ? options.onPostpone : undefined,
options ? options.temporaryReferences : undefined, options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined, __DEV__ && options ? options.filterStackFrame : undefined,
onAllReady,
onFatalError,
); );
if (options && options.signal) { if (options && options.signal) {
const signal = options.signal; const signal = options.signal;
if (signal.aborted) { if (signal.aborted) {
const reason = (signal: any).reason; const reason = (signal: any).reason;
if (enableHalt) { abort(request, reason);
halt(request, reason);
} else {
abort(request, reason);
}
} else { } else {
const listener = () => { const listener = () => {
const reason = (signal: any).reason; const reason = (signal: any).reason;
if (enableHalt) { abort(request, reason);
halt(request, reason);
} else {
abort(request, reason);
}
signal.removeEventListener('abort', listener); signal.removeEventListener('abort', listener);
}; };
signal.addEventListener('abort', listener); signal.addEventListener('abort', listener);

View File

@ -12,15 +12,13 @@ import type {Thenable} from 'shared/ReactTypes';
import type {ClientManifest} from './ReactFlightServerConfigWebpackBundler'; import type {ClientManifest} from './ReactFlightServerConfigWebpackBundler';
import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig'; import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig';
import {enableHalt} from 'shared/ReactFeatureFlags';
import { import {
createRequest, createRequest,
createPrerenderRequest,
startWork, startWork,
startFlowing, startFlowing,
stopFlowing, stopFlowing,
abort, abort,
halt,
} from 'react-server/src/ReactFlightServer'; } from 'react-server/src/ReactFlightServer';
import { import {
@ -134,35 +132,27 @@ function prerender(
); );
resolve({prelude: stream}); resolve({prelude: stream});
} }
const request = createRequest( const request = createPrerenderRequest(
model, model,
webpackMap, webpackMap,
onAllReady,
onFatalError,
options ? options.onError : undefined, options ? options.onError : undefined,
options ? options.identifierPrefix : undefined, options ? options.identifierPrefix : undefined,
options ? options.onPostpone : undefined, options ? options.onPostpone : undefined,
options ? options.temporaryReferences : undefined, options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined, __DEV__ && options ? options.filterStackFrame : undefined,
onAllReady,
onFatalError,
); );
if (options && options.signal) { if (options && options.signal) {
const signal = options.signal; const signal = options.signal;
if (signal.aborted) { if (signal.aborted) {
const reason = (signal: any).reason; const reason = (signal: any).reason;
if (enableHalt) { abort(request, reason);
halt(request, reason);
} else {
abort(request, reason);
}
} else { } else {
const listener = () => { const listener = () => {
const reason = (signal: any).reason; const reason = (signal: any).reason;
if (enableHalt) { abort(request, reason);
halt(request, reason);
} else {
abort(request, reason);
}
signal.removeEventListener('abort', listener); signal.removeEventListener('abort', listener);
}; };
signal.addEventListener('abort', listener); signal.addEventListener('abort', listener);

View File

@ -20,15 +20,13 @@ import type {Thenable} from 'shared/ReactTypes';
import {Readable} from 'stream'; import {Readable} from 'stream';
import {enableHalt} from 'shared/ReactFeatureFlags';
import { import {
createRequest, createRequest,
createPrerenderRequest,
startWork, startWork,
startFlowing, startFlowing,
stopFlowing, stopFlowing,
abort, abort,
halt,
} from 'react-server/src/ReactFlightServer'; } from 'react-server/src/ReactFlightServer';
import { import {
@ -177,35 +175,27 @@ function prerenderToNodeStream(
resolve({prelude: readable}); resolve({prelude: readable});
} }
const request = createRequest( const request = createPrerenderRequest(
model, model,
webpackMap, webpackMap,
onAllReady,
onFatalError,
options ? options.onError : undefined, options ? options.onError : undefined,
options ? options.identifierPrefix : undefined, options ? options.identifierPrefix : undefined,
options ? options.onPostpone : undefined, options ? options.onPostpone : undefined,
options ? options.temporaryReferences : undefined, options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined, __DEV__ && options ? options.filterStackFrame : undefined,
onAllReady,
onFatalError,
); );
if (options && options.signal) { if (options && options.signal) {
const signal = options.signal; const signal = options.signal;
if (signal.aborted) { if (signal.aborted) {
const reason = (signal: any).reason; const reason = (signal: any).reason;
if (enableHalt) { abort(request, reason);
halt(request, reason);
} else {
abort(request, reason);
}
} else { } else {
const listener = () => { const listener = () => {
const reason = (signal: any).reason; const reason = (signal: any).reason;
if (enableHalt) { abort(request, reason);
halt(request, reason);
} else {
abort(request, reason);
}
signal.removeEventListener('abort', listener); signal.removeEventListener('abort', listener);
}; };
signal.addEventListener('abort', listener); signal.addEventListener('abort', listener);

View File

@ -353,7 +353,8 @@ type Task = {
interface Reference {} interface Reference {}
export type Request = { export type Request = {
status: 0 | 1 | 2 | 3, status: 10 | 11 | 12 | 13,
type: 20 | 21,
flushScheduled: boolean, flushScheduled: boolean,
fatalError: mixed, fatalError: mixed,
destination: null | Destination, destination: null | Destination,
@ -425,13 +426,17 @@ function defaultPostponeHandler(reason: string) {
// Noop // Noop
} }
const OPEN = 0; const OPEN = 10;
const ABORTING = 1; const ABORTING = 11;
const CLOSING = 2; const CLOSING = 12;
const CLOSED = 3; const CLOSED = 13;
const RENDER = 20;
const PRERENDER = 21;
function RequestInstance( function RequestInstance(
this: $FlowFixMe, this: $FlowFixMe,
type: 20 | 21,
model: ReactClientValue, model: ReactClientValue,
bundlerConfig: ClientManifest, bundlerConfig: ClientManifest,
onError: void | ((error: mixed) => ?string), onError: void | ((error: mixed) => ?string),
@ -440,8 +445,8 @@ function RequestInstance(
temporaryReferences: void | TemporaryReferenceSet, temporaryReferences: void | TemporaryReferenceSet,
environmentName: void | string | (() => string), // DEV-only environmentName: void | string | (() => string), // DEV-only
filterStackFrame: void | ((url: string, functionName: string) => boolean), // DEV-only filterStackFrame: void | ((url: string, functionName: string) => boolean), // DEV-only
onAllReady: void | (() => void), onAllReady: () => void,
onFatalError: void | ((error: mixed) => void), onFatalError: (error: mixed) => void,
) { ) {
if ( if (
ReactSharedInternals.A !== null && ReactSharedInternals.A !== null &&
@ -466,6 +471,7 @@ function RequestInstance(
TaintRegistryPendingRequests.add(cleanupQueue); TaintRegistryPendingRequests.add(cleanupQueue);
} }
const hints = createHints(); const hints = createHints();
this.type = type;
this.status = OPEN; this.status = OPEN;
this.flushScheduled = false; this.flushScheduled = false;
this.fatalError = null; this.fatalError = null;
@ -493,8 +499,8 @@ function RequestInstance(
this.onError = onError === undefined ? defaultErrorHandler : onError; this.onError = onError === undefined ? defaultErrorHandler : onError;
this.onPostpone = this.onPostpone =
onPostpone === undefined ? defaultPostponeHandler : onPostpone; onPostpone === undefined ? defaultPostponeHandler : onPostpone;
this.onAllReady = onAllReady === undefined ? noop : onAllReady; this.onAllReady = onAllReady;
this.onFatalError = onFatalError === undefined ? noop : onFatalError; this.onFatalError = onFatalError;
if (__DEV__) { if (__DEV__) {
this.environmentName = this.environmentName =
@ -522,7 +528,7 @@ function RequestInstance(
pingedTasks.push(rootTask); pingedTasks.push(rootTask);
} }
function noop(): void {} function noop() {}
export function createRequest( export function createRequest(
model: ReactClientValue, model: ReactClientValue,
@ -533,11 +539,38 @@ export function createRequest(
temporaryReferences: void | TemporaryReferenceSet, temporaryReferences: void | TemporaryReferenceSet,
environmentName: void | string | (() => string), // DEV-only environmentName: void | string | (() => string), // DEV-only
filterStackFrame: void | ((url: string, functionName: string) => boolean), // DEV-only filterStackFrame: void | ((url: string, functionName: string) => boolean), // DEV-only
onAllReady: void | (() => void),
onFatalError: void | (() => void),
): Request { ): Request {
// $FlowFixMe[invalid-constructor]: the shapes are exact here but Flow doesn't like constructors // $FlowFixMe[invalid-constructor]: the shapes are exact here but Flow doesn't like constructors
return new RequestInstance( return new RequestInstance(
RENDER,
model,
bundlerConfig,
onError,
identifierPrefix,
onPostpone,
temporaryReferences,
environmentName,
filterStackFrame,
noop,
noop,
);
}
export function createPrerenderRequest(
model: ReactClientValue,
bundlerConfig: ClientManifest,
onAllReady: () => void,
onFatalError: () => void,
onError: void | ((error: mixed) => ?string),
identifierPrefix?: string,
onPostpone: void | ((reason: string) => void),
temporaryReferences: void | TemporaryReferenceSet,
environmentName: void | string | (() => string), // DEV-only
filterStackFrame: void | ((url: string, functionName: string) => boolean), // DEV-only
): Request {
// $FlowFixMe[invalid-constructor]: the shapes are exact here but Flow doesn't like constructors
return new RequestInstance(
PRERENDER,
model, model,
bundlerConfig, bundlerConfig,
onError, onError,
@ -616,13 +649,9 @@ function serializeThenable(
// We can no longer accept any resolved values // We can no longer accept any resolved values
request.abortableTasks.delete(newTask); request.abortableTasks.delete(newTask);
newTask.status = ABORTED; newTask.status = ABORTED;
if (enableHalt && request.fatalError === haltSymbol) { const errorId: number = (request.fatalError: any);
emitBlockedChunk(request, newTask.id); const model = stringify(serializeByValueID(errorId));
} else { emitModelChunk(request, newTask.id, model);
const errorId: number = (request.fatalError: any);
const model = stringify(serializeByValueID(errorId));
emitModelChunk(request, newTask.id, model);
}
return newTask.id; return newTask.id;
} }
if (typeof thenable.status === 'string') { if (typeof thenable.status === 'string') {
@ -732,7 +761,7 @@ function serializeReadableStream(
} }
if (entry.done) { if (entry.done) {
request.abortListeners.delete(error); request.abortListeners.delete(abortStream);
const endStreamRow = streamTask.id.toString(16) + ':C\n'; const endStreamRow = streamTask.id.toString(16) + ':C\n';
request.completedRegularChunks.push(stringToChunk(endStreamRow)); request.completedRegularChunks.push(stringToChunk(endStreamRow));
enqueueFlush(request); enqueueFlush(request);
@ -754,34 +783,49 @@ function serializeReadableStream(
return; return;
} }
aborted = true; aborted = true;
request.abortListeners.delete(error); request.abortListeners.delete(abortStream);
const digest = logRecoverableError(request, reason, streamTask);
emitErrorChunk(request, streamTask.id, digest, reason);
enqueueFlush(request);
let cancelWith: mixed; // $FlowFixMe should be able to pass mixed
if (enableHalt && request.fatalError === haltSymbol) { reader.cancel(reason).then(error, error);
cancelWith = reason; }
} else if ( function abortStream(reason: mixed) {
if (aborted) {
return;
}
aborted = true;
request.abortListeners.delete(abortStream);
if (
enablePostpone && enablePostpone &&
typeof reason === 'object' && typeof reason === 'object' &&
reason !== null && reason !== null &&
(reason: any).$$typeof === REACT_POSTPONE_TYPE (reason: any).$$typeof === REACT_POSTPONE_TYPE
) { ) {
cancelWith = reason;
const postponeInstance: Postpone = (reason: any); const postponeInstance: Postpone = (reason: any);
logPostpone(request, postponeInstance.message, streamTask); logPostpone(request, postponeInstance.message, streamTask);
emitPostponeChunk(request, streamTask.id, postponeInstance); if (enableHalt && request.type === PRERENDER) {
enqueueFlush(request); request.pendingChunks--;
} else {
emitPostponeChunk(request, streamTask.id, postponeInstance);
enqueueFlush(request);
}
} else { } else {
cancelWith = reason;
const digest = logRecoverableError(request, reason, streamTask); const digest = logRecoverableError(request, reason, streamTask);
emitErrorChunk(request, streamTask.id, digest, reason); if (enableHalt && request.type === PRERENDER) {
enqueueFlush(request); request.pendingChunks--;
} else {
emitErrorChunk(request, streamTask.id, digest, reason);
enqueueFlush(request);
}
} }
// $FlowFixMe should be able to pass mixed // $FlowFixMe should be able to pass mixed
reader.cancel(cancelWith).then(error, error); reader.cancel(reason).then(error, error);
} }
request.abortListeners.add(error); request.abortListeners.add(abortStream);
reader.read().then(progress, error); reader.read().then(progress, error);
return serializeByValueID(streamTask.id); return serializeByValueID(streamTask.id);
} }
@ -837,7 +881,7 @@ function serializeAsyncIterable(
} }
if (entry.done) { if (entry.done) {
request.abortListeners.delete(error); request.abortListeners.delete(abortIterable);
let endStreamRow; let endStreamRow;
if (entry.value === undefined) { if (entry.value === undefined) {
endStreamRow = streamTask.id.toString(16) + ':C\n'; endStreamRow = streamTask.id.toString(16) + ':C\n';
@ -881,34 +925,52 @@ function serializeAsyncIterable(
return; return;
} }
aborted = true; aborted = true;
request.abortListeners.delete(error); request.abortListeners.delete(abortIterable);
let throwWith: mixed; const digest = logRecoverableError(request, reason, streamTask);
if (enableHalt && request.fatalError === haltSymbol) { emitErrorChunk(request, streamTask.id, digest, reason);
throwWith = reason; enqueueFlush(request);
} else if ( if (typeof (iterator: any).throw === 'function') {
// The iterator protocol doesn't necessarily include this but a generator do.
// $FlowFixMe should be able to pass mixed
iterator.throw(reason).then(error, error);
}
}
function abortIterable(reason: mixed) {
if (aborted) {
return;
}
aborted = true;
request.abortListeners.delete(abortIterable);
if (
enablePostpone && enablePostpone &&
typeof reason === 'object' && typeof reason === 'object' &&
reason !== null && reason !== null &&
(reason: any).$$typeof === REACT_POSTPONE_TYPE (reason: any).$$typeof === REACT_POSTPONE_TYPE
) { ) {
throwWith = reason;
const postponeInstance: Postpone = (reason: any); const postponeInstance: Postpone = (reason: any);
logPostpone(request, postponeInstance.message, streamTask); logPostpone(request, postponeInstance.message, streamTask);
emitPostponeChunk(request, streamTask.id, postponeInstance); if (enableHalt && request.type === PRERENDER) {
enqueueFlush(request); request.pendingChunks--;
} else {
emitPostponeChunk(request, streamTask.id, postponeInstance);
enqueueFlush(request);
}
} else { } else {
throwWith = reason;
const digest = logRecoverableError(request, reason, streamTask); const digest = logRecoverableError(request, reason, streamTask);
emitErrorChunk(request, streamTask.id, digest, reason); if (enableHalt && request.type === PRERENDER) {
enqueueFlush(request); request.pendingChunks--;
} else {
emitErrorChunk(request, streamTask.id, digest, reason);
enqueueFlush(request);
}
} }
if (typeof (iterator: any).throw === 'function') { if (typeof (iterator: any).throw === 'function') {
// The iterator protocol doesn't necessarily include this but a generator do. // The iterator protocol doesn't necessarily include this but a generator do.
// $FlowFixMe should be able to pass mixed // $FlowFixMe should be able to pass mixed
iterator.throw(throwWith).then(error, error); iterator.throw(reason).then(error, error);
} }
} }
request.abortListeners.add(error); request.abortListeners.add(abortIterable);
if (__DEV__) { if (__DEV__) {
callIteratorInDEV(iterator, progress, error); callIteratorInDEV(iterator, progress, error);
} else { } else {
@ -2101,7 +2163,7 @@ function serializeBlob(request: Request, blob: Blob): string {
return; return;
} }
if (entry.done) { if (entry.done) {
request.abortListeners.delete(error); request.abortListeners.delete(abortBlob);
aborted = true; aborted = true;
pingTask(request, newTask); pingTask(request, newTask);
return; return;
@ -2111,28 +2173,52 @@ function serializeBlob(request: Request, blob: Blob): string {
// $FlowFixMe[incompatible-call] // $FlowFixMe[incompatible-call]
return reader.read().then(progress).catch(error); return reader.read().then(progress).catch(error);
} }
function error(reason: mixed) { function error(reason: mixed) {
if (aborted) { if (aborted) {
return; return;
} }
aborted = true; aborted = true;
request.abortListeners.delete(error); request.abortListeners.delete(abortBlob);
let cancelWith: mixed; const digest = logRecoverableError(request, reason, newTask);
if (enableHalt && request.fatalError === haltSymbol) { emitErrorChunk(request, newTask.id, digest, reason);
cancelWith = reason; enqueueFlush(request);
// $FlowFixMe should be able to pass mixed
reader.cancel(reason).then(error, error);
}
function abortBlob(reason: mixed) {
if (aborted) {
return;
}
aborted = true;
request.abortListeners.delete(abortBlob);
if (
enablePostpone &&
typeof reason === 'object' &&
reason !== null &&
(reason: any).$$typeof === REACT_POSTPONE_TYPE
) {
const postponeInstance: Postpone = (reason: any);
logPostpone(request, postponeInstance.message, newTask);
if (enableHalt && request.type === PRERENDER) {
request.pendingChunks--;
} else {
emitPostponeChunk(request, newTask.id, postponeInstance);
enqueueFlush(request);
}
} else { } else {
cancelWith = reason;
const digest = logRecoverableError(request, reason, newTask); const digest = logRecoverableError(request, reason, newTask);
emitErrorChunk(request, newTask.id, digest, reason); if (enableHalt && request.type === PRERENDER) {
request.abortableTasks.delete(newTask); request.pendingChunks--;
enqueueFlush(request); } else {
emitErrorChunk(request, newTask.id, digest, reason);
enqueueFlush(request);
}
} }
// $FlowFixMe should be able to pass mixed // $FlowFixMe should be able to pass mixed
reader.cancel(cancelWith).then(error, error); reader.cancel(reason).then(error, error);
} }
request.abortListeners.add(error); request.abortListeners.add(abortBlob);
// $FlowFixMe[incompatible-call] // $FlowFixMe[incompatible-call]
reader.read().then(progress).catch(error); reader.read().then(progress).catch(error);
@ -3001,12 +3087,6 @@ function emitPostponeChunk(
request.completedErrorChunks.push(processedChunk); request.completedErrorChunks.push(processedChunk);
} }
function emitBlockedChunk(request: Request, id: number): void {
const row = serializeRowHeader('#', id) + '\n';
const processedChunk = stringToChunk(row);
request.completedErrorChunks.push(processedChunk);
}
function emitErrorChunk( function emitErrorChunk(
request: Request, request: Request,
id: number, id: number,
@ -3755,13 +3835,9 @@ function retryTask(request: Request, task: Task): void {
if (request.status === ABORTING) { if (request.status === ABORTING) {
request.abortableTasks.delete(task); request.abortableTasks.delete(task);
task.status = ABORTED; task.status = ABORTED;
if (enableHalt && request.fatalError === haltSymbol) { const errorId: number = (request.fatalError: any);
emitBlockedChunk(request, task.id); const model = stringify(serializeByValueID(errorId));
} else { emitModelChunk(request, task.id, model);
const errorId: number = (request.fatalError: any);
const model = stringify(serializeByValueID(errorId));
emitModelChunk(request, task.id, model);
}
return; return;
} }
// Something suspended again, let's pick it back up later. // Something suspended again, let's pick it back up later.
@ -3783,13 +3859,9 @@ function retryTask(request: Request, task: Task): void {
if (request.status === ABORTING) { if (request.status === ABORTING) {
request.abortableTasks.delete(task); request.abortableTasks.delete(task);
task.status = ABORTED; task.status = ABORTED;
if (enableHalt && request.fatalError === haltSymbol) { const errorId: number = (request.fatalError: any);
emitBlockedChunk(request, task.id); const model = stringify(serializeByValueID(errorId));
} else { emitModelChunk(request, task.id, model);
const errorId: number = (request.fatalError: any);
const model = stringify(serializeByValueID(errorId));
emitModelChunk(request, task.id, model);
}
return; return;
} }
@ -3844,7 +3916,8 @@ function performWork(request: Request): void {
// We can ping after completing but if this happens there already // We can ping after completing but if this happens there already
// wouldn't be any abortable tasks. So we only call allReady after // wouldn't be any abortable tasks. So we only call allReady after
// the work which actually completed the last pending task // the work which actually completed the last pending task
allReady(request); const onAllReady = request.onAllReady;
onAllReady();
} }
} catch (error) { } catch (error) {
logRecoverableError(request, error, null); logRecoverableError(request, error, null);
@ -4007,17 +4080,17 @@ export function stopFlowing(request: Request): void {
request.destination = null; 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 { export function abort(request: Request, reason: mixed): void {
try { try {
if (request.status === OPEN) { if (request.status === OPEN) {
request.status = ABORTING; request.status = ABORTING;
} }
const abortableTasks = request.abortableTasks; const abortableTasks = request.abortableTasks;
// We have tasks to abort. We'll emit one error row and then emit a reference
// to that row from every row that's still remaining.
if (abortableTasks.size > 0) { if (abortableTasks.size > 0) {
request.pendingChunks++; // We have tasks to abort. We'll emit one error row and then emit a reference
// to that row from every row that's still remaining if we are rendering. If we
// are prerendering (and halt semantics are enabled) we will refer to an error row
// but not actually emit it so the reciever can at that point rather than error.
const errorId = request.nextChunkId++; const errorId = request.nextChunkId++;
request.fatalError = errorId; request.fatalError = errorId;
if ( if (
@ -4028,7 +4101,11 @@ export function abort(request: Request, reason: mixed): void {
) { ) {
const postponeInstance: Postpone = (reason: any); const postponeInstance: Postpone = (reason: any);
logPostpone(request, postponeInstance.message, null); logPostpone(request, postponeInstance.message, null);
emitPostponeChunk(request, errorId, postponeInstance); if (!enableHalt || request.type === PRERENDER) {
// When prerendering with halt semantics we omit the referred to postpone.
request.pendingChunks++;
emitPostponeChunk(request, errorId, postponeInstance);
}
} else { } else {
const error = const error =
reason === undefined reason === undefined
@ -4043,11 +4120,16 @@ export function abort(request: Request, reason: mixed): void {
) )
: reason; : reason;
const digest = logRecoverableError(request, error, null); const digest = logRecoverableError(request, error, null);
emitErrorChunk(request, errorId, digest, error); if (!enableHalt || request.type === RENDER) {
// When prerendering with halt semantics we omit the referred to error.
request.pendingChunks++;
emitErrorChunk(request, errorId, digest, error);
}
} }
abortableTasks.forEach(task => abortTask(task, request, errorId)); abortableTasks.forEach(task => abortTask(task, request, errorId));
abortableTasks.clear(); abortableTasks.clear();
allReady(request); const onAllReady = request.onAllReady;
onAllReady();
} }
const abortListeners = request.abortListeners; const abortListeners = request.abortListeners;
if (abortListeners.size > 0) { if (abortListeners.size > 0) {
@ -4087,43 +4169,3 @@ export function abort(request: Request, reason: mixed): void {
fatalError(request, error); fatalError(request, error);
} }
} }
const haltSymbol = Symbol('halt');
// This is called to stop rendering without erroring. All unfinished work is represented Promises
// that never resolve.
export function halt(request: Request, reason: mixed): void {
try {
if (request.status === OPEN) {
request.status = ABORTING;
}
request.fatalError = haltSymbol;
const abortableTasks = request.abortableTasks;
// We have tasks to abort. We'll emit one error row and then emit a reference
// to that row from every row that's still remaining.
if (abortableTasks.size > 0) {
request.pendingChunks++;
const errorId = request.nextChunkId++;
emitBlockedChunk(request, errorId);
abortableTasks.forEach(task => abortTask(task, request, errorId));
abortableTasks.clear();
allReady(request);
}
const abortListeners = request.abortListeners;
if (abortListeners.size > 0) {
abortListeners.forEach(callback => callback(reason));
abortListeners.clear();
}
if (request.destination !== null) {
flushCompletedChunks(request, request.destination);
}
} catch (error) {
logRecoverableError(request, error, null);
fatalError(request, error);
}
}
function allReady(request: Request) {
const onAllReady = request.onAllReady;
onAllReady();
}