-
- 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:
-
-
-
-
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:
+
+
+
+
Calls the callback within the frame when not blocked:
+
Expected:
+
+
+
-------------------------------------------------
+
If you see the same above and below it's correct.
-------------------------------------------------
Actual:
-
-
-
Accepts multiple callbacks and calls within frame when not blocked
-
-
Expected:
-
-
-
-------------------------------------------------
-
If you see the same above and below it's correct.
+
+
+
Accepts multiple callbacks and calls within frame when not blocked
+
+
Expected:
+
+
+
-------------------------------------------------
+
If you see the same above and below it's correct.
-------------------------------------------------
Actual:
-
-
-
Schedules callbacks in correct order when they use scheduleWork to schedule themselves
-
-
Expected:
-
-
-
-------------------------------------------------
-
If you see the same above and below it's correct.
+
+
+
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:
-
-
-
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.
+
+
+
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:
-
-
-
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!
-
-
-
-
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!
-
-
-
-
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.
-
-
-
-
-
-
+
+
+
-
+
\ 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};