Skip to content

Commit 633305c

Browse files
committed
[Flight] model halted references explicitly
using infinitely suspending promises isn't right because this will parse as a promise which is only appropriate if the value we're halting at is a promise. Instead we need to have a special marker type that says this reference will never resolve. Additionally flight client needs to not error any halted references when the stream closes because they will otherwise appear as an error
1 parent 7954db9 commit 633305c

File tree

3 files changed

+142
-24
lines changed

3 files changed

+142
-24
lines changed

packages/react-client/src/ReactFlightClient.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import {
4545
enableRefAsProp,
4646
enableFlightReadableStream,
4747
enableOwnerStacks,
48+
enableHalt,
4849
} from 'shared/ReactFeatureFlags';
4950

5051
import {
@@ -1995,6 +1996,14 @@ function resolvePostponeDev(
19951996
}
19961997
}
19971998

1999+
function resolveBlocked(response: Response, id: number): void {
2000+
const chunks = response._chunks;
2001+
const chunk = chunks.get(id);
2002+
if (!chunk) {
2003+
chunks.set(id, createBlockedChunk(response));
2004+
}
2005+
}
2006+
19982007
function resolveHint<Code: HintCode>(
19992008
response: Response,
20002009
code: Code,
@@ -2621,6 +2630,12 @@ function processFullStringRow(
26212630
}
26222631
}
26232632
// Fallthrough
2633+
case 35 /* "#" */: {
2634+
if (enableHalt) {
2635+
resolveBlocked(response, id);
2636+
}
2637+
}
2638+
// Fallthrough
26242639
default: /* """ "{" "[" "t" "f" "n" "0" - "9" */ {
26252640
// We assume anything else is JSON.
26262641
resolveModel(response, id, row);
@@ -2677,6 +2692,7 @@ export function processBinaryChunk(
26772692
i++;
26782693
} else if (
26792694
(resolvedRowTag > 64 && resolvedRowTag < 91) /* "A"-"Z" */ ||
2695+
resolvedRowTag === 35 /* "#" */ ||
26802696
resolvedRowTag === 114 /* "r" */ ||
26812697
resolvedRowTag === 120 /* "x" */
26822698
) {

packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2856,4 +2856,105 @@ describe('ReactFlightDOM', () => {
28562856
jest.advanceTimersByTime('100');
28572857
expect(await race).toBe('timeout');
28582858
});
2859+
2860+
// @gate enableHalt
2861+
it('will halt unfinished chunks inside Suspense when aborting a prerender', async () => {
2862+
const controller = new AbortController();
2863+
function ComponentThatAborts() {
2864+
controller.abort();
2865+
return null;
2866+
}
2867+
2868+
async function Greeting() {
2869+
await 1;
2870+
return 'hello world';
2871+
}
2872+
2873+
async function Farewell() {
2874+
return 'goodbye world';
2875+
}
2876+
2877+
async function Wrapper() {
2878+
return (
2879+
<Suspense fallback="loading too...">
2880+
<ComponentThatAborts />
2881+
</Suspense>
2882+
);
2883+
}
2884+
2885+
function App() {
2886+
return (
2887+
<div>
2888+
<Suspense fallback="loading...">
2889+
<Greeting />
2890+
</Suspense>
2891+
<Wrapper />
2892+
<Suspense fallback="loading three...">
2893+
<Farewell />
2894+
</Suspense>
2895+
</div>
2896+
);
2897+
}
2898+
2899+
const errors = [];
2900+
const {pendingResult} = await serverAct(() => {
2901+
return {
2902+
pendingResult: ReactServerDOMStaticServer.prerenderToNodeStream(
2903+
<App />,
2904+
{},
2905+
{
2906+
onError(x) {
2907+
errors.push(x);
2908+
},
2909+
signal: controller.signal,
2910+
},
2911+
),
2912+
};
2913+
});
2914+
2915+
controller.abort();
2916+
2917+
const {prelude} = await pendingResult;
2918+
expect(errors).toEqual([]);
2919+
2920+
const response = ReactServerDOMClient.createFromReadableStream(
2921+
Readable.toWeb(prelude),
2922+
);
2923+
2924+
const {writable: fizzWritable, readable: fizzReadable} = getTestStream();
2925+
2926+
function ClientApp() {
2927+
return use(response);
2928+
}
2929+
let abortFizz;
2930+
await serverAct(async () => {
2931+
const {pipe, abort} = ReactDOMFizzServer.renderToPipeableStream(
2932+
React.createElement(ClientApp),
2933+
{
2934+
onError(error, errorInfo) {
2935+
errors.push(error);
2936+
},
2937+
},
2938+
);
2939+
pipe(fizzWritable);
2940+
abortFizz = abort;
2941+
});
2942+
2943+
await serverAct(() => {
2944+
abortFizz('boom');
2945+
});
2946+
2947+
// one error per boundary
2948+
expect(errors).toEqual(['boom', 'boom', 'boom']);
2949+
2950+
const container = document.createElement('div');
2951+
await readInto(container, fizzReadable);
2952+
expect(getMeaningfulChildren(container)).toEqual(
2953+
<div>
2954+
{'loading...'}
2955+
{'loading too...'}
2956+
{'loading three...'}
2957+
</div>,
2958+
);
2959+
});
28592960
});

