From 60fbb7b1433c5b268a88e0f693f29e6541e1a831 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Wed, 14 Sep 2022 20:13:33 -0400 Subject: [PATCH] [Flight] Implement FlightClient in terms of Thenable/Promises instead of throwing Promises (#25260) * [Flight] Align Chunks with Thenable used with experimental_use Use the field names used by the Thenable data structure passed to use(). These are considered public in this model. This adds another field since we use a separate field name for "reason". * Implement Thenable Protocol on Chunks This doesn't just ping but resolves/rejects with the value. * Subclass Promises * Pass key through JSON parsing * Wait for preloadModules before resolving module chunks * Initialize lazy resolved values before reading the result * Block a model from initializing if its direct dependencies are pending If a module is blocked, then we can't complete initializing a model. However, we can still let it parse, and then fill in the missing pieces later. We need to block it from resolving until all dependencies have filled in which we can do with a ref count. * 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. --- .../react-client/src/ReactFlightClient.js | 407 +++++++++++++----- .../src/ReactFlightClientStream.js | 2 +- .../src/ReactFiberWakeable.new.js | 13 +- .../src/ReactFiberWakeable.old.js | 13 +- .../ReactFlightDOMRelayClientHostConfig.js | 8 +- .../ReactFlightClientWebpackBundlerConfig.js | 68 +-- .../ReactFlightNativeRelayClientHostConfig.js | 8 +- .../react-server/src/ReactFizzWakeable.js | 13 +- .../react-server/src/ReactFlightWakeable.js | 13 +- scripts/flow/react-relay-hooks.js | 4 +- 10 files changed, 384 insertions(+), 165 deletions(-) diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index bd78cd237640b..3ca33ff00e3d3 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -7,7 +7,7 @@ * @flow */ -import type {Wakeable} from 'shared/ReactTypes'; +import type {Thenable} from 'shared/ReactTypes'; import type {LazyComponent} from 'react/src/ReactLazy'; import type { @@ -37,63 +37,110 @@ export type JSONValue = | {+[key: string]: JSONValue} | $ReadOnlyArray; -const PENDING = 0; -const RESOLVED_MODEL = 1; -const RESOLVED_MODULE = 2; -const INITIALIZED = 3; -const ERRORED = 4; - -type PendingChunk = { - _status: 0, - _value: null | Array<() => mixed>, +const PENDING = 'pending'; +const BLOCKED = 'blocked'; +const RESOLVED_MODEL = 'resolved_model'; +const RESOLVED_MODULE = 'resolved_module'; +const INITIALIZED = 'fulfilled'; +const ERRORED = 'rejected'; + +type PendingChunk = { + status: 'pending', + value: null | Array<(T) => mixed>, + reason: null | Array<(mixed) => mixed>, + _response: Response, + then(resolve: (T) => mixed, reject: (mixed) => mixed): void, +}; +type BlockedChunk = { + status: 'blocked', + value: null | Array<(T) => mixed>, + reason: null | Array<(mixed) => mixed>, _response: Response, - then(resolve: () => mixed): void, + then(resolve: (T) => mixed, reject: (mixed) => mixed): void, }; -type ResolvedModelChunk = { - _status: 1, - _value: UninitializedModel, +type ResolvedModelChunk = { + status: 'resolved_model', + value: UninitializedModel, + reason: null, _response: Response, - then(resolve: () => mixed): void, + then(resolve: (T) => mixed, reject: (mixed) => mixed): void, }; type ResolvedModuleChunk = { - _status: 2, - _value: ModuleReference, + status: 'resolved_module', + value: ModuleReference, + reason: null, _response: Response, - then(resolve: () => mixed): void, + then(resolve: (T) => mixed, reject: (mixed) => mixed): void, }; type InitializedChunk = { - _status: 3, - _value: T, + status: 'fulfilled', + value: T, + reason: null, _response: Response, - then(resolve: () => mixed): void, + then(resolve: (T) => mixed, reject: (mixed) => mixed): void, }; -type ErroredChunk = { - _status: 4, - _value: Error, +type ErroredChunk = { + status: 'rejected', + value: null, + reason: mixed, _response: Response, - then(resolve: () => mixed): void, + then(resolve: (T) => mixed, reject: (mixed) => mixed): void, }; type SomeChunk = - | PendingChunk - | ResolvedModelChunk + | PendingChunk + | BlockedChunk + | ResolvedModelChunk | ResolvedModuleChunk | InitializedChunk - | ErroredChunk; + | ErroredChunk; -function Chunk(status: any, value: any, response: Response) { - this._status = status; - this._value = value; +function Chunk(status: any, value: any, reason: any, response: Response) { + this.status = status; + this.value = value; + this.reason = reason; this._response = response; } -Chunk.prototype.then = function(resolve: () => mixed) { +// We subclass Promise.prototype so that we get other methods like .catch +Chunk.prototype = (Object.create(Promise.prototype): any); +// TODO: This doesn't return a new Promise chain unlike the real .then +Chunk.prototype.then = function( + resolve: (value: T) => mixed, + reject: (reason: mixed) => mixed, +) { const chunk: SomeChunk = this; - if (chunk._status === PENDING) { - if (chunk._value === null) { - chunk._value = []; - } - chunk._value.push(resolve); - } else { - resolve(); + // If we have resolved content, we try to initialize it first which + // might put us back into one of the other states. + switch (chunk.status) { + case RESOLVED_MODEL: + initializeModelChunk(chunk); + break; + case RESOLVED_MODULE: + initializeModuleChunk(chunk); + break; + } + // The status might have changed after initialization. + switch (chunk.status) { + case INITIALIZED: + resolve(chunk.value); + break; + case PENDING: + case BLOCKED: + if (resolve) { + if (chunk.value === null) { + chunk.value = []; + } + chunk.value.push(resolve); + } + if (reject) { + if (chunk.reason === null) { + chunk.reason = []; + } + chunk.reason.push(reject); + } + break; + default: + reject(chunk.reason); + break; } }; @@ -107,18 +154,26 @@ export type ResponseBase = { export type {Response}; function readChunk(chunk: SomeChunk): T { - switch (chunk._status) { - case INITIALIZED: - return chunk._value; + // If we have resolved content, we try to initialize it first which + // might put us back into one of the other states. + switch (chunk.status) { case RESOLVED_MODEL: - return initializeModelChunk(chunk); + initializeModelChunk(chunk); + break; case RESOLVED_MODULE: - return initializeModuleChunk(chunk); + initializeModuleChunk(chunk); + break; + } + // The status might have changed after initialization. + switch (chunk.status) { + case INITIALIZED: + return chunk.value; case PENDING: + case BLOCKED: // eslint-disable-next-line no-throw-literal - throw (chunk: Wakeable); + throw ((chunk: any): Thenable); default: - throw chunk._value; + throw chunk.reason; } } @@ -128,14 +183,22 @@ function readRoot(): T { return readChunk(chunk); } -function createPendingChunk(response: Response): PendingChunk { +function createPendingChunk(response: Response): PendingChunk { + // $FlowFixMe Flow doesn't support functions as constructors + return new Chunk(PENDING, null, null, response); +} + +function createBlockedChunk(response: Response): BlockedChunk { // $FlowFixMe Flow doesn't support functions as constructors - return new Chunk(PENDING, null, response); + return new Chunk(BLOCKED, null, null, response); } -function createErrorChunk(response: Response, error: Error): ErroredChunk { +function createErrorChunk( + response: Response, + error: Error, +): ErroredChunk { // $FlowFixMe Flow doesn't support functions as constructors - return new Chunk(ERRORED, error, response); + return new Chunk(ERRORED, null, error, response); } function createInitializedChunk( @@ -143,36 +206,58 @@ function createInitializedChunk( value: T, ): InitializedChunk { // $FlowFixMe Flow doesn't support functions as constructors - return new Chunk(INITIALIZED, value, response); + return new Chunk(INITIALIZED, value, null, response); } -function wakeChunk(listeners: null | Array<() => mixed>) { - if (listeners !== null) { - for (let i = 0; i < listeners.length; i++) { - const listener = listeners[i]; - listener(); - } +function wakeChunk(listeners: Array<(T) => mixed>, value: T): void { + for (let i = 0; i < listeners.length; i++) { + const listener = listeners[i]; + listener(value); } } -function triggerErrorOnChunk(chunk: SomeChunk, error: Error): void { - if (chunk._status !== PENDING) { +function wakeChunkIfInitialized( + chunk: SomeChunk, + resolveListeners: Array<(T) => mixed>, + rejectListeners: null | Array<(mixed) => mixed>, +): void { + switch (chunk.status) { + case INITIALIZED: + wakeChunk(resolveListeners, chunk.value); + break; + case PENDING: + case BLOCKED: + chunk.value = resolveListeners; + chunk.reason = rejectListeners; + break; + case ERRORED: + if (rejectListeners) { + wakeChunk(rejectListeners, chunk.reason); + } + break; + } +} + +function triggerErrorOnChunk(chunk: SomeChunk, error: mixed): void { + if (chunk.status !== PENDING && chunk.status !== BLOCKED) { // We already resolved. We didn't expect to see this. return; } - const listeners = chunk._value; - const erroredChunk: ErroredChunk = (chunk: any); - erroredChunk._status = ERRORED; - erroredChunk._value = error; - wakeChunk(listeners); + const listeners = chunk.reason; + const erroredChunk: ErroredChunk = (chunk: any); + erroredChunk.status = ERRORED; + erroredChunk.reason = error; + if (listeners !== null) { + wakeChunk(listeners, error); + } } -function createResolvedModelChunk( +function createResolvedModelChunk( response: Response, value: UninitializedModel, -): ResolvedModelChunk { +): ResolvedModelChunk { // $FlowFixMe Flow doesn't support functions as constructors - return new Chunk(RESOLVED_MODEL, value, response); + return new Chunk(RESOLVED_MODEL, value, null, response); } function createResolvedModuleChunk( @@ -180,53 +265,97 @@ function createResolvedModuleChunk( value: ModuleReference, ): ResolvedModuleChunk { // $FlowFixMe Flow doesn't support functions as constructors - return new Chunk(RESOLVED_MODULE, value, response); + return new Chunk(RESOLVED_MODULE, value, null, response); } function resolveModelChunk( chunk: SomeChunk, value: UninitializedModel, ): void { - if (chunk._status !== PENDING) { + if (chunk.status !== PENDING) { // We already resolved. We didn't expect to see this. return; } - const listeners = chunk._value; - const resolvedChunk: ResolvedModelChunk = (chunk: any); - resolvedChunk._status = RESOLVED_MODEL; - resolvedChunk._value = value; - wakeChunk(listeners); + const resolveListeners = chunk.value; + const rejectListeners = chunk.reason; + const resolvedChunk: ResolvedModelChunk = (chunk: any); + resolvedChunk.status = RESOLVED_MODEL; + resolvedChunk.value = value; + if (resolveListeners !== null) { + // This is unfortunate that we're reading this eagerly if + // we already have listeners attached since they might no + // longer be rendered or might not be the highest pri. + initializeModelChunk(resolvedChunk); + // The status might have changed after initialization. + wakeChunkIfInitialized(chunk, resolveListeners, rejectListeners); + } } function resolveModuleChunk( chunk: SomeChunk, value: ModuleReference, ): void { - if (chunk._status !== PENDING) { + if (chunk.status !== PENDING && chunk.status !== BLOCKED) { // We already resolved. We didn't expect to see this. return; } - const listeners = chunk._value; + const resolveListeners = chunk.value; + const rejectListeners = chunk.reason; const resolvedChunk: ResolvedModuleChunk = (chunk: any); - resolvedChunk._status = RESOLVED_MODULE; - resolvedChunk._value = value; - wakeChunk(listeners); + resolvedChunk.status = RESOLVED_MODULE; + resolvedChunk.value = value; + if (resolveListeners !== null) { + initializeModuleChunk(resolvedChunk); + wakeChunkIfInitialized(chunk, resolveListeners, rejectListeners); + } } -function initializeModelChunk(chunk: ResolvedModelChunk): T { - const value: T = parseModel(chunk._response, chunk._value); - const initializedChunk: InitializedChunk = (chunk: any); - initializedChunk._status = INITIALIZED; - initializedChunk._value = value; - return value; +let initializingChunk: ResolvedModelChunk = (null: any); +let initializingChunkBlockedModel: null | {deps: number, value: any} = null; +function initializeModelChunk(chunk: ResolvedModelChunk): void { + const prevChunk = initializingChunk; + const prevBlocked = initializingChunkBlockedModel; + initializingChunk = chunk; + initializingChunkBlockedModel = null; + try { + const value: T = parseModel(chunk._response, chunk.value); + if ( + initializingChunkBlockedModel !== null && + initializingChunkBlockedModel.deps > 0 + ) { + initializingChunkBlockedModel.value = value; + // We discovered new dependencies on modules that are not yet resolved. + // We have to go the BLOCKED state until they're resolved. + const blockedChunk: BlockedChunk = (chunk: any); + blockedChunk.status = BLOCKED; + blockedChunk.value = null; + blockedChunk.reason = null; + } else { + const initializedChunk: InitializedChunk = (chunk: any); + initializedChunk.status = INITIALIZED; + initializedChunk.value = value; + } + } catch (error) { + const erroredChunk: ErroredChunk = (chunk: any); + erroredChunk.status = ERRORED; + erroredChunk.reason = error; + } finally { + initializingChunk = prevChunk; + initializingChunkBlockedModel = prevBlocked; + } } -function initializeModuleChunk(chunk: ResolvedModuleChunk): T { - const value: T = requireModule(chunk._value); - const initializedChunk: InitializedChunk = (chunk: any); - initializedChunk._status = INITIALIZED; - initializedChunk._value = value; - return value; +function initializeModuleChunk(chunk: ResolvedModuleChunk): void { + try { + const value: T = requireModule(chunk.value); + const initializedChunk: InitializedChunk = (chunk: any); + initializedChunk.status = INITIALIZED; + initializedChunk.value = value; + } catch (error) { + const erroredChunk: ErroredChunk = (chunk: any); + erroredChunk.status = ERRORED; + erroredChunk.reason = error; + } } // Report that any missing chunks in the model is now going to throw this @@ -236,7 +365,9 @@ export function reportGlobalError(response: Response, error: Error): void { // If this chunk was already resolved or errored, it won't // trigger an error but if it wasn't then we need to // because we won't be getting any new data to resolve it. - triggerErrorOnChunk(chunk, error); + if (chunk.status === PENDING) { + triggerErrorOnChunk(chunk, error); + } }); } @@ -302,9 +433,47 @@ function getChunk(response: Response, id: number): SomeChunk { return chunk; } +function createModelResolver( + chunk: SomeChunk, + parentObject: Object, + key: string, +) { + let blocked; + if (initializingChunkBlockedModel) { + blocked = initializingChunkBlockedModel; + blocked.deps++; + } else { + blocked = initializingChunkBlockedModel = { + deps: 1, + value: null, + }; + } + return value => { + parentObject[key] = value; + blocked.deps--; + if (blocked.deps === 0) { + if (chunk.status !== BLOCKED) { + return; + } + const resolveListeners = chunk.value; + const initializedChunk: InitializedChunk = (chunk: any); + initializedChunk.status = INITIALIZED; + initializedChunk.value = blocked.value; + if (resolveListeners !== null) { + wakeChunk(resolveListeners, blocked.value); + } + } + }; +} + +function createModelReject(chunk: SomeChunk) { + return error => triggerErrorOnChunk(chunk, error); +} + export function parseModelString( response: Response, parentObject: Object, + key: string, value: string, ): any { switch (value[0]) { @@ -317,7 +486,29 @@ export function parseModelString( } else { const id = parseInt(value.substring(1), 16); const chunk = getChunk(response, id); - return readChunk(chunk); + switch (chunk.status) { + case RESOLVED_MODEL: + initializeModelChunk(chunk); + break; + case RESOLVED_MODULE: + initializeModuleChunk(chunk); + break; + } + // The status might have changed after initialization. + switch (chunk.status) { + case INITIALIZED: + return chunk.value; + case PENDING: + case BLOCKED: + const parentChunk = initializingChunk; + chunk.then( + createModelResolver(parentChunk, parentObject, key), + createModelReject(parentChunk), + ); + return null; + default: + throw chunk.reason; + } } } case '@': { @@ -400,12 +591,32 @@ export function resolveModule( // TODO: Add an option to encode modules that are lazy loaded. // For now we preload all modules as early as possible since it's likely // that we'll need them. - preloadModule(moduleReference); - - if (!chunk) { - chunks.set(id, createResolvedModuleChunk(response, moduleReference)); + const promise = preloadModule(moduleReference); + if (promise) { + let blockedChunk: BlockedChunk; + if (!chunk) { + // Technically, we should just treat promise as the chunk in this + // case. Because it'll just behave as any other promise. + blockedChunk = createBlockedChunk(response); + chunks.set(id, blockedChunk); + } else { + // This can't actually happen because we don't have any forward + // references to modules. + blockedChunk = (chunk: any); + blockedChunk.status = BLOCKED; + } + promise.then( + () => resolveModuleChunk(blockedChunk, moduleReference), + error => triggerErrorOnChunk(blockedChunk, error), + ); } else { - resolveModuleChunk(chunk, moduleReference); + if (!chunk) { + chunks.set(id, createResolvedModuleChunk(response, moduleReference)); + } else { + // This can't actually happen because we don't have any forward + // references to modules. + resolveModuleChunk(chunk, moduleReference); + } } } diff --git a/packages/react-client/src/ReactFlightClientStream.js b/packages/react-client/src/ReactFlightClientStream.js index ed27a10f6e339..d832927a01737 100644 --- a/packages/react-client/src/ReactFlightClientStream.js +++ b/packages/react-client/src/ReactFlightClientStream.js @@ -114,7 +114,7 @@ function createFromJSONCallback(response: Response) { return function(key: string, value: JSONValue) { if (typeof value === 'string') { // We can't use .bind here because we need the "this" value. - return parseModelString(response, this, value); + return parseModelString(response, this, key, value); } if (typeof value === 'object' && value !== null) { return parseModelTuple(response, value); diff --git a/packages/react-reconciler/src/ReactFiberWakeable.new.js b/packages/react-reconciler/src/ReactFiberWakeable.new.js index 11f470cfab888..fad1d49f4474e 100644 --- a/packages/react-reconciler/src/ReactFiberWakeable.new.js +++ b/packages/react-reconciler/src/ReactFiberWakeable.new.js @@ -61,10 +61,6 @@ export function trackSuspendedWakeable(wakeable: Wakeable) { // If the thenable doesn't have a status, set it to "pending" and attach // a listener that will update its status and result when it resolves. switch (thenable.status) { - case 'pending': - // Since the status is already "pending", we can assume it will be updated - // when it resolves, either by React or something in userspace. - break; case 'fulfilled': case 'rejected': // A thenable that already resolved shouldn't have been thrown, so this is @@ -75,9 +71,12 @@ export function trackSuspendedWakeable(wakeable: Wakeable) { suspendedThenable = null; break; default: { - // TODO: 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. + 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 = (thenable: any); pendingThenable.status = 'pending'; pendingThenable.then( diff --git a/packages/react-reconciler/src/ReactFiberWakeable.old.js b/packages/react-reconciler/src/ReactFiberWakeable.old.js index 11f470cfab888..fad1d49f4474e 100644 --- a/packages/react-reconciler/src/ReactFiberWakeable.old.js +++ b/packages/react-reconciler/src/ReactFiberWakeable.old.js @@ -61,10 +61,6 @@ export function trackSuspendedWakeable(wakeable: Wakeable) { // If the thenable doesn't have a status, set it to "pending" and attach // a listener that will update its status and result when it resolves. switch (thenable.status) { - case 'pending': - // Since the status is already "pending", we can assume it will be updated - // when it resolves, either by React or something in userspace. - break; case 'fulfilled': case 'rejected': // A thenable that already resolved shouldn't have been thrown, so this is @@ -75,9 +71,12 @@ export function trackSuspendedWakeable(wakeable: Wakeable) { suspendedThenable = null; break; default: { - // TODO: 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. + 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 = (thenable: any); pendingThenable.status = 'pending'; pendingThenable.then( diff --git a/packages/react-server-dom-relay/src/ReactFlightDOMRelayClientHostConfig.js b/packages/react-server-dom-relay/src/ReactFlightDOMRelayClientHostConfig.js index 308bbef0ed234..3d9f5f7bb8f92 100644 --- a/packages/react-server-dom-relay/src/ReactFlightDOMRelayClientHostConfig.js +++ b/packages/react-server-dom-relay/src/ReactFlightDOMRelayClientHostConfig.js @@ -44,9 +44,9 @@ export function resolveModuleReference( return resolveModuleReferenceImpl(moduleData); } -function parseModelRecursively(response: Response, parentObj, value) { +function parseModelRecursively(response: Response, parentObj, key, value) { if (typeof value === 'string') { - return parseModelString(response, parentObj, value); + return parseModelString(response, parentObj, key, value); } if (typeof value === 'object' && value !== null) { if (isArray(value)) { @@ -55,6 +55,7 @@ function parseModelRecursively(response: Response, parentObj, value) { (parsedValue: any)[i] = parseModelRecursively( response, value, + '' + i, value[i], ); } @@ -65,6 +66,7 @@ function parseModelRecursively(response: Response, parentObj, value) { (parsedValue: any)[innerKey] = parseModelRecursively( response, value, + innerKey, value[innerKey], ); } @@ -77,5 +79,5 @@ function parseModelRecursively(response: Response, parentObj, value) { const dummy = {}; export function parseModel(response: Response, json: UninitializedModel): T { - return (parseModelRecursively(response, dummy, json): any); + return (parseModelRecursively(response, dummy, '', json): any); } diff --git a/packages/react-server-dom-webpack/src/ReactFlightClientWebpackBundlerConfig.js b/packages/react-server-dom-webpack/src/ReactFlightClientWebpackBundlerConfig.js index 288f63bfdc92c..5c47c7736d061 100644 --- a/packages/react-server-dom-webpack/src/ReactFlightClientWebpackBundlerConfig.js +++ b/packages/react-server-dom-webpack/src/ReactFlightClientWebpackBundlerConfig.js @@ -7,7 +7,11 @@ * @flow */ -import type {Thenable} from 'shared/ReactTypes'; +import type { + Thenable, + FulfilledThenable, + RejectedThenable, +} from 'shared/ReactTypes'; export type WebpackSSRMap = { [clientId: string]: { @@ -56,7 +60,9 @@ const asyncModuleCache: Map> = new Map(); // Start preloading the modules since we might need them soon. // This function doesn't suspend. -export function preloadModule(moduleData: ModuleReference): void { +export function preloadModule( + moduleData: ModuleReference, +): null | Thenable { const chunks = moduleData.chunks; const promises = []; for (let i = 0; i < chunks.length; i++) { @@ -72,20 +78,35 @@ export function preloadModule(moduleData: ModuleReference): void { } } if (moduleData.async) { - const modulePromise: any = Promise.all(promises).then(() => { - return __webpack_require__(moduleData.id); - }); - modulePromise.then( - value => { - modulePromise.status = 'fulfilled'; - modulePromise.value = value; - }, - reason => { - modulePromise.status = 'rejected'; - modulePromise.reason = reason; - }, - ); - asyncModuleCache.set(moduleData.id, modulePromise); + const existingPromise = asyncModuleCache.get(moduleData.id); + if (existingPromise) { + if (existingPromise.status === 'fulfilled') { + return null; + } + return existingPromise; + } else { + const modulePromise: Thenable = Promise.all(promises).then(() => { + return __webpack_require__(moduleData.id); + }); + modulePromise.then( + value => { + const fulfilledThenable: FulfilledThenable = (modulePromise: any); + fulfilledThenable.status = 'fulfilled'; + fulfilledThenable.value = value; + }, + reason => { + const rejectedThenable: RejectedThenable = (modulePromise: any); + rejectedThenable.status = 'rejected'; + rejectedThenable.reason = reason; + }, + ); + asyncModuleCache.set(moduleData.id, modulePromise); + return modulePromise; + } + } else if (promises.length > 0) { + return Promise.all(promises); + } else { + return null; } } @@ -99,23 +120,10 @@ export function requireModule(moduleData: ModuleReference): T { const promise: any = asyncModuleCache.get(moduleData.id); if (promise.status === 'fulfilled') { moduleExports = promise.value; - } else if (promise.status === 'rejected') { - throw promise.reason; } else { - throw promise; + throw promise.reason; } } else { - const chunks = moduleData.chunks; - for (let i = 0; i < chunks.length; i++) { - const chunkId = chunks[i]; - const entry = chunkCache.get(chunkId); - if (entry !== null) { - // We assume that preloadModule has been called before. - // So we don't expect to see entry being undefined here, that's an error. - // Let's throw either an error or the Promise. - throw entry; - } - } moduleExports = __webpack_require__(moduleData.id); } if (moduleData.name === '*') { diff --git a/packages/react-server-native-relay/src/ReactFlightNativeRelayClientHostConfig.js b/packages/react-server-native-relay/src/ReactFlightNativeRelayClientHostConfig.js index 83ab8800d4599..aaa81c08726fe 100644 --- a/packages/react-server-native-relay/src/ReactFlightNativeRelayClientHostConfig.js +++ b/packages/react-server-native-relay/src/ReactFlightNativeRelayClientHostConfig.js @@ -44,9 +44,9 @@ export function resolveModuleReference( return resolveModuleReferenceImpl(moduleData); } -function parseModelRecursively(response: Response, parentObj, value) { +function parseModelRecursively(response: Response, parentObj, key, value) { if (typeof value === 'string') { - return parseModelString(response, parentObj, value); + return parseModelString(response, parentObj, key, value); } if (typeof value === 'object' && value !== null) { if (isArray(value)) { @@ -55,6 +55,7 @@ function parseModelRecursively(response: Response, parentObj, value) { (parsedValue: any)[i] = parseModelRecursively( response, value, + '' + i, value[i], ); } @@ -65,6 +66,7 @@ function parseModelRecursively(response: Response, parentObj, value) { (parsedValue: any)[innerKey] = parseModelRecursively( response, value, + innerKey, value[innerKey], ); } @@ -77,5 +79,5 @@ function parseModelRecursively(response: Response, parentObj, value) { const dummy = {}; export function parseModel(response: Response, json: UninitializedModel): T { - return (parseModelRecursively(response, dummy, json): any); + return (parseModelRecursively(response, dummy, '', json): any); } diff --git a/packages/react-server/src/ReactFizzWakeable.js b/packages/react-server/src/ReactFizzWakeable.js index 5a42ed396c4d9..aea0b507c8ca7 100644 --- a/packages/react-server/src/ReactFizzWakeable.js +++ b/packages/react-server/src/ReactFizzWakeable.js @@ -44,10 +44,6 @@ export function trackSuspendedWakeable(wakeable: Wakeable) { // If the thenable doesn't have a status, set it to "pending" and attach // a listener that will update its status and result when it resolves. switch (thenable.status) { - case 'pending': - // Since the status is already "pending", we can assume it will be updated - // when it resolves, either by React or something in userspace. - break; case 'fulfilled': case 'rejected': // A thenable that already resolved shouldn't have been thrown, so this is @@ -57,9 +53,12 @@ export function trackSuspendedWakeable(wakeable: Wakeable) { // TODO: Log a warning? break; default: { - // TODO: 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. + 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 = (thenable: any); pendingThenable.status = 'pending'; pendingThenable.then( diff --git a/packages/react-server/src/ReactFlightWakeable.js b/packages/react-server/src/ReactFlightWakeable.js index c3eda3c16aee6..b1ff2aa5eb3ef 100644 --- a/packages/react-server/src/ReactFlightWakeable.js +++ b/packages/react-server/src/ReactFlightWakeable.js @@ -44,10 +44,6 @@ export function trackSuspendedWakeable(wakeable: Wakeable) { // If the thenable doesn't have a status, set it to "pending" and attach // a listener that will update its status and result when it resolves. switch (thenable.status) { - case 'pending': - // Since the status is already "pending", we can assume it will be updated - // when it resolves, either by React or something in userspace. - break; case 'fulfilled': case 'rejected': // A thenable that already resolved shouldn't have been thrown, so this is @@ -57,9 +53,12 @@ export function trackSuspendedWakeable(wakeable: Wakeable) { // TODO: Log a warning? break; default: { - // TODO: 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. + 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 = (thenable: any); pendingThenable.status = 'pending'; pendingThenable.then( diff --git a/scripts/flow/react-relay-hooks.js b/scripts/flow/react-relay-hooks.js index 9f452689976af..24dc92d5c7225 100644 --- a/scripts/flow/react-relay-hooks.js +++ b/scripts/flow/react-relay-hooks.js @@ -62,7 +62,7 @@ declare module 'ReactFlightDOMRelayClientIntegration' { ): JSResourceReference; declare export function preloadModule( moduleReference: JSResourceReference, - ): void; + ): null | Promise; declare export function requireModule( moduleReference: JSResourceReference, ): T; @@ -95,7 +95,7 @@ declare module 'ReactFlightNativeRelayClientIntegration' { ): JSResourceReference; declare export function preloadModule( moduleReference: JSResourceReference, - ): void; + ): null | Promise; declare export function requireModule( moduleReference: JSResourceReference, ): T;