1import { EventEmitter, UnavailabilityError } from '@unimodules/core';
2
3import ExpoTaskManager from './ExpoTaskManager';
4
5// @needsAudit @docsMissing
6/**
7 * Error object that can be received through [`TaskManagerTaskBody`](#taskmanagertaskbody) when the
8 * task fails.
9 */
10export interface TaskManagerError {
11  code: string | number;
12  message: string;
13}
14
15// @needsAudit
16/**
17 * Represents the object that is passed to the task executor.
18 */
19export interface TaskManagerTaskBody<T = object> {
20  /**
21   * An object of data passed to the task executor. Its properties depends on the type of the task.
22   */
23  data: T;
24
25  /**
26   * Error object if the task failed or `null` otherwise.
27   */
28  error: TaskManagerError | null;
29
30  /**
31   * Additional details containing unique ID of task event and name of the task.
32   */
33  executionInfo: TaskManagerTaskBodyExecutionInfo;
34}
35
36// @needsAudit
37/**
38 * Additional details about execution provided in `TaskManagerTaskBody`.
39 */
40export interface TaskManagerTaskBodyExecutionInfo {
41  /**
42   * __(iOS only)__ State of the application.
43   */
44  appState?: 'active' | 'background' | 'inactive';
45  /**
46   * Unique ID of task event.
47   */
48  eventId: string;
49  /**
50   * Name of the task.
51   */
52  taskName: string;
53}
54
55// @needsAudit
56/**
57 * Represents an already registered task.
58 */
59export interface TaskManagerTask {
60  /**
61   * Name that the task is registered with.
62   */
63  taskName: string;
64
65  /**
66   * Type of the task which depends on how the task was registered.
67   */
68  taskType: string;
69
70  /**
71   * Provides `options` that the task was registered with.
72   */
73  options: any;
74}
75
76/**
77 * @deprecated Use `TaskManagerTask` instead.
78 * @hidden
79 */
80export interface RegisteredTask extends TaskManagerTask {}
81
82// @needsAudit
83/**
84 * Type of task executor – a function that handles the task.
85 */
86export type TaskManagerTaskExecutor = (body: TaskManagerTaskBody) => void;
87
88const tasks: Map<string, TaskManagerTaskExecutor> = new Map<string, TaskManagerTaskExecutor>();
89
90function _validateTaskName(taskName) {
91  if (!taskName || typeof taskName !== 'string') {
92    throw new TypeError('`taskName` must be a non-empty string.');
93  }
94}
95
96// @needsAudit
97/**
98 * Defines task function. It must be called in the global scope of your JavaScript bundle.
99 * In particular, it cannot be called in any of React lifecycle methods like `componentDidMount`.
100 * This limitation is due to the fact that when the application is launched in the background,
101 * we need to spin up your JavaScript app, run your task and then shut down — no views are mounted
102 * in this scenario.
103 *
104 * @param taskName Name of the task. It must be the same as the name you provided when registering the task.
105 * @param taskExecutor A function that will be invoked when the task with given `taskName` is executed.
106 */
107export function defineTask(taskName: string, taskExecutor: TaskManagerTaskExecutor) {
108  if (!taskName || typeof taskName !== 'string') {
109    console.warn(`TaskManager.defineTask: 'taskName' argument must be a non-empty string.`);
110    return;
111  }
112  if (!taskExecutor || typeof taskExecutor !== 'function') {
113    console.warn(`TaskManager.defineTask: 'task' argument must be a function.`);
114    return;
115  }
116  tasks.set(taskName, taskExecutor);
117}
118
119// @needsAudit
120/**
121 * Checks whether the task is already defined.
122 *
123 * @param taskName Name of the task.
124 */
125export function isTaskDefined(taskName: string): boolean {
126  return tasks.has(taskName);
127}
128
129// @needsAudit
130/**
131 * Determine whether the task is registered. Registered tasks are stored in a persistent storage and
132 * preserved between sessions.
133 *
134 * @param taskName Name of the task.
135 * @returns A promise which fulfills with a `boolean` value whether or not the task with given name
136 * is already registered.
137 */
138export async function isTaskRegisteredAsync(taskName: string): Promise<boolean> {
139  if (!ExpoTaskManager.isTaskRegisteredAsync) {
140    throw new UnavailabilityError('TaskManager', 'isTaskRegisteredAsync');
141  }
142
143  _validateTaskName(taskName);
144  return ExpoTaskManager.isTaskRegisteredAsync(taskName);
145}
146
147// @needsAudit
148/**
149 * Retrieves `options` associated with the task, that were passed to the function registering the task
150 * (eg. `Location.startLocationUpdatesAsync`).
151 *
152 * @param taskName Name of the task.
153 * @return A promise which fulfills with the `options` object that was passed while registering task
154 * with given name or `null` if task couldn't be found.
155 */
156export async function getTaskOptionsAsync<TaskOptions>(taskName: string): Promise<TaskOptions> {
157  if (!ExpoTaskManager.getTaskOptionsAsync) {
158    throw new UnavailabilityError('TaskManager', 'getTaskOptionsAsync');
159  }
160
161  _validateTaskName(taskName);
162  return ExpoTaskManager.getTaskOptionsAsync(taskName);
163}
164
165// @needsAudit
166/**
167 * Provides information about tasks registered in the app.
168 *
169 * @returns A promise which fulfills with an array of tasks registered in the app. Example:
170 * ```json
171 * [
172 *   {
173 *     taskName: 'location-updates-task-name',
174 *     taskType: 'location',
175 *     options: {
176 *       accuracy: Location.Accuracy.High,
177 *       showsBackgroundLocationIndicator: false,
178 *     },
179 *   },
180 *   {
181 *     taskName: 'geofencing-task-name',
182 *     taskType: 'geofencing',
183 *     options: {
184 *       regions: [...],
185 *     },
186 *   },
187 * ]
188 * ```
189 */
190export async function getRegisteredTasksAsync(): Promise<TaskManagerTask[]> {
191  if (!ExpoTaskManager.getRegisteredTasksAsync) {
192    throw new UnavailabilityError('TaskManager', 'getRegisteredTasksAsync');
193  }
194
195  return ExpoTaskManager.getRegisteredTasksAsync();
196}
197
198// @needsAudit
199/**
200 * Unregisters task from the app, so the app will not be receiving updates for that task anymore.
201 * _It is recommended to use methods specialized by modules that registered the task, eg.
202 * [`Location.stopLocationUpdatesAsync`](./location/#expolocationstoplocationupdatesasynctaskname)._
203 *
204 * @param taskName Name of the task to unregister.
205 * @return A promise which fulfills as soon as the task is unregistered.
206 */
207export async function unregisterTaskAsync(taskName: string): Promise<void> {
208  if (!ExpoTaskManager.unregisterTaskAsync) {
209    throw new UnavailabilityError('TaskManager', 'unregisterTaskAsync');
210  }
211
212  _validateTaskName(taskName);
213  await ExpoTaskManager.unregisterTaskAsync(taskName);
214}
215
216// @needsAudit
217/**
218 * Unregisters all tasks registered for the running app. You may want to call this when the user is
219 * signing out and you no longer need to track his location or run any other background tasks.
220 * @return A promise which fulfills as soon as all tasks are completely unregistered.
221 */
222export async function unregisterAllTasksAsync(): Promise<void> {
223  if (!ExpoTaskManager.unregisterAllTasksAsync) {
224    throw new UnavailabilityError('TaskManager', 'unregisterAllTasksAsync');
225  }
226
227  await ExpoTaskManager.unregisterAllTasksAsync();
228}
229
230if (ExpoTaskManager) {
231  const eventEmitter = new EventEmitter(ExpoTaskManager);
232  eventEmitter.addListener<TaskManagerTaskBody>(
233    ExpoTaskManager.EVENT_NAME,
234    async ({ data, error, executionInfo }) => {
235      const { eventId, taskName } = executionInfo;
236      const taskExecutor = tasks.get(taskName);
237      let result: any = null;
238
239      if (taskExecutor) {
240        try {
241          // Execute JS task
242          result = await taskExecutor({ data, error, executionInfo });
243        } catch (error) {
244          console.error(`TaskManager: Task "${taskName}" failed:`, error);
245        } finally {
246          // Notify manager the task is finished.
247          await ExpoTaskManager.notifyTaskFinishedAsync(taskName, { eventId, result });
248        }
249      } else {
250        console.warn(
251          `TaskManager: Task "${taskName}" has been executed but looks like it is not defined. Please make sure that "TaskManager.defineTask" is called during initialization phase.`
252        );
253        // No tasks defined -> we need to notify about finish anyway.
254        await ExpoTaskManager.notifyTaskFinishedAsync(taskName, { eventId, result });
255        // We should also unregister such tasks automatically as the task might have been removed
256        // from the app or just renamed - in that case it needs to be registered again (with the new name).
257        await ExpoTaskManager.unregisterTaskAsync(taskName);
258      }
259    }
260  );
261}
262
263// @needsAudit
264/**
265 * Determine if the `TaskManager` API can be used in this app.
266 * @return A promise fulfills with `true` if the API can be used, and `false` otherwise.
267 * On the web it always returns `false`.
268 */
269export async function isAvailableAsync(): Promise<boolean> {
270  return await ExpoTaskManager.isAvailableAsync();
271}
272