From d5cbcd669581224a1da6e666614aaef9f2da7b5e Mon Sep 17 00:00:00 2001 From: Hendrik Liebau Date: Wed, 20 Mar 2024 21:47:13 +0100 Subject: [PATCH 1/2] Add a test for issue #28595 The added test, intended to fail and reproduce the [reported issue](https://github.com/facebook/react/issues/28595), unexpectedly passes in its current state. I see three possible reasons: 1. The bug report could be invalid. 2. How I've structured the test might be insufficient to replicate what `ai/rsc` is doing. 3. Something in the test setup could be masking the actual error. (Maybe related to fake timers?) If the problem lies in reason 2 or 3, this test could possibly serve as a foundation for further investigation. --- .../src/__tests__/ReactFlightDOM-test.js | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js index 9ff119ac1419d..cea35bc1b5c29 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js @@ -812,6 +812,105 @@ describe('ReactFlightDOM', () => { expect(reportedErrors).toEqual([]); }); + it('should handle streaming async server components', async () => { + const reportedErrors = []; + + const Row = async ({current, next}) => { + const chunk = await next; + + if (chunk.done) { + return chunk.value; + } + + return ( + + + + ); + }; + + function createResolvablePromise() { + let _resolve, _reject; + + const promise = new Promise((resolve, reject) => { + _resolve = resolve; + _reject = reject; + }); + + return {promise, resolve: _resolve, reject: _reject}; + } + + function createSuspendedChunk(initialValue) { + const {promise, resolve, reject} = createResolvablePromise(); + + return { + row: ( + + + + ), + resolve, + reject, + }; + } + + function makeDelayedText() { + const {promise, resolve, reject} = createResolvablePromise(); + async function DelayedText() { + const data = await promise; + return
{data}
; + } + return [DelayedText, resolve, reject]; + } + + const [Posts, resolvePostsData] = makeDelayedText(); + const suspendedChunk = createSuspendedChunk(

loading

); + const {writable, readable} = getTestStream(); + const {pipe} = ReactServerDOMServer.renderToPipeableStream( + suspendedChunk.row, + webpackMap, + { + onError(error) { + reportedErrors.push(error); + }, + }, + ); + pipe(writable); + const response = ReactServerDOMClient.createFromReadableStream(readable); + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + + function ClientRoot() { + return use(response); + } + + await act(() => { + root.render(); + }); + + expect(container.innerHTML).toBe('

loading

'); + + const donePromise = createResolvablePromise(); + const value = ; + + await act(async () => { + suspendedChunk.resolve({value, done: false, next: donePromise.promise}); + await Promise.resolve(); + donePromise.resolve({value, done: true}); + }); + + expect(container.innerHTML).toBe('

loading

'); + + await act(async () => { + jest.advanceTimersByTime(500); + await resolvePostsData('posts'); + await 'the inner async function'; + }); + + expect(container.innerHTML).toBe('
posts
'); + expect(reportedErrors).toEqual([]); + }); + it('should preserve state of client components on refetch', async () => { // Client From 15fefa1ad927c1453113a8ef776d3081faf5f5f0 Mon Sep 17 00:00:00 2001 From: Hendrik Liebau Date: Thu, 21 Mar 2024 09:14:27 +0100 Subject: [PATCH 2/2] Revert "[Flight] Serialize deduped elements by direct reference even if they suspend (#28283)" This reverts commit ba5e6a8329c7194a2c573c037a37f24ce45ee58f. --- packages/react-server/src/ReactFlightServer.js | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index d253db44c4a15..a7404833986c0 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -1334,16 +1334,12 @@ function renderModelDestructive( // but that is able to reuse the same task if we're already in one but then that // will be a lazy future value rather than guaranteed to exist but maybe that's good. const newId = outlineModel(request, (value: any)); - return serializeByValueID(newId); + return serializeLazyID(newId); } else { // We've already emitted this as an outlined object, so we can refer to that by its - // existing ID. TODO: We should use a lazy reference since, unlike plain objects, - // elements might suspend so it might not have emitted yet even if we have the ID for - // it. However, this creates an extra wrapper when it's not needed. We should really - // detect whether this already was emitted and synchronously available. In that - // case we can refer to it synchronously and only make it lazy otherwise. - // We currently don't have a data structure that lets us see that though. - return serializeByValueID(existingId); + // existing ID. We use a lazy reference since, unlike plain objects, elements might + // suspend so it might not have emitted yet even if we have the ID for it. + return serializeLazyID(existingId); } } else { // This is the first time we've seen this object. We may never see it again