Skip to content

Commit e6dc25d

Browse files
authored
[Flight] Always defer Promise values if they're not already resolved (facebook#33742)
If we have the ability to lazy load Promise values, i.e. if we have a debug channel, then we should always use it for Promises that aren't already resolved and instrumented. There's little downside to this since they're async anyway. This also lets us avoid adding `.then()` listeners too early. E.g. if adding the listener would have side-effect. This avoids covering up "unhandled rejection" errors. Since if we listen to a promise eagerly, including reject listeners, we'd have marked that Promise's rejection as handled where as maybe it wouldn't have been otherwise. In this mode we can also indefinitely wait for the Promise to resolve instead of just waiting a microtask for it to resolve.
1 parent 150f022 commit e6dc25d

File tree

2 files changed

+83
-2
lines changed

2 files changed

+83
-2
lines changed

packages/react-client/src/ReactFlightClient.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2048,6 +2048,18 @@ function parseModelString(
20482048
if (value.length > 2) {
20492049
const debugChannel = response._debugChannel;
20502050
if (debugChannel) {
2051+
if (value[2] === '@') {
2052+
// This is a deferred Promise.
2053+
const ref = value.slice(3); // We assume this doesn't have a path just id.
2054+
const id = parseInt(ref, 16);
2055+
if (!response._chunks.has(id)) {
2056+
// We haven't seen this id before. Query the server to start sending it.
2057+
debugChannel('P:' + ref);
2058+
}
2059+
// Start waiting. This now creates a pending chunk if it doesn't already exist.
2060+
// This is the actual Promise we're waiting for.
2061+
return getChunk(response, id);
2062+
}
20512063
const ref = value.slice(2); // We assume this doesn't have a path just id.
20522064
const id = parseInt(ref, 16);
20532065
if (!response._chunks.has(id)) {

packages/react-server/src/ReactFlightServer.js

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -801,6 +801,19 @@ function serializeDebugThenable(
801801
return ref;
802802
}
803803

804+
const deferredDebugObjects = request.deferredDebugObjects;
805+
if (deferredDebugObjects !== null) {
806+
// For Promises that are not yet resolved, we always defer them. They are async anyway so it's
807+
// safe to defer them. This also ensures that we don't eagerly call .then() on a Promise that
808+
// otherwise wouldn't have initialized. It also ensures that we don't "handle" a rejection
809+
// that otherwise would have triggered unhandled rejection.
810+
deferredDebugObjects.retained.set(id, (thenable: any));
811+
const deferredRef = '$Y@' + id.toString(16);
812+
// We can now refer to the deferred object in the future.
813+
request.writtenDebugObjects.set(thenable, deferredRef);
814+
return deferredRef;
815+
}
816+
804817
let cancelled = false;
805818

806819
thenable.then(
@@ -853,6 +866,36 @@ function serializeDebugThenable(
853866
return ref;
854867
}
855868

869+
function emitRequestedDebugThenable(
870+
request: Request,
871+
id: number,
872+
counter: {objectLimit: number},
873+
thenable: Thenable<any>,
874+
): void {
875+
thenable.then(
876+
value => {
877+
if (request.status === ABORTING) {
878+
emitDebugHaltChunk(request, id);
879+
enqueueFlush(request);
880+
return;
881+
}
882+
emitOutlinedDebugModelChunk(request, id, counter, value);
883+
enqueueFlush(request);
884+
},
885+
reason => {
886+
if (request.status === ABORTING) {
887+
emitDebugHaltChunk(request, id);
888+
enqueueFlush(request);
889+
return;
890+
}
891+
// We don't log these errors since they didn't actually throw into Flight.
892+
const digest = '';
893+
emitErrorChunk(request, id, digest, reason, true);
894+
enqueueFlush(request);
895+
},
896+
);
897+
}
898+
856899
function serializeThenable(
857900
request: Request,
858901
task: Task,
@@ -4384,8 +4427,15 @@ function renderDebugModel(
43844427
} else if (debugNoOutline !== value) {
43854428
// If this isn't the root object (like meta data) and we don't have an id for it, outline
43864429
// it so that we can dedupe it by reference later.
4387-
const outlinedId = outlineDebugModel(request, counter, value);
4388-
return serializeByValueID(outlinedId);
4430+
// $FlowFixMe[method-unbinding]
4431+
if (typeof value.then === 'function') {
4432+
// If this is a Promise we're going to assign it an external ID anyway which can be deduped.
4433+
const thenable: Thenable<any> = (value: any);
4434+
return serializeDebugThenable(request, counter, thenable);
4435+
} else {
4436+
const outlinedId = outlineDebugModel(request, counter, value);
4437+
return serializeByValueID(outlinedId);
4438+
}
43894439
}
43904440
}
43914441

@@ -5787,6 +5837,25 @@ export function resolveDebugMessage(request: Request, message: string): void {
57875837
}
57885838
}
57895839
break;
5840+
case 80 /* "P" */:
5841+
// Query Promise IDs
5842+
for (let i = 0; i < ids.length; i++) {
5843+
const id = ids[i];
5844+
const retainedValue = deferredDebugObjects.retained.get(id);
5845+
if (retainedValue !== undefined) {
5846+
// If we still have this Promise, and haven't emitted it before, wait for it
5847+
// and then emit it on the stream.
5848+
const counter = {objectLimit: 10};
5849+
deferredDebugObjects.retained.delete(id);
5850+
emitRequestedDebugThenable(
5851+
request,
5852+
id,
5853+
counter,
5854+
(retainedValue: any),
5855+
);
5856+
}
5857+
}
5858+
break;
57905859
default:
57915860
throw new Error(
57925861
'Unknown command. The debugChannel was not wired up properly.',

0 commit comments

Comments
 (0)