Skip to content
Permalink
Browse files

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
  • Loading branch information
sahrens authored and Facebook Github Bot 2 committed Jun 9, 2016
1 parent b03a725 commit be09cccb1f2153f84da85214b31cd4cdb4e783ec
@@ -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<any> {
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),

This comment has been minimized.

Copy link
@satya164

satya164 Jul 8, 2016

Contributor

@sahrens InteractionManager is broken in my setup. I tracked it down to this line. I don't have done in my Promise prototype. Using babel-preset-react-native, on Android. So my app is broken in 0.29 unless I do this Promise.prototype.done = () => {}

Willing to send a PR to fix this. What do you suggest I do?

This comment has been minimized.

Copy link
@roman01la

roman01la Jul 27, 2016

I'm also having this issue in 0.30 when running InteractionManager.runAfterInteractions. This issue was also present and later fixed in earlier versions of RN.

This comment has been minimized.

Copy link
@sahrens

sahrens via email Jul 27, 2016

Author Contributor

This comment has been minimized.

Copy link
@roman01la

roman01la Jul 27, 2016

@sahrens It is broken again.

This comment has been minimized.

Copy link
@sahrens

sahrens Sep 14, 2016

Author Contributor

Addressed in #9886

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.'
@@ -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<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.
*
@@ -86,18 +101,18 @@ 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(
typeof task === 'function',
'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();
})
@@ -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', () => {
@@ -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);
});
});

5 comments on commit be09ccc

@satya164

This comment has been minimized.

Copy link
Contributor

satya164 replied Jul 8, 2016

I'm also getting a crash when trying to cancel a task.

image

@satya164

This comment has been minimized.

@sahrens

This comment has been minimized.

Copy link
Contributor Author

sahrens replied Jul 9, 2016

Oops, yeah, it's possible to cancel the last task and leave _queueStack in an invalid state. Fix coming.

@gre

This comment has been minimized.

Copy link
Contributor

gre replied Jul 20, 2016

Also, can you guys confirm if this is a bug: #8624
(is this the fix you are talking about?)

or has the semantic of runAfterInteractions changed to a fire-and-forget? (is there a guarantee it's always called)

Thanks

@FangHaydn

This comment has been minimized.

Copy link

FangHaydn replied Dec 6, 2018

I have the same problem. in another component i add a loop animate, but I forgot to stop it. so I add this code:
componentWillUnmount () { this.timeAnimate.stop(); };

Animated.timing(this.rotation, { toValue: 1, duration: 800, isInteraction: false, // unstock runAfterInteractions() });

Please sign in to comment.
You can’t perform that action at this time.