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