Skip to content

Commit 96017d3

Browse files
committed
[Flight] land enableHalt flag
enableHalt was added as a flag to experiment with a new semantic for prerendering RSC results. In particular we model RSC aborts for prerenders as unfinished streams where models awaiting a value will never get them. This sets the stage for resuming RSC renders in the future where we can expect a concategnation and continuation of the RSC result picking up the holes left by the aborted prerender. Prerender is already an unstable API and there is no future for the non-halting semantics so this commit lands the flag. This change doesn't remove the flag entirely because halt semantics are also available while prerendering Fizz in the experimental channel. This API is not unstable and is more widely used so when we adopt the experimental behavior in a canary we should be closer to releasing the next major
1 parent e39b380 commit 96017d3

File tree

6 files changed

+24
-66
lines changed

6 files changed

+24
-66
lines changed

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

+4-12
Original file line numberDiff line numberDiff line change
@@ -62,21 +62,17 @@ describe('ReactFlightDOM', () => {
6262
jest.mock('react-server-dom-webpack/server', () =>
6363
require('react-server-dom-webpack/server.node.unbundled'),
6464
);
65-
if (__EXPERIMENTAL__) {
66-
jest.mock('react-server-dom-webpack/static', () =>
67-
require('react-server-dom-webpack/static.node.unbundled'),
68-
);
69-
}
65+
jest.mock('react-server-dom-webpack/static', () =>
66+
require('react-server-dom-webpack/static.node.unbundled'),
67+
);
7068
const WebpackMock = require('./utils/WebpackMock');
7169
clientExports = WebpackMock.clientExports;
7270
clientExportsESM = WebpackMock.clientExportsESM;
7371
clientModuleError = WebpackMock.clientModuleError;
7472
webpackMap = WebpackMock.webpackMap;
7573

7674
ReactServerDOMServer = require('react-server-dom-webpack/server');
77-
if (__EXPERIMENTAL__) {
78-
ReactServerDOMStaticServer = require('react-server-dom-webpack/static');
79-
}
75+
ReactServerDOMStaticServer = require('react-server-dom-webpack/static');
8076

8177
// This reset is to load modules for the SSR/Browser scope.
8278
jest.unmock('react-server-dom-webpack/server');
@@ -2771,7 +2767,6 @@ describe('ReactFlightDOM', () => {
27712767
);
27722768
});
27732769

2774-
// @gate experimental
27752770
it('can prerender', async () => {
27762771
let resolveGreeting;
27772772
const greetingPromise = new Promise(resolve => {
@@ -2834,7 +2829,6 @@ describe('ReactFlightDOM', () => {
28342829
expect(getMeaningfulChildren(container)).toEqual(<div>hello world</div>);
28352830
});
28362831

2837-
// @gate enableHalt
28382832
it('does not propagate abort reasons errors when aborting a prerender', async () => {
28392833
let resolveGreeting;
28402834
const greetingPromise = new Promise(resolve => {
@@ -2916,7 +2910,6 @@ describe('ReactFlightDOM', () => {
29162910
expect(getMeaningfulChildren(container)).toEqual(<div>loading...</div>);
29172911
});
29182912

2919-
// @gate enableHalt
29202913
it('will leave async iterables in an incomplete state when halting', async () => {
29212914
let resolve;
29222915
const wait = new Promise(r => (resolve = r));
@@ -2976,7 +2969,6 @@ describe('ReactFlightDOM', () => {
29762969
expect(await race).toBe('timeout');
29772970
});
29782971

2979-
// @gate enableHalt
29802972
it('will halt unfinished chunks inside Suspense when aborting a prerender', async () => {
29812973
const controller = new AbortController();
29822974
function ComponentThatAborts() {

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

+4-8
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,10 @@ describe('ReactFlightDOMBrowser', () => {
6363
webpackMap = WebpackMock.webpackMap;
6464
webpackServerMap = WebpackMock.webpackServerMap;
6565
ReactServerDOMServer = require('react-server-dom-webpack/server');
66-
if (__EXPERIMENTAL__) {
67-
jest.mock('react-server-dom-webpack/static', () =>
68-
require('react-server-dom-webpack/static.browser'),
69-
);
70-
ReactServerDOMStaticServer = require('react-server-dom-webpack/static');
71-
}
66+
jest.mock('react-server-dom-webpack/static', () =>
67+
require('react-server-dom-webpack/static.browser'),
68+
);
69+
ReactServerDOMStaticServer = require('react-server-dom-webpack/static');
7270

7371
__unmockReact();
7472
jest.resetModules();
@@ -2465,7 +2463,6 @@ describe('ReactFlightDOMBrowser', () => {
24652463
expect(errors).toEqual([reason]);
24662464
});
24672465

2468-
// @gate experimental
24692466
it('can prerender', async () => {
24702467
let resolveGreeting;
24712468
const greetingPromise = new Promise(resolve => {
@@ -2514,7 +2511,6 @@ describe('ReactFlightDOMBrowser', () => {
25142511
expect(container.innerHTML).toBe('<div>hello world</div>');
25152512
});
25162513

2517-
// @gate enableHalt
25182514
it('does not propagate abort reasons errors when aborting a prerender', async () => {
25192515
let resolveGreeting;
25202516
const greetingPromise = new Promise(resolve => {

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

+4-15
Original file line numberDiff line numberDiff line change
@@ -87,12 +87,10 @@ describe('ReactFlightDOMEdge', () => {
8787

8888
ReactServer = require('react');
8989
ReactServerDOMServer = require('react-server-dom-webpack/server');
90-
if (__EXPERIMENTAL__) {
91-
jest.mock('react-server-dom-webpack/static', () =>
92-
require('react-server-dom-webpack/static.edge'),
93-
);
94-
ReactServerDOMStaticServer = require('react-server-dom-webpack/static');
95-
}
90+
jest.mock('react-server-dom-webpack/static', () =>
91+
require('react-server-dom-webpack/static.edge'),
92+
);
93+
ReactServerDOMStaticServer = require('react-server-dom-webpack/static');
9694

9795
jest.resetModules();
9896
__unmockReact();
@@ -1291,7 +1289,6 @@ describe('ReactFlightDOMEdge', () => {
12911289
]);
12921290
});
12931291

1294-
// @gate experimental
12951292
it('can prerender', async () => {
12961293
let resolveGreeting;
12971294
const greetingPromise = new Promise(resolve => {
@@ -1345,7 +1342,6 @@ describe('ReactFlightDOMEdge', () => {
13451342
expect(result).toBe('<div>hello world</div>');
13461343
});
13471344

1348-
// @gate enableHalt
13491345
it('does not propagate abort reasons errors when aborting a prerender', async () => {
13501346
let resolveGreeting;
13511347
const greetingPromise = new Promise(resolve => {
@@ -1423,7 +1419,6 @@ describe('ReactFlightDOMEdge', () => {
14231419
expect(div.textContent).toBe('loading...');
14241420
});
14251421

1426-
// @gate enableHalt
14271422
it('should abort parsing an incomplete prerender payload', async () => {
14281423
const infinitePromise = new Promise(() => {});
14291424
const controller = new AbortController();
@@ -1471,7 +1466,6 @@ describe('ReactFlightDOMEdge', () => {
14711466
expect(error.message).toBe('Connection closed.');
14721467
});
14731468

1474-
// @gate experimental
14751469
it('should be able to handle a rejected promise in unstable_prerender', async () => {
14761470
const expectedError = new Error('Bam!');
14771471
const errors = [];
@@ -1510,7 +1504,6 @@ describe('ReactFlightDOMEdge', () => {
15101504
expect(error.message).toBe(expectedMessage);
15111505
});
15121506

1513-
// @gate experimental
15141507
it('should be able to handle an erroring async iterable in unstable_prerender', async () => {
15151508
const expectedError = new Error('Bam!');
15161509
const errors = [];
@@ -1557,7 +1550,6 @@ describe('ReactFlightDOMEdge', () => {
15571550
expect(error.message).toBe(expectedMessage);
15581551
});
15591552

1560-
// @gate experimental
15611553
it('should be able to handle an erroring readable stream in unstable_prerender', async () => {
15621554
const expectedError = new Error('Bam!');
15631555
const errors = [];
@@ -1605,7 +1597,6 @@ describe('ReactFlightDOMEdge', () => {
16051597
expect(error.message).toBe(expectedMessage);
16061598
});
16071599

1608-
// @gate experimental
16091600
it('can prerender an async iterable', async () => {
16101601
const errors = [];
16111602

@@ -1649,7 +1640,6 @@ describe('ReactFlightDOMEdge', () => {
16491640
expect(text).toBe('hello world');
16501641
});
16511642

1652-
// @gate experimental
16531643
it('can prerender a readable stream', async () => {
16541644
const errors = [];
16551645

@@ -1683,7 +1673,6 @@ describe('ReactFlightDOMEdge', () => {
16831673
expect(result).toBe('hello world');
16841674
});
16851675

1686-
// @gate experimental
16871676
it('does not return a prerender prelude early when an error is emitted and there are still pending tasks', async () => {
16881677
let rejectPromise;
16891678
const rejectingPromise = new Promise(

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

+4-8
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,10 @@ describe('ReactFlightDOMNode', () => {
4949
);
5050
ReactServer = require('react');
5151
ReactServerDOMServer = require('react-server-dom-webpack/server');
52-
if (__EXPERIMENTAL__) {
53-
jest.mock('react-server-dom-webpack/static', () =>
54-
require('react-server-dom-webpack/static.node'),
55-
);
56-
ReactServerDOMStaticServer = require('react-server-dom-webpack/static');
57-
}
52+
jest.mock('react-server-dom-webpack/static', () =>
53+
require('react-server-dom-webpack/static.node'),
54+
);
55+
ReactServerDOMStaticServer = require('react-server-dom-webpack/static');
5856

5957
const WebpackMock = require('./utils/WebpackMock');
6058
clientExports = WebpackMock.clientExports;
@@ -385,7 +383,6 @@ describe('ReactFlightDOMNode', () => {
385383
expect(errors).toEqual([reason]);
386384
});
387385

388-
// @gate experimental
389386
it('can prerender', async () => {
390387
let resolveGreeting;
391388
const greetingPromise = new Promise(resolve => {
@@ -440,7 +437,6 @@ describe('ReactFlightDOMNode', () => {
440437
expect(result).toBe('<div>hello world</div>');
441438
});
442439

443-
// @gate enableHalt
444440
it('does not propagate abort reasons errors when aborting a prerender', async () => {
445441
let resolveGreeting;
446442
const greetingPromise = new Promise(resolve => {

packages/react-server/src/ReactFlightServer.js

+7-8
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import type {TemporaryReferenceSet} from './ReactFlightServerTemporaryReferences
1515

1616
import {
1717
enablePostpone,
18-
enableHalt,
1918
enableTaint,
2019
enableProfilerTimer,
2120
enableComponentPerformanceTrack,
@@ -656,7 +655,7 @@ function serializeThenable(
656655
// We can no longer accept any resolved values
657656
request.abortableTasks.delete(newTask);
658657
newTask.status = ABORTED;
659-
if (enableHalt && request.type === PRERENDER) {
658+
if (request.type === PRERENDER) {
660659
request.pendingChunks--;
661660
} else {
662661
const errorId: number = (request.fatalError: any);
@@ -804,7 +803,7 @@ function serializeReadableStream(
804803
}
805804
aborted = true;
806805
request.abortListeners.delete(abortStream);
807-
if (enableHalt && request.type === PRERENDER) {
806+
if (request.type === PRERENDER) {
808807
request.pendingChunks--;
809808
} else {
810809
erroredTask(request, streamTask, reason);
@@ -930,7 +929,7 @@ function serializeAsyncIterable(
930929
}
931930
aborted = true;
932931
request.abortListeners.delete(abortIterable);
933-
if (enableHalt && request.type === PRERENDER) {
932+
if (request.type === PRERENDER) {
934933
request.pendingChunks--;
935934
} else {
936935
erroredTask(request, streamTask, reason);
@@ -2263,7 +2262,7 @@ function serializeBlob(request: Request, blob: Blob): string {
22632262
}
22642263
aborted = true;
22652264
request.abortListeners.delete(abortBlob);
2266-
if (enableHalt && request.type === PRERENDER) {
2265+
if (request.type === PRERENDER) {
22672266
request.pendingChunks--;
22682267
} else {
22692268
erroredTask(request, newTask, reason);
@@ -2318,7 +2317,7 @@ function renderModel(
23182317

23192318
if (request.status === ABORTING) {
23202319
task.status = ABORTED;
2321-
if (enableHalt && request.type === PRERENDER) {
2320+
if (request.type === PRERENDER) {
23222321
// This will create a new task and refer to it in this slot
23232322
// the new task won't be retried because we are aborting
23242323
return outlineHaltedTask(request, task, wasReactNode);
@@ -4048,7 +4047,7 @@ function retryTask(request: Request, task: Task): void {
40484047
if (request.status === ABORTING) {
40494048
request.abortableTasks.delete(task);
40504049
task.status = ABORTED;
4051-
if (enableHalt && request.type === PRERENDER) {
4050+
if (request.type === PRERENDER) {
40524051
// When aborting a prerener with halt semantics we don't emit
40534052
// anything into the slot for a task that aborts, it remains unresolved
40544053
request.pendingChunks--;
@@ -4325,7 +4324,7 @@ export function abort(request: Request, reason: mixed): void {
43254324
}
43264325
const abortableTasks = request.abortableTasks;
43274326
if (abortableTasks.size > 0) {
4328-
if (enableHalt && request.type === PRERENDER) {
4327+
if (request.type === PRERENDER) {
43294328
// When prerendering with halt semantics we simply halt the task
43304329
// and leave the reference unfulfilled.
43314330
abortableTasks.forEach(task => haltTask(task, request));

packages/shared/ReactVersion.js

+1-15
Original file line numberDiff line numberDiff line change
@@ -1,15 +1 @@
1-
/**
2-
* Copyright (c) Meta Platforms, Inc. and affiliates.
3-
*
4-
* This source code is licensed under the MIT license found in the
5-
* LICENSE file in the root directory of this source tree.
6-
*/
7-
8-
// TODO: this is special because it gets imported during build.
9-
//
10-
// It exists as a placeholder so that DevTools can support work tag changes between releases.
11-
// When we next publish a release, update the matching TODO in backend/renderer.js
12-
// TODO: This module is used both by the release scripts and to expose a version
13-
// at runtime. We should instead inject the version number as part of the build
14-
// process, and use the ReactVersions.js module as the single source of truth.
15-
export default '19.1.0';
1+
export default '19.2.0-canary-81f8be04-20250502';

0 commit comments

Comments
 (0)