# Task manager

> Define the code the OS runs when it wakes your app in the background, on every SymbioteNative adapter.

Run your code when the OS wakes the app in the background: a location update, a geofence event, a
periodic sync, a silent push. `@symbiote-native/task-manager` wraps
[`expo-task-manager`](https://github.com/expo/expo/tree/main/packages/expo-task-manager) so every
SymbioteNative adapter can define and inspect those tasks. It does not itself schedule anything: it
defines tasks, tracks which are registered, dispatches native's task-execute event to the matching
executor, and acks completion. Starting a task running (periodic scheduling, geofence triggers,
push delivery) is each consumer package's own job:
[`@symbiote-native/location`](/docs/packages/location/),
[`@symbiote-native/background-fetch`](/docs/packages/background-fetch/),
[`@symbiote-native/background-task`](/docs/packages/background-task/), and
[`@symbiote-native/notifications`](/docs/packages/notifications/) all register through it.

Every export is a plain function or a one-time module-load side effect — no hook/composable/
service to wrap, so every adapter's entry point is a plain re-export of the same core.

| OS platform | Support |
| ----------- | ------- |
| iOS         | ✅ live |
| Android     | ✅ live |

| Framework adapter | Support |
| ------------------ | ------- |
| React              | ✅ live |
| Vue                | ✅ live |
| Angular            | ✅ live |
| Svelte             | ✅ live |
| Solid              | ✅ live |

## Installation

```sh
npm install @symbiote-native/task-manager
```

Scaffolding or extending a SymbioteNative app? `npx @symbiote-native/cli new --task-manager` (or
`add --task-manager` in an existing app) installs and wires this for you — see
[`@symbiote-native/cli`](https://github.com/OneEyed1366/symbiote-native/tree/master/packages/cli).

`expo-task-manager`, `unimodules-app-loader`, and `expo-modules-core` come along as regular
dependencies, pinned to exact versions — never install them yourself, and never add the `expo`
meta-package to your project.

<Aside type="danger" title="Native setup is required before first use">
  Follow [How to: wire up an Expo native
  module](/docs/howtos/expo-native-module-setup/) once per app first; it
  covers this package and every future `expo-modules-core` package with zero
  further native changes. Android's headless-boot loader
  (`RNHeadlessAppLoader`) and iOS's `TaskManagerAppDelegateSubscriber` are
  both discovered automatically by autolinking the moment this package is
  installed. One iOS `Info.plist` key is wired automatically:
  `UIBackgroundModes: fetch` — the baseline mode any registered task's
  background delivery relies on, independent of which consumer package
  actually registers a task.
</Aside>

## Usage

`defineTask` must run at the top of the JS bundle, outside any component — the app can be
launched headlessly to run a background task, with no views mounted, so a task defined inside a
component lifecycle method would never register on that launch.

```ts
// index.ts, alongside AppRegistry.registerComponent — identical on every adapter
import { defineTask } from '@symbiote-native/task-manager';

const SYNC_TASK = 'background-sync';

defineTask(SYNC_TASK, async ({ data, error }) => {
  if (error) {
    console.error('background-sync failed:', error);
    return;
  }
  await runSync(data);
});
```

A consumer package registers the task with native through its own native module —
[`location`](/docs/packages/location/)'s `startLocationUpdatesAsync`/`startGeofencingAsync`,
[`background-fetch`](/docs/packages/background-fetch/)'s `registerTaskAsync`,
[`background-task`](/docs/packages/background-task/)'s `registerTaskAsync`, or
[`notifications`](/docs/packages/notifications/)'s `registerTaskAsync`. Once registered, inspect
it from here:

```ts
import {
  getRegisteredTasksAsync,
  isTaskRegisteredAsync,
  unregisterTaskAsync,
} from '@symbiote-native/task-manager';

const isRunning = await isTaskRegisteredAsync(SYNC_TASK);
const tasks = await getRegisteredTasksAsync();
await unregisterTaskAsync(SYNC_TASK); // stop receiving updates for this task
```

Identical import surface on every adapter — `@symbiote-native/task-manager/react`, `/vue`,
`/svelte`, `/solid`, `/angular` all re-export the same functions.

## API

### Functions

| Signature                                                | Description                                                                          |
| -------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| `defineTask(taskName, taskExecutor): void`               | Defines the code that runs for a task. Call it at module scope                       |
| `isTaskDefined(taskName): boolean`                       | Whether a task with this name is defined in the current bundle                       |
| `isTaskRegisteredAsync(taskName): Promise<boolean>`      | Whether a consumer has registered the task with native                               |
| `getTaskOptionsAsync(taskName): Promise<TOptions>`       | The options the task was registered with                                             |
| `getRegisteredTasksAsync(): Promise<ITaskManagerTask[]>` | Every registered task, with its name, type and options                               |
| `unregisterTaskAsync(taskName): Promise<void>`           | Stops delivery for one task                                                          |
| `unregisterAllTasksAsync(): Promise<void>`               | Stops delivery for every task                                                        |
| `isAvailableAsync(): Promise<boolean>`                   | Whether the task manager can be used at all                                          |

### What the executor receives

`taskExecutor` is called with an `ITaskManagerTaskBody`:

| Field                    | Type                          | Description                                                                 |
| ------------------------ | ----------------------------- | --------------------------------------------------------------------------- |
| `data`                   | `TData`                       | The payload. Its shape depends on the consumer (locations, a geofence event) |
| `error`                  | `ITaskManagerError \| null`   | The failure (`code`, `message`) if the task failed, otherwise `null`        |
| `executionInfo.taskName` | `string`                      | Name of the task that ran                                                   |
| `executionInfo.eventId`  | `string`                      | Unique ID of this task event                                                |
| `executionInfo.appState` | `'active' \| 'background' \| 'inactive' \| undefined` | App state when the task ran. iOS only             |

There is no free `registerTaskAsync` here — registration is always driven by the consumer package
that needs the task, never by task-manager itself.

## Notes

<Aside type="tip" title="isAvailableAsync resolves false rather than throwing">
  Every other guarded function throws an `UnavailabilityError` when the
  native method is missing. `isAvailableAsync` resolves `false` instead — "can
  this API be used at all" has to answer even where the rest of the surface
  can't.
</Aside>

- **A defined task that native fires but nobody registered still gets acked and unregistered.**
  If `defineTask` for a given name was renamed or deleted from the bundle after the task was
  registered, the dangling native registration is cleaned up automatically instead of leaking a
  wakelock forever.
- **A task executor that throws still acks native.** The failure is logged, but the completion ack
  always fires, so a bug in one task's executor can't leave the OS believing the task never
  completed.

## Common questions

**My task never runs.** First check `defineTask` runs at the top of the bundle, outside any
component, before anything registers the task. Registering a task that is not defined throws. Then
check that a consumer package actually registered it: `defineTask` alone schedules nothing.

**It stops when I swipe the app away.** Background work is stopped when the user kills the app,
though the platforms differ. On iOS swiping the app out of the switcher terminates it. On Android,
removing it from recents does not always stop it, and the result varies by device vendor.

**It works for 5 to 10 minutes on Android, then stops.** Doze mode puts the device to sleep. For
continuous tracking, use a foreground service (for example the `foregroundService` option of
location updates) rather than relying on a periodic wake-up.

**It works in a debug build but not in release.** Registration or delivery can differ in release
builds on Android. Test the release build early, and watch for a task that was renamed or removed:
a dangling registration is cleaned up the next time native fires it.

**Does it run on a simulator?** Background execution is only reliable on physical devices, and the
iOS Simulator does not support the iOS background task scheduler.

**How do I debug a task?** Log at the top of the executor and inside the `error` branch, and call
`getRegisteredTasksAsync()` to see what native has registered.

Sources: [Expo docs: BackgroundTask](https://docs.expo.dev/versions/latest/sdk/background-task/),
[Expo docs: BackgroundFetch](https://docs.expo.dev/versions/latest/sdk/background-fetch/),
[expo/expo#9570 Expo keeps stopping when the app is killed during background location](https://github.com/expo/expo/issues/9570),
[expo/expo#3535 background geolocation task disappears when the app is killed on Android](https://github.com/expo/expo/issues/3535),
[expo/expo#14076 task does not fire after 5 to 10 minutes on Android](https://github.com/expo/expo/issues/14076),
[expo/expo#26717 task is not defined, define it before registering](https://github.com/expo/expo/issues/26717).
