1import { NativeModulesProxy, EventEmitter } from 'expo-core';
2
3interface TaskError {
4  code: string | number,
5  message: string,
6}
7
8interface TaskBody {
9  data: object,
10  error: TaskError | null,
11  executionInfo: {
12    eventId: string,
13    taskName: string,
14  },
15}
16
17interface RegisteredTask {
18  taskName: string,
19  taskType: string,
20  options: any,
21}
22
23type Task = (body: TaskBody) => void;
24
25const { ExpoTaskManager } = NativeModulesProxy;
26const eventEmitter = new EventEmitter(ExpoTaskManager);
27const tasks: Map<string, Task> = new Map<string, Task>();
28
29let isRunningDuringInitialization = true;
30
31function _validateTaskName(taskName) {
32  if (!taskName || typeof taskName !== 'string') {
33    throw new TypeError('`taskName` must be a non-empty string.')
34  }
35}
36
37export function defineTask(taskName: string, task: Task) {
38  if (!isRunningDuringInitialization) {
39    console.error(`TaskManager.defineTask must be called during initialization phase!`);
40    return;
41  }
42  if (!taskName || typeof taskName !== 'string') {
43    console.warn(`TaskManager.defineTask: 'taskName' argument must be a non-empty string.`);
44    return;
45  }
46  if (!task || typeof task !== 'function') {
47    console.warn(`TaskManager.defineTask: 'task' argument must be a function.`);
48    return;
49  }
50  if (tasks.has(taskName)) {
51    console.warn(`TaskManager.defineTask: task '${taskName}' is already defined.`);
52    return;
53  }
54  tasks.set(taskName, task);
55}
56
57export function isTaskDefined(taskName: string): boolean {
58  return tasks.has(taskName);
59}
60
61export async function isTaskRegisteredAsync(taskName: string): Promise<boolean> {
62  _validateTaskName(taskName);
63  return ExpoTaskManager.isTaskRegisteredAsync(taskName);
64}
65
66export async function getTaskOptionsAsync<TaskOptions>(taskName: string): Promise<TaskOptions> {
67  _validateTaskName(taskName);
68  return ExpoTaskManager.getTaskOptionsAsync(taskName);
69}
70
71export async function getRegisteredTasksAsync(): Promise<RegisteredTask[]> {
72  return ExpoTaskManager.getRegisteredTasksAsync();
73}
74
75export async function unregisterTaskAsync(taskName: string): Promise<void> {
76  _validateTaskName(taskName);
77  await ExpoTaskManager.unregisterTaskAsync(taskName);
78}
79
80export async function unregisterAllTasksAsync(): Promise<void> {
81  await ExpoTaskManager.unregisterAllTasksAsync();
82}
83
84eventEmitter.addListener<TaskBody>(ExpoTaskManager.EVENT_NAME, async ({ data, error, executionInfo }) => {
85  const { eventId, taskName } = executionInfo;
86  const task = tasks.get(taskName);
87  let result: any = null;
88
89  if (task) {
90    try {
91      // Execute JS task
92      result = await task({ data, error, executionInfo });
93    } catch (error) {
94      console.error(`TaskManager: Task "${taskName}" failed:`, error);
95    } finally {
96      // Notify manager the task is finished.
97      await ExpoTaskManager.notifyTaskFinishedAsync(taskName, { eventId, result });
98    }
99  } else {
100    console.warn(`TaskManager: Task "${taskName}" has been executed but looks like it is not defined. Please make sure that "TaskManager.defineTask" is called during initialization phase.`);
101    // No tasks defined -> we need to notify about finish anyway.
102    await ExpoTaskManager.notifyTaskFinishedAsync(taskName, { eventId, result });
103    // We should also unregister such tasks automatically as the task might have been removed
104    // from the app or just renamed - in that case it needs to be registered again (with the new name).
105    await ExpoTaskManager.unregisterTaskAsync(taskName);
106  }
107});
108
109// @tsapeta: Turn off `defineTask` function right after the initialization phase.
110// Promise.resolve() ensures that it will be called as a microtask just after the first event loop.
111Promise.resolve().then(() => {
112  isRunningDuringInitialization = false;
113});
114