Make InteractionManager tasks cancellable

Summary:
Returns a promise-like object with a new cancel function that will dig through the queue
and remove relevant tasks before they are executed. Handy when tasks are scheduled in react
components but should be cleaned up in unmount.

Reviewed By: devknoll

Differential Revision: D3406953

fbshipit-source-id: edf1157d831d5d6b63f13ee64cfd1c46843e79fa
This commit is contained in:
Spencer Ahrens 2016-06-08 22:54:19 -07:00 committed by Facebook Github Bot 2
parent b03a725447
commit be09cccb1f
4 changed files with 97 additions and 28 deletions

View File

@ -16,6 +16,7 @@ const EventEmitter = require('EventEmitter');
const Set = require('Set'); const Set = require('Set');
const TaskQueue = require('TaskQueue'); const TaskQueue = require('TaskQueue');
const infoLog = require('infoLog');
const invariant = require('fbjs/lib/invariant'); const invariant = require('fbjs/lib/invariant');
const keyMirror = require('fbjs/lib/keyMirror'); const keyMirror = require('fbjs/lib/keyMirror');
const setImmediate = require('setImmediate'); const setImmediate = require('setImmediate');
@ -84,24 +85,33 @@ var InteractionManager = {
}), }),
/** /**
* Schedule a function to run after all interactions have completed. * Schedule a function to run after all interactions have completed. Returns a cancellable
* "promise".
*/ */
runAfterInteractions(task: ?Task): Promise<any> { runAfterInteractions(task: ?Task): {then: Function, done: Function, cancel: Function} {
return new Promise(resolve => { const tasks = [];
const promise = new Promise(resolve => {
_scheduleUpdate(); _scheduleUpdate();
if (task) { if (task) {
_taskQueue.enqueue(task); tasks.push(task);
} }
const name = task && task.name || '?'; tasks.push({run: resolve, name: 'resolve ' + (task && task.name || '?')});
_taskQueue.enqueue({run: resolve, name: 'resolve ' + name}); _taskQueue.enqueueTasks(tasks);
}); });
return {
then: promise.then.bind(promise),
done: promise.done.bind(promise),
cancel: function() {
_taskQueue.cancelTasks(tasks);
},
};
}, },
/** /**
* Notify manager that an interaction has started. * Notify manager that an interaction has started.
*/ */
createInteractionHandle(): Handle { createInteractionHandle(): Handle {
DEBUG && console.log('create interaction handle'); DEBUG && infoLog('create interaction handle');
_scheduleUpdate(); _scheduleUpdate();
var handle = ++_inc; var handle = ++_inc;
_addInteractionSet.add(handle); _addInteractionSet.add(handle);
@ -112,7 +122,7 @@ var InteractionManager = {
* Notify manager that an interaction has completed. * Notify manager that an interaction has completed.
*/ */
clearInteractionHandle(handle: Handle) { clearInteractionHandle(handle: Handle) {
DEBUG && console.log('clear interaction handle'); DEBUG && infoLog('clear interaction handle');
invariant( invariant(
!!handle, !!handle,
'Must provide a handle to clear.' 'Must provide a handle to clear.'

View File

@ -11,6 +11,7 @@
*/ */
'use strict'; 'use strict';
const infoLog = require('infoLog');
const invariant = require('fbjs/lib/invariant'); const invariant = require('fbjs/lib/invariant');
type SimpleTask = { type SimpleTask = {
@ -63,6 +64,20 @@ class TaskQueue {
this._getCurrentQueue().push(task); this._getCurrentQueue().push(task);
} }
enqueueTasks(tasks: Array<Task>): void {
tasks.forEach((task) => this.enqueue(task));
}
cancelTasks(tasksToCancel: Array<Task>): void {
// search through all tasks and remove them.
this._queueStack = this._queueStack
.map((queue) => ({
...queue,
tasks: queue.tasks.filter((task) => tasksToCancel.indexOf(task) === -1),
}))
.filter((queue) => queue.tasks.length > 0);
}
/** /**
* Check to see if `processNext` should be called. * Check to see if `processNext` should be called.
* *
@ -86,10 +101,10 @@ class TaskQueue {
const task = queue.shift(); const task = queue.shift();
try { try {
if (task.gen) { if (task.gen) {
DEBUG && console.log('genPromise for task ' + task.name); DEBUG && infoLog('genPromise for task ' + task.name);
this._genPromise((task: any)); // Rather than annoying tagged union this._genPromise((task: any)); // Rather than annoying tagged union
} else if (task.run) { } else if (task.run) {
DEBUG && console.log('run task ' + task.name); DEBUG && infoLog('run task ' + task.name);
task.run(); task.run();
} else { } else {
invariant( invariant(
@ -97,7 +112,7 @@ class TaskQueue {
'Expected Function, SimpleTask, or PromiseTask, but got:\n' + 'Expected Function, SimpleTask, or PromiseTask, but got:\n' +
JSON.stringify(task, null, 2) JSON.stringify(task, null, 2)
); );
DEBUG && console.log('run anonymous task'); DEBUG && infoLog('run anonymous task');
task(); task();
} }
} catch (e) { } catch (e) {
@ -118,7 +133,7 @@ class TaskQueue {
queue.tasks.length === 0 && queue.tasks.length === 0 &&
this._queueStack.length > 1) { this._queueStack.length > 1) {
this._queueStack.pop(); this._queueStack.pop();
DEBUG && console.log('popped queue: ', {stackIdx, queueStackSize: this._queueStack.length}); DEBUG && infoLog('popped queue: ', {stackIdx, queueStackSize: this._queueStack.length});
return this._getCurrentQueue(); return this._getCurrentQueue();
} else { } else {
return queue.tasks; return queue.tasks;
@ -132,11 +147,11 @@ class TaskQueue {
// happens once it is fully processed. // happens once it is fully processed.
this._queueStack.push({tasks: [], popable: false}); this._queueStack.push({tasks: [], popable: false});
const stackIdx = this._queueStack.length - 1; const stackIdx = this._queueStack.length - 1;
DEBUG && console.log('push new queue: ', {stackIdx}); DEBUG && infoLog('push new queue: ', {stackIdx});
DEBUG && console.log('exec gen task ' + task.name); DEBUG && infoLog('exec gen task ' + task.name);
task.gen() task.gen()
.then(() => { .then(() => {
DEBUG && console.log('onThen for gen task ' + task.name, {stackIdx, queueStackSize: this._queueStack.length}); DEBUG && infoLog('onThen for gen task ' + task.name, {stackIdx, queueStackSize: this._queueStack.length});
this._queueStack[stackIdx].popable = true; this._queueStack[stackIdx].popable = true;
this.hasTasksToProcess() && this._onMoreTasks(); this.hasTasksToProcess() && this._onMoreTasks();
}) })

View File

@ -1,5 +1,11 @@
/** /**
* Copyright 2004-present Facebook. All Rights Reserved. * Copyright (c) 2013-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.
*
*/ */
'use strict'; 'use strict';
@ -49,7 +55,7 @@ describe('InteractionManager', () => {
}); });
it('notifies asynchronously when interaction stops', () => { it('notifies asynchronously when interaction stops', () => {
var handle = InteractionManager.createInteractionHandle(); const handle = InteractionManager.createInteractionHandle();
jest.runAllTimers(); jest.runAllTimers();
interactionStart.mockClear(); interactionStart.mockClear();
InteractionManager.clearInteractionHandle(handle); InteractionManager.clearInteractionHandle(handle);
@ -61,7 +67,7 @@ describe('InteractionManager', () => {
}); });
it('does not notify when started & stopped in same event loop', () => { it('does not notify when started & stopped in same event loop', () => {
var handle = InteractionManager.createInteractionHandle(); const handle = InteractionManager.createInteractionHandle();
InteractionManager.clearInteractionHandle(handle); InteractionManager.clearInteractionHandle(handle);
jest.runAllTimers(); jest.runAllTimers();
@ -71,7 +77,7 @@ describe('InteractionManager', () => {
it('does not notify when going from two -> one active interactions', () => { it('does not notify when going from two -> one active interactions', () => {
InteractionManager.createInteractionHandle(); InteractionManager.createInteractionHandle();
var handle = InteractionManager.createInteractionHandle(); const handle = InteractionManager.createInteractionHandle();
jest.runAllTimers(); jest.runAllTimers();
interactionStart.mockClear(); interactionStart.mockClear();
@ -84,7 +90,7 @@ describe('InteractionManager', () => {
}); });
it('runs tasks asynchronously when there are interactions', () => { it('runs tasks asynchronously when there are interactions', () => {
var task = jest.fn(); const task = jest.fn();
InteractionManager.runAfterInteractions(task); InteractionManager.runAfterInteractions(task);
expect(task).not.toBeCalled(); expect(task).not.toBeCalled();
@ -93,8 +99,8 @@ describe('InteractionManager', () => {
}); });
it('runs tasks when interactions complete', () => { it('runs tasks when interactions complete', () => {
var task = jest.fn(); const task = jest.fn();
var handle = InteractionManager.createInteractionHandle(); const handle = InteractionManager.createInteractionHandle();
InteractionManager.runAfterInteractions(task); InteractionManager.runAfterInteractions(task);
jest.runAllTimers(); jest.runAllTimers();
@ -106,8 +112,8 @@ describe('InteractionManager', () => {
}); });
it('does not run tasks twice', () => { it('does not run tasks twice', () => {
var task1 = jest.fn(); const task1 = jest.fn();
var task2 = jest.fn(); const task2 = jest.fn();
InteractionManager.runAfterInteractions(task1); InteractionManager.runAfterInteractions(task1);
jest.runAllTimers(); jest.runAllTimers();
@ -118,10 +124,10 @@ describe('InteractionManager', () => {
}); });
it('runs tasks added while processing previous tasks', () => { it('runs tasks added while processing previous tasks', () => {
var task1 = jest.fn(() => { const task1 = jest.fn(() => {
InteractionManager.runAfterInteractions(task2); InteractionManager.runAfterInteractions(task2);
}); });
var task2 = jest.fn(); const task2 = jest.fn();
InteractionManager.runAfterInteractions(task1); InteractionManager.runAfterInteractions(task1);
expect(task2).not.toBeCalled(); expect(task2).not.toBeCalled();
@ -131,6 +137,20 @@ describe('InteractionManager', () => {
expect(task1).toBeCalled(); expect(task1).toBeCalled();
expect(task2).toBeCalled(); expect(task2).toBeCalled();
}); });
it('allows tasks to be cancelled', () => {
const task1 = jest.fn();
const task2 = jest.fn();
const promise1 = InteractionManager.runAfterInteractions(task1);
InteractionManager.runAfterInteractions(task2);
expect(task1).not.toBeCalled();
expect(task2).not.toBeCalled();
promise1.cancel();
jest.runAllTimers();
expect(task1).not.toBeCalled();
expect(task2).toBeCalled();
});
}); });
describe('promise tasks', () => { describe('promise tasks', () => {

View File

@ -1,5 +1,11 @@
/** /**
* Copyright 2004-present Facebook. All Rights Reserved. * Copyright (c) 2013-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.
*
*/ */
'use strict'; 'use strict';
@ -15,7 +21,7 @@ function clearTaskQueue(taskQueue) {
jest.runAllTimers(); jest.runAllTimers();
taskQueue.processNext(); taskQueue.processNext();
jest.runAllTimers(); jest.runAllTimers();
} while (taskQueue.hasTasksToProcess()) } while (taskQueue.hasTasksToProcess());
} }
describe('TaskQueue', () => { describe('TaskQueue', () => {
@ -118,4 +124,22 @@ describe('TaskQueue', () => {
expectToBeCalledOnce(task3); expectToBeCalledOnce(task3);
expectToBeCalledOnce(task4); expectToBeCalledOnce(task4);
}); });
it('should be able to cancel tasks', () => {
const task1 = jest.fn();
const task2 = createSequenceTask(1);
const task3 = jest.fn();
const task4 = createSequenceTask(2);
taskQueue.enqueue(task1);
taskQueue.enqueue(task2);
taskQueue.enqueue(task3);
taskQueue.enqueue(task4);
taskQueue.cancelTasks([task1, task3]);
clearTaskQueue(taskQueue);
expect(task1).not.toBeCalled();
expect(task3).not.toBeCalled();
expectToBeCalledOnce(task2);
expectToBeCalledOnce(task4);
expect(taskQueue.hasTasksToProcess()).toBe(false);
});
}); });