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;