Add yieldy, chained async task support to InteractionManager
Summary: Default behavior should be unchanged. If we queue up a bunch of expensive tasks during an interaction, the default `InteractionManager` behavior would execute them all in one synchronous loop at the end the JS event loop via one `setImmediate` call, blocking the JS thread the entire time. The `setDeadline` addition in this diff enables an option to only execute tasks until the `eventLoopRunningTime` is hit (added to MessageQueue/BatchedBridge), allowing the queue execution to be paused if an interaction starts in between tasks, making the app more responsive. Additionally, if a task ends up generating a bunch of additional tasks asynchronously, the previous implementation would execute these new tasks after already scheduled tasks. This is often fine, but I want it to fully resolve async tasks and all their dependencies before making progress in the rest of the queue, so I added support for `type PromiseTask = {gen: () => Promise}` to do just this. It works by building a stack of queues each time a `PromiseTask` is started, and pops them off the stack once they are resolved and the queues are processed. I also pulled all of the actual queue logic out of `InteractionManager` and into a new `TaskQueue` class to isolate concerns a bit. public Reviewed By: josephsavona Differential Revision: D2754311 fb-gh-sync-id: bfd6d0c54e6410cb261aa1d2c5024dd91a3959e6
This commit is contained in:
parent
f7de9678d4
commit
893a54d0cd
|
@ -11,23 +11,19 @@
|
|||
*/
|
||||
'use strict';
|
||||
|
||||
var ErrorUtils = require('ErrorUtils');
|
||||
var EventEmitter = require('EventEmitter');
|
||||
var Set = require('Set');
|
||||
const BatchedBridge = require('BatchedBridge');
|
||||
const EventEmitter = require('EventEmitter');
|
||||
const Set = require('Set');
|
||||
const TaskQueue = require('TaskQueue');
|
||||
|
||||
var invariant = require('invariant');
|
||||
var keyMirror = require('keyMirror');
|
||||
var setImmediate = require('setImmediate');
|
||||
const invariant = require('invariant');
|
||||
const keyMirror = require('keyMirror');
|
||||
const setImmediate = require('setImmediate');
|
||||
|
||||
type Handle = number;
|
||||
import type {Task} from 'TaskQueue';
|
||||
|
||||
var _emitter = new EventEmitter();
|
||||
var _interactionSet = new Set();
|
||||
var _addInteractionSet = new Set();
|
||||
var _deleteInteractionSet = new Set();
|
||||
var _nextUpdateHandle = null;
|
||||
var _queue = [];
|
||||
var _inc = 0;
|
||||
const _emitter = new EventEmitter();
|
||||
|
||||
/**
|
||||
* InteractionManager allows long-running work to be scheduled after any
|
||||
|
@ -63,6 +59,20 @@ var _inc = 0;
|
|||
* InteractionManager.clearInteractionHandle(handle);
|
||||
* // queued tasks run if all handles were cleared
|
||||
* ```
|
||||
*
|
||||
* `runAfterInteractions` takes either a plain callback function, or a
|
||||
* `PromiseTask` object with a `gen` method that returns a `Promise`. If a
|
||||
* `PromiseTask` is supplied, then it is fully resolved (including asynchronous
|
||||
* dependencies that also schedule more tasks via `runAfterInteractions`) before
|
||||
* starting on the next task that might have been queued up synchronously
|
||||
* earlier.
|
||||
*
|
||||
* By default, queued tasks are executed together in a loop in one
|
||||
* `setImmediate` batch. If `setDeadline` is called with a positive number, then
|
||||
* tasks will only be executed until the deadline (in terms of js event loop run
|
||||
* time) approaches, at which point execution will yield via setTimeout,
|
||||
* allowing events such as touches to start interactions and block queued tasks
|
||||
* from executing, making apps more responsive.
|
||||
*/
|
||||
var InteractionManager = {
|
||||
Events: keyMirror({
|
||||
|
@ -73,13 +83,14 @@ var InteractionManager = {
|
|||
/**
|
||||
* Schedule a function to run after all interactions have completed.
|
||||
*/
|
||||
runAfterInteractions(callback: ?Function): Promise {
|
||||
runAfterInteractions(task: ?Task): Promise {
|
||||
return new Promise(resolve => {
|
||||
scheduleUpdate();
|
||||
if (callback) {
|
||||
_queue.push(callback);
|
||||
_scheduleUpdate();
|
||||
if (task) {
|
||||
_taskQueue.enqueue(task);
|
||||
}
|
||||
_queue.push(resolve);
|
||||
const name = task && task.name || '?';
|
||||
_taskQueue.enqueue({run: resolve, name: 'resolve ' + name});
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -87,7 +98,7 @@ var InteractionManager = {
|
|||
* Notify manager that an interaction has started.
|
||||
*/
|
||||
createInteractionHandle(): Handle {
|
||||
scheduleUpdate();
|
||||
_scheduleUpdate();
|
||||
var handle = ++_inc;
|
||||
_addInteractionSet.add(handle);
|
||||
return handle;
|
||||
|
@ -101,28 +112,49 @@ var InteractionManager = {
|
|||
!!handle,
|
||||
'Must provide a handle to clear.'
|
||||
);
|
||||
scheduleUpdate();
|
||||
_scheduleUpdate();
|
||||
_addInteractionSet.delete(handle);
|
||||
_deleteInteractionSet.add(handle);
|
||||
},
|
||||
|
||||
addListener: _emitter.addListener.bind(_emitter),
|
||||
|
||||
/**
|
||||
* A positive number will use setTimeout to schedule any tasks after the
|
||||
* eventLoopRunningTime hits the deadline value, otherwise all tasks will be
|
||||
* executed in one setImmediate batch (default).
|
||||
*/
|
||||
setDeadline(deadline: number) {
|
||||
_deadline = deadline;
|
||||
},
|
||||
};
|
||||
|
||||
const _interactionSet = new Set();
|
||||
const _addInteractionSet = new Set();
|
||||
const _deleteInteractionSet = new Set();
|
||||
const _taskQueue = new TaskQueue({onMoreTasks: _scheduleUpdate});
|
||||
let _nextUpdateHandle = 0;
|
||||
let _inc = 0;
|
||||
let _deadline = -1;
|
||||
|
||||
/**
|
||||
* Schedule an asynchronous update to the interaction state.
|
||||
*/
|
||||
function scheduleUpdate() {
|
||||
function _scheduleUpdate() {
|
||||
if (!_nextUpdateHandle) {
|
||||
_nextUpdateHandle = setImmediate(processUpdate);
|
||||
if (_deadline > 0) {
|
||||
_nextUpdateHandle = setTimeout(_processUpdate, 0);
|
||||
} else {
|
||||
_nextUpdateHandle = setImmediate(_processUpdate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify listeners, process queue, etc
|
||||
*/
|
||||
function processUpdate() {
|
||||
_nextUpdateHandle = null;
|
||||
function _processUpdate() {
|
||||
_nextUpdateHandle = 0;
|
||||
|
||||
var interactionCount = _interactionSet.size;
|
||||
_addInteractionSet.forEach(handle =>
|
||||
|
@ -143,13 +175,16 @@ function processUpdate() {
|
|||
|
||||
// process the queue regardless of a transition
|
||||
if (nextInteractionCount === 0) {
|
||||
var queue = _queue;
|
||||
_queue = [];
|
||||
queue.forEach(callback => {
|
||||
ErrorUtils.applyWithGuard(callback);
|
||||
});
|
||||
while (_taskQueue.hasTasksToProcess()) {
|
||||
_taskQueue.processNext();
|
||||
if (_deadline > 0 &&
|
||||
BatchedBridge.getEventLoopRunningTime() >= _deadline) {
|
||||
// Hit deadline before processing all tasks, so process more later.
|
||||
_scheduleUpdate();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_addInteractionSet.clear();
|
||||
_deleteInteractionSet.clear();
|
||||
}
|
||||
|
|
|
@ -0,0 +1,148 @@
|
|||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*
|
||||
* @providesModule TaskQueue
|
||||
* @flow
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const ErrorUtils = require('ErrorUtils');
|
||||
|
||||
const invariant = require('invariant');
|
||||
|
||||
type SimpleTask = {
|
||||
name: string;
|
||||
run: () => void;
|
||||
};
|
||||
type PromiseTask = {
|
||||
name: string;
|
||||
gen: () => Promise;
|
||||
};
|
||||
export type Task = Function | SimpleTask | PromiseTask;
|
||||
|
||||
/**
|
||||
* TaskQueue - A system for queueing and executing a mix of simple callbacks and
|
||||
* trees of dependent tasks based on Promises. No tasks are executed unless
|
||||
* `processNext` is called.
|
||||
*
|
||||
* `enqueue` takes a Task object with either a simple `run` callback, or a
|
||||
* `gen` function that returns a `Promise` and puts it in the queue. If a gen
|
||||
* function is supplied, then the promise it returns will block execution of
|
||||
* tasks already in the queue until it resolves. This can be used to make sure
|
||||
* the first task is fully resolved (including asynchronous dependencies that
|
||||
* also schedule more tasks via `enqueue`) before starting on the next task that
|
||||
* might have been queued up earlier. The `onMoreTasks` constructor argument is
|
||||
* used to inform the owner that an async task has resolved and that the queue
|
||||
* should be processed again.
|
||||
*
|
||||
* Note: Tasks are only actually executed with explicit calls to `processNext`.
|
||||
*/
|
||||
class TaskQueue {
|
||||
/**
|
||||
* TaskQueue instances are self contained and independent, so multiple tasks
|
||||
* of varying semantics and priority can operate together.
|
||||
*
|
||||
* `onMoreTasks` is invoked when `PromiseTask`s resolve if there are more
|
||||
* tasks to process.
|
||||
*/
|
||||
constructor({onMoreTasks}: {onMoreTasks: () => void}) {
|
||||
this._onMoreTasks = onMoreTasks;
|
||||
this._queueStack = [{tasks: [], popable: false}];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a task to the queue. It is recommended to name your tasks for easier
|
||||
* async debugging. Tasks will not be executed until `processNext` is called
|
||||
* explicitly.
|
||||
*/
|
||||
enqueue(task: Task): void {
|
||||
this._getCurrentQueue().push(task);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check to see if `processNext` should be called.
|
||||
*
|
||||
* @returns {boolean} Returns true if there are tasks that are ready to be
|
||||
* processed with `processNext`, or returns false if there are no more tasks
|
||||
* to be processed right now, although there may be tasks in the queue that
|
||||
* are blocked by earlier `PromiseTask`s that haven't resolved yet.
|
||||
* `onMoreTasks` will be called after each `PromiseTask` resolves if there are
|
||||
* tasks ready to run at that point.
|
||||
*/
|
||||
hasTasksToProcess(): bool {
|
||||
return this._getCurrentQueue().length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the next task in the queue.
|
||||
*/
|
||||
processNext(): void {
|
||||
let queue = this._getCurrentQueue();
|
||||
if (queue.length) {
|
||||
const task = queue.shift();
|
||||
try {
|
||||
if (task.gen) {
|
||||
this._genPromise((task: any)); // Rather than annoying tagged union
|
||||
} else if (task.run) {
|
||||
task.run();
|
||||
} else {
|
||||
invariant(
|
||||
typeof task === 'function',
|
||||
'Expected Function, SimpleTask, or PromiseTask, but got: ' +
|
||||
JSON.stringify(task)
|
||||
);
|
||||
task();
|
||||
}
|
||||
} catch (e) {
|
||||
e.message = 'TaskQueue: Error with task' + (task.name || ' ') + ': ' +
|
||||
e.message;
|
||||
ErrorUtils.reportError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_queueStack: Array<{tasks: Array<Task>, popable: bool}>;
|
||||
_onMoreTasks: () => void;
|
||||
|
||||
_getCurrentQueue(): Array<Task> {
|
||||
const stackIdx = this._queueStack.length - 1;
|
||||
const queue = this._queueStack[stackIdx];
|
||||
if (queue.popable &&
|
||||
queue.tasks.length === 0 &&
|
||||
this._queueStack.length > 1) {
|
||||
this._queueStack.pop();
|
||||
return this._getCurrentQueue();
|
||||
} else {
|
||||
return queue.tasks;
|
||||
}
|
||||
}
|
||||
|
||||
_genPromise(task: PromiseTask) {
|
||||
// Each async task pushes it's own queue onto the queue stack. This
|
||||
// effectively defers execution of previously queued tasks until the promise
|
||||
// resolves, at which point we allow the new queue to be popped, which
|
||||
// happens once it is fully processed.
|
||||
this._queueStack.push({tasks: [], popable: false});
|
||||
const stackIdx = this._queueStack.length - 1;
|
||||
ErrorUtils.applyWithGuard(task.gen)
|
||||
.then(() => {
|
||||
this._queueStack[stackIdx].popable = true;
|
||||
this.hasTasksToProcess() && this._onMoreTasks();
|
||||
})
|
||||
.catch((ex) => {
|
||||
console.warn(
|
||||
'TaskQueue: Error resolving Promise in task ' + task.name,
|
||||
ex
|
||||
);
|
||||
throw ex;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
module.exports = TaskQueue;
|
|
@ -0,0 +1,288 @@
|
|||
/**
|
||||
* Copyright 2004-present Facebook. All Rights Reserved.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
jest
|
||||
.dontMock('InteractionManager')
|
||||
.dontMock('TaskQueue')
|
||||
.dontMock('invariant');
|
||||
|
||||
function expectToBeCalledOnce(fn) {
|
||||
expect(fn.mock.calls.length).toBe(1);
|
||||
}
|
||||
|
||||
describe('InteractionManager', () => {
|
||||
let InteractionManager;
|
||||
let interactionStart;
|
||||
let interactionComplete;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetModuleRegistry();
|
||||
InteractionManager = require('InteractionManager');
|
||||
|
||||
interactionStart = jest.genMockFunction();
|
||||
interactionComplete = jest.genMockFunction();
|
||||
|
||||
InteractionManager.addListener(
|
||||
InteractionManager.Events.interactionStart,
|
||||
interactionStart
|
||||
);
|
||||
InteractionManager.addListener(
|
||||
InteractionManager.Events.interactionComplete,
|
||||
interactionComplete
|
||||
);
|
||||
});
|
||||
|
||||
it('throws when clearing an undefined handle', () => {
|
||||
expect(() => InteractionManager.clearInteractionHandle()).toThrow();
|
||||
});
|
||||
|
||||
it('notifies asynchronously when interaction starts', () => {
|
||||
InteractionManager.createInteractionHandle();
|
||||
expect(interactionStart).not.toBeCalled();
|
||||
|
||||
jest.runAllTimers();
|
||||
expect(interactionStart).toBeCalled();
|
||||
expect(interactionComplete).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('notifies asynchronously when interaction stops', () => {
|
||||
var handle = InteractionManager.createInteractionHandle();
|
||||
jest.runAllTimers();
|
||||
interactionStart.mockClear();
|
||||
InteractionManager.clearInteractionHandle(handle);
|
||||
expect(interactionComplete).not.toBeCalled();
|
||||
|
||||
jest.runAllTimers();
|
||||
expect(interactionStart).not.toBeCalled();
|
||||
expect(interactionComplete).toBeCalled();
|
||||
});
|
||||
|
||||
it('does not notify when started & stopped in same event loop', () => {
|
||||
var handle = InteractionManager.createInteractionHandle();
|
||||
InteractionManager.clearInteractionHandle(handle);
|
||||
|
||||
jest.runAllTimers();
|
||||
expect(interactionStart).not.toBeCalled();
|
||||
expect(interactionComplete).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('does not notify when going from two -> one active interactions', () => {
|
||||
InteractionManager.createInteractionHandle();
|
||||
var handle = InteractionManager.createInteractionHandle();
|
||||
jest.runAllTimers();
|
||||
|
||||
interactionStart.mockClear();
|
||||
interactionComplete.mockClear();
|
||||
|
||||
InteractionManager.clearInteractionHandle(handle);
|
||||
jest.runAllTimers();
|
||||
expect(interactionStart).not.toBeCalled();
|
||||
expect(interactionComplete).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('runs tasks asynchronously when there are interactions', () => {
|
||||
var task = jest.genMockFunction();
|
||||
InteractionManager.runAfterInteractions(task);
|
||||
expect(task).not.toBeCalled();
|
||||
|
||||
jest.runAllTimers();
|
||||
expect(task).toBeCalled();
|
||||
});
|
||||
|
||||
it('runs tasks when interactions complete', () => {
|
||||
var task = jest.genMockFunction();
|
||||
var handle = InteractionManager.createInteractionHandle();
|
||||
InteractionManager.runAfterInteractions(task);
|
||||
|
||||
jest.runAllTimers();
|
||||
InteractionManager.clearInteractionHandle(handle);
|
||||
expect(task).not.toBeCalled();
|
||||
|
||||
jest.runAllTimers();
|
||||
expect(task).toBeCalled();
|
||||
});
|
||||
|
||||
it('does not run tasks twice', () => {
|
||||
var task1 = jest.genMockFunction();
|
||||
var task2 = jest.genMockFunction();
|
||||
InteractionManager.runAfterInteractions(task1);
|
||||
jest.runAllTimers();
|
||||
|
||||
InteractionManager.runAfterInteractions(task2);
|
||||
jest.runAllTimers();
|
||||
|
||||
expectToBeCalledOnce(task1);
|
||||
});
|
||||
|
||||
it('runs tasks added while processing previous tasks', () => {
|
||||
var task1 = jest.genMockFunction().mockImplementation(() => {
|
||||
InteractionManager.runAfterInteractions(task2);
|
||||
});
|
||||
var task2 = jest.genMockFunction();
|
||||
|
||||
InteractionManager.runAfterInteractions(task1);
|
||||
expect(task2).not.toBeCalled();
|
||||
|
||||
jest.runAllTimers();
|
||||
|
||||
expect(task1).toBeCalled();
|
||||
expect(task2).toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('promise tasks', () => {
|
||||
let InteractionManager;
|
||||
let BatchedBridge;
|
||||
let sequenceId;
|
||||
function createSequenceTask(expectedSequenceId) {
|
||||
return jest.genMockFunction().mockImplementation(() => {
|
||||
expect(++sequenceId).toBe(expectedSequenceId);
|
||||
});
|
||||
}
|
||||
beforeEach(() => {
|
||||
jest.resetModuleRegistry();
|
||||
InteractionManager = require('InteractionManager');
|
||||
BatchedBridge = require('BatchedBridge');
|
||||
sequenceId = 0;
|
||||
});
|
||||
|
||||
|
||||
it('should run a basic promise task', () => {
|
||||
const task1 = jest.genMockFunction().mockImplementation(() => {
|
||||
expect(++sequenceId).toBe(1);
|
||||
return new Promise(resolve => resolve());
|
||||
});
|
||||
InteractionManager.runAfterInteractions({gen: task1, name: 'gen1'});
|
||||
jest.runAllTimers();
|
||||
expectToBeCalledOnce(task1);
|
||||
});
|
||||
|
||||
it('should handle nested promises', () => {
|
||||
const task1 = jest.genMockFunction().mockImplementation(() => {
|
||||
expect(++sequenceId).toBe(1);
|
||||
return new Promise(resolve => {
|
||||
InteractionManager.runAfterInteractions({gen: task2, name: 'gen2'})
|
||||
.then(resolve);
|
||||
});
|
||||
});
|
||||
const task2 = jest.genMockFunction().mockImplementation(() => {
|
||||
expect(++sequenceId).toBe(2);
|
||||
return new Promise(resolve => resolve());
|
||||
});
|
||||
InteractionManager.runAfterInteractions({gen: task1, name: 'gen1'});
|
||||
jest.runAllTimers();
|
||||
expectToBeCalledOnce(task1);
|
||||
expectToBeCalledOnce(task2);
|
||||
});
|
||||
|
||||
it('should pause promise tasks during interactions then resume', () => {
|
||||
const task1 = createSequenceTask(1);
|
||||
const task2 = jest.genMockFunction().mockImplementation(() => {
|
||||
expect(++sequenceId).toBe(2);
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
InteractionManager.runAfterInteractions(task3).then(resolve);
|
||||
}, 1);
|
||||
});
|
||||
});
|
||||
const task3 = createSequenceTask(3);
|
||||
InteractionManager.runAfterInteractions(task1);
|
||||
InteractionManager.runAfterInteractions({gen: task2, name: 'gen2'});
|
||||
jest.runOnlyPendingTimers();
|
||||
expectToBeCalledOnce(task1);
|
||||
expectToBeCalledOnce(task2);
|
||||
const handle = InteractionManager.createInteractionHandle();
|
||||
jest.runAllTimers();
|
||||
jest.runAllTimers(); // Just to be sure...
|
||||
expect(task3).not.toBeCalled();
|
||||
InteractionManager.clearInteractionHandle(handle);
|
||||
jest.runAllTimers();
|
||||
expectToBeCalledOnce(task3);
|
||||
});
|
||||
|
||||
it('should execute tasks in loop within deadline', () => {
|
||||
InteractionManager.setDeadline(100);
|
||||
BatchedBridge.getEventLoopRunningTime.mockReturnValue(10);
|
||||
const task1 = createSequenceTask(1);
|
||||
const task2 = createSequenceTask(2);
|
||||
InteractionManager.runAfterInteractions(task1);
|
||||
InteractionManager.runAfterInteractions(task2);
|
||||
|
||||
jest.runOnlyPendingTimers();
|
||||
|
||||
expectToBeCalledOnce(task1);
|
||||
expectToBeCalledOnce(task2);
|
||||
});
|
||||
|
||||
it('should execute tasks one at a time if deadline exceeded', () => {
|
||||
InteractionManager.setDeadline(100);
|
||||
BatchedBridge.getEventLoopRunningTime.mockReturnValue(200);
|
||||
const task1 = createSequenceTask(1);
|
||||
const task2 = createSequenceTask(2);
|
||||
InteractionManager.runAfterInteractions(task1);
|
||||
InteractionManager.runAfterInteractions(task2);
|
||||
|
||||
jest.runOnlyPendingTimers();
|
||||
|
||||
expectToBeCalledOnce(task1);
|
||||
expect(task2).not.toBeCalled();
|
||||
|
||||
jest.runOnlyPendingTimers(); // resolve1
|
||||
jest.runOnlyPendingTimers(); // task2
|
||||
|
||||
expectToBeCalledOnce(task2);
|
||||
});
|
||||
|
||||
const bigAsyncTest = () => {
|
||||
const task1 = createSequenceTask(1);
|
||||
const task2 = jest.genMockFunction().mockImplementation(() => {
|
||||
expect(++sequenceId).toBe(2);
|
||||
return new Promise(resolve => {
|
||||
InteractionManager.runAfterInteractions(task3);
|
||||
setTimeout(() => {
|
||||
InteractionManager.runAfterInteractions({gen: task4, name: 'gen4'})
|
||||
.then(resolve);
|
||||
}, 1);
|
||||
});
|
||||
});
|
||||
const task3 = createSequenceTask(3);
|
||||
const task4 = jest.genMockFunction().mockImplementation(() => {
|
||||
expect(++sequenceId).toBe(4);
|
||||
return new Promise(resolve => {
|
||||
InteractionManager.runAfterInteractions(task5).then(resolve);
|
||||
});
|
||||
});
|
||||
const task5 = createSequenceTask(5);
|
||||
const task6 = createSequenceTask(6);
|
||||
|
||||
InteractionManager.runAfterInteractions(task1);
|
||||
InteractionManager.runAfterInteractions({gen: task2, name: 'gen2'});
|
||||
InteractionManager.runAfterInteractions(task6);
|
||||
|
||||
jest.runAllTimers();
|
||||
// runAllTimers doesn't actually run all timers with nested timer functions
|
||||
// inside Promises, so we have to call it extra times.
|
||||
jest.runAllTimers();
|
||||
jest.runAllTimers();
|
||||
|
||||
expectToBeCalledOnce(task1);
|
||||
expectToBeCalledOnce(task2);
|
||||
expectToBeCalledOnce(task3);
|
||||
expectToBeCalledOnce(task4);
|
||||
expectToBeCalledOnce(task5);
|
||||
expectToBeCalledOnce(task6);
|
||||
};
|
||||
|
||||
it('resolves async tasks recusively before other queued tasks', () => {
|
||||
bigAsyncTest();
|
||||
});
|
||||
|
||||
it('should also work with a deadline', () => {
|
||||
InteractionManager.setDeadline(100);
|
||||
BatchedBridge.getEventLoopRunningTime.mockReturnValue(200);
|
||||
bigAsyncTest();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,44 @@
|
|||
/**
|
||||
* Copyright 2004-present Facebook. All Rights Reserved.
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
jest.dontMock('InteractionMixin');
|
||||
|
||||
describe('InteractionMixin', () => {
|
||||
var InteractionManager;
|
||||
var InteractionMixin;
|
||||
var component;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetModuleRegistry();
|
||||
InteractionManager = require('InteractionManager');
|
||||
InteractionMixin = require('InteractionMixin');
|
||||
|
||||
component = Object.create(InteractionMixin);
|
||||
});
|
||||
|
||||
it('should start interactions', () => {
|
||||
var timeout = 123;
|
||||
component.createInteractionHandle(timeout);
|
||||
expect(InteractionManager.createInteractionHandle).toBeCalled(timeout);
|
||||
});
|
||||
|
||||
it('should end interactions', () => {
|
||||
var handle = {};
|
||||
component.clearInteractionHandle(handle);
|
||||
expect(InteractionManager.clearInteractionHandle).toBeCalledWith(handle);
|
||||
});
|
||||
|
||||
it('should schedule tasks', () => {
|
||||
var task = jest.genMockFunction();
|
||||
component.runAfterInteractions(task);
|
||||
expect(InteractionManager.runAfterInteractions).toBeCalledWith(task);
|
||||
});
|
||||
|
||||
it('should end unfinished interactions in componentWillUnmount', () => {
|
||||
var handle = component.createInteractionHandle();
|
||||
component.componentWillUnmount();
|
||||
expect(InteractionManager.clearInteractionHandle).toBeCalledWith(handle);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,121 @@
|
|||
/**
|
||||
* Copyright 2004-present Facebook. All Rights Reserved.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
jest.dontMock('TaskQueue');
|
||||
|
||||
function expectToBeCalledOnce(fn) {
|
||||
expect(fn.mock.calls.length).toBe(1);
|
||||
}
|
||||
|
||||
function clearTaskQueue(taskQueue) {
|
||||
do {
|
||||
jest.runAllTimers();
|
||||
taskQueue.processNext();
|
||||
jest.runAllTimers();
|
||||
} while (taskQueue.hasTasksToProcess())
|
||||
}
|
||||
|
||||
describe('TaskQueue', () => {
|
||||
let taskQueue;
|
||||
let onMoreTasks;
|
||||
let sequenceId;
|
||||
function createSequenceTask(expectedSequenceId) {
|
||||
return jest.genMockFunction().mockImplementation(() => {
|
||||
expect(++sequenceId).toBe(expectedSequenceId);
|
||||
});
|
||||
}
|
||||
beforeEach(() => {
|
||||
jest.resetModuleRegistry();
|
||||
onMoreTasks = jest.genMockFunction();
|
||||
const TaskQueue = require('TaskQueue');
|
||||
taskQueue = new TaskQueue({onMoreTasks});
|
||||
sequenceId = 0;
|
||||
});
|
||||
|
||||
|
||||
it('should run a basic task', () => {
|
||||
const task1 = createSequenceTask(1);
|
||||
taskQueue.enqueue({run: task1, name: 'run1'});
|
||||
expect(taskQueue.hasTasksToProcess()).toBe(true);
|
||||
taskQueue.processNext();
|
||||
expectToBeCalledOnce(task1);
|
||||
});
|
||||
|
||||
it('should handle blocking promise task', () => {
|
||||
const task1 = jest.genMockFunction().mockImplementation(() => {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
expect(++sequenceId).toBe(1);
|
||||
resolve();
|
||||
}, 1);
|
||||
});
|
||||
});
|
||||
const task2 = createSequenceTask(2);
|
||||
taskQueue.enqueue({gen: task1, name: 'gen1'});
|
||||
taskQueue.enqueue({run: task2, name: 'run2'});
|
||||
|
||||
taskQueue.processNext();
|
||||
|
||||
expectToBeCalledOnce(task1);
|
||||
expect(task2).not.toBeCalled();
|
||||
expect(onMoreTasks).not.toBeCalled();
|
||||
expect(taskQueue.hasTasksToProcess()).toBe(false);
|
||||
|
||||
clearTaskQueue(taskQueue);
|
||||
|
||||
expectToBeCalledOnce(onMoreTasks);
|
||||
expectToBeCalledOnce(task2);
|
||||
});
|
||||
|
||||
it('should handle nested simple tasks', () => {
|
||||
const task1 = jest.genMockFunction().mockImplementation(() => {
|
||||
expect(++sequenceId).toBe(1);
|
||||
taskQueue.enqueue({run: task3, name: 'run3'});
|
||||
});
|
||||
const task2 = createSequenceTask(2);
|
||||
const task3 = createSequenceTask(3);
|
||||
taskQueue.enqueue({run: task1, name: 'run1'});
|
||||
taskQueue.enqueue({run: task2, name: 'run2'}); // not blocked by task 1
|
||||
|
||||
clearTaskQueue(taskQueue);
|
||||
|
||||
expectToBeCalledOnce(task1);
|
||||
expectToBeCalledOnce(task2);
|
||||
expectToBeCalledOnce(task3);
|
||||
});
|
||||
|
||||
it('should handle nested promises', () => {
|
||||
const task1 = jest.genMockFunction().mockImplementation(() => {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
expect(++sequenceId).toBe(1);
|
||||
taskQueue.enqueue({gen: task2, name: 'gen2'});
|
||||
taskQueue.enqueue({run: resolve, name: 'resolve1'});
|
||||
}, 1);
|
||||
});
|
||||
});
|
||||
const task2 = jest.genMockFunction().mockImplementation(() => {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
expect(++sequenceId).toBe(2);
|
||||
taskQueue.enqueue({run: task3, name: 'run3'});
|
||||
taskQueue.enqueue({run: resolve, name: 'resolve2'});
|
||||
}, 1);
|
||||
});
|
||||
});
|
||||
const task3 = createSequenceTask(3);
|
||||
const task4 = createSequenceTask(4);
|
||||
taskQueue.enqueue({gen: task1, name: 'gen1'});
|
||||
taskQueue.enqueue({run: task4, name: 'run4'}); // blocked by task 1 promise
|
||||
|
||||
clearTaskQueue(taskQueue);
|
||||
|
||||
expectToBeCalledOnce(task1);
|
||||
expectToBeCalledOnce(task2);
|
||||
expectToBeCalledOnce(task3);
|
||||
expectToBeCalledOnce(task4);
|
||||
});
|
||||
});
|
|
@ -53,6 +53,7 @@ class MessageQueue {
|
|||
this._callbacks = [];
|
||||
this._callbackID = 0;
|
||||
this._lastFlush = 0;
|
||||
this._eventLoopStartTime = new Date().getTime();
|
||||
|
||||
[
|
||||
'invokeCallbackAndReturnFlushedQueue',
|
||||
|
@ -109,6 +110,10 @@ class MessageQueue {
|
|||
return module;
|
||||
}
|
||||
|
||||
getEventLoopRunningTime() {
|
||||
return new Date().getTime() - this._eventLoopStartTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* "Private" methods
|
||||
*/
|
||||
|
@ -150,6 +155,7 @@ class MessageQueue {
|
|||
|
||||
__callFunction(module, method, args) {
|
||||
this._lastFlush = new Date().getTime();
|
||||
this._eventLoopStartTime = this._lastFlush;
|
||||
if (isFinite(module)) {
|
||||
method = this._methodTable[module][method];
|
||||
module = this._moduleTable[module];
|
||||
|
@ -171,6 +177,7 @@ class MessageQueue {
|
|||
__invokeCallback(cbID, args) {
|
||||
Systrace.beginEvent(`MessageQueue.invokeCallback(${cbID})`);
|
||||
this._lastFlush = new Date().getTime();
|
||||
this._eventLoopStartTime = this._lastFlush;
|
||||
let callback = this._callbacks[cbID];
|
||||
if (!callback || __DEV__) {
|
||||
let debug = this._debugInfo[cbID >> 1];
|
||||
|
|
Loading…
Reference in New Issue