[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.
This commit is contained in:
Sebastian Markbåge 2022-09-14 20:13:33 -04:00 committed by GitHub
parent c91a1e03be
commit 60fbb7b143
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 383 additions and 164 deletions

View File

@ -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<JSONValue>;
const PENDING = 0;
const RESOLVED_MODEL = 1;
const RESOLVED_MODULE = 2;
const INITIALIZED = 3;
const ERRORED = 4;
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: 0,
_value: null | Array<() => mixed>,
type PendingChunk<T> = {
status: 'pending',
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 BlockedChunk<T> = {
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<T> = {
status: 'resolved_model',
value: UninitializedModel,
reason: null,
_response: Response,
then(resolve: (T) => mixed, reject: (mixed) => mixed): void,
};
type ResolvedModuleChunk<T> = {
_status: 2,
_value: ModuleReference<T>,
status: 'resolved_module',
value: ModuleReference<T>,
reason: null,
_response: Response,
then(resolve: () => mixed): void,
then(resolve: (T) => mixed, reject: (mixed) => mixed): void,
};
type InitializedChunk<T> = {
_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<T> = {
status: 'rejected',
value: null,
reason: mixed,
_response: Response,
then(resolve: () => mixed): void,
then(resolve: (T) => mixed, reject: (mixed) => mixed): void,
};
type SomeChunk<T> =
| PendingChunk
| ResolvedModelChunk
| PendingChunk<T>
| BlockedChunk<T>
| ResolvedModelChunk<T>
| ResolvedModuleChunk<T>
| InitializedChunk<T>
| ErroredChunk;
| ErroredChunk<T>;
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<T>(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<T>(
resolve: (value: T) => mixed,
reject: (reason: mixed) => mixed,
) {
const chunk: SomeChunk<T> = 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<T>(chunk: SomeChunk<T>): 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<T>);
default:
throw chunk._value;
throw chunk.reason;
}
}
@ -128,14 +183,22 @@ function readRoot<T>(): T {
return readChunk(chunk);
}
function createPendingChunk(response: Response): PendingChunk {
function createPendingChunk<T>(response: Response): PendingChunk<T> {
// $FlowFixMe Flow doesn't support functions as constructors
return new Chunk(PENDING, null, response);
return new Chunk(PENDING, null, null, response);
}
function createErrorChunk(response: Response, error: Error): ErroredChunk {
function createBlockedChunk<T>(response: Response): BlockedChunk<T> {
// $FlowFixMe Flow doesn't support functions as constructors
return new Chunk(ERRORED, error, response);
return new Chunk(BLOCKED, null, null, response);
}
function createErrorChunk<T>(
response: Response,
error: Error,
): ErroredChunk<T> {
// $FlowFixMe Flow doesn't support functions as constructors
return new Chunk(ERRORED, null, error, response);
}
function createInitializedChunk<T>(
@ -143,36 +206,58 @@ function createInitializedChunk<T>(
value: T,
): InitializedChunk<T> {
// $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<T>(listeners: Array<(T) => mixed>, value: T): void {
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i];
listener(value);
}
}
function triggerErrorOnChunk<T>(chunk: SomeChunk<T>, error: Error): void {
if (chunk._status !== PENDING) {
function wakeChunkIfInitialized<T>(
chunk: SomeChunk<T>,
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<T>(chunk: SomeChunk<T>, 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<T> = (chunk: any);
erroredChunk.status = ERRORED;
erroredChunk.reason = error;
if (listeners !== null) {
wakeChunk(listeners, error);
}
}
function createResolvedModelChunk(
function createResolvedModelChunk<T>(
response: Response,
value: UninitializedModel,
): ResolvedModelChunk {
): ResolvedModelChunk<T> {
// $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<T>(
@ -180,53 +265,97 @@ function createResolvedModuleChunk<T>(
value: ModuleReference<T>,
): ResolvedModuleChunk<T> {
// $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<T>(
chunk: SomeChunk<T>,
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<T> = (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<T>(
chunk: SomeChunk<T>,
value: ModuleReference<T>,
): 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<T> = (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<T>(chunk: ResolvedModelChunk): T {
const value: T = parseModel(chunk._response, chunk._value);
const initializedChunk: InitializedChunk<T> = (chunk: any);
initializedChunk._status = INITIALIZED;
initializedChunk._value = value;
return value;
let initializingChunk: ResolvedModelChunk<any> = (null: any);
let initializingChunkBlockedModel: null | {deps: number, value: any} = null;
function initializeModelChunk<T>(chunk: ResolvedModelChunk<T>): 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<T> = (chunk: any);
blockedChunk.status = BLOCKED;
blockedChunk.value = null;
blockedChunk.reason = null;
} else {
const initializedChunk: InitializedChunk<T> = (chunk: any);
initializedChunk.status = INITIALIZED;
initializedChunk.value = value;
}
} catch (error) {
const erroredChunk: ErroredChunk<T> = (chunk: any);
erroredChunk.status = ERRORED;
erroredChunk.reason = error;
} finally {
initializingChunk = prevChunk;
initializingChunkBlockedModel = prevBlocked;
}
}
function initializeModuleChunk<T>(chunk: ResolvedModuleChunk<T>): T {
const value: T = requireModule(chunk._value);
const initializedChunk: InitializedChunk<T> = (chunk: any);
initializedChunk._status = INITIALIZED;
initializedChunk._value = value;
return value;
function initializeModuleChunk<T>(chunk: ResolvedModuleChunk<T>): void {
try {
const value: T = requireModule(chunk.value);
const initializedChunk: InitializedChunk<T> = (chunk: any);
initializedChunk.status = INITIALIZED;
initializedChunk.value = value;
} catch (error) {
const erroredChunk: ErroredChunk<T> = (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<any> {
return chunk;
}
function createModelResolver<T>(
chunk: SomeChunk<T>,
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<T> = (chunk: any);
initializedChunk.status = INITIALIZED;
initializedChunk.value = blocked.value;
if (resolveListeners !== null) {
wakeChunk(resolveListeners, blocked.value);
}
}
};
}
function createModelReject<T>(chunk: SomeChunk<T>) {
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<any>;
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);
}
}
}

View File

@ -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);

View File

@ -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<mixed> = (thenable: any);
pendingThenable.status = 'pending';
pendingThenable.then(

View File

@ -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<mixed> = (thenable: any);
pendingThenable.status = 'pending';
pendingThenable.then(

View File

@ -44,9 +44,9 @@ export function resolveModuleReference<T>(
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<T>(response: Response, json: UninitializedModel): T {
return (parseModelRecursively(response, dummy, json): any);
return (parseModelRecursively(response, dummy, '', json): any);
}

View File

@ -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<string, Thenable<any>> = new Map();
// Start preloading the modules since we might need them soon.
// This function doesn't suspend.
export function preloadModule<T>(moduleData: ModuleReference<T>): void {
export function preloadModule<T>(
moduleData: ModuleReference<T>,
): null | Thenable<any> {
const chunks = moduleData.chunks;
const promises = [];
for (let i = 0; i < chunks.length; i++) {
@ -72,20 +78,35 @@ export function preloadModule<T>(moduleData: ModuleReference<T>): 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<T> = Promise.all(promises).then(() => {
return __webpack_require__(moduleData.id);
});
modulePromise.then(
value => {
const fulfilledThenable: FulfilledThenable<mixed> = (modulePromise: any);
fulfilledThenable.status = 'fulfilled';
fulfilledThenable.value = value;
},
reason => {
const rejectedThenable: RejectedThenable<mixed> = (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<T>(moduleData: ModuleReference<T>): 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 === '*') {

View File

@ -44,9 +44,9 @@ export function resolveModuleReference<T>(
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<T>(response: Response, json: UninitializedModel): T {
return (parseModelRecursively(response, dummy, json): any);
return (parseModelRecursively(response, dummy, '', json): any);
}

View File

@ -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<mixed> = (thenable: any);
pendingThenable.status = 'pending';
pendingThenable.then(

View File

@ -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<mixed> = (thenable: any);
pendingThenable.status = 'pending';
pendingThenable.then(

View File

@ -62,7 +62,7 @@ declare module 'ReactFlightDOMRelayClientIntegration' {
): JSResourceReference<T>;
declare export function preloadModule<T>(
moduleReference: JSResourceReference<T>,
): void;
): null | Promise<void>;
declare export function requireModule<T>(
moduleReference: JSResourceReference<T>,
): T;
@ -95,7 +95,7 @@ declare module 'ReactFlightNativeRelayClientIntegration' {
): JSResourceReference<T>;
declare export function preloadModule<T>(
moduleReference: JSResourceReference<T>,
): void;
): null | Promise<void>;
declare export function requireModule<T>(
moduleReference: JSResourceReference<T>,
): T;