@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 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,4 @@
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/index.js';
@@ -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,2 @@
1
+ export { getStatusAsync, registerTaskAsync, unregisterTaskAsync, triggerTaskWorkerForTestingAsync, addExpirationListener, } from './background-task.js';
2
+ export { BackgroundTaskStatus, BackgroundTaskResult } from './types.js';
@@ -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,3 @@
1
+ import { requireNativeModule } from 'expo-modules-core';
2
+ const EXPO_BACKGROUND_TASK_MODULE_NAME = 'ExpoBackgroundTask';
3
+ export const expoBackgroundTask = requireNativeModule(EXPO_BACKGROUND_TASK_MODULE_NAME);
@@ -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,3 @@
1
+ export { getStatusAsync, registerTaskAsync, unregisterTaskAsync, triggerTaskWorkerForTestingAsync, addExpirationListener, } from './background-task';
2
+ export { BackgroundTaskStatus, BackgroundTaskResult } from './types';
3
+ //# sourceMappingURL=index.js.map
@@ -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,4 @@
1
+ import { requireNativeModule } from 'expo-modules-core';
2
+ const EXPO_BACKGROUND_TASK_MODULE_NAME = 'ExpoBackgroundTask';
3
+ export const expoBackgroundTask = requireNativeModule(EXPO_BACKGROUND_TASK_MODULE_NAME);
4
+ //# sourceMappingURL=native-module.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"native-module.js","sourceRoot":"","sources":["../../src/core/native-module.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAA0B,MAAM,mBAAmB,CAAC;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"}
@@ -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,4 @@
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';
@@ -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
+ };