Skip to content

Commit 27b9016

Browse files
committed
Treat blocked modules or models as a special status
We currently loop over all chunks at the end to error them if they're still pending. We shouldn't do this if they're pending because they're blocked on an external resource like a module because the module might not resolve before the Flight connection closes and that's not an error. In an alternative solution I had a set that tracked pending chunks and removed one at a time. While the loop at the end is faster it's more work as we go. I figured the extra status might also help debugging. For modules we can probably assume no forward references, and the first async module we can just use the promise as the chunk. So we could probably get away with this only on models that are blocked by modules.
1 parent c84f7d2 commit 27b9016

File tree

5 files changed

+67
-44
lines changed

5 files changed

+67
-44
lines changed

packages/react-client/src/ReactFlightClient.js

+43-16
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export type JSONValue =
3838
| $ReadOnlyArray<JSONValue>;
3939

4040
const PENDING = 'pending';
41+
const BLOCKED = 'blocked';
4142
const RESOLVED_MODEL = 'resolved_model';
4243
const RESOLVED_MODULE = 'resolved_module';
4344
const INITIALIZED = 'fulfilled';
@@ -50,6 +51,13 @@ type PendingChunk<T> = {
5051
_response: Response,
5152
then(resolve: (T) => mixed, reject: (mixed) => mixed): void,
5253
};
54+
type BlockedChunk<T> = {
55+
status: 'blocked',
56+
value: null | Array<(T) => mixed>,
57+
reason: null | Array<(mixed) => mixed>,
58+
_response: Response,
59+
then(resolve: (T) => mixed, reject: (mixed) => mixed): void,
60+
};
5361
type ResolvedModelChunk<T> = {
5462
status: 'resolved_model',
5563
value: UninitializedModel,
@@ -80,6 +88,7 @@ type ErroredChunk<T> = {
8088
};
8189
type SomeChunk<T> =
8290
| PendingChunk<T>
91+
| BlockedChunk<T>
8392
| ResolvedModelChunk<T>
8493
| ResolvedModuleChunk<T>
8594
| InitializedChunk<T>
@@ -115,6 +124,7 @@ Chunk.prototype.then = function<T>(
115124
resolve(chunk.value);
116125
break;
117126
case PENDING:
127+
case BLOCKED:
118128
if (resolve) {
119129
if (chunk.value === null) {
120130
chunk.value = [];
@@ -159,8 +169,9 @@ function readChunk<T>(chunk: SomeChunk<T>): T {
159169
case INITIALIZED:
160170
return chunk.value;
161171
case PENDING:
172+
case BLOCKED:
162173
// eslint-disable-next-line no-throw-literal
163-
throw (chunk: Thenable<T>);
174+
throw ((chunk: any): Thenable<T>);
164175
default:
165176
throw chunk.reason;
166177
}
@@ -177,6 +188,11 @@ function createPendingChunk<T>(response: Response): PendingChunk<T> {
177188
return new Chunk(PENDING, null, null, response);
178189
}
179190

191+
function createBlockedChunk<T>(response: Response): BlockedChunk<T> {
192+
// $FlowFixMe Flow doesn't support functions as constructors
193+
return new Chunk(BLOCKED, null, null, response);
194+
}
195+
180196
function createErrorChunk<T>(
181197
response: Response,
182198
error: Error,
@@ -210,6 +226,7 @@ function wakeChunkIfInitialized<T>(
210226
wakeChunk(resolveListeners, chunk.value);
211227
break;
212228
case PENDING:
229+
case BLOCKED:
213230
chunk.value = resolveListeners;
214231
chunk.reason = rejectListeners;
215232
break;
@@ -222,7 +239,7 @@ function wakeChunkIfInitialized<T>(
222239
}
223240

224241
function triggerErrorOnChunk<T>(chunk: SomeChunk<T>, error: mixed): void {
225-
if (chunk.status !== PENDING) {
242+
if (chunk.status !== PENDING && chunk.status !== BLOCKED) {
226243
// We already resolved. We didn't expect to see this.
227244
return;
228245
}
@@ -278,7 +295,7 @@ function resolveModuleChunk<T>(
278295
chunk: SomeChunk<T>,
279296
value: ModuleReference<T>,
280297
): void {
281-
if (chunk.status !== PENDING) {
298+
if (chunk.status !== PENDING && chunk.status !== BLOCKED) {
282299
// We already resolved. We didn't expect to see this.
283300
return;
284301
}
@@ -308,11 +325,11 @@ function initializeModelChunk<T>(chunk: ResolvedModelChunk<T>): void {
308325
) {
309326
initializingChunkBlockedModel.value = value;
310327
// We discovered new dependencies on modules that are not yet resolved.
311-
// We have to return to the PENDING state until they're resolved.
312-
const pendingChunk: PendingChunk<T> = (chunk: any);
313-
pendingChunk.status = PENDING;
314-
pendingChunk.value = null;
315-
pendingChunk.reason = null;
328+
// We have to go the BLOCKED state until they're resolved.
329+
const blockedChunk: BlockedChunk<T> = (chunk: any);
330+
blockedChunk.status = BLOCKED;
331+
blockedChunk.value = null;
332+
blockedChunk.reason = null;
316333
} else {
317334
const initializedChunk: InitializedChunk<T> = (chunk: any);
318335
initializedChunk.status = INITIALIZED;
@@ -348,7 +365,9 @@ export function reportGlobalError(response: Response, error: Error): void {
348365
// If this chunk was already resolved or errored, it won't
349366
// trigger an error but if it wasn't then we need to
350367
// because we won't be getting any new data to resolve it.
351-
triggerErrorOnChunk(chunk, error);
368+
if (chunk.status === PENDING) {
369+
triggerErrorOnChunk(chunk, error);
370+
}
352371
});
353372
}
354373

@@ -433,7 +452,7 @@ function createModelResolver<T>(
433452
parentObject[key] = value;
434453
blocked.deps--;
435454
if (blocked.deps === 0) {
436-
if (chunk.status !== PENDING) {
455+
if (chunk.status !== BLOCKED) {
437456
return;
438457
}
439458
const resolveListeners = chunk.value;
@@ -480,6 +499,7 @@ export function parseModelString(
480499
case INITIALIZED:
481500
return chunk.value;
482501
case PENDING:
502+
case BLOCKED:
483503
const parentChunk = initializingChunk;
484504
chunk.then(
485505
createModelResolver(parentChunk, parentObject, key),
@@ -573,21 +593,28 @@ export function resolveModule(
573593
// that we'll need them.
574594
const promise = preloadModule(moduleReference);
575595
if (promise) {
576-
let pendingChunk;
596+
let blockedChunk: BlockedChunk<any>;
577597
if (!chunk) {
578-
pendingChunk = createPendingChunk(response);
579-
chunks.set(id, pendingChunk);
598+
// Technically, we should just treat promise as the chunk in this
599+
// case. Because it'll just behave as any other promise.
600+
blockedChunk = createBlockedChunk(response);
601+
chunks.set(id, blockedChunk);
580602
} else {
581-
pendingChunk = chunk;
603+
// This can't actually happen because we don't have any forward
604+
// references to modules.
605+
blockedChunk = (chunk: any);
606+
blockedChunk.status = BLOCKED;
582607
}
583608
promise.then(
584-
() => resolveModuleChunk(pendingChunk, moduleReference),
585-
error => triggerErrorOnChunk(pendingChunk, error),
609+
() => resolveModuleChunk(blockedChunk, moduleReference),
610+
error => triggerErrorOnChunk(blockedChunk, error),
586611
);
587612
} else {
588613
if (!chunk) {
589614
chunks.set(id, createResolvedModuleChunk(response, moduleReference));
590615
} else {
616+
// This can't actually happen because we don't have any forward
617+
// references to modules.
591618
resolveModuleChunk(chunk, moduleReference);
592619
}
593620
}

packages/react-reconciler/src/ReactFiberWakeable.new.js

+6-7
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,6 @@ export function trackSuspendedWakeable(wakeable: Wakeable) {
6161
// If the thenable doesn't have a status, set it to "pending" and attach
6262
// a listener that will update its status and result when it resolves.
6363
switch (thenable.status) {
64-
case 'pending':
65-
// Since the status is already "pending", we can assume it will be updated
66-
// when it resolves, either by React or something in userspace.
67-
break;
6864
case 'fulfilled':
6965
case 'rejected':
7066
// A thenable that already resolved shouldn't have been thrown, so this is
@@ -75,9 +71,12 @@ export function trackSuspendedWakeable(wakeable: Wakeable) {
7571
suspendedThenable = null;
7672
break;
7773
default: {
78-
// TODO: Only instrument the thenable if the status if not defined. If
79-
// it's defined, but an unknown value, assume it's been instrumented by
80-
// some custom userspace implementation.
74+
if (typeof thenable.status === 'string') {
75+
// Only instrument the thenable if the status if not defined. If
76+
// it's defined, but an unknown value, assume it's been instrumented by
77+
// some custom userspace implementation. We treat it as "pending".
78+
break;
79+
}
8180
const pendingThenable: PendingThenable<mixed> = (thenable: any);
8281
pendingThenable.status = 'pending';
8382
pendingThenable.then(

packages/react-reconciler/src/ReactFiberWakeable.old.js

+6-7
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,6 @@ export function trackSuspendedWakeable(wakeable: Wakeable) {
6161
// If the thenable doesn't have a status, set it to "pending" and attach
6262
// a listener that will update its status and result when it resolves.
6363
switch (thenable.status) {
64-
case 'pending':
65-
// Since the status is already "pending", we can assume it will be updated
66-
// when it resolves, either by React or something in userspace.
67-
break;
6864
case 'fulfilled':
6965
case 'rejected':
7066
// A thenable that already resolved shouldn't have been thrown, so this is
@@ -75,9 +71,12 @@ export function trackSuspendedWakeable(wakeable: Wakeable) {
7571
suspendedThenable = null;
7672
break;
7773
default: {
78-
// TODO: Only instrument the thenable if the status if not defined. If
79-
// it's defined, but an unknown value, assume it's been instrumented by
80-
// some custom userspace implementation.
74+
if (typeof thenable.status === 'string') {
75+
// Only instrument the thenable if the status if not defined. If
76+
// it's defined, but an unknown value, assume it's been instrumented by
77+
// some custom userspace implementation. We treat it as "pending".
78+
break;
79+
}
8180
const pendingThenable: PendingThenable<mixed> = (thenable: any);
8281
pendingThenable.status = 'pending';
8382
pendingThenable.then(

packages/react-server/src/ReactFizzWakeable.js

+6-7
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,6 @@ export function trackSuspendedWakeable(wakeable: Wakeable) {
4444
// If the thenable doesn't have a status, set it to "pending" and attach
4545
// a listener that will update its status and result when it resolves.
4646
switch (thenable.status) {
47-
case 'pending':
48-
// Since the status is already "pending", we can assume it will be updated
49-
// when it resolves, either by React or something in userspace.
50-
break;
5147
case 'fulfilled':
5248
case 'rejected':
5349
// A thenable that already resolved shouldn't have been thrown, so this is
@@ -57,9 +53,12 @@ export function trackSuspendedWakeable(wakeable: Wakeable) {
5753
// TODO: Log a warning?
5854
break;
5955
default: {
60-
// TODO: Only instrument the thenable if the status if not defined. If
61-
// it's defined, but an unknown value, assume it's been instrumented by
62-
// some custom userspace implementation.
56+
if (typeof thenable.status === 'string') {
57+
// Only instrument the thenable if the status if not defined. If
58+
// it's defined, but an unknown value, assume it's been instrumented by
59+
// some custom userspace implementation. We treat it as "pending".
60+
break;
61+
}
6362
const pendingThenable: PendingThenable<mixed> = (thenable: any);
6463
pendingThenable.status = 'pending';
6564
pendingThenable.then(

packages/react-server/src/ReactFlightWakeable.js

+6-7
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,6 @@ export function trackSuspendedWakeable(wakeable: Wakeable) {
4444
// If the thenable doesn't have a status, set it to "pending" and attach
4545
// a listener that will update its status and result when it resolves.
4646
switch (thenable.status) {
47-
case 'pending':
48-
// Since the status is already "pending", we can assume it will be updated
49-
// when it resolves, either by React or something in userspace.
50-
break;
5147
case 'fulfilled':
5248
case 'rejected':
5349
// A thenable that already resolved shouldn't have been thrown, so this is
@@ -57,9 +53,12 @@ export function trackSuspendedWakeable(wakeable: Wakeable) {
5753
// TODO: Log a warning?
5854
break;
5955
default: {
60-
// TODO: Only instrument the thenable if the status if not defined. If
61-
// it's defined, but an unknown value, assume it's been instrumented by
62-
// some custom userspace implementation.
56+
if (typeof thenable.status === 'string') {
57+
// Only instrument the thenable if the status if not defined. If
58+
// it's defined, but an unknown value, assume it's been instrumented by
59+
// some custom userspace implementation. We treat it as "pending".
60+
break;
61+
}
6362
const pendingThenable: PendingThenable<mixed> = (thenable: any);
6463
pendingThenable.status = 'pending';
6564
pendingThenable.then(

0 commit comments

Comments
 (0)