@symbiote-native/task-manager 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 A. Prokopenko
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,164 @@
1
+ # @symbiote-native/task-manager
2
+
3
+ A wrapper package for [SymbioteNative](../../README.md) that makes
4
+ [`expo-task-manager`](https://github.com/expo/expo/tree/main/packages/expo-task-manager)
5
+ usable from **every** adapter — React, Vue, Svelte, Solid, and Angular. Like
6
+ [`@symbiote-native/local-auth`](../local-auth), every export is a plain function or a one-time
7
+ module-load side effect, so there is no hook/composable/service to wrap: the React, Vue, Svelte,
8
+ Solid, and Angular entry points are plain re-exports of the same `core`.
9
+
10
+ This package is the low-level primitive other background-work packages register tasks through
11
+ (background location, geofencing, background notification delivery) — it does **not** itself
12
+ schedule anything. It defines tasks, tracks which are registered, dispatches native's
13
+ task-execute event to the matching executor, and acks completion. Starting a task running (e.g.
14
+ periodic scheduling, geofence triggers) is each consumer's own job, done through its own native
15
+ module.
16
+
17
+ ## Install
18
+
19
+ **New app:**
20
+
21
+ ```bash
22
+ npx @symbiote-native/cli new my-app --task-manager
23
+ ```
24
+
25
+ **Existing SymbioteNative app:**
26
+
27
+ ```bash
28
+ npx @symbiote-native/cli add --task-manager
29
+ ```
30
+
31
+ Either way: installs `@symbiote-native/task-manager` and wires the native autolinking
32
+ automatically — see [`@symbiote-native/cli`](../cli). Most apps get this transitively anyway, pulled in by
33
+ `--location`/`--audio`/`--background-fetch`/`--background-task`/`--notifications`.
34
+
35
+ <details>
36
+ <summary>Manual install (no CLI — installing and wiring native autolinking by hand)</summary>
37
+
38
+ ```bash
39
+ npm install @symbiote-native/task-manager
40
+ ```
41
+
42
+ `expo-task-manager`, `unimodules-app-loader`, and `expo-modules-core` come along as regular
43
+ dependencies, pinned to exact versions — never install them yourself, and never add the `expo`
44
+ meta-package to your project.
45
+
46
+ ## Required one-time step: native autolinking wiring
47
+
48
+ Same one-time step as every other `expo-modules-core` package this project ships — see
49
+ [`@symbiote-native/local-auth`'s README](../local-auth/README.md#required-one-time-step-native-autolinking-wiring)
50
+ and the `symbiote-expo-native-module` project skill. Android's headless-boot loader
51
+ (`RNHeadlessAppLoader`) and iOS's `TaskManagerAppDelegateSubscriber` are both discovered
52
+ automatically by Expo's own autolinking the moment this package is installed — no extra
53
+ `native-link.json` entry, no app-level edit.
54
+
55
+ One iOS `Info.plist` key IS wired: `UIBackgroundModes: fetch`
56
+ (`native-link.json`'s `ios.infoPlistArrayKeys`) — upstream's own `withTaskManager.ts` config
57
+ plugin adds this unconditionally, as the baseline mode any registered task's background delivery
58
+ relies on, independent of which consumer package (`background-fetch`, `location`, …) actually
59
+ registers a task. `@symbiote-native/expo-modules-link` merges it into the same array those
60
+ packages populate, so it lands once either way.
61
+
62
+ </details>
63
+
64
+ ## Shape
65
+
66
+ ```
67
+ src/core/ defineTask / isTaskDefined / isTaskRegisteredAsync / getTaskOptionsAsync /
68
+ getRegisteredTasksAsync / unregisterTaskAsync / unregisterAllTasksAsync /
69
+ isAvailableAsync, plus the task-body/executor types. native-module.ts resolves the
70
+ native module via expo-modules-core's requireNativeModule and wires one listener at
71
+ module load — native invokes it whenever a defined task should run.
72
+ src/angular/ @symbiote-native/task-manager/angular — export * from '../core'
73
+ ```
74
+
75
+ `./react`, `./vue`, `./svelte`, and `./solid` are `exports`-map aliases straight onto
76
+ `src/core/`. `./angular` stays a physical file/subpath since Angular ships through a separate
77
+ `ngc`/AOT build (`build-ngc/`).
78
+
79
+ ## Use it
80
+
81
+ `defineTask` must run at the top of the JS bundle, outside any component — the app can be
82
+ launched headlessly to run a background task, with no views mounted, so a task defined inside a
83
+ component lifecycle method would simply never register on that launch.
84
+
85
+ ```ts
86
+ // index.ts, alongside AppRegistry.registerComponent — identical on every adapter
87
+ import { defineTask } from '@symbiote-native/task-manager';
88
+
89
+ const SYNC_TASK = 'background-sync';
90
+
91
+ defineTask(SYNC_TASK, async ({ data, error }) => {
92
+ if (error) {
93
+ console.error('background-sync failed:', error);
94
+ return;
95
+ }
96
+ await runSync(data);
97
+ });
98
+ ```
99
+
100
+ A consumer registers the task with native through its own native module —
101
+ [`@symbiote-native/location`](../location)'s `startLocationUpdatesAsync`/`startGeofencingAsync`,
102
+ [`@symbiote-native/background-fetch`](../background-fetch)'s `registerTaskAsync`,
103
+ [`@symbiote-native/background-task`](../background-task)'s `registerTaskAsync`, or
104
+ [`@symbiote-native/notifications`](../notifications)'s `registerTaskAsync`. Once registered,
105
+ `isTaskRegisteredAsync`/`getTaskOptionsAsync`/`getRegisteredTasksAsync` reflect it:
106
+
107
+ ```ts
108
+ import {
109
+ getRegisteredTasksAsync,
110
+ isTaskRegisteredAsync,
111
+ unregisterTaskAsync,
112
+ } from '@symbiote-native/task-manager';
113
+
114
+ const isRunning = await isTaskRegisteredAsync(SYNC_TASK);
115
+ const tasks = await getRegisteredTasksAsync();
116
+ await unregisterTaskAsync(SYNC_TASK); // stop receiving updates for this task
117
+ ```
118
+
119
+ Identical import surface on every adapter — `@symbiote-native/task-manager/react`,
120
+ `/vue`, `/svelte`, `/solid`, `/angular` all re-export the same functions.
121
+
122
+ ## API
123
+
124
+ ```ts
125
+ defineTask<TData>(taskName: string, taskExecutor: ITaskManagerTaskExecutor<TData>): void
126
+ isTaskDefined(taskName: string): boolean
127
+ isTaskRegisteredAsync(taskName: string): Promise<boolean>
128
+ getTaskOptionsAsync<TOptions>(taskName: string): Promise<TOptions>
129
+ getRegisteredTasksAsync<TOptions>(): Promise<ITaskManagerTask<TOptions>[]>
130
+ unregisterTaskAsync(taskName: string): Promise<void>
131
+ unregisterAllTasksAsync(): Promise<void>
132
+ isAvailableAsync(): Promise<boolean>
133
+ ```
134
+
135
+ Plus `ITaskManagerError`, `ITaskManagerTask`, `ITaskManagerTaskBody`,
136
+ `ITaskManagerTaskBodyExecutionInfo`, `ITaskManagerTaskExecutor` — ported from upstream's
137
+ `TaskManager.ts`, renamed with this repo's `I`-prefix convention for exported types
138
+ (`ts-js-best-practices`). Unlike upstream, there is no `registerTaskAsync` free function here —
139
+ upstream never exposes one either; registration is always driven by the consumer that needs the
140
+ task (`@symbiote-native/location`, `@symbiote-native/background-fetch`,
141
+ `@symbiote-native/background-task`, `@symbiote-native/notifications`).
142
+
143
+ ## Notes
144
+
145
+ - **`isAvailableAsync` resolves `false` rather than throwing when the native method is missing** —
146
+ every other guarded function throws an `UnavailabilityError`. This matches upstream's own
147
+ contract: "can this API be used at all" has to answer even where the rest of the surface can't.
148
+ - **A defined task that native fires but nobody registered still gets acked and unregistered.**
149
+ If `defineTask` for a given name was renamed or deleted from the bundle after the task was
150
+ registered, the dangling native registration is cleaned up automatically instead of leaking a
151
+ wakelock forever.
152
+ - **A task executor that throws still acks native.** The failure is logged, but
153
+ `notifyTaskFinishedAsync` always fires in a `finally`, so a bug in one task's executor can't
154
+ leave the OS believing the task never completed.
155
+
156
+ ## Test it
157
+
158
+ No Fabric/Descriptor angle at all — every function here is a pure async-function surface plus one
159
+ module-load event listener, never a view or per-instance state. Tests inject a fake native-module
160
+ object in place of the real `requireNativeModule` resolution and fire the wired listener directly
161
+ (`src/core/task-manager.test.ts`) — no `installFabric()`, no ViewConfig. The headless-relaunch
162
+ mechanics themselves (native tearing down and re-executing the whole JS bundle with no UI mounted)
163
+ are OS-level and can only be verified on a real device — see the `symbiote-expo-native-module`
164
+ skill §10e for what's traced from source versus still unverified.
@@ -0,0 +1 @@
1
+ export * from '../core';
@@ -0,0 +1,4 @@
1
+ // @symbiote-native/task-manager/angular: the Angular entry over the framework-agnostic core. Same
2
+ // reasoning as the React/Vue entries — no per-instance state or event stream to wrap in a
3
+ // service, so this is a plain re-export.
4
+ export * from '../core/index.js';
@@ -0,0 +1,2 @@
1
+ export { defineTask, isTaskDefined, isTaskRegisteredAsync, getTaskOptionsAsync, getRegisteredTasksAsync, unregisterTaskAsync, unregisterAllTasksAsync, isAvailableAsync, } from './task-manager';
2
+ export type { ITaskManagerError, ITaskManagerTask, ITaskManagerTaskBody, ITaskManagerTaskBodyExecutionInfo, ITaskManagerTaskExecutor, } from './types';
@@ -0,0 +1 @@
1
+ export { defineTask, isTaskDefined, isTaskRegisteredAsync, getTaskOptionsAsync, getRegisteredTasksAsync, unregisterTaskAsync, unregisterAllTasksAsync, isAvailableAsync, } from './task-manager.js';
@@ -0,0 +1,22 @@
1
+ import { type EventSubscription } from 'expo-modules-core';
2
+ import type { ITaskManagerTaskBody } from './types';
3
+ export type INativeTaskManagerModule = {
4
+ /** Name of the event native emits when a defined task should run. */
5
+ EVENT_NAME: string;
6
+ addListener(eventName: string, listener: (event: ITaskManagerTaskBody) => void): EventSubscription;
7
+ isAvailableAsync?(): Promise<boolean>;
8
+ isTaskRegisteredAsync?(taskName: string): Promise<boolean>;
9
+ getTaskOptionsAsync?<TOptions>(taskName: string): Promise<TOptions>;
10
+ getRegisteredTasksAsync?(): Promise<{
11
+ taskName: string;
12
+ taskType: string;
13
+ options: unknown;
14
+ }[]>;
15
+ unregisterTaskAsync?(taskName: string): Promise<void>;
16
+ unregisterAllTasksAsync?(): Promise<void>;
17
+ notifyTaskFinishedAsync?(taskName: string, payload: {
18
+ eventId: string;
19
+ result: unknown;
20
+ }): Promise<void>;
21
+ };
22
+ export declare const expoTaskManager: INativeTaskManagerModule;
@@ -0,0 +1,3 @@
1
+ import { requireNativeModule } from 'expo-modules-core';
2
+ const EXPO_TASK_MANAGER_MODULE_NAME = 'ExpoTaskManager';
3
+ export const expoTaskManager = requireNativeModule(EXPO_TASK_MANAGER_MODULE_NAME);
@@ -0,0 +1,28 @@
1
+ import type { ITaskManagerTask, ITaskManagerTaskExecutor } from './types';
2
+ /**
3
+ * Defines a task executor. Must be called in the global scope of the JS bundle — never inside a
4
+ * component lifecycle method — because when the app is launched in the background there are no
5
+ * views mounted, only this module-scope registration for native to find.
6
+ */
7
+ export declare function defineTask<TData = unknown>(taskName: string, taskExecutor: ITaskManagerTaskExecutor<TData>): void;
8
+ /** Whether a task with the given name has been defined via `defineTask`. */
9
+ export declare function isTaskDefined(taskName: string): boolean;
10
+ /**
11
+ * Whether the task is registered with native. Registered tasks persist across sessions; a task
12
+ * can be defined without being registered (registration is driven by the task's own consumer,
13
+ * e.g. `Location.startLocationUpdatesAsync`).
14
+ */
15
+ export declare function isTaskRegisteredAsync(taskName: string): Promise<boolean>;
16
+ /** The `options` a task was registered with, or rejects if the task can't be found. */
17
+ export declare function getTaskOptionsAsync<TOptions>(taskName: string): Promise<TOptions>;
18
+ /** Every task registered in the app, with the `options` each was registered with. */
19
+ export declare function getRegisteredTasksAsync<TOptions = unknown>(): Promise<ITaskManagerTask<TOptions>[]>;
20
+ /**
21
+ * Unregisters a task so the app stops receiving updates for it. Prefer the specialized method of
22
+ * whichever module registered the task (e.g. `Location.stopLocationUpdatesAsync`) when one exists.
23
+ */
24
+ export declare function unregisterTaskAsync(taskName: string): Promise<void>;
25
+ /** Unregisters every task registered for the running app. */
26
+ export declare function unregisterAllTasksAsync(): Promise<void>;
27
+ /** Whether the TaskManager API can be used in this app. Always `false` outside a device build. */
28
+ export declare function isAvailableAsync(): Promise<boolean>;
@@ -0,0 +1,112 @@
1
+ import { UnavailabilityError } from 'expo-modules-core';
2
+ import { expoTaskManager } from './native-module.js';
3
+ const NATIVE_MODULE_NAME = 'expo-task-manager';
4
+ // The registry is necessarily heterogeneous — each task's own TData differs — so the executor's
5
+ // data parameter is typed `any` here only, exactly like upstream's own
6
+ // `Map<string, TaskManagerTaskExecutor<any>>`. `defineTask`'s own signature stays fully typed;
7
+ // this is the one place bridging distinct `ITaskManagerTaskExecutor<TData>` instances into one
8
+ // registry, which a cast could not do more soundly than `any` already does.
9
+ const tasks = new Map();
10
+ function assertValidTaskName(taskName) {
11
+ if (!taskName || typeof taskName !== 'string') {
12
+ throw new TypeError('`taskName` must be a non-empty string.');
13
+ }
14
+ }
15
+ /**
16
+ * Defines a task executor. Must be called in the global scope of the JS bundle — never inside a
17
+ * component lifecycle method — because when the app is launched in the background there are no
18
+ * views mounted, only this module-scope registration for native to find.
19
+ */
20
+ export function defineTask(taskName, taskExecutor) {
21
+ if (!taskName || typeof taskName !== 'string') {
22
+ console.warn("TaskManager.defineTask: 'taskName' argument must be a non-empty string.");
23
+ return;
24
+ }
25
+ if (!taskExecutor || typeof taskExecutor !== 'function') {
26
+ console.warn("TaskManager.defineTask: 'task' argument must be a function.");
27
+ return;
28
+ }
29
+ tasks.set(taskName, taskExecutor);
30
+ }
31
+ /** Whether a task with the given name has been defined via `defineTask`. */
32
+ export function isTaskDefined(taskName) {
33
+ return tasks.has(taskName);
34
+ }
35
+ /**
36
+ * Whether the task is registered with native. Registered tasks persist across sessions; a task
37
+ * can be defined without being registered (registration is driven by the task's own consumer,
38
+ * e.g. `Location.startLocationUpdatesAsync`).
39
+ */
40
+ export async function isTaskRegisteredAsync(taskName) {
41
+ if (!expoTaskManager.isTaskRegisteredAsync) {
42
+ throw new UnavailabilityError(NATIVE_MODULE_NAME, 'isTaskRegisteredAsync');
43
+ }
44
+ assertValidTaskName(taskName);
45
+ return expoTaskManager.isTaskRegisteredAsync(taskName);
46
+ }
47
+ /** The `options` a task was registered with, or rejects if the task can't be found. */
48
+ export async function getTaskOptionsAsync(taskName) {
49
+ if (!expoTaskManager.getTaskOptionsAsync) {
50
+ throw new UnavailabilityError(NATIVE_MODULE_NAME, 'getTaskOptionsAsync');
51
+ }
52
+ assertValidTaskName(taskName);
53
+ return expoTaskManager.getTaskOptionsAsync(taskName);
54
+ }
55
+ /** Every task registered in the app, with the `options` each was registered with. */
56
+ export async function getRegisteredTasksAsync() {
57
+ if (!expoTaskManager.getRegisteredTasksAsync) {
58
+ throw new UnavailabilityError(NATIVE_MODULE_NAME, 'getRegisteredTasksAsync');
59
+ }
60
+ return expoTaskManager.getRegisteredTasksAsync();
61
+ }
62
+ /**
63
+ * Unregisters a task so the app stops receiving updates for it. Prefer the specialized method of
64
+ * whichever module registered the task (e.g. `Location.stopLocationUpdatesAsync`) when one exists.
65
+ */
66
+ export async function unregisterTaskAsync(taskName) {
67
+ if (!expoTaskManager.unregisterTaskAsync) {
68
+ throw new UnavailabilityError(NATIVE_MODULE_NAME, 'unregisterTaskAsync');
69
+ }
70
+ assertValidTaskName(taskName);
71
+ await expoTaskManager.unregisterTaskAsync(taskName);
72
+ }
73
+ /** Unregisters every task registered for the running app. */
74
+ export async function unregisterAllTasksAsync() {
75
+ if (!expoTaskManager.unregisterAllTasksAsync) {
76
+ throw new UnavailabilityError(NATIVE_MODULE_NAME, 'unregisterAllTasksAsync');
77
+ }
78
+ await expoTaskManager.unregisterAllTasksAsync();
79
+ }
80
+ /** Whether the TaskManager API can be used in this app. Always `false` outside a device build. */
81
+ export async function isAvailableAsync() {
82
+ if (!expoTaskManager.isAvailableAsync)
83
+ return false;
84
+ return expoTaskManager.isAvailableAsync();
85
+ }
86
+ // Wired once at module load, exactly like every adapter's `defineTask` call site — native fires
87
+ // this event whenever a defined task should run, and we look the executor up by name. The task
88
+ // itself may have been dropped from the bundle (renamed, deleted) since it was registered, so an
89
+ // unknown taskName still needs to ack native and clean up the stale registration.
90
+ expoTaskManager.addListener(expoTaskManager.EVENT_NAME, (body) => {
91
+ const { data, error, executionInfo } = body;
92
+ const { eventId, taskName } = executionInfo;
93
+ const taskExecutor = tasks.get(taskName);
94
+ if (taskExecutor) {
95
+ void taskExecutor({ data, error, executionInfo })
96
+ .catch((taskError) => {
97
+ console.error(`TaskManager: Task "${taskName}" failed:`, taskError);
98
+ })
99
+ .finally(() => {
100
+ void expoTaskManager.notifyTaskFinishedAsync?.(taskName, {
101
+ eventId,
102
+ result: null,
103
+ });
104
+ });
105
+ return;
106
+ }
107
+ console.warn(`TaskManager: Execution of "${taskName}" was requested but it is not defined. ` +
108
+ `Available tasks: [${[...tasks.keys()].join(', ')}]. Make sure "defineTask" is called during initialization.`);
109
+ void expoTaskManager
110
+ .notifyTaskFinishedAsync?.(taskName, { eventId, result: null })
111
+ .then(() => expoTaskManager.unregisterTaskAsync?.(taskName));
112
+ });
@@ -0,0 +1,32 @@
1
+ export type ITaskManagerError = {
2
+ code: string | number;
3
+ message: string;
4
+ };
5
+ /** Additional details about execution, threaded through to the task executor. */
6
+ export type ITaskManagerTaskBodyExecutionInfo = {
7
+ /** State of the application. @platform ios */
8
+ appState?: 'active' | 'background' | 'inactive';
9
+ /** Unique ID of the task event. */
10
+ eventId: string;
11
+ /** Name of the task. */
12
+ taskName: string;
13
+ };
14
+ /** What native hands the task executor when it runs the task. */
15
+ export type ITaskManagerTaskBody<TData = unknown> = {
16
+ /** Data passed to the task executor; its shape depends on the task's own consumer (location, geofencing, …). */
17
+ data: TData;
18
+ /** Error object if the task failed, or `null` otherwise. */
19
+ error: ITaskManagerError | null;
20
+ executionInfo: ITaskManagerTaskBodyExecutionInfo;
21
+ };
22
+ /** An already-registered task, as reported by `getRegisteredTasksAsync`. */
23
+ export type ITaskManagerTask<TOptions = unknown> = {
24
+ /** Name the task is registered under. */
25
+ taskName: string;
26
+ /** Type of the task — depends on how it was registered (its consumer's own name). */
27
+ taskType: string;
28
+ /** `options` the task was registered with. */
29
+ options: TOptions;
30
+ };
31
+ /** A function that handles a defined task. */
32
+ export type ITaskManagerTaskExecutor<TData = unknown> = (body: ITaskManagerTaskBody<TData>) => Promise<unknown>;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export * from '../core';
@@ -0,0 +1,5 @@
1
+ // @symbiote-native/task-manager/angular: the Angular entry over the framework-agnostic core. Same
2
+ // reasoning as the React/Vue entries — no per-instance state or event stream to wrap in a
3
+ // service, so this is a plain re-export.
4
+ export * from '../core';
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/angular/index.ts"],"names":[],"mappings":"AAAA,kGAAkG;AAClG,0FAA0F;AAC1F,yCAAyC;AACzC,cAAc,SAAS,CAAC"}
@@ -0,0 +1,2 @@
1
+ export { defineTask, isTaskDefined, isTaskRegisteredAsync, getTaskOptionsAsync, getRegisteredTasksAsync, unregisterTaskAsync, unregisterAllTasksAsync, isAvailableAsync, } from './task-manager';
2
+ export type { ITaskManagerError, ITaskManagerTask, ITaskManagerTaskBody, ITaskManagerTaskBodyExecutionInfo, ITaskManagerTaskExecutor, } from './types';
@@ -0,0 +1,2 @@
1
+ export { defineTask, isTaskDefined, isTaskRegisteredAsync, getTaskOptionsAsync, getRegisteredTasksAsync, unregisterTaskAsync, unregisterAllTasksAsync, isAvailableAsync, } from './task-manager';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,UAAU,EACV,aAAa,EACb,qBAAqB,EACrB,mBAAmB,EACnB,uBAAuB,EACvB,mBAAmB,EACnB,uBAAuB,EACvB,gBAAgB,GACjB,MAAM,gBAAgB,CAAC"}
@@ -0,0 +1,22 @@
1
+ import { type EventSubscription } from 'expo-modules-core';
2
+ import type { ITaskManagerTaskBody } from './types';
3
+ export type INativeTaskManagerModule = {
4
+ /** Name of the event native emits when a defined task should run. */
5
+ EVENT_NAME: string;
6
+ addListener(eventName: string, listener: (event: ITaskManagerTaskBody) => void): EventSubscription;
7
+ isAvailableAsync?(): Promise<boolean>;
8
+ isTaskRegisteredAsync?(taskName: string): Promise<boolean>;
9
+ getTaskOptionsAsync?<TOptions>(taskName: string): Promise<TOptions>;
10
+ getRegisteredTasksAsync?(): Promise<{
11
+ taskName: string;
12
+ taskType: string;
13
+ options: unknown;
14
+ }[]>;
15
+ unregisterTaskAsync?(taskName: string): Promise<void>;
16
+ unregisterAllTasksAsync?(): Promise<void>;
17
+ notifyTaskFinishedAsync?(taskName: string, payload: {
18
+ eventId: string;
19
+ result: unknown;
20
+ }): Promise<void>;
21
+ };
22
+ export declare const expoTaskManager: INativeTaskManagerModule;
@@ -0,0 +1,4 @@
1
+ import { requireNativeModule } from 'expo-modules-core';
2
+ const EXPO_TASK_MANAGER_MODULE_NAME = 'ExpoTaskManager';
3
+ export const expoTaskManager = requireNativeModule(EXPO_TASK_MANAGER_MODULE_NAME);
4
+ //# sourceMappingURL=native-module.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"native-module.js","sourceRoot":"","sources":["../../src/core/native-module.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAA0B,MAAM,mBAAmB,CAAC;AAGhF,MAAM,6BAA6B,GAAG,iBAAiB,CAAC;AA8BxD,MAAM,CAAC,MAAM,eAAe,GAAG,mBAAmB,CAChD,6BAA6B,CAC9B,CAAC"}
@@ -0,0 +1,28 @@
1
+ import type { ITaskManagerTask, ITaskManagerTaskExecutor } from './types';
2
+ /**
3
+ * Defines a task executor. Must be called in the global scope of the JS bundle — never inside a
4
+ * component lifecycle method — because when the app is launched in the background there are no
5
+ * views mounted, only this module-scope registration for native to find.
6
+ */
7
+ export declare function defineTask<TData = unknown>(taskName: string, taskExecutor: ITaskManagerTaskExecutor<TData>): void;
8
+ /** Whether a task with the given name has been defined via `defineTask`. */
9
+ export declare function isTaskDefined(taskName: string): boolean;
10
+ /**
11
+ * Whether the task is registered with native. Registered tasks persist across sessions; a task
12
+ * can be defined without being registered (registration is driven by the task's own consumer,
13
+ * e.g. `Location.startLocationUpdatesAsync`).
14
+ */
15
+ export declare function isTaskRegisteredAsync(taskName: string): Promise<boolean>;
16
+ /** The `options` a task was registered with, or rejects if the task can't be found. */
17
+ export declare function getTaskOptionsAsync<TOptions>(taskName: string): Promise<TOptions>;
18
+ /** Every task registered in the app, with the `options` each was registered with. */
19
+ export declare function getRegisteredTasksAsync<TOptions = unknown>(): Promise<ITaskManagerTask<TOptions>[]>;
20
+ /**
21
+ * Unregisters a task so the app stops receiving updates for it. Prefer the specialized method of
22
+ * whichever module registered the task (e.g. `Location.stopLocationUpdatesAsync`) when one exists.
23
+ */
24
+ export declare function unregisterTaskAsync(taskName: string): Promise<void>;
25
+ /** Unregisters every task registered for the running app. */
26
+ export declare function unregisterAllTasksAsync(): Promise<void>;
27
+ /** Whether the TaskManager API can be used in this app. Always `false` outside a device build. */
28
+ export declare function isAvailableAsync(): Promise<boolean>;
@@ -0,0 +1,113 @@
1
+ import { UnavailabilityError } from 'expo-modules-core';
2
+ import { expoTaskManager } from './native-module';
3
+ const NATIVE_MODULE_NAME = 'expo-task-manager';
4
+ // The registry is necessarily heterogeneous — each task's own TData differs — so the executor's
5
+ // data parameter is typed `any` here only, exactly like upstream's own
6
+ // `Map<string, TaskManagerTaskExecutor<any>>`. `defineTask`'s own signature stays fully typed;
7
+ // this is the one place bridging distinct `ITaskManagerTaskExecutor<TData>` instances into one
8
+ // registry, which a cast could not do more soundly than `any` already does.
9
+ const tasks = new Map();
10
+ function assertValidTaskName(taskName) {
11
+ if (!taskName || typeof taskName !== 'string') {
12
+ throw new TypeError('`taskName` must be a non-empty string.');
13
+ }
14
+ }
15
+ /**
16
+ * Defines a task executor. Must be called in the global scope of the JS bundle — never inside a
17
+ * component lifecycle method — because when the app is launched in the background there are no
18
+ * views mounted, only this module-scope registration for native to find.
19
+ */
20
+ export function defineTask(taskName, taskExecutor) {
21
+ if (!taskName || typeof taskName !== 'string') {
22
+ console.warn("TaskManager.defineTask: 'taskName' argument must be a non-empty string.");
23
+ return;
24
+ }
25
+ if (!taskExecutor || typeof taskExecutor !== 'function') {
26
+ console.warn("TaskManager.defineTask: 'task' argument must be a function.");
27
+ return;
28
+ }
29
+ tasks.set(taskName, taskExecutor);
30
+ }
31
+ /** Whether a task with the given name has been defined via `defineTask`. */
32
+ export function isTaskDefined(taskName) {
33
+ return tasks.has(taskName);
34
+ }
35
+ /**
36
+ * Whether the task is registered with native. Registered tasks persist across sessions; a task
37
+ * can be defined without being registered (registration is driven by the task's own consumer,
38
+ * e.g. `Location.startLocationUpdatesAsync`).
39
+ */
40
+ export async function isTaskRegisteredAsync(taskName) {
41
+ if (!expoTaskManager.isTaskRegisteredAsync) {
42
+ throw new UnavailabilityError(NATIVE_MODULE_NAME, 'isTaskRegisteredAsync');
43
+ }
44
+ assertValidTaskName(taskName);
45
+ return expoTaskManager.isTaskRegisteredAsync(taskName);
46
+ }
47
+ /** The `options` a task was registered with, or rejects if the task can't be found. */
48
+ export async function getTaskOptionsAsync(taskName) {
49
+ if (!expoTaskManager.getTaskOptionsAsync) {
50
+ throw new UnavailabilityError(NATIVE_MODULE_NAME, 'getTaskOptionsAsync');
51
+ }
52
+ assertValidTaskName(taskName);
53
+ return expoTaskManager.getTaskOptionsAsync(taskName);
54
+ }
55
+ /** Every task registered in the app, with the `options` each was registered with. */
56
+ export async function getRegisteredTasksAsync() {
57
+ if (!expoTaskManager.getRegisteredTasksAsync) {
58
+ throw new UnavailabilityError(NATIVE_MODULE_NAME, 'getRegisteredTasksAsync');
59
+ }
60
+ return expoTaskManager.getRegisteredTasksAsync();
61
+ }
62
+ /**
63
+ * Unregisters a task so the app stops receiving updates for it. Prefer the specialized method of
64
+ * whichever module registered the task (e.g. `Location.stopLocationUpdatesAsync`) when one exists.
65
+ */
66
+ export async function unregisterTaskAsync(taskName) {
67
+ if (!expoTaskManager.unregisterTaskAsync) {
68
+ throw new UnavailabilityError(NATIVE_MODULE_NAME, 'unregisterTaskAsync');
69
+ }
70
+ assertValidTaskName(taskName);
71
+ await expoTaskManager.unregisterTaskAsync(taskName);
72
+ }
73
+ /** Unregisters every task registered for the running app. */
74
+ export async function unregisterAllTasksAsync() {
75
+ if (!expoTaskManager.unregisterAllTasksAsync) {
76
+ throw new UnavailabilityError(NATIVE_MODULE_NAME, 'unregisterAllTasksAsync');
77
+ }
78
+ await expoTaskManager.unregisterAllTasksAsync();
79
+ }
80
+ /** Whether the TaskManager API can be used in this app. Always `false` outside a device build. */
81
+ export async function isAvailableAsync() {
82
+ if (!expoTaskManager.isAvailableAsync)
83
+ return false;
84
+ return expoTaskManager.isAvailableAsync();
85
+ }
86
+ // Wired once at module load, exactly like every adapter's `defineTask` call site — native fires
87
+ // this event whenever a defined task should run, and we look the executor up by name. The task
88
+ // itself may have been dropped from the bundle (renamed, deleted) since it was registered, so an
89
+ // unknown taskName still needs to ack native and clean up the stale registration.
90
+ expoTaskManager.addListener(expoTaskManager.EVENT_NAME, (body) => {
91
+ const { data, error, executionInfo } = body;
92
+ const { eventId, taskName } = executionInfo;
93
+ const taskExecutor = tasks.get(taskName);
94
+ if (taskExecutor) {
95
+ void taskExecutor({ data, error, executionInfo })
96
+ .catch((taskError) => {
97
+ console.error(`TaskManager: Task "${taskName}" failed:`, taskError);
98
+ })
99
+ .finally(() => {
100
+ void expoTaskManager.notifyTaskFinishedAsync?.(taskName, {
101
+ eventId,
102
+ result: null,
103
+ });
104
+ });
105
+ return;
106
+ }
107
+ console.warn(`TaskManager: Execution of "${taskName}" was requested but it is not defined. ` +
108
+ `Available tasks: [${[...tasks.keys()].join(', ')}]. Make sure "defineTask" is called during initialization.`);
109
+ void expoTaskManager
110
+ .notifyTaskFinishedAsync?.(taskName, { eventId, result: null })
111
+ .then(() => expoTaskManager.unregisterTaskAsync?.(taskName));
112
+ });
113
+ //# sourceMappingURL=task-manager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"task-manager.js","sourceRoot":"","sources":["../../src/core/task-manager.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,32 @@
1
+ export type ITaskManagerError = {
2
+ code: string | number;
3
+ message: string;
4
+ };
5
+ /** Additional details about execution, threaded through to the task executor. */
6
+ export type ITaskManagerTaskBodyExecutionInfo = {
7
+ /** State of the application. @platform ios */
8
+ appState?: 'active' | 'background' | 'inactive';
9
+ /** Unique ID of the task event. */
10
+ eventId: string;
11
+ /** Name of the task. */
12
+ taskName: string;
13
+ };
14
+ /** What native hands the task executor when it runs the task. */
15
+ export type ITaskManagerTaskBody<TData = unknown> = {
16
+ /** Data passed to the task executor; its shape depends on the task's own consumer (location, geofencing, …). */
17
+ data: TData;
18
+ /** Error object if the task failed, or `null` otherwise. */
19
+ error: ITaskManagerError | null;
20
+ executionInfo: ITaskManagerTaskBodyExecutionInfo;
21
+ };
22
+ /** An already-registered task, as reported by `getRegisteredTasksAsync`. */
23
+ export type ITaskManagerTask<TOptions = unknown> = {
24
+ /** Name the task is registered under. */
25
+ taskName: string;
26
+ /** Type of the task — depends on how it was registered (its consumer's own name). */
27
+ taskType: string;
28
+ /** `options` the task was registered with. */
29
+ options: TOptions;
30
+ };
31
+ /** A function that handles a defined task. */
32
+ export type ITaskManagerTaskExecutor<TData = unknown> = (body: ITaskManagerTaskBody<TData>) => Promise<unknown>;
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,22 @@
1
+ {
2
+ "android": {
3
+ "gradleProjectName": "expo-task-manager",
4
+ "modules": [
5
+ {
6
+ "importPath": "expo.modules.taskManager.TaskManagerModule",
7
+ "className": "TaskManagerModule",
8
+ "nativeName": "ExpoTaskManager"
9
+ }
10
+ ],
11
+ "services": [
12
+ {
13
+ "importPath": "expo.modules.constants.ConstantsService",
14
+ "className": "ConstantsService",
15
+ "gradleProjectName": "expo-constants"
16
+ }
17
+ ]
18
+ },
19
+ "ios": {
20
+ "infoPlistArrayKeys": { "UIBackgroundModes": ["fetch"] }
21
+ }
22
+ }
package/package.json ADDED
@@ -0,0 +1,152 @@
1
+ {
2
+ "name": "@symbiote-native/task-manager",
3
+ "version": "0.1.0",
4
+ "description": "expo-task-manager wrapped for SymbioteNative — one framework-agnostic core, built once and reachable from the React, Vue, Svelte, Solid, and Angular adapters. Defines and tracks background tasks that native code invokes headlessly.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/OneEyed1366/symbiote-native.git",
9
+ "directory": "packages/task-manager"
10
+ },
11
+ "homepage": "https://github.com/OneEyed1366/symbiote-native/tree/master/packages/task-manager#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/OneEyed1366/symbiote-native/issues"
14
+ },
15
+ "author": "Andrey Prokopenko <psevdoproger@gmail.com>",
16
+ "keywords": [
17
+ "react-native",
18
+ "symbiote-native",
19
+ "expo-task-manager",
20
+ "react",
21
+ "vue",
22
+ "svelte",
23
+ "solid",
24
+ "angular",
25
+ "background",
26
+ "task"
27
+ ],
28
+ "type": "module",
29
+ "main": "./build/core/index.js",
30
+ "module": "./build/core/index.js",
31
+ "types": "./build/core/index.d.ts",
32
+ "exports": {
33
+ ".": {
34
+ "types": "./build/core/index.d.ts",
35
+ "default": "./build/core/index.js"
36
+ },
37
+ "./vue": {
38
+ "types": "./build/core/index.d.ts",
39
+ "default": "./build/core/index.js"
40
+ },
41
+ "./react": {
42
+ "types": "./build/core/index.d.ts",
43
+ "default": "./build/core/index.js"
44
+ },
45
+ "./svelte": {
46
+ "types": "./build/core/index.d.ts",
47
+ "default": "./build/core/index.js"
48
+ },
49
+ "./solid": {
50
+ "types": "./build/core/index.d.ts",
51
+ "default": "./build/core/index.js"
52
+ },
53
+ "./angular": {
54
+ "types": "./build-ngc/angular/index.d.ts",
55
+ "react-native": "./build-ngc/angular/index.js",
56
+ "default": "./src/angular/index.ts"
57
+ }
58
+ },
59
+ "files": [
60
+ "src",
61
+ "build",
62
+ "build-ngc",
63
+ "native-link.json",
64
+ "!src/**/*.test.*",
65
+ "!src/**/*.spec.*",
66
+ "!src/**/*.detox.*"
67
+ ],
68
+ "publishConfig": {
69
+ "access": "public"
70
+ },
71
+ "dependencies": {
72
+ "expo-task-manager": "57.0.15",
73
+ "unimodules-app-loader": "57.0.1",
74
+ "expo-constants": "57.0.19",
75
+ "expo-modules-core": "57.0.5"
76
+ },
77
+ "peerDependencies": {
78
+ "@angular/core": ">=20",
79
+ "@vue/runtime-core": "^3.5.13",
80
+ "react": ">=19.0.0",
81
+ "react-native": ">=0.86",
82
+ "solid-js": ">=1.9.0",
83
+ "svelte": ">=5.56.0",
84
+ "vue": ">=3.5.0",
85
+ "@symbiote-native/angular": "^3.1.0",
86
+ "@symbiote-native/react": "^3.0.2",
87
+ "@symbiote-native/solid": "^3.0.2",
88
+ "@symbiote-native/engine": "^1.3.0",
89
+ "@symbiote-native/vue": "^3.0.2",
90
+ "@symbiote-native/svelte": "^3.0.2"
91
+ },
92
+ "peerDependenciesMeta": {
93
+ "@symbiote-native/angular": {
94
+ "optional": true
95
+ },
96
+ "@symbiote-native/react": {
97
+ "optional": true
98
+ },
99
+ "@symbiote-native/solid": {
100
+ "optional": true
101
+ },
102
+ "@symbiote-native/svelte": {
103
+ "optional": true
104
+ },
105
+ "@symbiote-native/vue": {
106
+ "optional": true
107
+ },
108
+ "@angular/core": {
109
+ "optional": true
110
+ },
111
+ "@vue/runtime-core": {
112
+ "optional": true
113
+ },
114
+ "react": {
115
+ "optional": true
116
+ },
117
+ "solid-js": {
118
+ "optional": true
119
+ },
120
+ "svelte": {
121
+ "optional": true
122
+ },
123
+ "vue": {
124
+ "optional": true
125
+ }
126
+ },
127
+ "devDependencies": {
128
+ "@angular/compiler": "~22.0.8",
129
+ "@angular/compiler-cli": "~22.0.8",
130
+ "@angular/core": "~22.0.8",
131
+ "@types/node": "^26.0.0",
132
+ "@types/react": "^19.2.0",
133
+ "@vue/runtime-core": "^3.5.13",
134
+ "react": "19.2.3",
135
+ "solid-js": "^1.9.14",
136
+ "svelte": "^5.56.0",
137
+ "typescript": "~6.0.0",
138
+ "@symbiote-native/angular": "3.1.0",
139
+ "@symbiote-native/svelte": "3.0.2",
140
+ "@symbiote-native/solid": "3.0.2",
141
+ "@symbiote-native/react": "3.0.2",
142
+ "@symbiote-native/engine": "1.3.0",
143
+ "@symbiote-native/test-utils": "0.4.2",
144
+ "@symbiote-native/vue": "3.0.2"
145
+ },
146
+ "scripts": {
147
+ "typecheck": "tsc --build",
148
+ "clean": "rm -rf build-ngc",
149
+ "ng:build": "pnpm run clean && ngc -p tsconfig.angular.json",
150
+ "format": "prettier --write \"src/**/*.{ts,tsx}\""
151
+ }
152
+ }
@@ -0,0 +1,4 @@
1
+ // @symbiote-native/task-manager/angular: the Angular entry over the framework-agnostic core. Same
2
+ // reasoning as the React/Vue entries — no per-instance state or event stream to wrap in a
3
+ // service, so this is a plain re-export.
4
+ export * from '../core';
@@ -0,0 +1,17 @@
1
+ export {
2
+ defineTask,
3
+ isTaskDefined,
4
+ isTaskRegisteredAsync,
5
+ getTaskOptionsAsync,
6
+ getRegisteredTasksAsync,
7
+ unregisterTaskAsync,
8
+ unregisterAllTasksAsync,
9
+ isAvailableAsync,
10
+ } from './task-manager';
11
+ export type {
12
+ ITaskManagerError,
13
+ ITaskManagerTask,
14
+ ITaskManagerTaskBody,
15
+ ITaskManagerTaskBodyExecutionInfo,
16
+ ITaskManagerTaskExecutor,
17
+ } from './types';
@@ -0,0 +1,36 @@
1
+ import { requireNativeModule, type EventSubscription } from 'expo-modules-core';
2
+ import type { ITaskManagerTaskBody } from './types';
3
+
4
+ const EXPO_TASK_MANAGER_MODULE_NAME = 'ExpoTaskManager';
5
+
6
+ // Every method is optional — each call site checks for its presence before calling through and
7
+ // throws an UnavailabilityError itself, matching upstream's own per-platform capability checks
8
+ // rather than assuming the native module implements the whole surface (same pattern
9
+ // packages/local-auth/src/core/native-module.ts uses). `addListener` stays REQUIRED, mirroring
10
+ // packages/sensors/src/core/device-sensor.ts's INativeSensorModule — a modern expo-modules-core
11
+ // Module always implements the EventEmitter interface itself, so no LegacyEventEmitter shim is
12
+ // needed here (that shim exists for older native modules that predate it).
13
+ export type INativeTaskManagerModule = {
14
+ /** Name of the event native emits when a defined task should run. */
15
+ EVENT_NAME: string;
16
+ addListener(
17
+ eventName: string,
18
+ listener: (event: ITaskManagerTaskBody) => void,
19
+ ): EventSubscription;
20
+ isAvailableAsync?(): Promise<boolean>;
21
+ isTaskRegisteredAsync?(taskName: string): Promise<boolean>;
22
+ getTaskOptionsAsync?<TOptions>(taskName: string): Promise<TOptions>;
23
+ getRegisteredTasksAsync?(): Promise<
24
+ { taskName: string; taskType: string; options: unknown }[]
25
+ >;
26
+ unregisterTaskAsync?(taskName: string): Promise<void>;
27
+ unregisterAllTasksAsync?(): Promise<void>;
28
+ notifyTaskFinishedAsync?(
29
+ taskName: string,
30
+ payload: { eventId: string; result: unknown },
31
+ ): Promise<void>;
32
+ };
33
+
34
+ export const expoTaskManager = requireNativeModule<INativeTaskManagerModule>(
35
+ EXPO_TASK_MANAGER_MODULE_NAME,
36
+ );
@@ -0,0 +1,154 @@
1
+ import { UnavailabilityError } from 'expo-modules-core';
2
+ import { expoTaskManager } from './native-module';
3
+ import type {
4
+ ITaskManagerTask,
5
+ ITaskManagerTaskBody,
6
+ ITaskManagerTaskExecutor,
7
+ } from './types';
8
+
9
+ const NATIVE_MODULE_NAME = 'expo-task-manager';
10
+
11
+ // The registry is necessarily heterogeneous — each task's own TData differs — so the executor's
12
+ // data parameter is typed `any` here only, exactly like upstream's own
13
+ // `Map<string, TaskManagerTaskExecutor<any>>`. `defineTask`'s own signature stays fully typed;
14
+ // this is the one place bridging distinct `ITaskManagerTaskExecutor<TData>` instances into one
15
+ // registry, which a cast could not do more soundly than `any` already does.
16
+ const tasks = new Map<string, ITaskManagerTaskExecutor<any>>();
17
+
18
+ function assertValidTaskName(taskName: unknown): asserts taskName is string {
19
+ if (!taskName || typeof taskName !== 'string') {
20
+ throw new TypeError('`taskName` must be a non-empty string.');
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Defines a task executor. Must be called in the global scope of the JS bundle — never inside a
26
+ * component lifecycle method — because when the app is launched in the background there are no
27
+ * views mounted, only this module-scope registration for native to find.
28
+ */
29
+ export function defineTask<TData = unknown>(
30
+ taskName: string,
31
+ taskExecutor: ITaskManagerTaskExecutor<TData>,
32
+ ): void {
33
+ if (!taskName || typeof taskName !== 'string') {
34
+ console.warn(
35
+ "TaskManager.defineTask: 'taskName' argument must be a non-empty string.",
36
+ );
37
+ return;
38
+ }
39
+ if (!taskExecutor || typeof taskExecutor !== 'function') {
40
+ console.warn("TaskManager.defineTask: 'task' argument must be a function.");
41
+ return;
42
+ }
43
+ tasks.set(taskName, taskExecutor);
44
+ }
45
+
46
+ /** Whether a task with the given name has been defined via `defineTask`. */
47
+ export function isTaskDefined(taskName: string): boolean {
48
+ return tasks.has(taskName);
49
+ }
50
+
51
+ /**
52
+ * Whether the task is registered with native. Registered tasks persist across sessions; a task
53
+ * can be defined without being registered (registration is driven by the task's own consumer,
54
+ * e.g. `Location.startLocationUpdatesAsync`).
55
+ */
56
+ export async function isTaskRegisteredAsync(
57
+ taskName: string,
58
+ ): Promise<boolean> {
59
+ if (!expoTaskManager.isTaskRegisteredAsync) {
60
+ throw new UnavailabilityError(NATIVE_MODULE_NAME, 'isTaskRegisteredAsync');
61
+ }
62
+ assertValidTaskName(taskName);
63
+ return expoTaskManager.isTaskRegisteredAsync(taskName);
64
+ }
65
+
66
+ /** The `options` a task was registered with, or rejects if the task can't be found. */
67
+ export async function getTaskOptionsAsync<TOptions>(
68
+ taskName: string,
69
+ ): Promise<TOptions> {
70
+ if (!expoTaskManager.getTaskOptionsAsync) {
71
+ throw new UnavailabilityError(NATIVE_MODULE_NAME, 'getTaskOptionsAsync');
72
+ }
73
+ assertValidTaskName(taskName);
74
+ return expoTaskManager.getTaskOptionsAsync<TOptions>(taskName);
75
+ }
76
+
77
+ /** Every task registered in the app, with the `options` each was registered with. */
78
+ export async function getRegisteredTasksAsync<TOptions = unknown>(): Promise<
79
+ ITaskManagerTask<TOptions>[]
80
+ > {
81
+ if (!expoTaskManager.getRegisteredTasksAsync) {
82
+ throw new UnavailabilityError(
83
+ NATIVE_MODULE_NAME,
84
+ 'getRegisteredTasksAsync',
85
+ );
86
+ }
87
+ return expoTaskManager.getRegisteredTasksAsync() as Promise<
88
+ ITaskManagerTask<TOptions>[]
89
+ >;
90
+ }
91
+
92
+ /**
93
+ * Unregisters a task so the app stops receiving updates for it. Prefer the specialized method of
94
+ * whichever module registered the task (e.g. `Location.stopLocationUpdatesAsync`) when one exists.
95
+ */
96
+ export async function unregisterTaskAsync(taskName: string): Promise<void> {
97
+ if (!expoTaskManager.unregisterTaskAsync) {
98
+ throw new UnavailabilityError(NATIVE_MODULE_NAME, 'unregisterTaskAsync');
99
+ }
100
+ assertValidTaskName(taskName);
101
+ await expoTaskManager.unregisterTaskAsync(taskName);
102
+ }
103
+
104
+ /** Unregisters every task registered for the running app. */
105
+ export async function unregisterAllTasksAsync(): Promise<void> {
106
+ if (!expoTaskManager.unregisterAllTasksAsync) {
107
+ throw new UnavailabilityError(
108
+ NATIVE_MODULE_NAME,
109
+ 'unregisterAllTasksAsync',
110
+ );
111
+ }
112
+ await expoTaskManager.unregisterAllTasksAsync();
113
+ }
114
+
115
+ /** Whether the TaskManager API can be used in this app. Always `false` outside a device build. */
116
+ export async function isAvailableAsync(): Promise<boolean> {
117
+ if (!expoTaskManager.isAvailableAsync) return false;
118
+ return expoTaskManager.isAvailableAsync();
119
+ }
120
+
121
+ // Wired once at module load, exactly like every adapter's `defineTask` call site — native fires
122
+ // this event whenever a defined task should run, and we look the executor up by name. The task
123
+ // itself may have been dropped from the bundle (renamed, deleted) since it was registered, so an
124
+ // unknown taskName still needs to ack native and clean up the stale registration.
125
+ expoTaskManager.addListener(
126
+ expoTaskManager.EVENT_NAME,
127
+ (body: ITaskManagerTaskBody) => {
128
+ const { data, error, executionInfo } = body;
129
+ const { eventId, taskName } = executionInfo;
130
+ const taskExecutor = tasks.get(taskName);
131
+
132
+ if (taskExecutor) {
133
+ void taskExecutor({ data, error, executionInfo })
134
+ .catch((taskError: unknown) => {
135
+ console.error(`TaskManager: Task "${taskName}" failed:`, taskError);
136
+ })
137
+ .finally(() => {
138
+ void expoTaskManager.notifyTaskFinishedAsync?.(taskName, {
139
+ eventId,
140
+ result: null,
141
+ });
142
+ });
143
+ return;
144
+ }
145
+
146
+ console.warn(
147
+ `TaskManager: Execution of "${taskName}" was requested but it is not defined. ` +
148
+ `Available tasks: [${[...tasks.keys()].join(', ')}]. Make sure "defineTask" is called during initialization.`,
149
+ );
150
+ void expoTaskManager
151
+ .notifyTaskFinishedAsync?.(taskName, { eventId, result: null })
152
+ .then(() => expoTaskManager.unregisterTaskAsync?.(taskName));
153
+ },
154
+ );
@@ -0,0 +1,38 @@
1
+ export type ITaskManagerError = {
2
+ code: string | number;
3
+ message: string;
4
+ };
5
+
6
+ /** Additional details about execution, threaded through to the task executor. */
7
+ export type ITaskManagerTaskBodyExecutionInfo = {
8
+ /** State of the application. @platform ios */
9
+ appState?: 'active' | 'background' | 'inactive';
10
+ /** Unique ID of the task event. */
11
+ eventId: string;
12
+ /** Name of the task. */
13
+ taskName: string;
14
+ };
15
+
16
+ /** What native hands the task executor when it runs the task. */
17
+ export type ITaskManagerTaskBody<TData = unknown> = {
18
+ /** Data passed to the task executor; its shape depends on the task's own consumer (location, geofencing, …). */
19
+ data: TData;
20
+ /** Error object if the task failed, or `null` otherwise. */
21
+ error: ITaskManagerError | null;
22
+ executionInfo: ITaskManagerTaskBodyExecutionInfo;
23
+ };
24
+
25
+ /** An already-registered task, as reported by `getRegisteredTasksAsync`. */
26
+ export type ITaskManagerTask<TOptions = unknown> = {
27
+ /** Name the task is registered under. */
28
+ taskName: string;
29
+ /** Type of the task — depends on how it was registered (its consumer's own name). */
30
+ taskType: string;
31
+ /** `options` the task was registered with. */
32
+ options: TOptions;
33
+ };
34
+
35
+ /** A function that handles a defined task. */
36
+ export type ITaskManagerTaskExecutor<TData = unknown> = (
37
+ body: ITaskManagerTaskBody<TData>,
38
+ ) => Promise<unknown>;