UNPKG

expo-task-manager

Version:

Expo module that provides support for tasks that can run in the background.

299 lines (269 loc) 9.5 kB
import { isRunningInExpoGo } from 'expo'; import { LegacyEventEmitter, UnavailabilityError } from 'expo-modules-core'; import { Platform } from 'react-native'; import ExpoTaskManager from './ExpoTaskManager'; // @needsAudit @docsMissing /** * Error object that can be received through [`TaskManagerTaskBody`](#taskmanagertaskbody) when the * task fails. */ export interface TaskManagerError { code: string | number; message: string; } // @needsAudit /** * Represents the object that is passed to the task executor. */ export interface TaskManagerTaskBody<T = unknown> { /** * An object of data passed to the task executor. Its properties depend on the type of the task. */ data: T; /** * Error object if the task failed or `null` otherwise. */ error: TaskManagerError | null; /** * Additional details containing unique ID of task event and name of the task. */ executionInfo: TaskManagerTaskBodyExecutionInfo; } // @needsAudit /** * Additional details about execution provided in `TaskManagerTaskBody`. */ export interface TaskManagerTaskBodyExecutionInfo { /** * State of the application. * @platform ios */ appState?: 'active' | 'background' | 'inactive'; /** * Unique ID of task event. */ eventId: string; /** * Name of the task. */ taskName: string; } // @needsAudit /** * Represents an already registered task. */ export interface TaskManagerTask { /** * Name that the task is registered with. */ taskName: string; /** * Type of the task which depends on how the task was registered. */ taskType: string; /** * Provides `options` that the task was registered with. */ options: any; } /** * @deprecated Use `TaskManagerTask` instead. * @hidden */ export interface RegisteredTask extends TaskManagerTask {} // @needsAudit /** * Type of task executor – a function that handles the task. */ export type TaskManagerTaskExecutor<T = any> = (body: TaskManagerTaskBody<T>) => Promise<any>; const tasks: Map<string, TaskManagerTaskExecutor<any>> = new Map< string, TaskManagerTaskExecutor<any> >(); let warnedAboutExpoGo = false; function _validate(taskName: unknown) { if (isRunningInExpoGo()) { if (!warnedAboutExpoGo) { const message = '`TaskManager` functionality is limited in Expo Go:\n' + 'On Android, it is not available at all.\n' + 'On iOS, it is limited to foreground execution.\n' + 'Please use a development build to avoid limitations. Learn more: https://expo.fyi/dev-client.'; console.warn(message); warnedAboutExpoGo = true; } } if (!taskName || typeof taskName !== 'string') { throw new TypeError('`taskName` must be a non-empty string.'); } } // @needsAudit /** * Defines task function. It must be called in the global scope of your JavaScript bundle. * In particular, it cannot be called in any of React lifecycle methods like `componentDidMount`. * This limitation is due to the fact that when the application is launched in the background, * we need to spin up your JavaScript app, run your task and then shut down — no views are mounted * in this scenario. * * @param taskName Name of the task. It must be the same as the name you provided when registering the task. * @param taskExecutor A function that will be invoked when the task with given `taskName` is executed. */ export function defineTask<T = unknown>( taskName: string, taskExecutor: TaskManagerTaskExecutor<T> ) { if (!taskName || typeof taskName !== 'string') { console.warn(`TaskManager.defineTask: 'taskName' argument must be a non-empty string.`); return; } if (!taskExecutor || typeof taskExecutor !== 'function') { console.warn(`TaskManager.defineTask: 'task' argument must be a function.`); return; } tasks.set(taskName, taskExecutor); } // @needsAudit /** * Checks whether the task is already defined. * * @param taskName Name of the task. */ export function isTaskDefined(taskName: string): boolean { return tasks.has(taskName); } // @needsAudit /** * Determine whether the task is registered. Registered tasks are stored in a persistent storage and * preserved between sessions. * * @param taskName Name of the task. * @returns A promise which resolves to `true` if a task with the given name is registered, otherwise `false`. */ export async function isTaskRegisteredAsync(taskName: string): Promise<boolean> { if (!ExpoTaskManager.isTaskRegisteredAsync) { throw new UnavailabilityError('TaskManager', 'isTaskRegisteredAsync'); } _validate(taskName); return ExpoTaskManager.isTaskRegisteredAsync(taskName); } // @needsAudit /** * Retrieves `options` associated with the task, that were passed to the function registering the task * (e.g. `Location.startLocationUpdatesAsync`). * * @param taskName Name of the task. * @return A promise which fulfills with the `options` object that was passed while registering task * with given name or `null` if task couldn't be found. */ export async function getTaskOptionsAsync<TaskOptions>(taskName: string): Promise<TaskOptions> { if (!ExpoTaskManager.getTaskOptionsAsync) { throw new UnavailabilityError('TaskManager', 'getTaskOptionsAsync'); } _validate(taskName); return ExpoTaskManager.getTaskOptionsAsync(taskName); } // @needsAudit /** * Provides information about tasks registered in the app. * * @returns A promise which fulfills with an array of tasks registered in the app. * @example * ```js * [ * { * taskName: 'location-updates-task-name', * taskType: 'location', * options: { * accuracy: Location.Accuracy.High, * showsBackgroundLocationIndicator: false, * }, * }, * { * taskName: 'geofencing-task-name', * taskType: 'geofencing', * options: { * regions: [...], * }, * }, * ] * ``` */ export async function getRegisteredTasksAsync(): Promise<TaskManagerTask[]> { if (!ExpoTaskManager.getRegisteredTasksAsync) { throw new UnavailabilityError('TaskManager', 'getRegisteredTasksAsync'); } return ExpoTaskManager.getRegisteredTasksAsync(); } // @needsAudit /** * Unregisters task from the app, so the app will not be receiving updates for that task anymore. * _It is recommended to use methods specialized by modules that registered the task, eg. * [`Location.stopLocationUpdatesAsync`](./location/#expolocationstoplocationupdatesasynctaskname)._ * * @param taskName Name of the task to unregister. * @return A promise which fulfills as soon as the task is unregistered. */ export async function unregisterTaskAsync(taskName: string): Promise<void> { if (!ExpoTaskManager.unregisterTaskAsync) { throw new UnavailabilityError('TaskManager', 'unregisterTaskAsync'); } _validate(taskName); await ExpoTaskManager.unregisterTaskAsync(taskName); } // @needsAudit /** * Unregisters all tasks registered for the running app. You may want to call this when the user is * signing out and you no longer need to track his location or run any other background tasks. * @return A promise which fulfills as soon as all tasks are completely unregistered. */ export async function unregisterAllTasksAsync(): Promise<void> { if (!ExpoTaskManager.unregisterAllTasksAsync) { throw new UnavailabilityError('TaskManager', 'unregisterAllTasksAsync'); } await ExpoTaskManager.unregisterAllTasksAsync(); } if (ExpoTaskManager) { const eventEmitter = new LegacyEventEmitter(ExpoTaskManager); eventEmitter.addListener<TaskManagerTaskBody>( ExpoTaskManager.EVENT_NAME, async ({ data, error, executionInfo }) => { const { eventId, taskName } = executionInfo; const taskExecutor = tasks.get(taskName); let result: any = null; if (taskExecutor) { try { // Execute JS task result = await taskExecutor({ data, error, executionInfo }); } catch (error) { console.error(`TaskManager: Task "${taskName}" failed:`, error); } finally { // Notify manager the task is finished. await ExpoTaskManager.notifyTaskFinishedAsync(taskName, { eventId, result }); } } else { console.warn( `TaskManager: Task "${taskName}" has been executed but looks like it is not defined. Please make sure that "TaskManager.defineTask" is called during initialization phase.` ); // No tasks defined -> we need to notify about finish anyway. await ExpoTaskManager.notifyTaskFinishedAsync(taskName, { eventId, result }); // We should also unregister such tasks automatically as the task might have been removed // from the app or just renamed - in that case it needs to be registered again (with the new name). await ExpoTaskManager.unregisterTaskAsync(taskName); } } ); } // @needsAudit /** * Determine if the `TaskManager` API can be used in this app. * @return A promise which fulfills with `true` if the API can be used, and `false` otherwise. * With Expo Go, `TaskManager` is not available on Android, and does not support background execution on iOS. * Use a development build to avoid limitations: https://expo.fyi/dev-client. * On the web, it always returns `false`. */ export async function isAvailableAsync(): Promise<boolean> { if (Platform.OS === 'android') { return !isRunningInExpoGo() && ExpoTaskManager.isAvailableAsync(); } return ExpoTaskManager.isAvailableAsync(); }