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