@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 +21 -0
- package/README.md +164 -0
- package/build/angular/index.d.ts +1 -0
- package/build/angular/index.js +4 -0
- package/build/core/index.d.ts +2 -0
- package/build/core/index.js +1 -0
- package/build/core/native-module.d.ts +22 -0
- package/build/core/native-module.js +3 -0
- package/build/core/task-manager.d.ts +28 -0
- package/build/core/task-manager.js +112 -0
- package/build/core/types.d.ts +32 -0
- package/build/core/types.js +1 -0
- package/build-ngc/angular/index.d.ts +1 -0
- package/build-ngc/angular/index.js +5 -0
- package/build-ngc/angular/index.js.map +1 -0
- package/build-ngc/core/index.d.ts +2 -0
- package/build-ngc/core/index.js +2 -0
- package/build-ngc/core/index.js.map +1 -0
- package/build-ngc/core/native-module.d.ts +22 -0
- package/build-ngc/core/native-module.js +4 -0
- package/build-ngc/core/native-module.js.map +1 -0
- package/build-ngc/core/task-manager.d.ts +28 -0
- package/build-ngc/core/task-manager.js +113 -0
- package/build-ngc/core/task-manager.js.map +1 -0
- package/build-ngc/core/types.d.ts +32 -0
- package/build-ngc/core/types.js +2 -0
- package/build-ngc/core/types.js.map +1 -0
- package/native-link.json +22 -0
- package/package.json +152 -0
- package/src/angular/index.ts +4 -0
- package/src/core/index.ts +17 -0
- package/src/core/native-module.ts +36 -0
- package/src/core/task-manager.ts +154 -0
- package/src/core/types.ts +38 -0
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,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,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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":""}
|
package/native-link.json
ADDED
|
@@ -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,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>;
|