Skip to content

Commit 47d77ac

Browse files
ntdiarynecolas
authored andcommitted
[change] Add task queue for InteractionManager
Close #2399
1 parent 6186604 commit 47d77ac

File tree

5 files changed

+713
-26
lines changed

5 files changed

+713
-26
lines changed

packages/babel-plugin-react-native-web/src/__tests__/__snapshots__/index-test.js.snap

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -95,11 +95,8 @@ const { StyleSheet, Pressable } = require('react-native');
9595
↓ ↓ ↓ ↓ ↓ ↓
9696
9797
const ReactNative = require('react-native-web/dist/index');
98-
9998
const View = require('react-native-web/dist/exports/View').default;
100-
10199
const StyleSheet = require('react-native-web/dist/exports/StyleSheet').default;
102-
103100
const Pressable = require('react-native-web/dist/exports/Pressable').default;
104101
105102
@@ -114,12 +111,9 @@ const { StyleSheet, Pressable } = require('react-native');
114111
↓ ↓ ↓ ↓ ↓ ↓
115112
116113
const ReactNative = require('react-native-web/dist/cjs/index');
117-
118114
const View = require('react-native-web/dist/cjs/exports/View').default;
119-
120115
const StyleSheet =
121116
require('react-native-web/dist/cjs/exports/StyleSheet').default;
122-
123117
const Pressable =
124118
require('react-native-web/dist/cjs/exports/Pressable').default;
125119
@@ -135,16 +129,11 @@ const { StyleSheet, View, Pressable, processColor } = require('react-native-web'
135129
↓ ↓ ↓ ↓ ↓ ↓
136130
137131
const ReactNative = require('react-native-web/dist/index');
138-
139132
const unstable_createElement =
140133
require('react-native-web/dist/exports/createElement').default;
141-
142134
const StyleSheet = require('react-native-web/dist/exports/StyleSheet').default;
143-
144135
const View = require('react-native-web/dist/exports/View').default;
145-
146136
const Pressable = require('react-native-web/dist/exports/Pressable').default;
147-
148137
const processColor =
149138
require('react-native-web/dist/exports/processColor').default;
150139
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/**
2+
* Copyright (c) Nicolas Gallagher.
3+
* Copyright (c) Meta Platforms, Inc. and affiliates.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*
8+
* @flow
9+
*/
10+
11+
import invariant from 'fbjs/lib/invariant';
12+
13+
type SimpleTask = {|
14+
name: string,
15+
run: () => void
16+
|};
17+
type PromiseTask = {|
18+
name: string,
19+
gen: () => Promise<void>
20+
|};
21+
export type Task = SimpleTask | PromiseTask | (() => void);
22+
23+
class TaskQueue {
24+
constructor({ onMoreTasks }: { onMoreTasks: () => void, ... }) {
25+
this._onMoreTasks = onMoreTasks;
26+
this._queueStack = [{ tasks: [], popable: true }];
27+
}
28+
29+
enqueue(task: Task): void {
30+
this._getCurrentQueue().push(task);
31+
}
32+
33+
enqueueTasks(tasks: Array<Task>): void {
34+
tasks.forEach((task) => this.enqueue(task));
35+
}
36+
37+
cancelTasks(tasksToCancel: Array<Task>): void {
38+
this._queueStack = this._queueStack
39+
.map((queue) => ({
40+
...queue,
41+
tasks: queue.tasks.filter((task) => tasksToCancel.indexOf(task) === -1)
42+
}))
43+
.filter((queue, idx) => queue.tasks.length > 0 || idx === 0);
44+
}
45+
46+
hasTasksToProcess(): boolean {
47+
return this._getCurrentQueue().length > 0;
48+
}
49+
50+
/**
51+
* Executes the next task in the queue.
52+
*/
53+
processNext(): void {
54+
const queue = this._getCurrentQueue();
55+
if (queue.length) {
56+
const task = queue.shift();
57+
try {
58+
if (typeof task === 'object' && task.gen) {
59+
this._genPromise(task);
60+
} else if (typeof task === 'object' && task.run) {
61+
task.run();
62+
} else {
63+
invariant(
64+
typeof task === 'function',
65+
'Expected Function, SimpleTask, or PromiseTask, but got:\n' +
66+
JSON.stringify(task, null, 2)
67+
);
68+
task();
69+
}
70+
} catch (e) {
71+
e.message =
72+
'TaskQueue: Error with task ' + (task.name || '') + ': ' + e.message;
73+
throw e;
74+
}
75+
}
76+
}
77+
78+
_queueStack: Array<{
79+
tasks: Array<Task>,
80+
popable: boolean,
81+
...
82+
}>;
83+
_onMoreTasks: () => void;
84+
85+
_getCurrentQueue(): Array<Task> {
86+
const stackIdx = this._queueStack.length - 1;
87+
const queue = this._queueStack[stackIdx];
88+
if (queue.popable && queue.tasks.length === 0 && stackIdx > 0) {
89+
this._queueStack.pop();
90+
return this._getCurrentQueue();
91+
} else {
92+
return queue.tasks;
93+
}
94+
}
95+
96+
_genPromise(task: PromiseTask) {
97+
const length = this._queueStack.push({ tasks: [], popable: false });
98+
const stackIdx = length - 1;
99+
const stackItem = this._queueStack[stackIdx];
100+
task
101+
.gen()
102+
.then(() => {
103+
stackItem.popable = true;
104+
this.hasTasksToProcess() && this._onMoreTasks();
105+
})
106+
.catch((ex) => {
107+
setTimeout(() => {
108+
ex.message = `TaskQueue: Error resolving Promise in task ${task.name}: ${ex.message}`;
109+
throw ex;
110+
}, 0);
111+
});
112+
}
113+
}
114+
115+
export default TaskQueue;
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
/**
2+
* Copyright (c) Nicolas Gallagher.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
function expectToBeCalledOnce(fn) {
9+
expect(fn.mock.calls.length).toBe(1);
10+
}
11+
12+
function clearTaskQueue(taskQueue) {
13+
do {
14+
jest.runAllTimers();
15+
taskQueue.processNext();
16+
jest.runAllTimers();
17+
} while (taskQueue.hasTasksToProcess());
18+
}
19+
20+
describe('TaskQueue', () => {
21+
let taskQueue;
22+
let onMoreTasks;
23+
let sequenceId;
24+
25+
function createSequenceTask(expectedSequenceId) {
26+
return jest.fn(() => {
27+
expect(++sequenceId).toBe(expectedSequenceId);
28+
});
29+
}
30+
31+
beforeEach(() => {
32+
jest.resetModules();
33+
onMoreTasks = jest.fn();
34+
const TaskQueue = require('../TaskQueue');
35+
taskQueue = new TaskQueue({ onMoreTasks });
36+
sequenceId = 0;
37+
});
38+
39+
it('should run a basic task', () => {
40+
const task1 = createSequenceTask(1);
41+
taskQueue.enqueue({ run: task1, name: 'run1' });
42+
expect(taskQueue.hasTasksToProcess()).toBe(true);
43+
taskQueue.processNext();
44+
expectToBeCalledOnce(task1);
45+
});
46+
47+
it('should handle blocking promise task', () => {
48+
onMoreTasks.mockImplementation(() => {
49+
taskQueue.processNext();
50+
jest.runAllTimers();
51+
});
52+
53+
const task1 = jest.fn(() => {
54+
return new Promise((resolve) => {
55+
setTimeout(() => {
56+
expect(++sequenceId).toBe(1);
57+
resolve();
58+
}, 1);
59+
});
60+
});
61+
const task2 = createSequenceTask(2);
62+
taskQueue.enqueue({ gen: task1, name: 'gen1' });
63+
taskQueue.enqueue({ run: task2, name: 'run2' });
64+
65+
taskQueue.processNext();
66+
67+
expectToBeCalledOnce(task1);
68+
expect(task2).not.toBeCalled();
69+
expect(onMoreTasks).not.toBeCalled();
70+
expect(taskQueue.hasTasksToProcess()).toBe(false);
71+
72+
clearTaskQueue(taskQueue);
73+
74+
return new Promise((resolve) => {
75+
setTimeout(() => {
76+
resolve();
77+
});
78+
}).then(() => {
79+
expectToBeCalledOnce(onMoreTasks);
80+
expectToBeCalledOnce(task2);
81+
});
82+
});
83+
84+
it('should handle nested simple tasks', () => {
85+
const task1 = jest.fn(() => {
86+
expect(++sequenceId).toBe(1);
87+
taskQueue.enqueue({ run: task3, name: 'run3' });
88+
});
89+
const task2 = createSequenceTask(2);
90+
const task3 = createSequenceTask(3);
91+
taskQueue.enqueue({ run: task1, name: 'run1' });
92+
taskQueue.enqueue({ run: task2, name: 'run2' }); // not blocked by task 1
93+
94+
clearTaskQueue(taskQueue);
95+
96+
expectToBeCalledOnce(task1);
97+
expectToBeCalledOnce(task2);
98+
expectToBeCalledOnce(task3);
99+
});
100+
101+
it('should handle nested promises', () => {
102+
onMoreTasks.mockImplementation(() => {
103+
taskQueue.processNext();
104+
jest.runAllTimers();
105+
});
106+
107+
const task1 = jest.fn(() => {
108+
return new Promise((resolve) => {
109+
setTimeout(() => {
110+
expect(++sequenceId).toBe(1);
111+
taskQueue.enqueue({ gen: task2, name: 'gen2' });
112+
taskQueue.enqueue({ run: resolve, name: 'resolve1' });
113+
}, 1);
114+
});
115+
});
116+
const task2 = jest.fn(() => {
117+
return new Promise((resolve) => {
118+
setTimeout(() => {
119+
expect(++sequenceId).toBe(2);
120+
taskQueue.enqueue({ run: task3, name: 'run3' });
121+
taskQueue.enqueue({ run: resolve, name: 'resolve2' });
122+
}, 1);
123+
});
124+
});
125+
const task3 = createSequenceTask(3);
126+
const task4 = createSequenceTask(4);
127+
taskQueue.enqueue({ gen: task1, name: 'gen1' });
128+
taskQueue.enqueue({ run: task4, name: 'run4' }); // blocked by task 1 promise
129+
130+
clearTaskQueue(taskQueue);
131+
132+
return new Promise((resolve) => {
133+
setTimeout(() => {
134+
resolve();
135+
});
136+
}).then(() => {
137+
expectToBeCalledOnce(task1);
138+
expectToBeCalledOnce(task2);
139+
expectToBeCalledOnce(task3);
140+
expectToBeCalledOnce(task4);
141+
});
142+
});
143+
144+
it('should be able to cancel tasks', () => {
145+
const task1 = jest.fn();
146+
const task2 = createSequenceTask(1);
147+
const task3 = jest.fn();
148+
const task4 = createSequenceTask(2);
149+
taskQueue.enqueue(task1);
150+
taskQueue.enqueue(task2);
151+
taskQueue.enqueue(task3);
152+
taskQueue.enqueue(task4);
153+
taskQueue.cancelTasks([task1, task3]);
154+
clearTaskQueue(taskQueue);
155+
expect(task1).not.toBeCalled();
156+
expect(task3).not.toBeCalled();
157+
expectToBeCalledOnce(task2);
158+
expectToBeCalledOnce(task4);
159+
expect(taskQueue.hasTasksToProcess()).toBe(false);
160+
});
161+
162+
it('should not crash when last task is cancelled', () => {
163+
const task1 = jest.fn();
164+
taskQueue.enqueue(task1);
165+
taskQueue.cancelTasks([task1]);
166+
clearTaskQueue(taskQueue);
167+
expect(task1).not.toBeCalled();
168+
expect(taskQueue.hasTasksToProcess()).toBe(false);
169+
});
170+
171+
it('should not crash when task is cancelled between being started and resolved', () => {
172+
const task1 = jest.fn(() => {
173+
return new Promise((resolve) => {
174+
setTimeout(() => {
175+
resolve();
176+
}, 1);
177+
});
178+
});
179+
180+
taskQueue.enqueue({ gen: task1, name: 'gen1' });
181+
taskQueue.processNext();
182+
taskQueue.cancelTasks([task1]);
183+
jest.runAllTimers();
184+
});
185+
});

0 commit comments

Comments
 (0)