@symbiote-native/background-task 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 +170 -0
- package/build/angular/index.d.ts +1 -0
- package/build/angular/index.js +4 -0
- package/build/core/background-task.d.ts +25 -0
- package/build/core/background-task.js +85 -0
- package/build/core/index.d.ts +3 -0
- package/build/core/index.js +2 -0
- package/build/core/native-module.d.ts +10 -0
- package/build/core/native-module.js +3 -0
- package/build/core/types.d.ts +26 -0
- package/build/core/types.js +16 -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/background-task.d.ts +25 -0
- package/build-ngc/core/background-task.js +86 -0
- package/build-ngc/core/background-task.js.map +1 -0
- package/build-ngc/core/index.d.ts +3 -0
- package/build-ngc/core/index.js +3 -0
- package/build-ngc/core/index.js.map +1 -0
- package/build-ngc/core/native-module.d.ts +10 -0
- package/build-ngc/core/native-module.js +4 -0
- package/build-ngc/core/native-module.js.map +1 -0
- package/build-ngc/core/types.d.ts +26 -0
- package/build-ngc/core/types.js +17 -0
- package/build-ngc/core/types.js.map +1 -0
- package/native-link.json +20 -0
- package/package.json +153 -0
- package/src/angular/index.ts +4 -0
- package/src/core/background-task.ts +113 -0
- package/src/core/index.ts +9 -0
- package/src/core/native-module.ts +27 -0
- package/src/core/types.ts +28 -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,170 @@
|
|
|
1
|
+
# @symbiote-native/background-task
|
|
2
|
+
|
|
3
|
+
A wrapper package for [SymbioteNative](../../README.md) that makes
|
|
4
|
+
[`expo-background-task`](https://github.com/expo/expo/tree/main/packages/expo-background-task)
|
|
5
|
+
usable from **every** adapter — React, Vue, Svelte, Solid, and Angular. Like
|
|
6
|
+
[`@symbiote-native/task-manager`](../task-manager), every export is a plain function or a
|
|
7
|
+
one-time module-load side effect, so there is no hook/composable/service to wrap: the React,
|
|
8
|
+
Vue, Svelte, Solid, and Angular entry points are plain re-exports of the same `core`.
|
|
9
|
+
|
|
10
|
+
This is the modern replacement for [`@symbiote-native/background-fetch`](../background-fetch),
|
|
11
|
+
built on `BGTaskScheduler` (iOS) / `WorkManager` (Android) instead of a periodic-fetch alarm.
|
|
12
|
+
Like its sibling, this package registers a task with native so it fires **in the background on
|
|
13
|
+
the OS's own schedule** — it does not define what the task does. Define the task first via
|
|
14
|
+
[`@symbiote-native/task-manager`](../task-manager)'s `defineTask`, then register it for periodic
|
|
15
|
+
execution via this package's `registerTaskAsync`.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
**New app:**
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npx @symbiote-native/cli new my-app --background-task
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**Existing SymbioteNative app:**
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npx @symbiote-native/cli add --background-task
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Either way: installs `@symbiote-native/background-task` + `@symbiote-native/task-manager` and
|
|
32
|
+
wires the native autolinking automatically — see [`@symbiote-native/cli`](../cli).
|
|
33
|
+
|
|
34
|
+
<details>
|
|
35
|
+
<summary>Manual install (no CLI — installing and wiring native autolinking by hand)</summary>
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npm install @symbiote-native/background-task @symbiote-native/task-manager
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`expo-background-task` and `expo-modules-core` come along as regular dependencies, pinned to
|
|
42
|
+
exact versions — never install them yourself, and never add the `expo` meta-package to your
|
|
43
|
+
project.
|
|
44
|
+
|
|
45
|
+
## Required one-time step: native autolinking wiring
|
|
46
|
+
|
|
47
|
+
Same one-time step as every other `expo-modules-core` package this project ships — see
|
|
48
|
+
[`@symbiote-native/local-auth`'s README](../local-auth/README.md#required-one-time-step-native-autolinking-wiring)
|
|
49
|
+
and the `symbiote-expo-native-module` project skill.
|
|
50
|
+
|
|
51
|
+
iOS also needs `UIBackgroundModes: processing` and `BGTaskSchedulerPermittedIdentifiers` (the
|
|
52
|
+
fixed identifier baked into `expo-background-task`'s own native Swift source,
|
|
53
|
+
`BackgroundTaskConstants.swift`) in the app's Info.plist — `native-link.json`'s `ios.infoPlistArrayKeys`
|
|
54
|
+
covers this ARRAY-valued case (see `@symbiote-native/expo-modules-link`), so it's wired
|
|
55
|
+
automatically by the same postinstall step, no manual edit needed. Android needs no manual step
|
|
56
|
+
either; its `AndroidManifest.xml` declares no extra permission.
|
|
57
|
+
|
|
58
|
+
**The iOS Simulator has no `BGTaskScheduler` support at all** (Apple's own limitation — physical
|
|
59
|
+
device only), so `BackgroundTaskStatus` reads `Restricted` there and `registerTaskAsync` is a
|
|
60
|
+
no-op regardless of Info.plist. Test registration on a real device.
|
|
61
|
+
|
|
62
|
+
</details>
|
|
63
|
+
|
|
64
|
+
## Shape
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
src/core/ getStatusAsync / registerTaskAsync / unregisterTaskAsync /
|
|
68
|
+
triggerTaskWorkerForTestingAsync / addExpirationListener, plus
|
|
69
|
+
BackgroundTaskStatus / BackgroundTaskResult / IBackgroundTaskOptions.
|
|
70
|
+
native-module.ts resolves the native module via expo-modules-core's
|
|
71
|
+
requireNativeModule.
|
|
72
|
+
src/angular/ @symbiote-native/background-task/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
|
+
```ts
|
|
82
|
+
// index.ts, alongside AppRegistry.registerComponent — identical on every adapter
|
|
83
|
+
import { defineTask } from '@symbiote-native/task-manager';
|
|
84
|
+
import {
|
|
85
|
+
registerTaskAsync,
|
|
86
|
+
BackgroundTaskResult,
|
|
87
|
+
} from '@symbiote-native/background-task';
|
|
88
|
+
|
|
89
|
+
const SYNC_TASK = 'background-sync';
|
|
90
|
+
|
|
91
|
+
defineTask(SYNC_TASK, async () => {
|
|
92
|
+
try {
|
|
93
|
+
await runSync();
|
|
94
|
+
return BackgroundTaskResult.Success;
|
|
95
|
+
} catch (error) {
|
|
96
|
+
console.error('background-sync failed:', error);
|
|
97
|
+
return BackgroundTaskResult.Failed;
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Somewhere after the task is defined (a settings screen, app bootstrap, …):
|
|
102
|
+
await registerTaskAsync(SYNC_TASK, { minimumInterval: 15 });
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`@symbiote-native/task-manager` is the primitive both this package and
|
|
106
|
+
[`@symbiote-native/background-fetch`](../background-fetch) build on — `defineTask` lives there,
|
|
107
|
+
`registerTaskAsync`/`unregisterTaskAsync` live here. `defineTask` must run at the top of the JS
|
|
108
|
+
bundle, outside any component, for the same reason documented in task-manager's own README: the
|
|
109
|
+
app can be launched headlessly to run a background task, with no views mounted.
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
import {
|
|
113
|
+
getStatusAsync,
|
|
114
|
+
unregisterTaskAsync,
|
|
115
|
+
addExpirationListener,
|
|
116
|
+
} from '@symbiote-native/background-task';
|
|
117
|
+
|
|
118
|
+
const status = await getStatusAsync();
|
|
119
|
+
|
|
120
|
+
// iOS only — the system can interrupt a running background task before it finishes.
|
|
121
|
+
const subscription = addExpirationListener(() => {
|
|
122
|
+
console.warn('background-sync was interrupted before it finished');
|
|
123
|
+
});
|
|
124
|
+
subscription.remove();
|
|
125
|
+
|
|
126
|
+
await unregisterTaskAsync(SYNC_TASK); // stop receiving executions of it
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Identical import surface on every adapter — `@symbiote-native/background-task/react`,
|
|
130
|
+
`/vue`, `/svelte`, `/solid`, `/angular` all re-export the same functions.
|
|
131
|
+
|
|
132
|
+
## API
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
getStatusAsync(): Promise<BackgroundTaskStatus>
|
|
136
|
+
registerTaskAsync(taskName: string, options?: IBackgroundTaskOptions): Promise<void>
|
|
137
|
+
unregisterTaskAsync(taskName: string): Promise<void>
|
|
138
|
+
triggerTaskWorkerForTestingAsync(): Promise<boolean>
|
|
139
|
+
addExpirationListener(listener: () => void): { remove: () => void }
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Plus `BackgroundTaskStatus`, `BackgroundTaskResult`, `IBackgroundTaskOptions` — ported from
|
|
143
|
+
upstream's `BackgroundTask.types.ts`, the options type renamed with this repo's `I`-prefix
|
|
144
|
+
convention for exported types (`ts-js-best-practices`).
|
|
145
|
+
|
|
146
|
+
## Notes
|
|
147
|
+
|
|
148
|
+
- **`registerTaskAsync` requires the task to already be defined.** It throws if
|
|
149
|
+
`@symbiote-native/task-manager`'s `isTaskDefined(taskName)` is `false` — call `defineTask`
|
|
150
|
+
first.
|
|
151
|
+
- **`registerTaskAsync` is a no-op, twice over.** It skips silently (with a one-time console
|
|
152
|
+
warning) when the environment reports `BackgroundTaskStatus.Restricted` — the iOS Simulator has
|
|
153
|
+
no `BGTaskScheduler` support at all — and it skips again, quietly, when the task is already
|
|
154
|
+
registered (checked via `@symbiote-native/task-manager`'s `isTaskRegisteredAsync`).
|
|
155
|
+
- **`triggerTaskWorkerForTestingAsync` only runs in a dev build.** It always resolves `false` in
|
|
156
|
+
production, matching upstream's own `__DEV__` gate — read here through a narrow local type
|
|
157
|
+
rather than the bare RN global, since this package's own type graph never imports
|
|
158
|
+
`react-native`.
|
|
159
|
+
- **Expo Go is out of scope.** Upstream also warns when running inside Expo Go
|
|
160
|
+
(`isRunningInExpoGo`, imported from the `expo` meta-package) and reports
|
|
161
|
+
`BackgroundTaskStatus.Restricted` there. This project never installs `expo` — every app here is
|
|
162
|
+
a bare/dev-client build, never Expo Go — so that branch has no equivalent here and is
|
|
163
|
+
intentionally not ported.
|
|
164
|
+
|
|
165
|
+
## Test it
|
|
166
|
+
|
|
167
|
+
No Fabric/Descriptor angle at all — every function here is a pure async-function surface plus one
|
|
168
|
+
event subscription, never a view or per-instance state. Tests inject a fake native-module object
|
|
169
|
+
in place of the real `requireNativeModule` resolution and a fake `@symbiote-native/task-manager`
|
|
170
|
+
module (`src/core/background-task.test.ts`) — no `installFabric()`, no ViewConfig.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '../core';
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { BackgroundTaskStatus } from './types';
|
|
2
|
+
import type { IBackgroundTaskOptions } from './types';
|
|
3
|
+
/** Gets the current status for the Background Task API. */
|
|
4
|
+
export declare function getStatusAsync(): Promise<BackgroundTaskStatus>;
|
|
5
|
+
/**
|
|
6
|
+
* Registers a background task with the given name. The task must already be defined via
|
|
7
|
+
* `@symbiote-native/task-manager`'s `defineTask` — registration is driven by this package, but
|
|
8
|
+
* execution dispatch is task-manager's job.
|
|
9
|
+
*/
|
|
10
|
+
export declare function registerTaskAsync(taskName: string, options?: IBackgroundTaskOptions): Promise<void>;
|
|
11
|
+
/** Unregisters a background task, so the app stops receiving executions of it. */
|
|
12
|
+
export declare function unregisterTaskAsync(taskName: string): Promise<void>;
|
|
13
|
+
/**
|
|
14
|
+
* Debug builds only: triggers the OS to run the registered background task immediately, instead
|
|
15
|
+
* of waiting for its real schedule. Always resolves `false` in a production build.
|
|
16
|
+
*/
|
|
17
|
+
export declare function triggerTaskWorkerForTestingAsync(): Promise<boolean>;
|
|
18
|
+
/**
|
|
19
|
+
* Subscribes to the `onTasksExpired` event iOS fires when the system interrupts a running
|
|
20
|
+
* background task before it finishes — use it to clean up resources or save state.
|
|
21
|
+
* @platform ios
|
|
22
|
+
*/
|
|
23
|
+
export declare function addExpirationListener(listener: () => void): {
|
|
24
|
+
remove: () => void;
|
|
25
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { Platform, UnavailabilityError } from 'expo-modules-core';
|
|
2
|
+
import { isTaskDefined, isTaskRegisteredAsync, } from '@symbiote-native/task-manager';
|
|
3
|
+
import { expoBackgroundTask } from './native-module.js';
|
|
4
|
+
import { BackgroundTaskStatus } from './types.js';
|
|
5
|
+
const NATIVE_MODULE_NAME = 'BackgroundTask';
|
|
6
|
+
let didWarnUnsupportedEnvironment = false;
|
|
7
|
+
function assertValidTaskName(taskName) {
|
|
8
|
+
if (!taskName || typeof taskName !== 'string') {
|
|
9
|
+
throw new TypeError('`taskName` must be a non-empty string.');
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function isDevBuild() {
|
|
13
|
+
return Boolean(globalThis.__DEV__);
|
|
14
|
+
}
|
|
15
|
+
// Upstream also warns when running inside Expo Go (`isRunningInExpoGo`, imported from the `expo`
|
|
16
|
+
// meta-package). This project never installs `expo` — every app here is a bare/dev-client build,
|
|
17
|
+
// never Expo Go — so that check, and the `BackgroundTaskStatus.Restricted` branch it drives in
|
|
18
|
+
// upstream's own `getStatusAsync`, has no equivalent and is intentionally not ported.
|
|
19
|
+
/** Gets the current status for the Background Task API. */
|
|
20
|
+
export async function getStatusAsync() {
|
|
21
|
+
if (!expoBackgroundTask.getStatusAsync) {
|
|
22
|
+
throw new UnavailabilityError(NATIVE_MODULE_NAME, 'getStatusAsync');
|
|
23
|
+
}
|
|
24
|
+
return expoBackgroundTask.getStatusAsync();
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Registers a background task with the given name. The task must already be defined via
|
|
28
|
+
* `@symbiote-native/task-manager`'s `defineTask` — registration is driven by this package, but
|
|
29
|
+
* execution dispatch is task-manager's job.
|
|
30
|
+
*/
|
|
31
|
+
export async function registerTaskAsync(taskName, options = {}) {
|
|
32
|
+
if (!expoBackgroundTask.registerTaskAsync) {
|
|
33
|
+
throw new UnavailabilityError(NATIVE_MODULE_NAME, 'registerTaskAsync');
|
|
34
|
+
}
|
|
35
|
+
if (!isTaskDefined(taskName)) {
|
|
36
|
+
throw new Error(`Task '${taskName}' is not defined. You must define a task using defineTask (from @symbiote-native/task-manager) before registering.`);
|
|
37
|
+
}
|
|
38
|
+
if ((await getStatusAsync()) === BackgroundTaskStatus.Restricted) {
|
|
39
|
+
if (!didWarnUnsupportedEnvironment) {
|
|
40
|
+
didWarnUnsupportedEnvironment = true;
|
|
41
|
+
const message = Platform.OS === 'ios'
|
|
42
|
+
? `Background tasks are not supported on iOS simulators. Skipped registering task: ${taskName}.`
|
|
43
|
+
: `Background tasks are not available in the current environment. Skipped registering task: ${taskName}.`;
|
|
44
|
+
console.warn(message);
|
|
45
|
+
}
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
assertValidTaskName(taskName);
|
|
49
|
+
if (await isTaskRegisteredAsync(taskName))
|
|
50
|
+
return;
|
|
51
|
+
await expoBackgroundTask.registerTaskAsync(taskName, options);
|
|
52
|
+
}
|
|
53
|
+
/** Unregisters a background task, so the app stops receiving executions of it. */
|
|
54
|
+
export async function unregisterTaskAsync(taskName) {
|
|
55
|
+
if (!expoBackgroundTask.unregisterTaskAsync) {
|
|
56
|
+
throw new UnavailabilityError(NATIVE_MODULE_NAME, 'unregisterTaskAsync');
|
|
57
|
+
}
|
|
58
|
+
assertValidTaskName(taskName);
|
|
59
|
+
if (!(await isTaskRegisteredAsync(taskName)))
|
|
60
|
+
return;
|
|
61
|
+
await expoBackgroundTask.unregisterTaskAsync(taskName);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Debug builds only: triggers the OS to run the registered background task immediately, instead
|
|
65
|
+
* of waiting for its real schedule. Always resolves `false` in a production build.
|
|
66
|
+
*/
|
|
67
|
+
export async function triggerTaskWorkerForTestingAsync() {
|
|
68
|
+
if (!isDevBuild())
|
|
69
|
+
return false;
|
|
70
|
+
if (!expoBackgroundTask.triggerTaskWorkerForTestingAsync) {
|
|
71
|
+
throw new UnavailabilityError(NATIVE_MODULE_NAME, 'triggerTaskWorkerForTestingAsync');
|
|
72
|
+
}
|
|
73
|
+
return expoBackgroundTask.triggerTaskWorkerForTestingAsync();
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Subscribes to the `onTasksExpired` event iOS fires when the system interrupts a running
|
|
77
|
+
* background task before it finishes — use it to clean up resources or save state.
|
|
78
|
+
* @platform ios
|
|
79
|
+
*/
|
|
80
|
+
export function addExpirationListener(listener) {
|
|
81
|
+
if (!expoBackgroundTask.addListener) {
|
|
82
|
+
throw new UnavailabilityError(NATIVE_MODULE_NAME, 'addListener');
|
|
83
|
+
}
|
|
84
|
+
return expoBackgroundTask.addListener('onTasksExpired', listener);
|
|
85
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { getStatusAsync, registerTaskAsync, unregisterTaskAsync, triggerTaskWorkerForTestingAsync, addExpirationListener, } from './background-task';
|
|
2
|
+
export { BackgroundTaskStatus, BackgroundTaskResult } from './types';
|
|
3
|
+
export type { IBackgroundTaskOptions } from './types';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type EventSubscription } from 'expo-modules-core';
|
|
2
|
+
export type INativeBackgroundTaskModule = {
|
|
3
|
+
getStatusAsync?(): Promise<number>;
|
|
4
|
+
registerTaskAsync?(taskName: string, options: Record<string, unknown>): Promise<void>;
|
|
5
|
+
unregisterTaskAsync?(taskName: string): Promise<void>;
|
|
6
|
+
/** Debug-build-only: forces the OS to run the registered background task immediately. */
|
|
7
|
+
triggerTaskWorkerForTestingAsync?(): Promise<boolean>;
|
|
8
|
+
addListener?(eventName: string, listener: () => void): EventSubscription;
|
|
9
|
+
};
|
|
10
|
+
export declare const expoBackgroundTask: INativeBackgroundTaskModule;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** Availability status for the Background Task API. */
|
|
2
|
+
export declare enum BackgroundTaskStatus {
|
|
3
|
+
/** Background tasks are unavailable — e.g. running on an iOS Simulator. */
|
|
4
|
+
Restricted = 1,
|
|
5
|
+
/** Background tasks are available for the app. */
|
|
6
|
+
Available = 2
|
|
7
|
+
}
|
|
8
|
+
/** Return value a background-task executor should resolve with. */
|
|
9
|
+
export declare enum BackgroundTaskResult {
|
|
10
|
+
/** The task finished successfully. */
|
|
11
|
+
Success = 1,
|
|
12
|
+
/** The task failed. */
|
|
13
|
+
Failed = 2
|
|
14
|
+
}
|
|
15
|
+
/** Options accepted by {@link registerTaskAsync}. */
|
|
16
|
+
export type IBackgroundTaskOptions = {
|
|
17
|
+
/**
|
|
18
|
+
* Inexact interval in minutes between subsequent repeats of the background task. The final
|
|
19
|
+
* interval may differ from the specified one to minimize wakeups and battery usage.
|
|
20
|
+
* - Defaults to once every 12 hours; the minimum interval is 15 minutes.
|
|
21
|
+
* - The OS controls the real execution interval and treats this as a minimum delay only — on
|
|
22
|
+
* iOS a short interval is often ignored, since the system typically runs background tasks
|
|
23
|
+
* during specific windows (e.g. overnight).
|
|
24
|
+
*/
|
|
25
|
+
minimumInterval?: number;
|
|
26
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Availability status for the Background Task API. */
|
|
2
|
+
export var BackgroundTaskStatus;
|
|
3
|
+
(function (BackgroundTaskStatus) {
|
|
4
|
+
/** Background tasks are unavailable — e.g. running on an iOS Simulator. */
|
|
5
|
+
BackgroundTaskStatus[BackgroundTaskStatus["Restricted"] = 1] = "Restricted";
|
|
6
|
+
/** Background tasks are available for the app. */
|
|
7
|
+
BackgroundTaskStatus[BackgroundTaskStatus["Available"] = 2] = "Available";
|
|
8
|
+
})(BackgroundTaskStatus || (BackgroundTaskStatus = {}));
|
|
9
|
+
/** Return value a background-task executor should resolve with. */
|
|
10
|
+
export var BackgroundTaskResult;
|
|
11
|
+
(function (BackgroundTaskResult) {
|
|
12
|
+
/** The task finished successfully. */
|
|
13
|
+
BackgroundTaskResult[BackgroundTaskResult["Success"] = 1] = "Success";
|
|
14
|
+
/** The task failed. */
|
|
15
|
+
BackgroundTaskResult[BackgroundTaskResult["Failed"] = 2] = "Failed";
|
|
16
|
+
})(BackgroundTaskResult || (BackgroundTaskResult = {}));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '../core';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// @symbiote-native/background-task/angular: the Angular entry over the framework-agnostic core.
|
|
2
|
+
// Same reasoning as the React/Vue entries — no per-instance state to wrap in a service, so this
|
|
3
|
+
// 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,gGAAgG;AAChG,gGAAgG;AAChG,wBAAwB;AACxB,cAAc,SAAS,CAAC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { BackgroundTaskStatus } from './types';
|
|
2
|
+
import type { IBackgroundTaskOptions } from './types';
|
|
3
|
+
/** Gets the current status for the Background Task API. */
|
|
4
|
+
export declare function getStatusAsync(): Promise<BackgroundTaskStatus>;
|
|
5
|
+
/**
|
|
6
|
+
* Registers a background task with the given name. The task must already be defined via
|
|
7
|
+
* `@symbiote-native/task-manager`'s `defineTask` — registration is driven by this package, but
|
|
8
|
+
* execution dispatch is task-manager's job.
|
|
9
|
+
*/
|
|
10
|
+
export declare function registerTaskAsync(taskName: string, options?: IBackgroundTaskOptions): Promise<void>;
|
|
11
|
+
/** Unregisters a background task, so the app stops receiving executions of it. */
|
|
12
|
+
export declare function unregisterTaskAsync(taskName: string): Promise<void>;
|
|
13
|
+
/**
|
|
14
|
+
* Debug builds only: triggers the OS to run the registered background task immediately, instead
|
|
15
|
+
* of waiting for its real schedule. Always resolves `false` in a production build.
|
|
16
|
+
*/
|
|
17
|
+
export declare function triggerTaskWorkerForTestingAsync(): Promise<boolean>;
|
|
18
|
+
/**
|
|
19
|
+
* Subscribes to the `onTasksExpired` event iOS fires when the system interrupts a running
|
|
20
|
+
* background task before it finishes — use it to clean up resources or save state.
|
|
21
|
+
* @platform ios
|
|
22
|
+
*/
|
|
23
|
+
export declare function addExpirationListener(listener: () => void): {
|
|
24
|
+
remove: () => void;
|
|
25
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { Platform, UnavailabilityError } from 'expo-modules-core';
|
|
2
|
+
import { isTaskDefined, isTaskRegisteredAsync, } from '@symbiote-native/task-manager';
|
|
3
|
+
import { expoBackgroundTask } from './native-module';
|
|
4
|
+
import { BackgroundTaskStatus } from './types';
|
|
5
|
+
const NATIVE_MODULE_NAME = 'BackgroundTask';
|
|
6
|
+
let didWarnUnsupportedEnvironment = false;
|
|
7
|
+
function assertValidTaskName(taskName) {
|
|
8
|
+
if (!taskName || typeof taskName !== 'string') {
|
|
9
|
+
throw new TypeError('`taskName` must be a non-empty string.');
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function isDevBuild() {
|
|
13
|
+
return Boolean(globalThis.__DEV__);
|
|
14
|
+
}
|
|
15
|
+
// Upstream also warns when running inside Expo Go (`isRunningInExpoGo`, imported from the `expo`
|
|
16
|
+
// meta-package). This project never installs `expo` — every app here is a bare/dev-client build,
|
|
17
|
+
// never Expo Go — so that check, and the `BackgroundTaskStatus.Restricted` branch it drives in
|
|
18
|
+
// upstream's own `getStatusAsync`, has no equivalent and is intentionally not ported.
|
|
19
|
+
/** Gets the current status for the Background Task API. */
|
|
20
|
+
export async function getStatusAsync() {
|
|
21
|
+
if (!expoBackgroundTask.getStatusAsync) {
|
|
22
|
+
throw new UnavailabilityError(NATIVE_MODULE_NAME, 'getStatusAsync');
|
|
23
|
+
}
|
|
24
|
+
return expoBackgroundTask.getStatusAsync();
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Registers a background task with the given name. The task must already be defined via
|
|
28
|
+
* `@symbiote-native/task-manager`'s `defineTask` — registration is driven by this package, but
|
|
29
|
+
* execution dispatch is task-manager's job.
|
|
30
|
+
*/
|
|
31
|
+
export async function registerTaskAsync(taskName, options = {}) {
|
|
32
|
+
if (!expoBackgroundTask.registerTaskAsync) {
|
|
33
|
+
throw new UnavailabilityError(NATIVE_MODULE_NAME, 'registerTaskAsync');
|
|
34
|
+
}
|
|
35
|
+
if (!isTaskDefined(taskName)) {
|
|
36
|
+
throw new Error(`Task '${taskName}' is not defined. You must define a task using defineTask (from @symbiote-native/task-manager) before registering.`);
|
|
37
|
+
}
|
|
38
|
+
if ((await getStatusAsync()) === BackgroundTaskStatus.Restricted) {
|
|
39
|
+
if (!didWarnUnsupportedEnvironment) {
|
|
40
|
+
didWarnUnsupportedEnvironment = true;
|
|
41
|
+
const message = Platform.OS === 'ios'
|
|
42
|
+
? `Background tasks are not supported on iOS simulators. Skipped registering task: ${taskName}.`
|
|
43
|
+
: `Background tasks are not available in the current environment. Skipped registering task: ${taskName}.`;
|
|
44
|
+
console.warn(message);
|
|
45
|
+
}
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
assertValidTaskName(taskName);
|
|
49
|
+
if (await isTaskRegisteredAsync(taskName))
|
|
50
|
+
return;
|
|
51
|
+
await expoBackgroundTask.registerTaskAsync(taskName, options);
|
|
52
|
+
}
|
|
53
|
+
/** Unregisters a background task, so the app stops receiving executions of it. */
|
|
54
|
+
export async function unregisterTaskAsync(taskName) {
|
|
55
|
+
if (!expoBackgroundTask.unregisterTaskAsync) {
|
|
56
|
+
throw new UnavailabilityError(NATIVE_MODULE_NAME, 'unregisterTaskAsync');
|
|
57
|
+
}
|
|
58
|
+
assertValidTaskName(taskName);
|
|
59
|
+
if (!(await isTaskRegisteredAsync(taskName)))
|
|
60
|
+
return;
|
|
61
|
+
await expoBackgroundTask.unregisterTaskAsync(taskName);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Debug builds only: triggers the OS to run the registered background task immediately, instead
|
|
65
|
+
* of waiting for its real schedule. Always resolves `false` in a production build.
|
|
66
|
+
*/
|
|
67
|
+
export async function triggerTaskWorkerForTestingAsync() {
|
|
68
|
+
if (!isDevBuild())
|
|
69
|
+
return false;
|
|
70
|
+
if (!expoBackgroundTask.triggerTaskWorkerForTestingAsync) {
|
|
71
|
+
throw new UnavailabilityError(NATIVE_MODULE_NAME, 'triggerTaskWorkerForTestingAsync');
|
|
72
|
+
}
|
|
73
|
+
return expoBackgroundTask.triggerTaskWorkerForTestingAsync();
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Subscribes to the `onTasksExpired` event iOS fires when the system interrupts a running
|
|
77
|
+
* background task before it finishes — use it to clean up resources or save state.
|
|
78
|
+
* @platform ios
|
|
79
|
+
*/
|
|
80
|
+
export function addExpirationListener(listener) {
|
|
81
|
+
if (!expoBackgroundTask.addListener) {
|
|
82
|
+
throw new UnavailabilityError(NATIVE_MODULE_NAME, 'addListener');
|
|
83
|
+
}
|
|
84
|
+
return expoBackgroundTask.addListener('onTasksExpired', listener);
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=background-task.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"background-task.js","sourceRoot":"","sources":["../../src/core/background-task.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { getStatusAsync, registerTaskAsync, unregisterTaskAsync, triggerTaskWorkerForTestingAsync, addExpirationListener, } from './background-task';
|
|
2
|
+
export { BackgroundTaskStatus, BackgroundTaskResult } from './types';
|
|
3
|
+
export type { IBackgroundTaskOptions } from './types';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,iBAAiB,EACjB,mBAAmB,EACnB,gCAAgC,EAChC,qBAAqB,GACtB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type EventSubscription } from 'expo-modules-core';
|
|
2
|
+
export type INativeBackgroundTaskModule = {
|
|
3
|
+
getStatusAsync?(): Promise<number>;
|
|
4
|
+
registerTaskAsync?(taskName: string, options: Record<string, unknown>): Promise<void>;
|
|
5
|
+
unregisterTaskAsync?(taskName: string): Promise<void>;
|
|
6
|
+
/** Debug-build-only: forces the OS to run the registered background task immediately. */
|
|
7
|
+
triggerTaskWorkerForTestingAsync?(): Promise<boolean>;
|
|
8
|
+
addListener?(eventName: string, listener: () => void): EventSubscription;
|
|
9
|
+
};
|
|
10
|
+
export declare const expoBackgroundTask: INativeBackgroundTaskModule;
|
|
@@ -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;AAEhF,MAAM,gCAAgC,GAAG,oBAAoB,CAAC;AAqB9D,MAAM,CAAC,MAAM,kBAAkB,GAC7B,mBAAmB,CACjB,gCAAgC,CACjC,CAAC"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** Availability status for the Background Task API. */
|
|
2
|
+
export declare enum BackgroundTaskStatus {
|
|
3
|
+
/** Background tasks are unavailable — e.g. running on an iOS Simulator. */
|
|
4
|
+
Restricted = 1,
|
|
5
|
+
/** Background tasks are available for the app. */
|
|
6
|
+
Available = 2
|
|
7
|
+
}
|
|
8
|
+
/** Return value a background-task executor should resolve with. */
|
|
9
|
+
export declare enum BackgroundTaskResult {
|
|
10
|
+
/** The task finished successfully. */
|
|
11
|
+
Success = 1,
|
|
12
|
+
/** The task failed. */
|
|
13
|
+
Failed = 2
|
|
14
|
+
}
|
|
15
|
+
/** Options accepted by {@link registerTaskAsync}. */
|
|
16
|
+
export type IBackgroundTaskOptions = {
|
|
17
|
+
/**
|
|
18
|
+
* Inexact interval in minutes between subsequent repeats of the background task. The final
|
|
19
|
+
* interval may differ from the specified one to minimize wakeups and battery usage.
|
|
20
|
+
* - Defaults to once every 12 hours; the minimum interval is 15 minutes.
|
|
21
|
+
* - The OS controls the real execution interval and treats this as a minimum delay only — on
|
|
22
|
+
* iOS a short interval is often ignored, since the system typically runs background tasks
|
|
23
|
+
* during specific windows (e.g. overnight).
|
|
24
|
+
*/
|
|
25
|
+
minimumInterval?: number;
|
|
26
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Availability status for the Background Task API. */
|
|
2
|
+
export var BackgroundTaskStatus;
|
|
3
|
+
(function (BackgroundTaskStatus) {
|
|
4
|
+
/** Background tasks are unavailable — e.g. running on an iOS Simulator. */
|
|
5
|
+
BackgroundTaskStatus[BackgroundTaskStatus["Restricted"] = 1] = "Restricted";
|
|
6
|
+
/** Background tasks are available for the app. */
|
|
7
|
+
BackgroundTaskStatus[BackgroundTaskStatus["Available"] = 2] = "Available";
|
|
8
|
+
})(BackgroundTaskStatus || (BackgroundTaskStatus = {}));
|
|
9
|
+
/** Return value a background-task executor should resolve with. */
|
|
10
|
+
export var BackgroundTaskResult;
|
|
11
|
+
(function (BackgroundTaskResult) {
|
|
12
|
+
/** The task finished successfully. */
|
|
13
|
+
BackgroundTaskResult[BackgroundTaskResult["Success"] = 1] = "Success";
|
|
14
|
+
/** The task failed. */
|
|
15
|
+
BackgroundTaskResult[BackgroundTaskResult["Failed"] = 2] = "Failed";
|
|
16
|
+
})(BackgroundTaskResult || (BackgroundTaskResult = {}));
|
|
17
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":"AAAA,uDAAuD;AACvD,MAAM,CAAN,IAAY,oBAKX;AALD,WAAY,oBAAoB;IAC9B,2EAA2E;IAC3E,2EAAc,CAAA;IACd,kDAAkD;IAClD,yEAAa,CAAA;AACf,CAAC,EALW,oBAAoB,KAApB,oBAAoB,QAK/B;AAED,mEAAmE;AACnE,MAAM,CAAN,IAAY,oBAKX;AALD,WAAY,oBAAoB;IAC9B,sCAAsC;IACtC,qEAAW,CAAA;IACX,uBAAuB;IACvB,mEAAU,CAAA;AACZ,CAAC,EALW,oBAAoB,KAApB,oBAAoB,QAK/B"}
|
package/native-link.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"android": {
|
|
3
|
+
"gradleProjectName": "expo-background-task",
|
|
4
|
+
"modules": [
|
|
5
|
+
{
|
|
6
|
+
"importPath": "expo.modules.backgroundtask.BackgroundTaskModule",
|
|
7
|
+
"className": "BackgroundTaskModule",
|
|
8
|
+
"nativeName": "ExpoBackgroundTask"
|
|
9
|
+
}
|
|
10
|
+
]
|
|
11
|
+
},
|
|
12
|
+
"ios": {
|
|
13
|
+
"infoPlistArrayKeys": {
|
|
14
|
+
"UIBackgroundModes": ["processing"],
|
|
15
|
+
"BGTaskSchedulerPermittedIdentifiers": [
|
|
16
|
+
"com.expo.modules.backgroundtask.processing"
|
|
17
|
+
]
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@symbiote-native/background-task",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "expo-background-task wrapped for SymbioteNative — one framework-agnostic core, built once and reachable from the React, Vue, Svelte, Solid, and Angular adapters. Periodic background work via BGTaskScheduler (iOS) / WorkManager (Android), registered through @symbiote-native/task-manager.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/OneEyed1366/symbiote-native.git",
|
|
9
|
+
"directory": "packages/background-task"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/OneEyed1366/symbiote-native/tree/master/packages/background-task#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-background-task",
|
|
20
|
+
"react",
|
|
21
|
+
"vue",
|
|
22
|
+
"svelte",
|
|
23
|
+
"solid",
|
|
24
|
+
"angular",
|
|
25
|
+
"background",
|
|
26
|
+
"task",
|
|
27
|
+
"workmanager",
|
|
28
|
+
"bgtaskscheduler"
|
|
29
|
+
],
|
|
30
|
+
"type": "module",
|
|
31
|
+
"main": "./build/core/index.js",
|
|
32
|
+
"module": "./build/core/index.js",
|
|
33
|
+
"types": "./build/core/index.d.ts",
|
|
34
|
+
"exports": {
|
|
35
|
+
".": {
|
|
36
|
+
"types": "./build/core/index.d.ts",
|
|
37
|
+
"default": "./build/core/index.js"
|
|
38
|
+
},
|
|
39
|
+
"./vue": {
|
|
40
|
+
"types": "./build/core/index.d.ts",
|
|
41
|
+
"default": "./build/core/index.js"
|
|
42
|
+
},
|
|
43
|
+
"./react": {
|
|
44
|
+
"types": "./build/core/index.d.ts",
|
|
45
|
+
"default": "./build/core/index.js"
|
|
46
|
+
},
|
|
47
|
+
"./svelte": {
|
|
48
|
+
"types": "./build/core/index.d.ts",
|
|
49
|
+
"default": "./build/core/index.js"
|
|
50
|
+
},
|
|
51
|
+
"./solid": {
|
|
52
|
+
"types": "./build/core/index.d.ts",
|
|
53
|
+
"default": "./build/core/index.js"
|
|
54
|
+
},
|
|
55
|
+
"./angular": {
|
|
56
|
+
"types": "./build-ngc/angular/index.d.ts",
|
|
57
|
+
"react-native": "./build-ngc/angular/index.js",
|
|
58
|
+
"default": "./src/angular/index.ts"
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"files": [
|
|
62
|
+
"src",
|
|
63
|
+
"build",
|
|
64
|
+
"build-ngc",
|
|
65
|
+
"native-link.json",
|
|
66
|
+
"!src/**/*.test.*",
|
|
67
|
+
"!src/**/*.spec.*",
|
|
68
|
+
"!src/**/*.detox.*"
|
|
69
|
+
],
|
|
70
|
+
"publishConfig": {
|
|
71
|
+
"access": "public"
|
|
72
|
+
},
|
|
73
|
+
"dependencies": {
|
|
74
|
+
"expo-background-task": "57.0.15",
|
|
75
|
+
"expo-modules-core": "57.0.5",
|
|
76
|
+
"@symbiote-native/task-manager": "0.1.0"
|
|
77
|
+
},
|
|
78
|
+
"peerDependencies": {
|
|
79
|
+
"@angular/core": ">=20",
|
|
80
|
+
"@vue/runtime-core": "^3.5.13",
|
|
81
|
+
"react": ">=19.0.0",
|
|
82
|
+
"react-native": ">=0.86",
|
|
83
|
+
"solid-js": ">=1.9.0",
|
|
84
|
+
"svelte": ">=5.56.0",
|
|
85
|
+
"vue": ">=3.5.0",
|
|
86
|
+
"@symbiote-native/angular": "^3.1.0",
|
|
87
|
+
"@symbiote-native/engine": "^1.3.0",
|
|
88
|
+
"@symbiote-native/react": "^3.0.2",
|
|
89
|
+
"@symbiote-native/solid": "^3.0.2",
|
|
90
|
+
"@symbiote-native/svelte": "^3.0.2",
|
|
91
|
+
"@symbiote-native/vue": "^3.0.2"
|
|
92
|
+
},
|
|
93
|
+
"peerDependenciesMeta": {
|
|
94
|
+
"@symbiote-native/angular": {
|
|
95
|
+
"optional": true
|
|
96
|
+
},
|
|
97
|
+
"@symbiote-native/react": {
|
|
98
|
+
"optional": true
|
|
99
|
+
},
|
|
100
|
+
"@symbiote-native/solid": {
|
|
101
|
+
"optional": true
|
|
102
|
+
},
|
|
103
|
+
"@symbiote-native/svelte": {
|
|
104
|
+
"optional": true
|
|
105
|
+
},
|
|
106
|
+
"@symbiote-native/vue": {
|
|
107
|
+
"optional": true
|
|
108
|
+
},
|
|
109
|
+
"@angular/core": {
|
|
110
|
+
"optional": true
|
|
111
|
+
},
|
|
112
|
+
"@vue/runtime-core": {
|
|
113
|
+
"optional": true
|
|
114
|
+
},
|
|
115
|
+
"react": {
|
|
116
|
+
"optional": true
|
|
117
|
+
},
|
|
118
|
+
"solid-js": {
|
|
119
|
+
"optional": true
|
|
120
|
+
},
|
|
121
|
+
"svelte": {
|
|
122
|
+
"optional": true
|
|
123
|
+
},
|
|
124
|
+
"vue": {
|
|
125
|
+
"optional": true
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
"devDependencies": {
|
|
129
|
+
"@angular/compiler": "~22.0.8",
|
|
130
|
+
"@angular/compiler-cli": "~22.0.8",
|
|
131
|
+
"@angular/core": "~22.0.8",
|
|
132
|
+
"@types/node": "^26.0.0",
|
|
133
|
+
"@types/react": "^19.2.0",
|
|
134
|
+
"@vue/runtime-core": "^3.5.13",
|
|
135
|
+
"react": "19.2.3",
|
|
136
|
+
"solid-js": "^1.9.14",
|
|
137
|
+
"svelte": "^5.56.0",
|
|
138
|
+
"typescript": "~6.0.0",
|
|
139
|
+
"@symbiote-native/react": "3.0.2",
|
|
140
|
+
"@symbiote-native/angular": "3.1.0",
|
|
141
|
+
"@symbiote-native/engine": "1.3.0",
|
|
142
|
+
"@symbiote-native/svelte": "3.0.2",
|
|
143
|
+
"@symbiote-native/vue": "3.0.2",
|
|
144
|
+
"@symbiote-native/test-utils": "0.4.2",
|
|
145
|
+
"@symbiote-native/solid": "3.0.2"
|
|
146
|
+
},
|
|
147
|
+
"scripts": {
|
|
148
|
+
"typecheck": "tsc --build",
|
|
149
|
+
"clean": "rm -rf build-ngc",
|
|
150
|
+
"ng:build": "pnpm run clean && ngc -p tsconfig.angular.json",
|
|
151
|
+
"format": "prettier --write \"src/**/*.{ts,tsx}\""
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { Platform, UnavailabilityError } from 'expo-modules-core';
|
|
2
|
+
import {
|
|
3
|
+
isTaskDefined,
|
|
4
|
+
isTaskRegisteredAsync,
|
|
5
|
+
} from '@symbiote-native/task-manager';
|
|
6
|
+
import { expoBackgroundTask } from './native-module';
|
|
7
|
+
import { BackgroundTaskStatus } from './types';
|
|
8
|
+
import type { IBackgroundTaskOptions } from './types';
|
|
9
|
+
|
|
10
|
+
const NATIVE_MODULE_NAME = 'BackgroundTask';
|
|
11
|
+
|
|
12
|
+
let didWarnUnsupportedEnvironment = false;
|
|
13
|
+
|
|
14
|
+
function assertValidTaskName(taskName: unknown): asserts taskName is string {
|
|
15
|
+
if (!taskName || typeof taskName !== 'string') {
|
|
16
|
+
throw new TypeError('`taskName` must be a non-empty string.');
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Metro/Hermes define a global `__DEV__` boolean; nothing in this package's own type graph does
|
|
21
|
+
// (react-native is a peer, never imported here), so read it through a narrow local type instead
|
|
22
|
+
// of a bare identifier `tsc` would reject as undeclared. Resolves to `false` — i.e. "production
|
|
23
|
+
// build" — in any environment that never sets it (a plain Node/Vitest run included), matching
|
|
24
|
+
// upstream's own use of the identical global.
|
|
25
|
+
type IDevGlobal = { __DEV__?: boolean };
|
|
26
|
+
function isDevBuild(): boolean {
|
|
27
|
+
return Boolean((globalThis as IDevGlobal).__DEV__);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Upstream also warns when running inside Expo Go (`isRunningInExpoGo`, imported from the `expo`
|
|
31
|
+
// meta-package). This project never installs `expo` — every app here is a bare/dev-client build,
|
|
32
|
+
// never Expo Go — so that check, and the `BackgroundTaskStatus.Restricted` branch it drives in
|
|
33
|
+
// upstream's own `getStatusAsync`, has no equivalent and is intentionally not ported.
|
|
34
|
+
|
|
35
|
+
/** Gets the current status for the Background Task API. */
|
|
36
|
+
export async function getStatusAsync(): Promise<BackgroundTaskStatus> {
|
|
37
|
+
if (!expoBackgroundTask.getStatusAsync) {
|
|
38
|
+
throw new UnavailabilityError(NATIVE_MODULE_NAME, 'getStatusAsync');
|
|
39
|
+
}
|
|
40
|
+
return expoBackgroundTask.getStatusAsync() as Promise<BackgroundTaskStatus>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Registers a background task with the given name. The task must already be defined via
|
|
45
|
+
* `@symbiote-native/task-manager`'s `defineTask` — registration is driven by this package, but
|
|
46
|
+
* execution dispatch is task-manager's job.
|
|
47
|
+
*/
|
|
48
|
+
export async function registerTaskAsync(
|
|
49
|
+
taskName: string,
|
|
50
|
+
options: IBackgroundTaskOptions = {},
|
|
51
|
+
): Promise<void> {
|
|
52
|
+
if (!expoBackgroundTask.registerTaskAsync) {
|
|
53
|
+
throw new UnavailabilityError(NATIVE_MODULE_NAME, 'registerTaskAsync');
|
|
54
|
+
}
|
|
55
|
+
if (!isTaskDefined(taskName)) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`Task '${taskName}' is not defined. You must define a task using defineTask (from @symbiote-native/task-manager) before registering.`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
if ((await getStatusAsync()) === BackgroundTaskStatus.Restricted) {
|
|
61
|
+
if (!didWarnUnsupportedEnvironment) {
|
|
62
|
+
didWarnUnsupportedEnvironment = true;
|
|
63
|
+
const message =
|
|
64
|
+
Platform.OS === 'ios'
|
|
65
|
+
? `Background tasks are not supported on iOS simulators. Skipped registering task: ${taskName}.`
|
|
66
|
+
: `Background tasks are not available in the current environment. Skipped registering task: ${taskName}.`;
|
|
67
|
+
console.warn(message);
|
|
68
|
+
}
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
assertValidTaskName(taskName);
|
|
72
|
+
if (await isTaskRegisteredAsync(taskName)) return;
|
|
73
|
+
await expoBackgroundTask.registerTaskAsync(taskName, options);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Unregisters a background task, so the app stops receiving executions of it. */
|
|
77
|
+
export async function unregisterTaskAsync(taskName: string): Promise<void> {
|
|
78
|
+
if (!expoBackgroundTask.unregisterTaskAsync) {
|
|
79
|
+
throw new UnavailabilityError(NATIVE_MODULE_NAME, 'unregisterTaskAsync');
|
|
80
|
+
}
|
|
81
|
+
assertValidTaskName(taskName);
|
|
82
|
+
if (!(await isTaskRegisteredAsync(taskName))) return;
|
|
83
|
+
await expoBackgroundTask.unregisterTaskAsync(taskName);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Debug builds only: triggers the OS to run the registered background task immediately, instead
|
|
88
|
+
* of waiting for its real schedule. Always resolves `false` in a production build.
|
|
89
|
+
*/
|
|
90
|
+
export async function triggerTaskWorkerForTestingAsync(): Promise<boolean> {
|
|
91
|
+
if (!isDevBuild()) return false;
|
|
92
|
+
if (!expoBackgroundTask.triggerTaskWorkerForTestingAsync) {
|
|
93
|
+
throw new UnavailabilityError(
|
|
94
|
+
NATIVE_MODULE_NAME,
|
|
95
|
+
'triggerTaskWorkerForTestingAsync',
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
return expoBackgroundTask.triggerTaskWorkerForTestingAsync();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Subscribes to the `onTasksExpired` event iOS fires when the system interrupts a running
|
|
103
|
+
* background task before it finishes — use it to clean up resources or save state.
|
|
104
|
+
* @platform ios
|
|
105
|
+
*/
|
|
106
|
+
export function addExpirationListener(listener: () => void): {
|
|
107
|
+
remove: () => void;
|
|
108
|
+
} {
|
|
109
|
+
if (!expoBackgroundTask.addListener) {
|
|
110
|
+
throw new UnavailabilityError(NATIVE_MODULE_NAME, 'addListener');
|
|
111
|
+
}
|
|
112
|
+
return expoBackgroundTask.addListener('onTasksExpired', listener);
|
|
113
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export {
|
|
2
|
+
getStatusAsync,
|
|
3
|
+
registerTaskAsync,
|
|
4
|
+
unregisterTaskAsync,
|
|
5
|
+
triggerTaskWorkerForTestingAsync,
|
|
6
|
+
addExpirationListener,
|
|
7
|
+
} from './background-task';
|
|
8
|
+
export { BackgroundTaskStatus, BackgroundTaskResult } from './types';
|
|
9
|
+
export type { IBackgroundTaskOptions } from './types';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { requireNativeModule, type EventSubscription } from 'expo-modules-core';
|
|
2
|
+
|
|
3
|
+
const EXPO_BACKGROUND_TASK_MODULE_NAME = 'ExpoBackgroundTask';
|
|
4
|
+
|
|
5
|
+
// Every method is optional — each call site checks for its presence before calling through and
|
|
6
|
+
// throws an UnavailabilityError itself, matching upstream's own per-platform capability checks
|
|
7
|
+
// (same pattern as packages/task-manager/src/core/native-module.ts's INativeTaskManagerModule).
|
|
8
|
+
// Upstream types this module as a `NativeModule<Events>` subclass (imported from the `expo`
|
|
9
|
+
// meta-package, which this project never installs); a plain requireNativeModule() call resolves
|
|
10
|
+
// the same underlying object, and expo-modules-core's own EventEmitter interface (`addListener`)
|
|
11
|
+
// covers the one event this module fires (`onTasksExpired`).
|
|
12
|
+
export type INativeBackgroundTaskModule = {
|
|
13
|
+
getStatusAsync?(): Promise<number>;
|
|
14
|
+
registerTaskAsync?(
|
|
15
|
+
taskName: string,
|
|
16
|
+
options: Record<string, unknown>,
|
|
17
|
+
): Promise<void>;
|
|
18
|
+
unregisterTaskAsync?(taskName: string): Promise<void>;
|
|
19
|
+
/** Debug-build-only: forces the OS to run the registered background task immediately. */
|
|
20
|
+
triggerTaskWorkerForTestingAsync?(): Promise<boolean>;
|
|
21
|
+
addListener?(eventName: string, listener: () => void): EventSubscription;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const expoBackgroundTask =
|
|
25
|
+
requireNativeModule<INativeBackgroundTaskModule>(
|
|
26
|
+
EXPO_BACKGROUND_TASK_MODULE_NAME,
|
|
27
|
+
);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Availability status for the Background Task API. */
|
|
2
|
+
export enum BackgroundTaskStatus {
|
|
3
|
+
/** Background tasks are unavailable — e.g. running on an iOS Simulator. */
|
|
4
|
+
Restricted = 1,
|
|
5
|
+
/** Background tasks are available for the app. */
|
|
6
|
+
Available = 2,
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Return value a background-task executor should resolve with. */
|
|
10
|
+
export enum BackgroundTaskResult {
|
|
11
|
+
/** The task finished successfully. */
|
|
12
|
+
Success = 1,
|
|
13
|
+
/** The task failed. */
|
|
14
|
+
Failed = 2,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Options accepted by {@link registerTaskAsync}. */
|
|
18
|
+
export type IBackgroundTaskOptions = {
|
|
19
|
+
/**
|
|
20
|
+
* Inexact interval in minutes between subsequent repeats of the background task. The final
|
|
21
|
+
* interval may differ from the specified one to minimize wakeups and battery usage.
|
|
22
|
+
* - Defaults to once every 12 hours; the minimum interval is 15 minutes.
|
|
23
|
+
* - The OS controls the real execution interval and treats this as a minimum delay only — on
|
|
24
|
+
* iOS a short interval is often ignored, since the system typically runs background tasks
|
|
25
|
+
* during specific windows (e.g. overnight).
|
|
26
|
+
*/
|
|
27
|
+
minimumInterval?: number;
|
|
28
|
+
};
|