import * as Scheduler from '../SCHEDULER/Scheduler'
import { decoupleUpdatePriorityFromScheduler } from '../shared/ReactFeatureFlags';
import { getCurrentUpdateLanePriority, setCurrentUpdateLanePriority, SyncLanePriority } from './ReactFiberLane';
const {
unstable_runWithPriority: Scheduler_runWithPriority,
unstable_getCurrentPriorityLevel: Scheduler_getCurrentPriorityLevel,
unstable_cancelCallback: Scheduler_cancelCallback,
unstable_scheduleCallback: Scheduler_scheduleCallback,
unstable_ImmediatePriority: Scheduler_ImmediatePriority,
unstable_UserBlockingPriority: Scheduler_UserBlockingPriority,
unstable_NormalPriority: Scheduler_NormalPriority,
unstable_LowPriority: Scheduler_LowPriority,
unstable_IdlePriority: Scheduler_IdlePriority,
unstable_now: Scheduler_now
} = Scheduler;
const fakeCallbackNode = {};
let syncQueue = null;
let immediateQueueCallbackNode = null;
let isFlushingSyncQueue = false;
const initialTimeMs = Scheduler_now();
export const now =
initialTimeMs < 10000 ? Scheduler_now : () => Scheduler_now() - initialTimeMs;
// Except for NoPriority, these correspond to Scheduler priorities. We use
// ascending numbers so we can compare them like numbers. They start at 90 to
// avoid clashing with Scheduler's priorities.
export const ImmediatePriority = 99;
export const UserBlockingPriority = 98;
export const NormalPriority = 97;
export const LowPriority = 96;
export const IdlePriority = 95;
// NoPriority is the absence of priority. Also React-only.
export const NoPriority = 90;
export function getCurrentPriorityLevel() {
switch(Scheduler_getCurrentPriorityLevel()) {
case Scheduler_ImmediatePriority:
return ImmediatePriority;
case Scheduler_UserBlockingPriority:
return UserBlockingPriority;
case Scheduler_NormalPriority:
return NormalPriority;
case Scheduler_LowPriority:
return LowPriority;
case Scheduler_IdlePriority:
return IdlePriority;
default:
return 'unknowPriority';
}
}
function reactPriorityToSchedulerPriority(reactPriorityLevel) {
switch(reactPriorityLevel) {
case ImmediatePriority:
return Scheduler_ImmediatePriority;
case UserBlockingPriority:
return Scheduler_UserBlockingPriority;
case NormalPriority:
return Scheduler_NormalPriority;
case LowPriority:
return Scheduler_LowPriority;
case IdlePriority:
return Scheduler_IdlePriority;
default:
return 'unknowPriority';
}
}
export function runWithPriority(reactPriorityLevel, fn) {
const priorityLevel = reactPriorityToSchedulerPriority(reactPriorityLevel);
return Scheduler_runWithPriority(priorityLevel, fn);
}
export function scheduleCallback(reactPriorityLevel, callback, options) {
const priorityLevel = reactPriorityToSchedulerPriority(reactPriorityLevel);
return Scheduler_scheduleCallback(priorityLevel, callback, options);
}
export function flushSyncCallbackQueue() {
if(immediateQueueCallbackNode !== null) {
const node = immediateQueueCallbackNode;
immediateQueueCallbackNode = null;
Scheduler_cancelCallback(node);
}
flushSyncCallbackQueueImpl();
}
export function cancelCallback(callbackNode) {
if(callbackNode !== fakeCallbackNode) {
Scheduler_cancelCallback(callbackNode);
}
}
export function scheduleSyncCallback(callback) {
if(syncQueue === null) {
syncQueue = [callback];
immediateQueueCallbackNode = Scheduler_scheduleCallback(
Scheduler_ImmediatePriority,
flushSyncCallbackQueueImpl,
);
} else {
syncQueue.push(callback);
}
return fakeCallbackNode;
}
function flushSyncCallbackQueueImpl() {
if(!isFlushingSyncQueue && syncQueue !== null) {
isFlushingSyncQueue = true;
let i = 0;
if(decoupleUpdatePriorityFromScheduler) {
const previousLanePriority = getCurrentUpdateLanePriority();
try {
const isSync = true;
const queue = syncQueue;
setCurrentUpdateLanePriority(SyncLanePriority);
runWithPriority(
ImmediatePriority,
() => {
for(; i < queue.length; i++) {
let callback = queue[i];
do{
callback = callback(isSync);
} while(callback !== null)
}
}
);
syncQueue = null;
} catch(err) {
} finally {
setCurrentUpdateLanePriority(previousLanePriority);
isFlushingSyncQueue = false;
}
} else {
try {
const isSync = true;
const queue = syncQueue;
runWithPriority(
ImmediatePriority,
() => {
for(; i < queue.length; i++) {
let callback = queue[i];
do{
callback = callback(isSync);
} while(callback !== null)
}
}
);
syncQueue = null;
} catch(err) {
} finally {
isFlushingSyncQueue = false;
}
}
return true;
} else {
return false;
}
}
/**
* Copyright (c) Facebook, Inc. and its 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 {AnyNativeEvent} from '../events/PluginModuleType';
import type {FiberRoot} from 'react-reconciler/src/ReactInternalTypes';
import type {Container, SuspenseInstance} from '../client/ReactDOMHostConfig';
import type {DOMEventName} from '../events/DOMEventNames';
// Intentionally not named imports because Rollup would use dynamic dispatch for
// CommonJS interop named imports.
import * as Scheduler from 'scheduler';
import {
isReplayableDiscreteEvent,
queueDiscreteEvent,
hasQueuedDiscreteEvents,
clearIfContinuousEvent,
queueIfContinuousEvent,
} from './ReactDOMEventReplaying';
import {
getNearestMountedFiber,
getContainerFromFiber,
getSuspenseInstanceFromFiber,
} from 'react-reconciler/src/ReactFiberTreeReflection';
import {HostRoot, SuspenseComponent} from 'react-reconciler/src/ReactWorkTags';
import {
type EventSystemFlags,
IS_CAPTURE_PHASE,
IS_LEGACY_FB_SUPPORT_MODE,
} from './EventSystemFlags';
import getEventTarget from './getEventTarget';
import {getClosestInstanceFromNode} from '../client/ReactDOMComponentTree';
import {
enableLegacyFBSupport,
decoupleUpdatePriorityFromScheduler,
enableNewReconciler,
} from 'shared/ReactFeatureFlags';
import {
UserBlockingEvent,
ContinuousEvent,
DiscreteEvent,
} from 'shared/ReactTypes';
import {getEventPriorityForPluginSystem} from './DOMEventProperties';
import {dispatchEventForPluginEventSystem} from './DOMPluginEventSystem';
import {
flushDiscreteUpdatesIfNeeded,
discreteUpdates,
} from './ReactDOMUpdateBatching';
import {
InputContinuousLanePriority as InputContinuousLanePriority_old,
getCurrentUpdateLanePriority as getCurrentUpdateLanePriority_old,
setCurrentUpdateLanePriority as setCurrentUpdateLanePriority_old,
} from 'react-reconciler/src/ReactFiberLane.old';
import {
InputContinuousLanePriority as InputContinuousLanePriority_new,
getCurrentUpdateLanePriority as getCurrentUpdateLanePriority_new,
setCurrentUpdateLanePriority as setCurrentUpdateLanePriority_new,
} from 'react-reconciler/src/ReactFiberLane.new';
const InputContinuousLanePriority = enableNewReconciler
? InputContinuousLanePriority_new
: InputContinuousLanePriority_old;
const getCurrentUpdateLanePriority = enableNewReconciler
? getCurrentUpdateLanePriority_new
: getCurrentUpdateLanePriority_old;
const setCurrentUpdateLanePriority = enableNewReconciler
? setCurrentUpdateLanePriority_new
: setCurrentUpdateLanePriority_old;
const {
unstable_UserBlockingPriority: UserBlockingPriority,
unstable_runWithPriority: runWithPriority,
} = Scheduler;
// TODO: can we stop exporting these?
export let _enabled = true;
// This is exported in FB builds for use by legacy FB layer infra.
// We'd like to remove this but it's not clear if this is safe.
export function setEnabled(enabled: ?boolean) {
_enabled = !!enabled;
}
export function isEnabled() {
return _enabled;
}
export function createEventListenerWrapper(
targetContainer: EventTarget,
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
): Function {
return dispatchEvent.bind(
null,
domEventName,
eventSystemFlags,
targetContainer,
);
}
export function createEventListenerWrapperWithPriority(
targetContainer: EventTarget,
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
): Function {
const eventPriority = getEventPriorityForPluginSystem(domEventName);
let listenerWrapper;
switch (eventPriority) {
case DiscreteEvent:
listenerWrapper = dispatchDiscreteEvent;
break;
case UserBlockingEvent:
listenerWrapper = dispatchUserBlockingUpdate;
break;
case ContinuousEvent:
default:
listenerWrapper = dispatchEvent;
break;
}
return listenerWrapper.bind(
null,
domEventName,
eventSystemFlags,
targetContainer,
);
}
function dispatchDiscreteEvent(
domEventName,
eventSystemFlags,
container,
nativeEvent,
) {
if (
!enableLegacyFBSupport ||
// If we are in Legacy FB support mode, it means we've already
// flushed for this event and we don't need to do it again.
(eventSystemFlags & IS_LEGACY_FB_SUPPORT_MODE) === 0
) {
flushDiscreteUpdatesIfNeeded(nativeEvent.timeStamp);
}
discreteUpdates(
dispatchEvent,
domEventName,
eventSystemFlags,
container,
nativeEvent,
);
}
function dispatchUserBlockingUpdate(
domEventName,
eventSystemFlags,
container,
nativeEvent,
) {
if (decoupleUpdatePriorityFromScheduler) {
const previousPriority = getCurrentUpdateLanePriority();
try {
// TODO: Double wrapping is necessary while we decouple Scheduler priority.
setCurrentUpdateLanePriority(InputContinuousLanePriority);
runWithPriority(
UserBlockingPriority,
dispatchEvent.bind(
null,
domEventName,
eventSystemFlags,
container,
nativeEvent,
),
);
} finally {
setCurrentUpdateLanePriority(previousPriority);
}
} else {
runWithPriority(
UserBlockingPriority,
dispatchEvent.bind(
null,
domEventName,
eventSystemFlags,
container,
nativeEvent,
),
);
}
}
export function dispatchEvent(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
targetContainer: EventTarget,
nativeEvent: AnyNativeEvent,
): void {
if (!_enabled) {
return;
}
// TODO: replaying capture phase events is currently broken
// because we used to do it during top-level native bubble handlers
// but now we use different bubble and capture handlers.
// In eager mode, we attach capture listeners early, so we need
// to filter them out until we fix the logic to handle them correctly.
const allowReplay = (eventSystemFlags & IS_CAPTURE_PHASE) === 0;
if (
allowReplay &&
hasQueuedDiscreteEvents() &&
isReplayableDiscreteEvent(domEventName)
) {
// If we already have a queue of discrete events, and this is another discrete
// event, then we can't dispatch it regardless of its target, since they
// need to dispatch in order.
queueDiscreteEvent(
null, // Flags that we're not actually blocked on anything as far as we know.
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent,
);
return;
}
const blockedOn = attemptToDispatchEvent(
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent,
);
if (blockedOn === null) {
// We successfully dispatched this event.
if (allowReplay) {
clearIfContinuousEvent(domEventName, nativeEvent);
}
return;
}
if (allowReplay) {
if (isReplayableDiscreteEvent(domEventName)) {
// This this to be replayed later once the target is available.
queueDiscreteEvent(
blockedOn,
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent,
);
return;
}
if (
queueIfContinuousEvent(
blockedOn,
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent,
)
) {
return;
}
// We need to clear only if we didn't queue because
// queueing is accumulative.
clearIfContinuousEvent(domEventName, nativeEvent);
}
// This is not replayable so we'll invoke it but without a target,
// in case the event system needs to trace it.
dispatchEventForPluginEventSystem(
domEventName,
eventSystemFlags,
nativeEvent,
null,
targetContainer,
);
}
// Attempt dispatching an event. Returns a SuspenseInstance or Container if it's blocked.
export function attemptToDispatchEvent(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
targetContainer: EventTarget,
nativeEvent: AnyNativeEvent,
): null | Container | SuspenseInstance {
// TODO: Warn if _enabled is false.
const nativeEventTarget = getEventTarget(nativeEvent);
let targetInst = getClosestInstanceFromNode(nativeEventTarget);
if (targetInst !== null) {
const nearestMounted = getNearestMountedFiber(targetInst);
if (nearestMounted === null) {
// This tree has been unmounted already. Dispatch without a target.
targetInst = null;
} else {
const tag = nearestMounted.tag;
if (tag === SuspenseComponent) {
const instance = getSuspenseInstanceFromFiber(nearestMounted);
if (instance !== null) {
// Queue the event to be replayed later. Abort dispatching since we
// don't want this event dispatched twice through the event system.
// TODO: If this is the first discrete event in the queue. Schedule an increased
// priority for this boundary.
return instance;
}
// This shouldn't happen, something went wrong but to avoid blocking
// the whole system, dispatch the event without a target.
// TODO: Warn.
targetInst = null;
} else if (tag === HostRoot) {
const root: FiberRoot = nearestMounted.stateNode;
if (root.hydrate) {
// If this happens during a replay something went wrong and it might block
// the whole system.
return getContainerFromFiber(nearestMounted);
}
targetInst = null;
} else if (nearestMounted !== targetInst) {
// If we get an event (ex: img onload) before committing that
// component's mount, ignore it for now (that is, treat it as if it was an
// event on a non-React tree). We might also consider queueing events and
// dispatching them after the mount.
targetInst = null;
}
}
}
dispatchEventForPluginEventSystem(
domEventName,
eventSystemFlags,
nativeEvent,
targetInst,
targetContainer,
);
// We're not blocked on anything.
return null;
}