Skip to content

Commit 0ae7893

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 0ae7893

File tree

3 files changed

+55
-22
lines changed

3 files changed

+55
-22
lines changed

packages/react-client/src/ReactFlightClient.js

Lines changed: 43 additions & 16 deletions
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

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,12 @@ export function trackSuspendedWakeable(wakeable: Wakeable) {
7575
suspendedThenable = null;
7676
break;
7777
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.
78+
if (typeof thenable.status === 'string') {
79+
// Only instrument the thenable if the status if not defined. If
80+
// it's defined, but an unknown value, assume it's been instrumented by
81+
// some custom userspace implementation.
82+
break;
83+
}
8184
const pendingThenable: PendingThenable<mixed> = (thenable: any);
8285
pendingThenable.status = 'pending';
8386
pendingThenable.then(

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

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,12 @@ export function trackSuspendedWakeable(wakeable: Wakeable) {
7575
suspendedThenable = null;
7676
break;
7777
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.
78+
if (typeof thenable.status === 'string') {
79+
// Only instrument the thenable if the status if not defined. If
80+
// it's defined, but an unknown value, assume it's been instrumented by
81+
// some custom userspace implementation.
82+
break;
83+
}
8184
const pendingThenable: PendingThenable<mixed> = (thenable: any);
8285
pendingThenable.status = 'pending';
8386
pendingThenable.then(

0 commit comments

Comments
 (0)