Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Serialize Promises through Flight #26086

Merged
merged 3 commits into from
Feb 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/react-client/src/ReactFlightClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,12 @@ export function parseModelString(
// When passed into React, we'll know how to suspend on this.
return createLazyChunkWrapper(chunk);
}
case '@': {
// Promise
const id = parseInt(value.substring(2), 16);
const chunk = getChunk(response, id);
return chunk;
}
case 'S': {
return Symbol.for(value.substring(2));
}
Expand Down
23 changes: 13 additions & 10 deletions packages/react-reconciler/src/ReactFiberThenable.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ export function trackUsedThenable<T>(
// Only instrument the thenable if the status if not defined. If
// it's defined, but an unknown value, assume it's been instrumented by
// some custom userspace implementation. We treat it as "pending".
// Attach a dummy listener, to ensure that any lazy initialization can
// happen. Flight lazily parses JSON when the value is actually awaited.
thenable.then(noop, noop);
} else {
const pendingThenable: PendingThenable<T> = (thenable: any);
pendingThenable.status = 'pending';
Expand All @@ -107,17 +110,17 @@ export function trackUsedThenable<T>(
}
},
);
}

// Check one more time in case the thenable resolved synchronously
switch (thenable.status) {
case 'fulfilled': {
const fulfilledThenable: FulfilledThenable<T> = (thenable: any);
return fulfilledThenable.value;
}
case 'rejected': {
const rejectedThenable: RejectedThenable<T> = (thenable: any);
throw rejectedThenable.reason;
}
// Check one more time in case the thenable resolved synchronously.
switch (thenable.status) {
case 'fulfilled': {
const fulfilledThenable: FulfilledThenable<T> = (thenable: any);
return fulfilledThenable.value;
}
case 'rejected': {
const rejectedThenable: RejectedThenable<T> = (thenable: any);
throw rejectedThenable.reason;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -905,4 +905,50 @@ describe('ReactFlightDOM', () => {

expect(reportedErrors).toEqual(['bug in the bundler']);
});

// @gate enableUseHook
it('should pass a Promise through props and be able use() it on the client', async () => {
async function getData() {
return 'async hello';
}

function Component({data}) {
const text = use(data);
return <p>{text}</p>;
}

const ClientComponent = clientExports(Component);

function ServerComponent() {
const data = getData(); // no await here
return <ClientComponent data={data} />;
}

function Print({response}) {
return use(response);
}

function App({response}) {
return (
<Suspense fallback={<h1>Loading...</h1>}>
<Print response={response} />
</Suspense>
);
}

const {writable, readable} = getTestStream();
const {pipe} = ReactServerDOMWriter.renderToPipeableStream(
<ServerComponent />,
webpackMap,
);
pipe(writable);
const response = ReactServerDOMReader.createFromReadableStream(readable);

const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(<App response={response} />);
});
sebmarkbage marked this conversation as resolved.
Show resolved Hide resolved
expect(container.innerHTML).toBe('<p>async hello</p>');
});
});
109 changes: 104 additions & 5 deletions packages/react-server/src/ReactFlightServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,82 @@ const POP = {};
const jsxPropsParents: WeakMap<any, any> = new WeakMap();
const jsxChildrenParents: WeakMap<any, any> = new WeakMap();

function serializeThenable(request: Request, thenable: Thenable<any>): number {
request.pendingChunks++;
const newTask = createTask(
request,
null,
getActiveContext(),
request.abortableTasks,
);

switch (thenable.status) {
case 'fulfilled': {
// We have the resolved value, we can go ahead and schedule it for serialization.
newTask.model = thenable.value;
pingTask(request, newTask);
return newTask.id;
}
case 'rejected': {
const x = thenable.reason;
const digest = logRecoverableError(request, x);
if (__DEV__) {
const {message, stack} = getErrorMessageAndStackDev(x);
emitErrorChunkDev(request, newTask.id, digest, message, stack);
} else {
emitErrorChunkProd(request, newTask.id, digest);
}
return newTask.id;
}
default: {
if (typeof thenable.status === 'string') {
// Only instrument the thenable if the status if not defined. If
// it's defined, but an unknown value, assume it's been instrumented by
// some custom userspace implementation. We treat it as "pending".
break;
}
const pendingThenable: PendingThenable<mixed> = (thenable: any);
pendingThenable.status = 'pending';
pendingThenable.then(
fulfilledValue => {
if (thenable.status === 'pending') {
const fulfilledThenable: FulfilledThenable<mixed> = (thenable: any);
fulfilledThenable.status = 'fulfilled';
fulfilledThenable.value = fulfilledValue;
}
},
(error: mixed) => {
if (thenable.status === 'pending') {
const rejectedThenable: RejectedThenable<mixed> = (thenable: any);
rejectedThenable.status = 'rejected';
rejectedThenable.reason = error;
}
},
);
break;
}
}

thenable.then(
value => {
newTask.model = value;
pingTask(request, newTask);
},
reason => {
// TODO: Is it safe to directly emit these without being inside a retry?
const digest = logRecoverableError(request, reason);
if (__DEV__) {
const {message, stack} = getErrorMessageAndStackDev(reason);
emitErrorChunkDev(request, newTask.id, digest, message, stack);
} else {
emitErrorChunkProd(request, newTask.id, digest);
}
},
);

return newTask.id;
}

