diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js
index 8b82caac6bc749..09ea1515c99972 100644
--- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js
+++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js
@@ -69,6 +69,31 @@ describe('ReactFlightDOMBrowser', () => {
}
}
+ function makeDelayedText(Model) {
+ let error, _resolve, _reject;
+ let promise = new Promise((resolve, reject) => {
+ _resolve = () => {
+ promise = null;
+ resolve();
+ };
+ _reject = e => {
+ error = e;
+ promise = null;
+ reject(e);
+ };
+ });
+ function DelayedText({children}, data) {
+ if (promise) {
+ throw promise;
+ }
+ if (error) {
+ throw error;
+ }
+ return {children};
+ }
+ return [DelayedText, _resolve, _reject];
+ }
+
it('should resolve HTML using W3C streams', async () => {
function Text({children}) {
return {children};
@@ -174,36 +199,11 @@ describe('ReactFlightDOMBrowser', () => {
return children;
}
- function makeDelayedText() {
- let error, _resolve, _reject;
- let promise = new Promise((resolve, reject) => {
- _resolve = () => {
- promise = null;
- resolve();
- };
- _reject = e => {
- error = e;
- promise = null;
- reject(e);
- };
- });
- function DelayedText({children}, data) {
- if (promise) {
- throw promise;
- }
- if (error) {
- throw error;
- }
- return {children};
- }
- return [DelayedText, _resolve, _reject];
- }
-
- const [Friends, resolveFriends] = makeDelayedText();
- const [Name, resolveName] = makeDelayedText();
- const [Posts, resolvePosts] = makeDelayedText();
- const [Photos, resolvePhotos] = makeDelayedText();
- const [Games, , rejectGames] = makeDelayedText();
+ const [Friends, resolveFriends] = makeDelayedText(Text);
+ const [Name, resolveName] = makeDelayedText(Text);
+ const [Posts, resolvePosts] = makeDelayedText(Text);
+ const [Photos, resolvePhotos] = makeDelayedText(Text);
+ const [Games, , rejectGames] = makeDelayedText(Text);
// View
function ProfileDetails({avatar}) {
@@ -340,4 +340,117 @@ describe('ReactFlightDOMBrowser', () => {
expect(reportedErrors).toEqual([]);
});
+
+ it('should close the stream upon completion when rendering to W3C streams', async () => {
+ const {Suspense} = React;
+
+ // Model
+ function Text({children}) {
+ return children;
+ }
+
+ const [Friends, resolveFriends] = makeDelayedText(Text);
+ const [Name, resolveName] = makeDelayedText(Text);
+ const [Posts, resolvePosts] = makeDelayedText(Text);
+ const [Photos, resolvePhotos] = makeDelayedText(Text);
+
+ // View
+ function ProfileDetails({avatar}) {
+ return (
+
+ :name:
+ {avatar}
+
+ );
+ }
+ function ProfileSidebar({friends}) {
+ return (
+
+ );
+ }
+ function ProfilePosts({posts}) {
+ return {posts}
;
+ }
+
+ function ProfileContent() {
+ return (
+
+ :avatar:} />
+ (loading sidebar)}>
+ :friends:} />
+
+ (loading posts)}>
+ :posts:} />
+
+
+ );
+ }
+
+ const model = {
+ rootContent: ,
+ };
+
+ const stream = ReactServerDOMWriter.renderToReadableStream(
+ model,
+ webpackMap,
+ );
+
+ const reader = stream.getReader();
+ const decoder = new TextDecoder();
+
+ let flightResponse = '';
+ let isDone = false;
+
+ reader.read().then(function progress({done, value}) {
+ if (done) {
+ isDone = true;
+ return;
+ }
+
+ flightResponse += decoder.decode(value);
+
+ return reader.read().then(progress);
+ });
+
+ // Advance time enough to trigger a nested fallback.
+ jest.advanceTimersByTime(500);
+
+ await act(async () => {});
+
+ expect(flightResponse).toContain('(loading everything)');
+ expect(flightResponse).toContain('(loading sidebar)');
+ expect(flightResponse).toContain('(loading posts)');
+ expect(flightResponse).not.toContain(':friends:');
+ expect(flightResponse).not.toContain(':name:');
+
+ await act(async () => {
+ resolveFriends();
+ });
+
+ expect(flightResponse).toContain(':friends:');
+
+ await act(async () => {
+ resolveName();
+ });
+
+ expect(flightResponse).toContain(':name:');
+
+ await act(async () => {
+ resolvePhotos();
+ });
+
+ expect(flightResponse).toContain(':photos:');
+
+ await act(async () => {
+ resolvePosts();
+ });
+
+ expect(flightResponse).toContain(':posts:');
+
+ // Final pending chunk is written; stream should be closed.
+ expect(isDone).toBeTruthy();
+ });
});
diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js
index e46aee9a45cd23..39fcdf31682055 100644
--- a/packages/react-server/src/ReactFizzServer.js
+++ b/packages/react-server/src/ReactFizzServer.js
@@ -1950,6 +1950,10 @@ export function startFlowing(request: Request, destination: Destination): void {
if (request.status === CLOSED) {
return;
}
+ if (request.destination !== null) {
+ // We're already flowing.
+ return;
+ }
request.destination = destination;
try {
flushCompletedQueues(request, destination);
diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js
index 052fa730fd91e0..5ad7ed2ea041c6 100644
--- a/packages/react-server/src/ReactFlightServer.js
+++ b/packages/react-server/src/ReactFlightServer.js
@@ -790,6 +790,10 @@ export function startFlowing(request: Request, destination: Destination): void {
if (request.status === CLOSED) {
return;
}
+ if (request.destination !== null) {
+ // We're already flowing.
+ return;
+ }
request.destination = destination;
try {
flushCompletedChunks(request, destination);