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