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