diff --git a/package.json b/package.json index 1699d6847ef41..0cb9c51bcebab 100644 --- a/package.json +++ b/package.json @@ -102,7 +102,7 @@ "test": "cross-env NODE_ENV=development jest --config ./scripts/jest/config.source.js", "test-persistent": "cross-env NODE_ENV=development jest --config ./scripts/jest/config.source-persistent.js", "test-fire": "cross-env NODE_ENV=development jest --config ./scripts/jest/config.source-fire.js", - "test-new-scheduler": "cross-env NODE_ENV=development jest --config ./scripts/jest/config.source-new-scheduler.js", + "test-old-scheduler": "cross-env NODE_ENV=development jest --config ./scripts/jest/config.source-old-scheduler.js", "test-prod": "cross-env NODE_ENV=production jest --config ./scripts/jest/config.source.js", "test-fire-prod": "cross-env NODE_ENV=production jest --config ./scripts/jest/config.source-fire.js", "test-prod-build": "yarn test-build-prod", diff --git a/packages/react-cache/src/LRU.js b/packages/react-cache/src/LRU.js index c12a0f6f4bea2..0b150dee7c27b 100644 --- a/packages/react-cache/src/LRU.js +++ b/packages/react-cache/src/LRU.js @@ -11,7 +11,10 @@ import * as Scheduler from 'scheduler'; // Intentionally not named imports because Rollup would // use dynamic dispatch for CommonJS interop named imports. -const {unstable_scheduleCallback: scheduleCallback} = Scheduler; +const { + unstable_scheduleCallback: scheduleCallback, + unstable_IdlePriority: IdlePriority, +} = Scheduler; type Entry = {| value: T, @@ -34,7 +37,7 @@ export function createLRU(limit: number) { // The cache size exceeds the limit. Schedule a callback to delete the // least recently used entries. cleanUpIsScheduled = true; - scheduleCallback(cleanUp); + scheduleCallback(IdlePriority, cleanUp); } } diff --git a/packages/react-dom/src/__tests__/ReactDOMHooks-test.js b/packages/react-dom/src/__tests__/ReactDOMHooks-test.js index 5dff31ad17a60..a57194e2e897c 100644 --- a/packages/react-dom/src/__tests__/ReactDOMHooks-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMHooks-test.js @@ -72,7 +72,8 @@ describe('ReactDOMHooks', () => { expect(container3.textContent).toBe('6'); }); - it('can batch synchronous work inside effects with other work', () => { + // TODO: This behavior is wrong. Fix this in the old implementation. + it.skip('can batch synchronous work inside effects with other work', () => { let otherContainer = document.createElement('div'); let calledA = false; diff --git a/packages/react-dom/src/__tests__/ReactDOMSuspensePlaceholder-test.js b/packages/react-dom/src/__tests__/ReactDOMSuspensePlaceholder-test.js index 14e02d396da7a..5094679256193 100644 --- a/packages/react-dom/src/__tests__/ReactDOMSuspensePlaceholder-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMSuspensePlaceholder-test.js @@ -14,6 +14,7 @@ let ReactDOM; let Suspense; let ReactCache; let ReactTestUtils; +let Scheduler; let TextResource; let act; @@ -26,6 +27,7 @@ describe('ReactDOMSuspensePlaceholder', () => { ReactDOM = require('react-dom'); ReactCache = require('react-cache'); ReactTestUtils = require('react-dom/test-utils'); + Scheduler = require('scheduler'); act = ReactTestUtils.act; Suspense = React.Suspense; container = document.createElement('div'); @@ -94,6 +96,8 @@ describe('ReactDOMSuspensePlaceholder', () => { await advanceTimers(1000); + Scheduler.flushAll(); + expect(divs[0].current.style.display).toEqual(''); expect(divs[1].current.style.display).toEqual(''); // This div's display was set with a prop. @@ -115,6 +119,8 @@ describe('ReactDOMSuspensePlaceholder', () => { await advanceTimers(1000); + Scheduler.flushAll(); + expect(container.textContent).toEqual('ABC'); }); @@ -160,6 +166,8 @@ describe('ReactDOMSuspensePlaceholder', () => { await advanceTimers(1000); + Scheduler.flushAll(); + expect(container.innerHTML).toEqual( 'SiblingAsync', ); diff --git a/packages/react-reconciler/src/ReactDebugFiberPerf.js b/packages/react-reconciler/src/ReactDebugFiberPerf.js index 96b71a33c2dae..7cad531afbb68 100644 --- a/packages/react-reconciler/src/ReactDebugFiberPerf.js +++ b/packages/react-reconciler/src/ReactDebugFiberPerf.js @@ -253,7 +253,7 @@ export function stopRequestCallbackTimer( expirationTime: number, ): void { if (enableUserTimingAPI) { - if (supportsUserTiming) { + if (supportsUserTiming && isWaitingForCallback) { isWaitingForCallback = false; const warning = didExpire ? 'React was blocked by main thread' : null; endMark( diff --git a/packages/react-reconciler/src/ReactFiberExpirationTime.js b/packages/react-reconciler/src/ReactFiberExpirationTime.js index a29dfbdf17b25..f75ca42a74a57 100644 --- a/packages/react-reconciler/src/ReactFiberExpirationTime.js +++ b/packages/react-reconciler/src/ReactFiberExpirationTime.js @@ -7,8 +7,17 @@ * @flow */ +import type {ReactPriorityLevel} from './SchedulerWithReactIntegration'; + import MAX_SIGNED_31_BIT_INT from './maxSigned31BitInt'; +import { + ImmediatePriority, + UserBlockingPriority, + NormalPriority, + IdlePriority, +} from './SchedulerWithReactIntegration'; + export type ExpirationTime = number; export const NoWork = 0; @@ -46,6 +55,8 @@ function computeExpirationBucket( ); } +// TODO: This corresponds to Scheduler's NormalPriority, not LowPriority. Update +// the names to reflect. export const LOW_PRIORITY_EXPIRATION = 5000; export const LOW_PRIORITY_BATCH_SIZE = 250; @@ -80,3 +91,31 @@ export function computeInteractiveExpiration(currentTime: ExpirationTime) { HIGH_PRIORITY_BATCH_SIZE, ); } + +export function inferPriorityFromExpirationTime( + currentTime: ExpirationTime, + expirationTime: ExpirationTime, +): ReactPriorityLevel { + if (expirationTime === Sync) { + return ImmediatePriority; + } + if (expirationTime === Never) { + return IdlePriority; + } + const msUntil = + msToExpirationTime(expirationTime) - msToExpirationTime(currentTime); + if (msUntil <= 0) { + return ImmediatePriority; + } + if (msUntil <= HIGH_PRIORITY_EXPIRATION) { + return UserBlockingPriority; + } + if (msUntil <= LOW_PRIORITY_EXPIRATION) { + return NormalPriority; + } + + // TODO: Handle LowPriority + + // Assume anything lower has idle priority + return IdlePriority; +} diff --git a/packages/react-reconciler/src/ReactFiberReconciler.js b/packages/react-reconciler/src/ReactFiberReconciler.js index 01e303c74bc42..03b8424e777ac 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.js @@ -43,7 +43,6 @@ import { requestCurrentTime, computeExpirationForFiber, scheduleWork, - requestWork, flushRoot, batchedUpdates, unbatchedUpdates, @@ -300,7 +299,6 @@ export function updateContainer( export { flushRoot, - requestWork, computeUniqueAsyncExpiration, batchedUpdates, unbatchedUpdates, diff --git a/packages/react-reconciler/src/ReactFiberRoot.js b/packages/react-reconciler/src/ReactFiberRoot.js index e3e445b46ef7f..5ef38c081f78e 100644 --- a/packages/react-reconciler/src/ReactFiberRoot.js +++ b/packages/react-reconciler/src/ReactFiberRoot.js @@ -83,6 +83,13 @@ type BaseFiberRootProperties = {| firstBatch: Batch | null, // Linked-list of roots nextScheduledRoot: FiberRoot | null, + + // New Scheduler fields + callbackNode: *, + callbackExpirationTime: ExpirationTime, + firstPendingTime: ExpirationTime, + lastPendingTime: ExpirationTime, + pingTime: ExpirationTime, |}; // The following attributes are only used by interaction tracing builds. @@ -145,6 +152,12 @@ export function createFiberRoot( interactionThreadID: unstable_getThreadID(), memoizedInteractions: new Set(), pendingInteractionMap: new Map(), + + callbackNode: null, + callbackExpirationTime: NoWork, + firstPendingTime: NoWork, + lastPendingTime: NoWork, + pingTime: NoWork, }: FiberRoot); } else { root = ({ @@ -172,6 +185,12 @@ export function createFiberRoot( expirationTime: NoWork, firstBatch: null, nextScheduledRoot: null, + + callbackNode: null, + callbackExpirationTime: NoWork, + firstPendingTime: NoWork, + lastPendingTime: NoWork, + pingTime: NoWork, }: BaseFiberRootProperties); } diff --git a/packages/react-reconciler/src/ReactFiberScheduler.js b/packages/react-reconciler/src/ReactFiberScheduler.js index 5bea74a78b5ff..74cfdb94d3fef 100644 --- a/packages/react-reconciler/src/ReactFiberScheduler.js +++ b/packages/react-reconciler/src/ReactFiberScheduler.js @@ -22,7 +22,6 @@ import { markLegacyErrorBoundaryAsFailed as markLegacyErrorBoundaryAsFailed_old, isAlreadyFailedLegacyErrorBoundary as isAlreadyFailedLegacyErrorBoundary_old, scheduleWork as scheduleWork_old, - requestWork as requestWork_old, flushRoot as flushRoot_old, batchedUpdates as batchedUpdates_old, unbatchedUpdates as unbatchedUpdates_old, @@ -35,6 +34,7 @@ import { computeUniqueAsyncExpiration as computeUniqueAsyncExpiration_old, flushPassiveEffects as flushPassiveEffects_old, warnIfNotCurrentlyBatchingInDev as warnIfNotCurrentlyBatchingInDev_old, + inferStartTimeFromExpirationTime as inferStartTimeFromExpirationTime_old, } from './ReactFiberScheduler.old'; import { @@ -50,7 +50,6 @@ import { markLegacyErrorBoundaryAsFailed as markLegacyErrorBoundaryAsFailed_new, isAlreadyFailedLegacyErrorBoundary as isAlreadyFailedLegacyErrorBoundary_new, scheduleWork as scheduleWork_new, - requestWork as requestWork_new, flushRoot as flushRoot_new, batchedUpdates as batchedUpdates_new, unbatchedUpdates as unbatchedUpdates_new, @@ -63,6 +62,7 @@ import { computeUniqueAsyncExpiration as computeUniqueAsyncExpiration_new, flushPassiveEffects as flushPassiveEffects_new, warnIfNotCurrentlyBatchingInDev as warnIfNotCurrentlyBatchingInDev_new, + inferStartTimeFromExpirationTime as inferStartTimeFromExpirationTime_new, } from './ReactFiberScheduler.new'; export let requestCurrentTime = requestCurrentTime_old; @@ -77,7 +77,6 @@ export let resolveRetryThenable = resolveRetryThenable_old; export let markLegacyErrorBoundaryAsFailed = markLegacyErrorBoundaryAsFailed_old; export let isAlreadyFailedLegacyErrorBoundary = isAlreadyFailedLegacyErrorBoundary_old; export let scheduleWork = scheduleWork_old; -export let requestWork = requestWork_old; export let flushRoot = flushRoot_old; export let batchedUpdates = batchedUpdates_old; export let unbatchedUpdates = unbatchedUpdates_old; @@ -90,6 +89,7 @@ export let flushInteractiveUpdates = flushInteractiveUpdates_old; export let computeUniqueAsyncExpiration = computeUniqueAsyncExpiration_old; export let flushPassiveEffects = flushPassiveEffects_old; export let warnIfNotCurrentlyBatchingInDev = warnIfNotCurrentlyBatchingInDev_old; +export let inferStartTimeFromExpirationTime = inferStartTimeFromExpirationTime_old; if (enableNewScheduler) { requestCurrentTime = requestCurrentTime_new; @@ -104,7 +104,6 @@ if (enableNewScheduler) { markLegacyErrorBoundaryAsFailed = markLegacyErrorBoundaryAsFailed_new; isAlreadyFailedLegacyErrorBoundary = isAlreadyFailedLegacyErrorBoundary_new; scheduleWork = scheduleWork_new; - requestWork = requestWork_new; flushRoot = flushRoot_new; batchedUpdates = batchedUpdates_new; unbatchedUpdates = unbatchedUpdates_new; @@ -117,6 +116,7 @@ if (enableNewScheduler) { computeUniqueAsyncExpiration = computeUniqueAsyncExpiration_new; flushPassiveEffects = flushPassiveEffects_new; warnIfNotCurrentlyBatchingInDev = warnIfNotCurrentlyBatchingInDev_new; + inferStartTimeFromExpirationTime = inferStartTimeFromExpirationTime_new; } export type Thenable = { diff --git a/packages/react-reconciler/src/ReactFiberScheduler.new.js b/packages/react-reconciler/src/ReactFiberScheduler.new.js index e4df9dad4fe72..3b64676239a77 100644 --- a/packages/react-reconciler/src/ReactFiberScheduler.new.js +++ b/packages/react-reconciler/src/ReactFiberScheduler.new.js @@ -5,32 +5,1972 @@ * LICENSE file in the root directory of this source tree. */ -function notYetImplemented() { - throw new Error('Not yet implemented.'); -} - -export const requestCurrentTime = notYetImplemented; -export const computeExpirationForFiber = notYetImplemented; -export const captureCommitPhaseError = notYetImplemented; -export const onUncaughtError = notYetImplemented; -export const renderDidSuspend = notYetImplemented; -export const renderDidError = notYetImplemented; -export const pingSuspendedRoot = notYetImplemented; -export const retryTimedOutBoundary = notYetImplemented; -export const resolveRetryThenable = notYetImplemented; -export const markLegacyErrorBoundaryAsFailed = notYetImplemented; -export const isAlreadyFailedLegacyErrorBoundary = notYetImplemented; -export const scheduleWork = notYetImplemented; -export const requestWork = notYetImplemented; -export const flushRoot = notYetImplemented; -export const batchedUpdates = notYetImplemented; -export const unbatchedUpdates = notYetImplemented; -export const flushSync = notYetImplemented; -export const flushControlled = notYetImplemented; -export const deferredUpdates = notYetImplemented; -export const syncUpdates = notYetImplemented; -export const interactiveUpdates = notYetImplemented; -export const flushInteractiveUpdates = notYetImplemented; -export const computeUniqueAsyncExpiration = notYetImplemented; -export const flushPassiveEffects = notYetImplemented; -export const warnIfNotCurrentlyBatchingInDev = notYetImplemented; +import type {Fiber} from './ReactFiber'; +import type {ExpirationTime} from './ReactFiberExpirationTime'; +import type {ReactPriorityLevel} from './SchedulerWithReactIntegration'; + +import { + warnAboutDeprecatedLifecycles, + enableUserTimingAPI, + enableSuspenseServerRenderer, + replayFailedUnitOfWorkWithInvokeGuardedCallback, + enableProfilerTimer, + disableYielding, + enableSchedulerTracing, +} from 'shared/ReactFeatureFlags'; +import ReactSharedInternals from 'shared/ReactSharedInternals'; +import invariant from 'shared/invariant'; + +import { + scheduleCallback, + cancelCallback, + getCurrentPriorityLevel, + runWithPriority, + shouldYield, + now, + ImmediatePriority, + UserBlockingPriority, + NormalPriority, + LowPriority, + IdlePriority, + flushImmediateQueue, +} from './SchedulerWithReactIntegration'; + +import { + __interactionsRef, + __subscriberRef, + // unstable_wrap as Scheduler_tracing_wrap, +} from 'scheduler/tracing'; + +import { + prepareForCommit, + resetAfterCommit, + scheduleTimeout, + cancelTimeout, + noTimeout, +} from './ReactFiberHostConfig'; + +import {createWorkInProgress, assignFiberPropertiesInDEV} from './ReactFiber'; +import {NoContext, ConcurrentMode, ProfileMode} from './ReactTypeOfMode'; +import { + HostRoot, + ClassComponent, + SuspenseComponent, + DehydratedSuspenseComponent, + FunctionComponent, + ForwardRef, + MemoComponent, + SimpleMemoComponent, +} from 'shared/ReactWorkTags'; +import { + NoEffect, + PerformedWork, + Placement, + Update, + PlacementAndUpdate, + Deletion, + Ref, + ContentReset, + Snapshot, + Callback, + Passive, + Incomplete, + HostEffectMask, +} from 'shared/ReactSideEffectTags'; +import { + NoWork, + Sync, + Never, + msToExpirationTime, + expirationTimeToMs, + computeInteractiveExpiration, + computeAsyncExpiration, + inferPriorityFromExpirationTime, + LOW_PRIORITY_EXPIRATION, +} from './ReactFiberExpirationTime'; +import {beginWork as originalBeginWork} from './ReactFiberBeginWork'; +import {completeWork} from './ReactFiberCompleteWork'; +import { + throwException, + unwindWork, + unwindInterruptedWork, + createRootErrorUpdate, + createClassErrorUpdate, +} from './ReactFiberUnwindWork'; +import { + commitBeforeMutationLifeCycles as commitBeforeMutationEffectOnFiber, + commitLifeCycles as commitLayoutEffectOnFiber, + commitPassiveHookEffects, + commitPlacement, + commitWork, + commitDeletion, + commitDetachRef, + commitAttachRef, + commitResetTextContent, +} from './ReactFiberCommitWork'; +import {enqueueUpdate} from './ReactUpdateQueue'; +// TODO: Ahaha Andrew is bad at spellling +import {resetContextDependences as resetContextDependencies} from './ReactFiberNewContext'; +import {resetHooks, ContextOnlyDispatcher} from './ReactFiberHooks'; +import {createCapturedValue} from './ReactCapturedValue'; + +import { + recordCommitTime, + startProfilerTimer, + stopProfilerTimerIfRunningAndRecordDelta, +} from './ReactProfilerTimer'; + +// DEV stuff +import warningWithoutStack from 'shared/warningWithoutStack'; +import getComponentName from 'shared/getComponentName'; +import ReactStrictModeWarnings from './ReactStrictModeWarnings'; +import { + phase as ReactCurrentDebugFiberPhaseInDEV, + resetCurrentFiber as resetCurrentDebugFiberInDEV, + setCurrentFiber as setCurrentDebugFiberInDEV, + getStackByFiberInDevAndProd, +} from './ReactCurrentFiber'; +import { + recordEffect, + recordScheduleUpdate, + startRequestCallbackTimer, + stopRequestCallbackTimer, + startWorkTimer, + stopWorkTimer, + stopFailedWorkTimer, + startWorkLoopTimer, + stopWorkLoopTimer, + startCommitTimer, + stopCommitTimer, + startCommitSnapshotEffectsTimer, + stopCommitSnapshotEffectsTimer, + startCommitHostEffectsTimer, + stopCommitHostEffectsTimer, + startCommitLifeCyclesTimer, + stopCommitLifeCyclesTimer, +} from './ReactDebugFiberPerf'; +import { + invokeGuardedCallback, + hasCaughtError, + clearCaughtError, +} from 'shared/ReactErrorUtils'; + +const {ReactCurrentDispatcher, ReactCurrentOwner} = ReactSharedInternals; + +type WorkPhase = 0 | 1 | 2 | 3 | 4; +const NotWorking = 0; +const BatchedPhase = 1; +const FlushSyncPhase = 2; +const RenderPhase = 3; +const CommitPhase = 4; + +type RootExitStatus = 0 | 1 | 2 | 3; +const RootIncomplete = 0; +const RootErrored = 1; +const RootSuspended = 2; +const RootCompleted = 3; + +export type Thenable = { + then(resolve: () => mixed, reject?: () => mixed): mixed, +}; + +// The phase of work we're currently in +let workPhase: WorkPhase = NotWorking; +// The root we're working on +let workInProgressRoot: FiberRoot | null = null; +// The fiber we're working on +let workInProgress: Fiber | null = null; +// The expiration time we're rendering +let renderExpirationTime: ExpirationTime = NoWork; +// Whether to root completed, errored, suspended, etc. +let workInProgressRootExitStatus: RootExitStatus = RootIncomplete; +let workInProgressRootAbsoluteTimeoutMs: number = -1; + +let nextEffect: Fiber | null = null; +let hasUncaughtError = false; +let firstUncaughtError = null; +let legacyErrorBoundariesThatAlreadyFailed: Map | null = null; + +let rootDoesHavePassiveEffects: boolean = false; +let rootWithPendingPassiveEffects: FiberRoot | null = null; + +let rootsWithPendingDiscreteUpdates: Map< + FiberRoot, + expirationTime, +> | null = null; + +// Use these to prevent an infinite loop of nested updates +const NESTED_UPDATE_LIMIT = 50; +let nestedUpdateCount: number = 0; +let rootWithNestedUpdates: FiberRoot | null = null; + +let interruptedBy: Fiber | null = null; + +export function requestCurrentTime() { + return msToExpirationTime(now()); +} + +export function computeExpirationForFiber( + currentTime: ExpirationTime, + fiber: Fiber, +): ExpirationTime { + if ((fiber.mode & ConcurrentMode) === NoContext) { + return Sync; + } + + if (workPhase === RenderPhase) { + // Use whatever time we're already rendering + return renderExpirationTime; + } + + // Compute an expiration time based on the Scheduler priority. + let expirationTime; + const priorityLevel = getCurrentPriorityLevel(); + switch (priorityLevel) { + case ImmediatePriority: + expirationTime = Sync; + break; + case UserBlockingPriority: + // TODO: Rename this to computeUserBlockingExpiration + expirationTime = computeInteractiveExpiration(currentTime); + break; + case NormalPriority: + case LowPriority: // TODO: Handle LowPriority + // TODO: Rename this to... something better. + expirationTime = computeAsyncExpiration(currentTime); + break; + case IdlePriority: + expirationTime = Never; + break; + default: + invariant(false, 'Expected a valid priority level'); + } + + // If we're in the middle of rendering a tree, do not update at the same + // expiration time that is already rendering. + if (workInProgressRoot !== null && expirationTime === renderExpirationTime) { + // This is a trick to move this update into a separate batch + expirationTime -= 1; + } + + return expirationTime; +} + +let lastUniqueAsyncExpiration = NoWork; +export function computeUniqueAsyncExpiration(): ExpirationTime { + const currentTime = requestCurrentTime(); + let result = computeAsyncExpiration(currentTime); + if (result <= lastUniqueAsyncExpiration) { + // Since we assume the current time monotonically increases, we only hit + // this branch when computeUniqueAsyncExpiration is fired multiple times + // within a 200ms window (or whatever the async bucket size is). + result -= 1; + } + lastUniqueAsyncExpiration = result; + return result; +} + +export function scheduleUpdateOnFiber( + fiber: Fiber, + expirationTime: ExpirationTime, +) { + if (nestedUpdateCount > NESTED_UPDATE_LIMIT) { + nestedUpdateCount = 0; + rootWithNestedUpdates = null; + invariant( + false, + 'Maximum update depth exceeded. This can happen when a component ' + + 'repeatedly calls setState inside componentWillUpdate or ' + + 'componentDidUpdate. React limits the number of nested updates to ' + + 'prevent infinite loops.', + ); + } + + warnAboutInvalidUpdatesOnClassComponentsInDEV(fiber); + + const root = markUpdateTimeFromFiberToRoot(fiber, expirationTime); + if (root === null) { + warnAboutUpdateOnUnmountedFiberInDEV(fiber); + return; + } + + root.pingTime = NoWork; + + checkForInterruption(fiber, expirationTime); + recordScheduleUpdate(); + + if (expirationTime === Sync) { + scheduleCallbackForRoot(root, ImmediatePriority, Sync); + if (workPhase === NotWorking) { + // Flush the synchronous work now, wnless we're already working or inside + // a batch. This is intentionally inside scheduleUpdateOnFiber instead of + // scheduleCallbackForFiber to preserve the ability to schedule a callback + // without immediately flushing it. We only do this for user-initated + // updates, to preserve historical behavior of sync mode. + flushImmediateQueue(); + } + } else { + // TODO: computeExpirationForFiber also reads the priority. Pass the + // priority as an argument to that function and this one. + const priorityLevel = getCurrentPriorityLevel(); + if (priorityLevel === UserBlockingPriority) { + // This is the result of a discrete event. Track the lowest priority + // discrete update per root so we can flush them early, if needed. + if (rootsWithPendingDiscreteUpdates === null) { + rootsWithPendingDiscreteUpdates = new Map([[root, expirationTime]]); + } else { + const lastDiscreteTime = rootsWithPendingDiscreteUpdates.get(root); + if ( + lastDiscreteTime === undefined || + lastDiscreteTime > expirationTime + ) { + rootsWithPendingDiscreteUpdates.set(root, lastDiscreteTime); + } + } + } + scheduleCallbackForRoot(root, priorityLevel, expirationTime); + } +} +export const scheduleWork = scheduleUpdateOnFiber; + +// This is split into a separate function so we can mark a fiber with pending +// work without treating it as a typical update that originates from an event; +// e.g. retrying a Suspense boundary isn't an update, but it does schedule work +// on a fiber. +function markUpdateTimeFromFiberToRoot(fiber, expirationTime) { + // Update the source fiber's expiration time + if (fiber.expirationTime < expirationTime) { + fiber.expirationTime = expirationTime; + } + let alternate = fiber.alternate; + if (alternate !== null && alternate.expirationTime < expirationTime) { + alternate.expirationTime = expirationTime; + } + // Walk the parent path to the root and update the child expiration time. + let node = fiber.return; + let root = null; + if (node === null && fiber.tag === HostRoot) { + root = fiber.stateNode; + } else { + while (node !== null) { + alternate = node.alternate; + if (node.childExpirationTime < expirationTime) { + node.childExpirationTime = expirationTime; + if ( + alternate !== null && + alternate.childExpirationTime < expirationTime + ) { + alternate.childExpirationTime = expirationTime; + } + } else if ( + alternate !== null && + alternate.childExpirationTime < expirationTime + ) { + alternate.childExpirationTime = expirationTime; + } + if (node.return === null && node.tag === HostRoot) { + root = node.stateNode; + break; + } + node = node.return; + } + } + + if (root !== null) { + // Update the first and last pending expiration times in this root + const firstPendingTime = root.firstPendingTime; + if (expirationTime > firstPendingTime) { + root.firstPendingTime = expirationTime; + } + const lastPendingTime = root.lastPendingTime; + if (lastPendingTime === NoWork || expirationTime < lastPendingTime) { + root.lastPendingTime = expirationTime; + } + } + + return root; +} + +function scheduleCallbackForRoot( + root: FiberRoot, + priorityLevel: ReactPriorityLevel, + expirationTime: ExpirationTime, +) { + const existingCallbackExpirationTime = root.callbackExpirationTime; + if (existingCallbackExpirationTime < expirationTime) { + // New callback has higher priority than the existing one. + const existingCallbackNode = root.callbackNode; + if (existingCallbackNode !== null) { + cancelCallback(existingCallbackNode); + } + root.callbackExpirationTime = expirationTime; + root.callbackNode = scheduleCallback( + priorityLevel, + renderRoot.bind(null, root, expirationTime), + ); + if ( + enableUserTimingAPI && + expirationTime !== Sync && + workPhase !== RenderPhase && + workPhase !== CommitPhase + ) { + // Scheduled an async callback, and we're not already working. Add an + // entry to the flamegraph that shows we're waiting for a callback + // to fire. + startRequestCallbackTimer(); + } + } + + const timeoutHandle = root.timeoutHandle; + if (timeoutHandle !== noTimeout) { + // The root previous suspended and scheduled a timeout to commit a fallback + // state. Now that we have additional work, cancel the timeout. + root.timeoutHandle = noTimeout; + // $FlowFixMe Complains noTimeout is not a TimeoutID, despite the + // check above + cancelTimeout(timeoutHandle); + } + + schedulePendingInteraction(root, expirationTime); +} + +export function flushRoot(root: FiberRoot, expirationTime: ExpirationTime) { + if (workPhase === RenderPhase || workPhase === CommitPhase) { + invariant( + false, + 'work.commit(): Cannot commit while already rendering. This likely ' + + 'means you attempted to commit from inside a lifecycle method.', + ); + } + scheduleCallback( + ImmediatePriority, + renderRoot.bind(null, root, expirationTime), + ); + flushImmediateQueue(); +} + +export function flushInteractiveUpdates() { + if (workPhase === RenderPhase || workPhase === CommitPhase) { + // Can't synchronously flush interactive updates if React is already + // working. This is currently a no-op. + // TODO: Should we fire a warning? This happens if you synchronously invoke + // an input event inside an effect, like with `element.click()`. + return; + } + flushPendingDiscreteUpdates(); +} + +function resolveLocksOnRoot(root: FiberRoot, expirationTime: ExpirationTime) { + const firstBatch = root.firstBatch; + if ( + firstBatch !== null && + firstBatch._defer && + firstBatch._expirationTime >= expirationTime + ) { + scheduleCallback(NormalPriority, () => firstBatch._onComplete()); + return true; + } else { + return false; + } +} + +export function deferredUpdates(fn: () => A): A { + // TODO: Remove in favor of Scheduler.next + return runWithPriority(NormalPriority, fn); +} + +export function interactiveUpdates( + fn: (A, B, C) => R, + a: A, + b: B, + c: C, +): R { + if (workPhase === NotWorking) { + // TODO: Remove this call. Instead of doing this automatically, the caller + // should explicitly call flushInteractiveUpdates. + flushPendingDiscreteUpdates(); + } + return runWithPriority(UserBlockingPriority, fn.bind(null, a, b, c)); +} + +export function syncUpdates( + fn: (A, B, C) => R, + a: A, + b: B, + c: C, +): R { + return runWithPriority(ImmediatePriority, fn.bind(null, a, b, c)); +} + +function flushPendingDiscreteUpdates() { + if (rootsWithPendingDiscreteUpdates !== null) { + // For each root with pending discrete updates, schedule a callback to + // immediately flush them. + const roots = rootsWithPendingDiscreteUpdates; + rootsWithPendingDiscreteUpdates = null; + roots.forEach((expirationTime, root) => { + scheduleCallback( + ImmediatePriority, + renderRoot.bind(null, root, expirationTime), + ); + }); + // Now flush the immediate queue. + flushImmediateQueue(); + } +} + +export function batchedUpdates(fn: A => R, a): R { + if (workPhase !== NotWorking) { + // We're already working, or inside a batch, so batchedUpdates is a no-op. + return fn(a); + } + workPhase = BatchedPhase; + try { + return fn(a); + } finally { + workPhase = NotWorking; + // Flush the immediate callbacks that were scheduled during this batch + flushImmediateQueue(); + } +} + +export function unbatchedUpdates(fn: (a: A) => R, a: A): R { + if (workPhase === NotWorking) { + return fn(a); + } + const prevWorkPhase = workPhase; + workPhase = NotWorking; + try { + return fn(a); + } finally { + workPhase = prevWorkPhase; + } +} + +export function flushSync(fn: A => R, a): R { + if (workPhase === RenderPhase || workPhase === CommitPhase) { + invariant( + false, + 'flushSync was called from inside a lifecycle method. It cannot be ' + + 'called when React is already rendering.', + ); + } + const prevWorkPhase = workPhase; + workPhase = FlushSyncPhase; + try { + return runWithPriority(ImmediatePriority, fn.bind(null, a)); + } finally { + workPhase = prevWorkPhase; + // Flush the immediate callbacks that were scheduled during this batch. + // Note that this will happen even if batchedUpdates is higher up + // the stack. + flushImmediateQueue(); + } +} + +export function flushControlled(fn: () => mixed): void { + const prevWorkPhase = workPhase; + workPhase = BatchedPhase; + try { + runWithPriority(ImmediatePriority, fn); + } finally { + workPhase = prevWorkPhase; + if (workPhase === NotWorking) { + // Flush the immediate callbacks that were scheduled during this batch + flushImmediateQueue(); + } + } +} + +function prepareFreshStack(root, expirationTime) { + if (workInProgress !== null) { + let interruptedWork = workInProgress.return; + while (interruptedWork !== null) { + unwindInterruptedWork(interruptedWork); + interruptedWork = interruptedWork.return; + } + } + workInProgressRoot = root; + workInProgress = createWorkInProgress(root.current, null, expirationTime); + renderExpirationTime = expirationTime; + workInProgressRootExitStatus = RootIncomplete; + workInProgressRootAbsoluteTimeoutMs = -1; + + if (__DEV__) { + ReactStrictModeWarnings.discardPendingWarnings(); + } +} + +function renderRoot( + root: FiberRoot, + expirationTime: ExpirationTime, + isSync: boolean, +) { + invariant( + workPhase !== RenderPhase && workPhase !== CommitPhase, + 'Should not already be working.', + ); + + root.callbackExpirationTime = NoWork; + root.callbackNode = null; + + if (enableUserTimingAPI && expirationTime !== Sync) { + const didExpire = isSync; + const timeoutMs = expirationTimeToMs(expirationTime); + stopRequestCallbackTimer(didExpire, timeoutMs); + } + + if (root.lastPendingTime === NoWork) { + // If there's no work left, exit immediately. This happens when multiple + // callbacks are scheduled for a single root, but an earlier callback + // flushes the work of a later one. This is an optimization and isn't + // strictly necessary, but it reduces the cost of having too many callbacks. + return; + } + + flushPassiveEffects(); + + // If the root or expiration time have changed, throw out the existing stack + // and prepare a fresh one. Otherwise we'll continue where we left off. + if (root !== workInProgressRoot || expirationTime !== renderExpirationTime) { + prepareFreshStack(root, expirationTime); + startWorkOnPendingInteraction(root, expirationTime); + } + + // If we have a work-in-progress fiber, it means there's still work to do + // in this root. + if (workInProgress !== null) { + const prevWorkPhase = workPhase; + workPhase = RenderPhase; + const prevDispatcher = ReactCurrentDispatcher.current; + ReactCurrentDispatcher.current = ContextOnlyDispatcher; + let prevInteractions: Set | null = null; + if (enableSchedulerTracing) { + prevInteractions = __interactionsRef.current; + __interactionsRef.current = root.memoizedInteractions; + } + + startWorkLoopTimer(workInProgress); + do { + try { + if (isSync) { + if (expirationTime !== Sync) { + // An async update expired. There may be other expired updates on + // this root. We should render all the expired work in a + // single batch. + const currentTime = requestCurrentTime(); + if (currentTime < expirationTime) { + // Restart at the current time. + workPhase = prevWorkPhase; + ReactCurrentDispatcher.current = prevDispatcher; + return renderRoot.bind(null, root, currentTime); + } + } + workLoopSync(); + } else { + workLoop(); + } + break; + } catch (thrownValue) { + // Reset module-level state that was set during the render phase. + resetContextDependencies(); + resetHooks(); + + if (workInProgress === null || workInProgress.return === null) { + // Expected to be working on a non-root fiber. This is a fatal error + // because there's no ancestor that can handle it; the root is + // supposed to capture all errors that weren't caught by an error + // boundary. + prepareFreshStack(root, expirationTime); + workPhase = prevWorkPhase; + throw thrownValue; + } + + if (enableProfilerTimer && workInProgress.mode & ProfileMode) { + // Record the time spent rendering before an error was thrown. This + // avoids inaccurate Profiler durations in the case of a + // suspended render. + stopProfilerTimerIfRunningAndRecordDelta(workInProgress, true); + } + + const returnFiber = workInProgress.return; + throwException( + root, + returnFiber, + workInProgress, + thrownValue, + renderExpirationTime, + ); + workInProgress = completeUnitOfWork(workInProgress); + } + } while (true); + + workPhase = prevWorkPhase; + ReactCurrentDispatcher.current = prevDispatcher; + if (enableSchedulerTracing) { + __interactionsRef.current = prevInteractions; + } + + if (workInProgress !== null) { + // There's still work left over. Return a continuation. + stopInterruptedWorkLoopTimer(); + if (expirationTime !== Sync) { + startRequestCallbackTimer(); + } + return renderRoot.bind(null, root, expirationTime); + } + } + + // We now have a consistent tree. The next step is either to commit it, or, if + // something suspended, wait to commit it after a timeout. + stopFinishedWorkLoopTimer(); + + const isLocked = resolveLocksOnRoot(root, expirationTime); + if (isLocked) { + // This root has a lock that prevents it from committing. Exit. If we begin + // work on the root again, without any intervening updates, it will finish + // without doing additional work. + return; + } + + // Set this to null to indicate there's no in-progress render. + workInProgressRoot = null; + + switch (workInProgressRootExitStatus) { + case RootIncomplete: { + invariant(false, 'Should have a work-in-progress.'); + return; + } + case RootErrored: { + // An error was thrown. First check if there is lower priority work + // scheduled on this root. + const lastPendingTime = root.lastPendingTime; + if (root.lastPendingTime < expirationTime) { + // There's lower priority work. Before raising the error, try rendering + // at the lower priority to see if it fixes it. Use a continuation to + // maintain the existing priority and position in the queue. + return renderRoot.bind(null, root, lastPendingTime); + } + if (!isSync) { + // If we're rendering asynchronously, it's possible the error was + // caused by tearing due to a mutation during an event. Try rendering + // one more time without yiedling to events. + prepareFreshStack(root, expirationTime); + // Return a continutation to maintian the same priority, but pass + // isSync true to disable yielding. + return renderRoot.bind(null, root, expirationTime, true); + } + // If we're already rendering synchronously, commit the root in its + // errored state. + return commitRoot.bind(null, root, expirationTime); + } + case RootSuspended: { + const lastPendingTime = root.lastPendingTime; + if (root.lastPendingTime < expirationTime) { + // There's lower priority work. It might be unsuspended. Try rendering + // at that level. + return renderRoot.bind(null, root, lastPendingTime); + } + if (!isSync) { + const msUntilTimeout = computeMsUntilTimeout( + root, + workInProgressRootAbsoluteTimeoutMs, + ); + if (msUntilTimeout > 0) { + // The render is suspended, it hasn't timed out, and there's no lower + // priority work to do. Instead of committing the fallback + // immediately, wait for more data to arrive. + root.timeoutHandle = scheduleTimeout( + commitRoot.bind(null, root, expirationTime, true), + msUntilTimeout, + ); + return; + } + } + // The work expired. Commit immediately. + return commitRoot.bind(null, root, expirationTime); + } + case RootCompleted: { + // The work completed. Ready to commit. + return commitRoot.bind(null, root, expirationTime); + } + } +} + +export function renderDidSuspend( + root: FiberRoot, + absoluteTimeoutMs: number, + // TODO: Don't need this argument anymore + suspendedTime: ExpirationTime, +) { + if ( + absoluteTimeoutMs >= 0 && + workInProgressRootAbsoluteTimeoutMs < absoluteTimeoutMs + ) { + workInProgressRootAbsoluteTimeoutMs = absoluteTimeoutMs; + if (workInProgressRootExitStatus === RootIncomplete) { + workInProgressRootExitStatus = RootSuspended; + } + } +} + +export function renderDidError() { + if ( + workInProgressRootExitStatus === RootIncomplete || + workInProgressRootExitStatus === RootSuspended + ) { + workInProgressRootExitStatus = RootErrored; + } +} + +function workLoopSync() { + // Already timed out, so perform work without checking if we need to yield. + while (workInProgress !== null) { + workInProgress = performUnitOfWork(workInProgress); + } +} + +function workLoop() { + // Perform work until Scheduler asks us to yield + while (workInProgress !== null && !shouldYield()) { + workInProgress = performUnitOfWork(workInProgress); + } +} + +function performUnitOfWork(unitOfWork: Fiber): Fiber | null { + // The current, flushed, state of this fiber is the alternate. Ideally + // nothing should rely on this, but relying on it here means that we don't + // need an additional field on the work in progress. + const current = unitOfWork.alternate; + + startWorkTimer(unitOfWork); + setCurrentDebugFiberInDEV(unitOfWork); + + let next; + if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoContext) { + startProfilerTimer(unitOfWork); + next = beginWork(current, unitOfWork, renderExpirationTime); + stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true); + } else { + next = beginWork(current, unitOfWork, renderExpirationTime); + } + + resetCurrentDebugFiberInDEV(); + unitOfWork.memoizedProps = unitOfWork.pendingProps; + if (next === null) { + // If this doesn't spawn new work, complete the current work. + next = completeUnitOfWork(unitOfWork); + } + + ReactCurrentOwner.current = null; + return next; +} + +function completeUnitOfWork(unitOfWork: Fiber): Fiber | null { + // Attempt to complete the current unit of work, then move to the next + // sibling. If there are no more siblings, return to the parent fiber. + workInProgress = unitOfWork; + do { + // The current, flushed, state of this fiber is the alternate. Ideally + // nothing should rely on this, but relying on it here means that we don't + // need an additional field on the work in progress. + const current = workInProgress.alternate; + const returnFiber = workInProgress.return; + + // Check if the work completed or if something threw. + if ((workInProgress.effectTag & Incomplete) === NoEffect) { + setCurrentDebugFiberInDEV(workInProgress); + let next; + if ( + !enableProfilerTimer || + (workInProgress.mode & ProfileMode) === NoContext + ) { + next = completeWork(current, workInProgress, renderExpirationTime); + } else { + startProfilerTimer(workInProgress); + next = completeWork(current, workInProgress, renderExpirationTime); + // Update render duration assuming we didn't error. + stopProfilerTimerIfRunningAndRecordDelta(workInProgress, false); + } + stopWorkTimer(workInProgress); + resetCurrentDebugFiberInDEV(); + resetChildExpirationTime(workInProgress, renderExpirationTime); + + if (next !== null) { + // Completing this fiber spawned new work. Work on that next. + return next; + } + + if ( + returnFiber !== null && + // Do not append effects to parents if a sibling failed to complete + (returnFiber.effectTag & Incomplete) === NoEffect + ) { + // Append all the effects of the subtree and this fiber onto the effect + // list of the parent. The completion order of the children affects the + // side-effect order. + if (returnFiber.firstEffect === null) { + returnFiber.firstEffect = workInProgress.firstEffect; + } + if (workInProgress.lastEffect !== null) { + if (returnFiber.lastEffect !== null) { + returnFiber.lastEffect.nextEffect = workInProgress.firstEffect; + } + returnFiber.lastEffect = workInProgress.lastEffect; + } + + // If this fiber had side-effects, we append it AFTER the children's + // side-effects. We can perform certain side-effects earlier if needed, + // by doing multiple passes over the effect list. We don't want to + // schedule our own side-effect on our own list because if end up + // reusing children we'll schedule this effect onto itself since we're + // at the end. + const effectTag = workInProgress.effectTag; + + // Skip both NoWork and PerformedWork tags when creating the effect + // list. PerformedWork effect is read by React DevTools but shouldn't be + // committed. + if (effectTag > PerformedWork) { + if (returnFiber.lastEffect !== null) { + returnFiber.lastEffect.nextEffect = workInProgress; + } else { + returnFiber.firstEffect = workInProgress; + } + returnFiber.lastEffect = workInProgress; + } + } + } else { + // This fiber did not complete because something threw. Pop values off + // the stack without entering the complete phase. If this is a boundary, + // capture values if possible. + const next = unwindWork(workInProgress, renderExpirationTime); + + // Because this fiber did not complete, don't reset its expiration time. + + if ( + enableProfilerTimer && + (workInProgress.mode & ProfileMode) !== NoContext + ) { + // Record the render duration for the fiber that errored. + stopProfilerTimerIfRunningAndRecordDelta(workInProgress, false); + + // Include the time spent working on failed children before continuing. + let actualDuration = workInProgress.actualDuration; + let child = workInProgress.child; + while (child !== null) { + actualDuration += child.actualDuration; + child = child.sibling; + } + workInProgress.actualDuration = actualDuration; + } + + if (next !== null) { + // If completing this work spawned new work, do that next. We'll come + // back here again. + // Since we're restarting, remove anything that is not a host effect + // from the effect tag. + // TODO: The name stopFailedWorkTimer is misleading because Suspense + // also captures and restarts. + stopFailedWorkTimer(workInProgress); + next.effectTag &= HostEffectMask; + return next; + } + stopWorkTimer(workInProgress); + + if (returnFiber !== null) { + // Mark the parent fiber as incomplete and clear its effect list. + returnFiber.firstEffect = returnFiber.lastEffect = null; + returnFiber.effectTag |= Incomplete; + } + } + + const siblingFiber = workInProgress.sibling; + if (siblingFiber !== null) { + // If there is more work to do in this returnFiber, do that next. + return siblingFiber; + } + // Otherwise, return to the parent + workInProgress = returnFiber; + } while (workInProgress !== null); + + // We've reached the root. + if (workInProgressRootExitStatus === RootIncomplete) { + workInProgressRootExitStatus = RootCompleted; + } + return null; +} + +function resetChildExpirationTime(completedWork: Fiber) { + if ( + renderExpirationTime !== Never && + workInProgress.childExpirationTime === Never + ) { + // The children of this component are hidden. Don't bubble their + // expiration times. + return; + } + + let newChildExpirationTime = NoWork; + + // Bubble up the earliest expiration time. + if ( + enableProfilerTimer && + (workInProgress.mode & ProfileMode) !== NoContext + ) { + // In profiling mode, resetChildExpirationTime is also used to reset + // profiler durations. + let actualDuration = workInProgress.actualDuration; + let treeBaseDuration = workInProgress.selfBaseDuration; + + // When a fiber is cloned, its actualDuration is reset to 0. This value will + // only be updated if work is done on the fiber (i.e. it doesn't bailout). + // When work is done, it should bubble to the parent's actualDuration. If + // the fiber has not been cloned though, (meaning no work was done), then + // this value will reflect the amount of time spent working on a previous + // render. In that case it should not bubble. We determine whether it was + // cloned by comparing the child pointer. + const shouldBubbleActualDurations = + workInProgress.alternate === null || + workInProgress.child !== workInProgress.alternate.child; + + let child = workInProgress.child; + while (child !== null) { + const childUpdateExpirationTime = child.expirationTime; + const childChildExpirationTime = child.childExpirationTime; + if (childUpdateExpirationTime > newChildExpirationTime) { + newChildExpirationTime = childUpdateExpirationTime; + } + if (childChildExpirationTime > newChildExpirationTime) { + newChildExpirationTime = childChildExpirationTime; + } + if (shouldBubbleActualDurations) { + actualDuration += child.actualDuration; + } + treeBaseDuration += child.treeBaseDuration; + child = child.sibling; + } + workInProgress.actualDuration = actualDuration; + workInProgress.treeBaseDuration = treeBaseDuration; + } else { + let child = workInProgress.child; + while (child !== null) { + const childUpdateExpirationTime = child.expirationTime; + const childChildExpirationTime = child.childExpirationTime; + if (childUpdateExpirationTime > newChildExpirationTime) { + newChildExpirationTime = childUpdateExpirationTime; + } + if (childChildExpirationTime > newChildExpirationTime) { + newChildExpirationTime = childChildExpirationTime; + } + child = child.sibling; + } + } + + workInProgress.childExpirationTime = newChildExpirationTime; +} + +function commitRoot(root, expirationTime) { + runWithPriority( + ImmediatePriority, + commitRootImpl.bind(null, root, expirationTime), + ); +} + +function commitRootImpl(root, expirationTime) { + flushPassiveEffects(); + flushRenderPhaseStrictModeWarningsInDEV(); + + invariant( + workPhase !== RenderPhase && workPhase !== CommitPhase, + 'Should not already be working.', + ); + const finishedWork = root.current.alternate; + invariant(finishedWork !== null, 'Should have a work-in-progress root.'); + + startCommitTimer(); + + // Update the first and last pending times on this root. The new first + // pending time is whatever is left on the root fiber. + const updateExpirationTimeBeforeCommit = finishedWork.expirationTime; + const childExpirationTimeBeforeCommit = finishedWork.childExpirationTime; + const firstPendingTimeBeforeCommit = + childExpirationTimeBeforeCommit > updateExpirationTimeBeforeCommit + ? childExpirationTimeBeforeCommit + : updateExpirationTimeBeforeCommit; + root.firstPendingTime = firstPendingTimeBeforeCommit; + if (firstPendingTimeBeforeCommit < root.lastPendingTime) { + // This usually means we've finished all the work, but it can also happen + // when something gets downprioritized during render, like a hidden tree. + root.lastPendingTime = firstPendingTimeBeforeCommit; + } + + if (root === workInProgressRoot) { + // We can reset these now that they are finished. + workInProgressRoot = null; + workInProgress = null; + renderExpirationTime = NoWork; + } else { + // This indicates that the last root we worked on is not the same one that + // we're committing now. This most commonly happens when a suspended root + // times out. + } + + // Get the list of effects. + let firstEffect; + if (finishedWork.effectTag > PerformedWork) { + // A fiber's effect list consists only of its children, not itself. So if + // the root has an effect, we need to add it to the end of the list. The + // resulting list is the set that would belong to the root's parent, if it + // had one; that is, all the effects in the tree including the root. + if (finishedWork.lastEffect !== null) { + finishedWork.lastEffect.nextEffect = finishedWork; + firstEffect = finishedWork.firstEffect; + } else { + firstEffect = finishedWork; + } + } else { + // There is no effect on the root. + firstEffect = finishedWork.firstEffect; + } + + if (firstEffect !== null) { + const prevWorkPhase = workPhase; + workPhase = CommitPhase; + let prevInteractions: Set | null = null; + if (enableSchedulerTracing) { + prevInteractions = __interactionsRef.current; + __interactionsRef.current = root.memoizedInteractions; + } + + // Reset this to null before calling lifecycles + ReactCurrentOwner.current = null; + + // The commit phase is broken into several sub-phases. We do a separate pass + // of the effect list for each phase: all mutation effects come before all + // layout effects, and so on. + + // The first phase a "before mutation" phase. We use this phase to read the + // state of the host tree right before we mutate it. This is where + // getSnapshotBeforeUpdate is called. + startCommitSnapshotEffectsTimer(); + nextEffect = firstEffect; + do { + try { + prepareForCommit(root.containerInfo); + commitBeforeMutationEffects(); + } catch (error) { + invariant(nextEffect !== null, 'Should be working on an effect.'); + captureCommitPhaseError(nextEffect, error); + nextEffect = nextEffect.nextEffect; + } + } while (nextEffect !== null); + stopCommitSnapshotEffectsTimer(); + + if (enableProfilerTimer) { + // Mark the current commit time to be shared by all Profilers in this + // batch. This enables them to be grouped later. + recordCommitTime(); + } + + // The next phase is the mutation phase, where we mutate the host tree. + startCommitHostEffectsTimer(); + nextEffect = firstEffect; + do { + try { + commitMutationEffects(); + resetAfterCommit(root.containerInfo); + } catch (error) { + invariant(nextEffect !== null, 'Should be working on an effect.'); + captureCommitPhaseError(nextEffect, error); + nextEffect = nextEffect.nextEffect; + } + } while (nextEffect !== null); + stopCommitHostEffectsTimer(); + + // The work-in-progress tree is now the current tree. This must come after + // the mutation phase, so that the previous tree is still current during + // componentWillUnmount, but before the layout phase, so that the finished + // work is current during componentDidMount/Update. + root.current = finishedWork; + + // The next phase is the layout phase, where we call effects that read + // the host tree after it's been mutated. The idiomatic use case for this is + // layout, but class component lifecycles also fire here for legacy reasons. + startCommitLifeCyclesTimer(); + nextEffect = firstEffect; + do { + try { + commitLayoutEffects(root, expirationTime); + } catch (error) { + invariant(nextEffect !== null, 'Should be working on an effect.'); + captureCommitPhaseError(nextEffect, error); + nextEffect = nextEffect.nextEffect; + } + } while (nextEffect !== null); + stopCommitLifeCyclesTimer(); + + nextEffect = null; + + if (enableSchedulerTracing) { + __interactionsRef.current = prevInteractions; + } + workPhase = prevWorkPhase; + } else { + // No effects. + root.current = finishedWork; + // Measure these anyway so the flamegraph explicitly shows that there were + // no effects. + // TODO: Maybe there's a better way to report this. + startCommitSnapshotEffectsTimer(); + stopCommitSnapshotEffectsTimer(); + if (enableProfilerTimer) { + recordCommitTime(); + } + startCommitHostEffectsTimer(); + stopCommitHostEffectsTimer(); + startCommitLifeCyclesTimer(); + stopCommitLifeCyclesTimer(); + } + + stopCommitTimer(); + + if (rootDoesHavePassiveEffects) { + rootDoesHavePassiveEffects = false; + rootWithPendingPassiveEffects = root; + schedulePassiveEffectCallback(); + } + + // If there's still remaining work in the children, make sure we've scheduled + // a callback for it. We don't need to check if the root received an update, + // because we would have already scheduled a callback for it via + // scheduleUpdateOnFiber. This is only needed when a tree is deprioritized. + const remainingChildExpirationTime = finishedWork.childExpirationTime; + if (remainingChildExpirationTime !== NoWork) { + const currentTime = requestCurrentTime(); + const priorityLevel = inferPriorityFromExpirationTime( + currentTime, + remainingChildExpirationTime, + ); + scheduleCallbackForRoot(root, priorityLevel, remainingChildExpirationTime); + } else { + // If there's no remaining work, we can clear the set of already failed + // error boundaries. + legacyErrorBoundariesThatAlreadyFailed = null; + } + + if (enableSchedulerTracing) { + finishPendingInteractions(root, expirationTime); + } + + if (remainingChildExpirationTime === Sync) { + // Count the number of times the root synchronously re-renders without + // finishing. If there are too many, it indicates an infinite update loop. + if (root === rootWithNestedUpdates) { + nestedUpdateCount++; + } else { + nestedUpdateCount = 0; + rootWithNestedUpdates = root; + } + } else { + nestedUpdateCount = 0; + } + + if (hasUncaughtError) { + hasUncaughtError = false; + const error = firstUncaughtError; + firstUncaughtError = null; + throw error; + } + + // If layout work was scheduled, flush it now. + flushImmediateQueue(); +} + +function commitBeforeMutationEffects() { + while (nextEffect !== null) { + if ((nextEffect.effectTag & Snapshot) !== NoEffect) { + setCurrentDebugFiberInDEV(nextEffect); + recordEffect(); + + const current = nextEffect.alternate; + commitBeforeMutationEffectOnFiber(current, nextEffect); + + resetCurrentDebugFiberInDEV(); + } + nextEffect = nextEffect.nextEffect; + } +} + +function commitMutationEffects() { + // TODO: Should probably move the bulk of this function to commitWork. + while (nextEffect !== null) { + setCurrentDebugFiberInDEV(nextEffect); + + const effectTag = nextEffect.effectTag; + + if (effectTag & ContentReset) { + commitResetTextContent(nextEffect); + } + + if (effectTag & Ref) { + const current = nextEffect.alternate; + if (current !== null) { + commitDetachRef(current); + } + } + + // The following switch statement is only concerned about placement, + // updates, and deletions. To avoid needing to add a case for every possible + // bitmap value, we remove the secondary effects from the effect tag and + // switch on that value. + let primaryEffectTag = effectTag & (Placement | Update | Deletion); + switch (primaryEffectTag) { + case Placement: { + commitPlacement(nextEffect); + // Clear the "placement" from effect tag so that we know that this is + // inserted, before any life-cycles like componentDidMount gets called. + // TODO: findDOMNode doesn't rely on this any more but isMounted does + // and isMounted is deprecated anyway so we should be able to kill this. + nextEffect.effectTag &= ~Placement; + break; + } + case PlacementAndUpdate: { + // Placement + commitPlacement(nextEffect); + // Clear the "placement" from effect tag so that we know that this is + // inserted, before any life-cycles like componentDidMount gets called. + nextEffect.effectTag &= ~Placement; + + // Update + const current = nextEffect.alternate; + commitWork(current, nextEffect); + break; + } + case Update: { + const current = nextEffect.alternate; + commitWork(current, nextEffect); + break; + } + case Deletion: { + commitDeletion(nextEffect); + break; + } + } + + // TODO: Only record a mutation effect if primaryEffectTag is non-zero. + recordEffect(); + + resetCurrentDebugFiberInDEV(); + nextEffect = nextEffect.nextEffect; + } +} + +function commitLayoutEffects( + root: FiberRoot, + committedExpirationTime: ExpirationTime, +) { + // TODO: Should probably move the bulk of this function to commitWork. + while (nextEffect !== null) { + setCurrentDebugFiberInDEV(nextEffect); + + const effectTag = nextEffect.effectTag; + + if (effectTag & (Update | Callback)) { + recordEffect(); + const current = nextEffect.alternate; + commitLayoutEffectOnFiber( + root, + current, + nextEffect, + committedExpirationTime, + ); + } + + if (effectTag & Ref) { + recordEffect(); + commitAttachRef(nextEffect); + } + + if (effectTag & Passive) { + rootDoesHavePassiveEffects = true; + } + + resetCurrentDebugFiberInDEV(); + nextEffect = nextEffect.nextEffect; + } +} + +function schedulePassiveEffectCallback() { + scheduleCallback(NormalPriority, flushPassiveEffects); +} + +export function flushPassiveEffects() { + if (rootWithPendingPassiveEffects === null) { + return; + } + const root = rootWithPendingPassiveEffects; + rootWithPendingPassiveEffects = null; + + invariant( + workPhase !== RenderPhase && workPhase !== CommitPhase, + 'Cannot flush passive effects while already rendering.', + ); + const prevWorkPhase = workPhase; + workPhase = CommitPhase; + + // Note: This currently assumes there are no passive effects on the root + // fiber, because the root is not part of its own effect list. This could + // change in the future. + let effect = root.current.firstEffect; + while (effect !== null) { + setCurrentDebugFiberInDEV(effect); + + try { + commitPassiveHookEffects(effect); + } catch (error) { + invariant(effect !== null, 'Should be working on an effect.'); + captureCommitPhaseError(effect, error); + } + + resetCurrentDebugFiberInDEV(); + effect = effect.nextEffect; + } + + workPhase = prevWorkPhase; + flushImmediateQueue(); +} + +export function isAlreadyFailedLegacyErrorBoundary(instance: mixed): boolean { + return ( + legacyErrorBoundariesThatAlreadyFailed !== null && + legacyErrorBoundariesThatAlreadyFailed.has(instance) + ); +} + +export function markLegacyErrorBoundaryAsFailed(instance: mixed) { + if (legacyErrorBoundariesThatAlreadyFailed === null) { + legacyErrorBoundariesThatAlreadyFailed = new Set([instance]); + } else { + legacyErrorBoundariesThatAlreadyFailed.add(instance); + } +} + +function prepareToThrowUncaughtError(error: mixed) { + if (!hasUncaughtError) { + hasUncaughtError = true; + firstUncaughtError = error; + } +} +export const onUncaughtError = prepareToThrowUncaughtError; + +function captureCommitPhaseErrorOnRoot( + rootFiber: Fiber, + sourceFiber: Fiber, + error: mixed, +) { + const errorInfo = createCapturedValue(error, sourceFiber); + const update = createRootErrorUpdate(rootFiber, errorInfo, Sync); + enqueueUpdate(rootFiber, update); + const root = markUpdateTimeFromFiberToRoot(rootFiber, Sync); + if (root !== null) { + scheduleCallbackForRoot(root, ImmediatePriority, Sync); + } +} + +export function captureCommitPhaseError(sourceFiber: Fiber, error: mixed) { + if (sourceFiber.tag === HostRoot) { + // Error was thrown at the root. There is no parent, so the root + // itself should capture it. + captureCommitPhaseErrorOnRoot(sourceFiber, sourceFiber, error); + return; + } + + let fiber = sourceFiber.return; + while (fiber !== null) { + if (fiber.tag === HostRoot) { + captureCommitPhaseErrorOnRoot(fiber, sourceFiber, error); + return; + } else if (fiber.tag === ClassComponent) { + const ctor = fiber.type; + const instance = fiber.stateNode; + if ( + typeof ctor.getDerivedStateFromError === 'function' || + (typeof instance.componentDidCatch === 'function' && + !isAlreadyFailedLegacyErrorBoundary(instance)) + ) { + const errorInfo = createCapturedValue(error, sourceFiber); + const update = createClassErrorUpdate( + fiber, + errorInfo, + // TODO: This is always sync + Sync, + ); + enqueueUpdate(fiber, update); + const root = markUpdateTimeFromFiberToRoot(fiber, Sync); + if (root !== null) { + scheduleCallbackForRoot(root, ImmediatePriority, Sync); + } + return; + } + } + fiber = fiber.return; + } +} + +export function pingSuspendedRoot( + root: FiberRoot, + thenable: Thenable, + suspendedTime: ExpirationTime, +) { + const pingCache = root.pingCache; + if (pingCache !== null) { + // The thenable resolved, so we no longer need to memoize, because it will + // never be thrown again. + pingCache.delete(thenable); + } + + if (workInProgressRoot === root && renderExpirationTime === suspendedTime) { + // Received a ping at the same priority level at which we're currently + // rendering. Restart from the root. Don't need to schedule a ping because + // we're already working on this tree. + prepareFreshStack(root, renderExpirationTime); + return; + } + + const lastPendingTime = root.lastPendingTime; + if (lastPendingTime < suspendedTime) { + // The root is no longer suspended at this time. + return; + } + + const pingTime = root.pingTime; + if (pingTime !== NoWork && pingTime < suspendedTime) { + // There's already a lower priority ping scheduled. + return; + } + + // Mark the time at which this ping was scheduled. + root.pingTime = suspendedTime; + + const priorityLevel = inferPriorityFromExpirationTime(suspendedTime); + scheduleCallbackForRoot(root, priorityLevel, suspendedTime); +} + +export function retryTimedOutBoundary(boundaryFiber: Fiber) { + // The boundary fiber (a Suspense component) previously timed out and was + // rendered in its fallback state. One of the promises that suspended it has + // resolved, which means at least part of the tree was likely unblocked. Try + // rendering again, at a new expiration time. + const currentTime = requestCurrentTime(); + const retryTime = computeExpirationForFiber(currentTime, boundaryFiber); + // TODO: Special case idle priority? + const priorityLevel = inferPriorityFromExpirationTime(currentTime, retryTime); + const root = markUpdateTimeFromFiberToRoot(boundaryFiber, retryTime); + if (root !== null) { + scheduleCallbackForRoot(root, priorityLevel, retryTime); + } +} + +export function resolveRetryThenable(boundaryFiber: Fiber, thenable: Thenable) { + let retryCache: WeakSet | Set | null; + if (enableSuspenseServerRenderer) { + switch (boundaryFiber.tag) { + case SuspenseComponent: + retryCache = boundaryFiber.stateNode; + break; + case DehydratedSuspenseComponent: + retryCache = boundaryFiber.memoizedState; + break; + default: + invariant( + false, + 'Pinged unknown suspense boundary type. ' + + 'This is probably a bug in React.', + ); + } + } else { + retryCache = boundaryFiber.stateNode; + } + + if (retryCache !== null) { + // The thenable resolved, so we no longer need to memoize, because it will + // never be thrown again. + retryCache.delete(thenable); + } + + retryTimedOutBoundary(boundaryFiber); +} + +export function inferStartTimeFromExpirationTime( + root: FiberRoot, + expirationTime: ExpirationTime, +) { + // We don't know exactly when the update was scheduled, but we can infer an + // approximate start time from the expiration time. + const earliestExpirationTimeMs = expirationTimeToMs(root.firstPendingTime); + // TODO: Track this on the root instead. It's more accurate, doesn't rely on + // assumptions about priority, and isn't coupled to Scheduler details. + return earliestExpirationTimeMs - LOW_PRIORITY_EXPIRATION; +} + +function computeMsUntilTimeout(root, absoluteTimeoutMs) { + if (disableYielding) { + // Timeout immediately when yielding is disabled. + return 0; + } + + // Find the earliest uncommitted expiration time in the tree, including + // work that is suspended. The timeout threshold cannot be longer than + // the overall expiration. + const earliestExpirationTimeMs = expirationTimeToMs(root.firstPendingTime); + if (earliestExpirationTimeMs < absoluteTimeoutMs) { + absoluteTimeoutMs = earliestExpirationTimeMs; + } + + // Subtract the current time from the absolute timeout to get the number + // of milliseconds until the timeout. In other words, convert an absolute + // timestamp to a relative time. This is the value that is passed + // to `setTimeout`. + let msUntilTimeout = absoluteTimeoutMs - now(); + return msUntilTimeout < 0 ? 0 : msUntilTimeout; +} + +function flushRenderPhaseStrictModeWarningsInDEV() { + if (__DEV__) { + ReactStrictModeWarnings.flushPendingUnsafeLifecycleWarnings(); + ReactStrictModeWarnings.flushLegacyContextWarning(); + + if (warnAboutDeprecatedLifecycles) { + ReactStrictModeWarnings.flushPendingDeprecationWarnings(); + } + } +} + +function stopFinishedWorkLoopTimer() { + const didCompleteRoot = true; + stopWorkLoopTimer(interruptedBy, didCompleteRoot); + interruptedBy = null; +} + +function stopInterruptedWorkLoopTimer() { + // TODO: Track which fiber caused the interruption. + const didCompleteRoot = false; + stopWorkLoopTimer(interruptedBy, didCompleteRoot); + interruptedBy = null; +} + +function checkForInterruption( + fiberThatReceivedUpdate: Fiber, + updateExpirationTime: ExpirationTime, +) { + if ( + enableUserTimingAPI && + workInProgressRoot !== null && + updateExpirationTime > renderExpirationTime + ) { + interruptedBy = fiberThatReceivedUpdate; + } +} + +let didWarnStateUpdateForUnmountedComponent: Set | null = null; +function warnAboutUpdateOnUnmountedFiberInDEV(fiber) { + if (__DEV__) { + const tag = fiber.tag; + if ( + tag !== HostRoot && + tag !== ClassComponent && + tag !== FunctionComponent && + tag !== ForwardRef && + tag !== MemoComponent && + tag !== SimpleMemoComponent + ) { + // Only warn for user-defined components, not internal ones like Suspense. + return; + } + // We show the whole stack but dedupe on the top component's name because + // the problematic code almost always lies inside that component. + const componentName = getComponentName(fiber.type) || 'ReactComponent'; + if (didWarnStateUpdateForUnmountedComponent !== null) { + if (didWarnStateUpdateForUnmountedComponent.has(componentName)) { + return; + } + didWarnStateUpdateForUnmountedComponent.add(componentName); + } else { + didWarnStateUpdateForUnmountedComponent = new Set([componentName]); + } + warningWithoutStack( + false, + "Can't perform a React state update on an unmounted component. This " + + 'is a no-op, but it indicates a memory leak in your application. To ' + + 'fix, cancel all subscriptions and asynchronous tasks in %s.%s', + tag === ClassComponent + ? 'the componentWillUnmount method' + : 'a useEffect cleanup function', + getStackByFiberInDevAndProd(fiber), + ); + didWarnStateUpdateForUnmountedComponent[componentName] = true; + } +} + +let beginWork; +if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) { + let dummyFiber = null; + beginWork = (current, unitOfWork, expirationTime) => { + // If a component throws an error, we replay it again in a synchronously + // dispatched event, so that the debugger will treat it as an uncaught + // error See ReactErrorUtils for more information. + + // Before entering the begin phase, copy the work-in-progress onto a dummy + // fiber. If beginWork throws, we'll use this to reset the state. + const originalWorkInProgressCopy = assignFiberPropertiesInDEV( + dummyFiber, + unitOfWork, + ); + try { + return originalBeginWork(current, unitOfWork, expirationTime); + } catch (originalError) { + if ( + originalError !== null && + typeof originalError === 'object' && + typeof originalError.then === 'function' + ) { + // Don't replay promises. Treat everything else like an error. + throw originalError; + } + + // Keep this code in sync with renderRoot; any changes here must have + // corresponding changes there. + resetContextDependencies(); + resetHooks(); + + // Unwind the failed stack frame + unwindInterruptedWork(unitOfWork); + + // Restore the original properties of the fiber. + assignFiberPropertiesInDEV(unitOfWork, originalWorkInProgressCopy); + + if (enableProfilerTimer && workInProgress.mode & ProfileMode) { + // Reset the profiler timer. + startProfilerTimer(workInProgress); + } + + // Run beginWork again. + invokeGuardedCallback( + null, + originalBeginWork, + null, + current, + unitOfWork, + expirationTime, + ); + + if (hasCaughtError()) { + const replayError = clearCaughtError(); + // `invokeGuardedCallback` sometimes sets an expando `_suppressLogging`. + // Rethrow this error instead of the original one. + throw replayError; + } else { + // This branch is reachable if the render phase is impure. + throw originalError; + } + } + }; +} else { + beginWork = originalBeginWork; +} + +let didWarnAboutUpdateInRender = false; +let didWarnAboutUpdateInGetChildContext = false; +function warnAboutInvalidUpdatesOnClassComponentsInDEV(fiber) { + if (__DEV__) { + if (fiber.tag === ClassComponent) { + switch (ReactCurrentDebugFiberPhaseInDEV) { + case 'getChildContext': + if (didWarnAboutUpdateInGetChildContext) { + return; + } + warningWithoutStack( + false, + 'setState(...): Cannot call setState() inside getChildContext()', + ); + didWarnAboutUpdateInGetChildContext = true; + break; + case 'render': + if (didWarnAboutUpdateInRender) { + return; + } + warningWithoutStack( + false, + 'Cannot update during an existing state transition (such as ' + + 'within `render`). Render methods should be a pure function of ' + + 'props and state.', + ); + didWarnAboutUpdateInRender = true; + break; + } + } + } +} + +function warnIfNotCurrentlyBatchingInDEV(fiber: Fiber): void { + if (__DEV__) { + if (workPhase === NotWorking) { + warningWithoutStack( + false, + 'An update to %s inside a test was not wrapped in act(...).\n\n' + + 'When testing, code that causes React state updates should be ' + + 'wrapped into act(...):\n\n' + + 'act(() => {\n' + + ' /* fire events that update state */\n' + + '});\n' + + '/* assert on the output */\n\n' + + "This ensures that you're testing the behavior the user would see " + + 'in the browser.' + + ' Learn more at https://fb.me/react-wrap-tests-with-act' + + '%s', + getComponentName(fiber.type), + getStackByFiberInDevAndProd(fiber), + ); + } + } +} + +function computeThreadID(root, expirationTime) { + // Interaction threads are unique per root and expiration time. + return expirationTime * 1000 + root.interactionThreadID; +} + +function schedulePendingInteraction(root, expirationTime) { + // This is called when work is scheduled on a root. It sets up a pending + // interaction, which is completed once the work commits. + if (!enableSchedulerTracing) { + return; + } + + const interactions = __interactionsRef.current; + if (interactions.size > 0) { + const pendingInteractionMap = root.pendingInteractionMap; + const pendingInteractions = pendingInteractionMap.get(expirationTime); + if (pendingInteractions != null) { + interactions.forEach(interaction => { + if (!pendingInteractions.has(interaction)) { + // Update the pending async work count for previously unscheduled interaction. + interaction.__count++; + } + + pendingInteractions.add(interaction); + }); + } else { + pendingInteractionMap.set(expirationTime, new Set(interactions)); + + // Update the pending async work count for the current interactions. + interactions.forEach(interaction => { + interaction.__count++; + }); + } + + const subscriber = __subscriberRef.current; + if (subscriber !== null) { + const threadID = computeThreadID(root, expirationTime); + subscriber.onWorkScheduled(interactions, threadID); + } + } +} + +function startWorkOnPendingInteraction(root, expirationTime) { + // This is called when new work is started on a root. + if (!enableSchedulerTracing) { + return; + } + + // Determine which interactions this batch of work currently includes, So that + // we can accurately attribute time spent working on it, And so that cascading + // work triggered during the render phase will be associated with it. + const interactions: Set = new Set(); + root.pendingInteractionMap.forEach( + (scheduledInteractions, scheduledExpirationTime) => { + if (scheduledExpirationTime >= expirationTime) { + scheduledInteractions.forEach(interaction => + interactions.add(interaction), + ); + } + }, + ); + + // Store the current set of interactions on the FiberRoot for a few reasons: + // We can re-use it in hot functions like renderRoot() without having to + // recalculate it. We will also use it in commitWork() to pass to any Profiler + // onRender() hooks. This also provides DevTools with a way to access it when + // the onCommitRoot() hook is called. + root.memoizedInteractions = interactions; + + if (interactions.size > 0) { + const subscriber = __subscriberRef.current; + if (subscriber !== null) { + const threadID = computeThreadID(root, expirationTime); + try { + subscriber.onWorkStarted(interactions, threadID); + } catch (error) { + // If the subscriber throws, rethrow it in a separate task + scheduleCallback(ImmediatePriority, () => { + throw error; + }); + } + } + } +} + +function finishPendingInteractions(root, committedExpirationTime) { + if (!enableSchedulerTracing) { + return; + } + + const earliestRemainingTimeAfterCommit = root.firstPendingTime; + + let subscriber; + + try { + subscriber = __subscriberRef.current; + if (subscriber !== null && root.memoizedInteractions.size > 0) { + const threadID = computeThreadID(root, committedExpirationTime); + subscriber.onWorkStopped(root.memoizedInteractions, threadID); + } + } catch (error) { + // If the subscriber throws, rethrow it in a separate task + scheduleCallback(ImmediatePriority, () => { + throw error; + }); + } finally { + // Clear completed interactions from the pending Map. + // Unless the render was suspended or cascading work was scheduled, + // In which case– leave pending interactions until the subsequent render. + const pendingInteractionMap = root.pendingInteractionMap; + pendingInteractionMap.forEach( + (scheduledInteractions, scheduledExpirationTime) => { + // Only decrement the pending interaction count if we're done. + // If there's still work at the current priority, + // That indicates that we are waiting for suspense data. + if (scheduledExpirationTime > earliestRemainingTimeAfterCommit) { + pendingInteractionMap.delete(scheduledExpirationTime); + + scheduledInteractions.forEach(interaction => { + interaction.__count--; + + if (subscriber !== null && interaction.__count === 0) { + try { + subscriber.onInteractionScheduledWorkCompleted(interaction); + } catch (error) { + // If the subscriber throws, rethrow it in a separate task + scheduleCallback(ImmediatePriority, () => { + throw error; + }); + } + } + }); + } + }, + ); + } +} + +export const warnIfNotCurrentlyBatchingInDev = warnIfNotCurrentlyBatchingInDEV; diff --git a/packages/react-reconciler/src/ReactFiberScheduler.old.js b/packages/react-reconciler/src/ReactFiberScheduler.old.js index 887305457f1d7..b582ea2a28269 100644 --- a/packages/react-reconciler/src/ReactFiberScheduler.old.js +++ b/packages/react-reconciler/src/ReactFiberScheduler.old.js @@ -123,6 +123,7 @@ import { expirationTimeToMs, computeAsyncExpiration, computeInteractiveExpiration, + LOW_PRIORITY_EXPIRATION, } from './ReactFiberExpirationTime'; import {ConcurrentMode, ProfileMode} from './ReactTypeOfMode'; import {enqueueUpdate, resetCurrentlyProcessingQueue} from './ReactUpdateQueue'; @@ -173,6 +174,8 @@ const { unstable_cancelCallback: cancelCallback, unstable_shouldYield: shouldYield, unstable_now: now, + unstable_getCurrentPriorityLevel: getCurrentPriorityLevel, + unstable_NormalPriority: NormalPriority, } = Scheduler; export type Thenable = { @@ -820,7 +823,7 @@ function commitRoot(root: FiberRoot, finishedWork: Fiber): void { // here because that code is still in flux. callback = Scheduler_tracing_wrap(callback); } - passiveEffectCallbackHandle = scheduleCallback(callback); + passiveEffectCallbackHandle = scheduleCallback(NormalPriority, callback); passiveEffectCallback = callback; } @@ -1671,6 +1674,25 @@ function renderDidError() { nextRenderDidError = true; } +function inferStartTimeFromExpirationTime( + root: FiberRoot, + expirationTime: ExpirationTime, +) { + // We don't know exactly when the update was scheduled, but we can infer an + // approximate start time from the expiration time. First, find the earliest + // uncommitted expiration time in the tree, including work that is suspended. + // Then subtract the offset used to compute an async update's expiration time. + // This will cause high priority (interactive) work to expire earlier than + // necessary, but we can account for this by adjusting for the Just + // Noticeable Difference. + const earliestExpirationTime = findEarliestOutstandingPriorityLevel( + root, + expirationTime, + ); + const earliestExpirationTimeMs = expirationTimeToMs(earliestExpirationTime); + return earliestExpirationTimeMs - LOW_PRIORITY_EXPIRATION; +} + function pingSuspendedRoot( root: FiberRoot, thenable: Thenable, @@ -2027,7 +2049,8 @@ function scheduleCallbackWithExpirationTime( const currentMs = now() - originalStartTimeMs; const expirationTimeMs = expirationTimeToMs(expirationTime); const timeout = expirationTimeMs - currentMs; - callbackID = scheduleCallback(performAsyncWork, {timeout}); + const priorityLevel = getCurrentPriorityLevel(); + callbackID = scheduleCallback(priorityLevel, performAsyncWork, {timeout}); } // For every call to renderRoot, one of onFatal, onComplete, onSuspend, and @@ -2660,7 +2683,6 @@ export { markLegacyErrorBoundaryAsFailed, isAlreadyFailedLegacyErrorBoundary, scheduleWork, - requestWork, flushRoot, batchedUpdates, unbatchedUpdates, @@ -2672,4 +2694,5 @@ export { flushInteractiveUpdates, computeUniqueAsyncExpiration, flushPassiveEffects, + inferStartTimeFromExpirationTime, }; diff --git a/packages/react-reconciler/src/ReactFiberUnwindWork.js b/packages/react-reconciler/src/ReactFiberUnwindWork.js index 2d76a3b6b5307..8d941c10c5577 100644 --- a/packages/react-reconciler/src/ReactFiberUnwindWork.js +++ b/packages/react-reconciler/src/ReactFiberUnwindWork.js @@ -67,16 +67,12 @@ import { isAlreadyFailedLegacyErrorBoundary, pingSuspendedRoot, resolveRetryThenable, + inferStartTimeFromExpirationTime, } from './ReactFiberScheduler'; import invariant from 'shared/invariant'; import maxSigned31BitInt from './maxSigned31BitInt'; -import { - Sync, - expirationTimeToMs, - LOW_PRIORITY_EXPIRATION, -} from './ReactFiberExpirationTime'; -import {findEarliestOutstandingPriorityLevel} from './ReactFiberPendingPriority'; +import {Sync, expirationTimeToMs} from './ReactFiberExpirationTime'; const PossiblyWeakSet = typeof WeakSet === 'function' ? WeakSet : Set; const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map; @@ -323,21 +319,12 @@ function throwException( if (startTimeMs === -1) { // This suspend happened outside of any already timed-out // placeholders. We don't know exactly when the update was - // scheduled, but we can infer an approximate start time from the - // expiration time. First, find the earliest uncommitted expiration - // time in the tree, including work that is suspended. Then subtract - // the offset used to compute an async update's expiration time. - // This will cause high priority (interactive) work to expire - // earlier than necessary, but we can account for this by adjusting - // for the Just Noticeable Difference. - const earliestExpirationTime = findEarliestOutstandingPriorityLevel( + // scheduled, but we can infer an approximate start time based on + // the expiration time and the priority. + startTimeMs = inferStartTimeFromExpirationTime( root, renderExpirationTime, ); - const earliestExpirationTimeMs = expirationTimeToMs( - earliestExpirationTime, - ); - startTimeMs = earliestExpirationTimeMs - LOW_PRIORITY_EXPIRATION; } absoluteTimeoutMs = startTimeMs + earliestTimeoutMs; } diff --git a/packages/react-reconciler/src/SchedulerWithReactIntegration.js b/packages/react-reconciler/src/SchedulerWithReactIntegration.js new file mode 100644 index 0000000000000..2b05cb2be3269 --- /dev/null +++ b/packages/react-reconciler/src/SchedulerWithReactIntegration.js @@ -0,0 +1,166 @@ +/** + * 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 + */ + +// Intentionally not named imports because Rollup would use dynamic dispatch for +// CommonJS interop named imports. +import * as Scheduler from 'scheduler'; + +import {disableYielding} from 'shared/ReactFeatureFlags'; +import invariant from 'shared/invariant'; + +const { + unstable_runWithPriority: Scheduler_runWithPriority, + unstable_scheduleCallback: Scheduler_scheduleCallback, + unstable_cancelCallback: Scheduler_cancelCallback, + unstable_shouldYield: Scheduler_shouldYield, + unstable_now: Scheduler_now, + unstable_getCurrentPriorityLevel: Scheduler_getCurrentPriorityLevel, + unstable_ImmediatePriority: Scheduler_ImmediatePriority, + unstable_UserBlockingPriority: Scheduler_UserBlockingPriority, + unstable_NormalPriority: Scheduler_NormalPriority, + unstable_LowPriority: Scheduler_LowPriority, + unstable_IdlePriority: Scheduler_IdlePriority, +} = Scheduler; + +export opaque type ReactPriorityLevel = 99 | 98 | 97 | 96 | 95 | 90; +type Callback = (isSync: boolean) => Callback | void; + +const fakeCallbackNode = {}; + +// 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: ReactPriorityLevel = 99; +export const UserBlockingPriority: ReactPriorityLevel = 98; +export const NormalPriority: ReactPriorityLevel = 97; +export const LowPriority: ReactPriorityLevel = 96; +export const IdlePriority: ReactPriorityLevel = 95; +// NoPriority is the absence of priority. Also React-only. +export const NoPriority: ReactPriorityLevel = 90; + +export const now = Scheduler_now; +export const shouldYield = disableYielding + ? () => false // Never yield when `disableYielding` is on + : Scheduler_shouldYield; + +let immediateQueue: Array | null = null; +let immediateQueueCallbackNode: mixed | null = null; +let isFlushingImmediate: boolean = false; + +export function getCurrentPriorityLevel(): ReactPriorityLevel { + 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: + invariant(false, 'Unknown priority level.'); + } +} + +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: + invariant(false, 'Unknown priority level.'); + } +} + +export function runWithPriority( + reactPriorityLevel: ReactPriorityLevel, + fn: () => T, +): T { + const priorityLevel = reactPriorityToSchedulerPriority(reactPriorityLevel); + return Scheduler_runWithPriority(priorityLevel, fn); +} + +export function scheduleCallback( + reactPriorityLevel: ReactPriorityLevel, + callback: Callback, +) { + if (reactPriorityLevel === ImmediatePriority) { + // Push this callback into an internal queue. We'll flush these either in + // the next tick, or earlier if something calls `flushImmediateQueue`. + if (immediateQueue === null) { + immediateQueue = [callback]; + // Flush the queue in the next tick, at the earliest. + immediateQueueCallbackNode = Scheduler_scheduleCallback( + Scheduler_ImmediatePriority, + flushImmediateQueueImpl, + ); + } else { + // Push onto existing queue. Don't need to schedule a callback because + // we already scheduled one when we created the queue. + immediateQueue.push(callback); + } + return fakeCallbackNode; + } + // Otherwise pass through to Scheduler. + const priorityLevel = reactPriorityToSchedulerPriority(reactPriorityLevel); + return Scheduler_scheduleCallback(priorityLevel, callback); +} + +export function cancelCallback(callbackNode: mixed) { + if (callbackNode !== fakeCallbackNode) { + Scheduler_cancelCallback(callbackNode); + } +} + +export function flushImmediateQueue() { + if (immediateQueueCallbackNode !== null) { + Scheduler_cancelCallback(immediateQueueCallbackNode); + } + flushImmediateQueueImpl(); +} + +function flushImmediateQueueImpl() { + if (!isFlushingImmediate && immediateQueue !== null) { + // Prevent re-entrancy. + isFlushingImmediate = true; + let i = 0; + try { + const isSync = true; + for (; i < immediateQueue.length; i++) { + let callback = immediateQueue[i]; + do { + callback = callback(isSync); + } while (callback !== undefined); + } + immediateQueue = null; + } catch (error) { + // If something throws, leave the remaining callbacks on the queue. + if (immediateQueue !== null) { + immediateQueue = immediateQueue.slice(i + 1); + } + // Resume flushing in the next tick + Scheduler_scheduleCallback( + Scheduler_ImmediatePriority, + flushImmediateQueue, + ); + throw error; + } finally { + isFlushingImmediate = false; + } + } +} diff --git a/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js b/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js index d1f360592b4b7..06d055f0ee960 100644 --- a/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js @@ -15,6 +15,7 @@ let ReactFeatureFlags; let React; let ReactNoop; let Scheduler; +let enableNewScheduler; describe('ReactIncrementalErrorHandling', () => { beforeEach(() => { @@ -22,6 +23,7 @@ describe('ReactIncrementalErrorHandling', () => { ReactFeatureFlags = require('shared/ReactFeatureFlags'); ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false; ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false; + enableNewScheduler = ReactFeatureFlags.enableNewScheduler; PropTypes = require('prop-types'); React = require('react'); ReactNoop = require('react-noop-renderer'); @@ -131,6 +133,12 @@ describe('ReactIncrementalErrorHandling', () => { 'ErrorBoundary (catch)', 'ErrorMessage', ]); + + if (enableNewScheduler) { + expect(ReactNoop.getChildren()).toEqual([]); + Scheduler.flushAll(); + } + expect(ReactNoop.getChildren()).toEqual([span('Caught an error: oops!')]); }); @@ -306,21 +314,17 @@ describe('ReactIncrementalErrorHandling', () => { }); it('retries one more time before handling error', () => { - let ops = []; function BadRender() { - ops.push('BadRender'); Scheduler.yieldValue('BadRender'); throw new Error('oops'); } function Sibling() { - ops.push('Sibling'); Scheduler.yieldValue('Sibling'); return ; } function Parent() { - ops.push('Parent'); Scheduler.yieldValue('Parent'); return ( @@ -339,9 +343,23 @@ describe('ReactIncrementalErrorHandling', () => { expect(Scheduler).toFlushAndYieldThrough(['Sibling']); // React retries once, synchronously, before throwing. - ops = []; - expect(() => ReactNoop.flushNextYield()).toThrow('oops'); - expect(ops).toEqual(['Parent', 'BadRender', 'Sibling']); + if (enableNewScheduler) { + // New scheduler yields in between render and commit + Scheduler.unstable_flushNumberOfYields(1); + expect(Scheduler).toHaveYielded(['Parent', 'BadRender', 'Sibling']); + // Error is thrown during commit + expect(() => Scheduler.flushAll()).toThrow('oops'); + } else { + // Old scheduler renders, commits, and throws synchronously + expect(() => Scheduler.unstable_flushNumberOfYields(1)).toThrow('oops'); + expect(Scheduler).toHaveYielded([ + 'Parent', + 'BadRender', + 'Sibling', + 'commit', + ]); + } + expect(ReactNoop.getChildren()).toEqual([]); }); // TODO: This is currently unobservable, but will be once we lift renderRoot @@ -744,7 +762,8 @@ describe('ReactIncrementalErrorHandling', () => { expect(ReactNoop.getChildren()).toEqual([span('a:5')]); }); - it('applies sync updates regardless despite errors in scheduling', () => { + // TODO: Is this a breaking change? + it('defers additional sync work to a separate event after an error', () => { ReactNoop.render(); expect(() => { ReactNoop.flushSync(() => { @@ -755,6 +774,7 @@ describe('ReactIncrementalErrorHandling', () => { }); }); }).toThrow('Hello'); + Scheduler.flushAll(); expect(ReactNoop.getChildren()).toEqual([span('a:3')]); }); @@ -962,43 +982,46 @@ describe('ReactIncrementalErrorHandling', () => { expect(ReactNoop.getChildren('a')).toEqual([ span('Caught an error: Hello.'), ]); + expect(Scheduler).toFlushWithoutYielding(); expect(ReactNoop.getChildren('b')).toEqual([span('b:1')]); }); it('continues work on other roots despite uncaught errors', () => { function BrokenRender(props) { - throw new Error('Hello'); + throw new Error(props.label); } - ReactNoop.renderToRootWithID(, 'a'); + ReactNoop.renderToRootWithID(, 'a'); expect(() => { expect(Scheduler).toFlushWithoutYielding(); - }).toThrow('Hello'); + }).toThrow('a'); expect(ReactNoop.getChildren('a')).toEqual([]); - ReactNoop.renderToRootWithID(, 'a'); + ReactNoop.renderToRootWithID(, 'a'); ReactNoop.renderToRootWithID(, 'b'); expect(() => { expect(Scheduler).toFlushWithoutYielding(); - }).toThrow('Hello'); + }).toThrow('a'); + expect(Scheduler).toFlushWithoutYielding(); expect(ReactNoop.getChildren('a')).toEqual([]); expect(ReactNoop.getChildren('b')).toEqual([span('b:2')]); ReactNoop.renderToRootWithID(, 'a'); - ReactNoop.renderToRootWithID(, 'b'); + ReactNoop.renderToRootWithID(, 'b'); expect(() => { expect(Scheduler).toFlushWithoutYielding(); - }).toThrow('Hello'); + }).toThrow('b'); expect(ReactNoop.getChildren('a')).toEqual([span('a:3')]); expect(ReactNoop.getChildren('b')).toEqual([]); ReactNoop.renderToRootWithID(, 'a'); - ReactNoop.renderToRootWithID(, 'b'); + ReactNoop.renderToRootWithID(, 'b'); ReactNoop.renderToRootWithID(, 'c'); expect(() => { expect(Scheduler).toFlushWithoutYielding(); - }).toThrow('Hello'); + }).toThrow('b'); + expect(Scheduler).toFlushWithoutYielding(); expect(ReactNoop.getChildren('a')).toEqual([span('a:4')]); expect(ReactNoop.getChildren('b')).toEqual([]); expect(ReactNoop.getChildren('c')).toEqual([span('c:4')]); @@ -1007,25 +1030,43 @@ describe('ReactIncrementalErrorHandling', () => { ReactNoop.renderToRootWithID(, 'b'); ReactNoop.renderToRootWithID(, 'c'); ReactNoop.renderToRootWithID(, 'd'); - ReactNoop.renderToRootWithID(, 'e'); + ReactNoop.renderToRootWithID(, 'e'); expect(() => { expect(Scheduler).toFlushWithoutYielding(); - }).toThrow('Hello'); + }).toThrow('e'); + expect(Scheduler).toFlushWithoutYielding(); expect(ReactNoop.getChildren('a')).toEqual([span('a:5')]); expect(ReactNoop.getChildren('b')).toEqual([span('b:5')]); expect(ReactNoop.getChildren('c')).toEqual([span('c:5')]); expect(ReactNoop.getChildren('d')).toEqual([span('d:5')]); expect(ReactNoop.getChildren('e')).toEqual([]); - ReactNoop.renderToRootWithID(, 'a'); + ReactNoop.renderToRootWithID(, 'a'); ReactNoop.renderToRootWithID(, 'b'); - ReactNoop.renderToRootWithID(, 'c'); + ReactNoop.renderToRootWithID(, 'c'); ReactNoop.renderToRootWithID(, 'd'); - ReactNoop.renderToRootWithID(, 'e'); + ReactNoop.renderToRootWithID(, 'e'); ReactNoop.renderToRootWithID(, 'f'); - expect(() => { - expect(Scheduler).toFlushWithoutYielding(); - }).toThrow('Hello'); + + if (enableNewScheduler) { + // The new scheduler will throw all three errors. + expect(() => { + expect(Scheduler).toFlushWithoutYielding(); + }).toThrow('a'); + expect(() => { + expect(Scheduler).toFlushWithoutYielding(); + }).toThrow('c'); + expect(() => { + expect(Scheduler).toFlushWithoutYielding(); + }).toThrow('e'); + } else { + // The old scheduler only throws the first one. + expect(() => { + expect(Scheduler).toFlushWithoutYielding(); + }).toThrow('a'); + } + + expect(Scheduler).toFlushWithoutYielding(); expect(ReactNoop.getChildren('a')).toEqual([]); expect(ReactNoop.getChildren('b')).toEqual([span('b:6')]); expect(ReactNoop.getChildren('c')).toEqual([]); diff --git a/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js b/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js index 4e80064514006..7835ba74ce8a1 100644 --- a/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js @@ -5,6 +5,7 @@ let Scheduler; let ReactFeatureFlags; let Suspense; let lazy; +let enableNewScheduler; describe('ReactLazy', () => { beforeEach(() => { @@ -18,6 +19,7 @@ describe('ReactLazy', () => { lazy = React.lazy; ReactTestRenderer = require('react-test-renderer'); Scheduler = require('scheduler'); + enableNewScheduler = ReactFeatureFlags.enableNewScheduler; }); function Text(props) { @@ -485,19 +487,33 @@ describe('ReactLazy', () => { await Promise.resolve(); + if (enableNewScheduler) { + // The new scheduler pings in a separate task + expect(Scheduler).toHaveYielded([]); + } else { + // The old scheduler pings synchronously + expect(Scheduler).toHaveYielded(['UNSAFE_componentWillMount: A', 'A1']); + } + root.update( }> , ); - expect(Scheduler).toHaveYielded([ - 'UNSAFE_componentWillMount: A', - 'A1', - 'UNSAFE_componentWillReceiveProps: A -> A', - 'UNSAFE_componentWillUpdate: A -> A', - 'A2', - ]); - expect(Scheduler).toFlushAndYield([]); + + if (enableNewScheduler) { + // Because this ping happens in a new task, the ping and the update + // are batched together + expect(Scheduler).toHaveYielded(['UNSAFE_componentWillMount: A', 'A2']); + } else { + // The old scheduler must do two separate renders, no batching. + expect(Scheduler).toHaveYielded([ + 'UNSAFE_componentWillReceiveProps: A -> A', + 'UNSAFE_componentWillUpdate: A -> A', + 'A2', + ]); + } + expect(root).toMatchRenderedOutput('A2'); root.update( diff --git a/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js b/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js index 913cbfbd32c4b..bac780f7f9a90 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js @@ -5,6 +5,7 @@ let Scheduler; let ReactCache; let Suspense; let act; +let enableNewScheduler; let TextResource; let textResourceShouldFail; @@ -22,6 +23,7 @@ describe('ReactSuspense', () => { act = ReactTestRenderer.act; Scheduler = require('scheduler'); ReactCache = require('react-cache'); + enableNewScheduler = ReactFeatureFlags.enableNewScheduler; Suspense = React.Suspense; @@ -265,7 +267,11 @@ describe('ReactSuspense', () => { await LazyClass; - expect(Scheduler).toHaveYielded(['Hi', 'Did mount: Hi']); + if (enableNewScheduler) { + expect(Scheduler).toFlushExpired(['Hi', 'Did mount: Hi']); + } else { + expect(Scheduler).toHaveYielded(['Hi', 'Did mount: Hi']); + } expect(root).toMatchRenderedOutput('Hi'); }); @@ -395,13 +401,24 @@ describe('ReactSuspense', () => { expect(root).toMatchRenderedOutput('Loading...'); jest.advanceTimersByTime(100); - expect(Scheduler).toHaveYielded([ - 'Promise resolved [B:1]', - 'B:1', - 'Unmount [Loading...]', - // Should be a mount, not an update - 'Mount [B:1]', - ]); + + if (enableNewScheduler) { + expect(Scheduler).toHaveYielded(['Promise resolved [B:1]']); + expect(Scheduler).toFlushExpired([ + 'B:1', + 'Unmount [Loading...]', + // Should be a mount, not an update + 'Mount [B:1]', + ]); + } else { + expect(Scheduler).toHaveYielded([ + 'Promise resolved [B:1]', + 'B:1', + 'Unmount [Loading...]', + // Should be a mount, not an update + 'Mount [B:1]', + ]); + } expect(root).toMatchRenderedOutput('AB:1C'); @@ -415,12 +432,21 @@ describe('ReactSuspense', () => { jest.advanceTimersByTime(100); - expect(Scheduler).toHaveYielded([ - 'Promise resolved [B:2]', - 'B:2', - 'Unmount [Loading...]', - 'Update [B:2]', - ]); + if (enableNewScheduler) { + expect(Scheduler).toHaveYielded(['Promise resolved [B:2]']); + expect(Scheduler).toFlushExpired([ + 'B:2', + 'Unmount [Loading...]', + 'Update [B:2]', + ]); + } else { + expect(Scheduler).toHaveYielded([ + 'Promise resolved [B:2]', + 'B:2', + 'Unmount [Loading...]', + 'Update [B:2]', + ]); + } expect(root).toMatchRenderedOutput('AB:2C'); }); @@ -452,7 +478,14 @@ describe('ReactSuspense', () => { ]); jest.advanceTimersByTime(1000); - expect(Scheduler).toHaveYielded(['Promise resolved [A]', 'A']); + + if (enableNewScheduler) { + expect(Scheduler).toHaveYielded(['Promise resolved [A]']); + expect(Scheduler).toFlushExpired(['A']); + } else { + expect(Scheduler).toHaveYielded(['Promise resolved [A]', 'A']); + } + expect(root).toMatchRenderedOutput('Stateful: 1A'); root.update(); @@ -468,7 +501,14 @@ describe('ReactSuspense', () => { expect(root).toMatchRenderedOutput('Loading...'); jest.advanceTimersByTime(1000); - expect(Scheduler).toHaveYielded(['Promise resolved [B]', 'B']); + + if (enableNewScheduler) { + expect(Scheduler).toHaveYielded(['Promise resolved [B]']); + expect(Scheduler).toFlushExpired(['B']); + } else { + expect(Scheduler).toHaveYielded(['Promise resolved [B]', 'B']); + } + expect(root).toMatchRenderedOutput('Stateful: 2B'); }); @@ -508,7 +548,13 @@ describe('ReactSuspense', () => { ]); jest.advanceTimersByTime(1000); - expect(Scheduler).toHaveYielded(['Promise resolved [A]', 'A']); + + if (enableNewScheduler) { + expect(Scheduler).toHaveYielded(['Promise resolved [A]']); + expect(Scheduler).toFlushExpired(['A']); + } else { + expect(Scheduler).toHaveYielded(['Promise resolved [A]', 'A']); + } expect(root).toMatchRenderedOutput('Stateful: 1A'); root.update(); @@ -531,7 +577,14 @@ describe('ReactSuspense', () => { expect(root).toMatchRenderedOutput('Loading...'); jest.advanceTimersByTime(1000); - expect(Scheduler).toHaveYielded(['Promise resolved [B]', 'B']); + + if (enableNewScheduler) { + expect(Scheduler).toHaveYielded(['Promise resolved [B]']); + expect(Scheduler).toFlushExpired(['B']); + } else { + expect(Scheduler).toHaveYielded(['Promise resolved [B]', 'B']); + } + expect(root).toMatchRenderedOutput('Stateful: 2B'); }); @@ -612,11 +665,17 @@ describe('ReactSuspense', () => { ReactTestRenderer.create(); expect(Scheduler).toHaveYielded(['Suspend! [A]', 'Loading...']); jest.advanceTimersByTime(500); - expect(Scheduler).toHaveYielded([ - 'Promise resolved [A]', - 'A', - 'Did commit: A', - ]); + + if (enableNewScheduler) { + expect(Scheduler).toHaveYielded(['Promise resolved [A]']); + expect(Scheduler).toFlushExpired(['A', 'Did commit: A']); + } else { + expect(Scheduler).toHaveYielded([ + 'Promise resolved [A]', + 'A', + 'Did commit: A', + ]); + } }); it('retries when an update is scheduled on a timed out tree', () => { @@ -700,23 +759,42 @@ describe('ReactSuspense', () => { ]); expect(Scheduler).toFlushAndYield([]); jest.advanceTimersByTime(1000); - expect(Scheduler).toHaveYielded([ - 'Promise resolved [Child 1]', - 'Child 1', - 'Suspend! [Child 2]', - 'Suspend! [Child 3]', - ]); + if (enableNewScheduler) { + expect(Scheduler).toHaveYielded(['Promise resolved [Child 1]']); + expect(Scheduler).toFlushExpired([ + 'Child 1', + 'Suspend! [Child 2]', + 'Suspend! [Child 3]', + ]); + } else { + expect(Scheduler).toHaveYielded([ + 'Promise resolved [Child 1]', + 'Child 1', + 'Suspend! [Child 2]', + 'Suspend! [Child 3]', + ]); + } jest.advanceTimersByTime(1000); - expect(Scheduler).toHaveYielded([ - 'Promise resolved [Child 2]', - 'Child 2', - 'Suspend! [Child 3]', - ]); + if (enableNewScheduler) { + expect(Scheduler).toHaveYielded(['Promise resolved [Child 2]']); + expect(Scheduler).toFlushExpired(['Child 2', 'Suspend! [Child 3]']); + } else { + expect(Scheduler).toHaveYielded([ + 'Promise resolved [Child 2]', + 'Child 2', + 'Suspend! [Child 3]', + ]); + } jest.advanceTimersByTime(1000); - expect(Scheduler).toHaveYielded([ - 'Promise resolved [Child 3]', - 'Child 3', - ]); + if (enableNewScheduler) { + expect(Scheduler).toHaveYielded(['Promise resolved [Child 3]']); + expect(Scheduler).toFlushExpired(['Child 3']); + } else { + expect(Scheduler).toHaveYielded([ + 'Promise resolved [Child 3]', + 'Child 3', + ]); + } expect(root).toMatchRenderedOutput( ['Child 1', 'Child 2', 'Child 3'].join(''), ); @@ -775,7 +853,16 @@ describe('ReactSuspense', () => { ]); expect(root).toMatchRenderedOutput('Loading...'); jest.advanceTimersByTime(1000); - expect(Scheduler).toHaveYielded(['Promise resolved [Tab: 0]', 'Tab: 0']); + + if (enableNewScheduler) { + expect(Scheduler).toHaveYielded(['Promise resolved [Tab: 0]']); + expect(Scheduler).toFlushExpired(['Tab: 0']); + } else { + expect(Scheduler).toHaveYielded([ + 'Promise resolved [Tab: 0]', + 'Tab: 0', + ]); + } expect(root).toMatchRenderedOutput('Tab: 0 + sibling'); act(() => setTab(1)); @@ -786,7 +873,17 @@ describe('ReactSuspense', () => { ]); expect(root).toMatchRenderedOutput('Loading...'); jest.advanceTimersByTime(1000); - expect(Scheduler).toHaveYielded(['Promise resolved [Tab: 1]', 'Tab: 1']); + + if (enableNewScheduler) { + expect(Scheduler).toHaveYielded(['Promise resolved [Tab: 1]']); + expect(Scheduler).toFlushExpired(['Tab: 1']); + } else { + expect(Scheduler).toHaveYielded([ + 'Promise resolved [Tab: 1]', + 'Tab: 1', + ]); + } + expect(root).toMatchRenderedOutput('Tab: 1 + sibling'); act(() => setTab(2)); @@ -797,7 +894,17 @@ describe('ReactSuspense', () => { ]); expect(root).toMatchRenderedOutput('Loading...'); jest.advanceTimersByTime(1000); - expect(Scheduler).toHaveYielded(['Promise resolved [Tab: 2]', 'Tab: 2']); + + if (enableNewScheduler) { + expect(Scheduler).toHaveYielded(['Promise resolved [Tab: 2]']); + expect(Scheduler).toFlushExpired(['Tab: 2']); + } else { + expect(Scheduler).toHaveYielded([ + 'Promise resolved [Tab: 2]', + 'Tab: 2', + ]); + } + expect(root).toMatchRenderedOutput('Tab: 2 + sibling'); }); @@ -833,7 +940,14 @@ describe('ReactSuspense', () => { expect(Scheduler).toHaveYielded(['Suspend! [A:0]', 'Loading...']); jest.advanceTimersByTime(1000); - expect(Scheduler).toHaveYielded(['Promise resolved [A:0]', 'A:0']); + + if (enableNewScheduler) { + expect(Scheduler).toHaveYielded(['Promise resolved [A:0]']); + expect(Scheduler).toFlushExpired(['A:0']); + } else { + expect(Scheduler).toHaveYielded(['Promise resolved [A:0]', 'A:0']); + } + expect(root).toMatchRenderedOutput('A:0'); act(() => setStep(1)); @@ -870,34 +984,65 @@ describe('ReactSuspense', () => { // Resolve A jest.advanceTimersByTime(1000); - expect(Scheduler).toHaveYielded([ - 'Promise resolved [A]', - 'A', - // The promises for B and C have now been thrown twice - 'Suspend! [B]', - 'Suspend! [C]', - ]); + if (enableNewScheduler) { + expect(Scheduler).toHaveYielded(['Promise resolved [A]']); + expect(Scheduler).toFlushExpired([ + 'A', + // The promises for B and C have now been thrown twice + 'Suspend! [B]', + 'Suspend! [C]', + ]); + } else { + expect(Scheduler).toHaveYielded([ + 'Promise resolved [A]', + 'A', + // The promises for B and C have now been thrown twice + 'Suspend! [B]', + 'Suspend! [C]', + ]); + } // Resolve B jest.advanceTimersByTime(1000); - expect(Scheduler).toHaveYielded([ - 'Promise resolved [B]', - // Even though the promise for B was thrown twice, we should only - // re-render once. - 'B', - // The promise for C has now been thrown three times - 'Suspend! [C]', - ]); + if (enableNewScheduler) { + expect(Scheduler).toHaveYielded(['Promise resolved [B]']); + expect(Scheduler).toFlushExpired([ + // Even though the promise for B was thrown twice, we should only + // re-render once. + 'B', + // The promise for C has now been thrown three times + 'Suspend! [C]', + ]); + } else { + expect(Scheduler).toHaveYielded([ + 'Promise resolved [B]', + // Even though the promise for B was thrown twice, we should only + // re-render once. + 'B', + // The promise for C has now been thrown three times + 'Suspend! [C]', + ]); + } // Resolve C jest.advanceTimersByTime(1000); - expect(Scheduler).toHaveYielded([ - 'Promise resolved [C]', - // Even though the promise for C was thrown three times, we should only - // re-render once. - 'C', - ]); + + if (enableNewScheduler) { + expect(Scheduler).toHaveYielded(['Promise resolved [C]']); + expect(Scheduler).toFlushExpired([ + // Even though the promise for C was thrown three times, we should only + // re-render once. + 'C', + ]); + } else { + expect(Scheduler).toHaveYielded([ + 'Promise resolved [C]', + // Even though the promise for C was thrown three times, we should only + // re-render once. + 'C', + ]); + } }); it('#14162', () => { diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js index f438dbb2dafd4..629d4b0f0b382 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js @@ -7,6 +7,7 @@ let ReactCache; let Suspense; let StrictMode; let ConcurrentMode; +let enableNewScheduler; let TextResource; let textResourceShouldFail; @@ -28,6 +29,7 @@ describe('ReactSuspenseWithNoopRenderer', () => { Suspense = React.Suspense; StrictMode = React.StrictMode; ConcurrentMode = React.unstable_ConcurrentMode; + enableNewScheduler = ReactFeatureFlags.enableNewScheduler; TextResource = ReactCache.unstable_createResource(([text, ms = 0]) => { return new Promise((resolve, reject) => @@ -863,7 +865,16 @@ describe('ReactSuspenseWithNoopRenderer', () => { ReactNoop.expire(100); await advanceTimers(100); - expect(Scheduler).toHaveYielded(['Promise resolved [Result]', 'Result']); + + if (enableNewScheduler) { + expect(Scheduler).toHaveYielded(['Promise resolved [Result]']); + expect(Scheduler).toFlushExpired(['Result']); + } else { + expect(Scheduler).toHaveYielded([ + 'Promise resolved [Result]', + 'Result', + ]); + } expect(ReactNoop.getChildren()).toEqual([span('Result')]); }); @@ -901,15 +912,27 @@ describe('ReactSuspenseWithNoopRenderer', () => { // Initial mount. This is synchronous, because the root is sync. ReactNoop.renderLegacySyncRoot(); await advanceTimers(100); - expect(Scheduler).toHaveYielded([ - 'Suspend! [Step: 1]', - 'Sibling', - 'Loading (1)', - 'Loading (2)', - 'Loading (3)', - 'Promise resolved [Step: 1]', - 'Step: 1', - ]); + if (enableNewScheduler) { + expect(Scheduler).toHaveYielded([ + 'Suspend! [Step: 1]', + 'Sibling', + 'Loading (1)', + 'Loading (2)', + 'Loading (3)', + 'Promise resolved [Step: 1]', + ]); + expect(Scheduler).toFlushExpired(['Step: 1']); + } else { + expect(Scheduler).toHaveYielded([ + 'Suspend! [Step: 1]', + 'Sibling', + 'Loading (1)', + 'Loading (2)', + 'Loading (3)', + 'Promise resolved [Step: 1]', + 'Step: 1', + ]); + } expect(ReactNoop).toMatchRenderedOutput( @@ -941,10 +964,15 @@ describe('ReactSuspenseWithNoopRenderer', () => { ); await advanceTimers(100); - expect(Scheduler).toHaveYielded([ - 'Promise resolved [Step: 2]', - 'Step: 2', - ]); + if (enableNewScheduler) { + expect(Scheduler).toHaveYielded(['Promise resolved [Step: 2]']); + expect(Scheduler).toFlushExpired(['Step: 2']); + } else { + expect(Scheduler).toHaveYielded([ + 'Promise resolved [Step: 2]', + 'Step: 2', + ]); + } expect(ReactNoop).toMatchRenderedOutput( @@ -1004,18 +1032,34 @@ describe('ReactSuspenseWithNoopRenderer', () => { Scheduler.yieldValue('Did mount'), ); await advanceTimers(100); - expect(Scheduler).toHaveYielded([ - 'Before', - 'Suspend! [Async: 1]', - 'After', - 'Loading...', - 'Before', - 'Sync: 1', - 'After', - 'Did mount', - 'Promise resolved [Async: 1]', - 'Async: 1', - ]); + + if (enableNewScheduler) { + expect(Scheduler).toHaveYielded([ + 'Before', + 'Suspend! [Async: 1]', + 'After', + 'Loading...', + 'Before', + 'Sync: 1', + 'After', + 'Did mount', + 'Promise resolved [Async: 1]', + ]); + expect(Scheduler).toFlushExpired(['Async: 1']); + } else { + expect(Scheduler).toHaveYielded([ + 'Before', + 'Suspend! [Async: 1]', + 'After', + 'Loading...', + 'Before', + 'Sync: 1', + 'After', + 'Did mount', + 'Promise resolved [Async: 1]', + 'Async: 1', + ]); + } expect(ReactNoop).toMatchRenderedOutput( @@ -1069,10 +1113,16 @@ describe('ReactSuspenseWithNoopRenderer', () => { // When the placeholder is pinged, the boundary must be re-rendered // synchronously. await advanceTimers(100); - expect(Scheduler).toHaveYielded([ - 'Promise resolved [Async: 2]', - 'Async: 2', - ]); + + if (enableNewScheduler) { + expect(Scheduler).toHaveYielded(['Promise resolved [Async: 2]']); + expect(Scheduler).toFlushExpired(['Async: 2']); + } else { + expect(Scheduler).toHaveYielded([ + 'Promise resolved [Async: 2]', + 'Async: 2', + ]); + } expect(ReactNoop).toMatchRenderedOutput( @@ -1139,18 +1189,33 @@ describe('ReactSuspenseWithNoopRenderer', () => { Scheduler.yieldValue('Did mount'), ); await advanceTimers(100); - expect(Scheduler).toHaveYielded([ - 'Before', - 'Suspend! [Async: 1]', - 'After', - 'Loading...', - 'Before', - 'Sync: 1', - 'After', - 'Did mount', - 'Promise resolved [Async: 1]', - 'Async: 1', - ]); + if (enableNewScheduler) { + expect(Scheduler).toHaveYielded([ + 'Before', + 'Suspend! [Async: 1]', + 'After', + 'Loading...', + 'Before', + 'Sync: 1', + 'After', + 'Did mount', + 'Promise resolved [Async: 1]', + ]); + expect(Scheduler).toFlushExpired(['Async: 1']); + } else { + expect(Scheduler).toHaveYielded([ + 'Before', + 'Suspend! [Async: 1]', + 'After', + 'Loading...', + 'Before', + 'Sync: 1', + 'After', + 'Did mount', + 'Promise resolved [Async: 1]', + 'Async: 1', + ]); + } expect(ReactNoop).toMatchRenderedOutput( @@ -1204,10 +1269,16 @@ describe('ReactSuspenseWithNoopRenderer', () => { // When the placeholder is pinged, the boundary must be re-rendered // synchronously. await advanceTimers(100); - expect(Scheduler).toHaveYielded([ - 'Promise resolved [Async: 2]', - 'Async: 2', - ]); + + if (enableNewScheduler) { + expect(Scheduler).toHaveYielded(['Promise resolved [Async: 2]']); + expect(Scheduler).toFlushExpired(['Async: 2']); + } else { + expect(Scheduler).toHaveYielded([ + 'Promise resolved [Async: 2]', + 'Async: 2', + ]); + } expect(ReactNoop).toMatchRenderedOutput( @@ -1287,7 +1358,13 @@ describe('ReactSuspenseWithNoopRenderer', () => { ReactNoop.expire(1000); await advanceTimers(1000); - expect(Scheduler).toHaveYielded(['Promise resolved [B]', 'B']); + + if (enableNewScheduler) { + expect(Scheduler).toHaveYielded(['Promise resolved [B]']); + expect(Scheduler).toFlushExpired(['B']); + } else { + expect(Scheduler).toHaveYielded(['Promise resolved [B]', 'B']); + } expect(ReactNoop).toMatchRenderedOutput( @@ -1339,12 +1416,22 @@ describe('ReactSuspenseWithNoopRenderer', () => { expect(ReactNoop.getChildren()).toEqual([span('Loading...')]); await advanceTimers(1000); - expect(Scheduler).toHaveYielded([ - 'Promise resolved [Hi]', - 'constructor', - 'Hi', - 'componentDidMount', - ]); + + if (enableNewScheduler) { + expect(Scheduler).toHaveYielded(['Promise resolved [Hi]']); + expect(Scheduler).toFlushExpired([ + 'constructor', + 'Hi', + 'componentDidMount', + ]); + } else { + expect(Scheduler).toHaveYielded([ + 'Promise resolved [Hi]', + 'constructor', + 'Hi', + 'componentDidMount', + ]); + } expect(ReactNoop.getChildren()).toEqual([span('Hi')]); }); @@ -1383,7 +1470,12 @@ describe('ReactSuspenseWithNoopRenderer', () => { ]); expect(ReactNoop.getChildren()).toEqual([span('Loading...')]); await advanceTimers(100); - expect(Scheduler).toHaveYielded(['Promise resolved [Hi]', 'Hi']); + if (enableNewScheduler) { + expect(Scheduler).toHaveYielded(['Promise resolved [Hi]']); + expect(Scheduler).toFlushExpired(['Hi']); + } else { + expect(Scheduler).toHaveYielded(['Promise resolved [Hi]', 'Hi']); + } expect(ReactNoop.getChildren()).toEqual([span('Hi')]); }); @@ -1427,7 +1519,12 @@ describe('ReactSuspenseWithNoopRenderer', () => { await advanceTimers(1000); - expect(Scheduler).toHaveYielded(['Promise resolved [Hi]', 'Hi']); + if (enableNewScheduler) { + expect(Scheduler).toHaveYielded(['Promise resolved [Hi]']); + expect(Scheduler).toFlushExpired(['Hi']); + } else { + expect(Scheduler).toHaveYielded(['Promise resolved [Hi]', 'Hi']); + } }); } else { it('hides/unhides suspended children before layout effects fire (mutation)', async () => { @@ -1466,7 +1563,12 @@ describe('ReactSuspenseWithNoopRenderer', () => { await advanceTimers(1000); - expect(Scheduler).toHaveYielded(['Promise resolved [Hi]', 'Hi']); + if (enableNewScheduler) { + expect(Scheduler).toHaveYielded(['Promise resolved [Hi]']); + expect(Scheduler).toFlushExpired(['Hi']); + } else { + expect(Scheduler).toHaveYielded(['Promise resolved [Hi]', 'Hi']); + } }); } }); diff --git a/packages/react-test-renderer/src/__tests__/ReactTestRenderer-test.js b/packages/react-test-renderer/src/__tests__/ReactTestRenderer-test.js index 177ac9e11d89f..50de254aa3022 100644 --- a/packages/react-test-renderer/src/__tests__/ReactTestRenderer-test.js +++ b/packages/react-test-renderer/src/__tests__/ReactTestRenderer-test.js @@ -9,15 +9,25 @@ 'use strict'; -const ReactDOM = require('react-dom'); - -// Isolate test renderer. -jest.resetModules(); -const React = require('react'); -const ReactCache = require('react-cache'); -const ReactTestRenderer = require('react-test-renderer'); +let ReactDOM; +let React; +let ReactCache; +let ReactTestRenderer; +let Scheduler; describe('ReactTestRenderer', () => { + beforeEach(() => { + jest.resetModules(); + ReactDOM = require('react-dom'); + + // Isolate test renderer. + jest.resetModules(); + React = require('react'); + ReactCache = require('react-cache'); + ReactTestRenderer = require('react-test-renderer'); + Scheduler = require('scheduler'); + }); + it('should warn if used to render a ReactDOM portal', () => { const container = document.createElement('div'); expect(() => { @@ -62,6 +72,7 @@ describe('ReactTestRenderer', () => { const root = ReactTestRenderer.create(); PendingResources.initial('initial'); await Promise.resolve(); + Scheduler.flushAll(); expect(root.toJSON()).toEqual('initial'); root.update(); @@ -69,6 +80,7 @@ describe('ReactTestRenderer', () => { PendingResources.dynamic('dynamic'); await Promise.resolve(); + Scheduler.flushAll(); expect(root.toJSON()).toEqual('dynamic'); done(); @@ -88,6 +100,7 @@ describe('ReactTestRenderer', () => { const root = ReactTestRenderer.create(); PendingResources.initial('initial'); await Promise.resolve(); + Scheduler.flushAll(); expect(root.toJSON().children).toEqual(['initial']); root.update(); @@ -95,6 +108,7 @@ describe('ReactTestRenderer', () => { PendingResources.dynamic('dynamic'); await Promise.resolve(); + Scheduler.flushAll(); expect(root.toJSON().children).toEqual(['dynamic']); done(); diff --git a/packages/react/src/__tests__/ReactProfiler-test.internal.js b/packages/react/src/__tests__/ReactProfiler-test.internal.js index ed4cca9b566e1..1a9a386fe8667 100644 --- a/packages/react/src/__tests__/ReactProfiler-test.internal.js +++ b/packages/react/src/__tests__/ReactProfiler-test.internal.js @@ -12,6 +12,7 @@ let React; let ReactFeatureFlags; +let enableNewScheduler; let ReactNoop; let Scheduler; let ReactCache; @@ -35,6 +36,7 @@ function loadModules({ ReactFeatureFlags.enableProfilerTimer = enableProfilerTimer; ReactFeatureFlags.enableSchedulerTracing = enableSchedulerTracing; ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = replayFailedUnitOfWorkWithInvokeGuardedCallback; + enableNewScheduler = ReactFeatureFlags.enableNewScheduler; React = require('react'); Scheduler = require('scheduler'); @@ -1352,6 +1354,9 @@ describe('Profiler', () => { }, ); }).toThrow('Expected error onWorkScheduled'); + if (enableNewScheduler) { + expect(Scheduler).toFlushAndYield(['Component:fail']); + } throwInOnWorkScheduled = false; expect(onWorkScheduled).toHaveBeenCalled(); @@ -1386,7 +1391,14 @@ describe('Profiler', () => { // Errors that happen inside of a subscriber should throw, throwInOnWorkStarted = true; expect(Scheduler).toFlushAndThrow('Expected error onWorkStarted'); - expect(Scheduler).toHaveYielded(['Component:text']); + if (enableNewScheduler) { + // Rendering was interrupted by the error that was thrown + expect(Scheduler).toHaveYielded([]); + // Rendering continues in the next task + expect(Scheduler).toFlushAndYield(['Component:text']); + } else { + expect(Scheduler).toHaveYielded(['Component:text']); + } throwInOnWorkStarted = false; expect(onWorkStarted).toHaveBeenCalled(); @@ -2372,11 +2384,23 @@ describe('Profiler', () => { }, ); + expect(Scheduler).toHaveYielded(['Suspend [loaded]', 'Text [loading]']); + expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); jest.runAllTimers(); await resourcePromise; + if (enableNewScheduler) { + expect(Scheduler).toHaveYielded(['Promise resolved [loaded]']); + expect(Scheduler).toFlushExpired(['AsyncText [loaded]']); + } else { + expect(Scheduler).toHaveYielded([ + 'Promise resolved [loaded]', + 'AsyncText [loaded]', + ]); + } + expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1); expect( onInteractionScheduledWorkCompleted, @@ -2428,9 +2452,16 @@ describe('Profiler', () => { expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); + expect(Scheduler).toHaveYielded(['Text [loading]']); + jest.runAllTimers(); await resourcePromise; + expect(Scheduler).toHaveYielded(['Promise resolved [loaded]']); + if (enableNewScheduler) { + expect(Scheduler).toFlushExpired([]); + } + expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); wrappedCascadingFn(); @@ -2590,6 +2621,14 @@ describe('Profiler', () => { }, ); }); + expect(Scheduler).toHaveYielded([ + 'Suspend [loaded]', + 'Text [loading]', + 'Text [initial]', + 'Suspend [loaded]', + 'Text [loading]', + 'Text [updated]', + ]); expect(renderer.toJSON()).toEqual(['loading', 'updated']); expect(onRender).toHaveBeenCalledTimes(1); @@ -2603,6 +2642,17 @@ describe('Profiler', () => { Scheduler.advanceTime(1000); jest.advanceTimersByTime(1000); await originalPromise; + + if (enableNewScheduler) { + expect(Scheduler).toHaveYielded(['Promise resolved [loaded]']); + expect(Scheduler).toFlushExpired(['AsyncText [loaded]']); + } else { + expect(Scheduler).toHaveYielded([ + 'Promise resolved [loaded]', + 'AsyncText [loaded]', + ]); + } + expect(renderer.toJSON()).toEqual(['loaded', 'updated']); expect(onRender).toHaveBeenCalledTimes(1); @@ -2703,10 +2753,18 @@ describe('Profiler', () => { expect(renderer.toJSON()).toEqual(['loaded', 'updated']); expect(onRender).toHaveBeenCalledTimes(1); - expect(onRender.mock.calls[0][6]).toMatchInteractions([ - initialRenderInteraction, - highPriUpdateInteraction, - ]); + + if (enableNewScheduler) { + // The initial render and the update are batched together. + expect(onRender.mock.calls[0][6]).toMatchInteractions([ + highPriUpdateInteraction, + ]); + } else { + expect(onRender.mock.calls[0][6]).toMatchInteractions([ + initialRenderInteraction, + highPriUpdateInteraction, + ]); + } expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(2); expect( diff --git a/packages/scheduler/src/Scheduler.js b/packages/scheduler/src/Scheduler.js index ab533e9cd878b..5c3ed6b7a968f 100644 --- a/packages/scheduler/src/Scheduler.js +++ b/packages/scheduler/src/Scheduler.js @@ -48,29 +48,35 @@ var currentPriorityLevel = NormalPriority; var currentEventStartTime = -1; var currentExpirationTime = -1; -// This is set when a callback is being executed, to prevent re-entrancy. -var isExecutingCallback = false; +// This is set while performing work, to prevent re-entrancy. +var isPerformingWork = false; + +// This is set while working on a callback, to detect when a callback +// cancels itself. +var currentlyFlushingCallback = null; var isHostCallbackScheduled = false; -function ensureHostCallbackIsScheduled() { - if (isExecutingCallback) { +function scheduleHostCallbackIfNeeded() { + if (isPerformingWork) { // Don't schedule work yet; wait until the next time we yield. return; } - // Schedule the host callback using the earliest expiration in the list. - var expirationTime = firstCallbackNode.expirationTime; - if (!isHostCallbackScheduled) { - isHostCallbackScheduled = true; - } else { - // Cancel the existing host callback. - cancelHostCallback(); + if (firstCallbackNode !== null) { + // Schedule the host callback using the earliest expiration in the list. + var expirationTime = firstCallbackNode.expirationTime; + if (isHostCallbackScheduled) { + // Cancel the existing host callback. + cancelHostCallback(); + } else { + isHostCallbackScheduled = true; + } + requestHostCallback(flushWork, expirationTime); } - requestHostCallback(flushWork, expirationTime); } function flushFirstCallback() { - var flushedNode = firstCallbackNode; + currentlyFlushingCallback = firstCallbackNode; // Remove the node from the list before calling the callback. That way the // list is in a consistent state even if the callback throws. @@ -85,24 +91,36 @@ function flushFirstCallback() { next.previous = lastCallbackNode; } - flushedNode.next = flushedNode.previous = null; + currentlyFlushingCallback.next = currentlyFlushingCallback.previous = null; // Now it's safe to call the callback. - var callback = flushedNode.callback; - var expirationTime = flushedNode.expirationTime; - var priorityLevel = flushedNode.priorityLevel; + var callback = currentlyFlushingCallback.callback; + var expirationTime = currentlyFlushingCallback.expirationTime; + var priorityLevel = currentlyFlushingCallback.priorityLevel; var previousPriorityLevel = currentPriorityLevel; var previousExpirationTime = currentExpirationTime; currentPriorityLevel = priorityLevel; currentExpirationTime = expirationTime; var continuationCallback; try { - continuationCallback = callback(currentDidTimeout); + continuationCallback = callback( + currentDidTimeout || priorityLevel === ImmediatePriority, + ); + } catch (error) { + currentlyFlushingCallback = null; + throw error; } finally { currentPriorityLevel = previousPriorityLevel; currentExpirationTime = previousExpirationTime; } + if (currentlyFlushingCallback === null) { + // Callback was cancelled during execution. Exit, even if there is + // a continutation. + return; + } + currentlyFlushingCallback = null; + // A callback may return a continuation. The continuation should be scheduled // with the same priority and expiration as the just-finished callback. if (typeof continuationCallback === 'function') { @@ -141,7 +159,7 @@ function flushFirstCallback() { } else if (nextAfterContinuation === firstCallbackNode) { // The new callback is the highest priority callback in the list. firstCallbackNode = continuationNode; - ensureHostCallbackIsScheduled(); + scheduleHostCallbackIfNeeded(); } var previous = nextAfterContinuation.previous; @@ -152,42 +170,16 @@ function flushFirstCallback() { } } -function flushImmediateWork() { - if ( - // Confirm we've exited the outer most event handler - currentEventStartTime === -1 && - firstCallbackNode !== null && - firstCallbackNode.priorityLevel === ImmediatePriority - ) { - isExecutingCallback = true; - try { - do { - flushFirstCallback(); - } while ( - // Keep flushing until there are no more immediate callbacks - firstCallbackNode !== null && - firstCallbackNode.priorityLevel === ImmediatePriority - ); - } finally { - isExecutingCallback = false; - if (firstCallbackNode !== null) { - // There's still work remaining. Request another callback. - ensureHostCallbackIsScheduled(); - } else { - isHostCallbackScheduled = false; - } - } - } -} - function flushWork(didTimeout) { // Exit right away if we're currently paused - if (enableSchedulerDebugging && isSchedulerPaused) { return; } - isExecutingCallback = true; + // We'll need a new host callback the next time work is scheduled. + isHostCallbackScheduled = false; + + isPerformingWork = true; const previousDidTimeout = currentDidTimeout; currentDidTimeout = didTimeout; try { @@ -226,16 +218,10 @@ function flushWork(didTimeout) { } } } finally { - isExecutingCallback = false; + isPerformingWork = false; currentDidTimeout = previousDidTimeout; - if (firstCallbackNode !== null) { - // There's still work remaining. Request another callback. - ensureHostCallbackIsScheduled(); - } else { - isHostCallbackScheduled = false; - } - // Before exiting, flush all the immediate work that was scheduled. - flushImmediateWork(); + // There's still work remaining. Request another callback. + scheduleHostCallbackIfNeeded(); } } @@ -258,12 +244,13 @@ function unstable_runWithPriority(priorityLevel, eventHandler) { try { return eventHandler(); + } catch (error) { + // There's still work remaining. Request another callback. + scheduleHostCallbackIfNeeded(); + throw error; } finally { currentPriorityLevel = previousPriorityLevel; currentEventStartTime = previousEventStartTime; - - // Before exiting, flush all the immediate work that was scheduled. - flushImmediateWork(); } } @@ -289,12 +276,13 @@ function unstable_next(eventHandler) { try { return eventHandler(); + } catch (error) { + // There's still work remaining. Request another callback. + scheduleHostCallbackIfNeeded(); + throw error; } finally { currentPriorityLevel = previousPriorityLevel; currentEventStartTime = previousEventStartTime; - - // Before exiting, flush all the immediate work that was scheduled. - flushImmediateWork(); } } @@ -309,15 +297,22 @@ function unstable_wrapCallback(callback) { try { return callback.apply(this, arguments); + } catch (error) { + // There's still work remaining. Request another callback. + scheduleHostCallbackIfNeeded(); + throw error; } finally { currentPriorityLevel = previousPriorityLevel; currentEventStartTime = previousEventStartTime; - flushImmediateWork(); } }; } -function unstable_scheduleCallback(callback, deprecated_options) { +function unstable_scheduleCallback( + priorityLevel, + callback, + deprecated_options, +) { var startTime = currentEventStartTime !== -1 ? currentEventStartTime : getCurrentTime(); @@ -330,7 +325,7 @@ function unstable_scheduleCallback(callback, deprecated_options) { // FIXME: Remove this branch once we lift expiration times out of React. expirationTime = startTime + deprecated_options.timeout; } else { - switch (currentPriorityLevel) { + switch (priorityLevel) { case ImmediatePriority: expirationTime = startTime + IMMEDIATE_PRIORITY_TIMEOUT; break; @@ -351,7 +346,7 @@ function unstable_scheduleCallback(callback, deprecated_options) { var newNode = { callback, - priorityLevel: currentPriorityLevel, + priorityLevel: priorityLevel, expirationTime, next: null, previous: null, @@ -363,7 +358,7 @@ function unstable_scheduleCallback(callback, deprecated_options) { if (firstCallbackNode === null) { // This is the first callback in the list. firstCallbackNode = newNode.next = newNode.previous = newNode; - ensureHostCallbackIsScheduled(); + scheduleHostCallbackIfNeeded(); } else { var next = null; var node = firstCallbackNode; @@ -383,7 +378,7 @@ function unstable_scheduleCallback(callback, deprecated_options) { } else if (next === firstCallbackNode) { // The new callback has the earliest expiration in the entire list. firstCallbackNode = newNode; - ensureHostCallbackIsScheduled(); + scheduleHostCallbackIfNeeded(); } var previous = next.previous; @@ -402,7 +397,7 @@ function unstable_pauseExecution() { function unstable_continueExecution() { isSchedulerPaused = false; if (firstCallbackNode !== null) { - ensureHostCallbackIsScheduled(); + scheduleHostCallbackIfNeeded(); } } @@ -413,6 +408,9 @@ function unstable_getFirstCallbackNode() { function unstable_cancelCallback(callbackNode) { var next = callbackNode.next; if (next === null) { + if (currentlyFlushingCallback === callbackNode) { + currentlyFlushingCallback = null; + } // Already cancelled. return; } @@ -439,10 +437,11 @@ function unstable_getCurrentPriorityLevel() { function unstable_shouldYield() { return ( - !currentDidTimeout && - ((firstCallbackNode !== null && - firstCallbackNode.expirationTime < currentExpirationTime) || - shouldYieldToHost()) + currentlyFlushingCallback === null || + (!currentDidTimeout && + ((firstCallbackNode !== null && + firstCallbackNode.expirationTime < currentExpirationTime) || + shouldYieldToHost())) ); } diff --git a/packages/scheduler/src/__tests__/Scheduler-test.js b/packages/scheduler/src/__tests__/Scheduler-test.js index 117daf69166ac..20cd67fa27adb 100644 --- a/packages/scheduler/src/__tests__/Scheduler-test.js +++ b/packages/scheduler/src/__tests__/Scheduler-test.js @@ -39,10 +39,10 @@ describe('Scheduler', () => { }); it('flushes work incrementally', () => { - scheduleCallback(() => Scheduler.yieldValue('A')); - scheduleCallback(() => Scheduler.yieldValue('B')); - scheduleCallback(() => Scheduler.yieldValue('C')); - scheduleCallback(() => Scheduler.yieldValue('D')); + scheduleCallback(NormalPriority, () => Scheduler.yieldValue('A')); + scheduleCallback(NormalPriority, () => Scheduler.yieldValue('B')); + scheduleCallback(NormalPriority, () => Scheduler.yieldValue('C')); + scheduleCallback(NormalPriority, () => Scheduler.yieldValue('D')); expect(Scheduler).toFlushAndYieldThrough(['A', 'B']); expect(Scheduler).toFlushAndYieldThrough(['C']); @@ -50,9 +50,11 @@ describe('Scheduler', () => { }); it('cancels work', () => { - scheduleCallback(() => Scheduler.yieldValue('A')); - const callbackHandleB = scheduleCallback(() => Scheduler.yieldValue('B')); - scheduleCallback(() => Scheduler.yieldValue('C')); + scheduleCallback(NormalPriority, () => Scheduler.yieldValue('A')); + const callbackHandleB = scheduleCallback(NormalPriority, () => + Scheduler.yieldValue('B'), + ); + scheduleCallback(NormalPriority, () => Scheduler.yieldValue('C')); cancelCallback(callbackHandleB); @@ -64,37 +66,31 @@ describe('Scheduler', () => { }); it('executes the highest priority callbacks first', () => { - scheduleCallback(() => Scheduler.yieldValue('A')); - scheduleCallback(() => Scheduler.yieldValue('B')); + scheduleCallback(NormalPriority, () => Scheduler.yieldValue('A')); + scheduleCallback(NormalPriority, () => Scheduler.yieldValue('B')); // Yield before B is flushed expect(Scheduler).toFlushAndYieldThrough(['A']); - runWithPriority(UserBlockingPriority, () => { - scheduleCallback(() => Scheduler.yieldValue('C')); - scheduleCallback(() => Scheduler.yieldValue('D')); - }); + scheduleCallback(UserBlockingPriority, () => Scheduler.yieldValue('C')); + scheduleCallback(UserBlockingPriority, () => Scheduler.yieldValue('D')); // C and D should come first, because they are higher priority expect(Scheduler).toFlushAndYield(['C', 'D', 'B']); }); it('expires work', () => { - scheduleCallback(didTimeout => { + scheduleCallback(NormalPriority, didTimeout => { Scheduler.advanceTime(100); Scheduler.yieldValue(`A (did timeout: ${didTimeout})`); }); - runWithPriority(UserBlockingPriority, () => { - scheduleCallback(didTimeout => { - Scheduler.advanceTime(100); - Scheduler.yieldValue(`B (did timeout: ${didTimeout})`); - }); + scheduleCallback(UserBlockingPriority, didTimeout => { + Scheduler.advanceTime(100); + Scheduler.yieldValue(`B (did timeout: ${didTimeout})`); }); - runWithPriority(UserBlockingPriority, () => { - scheduleCallback(didTimeout => { - Scheduler.advanceTime(100); - Scheduler.yieldValue(`C (did timeout: ${didTimeout})`); - }); + scheduleCallback(UserBlockingPriority, didTimeout => { + Scheduler.advanceTime(100); + Scheduler.yieldValue(`C (did timeout: ${didTimeout})`); }); // Advance time, but not by enough to expire any work @@ -102,11 +98,11 @@ describe('Scheduler', () => { expect(Scheduler).toHaveYielded([]); // Schedule a few more callbacks - scheduleCallback(didTimeout => { + scheduleCallback(NormalPriority, didTimeout => { Scheduler.advanceTime(100); Scheduler.yieldValue(`D (did timeout: ${didTimeout})`); }); - scheduleCallback(didTimeout => { + scheduleCallback(NormalPriority, didTimeout => { Scheduler.advanceTime(100); Scheduler.yieldValue(`E (did timeout: ${didTimeout})`); }); @@ -130,7 +126,7 @@ describe('Scheduler', () => { }); it('has a default expiration of ~5 seconds', () => { - scheduleCallback(() => Scheduler.yieldValue('A')); + scheduleCallback(NormalPriority, () => Scheduler.yieldValue('A')); Scheduler.advanceTime(4999); expect(Scheduler).toHaveYielded([]); @@ -140,11 +136,11 @@ describe('Scheduler', () => { }); it('continues working on same task after yielding', () => { - scheduleCallback(() => { + scheduleCallback(NormalPriority, () => { Scheduler.advanceTime(100); Scheduler.yieldValue('A'); }); - scheduleCallback(() => { + scheduleCallback(NormalPriority, () => { Scheduler.advanceTime(100); Scheduler.yieldValue('B'); }); @@ -163,13 +159,13 @@ describe('Scheduler', () => { } }; - scheduleCallback(C); + scheduleCallback(NormalPriority, C); - scheduleCallback(() => { + scheduleCallback(NormalPriority, () => { Scheduler.advanceTime(100); Scheduler.yieldValue('D'); }); - scheduleCallback(() => { + scheduleCallback(NormalPriority, () => { Scheduler.advanceTime(100); Scheduler.yieldValue('E'); }); @@ -197,7 +193,7 @@ describe('Scheduler', () => { }; // Schedule a high priority callback - runWithPriority(UserBlockingPriority, () => scheduleCallback(work)); + scheduleCallback(UserBlockingPriority, work); // Flush until just before the expiration time expect(Scheduler).toFlushAndYieldThrough(['A', 'B']); @@ -207,26 +203,6 @@ describe('Scheduler', () => { expect(Scheduler).toHaveYielded(['C', 'D']); }); - it('nested callbacks inherit the priority of the currently executing callback', () => { - runWithPriority(UserBlockingPriority, () => { - scheduleCallback(() => { - Scheduler.advanceTime(100); - Scheduler.yieldValue('Parent callback'); - scheduleCallback(() => { - Scheduler.advanceTime(100); - Scheduler.yieldValue('Nested callback'); - }); - }); - }); - - expect(Scheduler).toFlushAndYieldThrough(['Parent callback']); - - // The nested callback has user-blocking priority, so it should - // expire quickly. - Scheduler.advanceTime(250 + 100); - expect(Scheduler).toHaveYielded(['Nested callback']); - }); - it('continuations are interrupted by higher priority work', () => { const tasks = [['A', 100], ['B', 100], ['C', 100], ['D', 100]]; const work = () => { @@ -239,14 +215,12 @@ describe('Scheduler', () => { } } }; - scheduleCallback(work); + scheduleCallback(NormalPriority, work); expect(Scheduler).toFlushAndYieldThrough(['A']); - runWithPriority(UserBlockingPriority, () => { - scheduleCallback(() => { - Scheduler.advanceTime(100); - Scheduler.yieldValue('High pri'); - }); + scheduleCallback(UserBlockingPriority, () => { + Scheduler.advanceTime(100); + Scheduler.yieldValue('High pri'); }); expect(Scheduler).toFlushAndYield(['High pri', 'B', 'C', 'D']); @@ -266,12 +240,10 @@ describe('Scheduler', () => { if (task[0] === 'B') { // Schedule high pri work from inside another callback Scheduler.yieldValue('Schedule high pri'); - runWithPriority(UserBlockingPriority, () => - scheduleCallback(() => { - Scheduler.advanceTime(100); - Scheduler.yieldValue('High pri'); - }), - ); + scheduleCallback(UserBlockingPriority, () => { + Scheduler.advanceTime(100); + Scheduler.yieldValue('High pri'); + }); } if (tasks.length > 0 && shouldYield()) { Scheduler.yieldValue('Yield!'); @@ -279,7 +251,7 @@ describe('Scheduler', () => { } } }; - scheduleCallback(work); + scheduleCallback(NormalPriority, work); expect(Scheduler).toFlushAndYield([ 'A', 'B', @@ -295,21 +267,28 @@ describe('Scheduler', () => { }, ); - it('immediate callbacks fire at the end of outermost event', () => { - runWithPriority(ImmediatePriority, () => { - scheduleCallback(() => Scheduler.yieldValue('A')); - scheduleCallback(() => Scheduler.yieldValue('B')); - // Nested event - runWithPriority(ImmediatePriority, () => { - scheduleCallback(() => Scheduler.yieldValue('C')); - // Nothing should have fired yet - expect(Scheduler).toHaveYielded([]); - }); - // Nothing should have fired yet - expect(Scheduler).toHaveYielded([]); + it('top-level immediate callbacks fire in a subsequent task', () => { + scheduleCallback(ImmediatePriority, () => Scheduler.yieldValue('A')); + scheduleCallback(ImmediatePriority, () => Scheduler.yieldValue('B')); + scheduleCallback(ImmediatePriority, () => Scheduler.yieldValue('C')); + scheduleCallback(ImmediatePriority, () => Scheduler.yieldValue('D')); + // Immediate callback hasn't fired, yet. + expect(Scheduler).toHaveYielded([]); + // They all flush immediately within the subsequent task. + expect(Scheduler).toFlushExpired(['A', 'B', 'C', 'D']); + }); + + it('nested immediate callbacks are added to the queue of immediate callbacks', () => { + scheduleCallback(ImmediatePriority, () => Scheduler.yieldValue('A')); + scheduleCallback(ImmediatePriority, () => { + Scheduler.yieldValue('B'); + // This callback should go to the end of the queue + scheduleCallback(ImmediatePriority, () => Scheduler.yieldValue('C')); }); - // The callbacks were called at the end of the outer event - expect(Scheduler).toHaveYielded(['A', 'B', 'C']); + scheduleCallback(ImmediatePriority, () => Scheduler.yieldValue('D')); + expect(Scheduler).toHaveYielded([]); + // C should flush at the end + expect(Scheduler).toFlushExpired(['A', 'B', 'D', 'C']); }); it('wrapped callbacks have same signature as original callback', () => { @@ -318,108 +297,83 @@ describe('Scheduler', () => { }); it('wrapped callbacks inherit the current priority', () => { - const wrappedCallback = wrapCallback(() => { - scheduleCallback(() => { - Scheduler.advanceTime(100); - Scheduler.yieldValue('Normal'); - }); - }); - const wrappedInteractiveCallback = runWithPriority( + const wrappedCallback = runWithPriority(NormalPriority, () => + wrapCallback(() => { + Scheduler.yieldValue(getCurrentPriorityLevel()); + }), + ); + + const wrappedUserBlockingCallback = runWithPriority( UserBlockingPriority, () => wrapCallback(() => { - scheduleCallback(() => { - Scheduler.advanceTime(100); - Scheduler.yieldValue('User-blocking'); - }); + Scheduler.yieldValue(getCurrentPriorityLevel()); }), ); - // This should schedule a normal callback wrappedCallback(); - // This should schedule an user-blocking callback - wrappedInteractiveCallback(); + expect(Scheduler).toHaveYielded([NormalPriority]); - Scheduler.advanceTime(249); - expect(Scheduler).toHaveYielded([]); - Scheduler.advanceTime(1); - expect(Scheduler).toHaveYielded(['User-blocking']); - - Scheduler.advanceTime(10000); - expect(Scheduler).toHaveYielded(['Normal']); + wrappedUserBlockingCallback(); + expect(Scheduler).toHaveYielded([UserBlockingPriority]); }); it('wrapped callbacks inherit the current priority even when nested', () => { - const wrappedCallback = wrapCallback(() => { - scheduleCallback(() => { - Scheduler.advanceTime(100); - Scheduler.yieldValue('Normal'); + let wrappedCallback; + let wrappedUserBlockingCallback; + + runWithPriority(NormalPriority, () => { + wrappedCallback = wrapCallback(() => { + Scheduler.yieldValue(getCurrentPriorityLevel()); }); - }); - const wrappedInteractiveCallback = runWithPriority( - UserBlockingPriority, - () => + wrappedUserBlockingCallback = runWithPriority(UserBlockingPriority, () => wrapCallback(() => { - scheduleCallback(() => { - Scheduler.advanceTime(100); - Scheduler.yieldValue('User-blocking'); - }); + Scheduler.yieldValue(getCurrentPriorityLevel()); }), - ); - - runWithPriority(UserBlockingPriority, () => { - // This should schedule a normal callback - wrappedCallback(); - // This should schedule an user-blocking callback - wrappedInteractiveCallback(); + ); }); - Scheduler.advanceTime(249); - expect(Scheduler).toHaveYielded([]); - Scheduler.advanceTime(1); - expect(Scheduler).toHaveYielded(['User-blocking']); - - Scheduler.advanceTime(10000); - expect(Scheduler).toHaveYielded(['Normal']); - }); - - it('immediate callbacks fire at the end of callback', () => { - const immediateCallback = runWithPriority(ImmediatePriority, () => - wrapCallback(() => { - scheduleCallback(() => Scheduler.yieldValue('callback')); - }), - ); - immediateCallback(); + wrappedCallback(); + expect(Scheduler).toHaveYielded([NormalPriority]); - // The callback was called at the end of the outer event - expect(Scheduler).toHaveYielded(['callback']); + wrappedUserBlockingCallback(); + expect(Scheduler).toHaveYielded([UserBlockingPriority]); }); it("immediate callbacks fire even if there's an error", () => { - expect(() => { - runWithPriority(ImmediatePriority, () => { - scheduleCallback(() => { - Scheduler.yieldValue('A'); - throw new Error('Oops A'); - }); - scheduleCallback(() => { - Scheduler.yieldValue('B'); - }); - scheduleCallback(() => { - Scheduler.yieldValue('C'); - throw new Error('Oops C'); - }); - }); - }).toThrow('Oops A'); + scheduleCallback(ImmediatePriority, () => { + Scheduler.yieldValue('A'); + throw new Error('Oops A'); + }); + scheduleCallback(ImmediatePriority, () => { + Scheduler.yieldValue('B'); + }); + scheduleCallback(ImmediatePriority, () => { + Scheduler.yieldValue('C'); + throw new Error('Oops C'); + }); + expect(() => expect(Scheduler).toFlushExpired()).toThrow('Oops A'); expect(Scheduler).toHaveYielded(['A']); // B and C flush in a subsequent event. That way, the second error is not // swallowed. - expect(() => Scheduler.unstable_flushExpired()).toThrow('Oops C'); + expect(() => expect(Scheduler).toFlushExpired()).toThrow('Oops C'); expect(Scheduler).toHaveYielded(['B', 'C']); }); + it('multiple immediate callbacks can throw and there will be an error for each one', () => { + scheduleCallback(ImmediatePriority, () => { + throw new Error('First error'); + }); + scheduleCallback(ImmediatePriority, () => { + throw new Error('Second error'); + }); + expect(() => Scheduler.flushAll()).toThrow('First error'); + // The next error is thrown in the subsequent event + expect(() => Scheduler.flushAll()).toThrow('Second error'); + }); + it('exposes the current priority level', () => { Scheduler.yieldValue(getCurrentPriorityLevel()); runWithPriority(ImmediatePriority, () => { diff --git a/packages/scheduler/src/__tests__/SchedulerDOM-test.js b/packages/scheduler/src/__tests__/SchedulerDOM-test.js index 7565e84454a1d..3a66657d17ee2 100644 --- a/packages/scheduler/src/__tests__/SchedulerDOM-test.js +++ b/packages/scheduler/src/__tests__/SchedulerDOM-test.js @@ -102,20 +102,26 @@ describe('SchedulerDOM', () => { describe('scheduleCallback', () => { it('calls the callback within the frame when not blocked', () => { - const {unstable_scheduleCallback: scheduleCallback} = Scheduler; + const { + unstable_scheduleCallback: scheduleCallback, + unstable_NormalPriority: NormalPriority, + } = Scheduler; const cb = jest.fn(); - scheduleCallback(cb); + scheduleCallback(NormalPriority, cb); advanceOneFrame({timeLeftInFrame: 15}); expect(cb).toHaveBeenCalledTimes(1); }); it('inserts its rAF callback as early into the queue as possible', () => { - const {unstable_scheduleCallback: scheduleCallback} = Scheduler; + const { + unstable_scheduleCallback: scheduleCallback, + unstable_NormalPriority: NormalPriority, + } = Scheduler; const log = []; const useRAFCallback = () => { log.push('userRAFCallback'); }; - scheduleCallback(() => { + scheduleCallback(NormalPriority, () => { // Call rAF while idle work is being flushed. requestAnimationFrame(useRAFCallback); }); @@ -130,15 +136,18 @@ describe('SchedulerDOM', () => { describe('with multiple callbacks', () => { it('accepts multiple callbacks and calls within frame when not blocked', () => { - const {unstable_scheduleCallback: scheduleCallback} = Scheduler; + const { + unstable_scheduleCallback: scheduleCallback, + unstable_NormalPriority: NormalPriority, + } = Scheduler; const callbackLog = []; const callbackA = jest.fn(() => callbackLog.push('A')); const callbackB = jest.fn(() => callbackLog.push('B')); - scheduleCallback(callbackA); + scheduleCallback(NormalPriority, callbackA); // initially waits to call the callback expect(callbackLog).toEqual([]); // waits while second callback is passed - scheduleCallback(callbackB); + scheduleCallback(NormalPriority, callbackB); expect(callbackLog).toEqual([]); // after a delay, calls as many callbacks as it has time for advanceOneFrame({timeLeftInFrame: 15}); @@ -146,17 +155,20 @@ describe('SchedulerDOM', () => { }); it("accepts callbacks between animationFrame and postMessage and doesn't stall", () => { - const {unstable_scheduleCallback: scheduleCallback} = Scheduler; + const { + unstable_scheduleCallback: scheduleCallback, + unstable_NormalPriority: NormalPriority, + } = Scheduler; const callbackLog = []; const callbackA = jest.fn(() => callbackLog.push('A')); const callbackB = jest.fn(() => callbackLog.push('B')); const callbackC = jest.fn(() => callbackLog.push('C')); - scheduleCallback(callbackA); + scheduleCallback(NormalPriority, callbackA); // initially waits to call the callback expect(callbackLog).toEqual([]); runRAFCallbacks(); // this should schedule work *after* the requestAnimationFrame but before the message handler - scheduleCallback(callbackB); + scheduleCallback(NormalPriority, callbackB); expect(callbackLog).toEqual([]); // now it should drain the message queue and do all scheduled work runPostMessageCallbacks({timeLeftInFrame: 15}); @@ -166,7 +178,7 @@ describe('SchedulerDOM', () => { advanceOneFrame({timeLeftInFrame: 15}); // see if more work can be done now. - scheduleCallback(callbackC); + scheduleCallback(NormalPriority, callbackC); expect(callbackLog).toEqual(['A', 'B']); advanceOneFrame({timeLeftInFrame: 15}); expect(callbackLog).toEqual(['A', 'B', 'C']); @@ -176,11 +188,14 @@ describe('SchedulerDOM', () => { 'schedules callbacks in correct order and' + 'keeps calling them if there is time', () => { - const {unstable_scheduleCallback: scheduleCallback} = Scheduler; + const { + unstable_scheduleCallback: scheduleCallback, + unstable_NormalPriority: NormalPriority, + } = Scheduler; const callbackLog = []; const callbackA = jest.fn(() => { callbackLog.push('A'); - scheduleCallback(callbackC); + scheduleCallback(NormalPriority, callbackC); }); const callbackB = jest.fn(() => { callbackLog.push('B'); @@ -189,11 +204,11 @@ describe('SchedulerDOM', () => { callbackLog.push('C'); }); - scheduleCallback(callbackA); + scheduleCallback(NormalPriority, callbackA); // initially waits to call the callback expect(callbackLog).toEqual([]); // continues waiting while B is scheduled - scheduleCallback(callbackB); + scheduleCallback(NormalPriority, callbackB); expect(callbackLog).toEqual([]); // after a delay, calls the scheduled callbacks, // and also calls new callbacks scheduled by current callbacks @@ -203,17 +218,20 @@ describe('SchedulerDOM', () => { ); it('schedules callbacks in correct order when callbacks have many nested scheduleCallback calls', () => { - const {unstable_scheduleCallback: scheduleCallback} = Scheduler; + const { + unstable_scheduleCallback: scheduleCallback, + unstable_NormalPriority: NormalPriority, + } = Scheduler; const callbackLog = []; const callbackA = jest.fn(() => { callbackLog.push('A'); - scheduleCallback(callbackC); - scheduleCallback(callbackD); + scheduleCallback(NormalPriority, callbackC); + scheduleCallback(NormalPriority, callbackD); }); const callbackB = jest.fn(() => { callbackLog.push('B'); - scheduleCallback(callbackE); - scheduleCallback(callbackF); + scheduleCallback(NormalPriority, callbackE); + scheduleCallback(NormalPriority, callbackF); }); const callbackC = jest.fn(() => { callbackLog.push('C'); @@ -228,8 +246,8 @@ describe('SchedulerDOM', () => { callbackLog.push('F'); }); - scheduleCallback(callbackA); - scheduleCallback(callbackB); + scheduleCallback(NormalPriority, callbackA); + scheduleCallback(NormalPriority, callbackB); // initially waits to call the callback expect(callbackLog).toEqual([]); // while flushing callbacks, calls as many as it has time for @@ -238,22 +256,25 @@ describe('SchedulerDOM', () => { }); it('schedules callbacks in correct order when they use scheduleCallback to schedule themselves', () => { - const {unstable_scheduleCallback: scheduleCallback} = Scheduler; + const { + unstable_scheduleCallback: scheduleCallback, + unstable_NormalPriority: NormalPriority, + } = Scheduler; const callbackLog = []; let callbackAIterations = 0; const callbackA = jest.fn(() => { if (callbackAIterations < 1) { - scheduleCallback(callbackA); + scheduleCallback(NormalPriority, callbackA); } callbackLog.push('A' + callbackAIterations); callbackAIterations++; }); const callbackB = jest.fn(() => callbackLog.push('B')); - scheduleCallback(callbackA); + scheduleCallback(NormalPriority, callbackA); // initially waits to call the callback expect(callbackLog).toEqual([]); - scheduleCallback(callbackB); + scheduleCallback(NormalPriority, callbackB); expect(callbackLog).toEqual([]); // after a delay, calls the latest callback passed advanceOneFrame({timeLeftInFrame: 15}); @@ -269,7 +290,10 @@ describe('SchedulerDOM', () => { describe('when there is no more time left in the frame', () => { it('calls any callback which has timed out, waits for others', () => { - const {unstable_scheduleCallback: scheduleCallback} = Scheduler; + const { + unstable_scheduleCallback: scheduleCallback, + unstable_NormalPriority: NormalPriority, + } = Scheduler; startOfLatestFrame = 1000000000000; currentTime = startOfLatestFrame - 10; const callbackLog = []; @@ -278,9 +302,9 @@ describe('SchedulerDOM', () => { const callbackB = jest.fn(() => callbackLog.push('B')); const callbackC = jest.fn(() => callbackLog.push('C')); - scheduleCallback(callbackA); // won't time out - scheduleCallback(callbackB, {timeout: 100}); // times out later - scheduleCallback(callbackC, {timeout: 2}); // will time out fast + scheduleCallback(NormalPriority, callbackA); // won't time out + scheduleCallback(NormalPriority, callbackB, {timeout: 100}); // times out later + scheduleCallback(NormalPriority, callbackC, {timeout: 2}); // will time out fast // push time ahead a bit so that we have no idle time advanceOneFrame({timePastFrameDeadline: 16}); @@ -304,7 +328,10 @@ describe('SchedulerDOM', () => { describe('when there is some time left in the frame', () => { it('calls timed out callbacks and then any more pending callbacks, defers others if time runs out', () => { - const {unstable_scheduleCallback: scheduleCallback} = Scheduler; + const { + unstable_scheduleCallback: scheduleCallback, + unstable_NormalPriority: NormalPriority, + } = Scheduler; startOfLatestFrame = 1000000000000; currentTime = startOfLatestFrame - 10; const callbackLog = []; @@ -318,10 +345,10 @@ describe('SchedulerDOM', () => { const callbackC = jest.fn(() => callbackLog.push('C')); const callbackD = jest.fn(() => callbackLog.push('D')); - scheduleCallback(callbackA, {timeout: 100}); // won't time out - scheduleCallback(callbackB, {timeout: 100}); // times out later - scheduleCallback(callbackC, {timeout: 2}); // will time out fast - scheduleCallback(callbackD, {timeout: 200}); // won't time out + scheduleCallback(NormalPriority, callbackA, {timeout: 100}); // won't time out + scheduleCallback(NormalPriority, callbackB, {timeout: 100}); // times out later + scheduleCallback(NormalPriority, callbackC, {timeout: 2}); // will time out fast + scheduleCallback(NormalPriority, callbackD, {timeout: 200}); // won't time out advanceOneFrame({timeLeftInFrame: 15}); // runs rAF and postMessage callbacks @@ -355,9 +382,10 @@ describe('SchedulerDOM', () => { const { unstable_scheduleCallback: scheduleCallback, unstable_cancelCallback: cancelCallback, + unstable_NormalPriority: NormalPriority, } = Scheduler; const cb = jest.fn(); - const callbackId = scheduleCallback(cb); + const callbackId = scheduleCallback(NormalPriority, cb); expect(cb).toHaveBeenCalledTimes(0); cancelCallback(callbackId); advanceOneFrame({timeLeftInFrame: 15}); @@ -369,14 +397,15 @@ describe('SchedulerDOM', () => { const { unstable_scheduleCallback: scheduleCallback, unstable_cancelCallback: cancelCallback, + unstable_NormalPriority: NormalPriority, } = Scheduler; const callbackLog = []; const callbackA = jest.fn(() => callbackLog.push('A')); const callbackB = jest.fn(() => callbackLog.push('B')); const callbackC = jest.fn(() => callbackLog.push('C')); - scheduleCallback(callbackA); - const callbackId = scheduleCallback(callbackB); - scheduleCallback(callbackC); + scheduleCallback(NormalPriority, callbackA); + const callbackId = scheduleCallback(NormalPriority, callbackB); + scheduleCallback(NormalPriority, callbackC); cancelCallback(callbackId); cancelCallback(callbackId); cancelCallback(callbackId); @@ -393,6 +422,7 @@ describe('SchedulerDOM', () => { const { unstable_scheduleCallback: scheduleCallback, unstable_cancelCallback: cancelCallback, + unstable_NormalPriority: NormalPriority, } = Scheduler; const callbackLog = []; let callbackBId; @@ -401,8 +431,8 @@ describe('SchedulerDOM', () => { cancelCallback(callbackBId); }); const callbackB = jest.fn(() => callbackLog.push('B')); - scheduleCallback(callbackA); - callbackBId = scheduleCallback(callbackB); + scheduleCallback(NormalPriority, callbackA); + callbackBId = scheduleCallback(NormalPriority, callbackB); // Initially doesn't call anything expect(callbackLog).toEqual([]); advanceOneFrame({timeLeftInFrame: 15}); @@ -430,7 +460,10 @@ describe('SchedulerDOM', () => { * */ it('still calls all callbacks within same frame', () => { - const {unstable_scheduleCallback: scheduleCallback} = Scheduler; + const { + unstable_scheduleCallback: scheduleCallback, + unstable_NormalPriority: NormalPriority, + } = Scheduler; const callbackLog = []; const callbackA = jest.fn(() => callbackLog.push('A')); const callbackB = jest.fn(() => { @@ -443,11 +476,11 @@ describe('SchedulerDOM', () => { throw new Error('D error'); }); const callbackE = jest.fn(() => callbackLog.push('E')); - scheduleCallback(callbackA); - scheduleCallback(callbackB); - scheduleCallback(callbackC); - scheduleCallback(callbackD); - scheduleCallback(callbackE); + scheduleCallback(NormalPriority, callbackA); + scheduleCallback(NormalPriority, callbackB); + scheduleCallback(NormalPriority, callbackC); + scheduleCallback(NormalPriority, callbackD); + scheduleCallback(NormalPriority, callbackE); // Initially doesn't call anything expect(callbackLog).toEqual([]); catchPostMessageErrors = true; @@ -476,7 +509,10 @@ describe('SchedulerDOM', () => { * */ it('and with some timed out callbacks, still calls all callbacks within same frame', () => { - const {unstable_scheduleCallback: scheduleCallback} = Scheduler; + const { + unstable_scheduleCallback: scheduleCallback, + unstable_NormalPriority: NormalPriority, + } = Scheduler; const callbackLog = []; const callbackA = jest.fn(() => { callbackLog.push('A'); @@ -489,11 +525,11 @@ describe('SchedulerDOM', () => { throw new Error('D error'); }); const callbackE = jest.fn(() => callbackLog.push('E')); - scheduleCallback(callbackA); - scheduleCallback(callbackB); - scheduleCallback(callbackC, {timeout: 2}); // times out fast - scheduleCallback(callbackD, {timeout: 2}); // times out fast - scheduleCallback(callbackE, {timeout: 2}); // times out fast + scheduleCallback(NormalPriority, callbackA); + scheduleCallback(NormalPriority, callbackB); + scheduleCallback(NormalPriority, callbackC, {timeout: 2}); // times out fast + scheduleCallback(NormalPriority, callbackD, {timeout: 2}); // times out fast + scheduleCallback(NormalPriority, callbackE, {timeout: 2}); // times out fast // Initially doesn't call anything expect(callbackLog).toEqual([]); catchPostMessageErrors = true; @@ -522,7 +558,10 @@ describe('SchedulerDOM', () => { * */ it('still calls all callbacks within same frame', () => { - const {unstable_scheduleCallback: scheduleCallback} = Scheduler; + const { + unstable_scheduleCallback: scheduleCallback, + unstable_NormalPriority: NormalPriority, + } = Scheduler; const callbackLog = []; const callbackA = jest.fn(() => { callbackLog.push('A'); @@ -544,11 +583,11 @@ describe('SchedulerDOM', () => { callbackLog.push('E'); throw new Error('E error'); }); - scheduleCallback(callbackA); - scheduleCallback(callbackB); - scheduleCallback(callbackC); - scheduleCallback(callbackD); - scheduleCallback(callbackE); + scheduleCallback(NormalPriority, callbackA); + scheduleCallback(NormalPriority, callbackB); + scheduleCallback(NormalPriority, callbackC); + scheduleCallback(NormalPriority, callbackD); + scheduleCallback(NormalPriority, callbackE); // Initially doesn't call anything expect(callbackLog).toEqual([]); catchPostMessageErrors = true; @@ -583,7 +622,10 @@ describe('SchedulerDOM', () => { * */ it('and with all timed out callbacks, still calls all callbacks within same frame', () => { - const {unstable_scheduleCallback: scheduleCallback} = Scheduler; + const { + unstable_scheduleCallback: scheduleCallback, + unstable_NormalPriority: NormalPriority, + } = Scheduler; const callbackLog = []; const callbackA = jest.fn(() => { callbackLog.push('A'); @@ -605,11 +647,11 @@ describe('SchedulerDOM', () => { callbackLog.push('E'); throw new Error('E error'); }); - scheduleCallback(callbackA, {timeout: 2}); // times out fast - scheduleCallback(callbackB, {timeout: 2}); // times out fast - scheduleCallback(callbackC, {timeout: 2}); // times out fast - scheduleCallback(callbackD, {timeout: 2}); // times out fast - scheduleCallback(callbackE, {timeout: 2}); // times out fast + scheduleCallback(NormalPriority, callbackA, {timeout: 2}); // times out fast + scheduleCallback(NormalPriority, callbackB, {timeout: 2}); // times out fast + scheduleCallback(NormalPriority, callbackC, {timeout: 2}); // times out fast + scheduleCallback(NormalPriority, callbackD, {timeout: 2}); // times out fast + scheduleCallback(NormalPriority, callbackE, {timeout: 2}); // times out fast // Initially doesn't call anything expect(callbackLog).toEqual([]); catchPostMessageErrors = true; @@ -664,7 +706,10 @@ describe('SchedulerDOM', () => { * */ it('still calls all callbacks within same frame', () => { - const {unstable_scheduleCallback: scheduleCallback} = Scheduler; + const { + unstable_scheduleCallback: scheduleCallback, + unstable_NormalPriority: NormalPriority, + } = Scheduler; startOfLatestFrame = 1000000000000; currentTime = startOfLatestFrame - 10; catchPostMessageErrors = true; @@ -698,13 +743,13 @@ describe('SchedulerDOM', () => { }); const callbackG = jest.fn(() => callbackLog.push('G')); - scheduleCallback(callbackA); - scheduleCallback(callbackB); - scheduleCallback(callbackC); - scheduleCallback(callbackD); - scheduleCallback(callbackE); - scheduleCallback(callbackF); - scheduleCallback(callbackG); + scheduleCallback(NormalPriority, callbackA); + scheduleCallback(NormalPriority, callbackB); + scheduleCallback(NormalPriority, callbackC); + scheduleCallback(NormalPriority, callbackD); + scheduleCallback(NormalPriority, callbackE); + scheduleCallback(NormalPriority, callbackF); + scheduleCallback(NormalPriority, callbackG); // does nothing initially expect(callbackLog).toEqual([]); diff --git a/packages/scheduler/src/__tests__/SchedulerNoDOM-test.js b/packages/scheduler/src/__tests__/SchedulerNoDOM-test.js index 55d47037e0936..a8c45c0bc840a 100644 --- a/packages/scheduler/src/__tests__/SchedulerNoDOM-test.js +++ b/packages/scheduler/src/__tests__/SchedulerNoDOM-test.js @@ -9,10 +9,11 @@ 'use strict'; +let Scheduler; let scheduleCallback; -let runWithPriority; let ImmediatePriority; let UserBlockingPriority; +let NormalPriority; describe('SchedulerNoDOM', () => { // If Scheduler runs in a non-DOM environment, it falls back to a naive @@ -30,22 +31,22 @@ describe('SchedulerNoDOM', () => { ), ); - const Scheduler = require('scheduler'); + Scheduler = require('scheduler'); scheduleCallback = Scheduler.unstable_scheduleCallback; - runWithPriority = Scheduler.unstable_runWithPriority; ImmediatePriority = Scheduler.unstable_ImmediatePriority; UserBlockingPriority = Scheduler.unstable_UserBlockingPriority; + NormalPriority = Scheduler.unstable_NormalPriority; }); it('runAllTimers flushes all scheduled callbacks', () => { let log = []; - scheduleCallback(() => { + scheduleCallback(NormalPriority, () => { log.push('A'); }); - scheduleCallback(() => { + scheduleCallback(NormalPriority, () => { log.push('B'); }); - scheduleCallback(() => { + scheduleCallback(NormalPriority, () => { log.push('C'); }); expect(log).toEqual([]); @@ -56,19 +57,17 @@ describe('SchedulerNoDOM', () => { it('executes callbacks in order of priority', () => { let log = []; - scheduleCallback(() => { + scheduleCallback(NormalPriority, () => { log.push('A'); }); - scheduleCallback(() => { + scheduleCallback(NormalPriority, () => { log.push('B'); }); - runWithPriority(UserBlockingPriority, () => { - scheduleCallback(() => { - log.push('C'); - }); - scheduleCallback(() => { - log.push('D'); - }); + scheduleCallback(UserBlockingPriority, () => { + log.push('C'); + }); + scheduleCallback(UserBlockingPriority, () => { + log.push('D'); }); expect(log).toEqual([]); @@ -76,39 +75,22 @@ describe('SchedulerNoDOM', () => { expect(log).toEqual(['C', 'D', 'A', 'B']); }); - it('calls immediate callbacks immediately', () => { + it('handles errors', () => { let log = []; - runWithPriority(ImmediatePriority, () => { - scheduleCallback(() => { - log.push('A'); - scheduleCallback(() => { - log.push('B'); - }); - }); + scheduleCallback(ImmediatePriority, () => { + log.push('A'); + throw new Error('Oops A'); + }); + scheduleCallback(ImmediatePriority, () => { + log.push('B'); + }); + scheduleCallback(ImmediatePriority, () => { + log.push('C'); + throw new Error('Oops C'); }); - expect(log).toEqual(['A', 'B']); - }); - - it('handles errors', () => { - let log = []; - - expect(() => { - runWithPriority(ImmediatePriority, () => { - scheduleCallback(() => { - log.push('A'); - throw new Error('Oops A'); - }); - scheduleCallback(() => { - log.push('B'); - }); - scheduleCallback(() => { - log.push('C'); - throw new Error('Oops C'); - }); - }); - }).toThrow('Oops A'); + expect(() => jest.runAllTimers()).toThrow('Oops A'); expect(log).toEqual(['A']); diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index 9ace0090d6ca7..d4ca8405e1dcc 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -67,4 +67,4 @@ export const enableEventAPI = false; // Enables rewritten version of ReactFiberScheduler. Added in case we need to // quickly revert it. -export const enableNewScheduler = false; +export const enableNewScheduler = true; diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index 658f7781ff313..e509f584ed3a3 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -31,7 +31,7 @@ export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__; export const warnAboutDeprecatedLifecycles = true; export const warnAboutDeprecatedSetNativeProps = true; export const enableEventAPI = false; -export const enableNewScheduler = false; +export const enableNewScheduler = true; // Only used in www builds. export function addUserTimingListener() { diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index e264fd3a34419..20998e0eaf679 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -28,7 +28,7 @@ export const warnAboutShorthandPropertyCollision = false; export const enableSchedulerDebugging = false; export const warnAboutDeprecatedSetNativeProps = false; export const enableEventAPI = false; -export const enableNewScheduler = false; +export const enableNewScheduler = true; // Only used in www builds. export function addUserTimingListener() { diff --git a/packages/shared/forks/ReactFeatureFlags.persistent.js b/packages/shared/forks/ReactFeatureFlags.persistent.js index 2b048a082236f..cf2daad77b5f2 100644 --- a/packages/shared/forks/ReactFeatureFlags.persistent.js +++ b/packages/shared/forks/ReactFeatureFlags.persistent.js @@ -28,7 +28,7 @@ export const warnAboutShorthandPropertyCollision = false; export const enableSchedulerDebugging = false; export const warnAboutDeprecatedSetNativeProps = false; export const enableEventAPI = false; -export const enableNewScheduler = false; +export const enableNewScheduler = true; // Only used in www builds. export function addUserTimingListener() { diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index 08fe309d20b79..fbd41ffa5e1a0 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -28,7 +28,7 @@ export const warnAboutShorthandPropertyCollision = false; export const enableSchedulerDebugging = false; export const warnAboutDeprecatedSetNativeProps = false; export const enableEventAPI = false; -export const enableNewScheduler = false; +export const enableNewScheduler = true; // Only used in www builds. export function addUserTimingListener() { diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index 4b5df2e904080..18dce7eeaa51e 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -26,7 +26,7 @@ export const warnAboutDeprecatedSetNativeProps = false; export const disableJavaScriptURLs = false; export const disableYielding = false; export const enableEventAPI = true; -export const enableNewScheduler = false; +export const enableNewScheduler = true; // Only used in www builds. export function addUserTimingListener() { diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index f47b6097e4027..ed692d15047f9 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -42,7 +42,7 @@ export const enableSuspenseServerRenderer = true; // I've chosen to make this a static flag instead of a dynamic flag controlled // by a GK so that it doesn't increase bundle size. It should still be easy // to rollback by reverting the commit that turns this on. -export const enableNewScheduler = false; +export const enableNewScheduler = true; let refCount = 0; export function addUserTimingListener() { diff --git a/scripts/circleci/test_entry_point.sh b/scripts/circleci/test_entry_point.sh index 87bbad4aba9e8..5266dafbf5c59 100755 --- a/scripts/circleci/test_entry_point.sh +++ b/scripts/circleci/test_entry_point.sh @@ -11,6 +11,7 @@ if [ $((0 % CIRCLE_NODE_TOTAL)) -eq "$CIRCLE_NODE_INDEX" ]; then COMMANDS_TO_RUN+=('node ./scripts/tasks/flow-ci') COMMANDS_TO_RUN+=('node ./scripts/tasks/eslint') COMMANDS_TO_RUN+=('yarn test --maxWorkers=2') + COMMANDS_TO_RUN+=('yarn test-old-scheduler --maxWorkers=2') COMMANDS_TO_RUN+=('yarn test-persistent --maxWorkers=2') COMMANDS_TO_RUN+=('./scripts/circleci/check_license.sh') COMMANDS_TO_RUN+=('./scripts/circleci/check_modules.sh') diff --git a/scripts/jest/config.source-new-scheduler.js b/scripts/jest/config.source-old-scheduler.js similarity index 81% rename from scripts/jest/config.source-new-scheduler.js rename to scripts/jest/config.source-old-scheduler.js index 6d74d5bb1b0fa..cb049ae6d7ddc 100644 --- a/scripts/jest/config.source-new-scheduler.js +++ b/scripts/jest/config.source-old-scheduler.js @@ -5,7 +5,7 @@ const baseConfig = require('./config.base'); module.exports = Object.assign({}, baseConfig, { setupFiles: [ ...baseConfig.setupFiles, - require.resolve('./setupNewScheduler.js'), + require.resolve('./setupOldScheduler.js'), require.resolve('./setupHostConfigs.js'), ], }); diff --git a/scripts/jest/matchers/schedulerTestMatchers.js b/scripts/jest/matchers/schedulerTestMatchers.js index 4984ea42b50ca..b19a984904dfd 100644 --- a/scripts/jest/matchers/schedulerTestMatchers.js +++ b/scripts/jest/matchers/schedulerTestMatchers.js @@ -48,6 +48,15 @@ function toFlushWithoutYielding(Scheduler) { return toFlushAndYield(Scheduler, []); } +function toFlushExpired(Scheduler, expectedYields) { + assertYieldsWereCleared(Scheduler); + Scheduler.unstable_flushExpired(); + const actualYields = Scheduler.unstable_clearYields(); + return captureAssertion(() => { + expect(actualYields).toEqual(expectedYields); + }); +} + function toHaveYielded(Scheduler, expectedYields) { return captureAssertion(() => { const actualYields = Scheduler.unstable_clearYields(); @@ -68,6 +77,7 @@ module.exports = { toFlushAndYield, toFlushAndYieldThrough, toFlushWithoutYielding, + toFlushExpired, toHaveYielded, toFlushAndThrow, }; diff --git a/scripts/jest/setupNewScheduler.js b/scripts/jest/setupOldScheduler.js similarity index 78% rename from scripts/jest/setupNewScheduler.js rename to scripts/jest/setupOldScheduler.js index d3d58bd5653db..591a03e119366 100644 --- a/scripts/jest/setupNewScheduler.js +++ b/scripts/jest/setupOldScheduler.js @@ -2,6 +2,6 @@ jest.mock('shared/ReactFeatureFlags', () => { const ReactFeatureFlags = require.requireActual('shared/ReactFeatureFlags'); - ReactFeatureFlags.enableNewScheduler = true; + ReactFeatureFlags.enableNewScheduler = false; return ReactFeatureFlags; });