131 lines
3.6 KiB
JavaScript
131 lines
3.6 KiB
JavaScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @flow
|
|
*/
|
|
|
|
import type {Wakeable} from 'shared/ReactTypes';
|
|
import type {Lane} from './ReactFiberLane';
|
|
import {requestTransitionLane} from './ReactFiberRootScheduler';
|
|
|
|
interface AsyncActionImpl {
|
|
lane: Lane;
|
|
listeners: Array<(false) => mixed>;
|
|
count: number;
|
|
then(
|
|
onFulfill: (value: boolean) => mixed,
|
|
onReject: (error: mixed) => mixed,
|
|
): void;
|
|
}
|
|
|
|
interface PendingAsyncAction extends AsyncActionImpl {
|
|
status: 'pending';
|
|
}
|
|
|
|
interface FulfilledAsyncAction extends AsyncActionImpl {
|
|
status: 'fulfilled';
|
|
value: boolean;
|
|
}
|
|
|
|
interface RejectedAsyncAction extends AsyncActionImpl {
|
|
status: 'rejected';
|
|
reason: mixed;
|
|
}
|
|
|
|
type AsyncAction =
|
|
| PendingAsyncAction
|
|
| FulfilledAsyncAction
|
|
| RejectedAsyncAction;
|
|
|
|
let currentAsyncAction: AsyncAction | null = null;
|
|
|
|
export function requestAsyncActionContext(
|
|
actionReturnValue: mixed,
|
|
): AsyncAction | false {
|
|
if (
|
|
actionReturnValue !== null &&
|
|
typeof actionReturnValue === 'object' &&
|
|
typeof actionReturnValue.then === 'function'
|
|
) {
|
|
// This is an async action.
|
|
//
|
|
// Return a thenable that resolves once the action scope (i.e. the async
|
|
// function passed to startTransition) has finished running. The fulfilled
|
|
// value is `false` to represent that the action is not pending.
|
|
const thenable: Wakeable = (actionReturnValue: any);
|
|
if (currentAsyncAction === null) {
|
|
// There's no outer async action scope. Create a new one.
|
|
const asyncAction: AsyncAction = {
|
|
lane: requestTransitionLane(),
|
|
listeners: [],
|
|
count: 0,
|
|
status: 'pending',
|
|
value: false,
|
|
reason: undefined,
|
|
then(resolve: boolean => mixed) {
|
|
asyncAction.listeners.push(resolve);
|
|
},
|
|
};
|
|
attachPingListeners(thenable, asyncAction);
|
|
currentAsyncAction = asyncAction;
|
|
return asyncAction;
|
|
} else {
|
|
// Inherit the outer scope.
|
|
const asyncAction: AsyncAction = (currentAsyncAction: any);
|
|
attachPingListeners(thenable, asyncAction);
|
|
return asyncAction;
|
|
}
|
|
} else {
|
|
// This is not an async action, but it may be part of an outer async action.
|
|
if (currentAsyncAction === null) {
|
|
// There's no outer async action scope.
|
|
return false;
|
|
} else {
|
|
// Inherit the outer scope.
|
|
return currentAsyncAction;
|
|
}
|
|
}
|
|
}
|
|
|
|
export function peekAsyncActionContext(): AsyncAction | null {
|
|
return currentAsyncAction;
|
|
}
|
|
|
|
function attachPingListeners(thenable: Wakeable, asyncAction: AsyncAction) {
|
|
asyncAction.count++;
|
|
thenable.then(
|
|
() => {
|
|
if (--asyncAction.count === 0) {
|
|
const fulfilledAsyncAction: FulfilledAsyncAction = (asyncAction: any);
|
|
fulfilledAsyncAction.status = 'fulfilled';
|
|
completeAsyncActionScope(asyncAction);
|
|
}
|
|
},
|
|
(error: mixed) => {
|
|
if (--asyncAction.count === 0) {
|
|
const rejectedAsyncAction: RejectedAsyncAction = (asyncAction: any);
|
|
rejectedAsyncAction.status = 'rejected';
|
|
rejectedAsyncAction.reason = error;
|
|
completeAsyncActionScope(asyncAction);
|
|
}
|
|
},
|
|
);
|
|
return asyncAction;
|
|
}
|
|
|
|
function completeAsyncActionScope(action: AsyncAction) {
|
|
if (currentAsyncAction === action) {
|
|
currentAsyncAction = null;
|
|
}
|
|
|
|
const listeners = action.listeners;
|
|
action.listeners = [];
|
|
for (let i = 0; i < listeners.length; i++) {
|
|
const listener = listeners[i];
|
|
listener(false);
|
|
}
|
|
}
|