[Flight] Allow Temporary References to be awaited (#34084)

Fixes #33534.

`.then` method can be tested when you await a value that's not a
Promise. For regular Client References we have a way to mark those as
"async" and yield a reference to the unwrapped value in case it's a
Promise on the Client.

However, the realization is that we never serialize Promises as opaque
when passed from the client to the server. If a Promise is passed, then
it would've been deserialized as a Promise (while still registered as a
temporary reference) and not one of these Proxy objects.

Technically it could be a non-function value on the client which would
be wrong but you're not supposed to dot into it in the first place.

So we can just assume it's `undefined`.
This commit is contained in:
Sebastian Markbåge 2025-08-02 18:44:20 -04:00 committed by GitHub
parent 1d163962b2
commit c499adf8c8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 49 additions and 37 deletions

View File

@ -438,6 +438,50 @@ describe('ReactFlightDOMReply', () => {
expect(response.obj).toBe(obj); expect(response.obj).toBe(obj);
}); });
it('can return an opaque object through an async function', async () => {
function fn() {
return 'this is a client function';
}
const args = [fn];
const temporaryReferences =
ReactServerDOMClient.createTemporaryReferenceSet();
const body = await ReactServerDOMClient.encodeReply(args, {
temporaryReferences,
});
const temporaryReferencesServer =
ReactServerDOMServer.createTemporaryReferenceSet();
const serverPayload = await ReactServerDOMServer.decodeReply(
body,
webpackServerMap,
{temporaryReferences: temporaryReferencesServer},
);
async function action(arg) {
return arg;
}
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(
{
result: action.apply(null, serverPayload),
},
null,
{temporaryReferences: temporaryReferencesServer},
),
);
const response = await ReactServerDOMClient.createFromReadableStream(
stream,
{
temporaryReferences,
},
);
expect(await response.result).toBe(fn);
});
it('should supports streaming ReadableStream with objects', async () => { it('should supports streaming ReadableStream with objects', async () => {
let controller1; let controller1;
let controller2; let controller2;

View File

@ -70,8 +70,12 @@ const proxyHandlers = {
`Instead, you can export a Client Component wrapper ` + `Instead, you can export a Client Component wrapper ` +
`that itself renders a Client Context Provider.`, `that itself renders a Client Context Provider.`,
); );
// Allow returning a temporary reference from an async function
case 'then': case 'then':
// Allow returning a temporary reference from an async function
// Unlike regular Client References, a Promise would never have been serialized as
// an opaque Temporary Reference, but instead would have been serialized as a
// Promise on the server and so doesn't hit this path. So we can assume this wasn't
// a Promise on the client.
return undefined; return undefined;
} }
throw new Error( throw new Error(

View File

@ -1,36 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
* @jest-environment node
*/
'use strict';
let ReactFlightServerTemporaryReferences;
describe('ReactFlightServerTemporaryReferences', () => {
beforeEach(() => {
jest.resetModules();
ReactFlightServerTemporaryReferences = require('react-server/src/ReactFlightServerTemporaryReferences');
});
it('can return a temporary reference from an async function', async () => {
const temporaryReferenceSet =
ReactFlightServerTemporaryReferences.createTemporaryReferenceSet();
const temporaryReference =
ReactFlightServerTemporaryReferences.createTemporaryReference(
temporaryReferenceSet,
'test',
);
async function foo() {
return temporaryReference;
}
await expect(foo()).resolves.toBe(temporaryReference);
});
});