packages/react-server/src/ReactFlightServer.js

Lines changed: 25 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -615,7 +615,7 @@ function serializeThenable(
615615
request.abortableTasks.delete(newTask);
616616
newTask.status = ABORTED;
617617
if (enableHalt && request.fatalError === haltSymbol) {
618-
emitModelChunk(request, newTask.id, reusableInfinitePromiseModel);
618+
emitBlockedChunk(request, newTask.id);
619619
} else {
620620
const errorId: number = (request.fatalError: any);
621621
const model = stringify(serializeByValueID(errorId));
@@ -1818,7 +1818,6 @@ function serializeLazyID(id: number): string {
18181818
function serializeInfinitePromise(): string {
18191819
return '$@';
18201820
}
1821-
const reusableInfinitePromiseModel = stringify(serializeInfinitePromise());
18221821

18231822
function serializePromiseID(id: number): string {
18241823
return '$@' + id.toString(16);
@@ -2176,9 +2175,6 @@ function renderModel(
21762175
if (typeof x.then === 'function') {
21772176
if (request.status === ABORTING) {
21782177
task.status = ABORTED;
2179-
if (enableHalt && request.fatalError === haltSymbol) {
2180-
return serializeInfinitePromise();
2181-
}
21822178
const errorId: number = (request.fatalError: any);
21832179
if (wasReactNode) {
21842180
return serializeLazyID(errorId);
@@ -2232,9 +2228,6 @@ function renderModel(
22322228

22332229
if (request.status === ABORTING) {
22342230
task.status = ABORTED;
2235-
if (enableHalt && request.fatalError === haltSymbol) {
2236-
return serializeInfinitePromise();
2237-
}
22382231
const errorId: number = (request.fatalError: any);
22392232
if (wasReactNode) {
22402233
return serializeLazyID(errorId);
@@ -2976,6 +2969,12 @@ function emitPostponeChunk(
29762969
request.completedErrorChunks.push(processedChunk);
29772970
}
29782971

2972+
function emitBlockedChunk(request: Request, id: number): void {
2973+
const row = serializeRowHeader('#', id) + '\n';
2974+
const processedChunk = stringToChunk(row);
2975+
request.completedErrorChunks.push(processedChunk);
2976+
}
2977+
29792978
function emitErrorChunk(
29802979
request: Request,
29812980
id: number,
@@ -3725,7 +3724,7 @@ function retryTask(request: Request, task: Task): void {
37253724
request.abortableTasks.delete(task);
37263725
task.status = ABORTED;
37273726
if (enableHalt && request.fatalError === haltSymbol) {
3728-
emitModelChunk(request, task.id, reusableInfinitePromiseModel);
3727+
emitBlockedChunk(request, task.id);
37293728
} else {
37303729
const errorId: number = (request.fatalError: any);
37313730
const model = stringify(serializeByValueID(errorId));
@@ -3753,7 +3752,7 @@ function retryTask(request: Request, task: Task): void {
37533752
request.abortableTasks.delete(task);
37543753
task.status = ABORTED;
37553754
if (enableHalt && request.fatalError === haltSymbol) {
3756-
emitModelChunk(request, task.id, reusableInfinitePromiseModel);
3755+
emitBlockedChunk(request, task.id);
37573756
} else {
37583757
const errorId: number = (request.fatalError: any);
37593758
const model = stringify(serializeByValueID(errorId));
@@ -3798,6 +3797,7 @@ function performWork(request: Request): void {
37983797
currentRequest = request;
37993798
prepareToUseHooksForRequest(request);
38003799

3800+
const hadAbortableTasks = request.abortableTasks.size > 0;
38013801
try {
38023802
const pingedTasks = request.pingedTasks;
38033803
request.pingedTasks = [];
@@ -3808,10 +3808,11 @@ function performWork(request: Request): void {
38083808
if (request.destination !== null) {
38093809
flushCompletedChunks(request, request.destination);
38103810
}
3811-
if (request.abortableTasks.size === 0) {
3812-
// we're done rendering
3813-
const onAllReady = request.onAllReady;
3814-
onAllReady();
3811+
if (hadAbortableTasks && request.abortableTasks.size === 0) {
3812+
// We can ping after completing but if this happens there already
3813+
// wouldn't be any abortable tasks. So we only call allReady after
3814+
// the work which actually completed the last pending task
3815+
allReady(request);
38153816
}
38163817
} catch (error) {
38173818
logRecoverableError(request, error, null);
@@ -3836,15 +3837,6 @@ function abortTask(task: Task, request: Request, errorId: number): void {
38363837
request.completedErrorChunks.push(processedChunk);
38373838
}
38383839

3839-
function haltTask(task: Task, request: Request): void {
3840-
if (task.status === RENDERING) {
3841-
// This task will be aborted by the render
3842-
return;
3843-
}
3844-
task.status = ABORTED;
3845-
emitModelChunk(request, task.id, reusableInfinitePromiseModel);
3846-
}
3847-
38483840
function flushCompletedChunks(
38493841
request: Request,
38503842
destination: Destination,
@@ -4023,6 +4015,7 @@ export function abort(request: Request, reason: mixed): void {
40234015
}
40244016
abortableTasks.forEach(task => abortTask(task, request, errorId));
40254017
abortableTasks.clear();
4018+
allReady(request);
40264019
}
40274020
const abortListeners = request.abortListeners;
40284021
if (abortListeners.size > 0) {
@@ -4078,8 +4071,11 @@ export function halt(request: Request, reason: mixed): void {
40784071
// to that row from every row that's still remaining.
40794072
if (abortableTasks.size > 0) {
40804073
request.pendingChunks++;
4081-
abortableTasks.forEach(task => haltTask(task, request));
4074+
const errorId = request.nextChunkId++;
4075+
emitBlockedChunk(request, errorId);
4076+
abortableTasks.forEach(task => abortTask(task, request, errorId));
40824077
abortableTasks.clear();
4078+
allReady(request);
40834079
}
40844080
const abortListeners = request.abortListeners;
40854081
if (abortListeners.size > 0) {
@@ -4094,3 +4090,8 @@ export function halt(request: Request, reason: mixed): void {
40944090
fatalError(request, error);
40954091
}
40964092
}
4093+
4094+
function allReady(request: Request) {
4095+
const onAllReady = request.onAllReady;
4096+
onAllReady();
4097+
}

0 commit comments

Comments
 (0)