diff --git a/Libraries/Interaction/InteractionManager.js b/Libraries/Interaction/InteractionManager.js index 6d36468508e..845df4d8983 100644 --- a/Libraries/Interaction/InteractionManager.js +++ b/Libraries/Interaction/InteractionManager.js @@ -16,6 +16,7 @@ const EventEmitter = require('EventEmitter'); const Set = require('Set'); const TaskQueue = require('TaskQueue'); +const infoLog = require('infoLog'); const invariant = require('fbjs/lib/invariant'); const keyMirror = require('fbjs/lib/keyMirror'); 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 { - return new Promise(resolve => { + runAfterInteractions(task: ?Task): {then: Function, done: Function, cancel: Function} { + const tasks = []; + const promise = new Promise(resolve => { _scheduleUpdate(); if (task) { - _taskQueue.enqueue(task); + tasks.push(task); } - const name = task && task.name || '?'; - _taskQueue.enqueue({run: resolve, name: 'resolve ' + name}); + tasks.push({run: resolve, name: 'resolve ' + (task && task.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. */ createInteractionHandle(): Handle { - DEBUG && console.log('create interaction handle'); + DEBUG && infoLog('create interaction handle'); _scheduleUpdate(); var handle = ++_inc; _addInteractionSet.add(handle); @@ -112,7 +122,7 @@ var InteractionManager = { * Notify manager that an interaction has completed. */ clearInteractionHandle(handle: Handle) { - DEBUG && console.log('clear interaction handle'); + DEBUG && infoLog('clear interaction handle'); invariant( !!handle, 'Must provide a handle to clear.' diff --git a/Libraries/Interaction/TaskQueue.js b/Libraries/Interaction/TaskQueue.js index 5d1d1160bdc..33838b15a15 100644 --- a/Libraries/Interaction/TaskQueue.js +++ b/Libraries/Interaction/TaskQueue.js @@ -11,6 +11,7 @@ */ 'use strict'; +const infoLog = require('infoLog'); const invariant = require('fbjs/lib/invariant'); type SimpleTask = { @@ -63,6 +64,20 @@ class TaskQueue { this._getCurrentQueue().push(task); } + enqueueTasks(tasks: Array): void { + tasks.forEach((task) => this.enqueue(task)); + } + + cancelTasks(tasksToCancel: Array): 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. * @@ -86,10 +101,10 @@ class TaskQueue { const task = queue.shift(); try { 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 } else if (task.run) { - DEBUG && console.log('run task ' + task.name); + DEBUG && infoLog('run task ' + task.name); task.run(); } else { invariant( @@ -97,7 +112,7 @@ class TaskQueue { 'Expected Function, SimpleTask, or PromiseTask, but got:\n' + JSON.stringify(task, null, 2) ); - DEBUG && console.log('run anonymous task'); + DEBUG && infoLog('run anonymous task'); task(); } } catch (e) { @@ -118,7 +133,7 @@ class TaskQueue { queue.tasks.length === 0 && this._queueStack.length > 1) { 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(); } else { return queue.tasks; @@ -132,11 +147,11 @@ class TaskQueue { // happens once it is fully processed. this._queueStack.push({tasks: [], popable: false}); const stackIdx = this._queueStack.length - 1; - DEBUG && console.log('push new queue: ', {stackIdx}); - DEBUG && console.log('exec gen task ' + task.name); + DEBUG && infoLog('push new queue: ', {stackIdx}); + DEBUG && infoLog('exec gen task ' + task.name); task.gen() .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.hasTasksToProcess() && this._onMoreTasks(); }) diff --git a/Libraries/Interaction/__tests__/InteractionManager-test.js b/Libraries/Interaction/__tests__/InteractionManager-test.js index c41b1e0636d..d7663271868 100644 --- a/Libraries/Interaction/__tests__/InteractionManager-test.js +++ b/Libraries/Interaction/__tests__/InteractionManager-test.js @@ -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'; @@ -49,7 +55,7 @@ describe('InteractionManager', () => { }); it('notifies asynchronously when interaction stops', () => { - var handle = InteractionManager.createInteractionHandle(); + const handle = InteractionManager.createInteractionHandle(); jest.runAllTimers(); interactionStart.mockClear(); InteractionManager.clearInteractionHandle(handle); @@ -61,7 +67,7 @@ describe('InteractionManager', () => { }); it('does not notify when started & stopped in same event loop', () => { - var handle = InteractionManager.createInteractionHandle(); + const handle = InteractionManager.createInteractionHandle(); InteractionManager.clearInteractionHandle(handle); jest.runAllTimers(); @@ -71,7 +77,7 @@ describe('InteractionManager', () => { it('does not notify when going from two -> one active interactions', () => { InteractionManager.createInteractionHandle(); - var handle = InteractionManager.createInteractionHandle(); + const handle = InteractionManager.createInteractionHandle(); jest.runAllTimers(); interactionStart.mockClear(); @@ -84,7 +90,7 @@ describe('InteractionManager', () => { }); it('runs tasks asynchronously when there are interactions', () => { - var task = jest.fn(); + const task = jest.fn(); InteractionManager.runAfterInteractions(task); expect(task).not.toBeCalled(); @@ -93,8 +99,8 @@ describe('InteractionManager', () => { }); it('runs tasks when interactions complete', () => { - var task = jest.fn(); - var handle = InteractionManager.createInteractionHandle(); + const task = jest.fn(); + const handle = InteractionManager.createInteractionHandle(); InteractionManager.runAfterInteractions(task); jest.runAllTimers(); @@ -106,8 +112,8 @@ describe('InteractionManager', () => { }); it('does not run tasks twice', () => { - var task1 = jest.fn(); - var task2 = jest.fn(); + const task1 = jest.fn(); + const task2 = jest.fn(); InteractionManager.runAfterInteractions(task1); jest.runAllTimers(); @@ -118,10 +124,10 @@ describe('InteractionManager', () => { }); it('runs tasks added while processing previous tasks', () => { - var task1 = jest.fn(() => { + const task1 = jest.fn(() => { InteractionManager.runAfterInteractions(task2); }); - var task2 = jest.fn(); + const task2 = jest.fn(); InteractionManager.runAfterInteractions(task1); expect(task2).not.toBeCalled(); @@ -131,6 +137,20 @@ describe('InteractionManager', () => { expect(task1).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', () => { diff --git a/Libraries/Interaction/__tests__/TaskQueue-test.js b/Libraries/Interaction/__tests__/TaskQueue-test.js index 194b333a59e..1da2e4430f2 100644 --- a/Libraries/Interaction/__tests__/TaskQueue-test.js +++ b/Libraries/Interaction/__tests__/TaskQueue-test.js @@ -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'; @@ -15,7 +21,7 @@ function clearTaskQueue(taskQueue) { jest.runAllTimers(); taskQueue.processNext(); jest.runAllTimers(); - } while (taskQueue.hasTasksToProcess()) + } while (taskQueue.hasTasksToProcess()); } describe('TaskQueue', () => { @@ -118,4 +124,22 @@ describe('TaskQueue', () => { expectToBeCalledOnce(task3); 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); + }); });