function readThenable<T>(thenable: Thenable<T>): T {
if (thenable.status === 'fulfilled') {
return thenable.value;
Expand Down Expand Up @@ -270,6 +346,7 @@ function createLazyWrapperAroundWakeable(wakeable: Wakeable) {
}

function attemptResolveElement(
request: Request,
type: any,
key: null | React$Key,
ref: mixed,
Expand Down Expand Up @@ -303,6 +380,14 @@ function attemptResolveElement(
result !== null &&
typeof result.then === 'function'
) {
// When the return value is in children position we can resolve it immediately,
// to its value without a wrapper if it's synchronously available.
const thenable: Thenable<any> = result;
if (thenable.status === 'fulfilled') {
return thenable.value;
}
// TODO: Once we accept Promises as children on the client, we can just return
// the thenable here.
return createLazyWrapperAroundWakeable(result);
}
return result;
Expand Down Expand Up @@ -331,6 +416,7 @@ function attemptResolveElement(
const init = type._init;
const wrappedType = init(payload);
return attemptResolveElement(
request,
wrappedType,
key,
ref,
Expand All @@ -345,6 +431,7 @@ function attemptResolveElement(
}
case REACT_MEMO_TYPE: {
return attemptResolveElement(
request,
type.type,
key,
ref,
Expand Down Expand Up @@ -414,10 +501,14 @@ function serializeByValueID(id: number): string {
return '$' + id.toString(16);
}

function serializeByRefID(id: number): string {
function serializeLazyID(id: number): string {
return '$L' + id.toString(16);
}

function serializePromiseID(id: number): string {
return '$@' + id.toString(16);
}

function serializeSymbolReference(name: string): string {
return '$S' + name;
}
Expand All @@ -442,7 +533,7 @@ function serializeClientReference(
// knows how to deal with lazy values. This lets us suspend
// on this component rather than its parent until the code has
// loaded.
return serializeByRefID(existingId);
return serializeLazyID(existingId);
}
return serializeByValueID(existingId);
}
Expand All @@ -461,7 +552,7 @@ function serializeClientReference(
// knows how to deal with lazy values. This lets us suspend
// on this component rather than its parent until the code has
// loaded.
return serializeByRefID(moduleId);
return serializeLazyID(moduleId);
}
return serializeByValueID(moduleId);
} catch (x) {
Expand Down Expand Up @@ -835,6 +926,7 @@ export function resolveModelToJSON(
const element: React$Element<any> = (value: any);
// Attempt to render the Server Component.
value = attemptResolveElement(
request,
element.type,
element.key,
element.ref,
Expand Down Expand Up @@ -873,7 +965,7 @@ export function resolveModelToJSON(
const ping = newTask.ping;
x.then(ping, ping);
newTask.thenableState = getThenableStateAfterSuspending();
return serializeByRefID(newTask.id);
return serializeLazyID(newTask.id);
} else {
// Something errored. We'll still send everything we have up until this point.
// We'll replace this element with a lazy reference that throws on the client
Expand All @@ -887,7 +979,7 @@ export function resolveModelToJSON(
} else {
emitErrorChunkProd(request, errorId, digest);
}
return serializeByRefID(errorId);
return serializeLazyID(errorId);
}
}
}
Expand All @@ -899,6 +991,11 @@ export function resolveModelToJSON(
if (typeof value === 'object') {
if (isClientReference(value)) {
return serializeClientReference(request, parent, key, (value: any));
} else if (typeof value.then === 'function') {
// We assume that any object with a .then property is a "Thenable" type,
// or a Promise type. Either of which can be represented by a Promise.
const promiseId = serializeThenable(request, (value: any));
return serializePromiseID(promiseId);
} else if ((value: any).$$typeof === REACT_PROVIDER_TYPE) {
const providerKey = ((value: any): ReactProviderType<any>)._context
._globalName;
Expand Down Expand Up @@ -1157,6 +1254,7 @@ function retryTask(request: Request, task: Task): void {
// also suspends.
task.model = value;
value = attemptResolveElement(
request,
element.type,
element.key,
element.ref,
Expand All @@ -1180,6 +1278,7 @@ function retryTask(request: Request, task: Task): void {
const nextElement: React$Element<any> = (value: any);
task.model = value;
value = attemptResolveElement(
request,
nextElement.type,
nextElement.key,
nextElement.ref,
Expand Down