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