Skip to content

Commit

Permalink
[change] Add task queue for InteractionManager
Browse files Browse the repository at this point in the history
Close #2399
  • Loading branch information
ntdiary authored and necolas committed Mar 20, 2023
1 parent 6186604 commit 47d77ac
Show file tree
Hide file tree
Showing 5 changed files with 713 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,8 @@ const { StyleSheet, Pressable } = require('react-native');
↓ ↓ ↓ ↓ ↓ ↓
const ReactNative = require('react-native-web/dist/index');
const View = require('react-native-web/dist/exports/View').default;
const StyleSheet = require('react-native-web/dist/exports/StyleSheet').default;
const Pressable = require('react-native-web/dist/exports/Pressable').default;
Expand All @@ -114,12 +111,9 @@ const { StyleSheet, Pressable } = require('react-native');
↓ ↓ ↓ ↓ ↓ ↓
const ReactNative = require('react-native-web/dist/cjs/index');
const View = require('react-native-web/dist/cjs/exports/View').default;
const StyleSheet =
require('react-native-web/dist/cjs/exports/StyleSheet').default;
const Pressable =
require('react-native-web/dist/cjs/exports/Pressable').default;
Expand All @@ -135,16 +129,11 @@ const { StyleSheet, View, Pressable, processColor } = require('react-native-web'
↓ ↓ ↓ ↓ ↓ ↓
const ReactNative = require('react-native-web/dist/index');
const unstable_createElement =
require('react-native-web/dist/exports/createElement').default;
const StyleSheet = require('react-native-web/dist/exports/StyleSheet').default;
const View = require('react-native-web/dist/exports/View').default;
const Pressable = require('react-native-web/dist/exports/Pressable').default;
const processColor =
require('react-native-web/dist/exports/processColor').default;
Expand Down
115 changes: 115 additions & 0 deletions packages/react-native-web/src/exports/InteractionManager/TaskQueue.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/**
* Copyright (c) Nicolas Gallagher.
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/

import invariant from 'fbjs/lib/invariant';

type SimpleTask = {|
name: string,
run: () => void
|};
type PromiseTask = {|
name: string,
gen: () => Promise<void>
|};
export type Task = SimpleTask | PromiseTask | (() => void);

class TaskQueue {
constructor({ onMoreTasks }: { onMoreTasks: () => void, ... }) {
this._onMoreTasks = onMoreTasks;
this._queueStack = [{ tasks: [], popable: true }];
}

enqueue(task: Task): void {
this._getCurrentQueue().push(task);
}

enqueueTasks(tasks: Array<Task>): void {
tasks.forEach((task) => this.enqueue(task));
}

cancelTasks(tasksToCancel: Array<Task>): void {
this._queueStack = this._queueStack
.map((queue) => ({
...queue,
tasks: queue.tasks.filter((task) => tasksToCancel.indexOf(task) === -1)
}))
.filter((queue, idx) => queue.tasks.length > 0 || idx === 0);
}

hasTasksToProcess(): boolean {
return this._getCurrentQueue().length > 0;
}

/**
* Executes the next task in the queue.
*/
processNext(): void {
const queue = this._getCurrentQueue();
if (queue.length) {
const task = queue.shift();
try {
if (typeof task === 'object' && task.gen) {
this._genPromise(task);
} else if (typeof task === 'object' && task.run) {
task.run();
} else {
invariant(
typeof task === 'function',
'Expected Function, SimpleTask, or PromiseTask, but got:\n' +
JSON.stringify(task, null, 2)
);
task();
}
} catch (e) {
e.message =
'TaskQueue: Error with task ' + (task.name || '') + ': ' + e.message;
throw e;
}
}
}

_queueStack: Array<{
tasks: Array<Task>,
popable: boolean,
...
}>;
_onMoreTasks: () => void;

_getCurrentQueue(): Array<Task> {
const stackIdx = this._queueStack.length - 1;
const queue = this._queueStack[stackIdx];
if (queue.popable && queue.tasks.length === 0 && stackIdx > 0) {
this._queueStack.pop();
return this._getCurrentQueue();
} else {
return queue.tasks;
}
}

_genPromise(task: PromiseTask) {
const length = this._queueStack.push({ tasks: [], popable: false });
const stackIdx = length - 1;
const stackItem = this._queueStack[stackIdx];
task
.gen()
.then(() => {
stackItem.popable = true;
this.hasTasksToProcess() && this._onMoreTasks();
})
.catch((ex) => {
setTimeout(() => {
ex.message = `TaskQueue: Error resolving Promise in task ${task.name}: ${ex.message}`;
throw ex;
}, 0);
});
}
}

export default TaskQueue;
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
/**
* Copyright (c) Nicolas Gallagher.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

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.fn(() => {
expect(++sequenceId).toBe(expectedSequenceId);
});
}

beforeEach(() => {
jest.resetModules();
onMoreTasks = jest.fn();
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', () => {
onMoreTasks.mockImplementation(() => {
taskQueue.processNext();
jest.runAllTimers();
});

const task1 = jest.fn(() => {
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);

return new Promise((resolve) => {
setTimeout(() => {
resolve();
});
}).then(() => {
expectToBeCalledOnce(onMoreTasks);
expectToBeCalledOnce(task2);
});
});

it('should handle nested simple tasks', () => {
const task1 = jest.fn(() => {
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', () => {
onMoreTasks.mockImplementation(() => {
taskQueue.processNext();
jest.runAllTimers();
});

const task1 = jest.fn(() => {
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.fn(() => {
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);

return new Promise((resolve) => {
setTimeout(() => {
resolve();
});
}).then(() => {
expectToBeCalledOnce(task1);
expectToBeCalledOnce(task2);
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);
});

it('should not crash when last task is cancelled', () => {
const task1 = jest.fn();
taskQueue.enqueue(task1);
taskQueue.cancelTasks([task1]);
clearTaskQueue(taskQueue);
expect(task1).not.toBeCalled();
expect(taskQueue.hasTasksToProcess()).toBe(false);
});

it('should not crash when task is cancelled between being started and resolved', () => {
const task1 = jest.fn(() => {
return new Promise((resolve) => {
setTimeout(() => {
resolve();
}, 1);
});
});

taskQueue.enqueue({ gen: task1, name: 'gen1' });
taskQueue.processNext();
taskQueue.cancelTasks([task1]);
jest.runAllTimers();
});
});
Loading

0 comments on commit 47d77ac

Please sign in to comment.