1import { EventEmitter, UnavailabilityError } from '@unimodules/core';
2
3import ExpoTaskManager from './ExpoTaskManager';
4
5/**
6 * Error object that can be received through TaskManagerTaskBody when the task fails.
7 */
8export interface TaskManagerError {
9  code: string | number;
10  message: string;
11}
12
13/**
14 * Represents the object that is passed to the task executor.
15 */
16export interface TaskManagerTaskBody<T = object> {
17  /**
18   * An object of data passed to the task executor. Its properties depends on the type of the task.
19   */
20  data: T;
21
22  /**
23   * Error object if the task failed or `null` otherwise.
24   */
25  error: TaskManagerError | null;
26
27  /**
28   * Additional details containing unique ID of task event and name of the task.
29   */
30  executionInfo: {
31    eventId: string;
32    taskName: string;
33  };
34}
35
36/**
37 * Represents an already registered task.
38 */
39export interface TaskManagerTask {
40  /**
41   * Name that the task is registered with.
42   */
43  taskName: string;
44
45  /**
46   * Type of the task which depends on how the task was registered.
47   */
48  taskType: string;
49
50  /**
51   * Provides `options` that the task was registered with.
52   */
53  options: any;
54}
55
56/**
57 * @deprecated Use `TaskManagerTask` instead.
58 */
59export interface RegisteredTask extends TaskManagerTask {}
60
61/**
62 * Type of task executor – a function that handles the task.
63 */
64export type TaskManagerTaskExecutor = (body: TaskManagerTaskBody) => void;
65
66const tasks: Map<string, TaskManagerTaskExecutor> = new Map<string, TaskManagerTaskExecutor>();
67
68function _validateTaskName(taskName) {
69  if (!taskName || typeof taskName !== 'string') {
70    throw new TypeError('`taskName` must be a non-empty string.');
71  }
72}
73
74/**
75 * Method that you use to define a task – it saves given task executor under given task name
76 * which must be correlated with the task name used when registering the task.
77 *
78 * @param taskName Name of the task. It must be the same as the name you provided when registering the task.
79 * @param taskExecutor A function that handles the task.
80 */
81export function defineTask(taskName: string, taskExecutor: TaskManagerTaskExecutor) {
82  if (!taskName || typeof taskName !== 'string') {
83    console.warn(`TaskManager.defineTask: 'taskName' argument must be a non-empty string.`);
84    return;
85  }
86  if (!taskExecutor || typeof taskExecutor !== 'function') {
87    console.warn(`TaskManager.defineTask: 'task' argument must be a function.`);
88    return;
89  }
90  tasks.set(taskName, taskExecutor);
91}
92
93/**
94 * Checks whether the task is already defined.
95 *
96 * @param taskName Name of the task.
97 */
98export function isTaskDefined(taskName: string): boolean {
99  return tasks.has(taskName);
100}
101
102/**
103 * Checks whether the task has been registered.
104 *
105 * @param taskName Name of the task.
106 * @returns A promise resolving to boolean value. If `false` then even if the task is defined, it won't be called because it's not registered.
107 */
108export async function isTaskRegisteredAsync(taskName: string): Promise<boolean> {
109  if (!ExpoTaskManager.isTaskRegisteredAsync) {
110    throw new UnavailabilityError('TaskManager', 'isTaskRegisteredAsync');
111  }
112
113  _validateTaskName(taskName);
114  return ExpoTaskManager.isTaskRegisteredAsync(taskName);
115}
116
117/**
118 * Retrieves an `options` object for provided `taskName`.
119 *
120 * @param taskName Name of the task.
121 */
122export async function getTaskOptionsAsync<TaskOptions>(taskName: string): Promise<TaskOptions> {
123  if (!ExpoTaskManager.getTaskOptionsAsync) {
124    throw new UnavailabilityError('TaskManager', 'getTaskOptionsAsync');
125  }
126
127  _validateTaskName(taskName);
128  return ExpoTaskManager.getTaskOptionsAsync(taskName);
129}
130
131/**
132 * Provides informations about registered tasks.
133 *
134 * @returns Returns a promise resolving to an array containing all tasks registered by the app.
135 */
136export async function getRegisteredTasksAsync(): Promise<TaskManagerTask[]> {
137  if (!ExpoTaskManager.getRegisteredTasksAsync) {
138    throw new UnavailabilityError('TaskManager', 'getRegisteredTasksAsync');
139  }
140
141  return ExpoTaskManager.getRegisteredTasksAsync();
142}
143
144/**
145 * Unregisters the task. Tasks are usually registered by other modules (e.g. expo-location).
146 *
147 * @param taskName Name of the task.
148 */
149export async function unregisterTaskAsync(taskName: string): Promise<void> {
150  if (!ExpoTaskManager.unregisterTaskAsync) {
151    throw new UnavailabilityError('TaskManager', 'unregisterTaskAsync');
152  }
153
154  _validateTaskName(taskName);
155  await ExpoTaskManager.unregisterTaskAsync(taskName);
156}
157
158/**
159 * Unregisters all tasks registered by the app. You may want to call this when the user is
160 * signing out and you no longer need to track his location or run any other background tasks.
161 */
162export async function unregisterAllTasksAsync(): Promise<void> {
163  if (!ExpoTaskManager.unregisterAllTasksAsync) {
164    throw new UnavailabilityError('TaskManager', 'unregisterAllTasksAsync');
165  }
166
167  await ExpoTaskManager.unregisterAllTasksAsync();
168}
169
170if (ExpoTaskManager) {
171  const eventEmitter = new EventEmitter(ExpoTaskManager);
172  eventEmitter.addListener<TaskManagerTaskBody>(
173    ExpoTaskManager.EVENT_NAME,
174    async ({ data, error, executionInfo }) => {
175      const { eventId, taskName } = executionInfo;
176      const taskExecutor = tasks.get(taskName);
177      let result: any = null;
178
179      if (taskExecutor) {
180        try {
181          // Execute JS task
182          result = await taskExecutor({ data, error, executionInfo });
183        } catch (error) {
184          console.error(`TaskManager: Task "${taskName}" failed:`, error);
185        } finally {
186          // Notify manager the task is finished.
187          await ExpoTaskManager.notifyTaskFinishedAsync(taskName, { eventId, result });
188        }
189      } else {
190        console.warn(
191          `TaskManager: Task "${taskName}" has been executed but looks like it is not defined. Please make sure that "TaskManager.defineTask" is called during initialization phase.`
192        );
193        // No tasks defined -> we need to notify about finish anyway.
194        await ExpoTaskManager.notifyTaskFinishedAsync(taskName, { eventId, result });
195        // We should also unregister such tasks automatically as the task might have been removed
196        // from the app or just renamed - in that case it needs to be registered again (with the new name).
197        await ExpoTaskManager.unregisterTaskAsync(taskName);
198      }
199    }
200  );
201}
202
203export async function isAvailableAsync(): Promise<boolean> {
204  return await ExpoTaskManager.isAvailableAsync();
205}
206