Skip to content

Commit

Permalink
[flight] When halting call onError/onPostpone
Browse files Browse the repository at this point in the history
Halt was originally implemented as an alternative to error handling and thus halted reasons were not exposed through any observability event like onError or onPostpone. We could add something like onAbort or onHalt in it's place but it's not clear that this is particularly well motivated. Instead in this change we update halt semantics to still call onError and onPostpone with the abort reason. So a halt doesn't change what you can observe but it does change the serialization model. So while you will see errors through onError they won't propagate to the consumer as errors.
  • Loading branch information
gnoff committed Aug 19, 2024
1 parent 633305c commit 615833a
Show file tree
Hide file tree
Showing 5 changed files with 159 additions and 109 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2724,7 +2724,7 @@ describe('ReactFlightDOM', () => {
});

// @gate enableHalt
it('serializes unfinished tasks with infinite promises when aborting a prerender', async () => {
it('serializes a forever blocked reference when aborting a prerender', async () => {
let resolveGreeting;
const greetingPromise = new Promise(resolve => {
resolveGreeting = resolve;
Expand All @@ -2746,6 +2746,7 @@ describe('ReactFlightDOM', () => {
}

const controller = new AbortController();
const errors = [];
const {pendingResult} = await serverAct(async () => {
// destructure trick to avoid the act scope from awaiting the returned value
return {
Expand All @@ -2754,15 +2755,20 @@ describe('ReactFlightDOM', () => {
webpackMap,
{
signal: controller.signal,
onError(err) {
errors.push(err);
},
},
),
};
});

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

expect(errors).toEqual(['boom']);

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

Expand All @@ -2772,7 +2778,7 @@ describe('ReactFlightDOM', () => {
return use(response);
}

const errors = [];
errors.length = 0;
let abortFizz;
await serverAct(async () => {
const {pipe, abort} = ReactDOMFizzServer.renderToPipeableStream(
Expand All @@ -2788,10 +2794,10 @@ describe('ReactFlightDOM', () => {
});

await serverAct(() => {
abortFizz('boom');
abortFizz('bam');
});

expect(errors).toEqual(['boom']);
expect(errors).toEqual(['bam']);

const container = document.createElement('div');
await readInto(container, fizzReadable);
Expand Down Expand Up @@ -2861,7 +2867,7 @@ describe('ReactFlightDOM', () => {
it('will halt unfinished chunks inside Suspense when aborting a prerender', async () => {
const controller = new AbortController();
function ComponentThatAborts() {
controller.abort();
controller.abort('boom');
return null;
}

Expand Down Expand Up @@ -2912,11 +2918,8 @@ describe('ReactFlightDOM', () => {
};
});

controller.abort();

const {prelude} = await pendingResult;
expect(errors).toEqual([]);

expect(errors).toEqual(['boom']);
const response = ReactServerDOMClient.createFromReadableStream(
Readable.toWeb(prelude),
);
Expand All @@ -2926,6 +2929,7 @@ describe('ReactFlightDOM', () => {
function ClientApp() {
return use(response);
}
errors.length = 0;
let abortFizz;
await serverAct(async () => {
const {pipe, abort} = ReactDOMFizzServer.renderToPipeableStream(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2390,7 +2390,7 @@ describe('ReactFlightDOMBrowser', () => {
});

// @gate enableHalt
it('serializes unfinished tasks with infinite promises when aborting a prerender', async () => {
it('serializes a forever blocked reference when aborting a prerender', async () => {
let resolveGreeting;
const greetingPromise = new Promise(resolve => {
resolveGreeting = resolve;
Expand All @@ -2412,6 +2412,7 @@ describe('ReactFlightDOMBrowser', () => {
}

const controller = new AbortController();
const errors = [];
const {pendingResult} = await serverAct(async () => {
// destructure trick to avoid the act scope from awaiting the returned value
return {
Expand All @@ -2420,14 +2421,18 @@ describe('ReactFlightDOMBrowser', () => {
webpackMap,
{
signal: controller.signal,
onError(err) {
errors.push(err);
},
},
),
};
});

controller.abort();
controller.abort('boom');
resolveGreeting();
const {prelude} = await pendingResult;
expect(errors).toEqual(['boom']);

function ClientRoot({response}) {
return use(response);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1103,7 +1103,7 @@ describe('ReactFlightDOMEdge', () => {
});

// @gate enableHalt
it('serializes unfinished tasks with infinite promises when aborting a prerender', async () => {
it('serializes a forever blocked reference when aborting a prerender', async () => {
let resolveGreeting;
const greetingPromise = new Promise(resolve => {
resolveGreeting = resolve;
Expand All @@ -1125,6 +1125,7 @@ describe('ReactFlightDOMEdge', () => {
}

const controller = new AbortController();
const errors = [];
const {pendingResult} = await serverAct(async () => {
// destructure trick to avoid the act scope from awaiting the returned value
return {
Expand All @@ -1133,15 +1134,20 @@ describe('ReactFlightDOMEdge', () => {
webpackMap,
{
signal: controller.signal,
onError(err) {
errors.push(err);
},
},
),
};
});

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

expect(errors).toEqual(['boom']);

function ClientRoot({response}) {
return use(response);
}
Expand All @@ -1153,7 +1159,7 @@ describe('ReactFlightDOMEdge', () => {
},
});
const fizzController = new AbortController();
const errors = [];
errors.length = 0;
const ssrStream = await serverAct(() =>
ReactDOMServer.renderToReadableStream(
React.createElement(ClientRoot, {response}),
Expand All @@ -1165,8 +1171,8 @@ describe('ReactFlightDOMEdge', () => {
},
),
);
fizzController.abort('boom');
expect(errors).toEqual(['boom']);
fizzController.abort('bam');
expect(errors).toEqual(['bam']);
// Should still match the result when parsed
const result = await readResult(ssrStream);
const div = document.createElement('div');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -443,7 +443,7 @@ describe('ReactFlightDOMNode', () => {
});

// @gate enableHalt
it('serializes unfinished tasks with infinite promises when aborting a prerender', async () => {
it('serializes a forever blocked reference when aborting a prerender', async () => {
let resolveGreeting;
const greetingPromise = new Promise(resolve => {
resolveGreeting = resolve;
Expand All @@ -465,6 +465,7 @@ describe('ReactFlightDOMNode', () => {
}

const controller = new AbortController();
const errors = [];
const {pendingResult} = await serverAct(async () => {
// destructure trick to avoid the act scope from awaiting the returned value
return {
Expand All @@ -473,14 +474,18 @@ describe('ReactFlightDOMNode', () => {
webpackMap,
{
signal: controller.signal,
onError(err) {
errors.push(err);
},
},
),
};
});

controller.abort();
controller.abort('boom');
resolveGreeting();
const {prelude} = await pendingResult;
expect(errors).toEqual(['boom']);

function ClientRoot({response}) {
return use(response);
Expand All @@ -492,7 +497,7 @@ describe('ReactFlightDOMNode', () => {
moduleLoading: null,
},
});
const errors = [];
errors.length = 0;
const ssrStream = await serverAct(() =>
ReactDOMServer.renderToPipeableStream(
React.createElement(ClientRoot, {response}),
Expand All @@ -503,8 +508,8 @@ describe('ReactFlightDOMNode', () => {
},
),
);
ssrStream.abort('boom');
expect(errors).toEqual(['boom']);
ssrStream.abort('bam');
expect(errors).toEqual(['bam']);
// Should still match the result when parsed
const result = await readResult(ssrStream);
const div = document.createElement('div');
Expand Down
Loading

0 comments on commit 615833a

Please sign in to comment.