diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 37e7657115..da00763840 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -2048,6 +2048,18 @@ function parseModelString( if (value.length > 2) { const debugChannel = response._debugChannel; if (debugChannel) { + if (value[2] === '@') { + // This is a deferred Promise. + const ref = value.slice(3); // We assume this doesn't have a path just id. + const id = parseInt(ref, 16); + if (!response._chunks.has(id)) { + // We haven't seen this id before. Query the server to start sending it. + debugChannel('P:' + ref); + } + // Start waiting. This now creates a pending chunk if it doesn't already exist. + // This is the actual Promise we're waiting for. + return getChunk(response, id); + } const ref = value.slice(2); // We assume this doesn't have a path just id. const id = parseInt(ref, 16); if (!response._chunks.has(id)) { diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index ef5938a6bb..89f37d7e9f 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -801,6 +801,19 @@ function serializeDebugThenable( return ref; } + const deferredDebugObjects = request.deferredDebugObjects; + if (deferredDebugObjects !== null) { + // For Promises that are not yet resolved, we always defer them. They are async anyway so it's + // safe to defer them. This also ensures that we don't eagerly call .then() on a Promise that + // otherwise wouldn't have initialized. It also ensures that we don't "handle" a rejection + // that otherwise would have triggered unhandled rejection. + deferredDebugObjects.retained.set(id, (thenable: any)); + const deferredRef = '$Y@' + id.toString(16); + // We can now refer to the deferred object in the future. + request.writtenDebugObjects.set(thenable, deferredRef); + return deferredRef; + } + let cancelled = false; thenable.then( @@ -853,6 +866,36 @@ function serializeDebugThenable( return ref; } +function emitRequestedDebugThenable( + request: Request, + id: number, + counter: {objectLimit: number}, + thenable: Thenable, +): void { + thenable.then( + value => { + if (request.status === ABORTING) { + emitDebugHaltChunk(request, id); + enqueueFlush(request); + return; + } + emitOutlinedDebugModelChunk(request, id, counter, value); + enqueueFlush(request); + }, + reason => { + if (request.status === ABORTING) { + emitDebugHaltChunk(request, id); + enqueueFlush(request); + return; + } + // We don't log these errors since they didn't actually throw into Flight. + const digest = ''; + emitErrorChunk(request, id, digest, reason, true); + enqueueFlush(request); + }, + ); +} + function serializeThenable( request: Request, task: Task, @@ -4384,8 +4427,15 @@ function renderDebugModel( } else if (debugNoOutline !== value) { // If this isn't the root object (like meta data) and we don't have an id for it, outline // it so that we can dedupe it by reference later. - const outlinedId = outlineDebugModel(request, counter, value); - return serializeByValueID(outlinedId); + // $FlowFixMe[method-unbinding] + if (typeof value.then === 'function') { + // If this is a Promise we're going to assign it an external ID anyway which can be deduped. + const thenable: Thenable = (value: any); + return serializeDebugThenable(request, counter, thenable); + } else { + const outlinedId = outlineDebugModel(request, counter, value); + return serializeByValueID(outlinedId); + } } } @@ -5787,6 +5837,25 @@ export function resolveDebugMessage(request: Request, message: string): void { } } break; + case 80 /* "P" */: + // Query Promise IDs + for (let i = 0; i < ids.length; i++) { + const id = ids[i]; + const retainedValue = deferredDebugObjects.retained.get(id); + if (retainedValue !== undefined) { + // If we still have this Promise, and haven't emitted it before, wait for it + // and then emit it on the stream. + const counter = {objectLimit: 10}; + deferredDebugObjects.retained.delete(id); + emitRequestedDebugThenable( + request, + id, + counter, + (retainedValue: any), + ); + } + } + break; default: throw new Error( 'Unknown command. The debugChannel was not wired up properly.',