diff --git a/fixtures/scheduler/index.html b/fixtures/scheduler/index.html index a9ae2ded7ef46..f522e2534240b 100644 --- a/fixtures/scheduler/index.html +++ b/fixtures/scheduler/index.html @@ -1,8 +1,9 @@ - - - Scheduler Test Page + + + + Scheduler Test Page - - -

Scheduler Fixture

-

- This fixture is for manual testing purposes, and the patterns used in - implementing it should not be used as a model. This is mainly for anyone - working on making changes to the `schedule` module. -

-

Tests:

-
    -
  1. - -

    Calls the callback within the frame when not blocked:

    -
    Expected:
    -
    -
    -
    -------------------------------------------------
    -
    If you see the same above and below it's correct. + + + +

    Scheduler Fixture

    +

    + This fixture is for manual testing purposes, and the patterns used in + implementing it should not be used as a model. This is mainly for anyone + working on making changes to the `schedule` module. +

    +

    Tests:

    +
      +
    1. + +

      Calls the callback within the frame when not blocked:

      +
      Expected:
      +
      +
      +
      -------------------------------------------------
      +
      If you see the same above and below it's correct.
      -------------------------------------------------
      Actual:
      -
    2. -
    3. -

      Accepts multiple callbacks and calls within frame when not blocked

      - -
      Expected:
      -
      -
      -
      -------------------------------------------------
      -
      If you see the same above and below it's correct. +
    4. +
    5. +

      Accepts multiple callbacks and calls within frame when not blocked

      + +
      Expected:
      +
      +
      +
      -------------------------------------------------
      +
      If you see the same above and below it's correct.
      -------------------------------------------------
      Actual:
      -
    6. -
    7. -

      Schedules callbacks in correct order when they use scheduleWork to schedule themselves

      - -
      Expected:
      -
      -
      -
      -------------------------------------------------
      -
      If you see the same above and below it's correct. +
    8. +
    9. +

      Schedules callbacks in correct order when they use scheduleWork to schedule themselves

      + +
      Expected:
      +
      +
      +
      -------------------------------------------------
      +
      If you see the same above and below it's correct.
      -------------------------------------------------
      Actual:
      -
    10. -
    11. -

      Calls timed out callbacks and then any more pending callbacks, defers others if time runs out

      - -
      Expected:
      -
      -
      -
      -------------------------------------------------
      -
      If you see the same above and below it's correct. +
    12. +
    13. +

      Calls timed out callbacks and then any more pending callbacks, defers others if time runs out

      + +
      Expected:
      +
      +
      +
      -------------------------------------------------
      +
      If you see the same above and below it's correct.
      -------------------------------------------------
      Actual:
      -
    14. -
    15. -

      When some callbacks throw errors, still calls them all within the same frame

      -

      IMPORTANT: Open the console when you run this! Inspect the logs there!

      - -
    16. -
    17. -

      When some callbacks throw errors and some also time out, still calls them all within the same frame

      -

      IMPORTANT: Open the console when you run this! Inspect the logs there!

      - -
    18. -
    19. -

      Continues calling callbacks even when user switches away from this tab

      - -
      Click the button above, observe the counter, then switch to - another tab and switch back:
      -
      -
      -
      If the counter advanced while you were away from this tab, it's correct.
      -
    20. -
    - - - - + + + - + \ No newline at end of file diff --git a/fixtures/tracing/script.js b/fixtures/tracing/script.js index 309efcf2a7725..48d98308eacbe 100644 --- a/fixtures/tracing/script.js +++ b/fixtures/tracing/script.js @@ -30,8 +30,8 @@ function checkSchedulerAPI() { if ( typeof Scheduler === 'undefined' || typeof Scheduler.unstable_now !== 'function' || - typeof Scheduler.unstable_scheduleWork !== 'function' || - typeof Scheduler.unstable_cancelScheduledWork !== 'function' + typeof Scheduler.unstable_scheduleCallback !== 'function' || + typeof Scheduler.unstable_cancelCallback !== 'function' ) { throw 'API is not defined'; } diff --git a/fixtures/unstable-async/suspense/src/components/App.js b/fixtures/unstable-async/suspense/src/components/App.js index 82198ce8d8c79..26d16f7d88c9e 100644 --- a/fixtures/unstable-async/suspense/src/components/App.js +++ b/fixtures/unstable-async/suspense/src/components/App.js @@ -1,5 +1,5 @@ import React, {Placeholder, PureComponent} from 'react'; -import {unstable_scheduleWork} from 'scheduler'; +import {unstable_scheduleCallback} from 'scheduler'; import { unstable_trace as trace, unstable_wrap as wrap, @@ -38,7 +38,7 @@ export default class App extends PureComponent { currentId: id, }) ); - unstable_scheduleWork( + unstable_scheduleCallback( wrap(() => trace(`View ${id} (low-pri)`, performance.now(), () => this.setState({ diff --git a/fixtures/unstable-async/time-slicing/src/index.js b/fixtures/unstable-async/time-slicing/src/index.js index 291bc8ab13fe5..0b1436e04eceb 100644 --- a/fixtures/unstable-async/time-slicing/src/index.js +++ b/fixtures/unstable-async/time-slicing/src/index.js @@ -1,6 +1,6 @@ import React, {PureComponent} from 'react'; import {flushSync, render} from 'react-dom'; -import {unstable_scheduleWork} from 'scheduler'; +import {unstable_scheduleCallback} from 'scheduler'; import _ from 'lodash'; import Charts from './Charts'; import Clock from './Clock'; @@ -67,7 +67,7 @@ class App extends PureComponent { } this._ignoreClick = true; - unstable_scheduleWork(() => { + unstable_scheduleCallback(() => { this.setState({showDemo: true}, () => { this._ignoreClick = false; }); @@ -107,7 +107,7 @@ class App extends PureComponent { this.debouncedHandleChange(value); break; case 'async': - unstable_scheduleWork(() => { + unstable_scheduleCallback(() => { this.setState({value}); }); break; diff --git a/packages/react-art/src/ReactARTHostConfig.js b/packages/react-art/src/ReactARTHostConfig.js index ea75201cb4872..bd7f8d043f4c5 100644 --- a/packages/react-art/src/ReactARTHostConfig.js +++ b/packages/react-art/src/ReactARTHostConfig.js @@ -7,8 +7,8 @@ export { unstable_now as now, - unstable_scheduleWork as scheduleDeferredCallback, - unstable_cancelScheduledWork as cancelDeferredCallback, + unstable_scheduleCallback as scheduleDeferredCallback, + unstable_cancelCallback as cancelDeferredCallback, } from 'scheduler'; import Transform from 'art/core/transform'; import Mode from 'art/modes/current'; diff --git a/packages/react-dom/src/client/ReactDOMHostConfig.js b/packages/react-dom/src/client/ReactDOMHostConfig.js index fedb694200c9a..a9dd23f2fac3c 100644 --- a/packages/react-dom/src/client/ReactDOMHostConfig.js +++ b/packages/react-dom/src/client/ReactDOMHostConfig.js @@ -62,8 +62,8 @@ export type NoTimeout = -1; export { unstable_now as now, - unstable_scheduleWork as scheduleDeferredCallback, - unstable_cancelScheduledWork as cancelDeferredCallback, + unstable_scheduleCallback as scheduleDeferredCallback, + unstable_cancelCallback as cancelDeferredCallback, } from 'scheduler'; let SUPPRESS_HYDRATION_WARNING; diff --git a/packages/react/src/ReactSharedInternals.js b/packages/react/src/ReactSharedInternals.js index 34db8d778ae71..c6cf7ded26b92 100644 --- a/packages/react/src/ReactSharedInternals.js +++ b/packages/react/src/ReactSharedInternals.js @@ -7,9 +7,12 @@ import assign from 'object-assign'; import { - unstable_cancelScheduledWork, + unstable_cancelCallback, unstable_now, - unstable_scheduleWork, + unstable_scheduleCallback, + unstable_runWithPriority, + unstable_wrapCallback, + unstable_getCurrentPriorityLevel, } from 'scheduler'; import { __interactionsRef, @@ -39,9 +42,12 @@ if (__UMD__) { // CJS bundles use the shared NPM package. Object.assign(ReactSharedInternals, { Scheduler: { - unstable_cancelScheduledWork, + unstable_cancelCallback, unstable_now, - unstable_scheduleWork, + unstable_scheduleCallback, + unstable_runWithPriority, + unstable_wrapCallback, + unstable_getCurrentPriorityLevel, }, SchedulerTracing: { __interactionsRef, diff --git a/packages/scheduler/npm/umd/scheduler.development.js b/packages/scheduler/npm/umd/scheduler.development.js index 67f9f903956d5..21c4c70dc1dc4 100644 --- a/packages/scheduler/npm/umd/scheduler.development.js +++ b/packages/scheduler/npm/umd/scheduler.development.js @@ -7,6 +7,8 @@ * LICENSE file in the root directory of this source tree. */ +/* eslint-disable max-len */ + 'use strict'; (function(global, factory) { @@ -23,15 +25,36 @@ ); } - function unstable_scheduleWork() { - return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_scheduleWork.apply( + function unstable_scheduleCallback() { + return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_scheduleCallback.apply( + this, + arguments + ); + } + + function unstable_cancelCallback() { + return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_cancelCallback.apply( + this, + arguments + ); + } + + function unstable_runWithPriority() { + return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_runWithPriority.apply( + this, + arguments + ); + } + + function unstable_wrapCallback() { + return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_wrapCallback.apply( this, arguments ); } - function unstable_cancelScheduledWork() { - return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_cancelScheduledWork.apply( + function unstable_getCurrentPriorityLevel() { + return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_getCurrentPriorityLevel.apply( this, arguments ); @@ -39,7 +62,10 @@ return Object.freeze({ unstable_now: unstable_now, - unstable_scheduleWork: unstable_scheduleWork, - unstable_cancelScheduledWork: unstable_cancelScheduledWork, + unstable_scheduleCallback: unstable_scheduleCallback, + unstable_cancelCallback: unstable_cancelCallback, + unstable_runWithPriority: unstable_runWithPriority, + unstable_wrapCallback: unstable_wrapCallback, + unstable_getCurrentPriorityLevel: unstable_getCurrentPriorityLevel, }); }); diff --git a/packages/scheduler/npm/umd/scheduler.production.min.js b/packages/scheduler/npm/umd/scheduler.production.min.js index 67f9f903956d5..21c4c70dc1dc4 100644 --- a/packages/scheduler/npm/umd/scheduler.production.min.js +++ b/packages/scheduler/npm/umd/scheduler.production.min.js @@ -7,6 +7,8 @@ * LICENSE file in the root directory of this source tree. */ +/* eslint-disable max-len */ + 'use strict'; (function(global, factory) { @@ -23,15 +25,36 @@ ); } - function unstable_scheduleWork() { - return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_scheduleWork.apply( + function unstable_scheduleCallback() { + return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_scheduleCallback.apply( + this, + arguments + ); + } + + function unstable_cancelCallback() { + return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_cancelCallback.apply( + this, + arguments + ); + } + + function unstable_runWithPriority() { + return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_runWithPriority.apply( + this, + arguments + ); + } + + function unstable_wrapCallback() { + return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_wrapCallback.apply( this, arguments ); } - function unstable_cancelScheduledWork() { - return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_cancelScheduledWork.apply( + function unstable_getCurrentPriorityLevel() { + return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_getCurrentPriorityLevel.apply( this, arguments ); @@ -39,7 +62,10 @@ return Object.freeze({ unstable_now: unstable_now, - unstable_scheduleWork: unstable_scheduleWork, - unstable_cancelScheduledWork: unstable_cancelScheduledWork, + unstable_scheduleCallback: unstable_scheduleCallback, + unstable_cancelCallback: unstable_cancelCallback, + unstable_runWithPriority: unstable_runWithPriority, + unstable_wrapCallback: unstable_wrapCallback, + unstable_getCurrentPriorityLevel: unstable_getCurrentPriorityLevel, }); }); diff --git a/packages/scheduler/npm/umd/scheduler.profiling.min.js b/packages/scheduler/npm/umd/scheduler.profiling.min.js index 67f9f903956d5..21c4c70dc1dc4 100644 --- a/packages/scheduler/npm/umd/scheduler.profiling.min.js +++ b/packages/scheduler/npm/umd/scheduler.profiling.min.js @@ -7,6 +7,8 @@ * LICENSE file in the root directory of this source tree. */ +/* eslint-disable max-len */ + 'use strict'; (function(global, factory) { @@ -23,15 +25,36 @@ ); } - function unstable_scheduleWork() { - return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_scheduleWork.apply( + function unstable_scheduleCallback() { + return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_scheduleCallback.apply( + this, + arguments + ); + } + + function unstable_cancelCallback() { + return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_cancelCallback.apply( + this, + arguments + ); + } + + function unstable_runWithPriority() { + return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_runWithPriority.apply( + this, + arguments + ); + } + + function unstable_wrapCallback() { + return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_wrapCallback.apply( this, arguments ); } - function unstable_cancelScheduledWork() { - return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_cancelScheduledWork.apply( + function unstable_getCurrentPriorityLevel() { + return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_getCurrentPriorityLevel.apply( this, arguments ); @@ -39,7 +62,10 @@ return Object.freeze({ unstable_now: unstable_now, - unstable_scheduleWork: unstable_scheduleWork, - unstable_cancelScheduledWork: unstable_cancelScheduledWork, + unstable_scheduleCallback: unstable_scheduleCallback, + unstable_cancelCallback: unstable_cancelCallback, + unstable_runWithPriority: unstable_runWithPriority, + unstable_wrapCallback: unstable_wrapCallback, + unstable_getCurrentPriorityLevel: unstable_getCurrentPriorityLevel, }); }); diff --git a/packages/scheduler/src/Scheduler.js b/packages/scheduler/src/Scheduler.js index 07bade1aee5b2..412f96930eb36 100644 --- a/packages/scheduler/src/Scheduler.js +++ b/packages/scheduler/src/Scheduler.js @@ -8,14 +8,34 @@ /* eslint-disable no-var */ -// TODO: Currently there's only a single priority level, Deferred. Will add -// additional priorities. -var DEFERRED_TIMEOUT = 5000; +// TODO: Use symbols? +var ImmediatePriority = 1; +var InteractivePriority = 2; +var NormalPriority = 3; +var WheneverPriority = 4; + +// Max 31 bit integer. The max integer size in V8 for 32-bit systems. +// Math.pow(2, 30) - 1 +// 0b111111111111111111111111111111 +var maxSigned31BitInt = 1073741823; + +// Times out immediately +var IMMEDIATE_PRIORITY_TIMEOUT = -1; +// Eventually times out +var INTERACTIVE_PRIORITY_TIMEOUT = 250; +var NORMAL_PRIORITY_TIMEOUT = 5000; +// Never times out +var WHENEVER_PRIORITY_TIMEOUT = maxSigned31BitInt; // Callbacks are stored as a circular, doubly linked list. var firstCallbackNode = null; -var isPerformingWork = false; +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; var isHostCallbackScheduled = false; @@ -25,6 +45,14 @@ var hasNativePerformanceNow = var timeRemaining; if (hasNativePerformanceNow) { timeRemaining = function() { + if ( + firstCallbackNode !== null && + firstCallbackNode.expirationTime < currentExpirationTime + ) { + // A higher priority callback was scheduled. Yield so we can switch to + // working on that. + return 0; + } // We assume that if we have a performance timer that the rAF callback // gets a performance timer value. Not sure if this is always true. var remaining = getFrameDeadline() - performance.now(); @@ -33,6 +61,12 @@ if (hasNativePerformanceNow) { } else { timeRemaining = function() { // Fallback to Date.now() + if ( + firstCallbackNode !== null && + firstCallbackNode.expirationTime < currentExpirationTime + ) { + return 0; + } var remaining = getFrameDeadline() - Date.now(); return remaining > 0 ? remaining : 0; }; @@ -44,22 +78,22 @@ var deadlineObject = { }; function ensureHostCallbackIsScheduled() { - if (isPerformingWork) { + if (isExecutingCallback) { // Don't schedule work yet; wait until the next time we yield. return; } - // Schedule the host callback using the earliest timeout in the list. - var timesOutAt = firstCallbackNode.timesOutAt; + // 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. - cancelCallback(); + cancelHostCallback(); } - requestCallback(flushWork, timesOutAt); + requestHostCallback(flushWork, expirationTime); } -function flushFirstCallback(node) { +function flushFirstCallback() { var flushedNode = firstCallbackNode; // Remove the node from the list before calling the callback. That way the @@ -70,35 +104,124 @@ function flushFirstCallback(node) { firstCallbackNode = null; next = null; } else { - var previous = firstCallbackNode.previous; - firstCallbackNode = previous.next = next; - next.previous = previous; + var lastCallbackNode = firstCallbackNode.previous; + firstCallbackNode = lastCallbackNode.next = next; + next.previous = lastCallbackNode; } flushedNode.next = flushedNode.previous = null; // Now it's safe to call the callback. var callback = flushedNode.callback; - callback(deadlineObject); + var expirationTime = flushedNode.expirationTime; + var priorityLevel = flushedNode.priorityLevel; + var previousPriorityLevel = currentPriorityLevel; + var previousExpirationTime = currentExpirationTime; + currentPriorityLevel = priorityLevel; + currentExpirationTime = expirationTime; + var continuationCallback; + try { + continuationCallback = callback(deadlineObject); + } finally { + currentPriorityLevel = previousPriorityLevel; + currentExpirationTime = previousExpirationTime; + } + + // 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') { + var continuationNode: CallbackNode = { + callback: continuationCallback, + priorityLevel, + expirationTime, + next: null, + previous: null, + }; + + // Insert the new callback into the list, sorted by its expiration. This is + // almost the same as the code in `scheduleCallback`, except the callback + // is inserted into the list *before* callbacks of equal expiration instead + // of after. + if (firstCallbackNode === null) { + // This is the first callback in the list. + firstCallbackNode = continuationNode.next = continuationNode.previous = continuationNode; + } else { + var nextAfterContinuation = null; + var node = firstCallbackNode; + do { + if (node.expirationTime >= expirationTime) { + // This callback expires at or after the continuation. We will insert + // the continuation *before* this callback. + nextAfterContinuation = node; + break; + } + node = node.next; + } while (node !== firstCallbackNode); + + if (nextAfterContinuation === null) { + // No equal or lower priority callback was found, which means the new + // callback is the lowest priority callback in the list. + nextAfterContinuation = firstCallbackNode; + } else if (nextAfterContinuation === firstCallbackNode) { + // The new callback is the highest priority callback in the list. + firstCallbackNode = continuationNode; + ensureHostCallbackIsScheduled(firstCallbackNode); + } + + var previous = nextAfterContinuation.previous; + previous.next = nextAfterContinuation.previous = continuationNode; + continuationNode.next = nextAfterContinuation; + continuationNode.previous = previous; + } + } +} + +function flushImmediateWork() { + if ( + // Confirm we've exited the outer most event handler + currentEventStartTime === -1 && + firstCallbackNode !== null && + firstCallbackNode.priorityLevel === ImmediatePriority + ) { + isExecutingCallback = true; + deadlineObject.didTimeout = 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(firstCallbackNode); + } else { + isHostCallbackScheduled = false; + } + } + } } function flushWork(didTimeout) { - isPerformingWork = true; + isExecutingCallback = true; deadlineObject.didTimeout = didTimeout; try { if (didTimeout) { - // Flush all the timed out callbacks without yielding. + // Flush all the expired callbacks without yielding. while (firstCallbackNode !== null) { // Read the current time. Flush all the callbacks that expire at or // earlier than that time. Then read the current time again and repeat. // This optimizes for as few performance.now calls as possible. var currentTime = getCurrentTime(); - if (firstCallbackNode.timesOutAt <= currentTime) { + if (firstCallbackNode.expirationTime <= currentTime) { do { flushFirstCallback(); } while ( firstCallbackNode !== null && - firstCallbackNode.timesOutAt <= currentTime + firstCallbackNode.expirationTime <= currentTime ); continue; } @@ -116,41 +239,104 @@ function flushWork(didTimeout) { } } } finally { - isPerformingWork = false; + isExecutingCallback = false; if (firstCallbackNode !== null) { // There's still work remaining. Request another callback. ensureHostCallbackIsScheduled(firstCallbackNode); } else { isHostCallbackScheduled = false; } + // Before exiting, flush all the immediate work that was scheduled. + flushImmediateWork(); } } -function unstable_scheduleWork(callback, options) { - var currentTime = getCurrentTime(); +function unstable_runWithPriority(priorityLevel, eventHandler) { + switch (priorityLevel) { + case ImmediatePriority: + case InteractivePriority: + case NormalPriority: + case WheneverPriority: + break; + default: + priorityLevel = NormalPriority; + } + + var previousPriorityLevel = currentPriorityLevel; + var previousEventStartTime = currentEventStartTime; + currentPriorityLevel = priorityLevel; + currentEventStartTime = getCurrentTime(); + + try { + return eventHandler(); + } finally { + currentPriorityLevel = previousPriorityLevel; + currentEventStartTime = previousEventStartTime; + + // Before exiting, flush all the immediate work that was scheduled. + flushImmediateWork(); + } +} - var timesOutAt; +function unstable_wrapCallback(callback) { + var parentPriorityLevel = currentPriorityLevel; + return function() { + // This is a fork of runWithPriority, inlined for performance. + var previousPriorityLevel = currentPriorityLevel; + var previousEventStartTime = currentEventStartTime; + currentPriorityLevel = parentPriorityLevel; + currentEventStartTime = getCurrentTime(); + + try { + return callback.apply(this, arguments); + } finally { + currentPriorityLevel = previousPriorityLevel; + currentEventStartTime = previousEventStartTime; + flushImmediateWork(); + } + }; +} + +function unstable_scheduleCallback(callback, deprecated_options) { + var startTime = + currentEventStartTime !== -1 ? currentEventStartTime : getCurrentTime(); + + var expirationTime; if ( - options !== undefined && - options !== null && - options.timeout !== null && - options.timeout !== undefined + typeof deprecated_options === 'object' && + deprecated_options !== null && + typeof deprecated_options.timeout === 'number' ) { - // Check for an explicit timeout - timesOutAt = currentTime + options.timeout; + // FIXME: Remove this branch once we lift expiration times out of React. + expirationTime = startTime + deprecated_options.timeout; } else { - // Compute an absolute timeout using the default constant. - timesOutAt = currentTime + DEFERRED_TIMEOUT; + switch (currentPriorityLevel) { + case ImmediatePriority: + expirationTime = startTime + IMMEDIATE_PRIORITY_TIMEOUT; + break; + case InteractivePriority: + expirationTime = startTime + INTERACTIVE_PRIORITY_TIMEOUT; + break; + case WheneverPriority: + expirationTime = startTime + WHENEVER_PRIORITY_TIMEOUT; + break; + case NormalPriority: + default: + expirationTime = startTime + NORMAL_PRIORITY_TIMEOUT; + } } var newNode = { callback, - timesOutAt, + priorityLevel: currentPriorityLevel, + expirationTime, next: null, previous: null, }; - // Insert the new callback into the list, sorted by its timeout. + // Insert the new callback into the list, ordered first by expiration, then + // by insertion. So the new callback is inserted any other callback with + // equal expiration. if (firstCallbackNode === null) { // This is the first callback in the list. firstCallbackNode = newNode.next = newNode.previous = newNode; @@ -159,8 +345,8 @@ function unstable_scheduleWork(callback, options) { var next = null; var node = firstCallbackNode; do { - if (node.timesOutAt > timesOutAt) { - // The new callback times out before this one. + if (node.expirationTime > expirationTime) { + // The new callback expires before this one. next = node; break; } @@ -168,11 +354,11 @@ function unstable_scheduleWork(callback, options) { } while (node !== firstCallbackNode); if (next === null) { - // No callback with a later timeout was found, which means the new - // callback has the latest timeout in the list. + // No callback with a later expiration was found, which means the new + // callback has the latest expiration in the list. next = firstCallbackNode; } else if (next === firstCallbackNode) { - // The new callback has the earliest timeout in the entire list. + // The new callback has the earliest expiration in the entire list. firstCallbackNode = newNode; ensureHostCallbackIsScheduled(firstCallbackNode); } @@ -186,7 +372,7 @@ function unstable_scheduleWork(callback, options) { return newNode; } -function unstable_cancelScheduledWork(callbackNode) { +function unstable_cancelCallback(callbackNode) { var next = callbackNode.next; if (next === null) { // Already cancelled. @@ -209,6 +395,10 @@ function unstable_cancelScheduledWork(callbackNode) { callbackNode.next = callbackNode.previous = null; } +function unstable_getCurrentPriorityLevel() { + return currentPriorityLevel; +} + // The remaining code is essentially a polyfill for requestIdleCallback. It // works by scheduling a requestAnimationFrame, storing the time for the start // of the frame, then scheduling a postMessage which gets scheduled after paint. @@ -274,18 +464,18 @@ if (hasNativePerformanceNow) { }; } -var requestCallback; -var cancelCallback; +var requestHostCallback; +var cancelHostCallback; var getFrameDeadline; if (typeof window === 'undefined') { // If this accidentally gets imported in a non-browser environment, fallback // to a naive implementation. var timeoutID = -1; - requestCallback = function(callback, absoluteTimeout) { + requestHostCallback = function(callback, absoluteTimeout) { timeoutID = setTimeout(callback, 0, true); }; - cancelCallback = function() { + cancelHostCallback = function() { clearTimeout(timeoutID); }; getFrameDeadline = function() { @@ -294,11 +484,12 @@ if (typeof window === 'undefined') { } else if (window._schedMock) { // Dynamic injection, only for testing purposes. var impl = window._schedMock; - requestCallback = impl[0]; - cancelCallback = impl[1]; + requestHostCallback = impl[0]; + cancelHostCallback = impl[1]; getFrameDeadline = impl[2]; } else { if (typeof console !== 'undefined') { + // TODO: Remove fb.me link if (typeof localRequestAnimationFrame !== 'function') { console.error( "This browser doesn't support requestAnimationFrame. " + @@ -416,11 +607,10 @@ if (typeof window === 'undefined') { } }; - requestCallback = function(callback, absoluteTimeout) { + requestHostCallback = function(callback, absoluteTimeout) { scheduledCallback = callback; timeoutTime = absoluteTimeout; - if (isPerformingIdleWork) { - // If we're already performing idle work, an error must have been thrown. + if (isPerformingIdleWork || absoluteTimeout < 0) { // Don't wait for the next frame. Continue working ASAP, in a new event. window.postMessage(messageKey, '*'); } else if (!isAnimationFrameScheduled) { @@ -433,7 +623,7 @@ if (typeof window === 'undefined') { } }; - cancelCallback = function() { + cancelHostCallback = function() { scheduledCallback = null; isIdleScheduled = false; timeoutTime = -1; @@ -441,7 +631,14 @@ if (typeof window === 'undefined') { } export { - unstable_scheduleWork, - unstable_cancelScheduledWork, + ImmediatePriority as unstable_ImmediatePriority, + InteractivePriority as unstable_InteractivePriority, + NormalPriority as unstable_NormalPriority, + WheneverPriority as unstable_WheneverPriority, + unstable_runWithPriority, + unstable_scheduleCallback, + unstable_cancelCallback, + unstable_wrapCallback, + unstable_getCurrentPriorityLevel, getCurrentTime as unstable_now, }; diff --git a/packages/scheduler/src/__tests__/Scheduler-test.internal.js b/packages/scheduler/src/__tests__/Scheduler-test.internal.js index eaaae4ec911ce..f96b690a356ff 100644 --- a/packages/scheduler/src/__tests__/Scheduler-test.internal.js +++ b/packages/scheduler/src/__tests__/Scheduler-test.internal.js @@ -9,8 +9,14 @@ 'use strict'; -let scheduleWork; -let cancelScheduledWork; +let runWithPriority; +let ImmediatePriority; +let InteractivePriority; +let NormalPriority; +let scheduleCallback; +let cancelCallback; +let wrapCallback; +let getCurrentPriorityLevel; let flushWork; let advanceTime; let doWork; @@ -24,12 +30,16 @@ describe('Scheduler', () => { jest.resetModules(); let _flushWork = null; + let isFlushing = false; let timeoutID = -1; let endOfFrame = -1; let currentTime = 0; flushWork = frameSize => { + if (isFlushing) { + throw new Error('Already flushing work.'); + } if (frameSize === null || frameSize === undefined) { frameSize = Infinity; } @@ -39,8 +49,10 @@ describe('Scheduler', () => { timeoutID = -1; endOfFrame = currentTime + frameSize; try { - _flushWork(); + isFlushing = true; + _flushWork(false); } finally { + isFlushing = false; endOfFrame = -1; } const yields = yieldedValues; @@ -54,6 +66,9 @@ describe('Scheduler', () => { }; doWork = (label, timeCost) => { + if (typeof timeCost !== 'number') { + throw new Error('Second arg must be a number.'); + } advanceTime(timeCost); yieldValue(label); }; @@ -69,16 +84,32 @@ describe('Scheduler', () => { return yields; }; - function requestCallback(fw, absoluteTimeout) { + function onTimeout() { + if (_flushWork === null) { + return; + } + if (isFlushing) { + // Jest fires timers synchronously when jest.advanceTimersByTime is + // called. Use setImmediate to prevent re-entrancy. + setImmediate(onTimeout); + } else { + try { + isFlushing = true; + _flushWork(true); + } finally { + isFlushing = false; + } + } + } + + function requestHostCallback(fw, absoluteTimeout) { if (_flushWork !== null) { throw new Error('Work is already scheduled.'); } _flushWork = fw; - timeoutID = setTimeout(() => { - _flushWork(true); - }, absoluteTimeout - currentTime); + timeoutID = setTimeout(onTimeout, absoluteTimeout - currentTime); } - function cancelCallback() { + function cancelHostCallback() { if (_flushWork === null) { throw new Error('No work is scheduled.'); } @@ -91,19 +122,31 @@ describe('Scheduler', () => { // Override host implementation delete global.performance; - global.Date.now = () => currentTime; - window._schedMock = [requestCallback, cancelCallback, getTimeRemaining]; + global.Date.now = () => { + return currentTime; + }; + window._schedMock = [ + requestHostCallback, + cancelHostCallback, + getTimeRemaining, + ]; - const Scheduler = require('scheduler'); - scheduleWork = Scheduler.unstable_scheduleWork; - cancelScheduledWork = Scheduler.unstable_cancelScheduledWork; + const Schedule = require('scheduler'); + runWithPriority = Schedule.unstable_runWithPriority; + ImmediatePriority = Schedule.unstable_ImmediatePriority; + InteractivePriority = Schedule.unstable_InteractivePriority; + NormalPriority = Schedule.unstable_NormalPriority; + scheduleCallback = Schedule.unstable_scheduleCallback; + cancelCallback = Schedule.unstable_cancelCallback; + wrapCallback = Schedule.unstable_wrapCallback; + getCurrentPriorityLevel = Schedule.unstable_getCurrentPriorityLevel; }); it('flushes work incrementally', () => { - scheduleWork(() => doWork('A', 100)); - scheduleWork(() => doWork('B', 200)); - scheduleWork(() => doWork('C', 300)); - scheduleWork(() => doWork('D', 400)); + scheduleCallback(() => doWork('A', 100)); + scheduleCallback(() => doWork('B', 200)); + scheduleCallback(() => doWork('C', 300)); + scheduleCallback(() => doWork('D', 400)); expect(flushWork(300)).toEqual(['A', 'B']); expect(flushWork(300)).toEqual(['C']); @@ -111,11 +154,11 @@ describe('Scheduler', () => { }); it('cancels work', () => { - scheduleWork(() => doWork('A', 100)); - const callbackHandleB = scheduleWork(() => doWork('B', 200)); - scheduleWork(() => doWork('C', 300)); + scheduleCallback(() => doWork('A', 100)); + const callbackHandleB = scheduleCallback(() => doWork('B', 200)); + scheduleCallback(() => doWork('C', 300)); - cancelScheduledWork(callbackHandleB); + cancelCallback(callbackHandleB); expect(flushWork()).toEqual([ 'A', @@ -124,51 +167,336 @@ describe('Scheduler', () => { ]); }); - it('prioritizes callbacks according to their timeouts', () => { - scheduleWork(() => doWork('A', 10), {timeout: 5000}); - scheduleWork(() => doWork('B', 20), {timeout: 5000}); - scheduleWork(() => doWork('C', 30), {timeout: 1000}); - scheduleWork(() => doWork('D', 40), {timeout: 5000}); + it('executes the highest priority callbacks first', () => { + scheduleCallback(() => doWork('A', 100)); + scheduleCallback(() => doWork('B', 100)); + + // Yield before B is flushed + expect(flushWork(100)).toEqual(['A']); + + runWithPriority(InteractivePriority, () => { + scheduleCallback(() => doWork('C', 100)); + scheduleCallback(() => doWork('D', 100)); + }); + + // C and D should come first, because they are higher priority + expect(flushWork()).toEqual(['C', 'D', 'B']); + }); + + it('expires work', () => { + scheduleCallback(() => doWork('A', 100)); + runWithPriority(InteractivePriority, () => { + scheduleCallback(() => doWork('B', 100)); + }); + scheduleCallback(() => doWork('C', 100)); + runWithPriority(InteractivePriority, () => { + scheduleCallback(() => doWork('D', 100)); + }); + + // Advance time, but not by enough to expire any work + advanceTime(249); + expect(clearYieldedValues()).toEqual([]); + + // Advance by just a bit more to expire the high pri callbacks + advanceTime(1); + expect(clearYieldedValues()).toEqual(['B', 'D']); - // C should be first because it has the earliest timeout - expect(flushWork()).toEqual(['C', 'A', 'B', 'D']); + // Expire the rest + advanceTime(10000); + expect(clearYieldedValues()).toEqual(['A', 'C']); }); - it('times out work', () => { - scheduleWork(() => doWork('A', 100), {timeout: 5000}); - scheduleWork(() => doWork('B', 200), {timeout: 5000}); - scheduleWork(() => doWork('C', 300), {timeout: 1000}); - scheduleWork(() => doWork('D', 400), {timeout: 5000}); + it('has a default expiration of ~5 seconds', () => { + scheduleCallback(() => doWork('A', 100)); - // Advance time, but not by enough to flush any work - advanceTime(999); + advanceTime(4999); expect(clearYieldedValues()).toEqual([]); - // Advance by just a bit more to flush C advanceTime(1); - expect(clearYieldedValues()).toEqual(['C']); + expect(clearYieldedValues()).toEqual(['A']); + }); + + it('continues working on same task after yielding', () => { + scheduleCallback(() => doWork('A', 100)); + scheduleCallback(() => doWork('B', 100)); + + const tasks = [['C1', 100], ['C2', 100], ['C3', 100]]; + const C = deadline => { + while (tasks.length > 0) { + doWork(...tasks.shift()); + if ( + tasks.length > 0 && + !deadline.didTimeout && + deadline.timeRemaining() <= 0 + ) { + yieldValue('Yield!'); + return C; + } + } + }; + + scheduleCallback(C); + + scheduleCallback(() => doWork('D', 100)); + scheduleCallback(() => doWork('E', 100)); + + expect(flushWork(300)).toEqual(['A', 'B', 'C1', 'Yield!']); + + expect(flushWork()).toEqual(['C2', 'C3', 'D', 'E']); + }); + + it('continuation callbacks inherit the expiration of the previous callback', () => { + const tasks = [['A', 125], ['B', 125], ['C', 125], ['D', 125]]; + const work = deadline => { + while (tasks.length > 0) { + doWork(...tasks.shift()); + if ( + tasks.length > 0 && + !deadline.didTimeout && + deadline.timeRemaining() <= 0 + ) { + yieldValue('Yield!'); + return work; + } + } + }; + + // Schedule a high priority callback + runWithPriority(InteractivePriority, () => scheduleCallback(work)); + + // Flush until just before the expiration time + expect(flushWork(249)).toEqual(['A', 'B', 'Yield!']); + + // Advance time by just a bit more. This should expire all the remaining work. + advanceTime(1); + expect(clearYieldedValues()).toEqual(['C', 'D']); + }); + + it('nested callbacks inherit the priority of the currently executing callback', () => { + runWithPriority(InteractivePriority, () => { + scheduleCallback(() => { + doWork('Parent callback', 100); + scheduleCallback(() => { + doWork('Nested callback', 100); + }); + }); + }); - // Flush the rest - advanceTime(4000); - expect(clearYieldedValues()).toEqual(['A', 'B', 'D']); + expect(flushWork(100)).toEqual(['Parent callback']); + + // The nested callback has interactive priority, so it should + // expire quickly. + advanceTime(250 + 100); + expect(clearYieldedValues()).toEqual(['Nested callback']); }); - it('has a default timeout of 5 seconds', () => { - scheduleWork(() => doWork('A', 100)); - scheduleWork(() => doWork('B', 200)); - scheduleWork(() => doWork('C', 300), {timeout: 1000}); - scheduleWork(() => doWork('D', 400)); + it('continuations are interrupted by higher priority work', () => { + const tasks = [['A', 100], ['B', 100], ['C', 100], ['D', 100]]; + const work = deadline => { + while (tasks.length > 0) { + doWork(...tasks.shift()); + if ( + tasks.length > 0 && + !deadline.didTimeout && + deadline.timeRemaining() <= 0 + ) { + yieldValue('Yield!'); + return work; + } + } + }; + scheduleCallback(work); + expect(flushWork(100)).toEqual(['A', 'Yield!']); + + runWithPriority(InteractivePriority, () => { + scheduleCallback(() => doWork('High pri', 100)); + }); + + expect(flushWork()).toEqual(['High pri', 'B', 'C', 'D']); + }); - // Flush C - advanceTime(1000); - expect(clearYieldedValues()).toEqual(['C']); + it( + 'continutations are interrupted by higher priority work scheduled ' + + 'inside an executing callback', + () => { + const tasks = [['A', 100], ['B', 100], ['C', 100], ['D', 100]]; + const work = deadline => { + while (tasks.length > 0) { + const task = tasks.shift(); + doWork(...task); + if (task[0] === 'B') { + // Schedule high pri work from inside another callback + yieldValue('Schedule high pri'); + runWithPriority(InteractivePriority, () => + scheduleCallback(() => doWork('High pri', 100)), + ); + } + if ( + tasks.length > 0 && + !deadline.didTimeout && + deadline.timeRemaining() <= 0 + ) { + yieldValue('Yield!'); + return work; + } + } + }; + scheduleCallback(work); + expect(flushWork()).toEqual([ + 'A', + 'B', + 'Schedule high pri', + // Even though there's time left in the frame, the low pri callback + // should yield to the high pri callback + 'Yield!', + 'High pri', + // Continue low pri work + 'C', + 'D', + ]); + }, + ); - // Advance time until right before the rest of the work expires - advanceTime(3699); + it('immediate callbacks fire at the end of outermost event', () => { + runWithPriority(ImmediatePriority, () => { + scheduleCallback(() => yieldValue('A')); + scheduleCallback(() => yieldValue('B')); + // Nested event + runWithPriority(ImmediatePriority, () => { + scheduleCallback(() => yieldValue('C')); + // Nothing should have fired yet + expect(clearYieldedValues()).toEqual([]); + }); + // Nothing should have fired yet + expect(clearYieldedValues()).toEqual([]); + }); + // The callbacks were called at the end of the outer event + expect(clearYieldedValues()).toEqual(['A', 'B', 'C']); + }); + + it('wrapped callbacks have same signature as original callback', () => { + const wrappedCallback = wrapCallback((...args) => ({args})); + expect(wrappedCallback('a', 'b')).toEqual({args: ['a', 'b']}); + }); + + it('wrapped callbacks inherit the current priority', () => { + const wrappedCallback = wrapCallback(() => { + scheduleCallback(() => { + doWork('Normal', 100); + }); + }); + const wrappedInteractiveCallback = runWithPriority( + InteractivePriority, + () => + wrapCallback(() => { + scheduleCallback(() => { + doWork('Interactive', 100); + }); + }), + ); + + // This should schedule a normal callback + wrappedCallback(); + // This should schedule an interactive callback + wrappedInteractiveCallback(); + + advanceTime(249); expect(clearYieldedValues()).toEqual([]); + advanceTime(1); + expect(clearYieldedValues()).toEqual(['Interactive']); - // Now advance by just a bit more + advanceTime(10000); + expect(clearYieldedValues()).toEqual(['Normal']); + }); + + it('wrapped callbacks inherit the current priority even when nested', () => { + const wrappedCallback = wrapCallback(() => { + scheduleCallback(() => { + doWork('Normal', 100); + }); + }); + const wrappedInteractiveCallback = runWithPriority( + InteractivePriority, + () => + wrapCallback(() => { + scheduleCallback(() => { + doWork('Interactive', 100); + }); + }), + ); + + runWithPriority(InteractivePriority, () => { + // This should schedule a normal callback + wrappedCallback(); + // This should schedule an interactive callback + wrappedInteractiveCallback(); + }); + + advanceTime(249); + expect(clearYieldedValues()).toEqual([]); advanceTime(1); - expect(clearYieldedValues()).toEqual(['A', 'B', 'D']); + expect(clearYieldedValues()).toEqual(['Interactive']); + + advanceTime(10000); + expect(clearYieldedValues()).toEqual(['Normal']); + }); + + it('immediate callbacks fire at the end of callback', () => { + const immediateCallback = runWithPriority(ImmediatePriority, () => + wrapCallback(() => { + scheduleCallback(() => yieldValue('callback')); + }), + ); + immediateCallback(); + + // The callback was called at the end of the outer event + expect(clearYieldedValues()).toEqual(['callback']); + }); + + it("immediate callbacks fire even if there's an error", () => { + expect(() => { + runWithPriority(ImmediatePriority, () => { + scheduleCallback(() => { + yieldValue('A'); + throw new Error('Oops A'); + }); + scheduleCallback(() => { + yieldValue('B'); + }); + scheduleCallback(() => { + yieldValue('C'); + throw new Error('Oops C'); + }); + }); + }).toThrow('Oops A'); + + expect(clearYieldedValues()).toEqual(['A']); + + // B and C flush in a subsequent event. That way, the second error is not + // swallowed. + expect(() => flushWork(0)).toThrow('Oops C'); + expect(clearYieldedValues()).toEqual(['B', 'C']); + }); + + it('exposes the current priority level', () => { + yieldValue(getCurrentPriorityLevel()); + runWithPriority(ImmediatePriority, () => { + yieldValue(getCurrentPriorityLevel()); + runWithPriority(NormalPriority, () => { + yieldValue(getCurrentPriorityLevel()); + runWithPriority(InteractivePriority, () => { + yieldValue(getCurrentPriorityLevel()); + }); + }); + yieldValue(getCurrentPriorityLevel()); + }); + + expect(clearYieldedValues()).toEqual([ + NormalPriority, + ImmediatePriority, + NormalPriority, + InteractivePriority, + ImmediatePriority, + ]); }); }); diff --git a/packages/scheduler/src/__tests__/SchedulerDOM-test.js b/packages/scheduler/src/__tests__/SchedulerDOM-test.js index ae9c0826c46ee..b1253ffe9ddc3 100644 --- a/packages/scheduler/src/__tests__/SchedulerDOM-test.js +++ b/packages/scheduler/src/__tests__/SchedulerDOM-test.js @@ -97,11 +97,11 @@ describe('SchedulerDOM', () => { Scheduler = require('scheduler'); }); - describe('scheduleWork', () => { + describe('scheduleCallback', () => { it('calls the callback within the frame when not blocked', () => { - const {unstable_scheduleWork: scheduleWork} = Scheduler; + const {unstable_scheduleCallback: scheduleCallback} = Scheduler; const cb = jest.fn(); - scheduleWork(cb); + scheduleCallback(cb); advanceOneFrame({timeLeftInFrame: 15}); expect(cb).toHaveBeenCalledTimes(1); // should not have timed out and should include a timeRemaining method @@ -111,15 +111,15 @@ describe('SchedulerDOM', () => { describe('with multiple callbacks', () => { it('accepts multiple callbacks and calls within frame when not blocked', () => { - const {unstable_scheduleWork: scheduleWork} = Scheduler; + const {unstable_scheduleCallback: scheduleCallback} = Scheduler; const callbackLog = []; const callbackA = jest.fn(() => callbackLog.push('A')); const callbackB = jest.fn(() => callbackLog.push('B')); - scheduleWork(callbackA); + scheduleCallback(callbackA); // initially waits to call the callback expect(callbackLog).toEqual([]); // waits while second callback is passed - scheduleWork(callbackB); + scheduleCallback(callbackB); expect(callbackLog).toEqual([]); // after a delay, calls as many callbacks as it has time for advanceOneFrame({timeLeftInFrame: 15}); @@ -137,17 +137,17 @@ describe('SchedulerDOM', () => { }); it("accepts callbacks betweeen animationFrame and postMessage and doesn't stall", () => { - const {unstable_scheduleWork: scheduleWork} = Scheduler; + const {unstable_scheduleCallback: scheduleCallback} = Scheduler; const callbackLog = []; const callbackA = jest.fn(() => callbackLog.push('A')); const callbackB = jest.fn(() => callbackLog.push('B')); const callbackC = jest.fn(() => callbackLog.push('C')); - scheduleWork(callbackA); + scheduleCallback(callbackA); // initially waits to call the callback expect(callbackLog).toEqual([]); runRAFCallbacks(); // this should schedule work *after* the requestAnimationFrame but before the message handler - scheduleWork(callbackB); + scheduleCallback(callbackB); expect(callbackLog).toEqual([]); // now it should drain the message queue and do all scheduled work runPostMessageCallbacks({timeLeftInFrame: 15}); @@ -157,7 +157,7 @@ describe('SchedulerDOM', () => { advanceOneFrame({timeLeftInFrame: 15}); // see if more work can be done now. - scheduleWork(callbackC); + scheduleCallback(callbackC); expect(callbackLog).toEqual(['A', 'B']); advanceOneFrame({timeLeftInFrame: 15}); expect(callbackLog).toEqual(['A', 'B', 'C']); @@ -167,11 +167,11 @@ describe('SchedulerDOM', () => { 'schedules callbacks in correct order and' + 'keeps calling them if there is time', () => { - const {unstable_scheduleWork: scheduleWork} = Scheduler; + const {unstable_scheduleCallback: scheduleCallback} = Scheduler; const callbackLog = []; const callbackA = jest.fn(() => { callbackLog.push('A'); - scheduleWork(callbackC); + scheduleCallback(callbackC); }); const callbackB = jest.fn(() => { callbackLog.push('B'); @@ -180,11 +180,11 @@ describe('SchedulerDOM', () => { callbackLog.push('C'); }); - scheduleWork(callbackA); + scheduleCallback(callbackA); // initially waits to call the callback expect(callbackLog).toEqual([]); // continues waiting while B is scheduled - scheduleWork(callbackB); + scheduleCallback(callbackB); expect(callbackLog).toEqual([]); // after a delay, calls the scheduled callbacks, // and also calls new callbacks scheduled by current callbacks @@ -193,18 +193,18 @@ describe('SchedulerDOM', () => { }, ); - it('schedules callbacks in correct order when callbacks have many nested scheduleWork calls', () => { - const {unstable_scheduleWork: scheduleWork} = Scheduler; + it('schedules callbacks in correct order when callbacks have many nested scheduleCallback calls', () => { + const {unstable_scheduleCallback: scheduleCallback} = Scheduler; const callbackLog = []; const callbackA = jest.fn(() => { callbackLog.push('A'); - scheduleWork(callbackC); - scheduleWork(callbackD); + scheduleCallback(callbackC); + scheduleCallback(callbackD); }); const callbackB = jest.fn(() => { callbackLog.push('B'); - scheduleWork(callbackE); - scheduleWork(callbackF); + scheduleCallback(callbackE); + scheduleCallback(callbackF); }); const callbackC = jest.fn(() => { callbackLog.push('C'); @@ -219,8 +219,8 @@ describe('SchedulerDOM', () => { callbackLog.push('F'); }); - scheduleWork(callbackA); - scheduleWork(callbackB); + scheduleCallback(callbackA); + scheduleCallback(callbackB); // initially waits to call the callback expect(callbackLog).toEqual([]); // while flushing callbacks, calls as many as it has time for @@ -228,23 +228,23 @@ describe('SchedulerDOM', () => { expect(callbackLog).toEqual(['A', 'B', 'C', 'D', 'E', 'F']); }); - it('schedules callbacks in correct order when they use scheduleWork to schedule themselves', () => { - const {unstable_scheduleWork: scheduleWork} = Scheduler; + it('schedules callbacks in correct order when they use scheduleCallback to schedule themselves', () => { + const {unstable_scheduleCallback: scheduleCallback} = Scheduler; const callbackLog = []; let callbackAIterations = 0; const callbackA = jest.fn(() => { if (callbackAIterations < 1) { - scheduleWork(callbackA); + scheduleCallback(callbackA); } callbackLog.push('A' + callbackAIterations); callbackAIterations++; }); const callbackB = jest.fn(() => callbackLog.push('B')); - scheduleWork(callbackA); + scheduleCallback(callbackA); // initially waits to call the callback expect(callbackLog).toEqual([]); - scheduleWork(callbackB); + scheduleCallback(callbackB); expect(callbackLog).toEqual([]); // after a delay, calls the latest callback passed advanceOneFrame({timeLeftInFrame: 15}); @@ -260,7 +260,7 @@ 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_scheduleWork: scheduleWork} = Scheduler; + const {unstable_scheduleCallback: scheduleCallback} = Scheduler; startOfLatestFrame = 1000000000000; currentTime = startOfLatestFrame - 10; const callbackLog = []; @@ -269,9 +269,9 @@ describe('SchedulerDOM', () => { const callbackB = jest.fn(() => callbackLog.push('B')); const callbackC = jest.fn(() => callbackLog.push('C')); - scheduleWork(callbackA); // won't time out - scheduleWork(callbackB, {timeout: 100}); // times out later - scheduleWork(callbackC, {timeout: 2}); // will time out fast + scheduleCallback(callbackA); // won't time out + scheduleCallback(callbackB, {timeout: 100}); // times out later + scheduleCallback(callbackC, {timeout: 2}); // will time out fast // push time ahead a bit so that we have no idle time advanceOneFrame({timePastFrameDeadline: 16}); @@ -295,7 +295,7 @@ 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_scheduleWork: scheduleWork} = Scheduler; + const {unstable_scheduleCallback: scheduleCallback} = Scheduler; startOfLatestFrame = 1000000000000; currentTime = startOfLatestFrame - 10; const callbackLog = []; @@ -309,10 +309,10 @@ describe('SchedulerDOM', () => { const callbackC = jest.fn(() => callbackLog.push('C')); const callbackD = jest.fn(() => callbackLog.push('D')); - scheduleWork(callbackA, {timeout: 100}); // won't time out - scheduleWork(callbackB, {timeout: 100}); // times out later - scheduleWork(callbackC, {timeout: 2}); // will time out fast - scheduleWork(callbackD, {timeout: 200}); // won't time out + 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 advanceOneFrame({timeLeftInFrame: 15}); // runs rAF and postMessage callbacks @@ -341,16 +341,16 @@ describe('SchedulerDOM', () => { }); }); - describe('cancelScheduledWork', () => { + describe('cancelCallback', () => { it('cancels the scheduled callback', () => { const { - unstable_scheduleWork: scheduleWork, - unstable_cancelScheduledWork: cancelScheduledWork, + unstable_scheduleCallback: scheduleCallback, + unstable_cancelCallback: cancelCallback, } = Scheduler; const cb = jest.fn(); - const callbackId = scheduleWork(cb); + const callbackId = scheduleCallback(cb); expect(cb).toHaveBeenCalledTimes(0); - cancelScheduledWork(callbackId); + cancelCallback(callbackId); advanceOneFrame({timeLeftInFrame: 15}); expect(cb).toHaveBeenCalledTimes(0); }); @@ -358,19 +358,19 @@ describe('SchedulerDOM', () => { describe('with multiple callbacks', () => { it('when called more than once', () => { const { - unstable_scheduleWork: scheduleWork, - unstable_cancelScheduledWork: cancelScheduledWork, + unstable_scheduleCallback: scheduleCallback, + unstable_cancelCallback: cancelCallback, } = Scheduler; const callbackLog = []; const callbackA = jest.fn(() => callbackLog.push('A')); const callbackB = jest.fn(() => callbackLog.push('B')); const callbackC = jest.fn(() => callbackLog.push('C')); - scheduleWork(callbackA); - const callbackId = scheduleWork(callbackB); - scheduleWork(callbackC); - cancelScheduledWork(callbackId); - cancelScheduledWork(callbackId); - cancelScheduledWork(callbackId); + scheduleCallback(callbackA); + const callbackId = scheduleCallback(callbackB); + scheduleCallback(callbackC); + cancelCallback(callbackId); + cancelCallback(callbackId); + cancelCallback(callbackId); // Initially doesn't call anything expect(callbackLog).toEqual([]); advanceOneFrame({timeLeftInFrame: 15}); @@ -382,18 +382,18 @@ describe('SchedulerDOM', () => { it('when one callback cancels the next one', () => { const { - unstable_scheduleWork: scheduleWork, - unstable_cancelScheduledWork: cancelScheduledWork, + unstable_scheduleCallback: scheduleCallback, + unstable_cancelCallback: cancelCallback, } = Scheduler; const callbackLog = []; let callbackBId; const callbackA = jest.fn(() => { callbackLog.push('A'); - cancelScheduledWork(callbackBId); + cancelCallback(callbackBId); }); const callbackB = jest.fn(() => callbackLog.push('B')); - scheduleWork(callbackA); - callbackBId = scheduleWork(callbackB); + scheduleCallback(callbackA); + callbackBId = scheduleCallback(callbackB); // Initially doesn't call anything expect(callbackLog).toEqual([]); advanceOneFrame({timeLeftInFrame: 15}); @@ -421,7 +421,7 @@ describe('SchedulerDOM', () => { * */ it('still calls all callbacks within same frame', () => { - const {unstable_scheduleWork: scheduleWork} = Scheduler; + const {unstable_scheduleCallback: scheduleCallback} = Scheduler; const callbackLog = []; const callbackA = jest.fn(() => callbackLog.push('A')); const callbackB = jest.fn(() => { @@ -434,11 +434,11 @@ describe('SchedulerDOM', () => { throw new Error('D error'); }); const callbackE = jest.fn(() => callbackLog.push('E')); - scheduleWork(callbackA); - scheduleWork(callbackB); - scheduleWork(callbackC); - scheduleWork(callbackD); - scheduleWork(callbackE); + scheduleCallback(callbackA); + scheduleCallback(callbackB); + scheduleCallback(callbackC); + scheduleCallback(callbackD); + scheduleCallback(callbackE); // Initially doesn't call anything expect(callbackLog).toEqual([]); catchPostMessageErrors = true; @@ -467,7 +467,7 @@ describe('SchedulerDOM', () => { * */ it('and with some timed out callbacks, still calls all callbacks within same frame', () => { - const {unstable_scheduleWork: scheduleWork} = Scheduler; + const {unstable_scheduleCallback: scheduleCallback} = Scheduler; const callbackLog = []; const callbackA = jest.fn(() => { callbackLog.push('A'); @@ -480,11 +480,11 @@ describe('SchedulerDOM', () => { throw new Error('D error'); }); const callbackE = jest.fn(() => callbackLog.push('E')); - scheduleWork(callbackA); - scheduleWork(callbackB); - scheduleWork(callbackC, {timeout: 2}); // times out fast - scheduleWork(callbackD, {timeout: 2}); // times out fast - scheduleWork(callbackE, {timeout: 2}); // times out fast + 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 // Initially doesn't call anything expect(callbackLog).toEqual([]); catchPostMessageErrors = true; @@ -513,7 +513,7 @@ describe('SchedulerDOM', () => { * */ it('still calls all callbacks within same frame', () => { - const {unstable_scheduleWork: scheduleWork} = Scheduler; + const {unstable_scheduleCallback: scheduleCallback} = Scheduler; const callbackLog = []; const callbackA = jest.fn(() => { callbackLog.push('A'); @@ -535,11 +535,11 @@ describe('SchedulerDOM', () => { callbackLog.push('E'); throw new Error('E error'); }); - scheduleWork(callbackA); - scheduleWork(callbackB); - scheduleWork(callbackC); - scheduleWork(callbackD); - scheduleWork(callbackE); + scheduleCallback(callbackA); + scheduleCallback(callbackB); + scheduleCallback(callbackC); + scheduleCallback(callbackD); + scheduleCallback(callbackE); // Initially doesn't call anything expect(callbackLog).toEqual([]); catchPostMessageErrors = true; @@ -574,7 +574,7 @@ describe('SchedulerDOM', () => { * */ it('and with all timed out callbacks, still calls all callbacks within same frame', () => { - const {unstable_scheduleWork: scheduleWork} = Scheduler; + const {unstable_scheduleCallback: scheduleCallback} = Scheduler; const callbackLog = []; const callbackA = jest.fn(() => { callbackLog.push('A'); @@ -596,11 +596,11 @@ describe('SchedulerDOM', () => { callbackLog.push('E'); throw new Error('E error'); }); - scheduleWork(callbackA, {timeout: 2}); // times out fast - scheduleWork(callbackB, {timeout: 2}); // times out fast - scheduleWork(callbackC, {timeout: 2}); // times out fast - scheduleWork(callbackD, {timeout: 2}); // times out fast - scheduleWork(callbackE, {timeout: 2}); // times out fast + 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 // Initially doesn't call anything expect(callbackLog).toEqual([]); catchPostMessageErrors = true; @@ -655,7 +655,7 @@ describe('SchedulerDOM', () => { * */ it('still calls all callbacks within same frame', () => { - const {unstable_scheduleWork: scheduleWork} = Scheduler; + const {unstable_scheduleCallback: scheduleCallback} = Scheduler; startOfLatestFrame = 1000000000000; currentTime = startOfLatestFrame - 10; catchPostMessageErrors = true; @@ -689,13 +689,13 @@ describe('SchedulerDOM', () => { }); const callbackG = jest.fn(() => callbackLog.push('G')); - scheduleWork(callbackA); - scheduleWork(callbackB); - scheduleWork(callbackC); - scheduleWork(callbackD); - scheduleWork(callbackE); - scheduleWork(callbackF); - scheduleWork(callbackG); + scheduleCallback(callbackA); + scheduleCallback(callbackB); + scheduleCallback(callbackC); + scheduleCallback(callbackD); + scheduleCallback(callbackE); + scheduleCallback(callbackF); + scheduleCallback(callbackG); // does nothing initially expect(callbackLog).toEqual([]); diff --git a/packages/scheduler/src/__tests__/SchedulerUMDBundle-test.internal.js b/packages/scheduler/src/__tests__/SchedulerUMDBundle-test.internal.js index 5b6f04a2a1cc5..8fd78522480f1 100644 --- a/packages/scheduler/src/__tests__/SchedulerUMDBundle-test.internal.js +++ b/packages/scheduler/src/__tests__/SchedulerUMDBundle-test.internal.js @@ -17,7 +17,8 @@ describe('Scheduling UMD bundle', () => { }); function filterPrivateKeys(name) { - return !name.startsWith('_'); + // TODO: Figure out how to forward priority levels. + return !name.startsWith('_') && !name.endsWith('Priority'); } function validateForwardedAPIs(api, forwardedAPIs) { diff --git a/packages/shared/forks/Scheduler.umd.js b/packages/shared/forks/Scheduler.umd.js index 86ad51ac79735..66d3125c37106 100644 --- a/packages/shared/forks/Scheduler.umd.js +++ b/packages/shared/forks/Scheduler.umd.js @@ -12,9 +12,9 @@ import React from 'react'; const ReactInternals = React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED; const { - unstable_cancelScheduledWork, + unstable_cancelCallback, unstable_now, - unstable_scheduleWork, + unstable_scheduleCallback, } = ReactInternals.Scheduler; -export {unstable_cancelScheduledWork, unstable_now, unstable_scheduleWork}; +export {unstable_cancelCallback, unstable_now, unstable_scheduleCallback};