@zykeco/expo-background-task 1.0.3 → 1.2.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.
@@ -1,5 +1,7 @@
1
1
  package expo.modules.backgroundtask
2
2
 
3
+ import android.os.Handler
4
+ import android.os.Looper
3
5
  import android.util.Log
4
6
  import com.facebook.react.common.build.ReactBuildConfig
5
7
  import expo.modules.interfaces.taskManager.TaskManagerInterface
@@ -19,10 +21,39 @@ class BackgroundTaskModule : Module() {
19
21
  override fun definition() = ModuleDefinition {
20
22
  Name("ExpoBackgroundTask")
21
23
 
24
+ Events("onTasksExpired")
25
+
26
+ OnStartObserving("onTasksExpired") {
27
+ BackgroundTaskScheduler.setOnTasksExpiredListener {
28
+ // onStopped() runs on a WorkManager thread — sendEvent requires main thread
29
+ Handler(Looper.getMainLooper()).post {
30
+ sendEvent("onTasksExpired", emptyMap<String, Any>())
31
+ }
32
+ }
33
+ // Deferred delivery: if a task was stopped before JS attached its listener,
34
+ // the flag is still pending — deliver now that JS is ready to receive it.
35
+ appContext.reactContext?.let { context ->
36
+ if (BackgroundTaskScheduler.consumeStoppedFlag(context)) {
37
+ Log.d(TAG, "Deferred task-stopped event — emitting onTasksExpired")
38
+ sendEvent("onTasksExpired", emptyMap<String, Any>())
39
+ }
40
+ }
41
+ }
42
+
43
+ OnStopObserving("onTasksExpired") {
44
+ BackgroundTaskScheduler.setOnTasksExpiredListener(null)
45
+ }
46
+
22
47
  AsyncFunction("getStatusAsync") {
23
48
  return@AsyncFunction 2 // WorkManager is always available on Android.
24
49
  }
25
50
 
51
+ AsyncFunction("getSchedulerDiagnosticsAsync") Coroutine { ->
52
+ appContext.reactContext?.let {
53
+ return@Coroutine BackgroundTaskScheduler.diagnostics(it)
54
+ } ?: throw MissingContextException()
55
+ }
56
+
26
57
  AsyncFunction("triggerTaskWorkerForTestingAsync") Coroutine { ->
27
58
  if (ReactBuildConfig.DEBUG) {
28
59
  Log.d(TAG, "Triggering tasks for testing")
@@ -33,6 +33,10 @@ object BackgroundTaskScheduler {
33
33
  // Unique identifier (generated by us) to identify the worker
34
34
  private const val WORKER_IDENTIFIER = "EXPO_BACKGROUND_WORKER"
35
35
 
36
+ // SharedPreferences keys for deferred task-stopped delivery
37
+ private const val PREFS_NAME = "expo_background_task"
38
+ private const val KEY_TASK_WAS_STOPPED = "task_was_stopped"
39
+
36
40
  // Log tag
37
41
  private val TAG = BackgroundTaskScheduler::class.java.simpleName
38
42
 
@@ -54,6 +58,46 @@ object BackgroundTaskScheduler {
54
58
  @Volatile
55
59
  var inForeground: Boolean = false
56
60
 
61
+ // Listener for task stopped/expired events (set by BackgroundTaskModule)
62
+ // @Volatile: written on main thread (OnStartObserving), read on WorkManager thread (onStopped)
63
+ @Volatile
64
+ private var onTasksExpiredListener: (() -> Unit)? = null
65
+
66
+ fun setOnTasksExpiredListener(listener: (() -> Unit)?) {
67
+ onTasksExpiredListener = listener
68
+ }
69
+
70
+ /**
71
+ * Called by BackgroundTaskWork.onStopped() when the system stops the worker.
72
+ * Notifies the listener in real-time (if JS is still running) and persists a flag
73
+ * as a fallback for deferred delivery on next foreground.
74
+ */
75
+ fun onWorkerStopped(context: Context) {
76
+ Log.d(TAG, "Worker was stopped by the system — notifying listener")
77
+ // Try real-time notification (JS may still be running)
78
+ onTasksExpiredListener?.invoke()
79
+ // Persist flag as fallback for deferred delivery when JS attaches its listener.
80
+ // commit() is intentional: onStopped() runs as the process is being killed,
81
+ // so apply()'s async write may never flush to disk.
82
+ context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
83
+ .edit()
84
+ .putBoolean(KEY_TASK_WAS_STOPPED, true)
85
+ .commit()
86
+ }
87
+
88
+ /**
89
+ * Check and clear the deferred "task was stopped" flag.
90
+ * Returns true if a previous task was stopped by the system.
91
+ */
92
+ fun consumeStoppedFlag(context: Context): Boolean {
93
+ val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
94
+ val wasStopped = prefs.getBoolean(KEY_TASK_WAS_STOPPED, false)
95
+ if (wasStopped) {
96
+ prefs.edit().putBoolean(KEY_TASK_WAS_STOPPED, false).apply()
97
+ }
98
+ return wasStopped
99
+ }
100
+
57
101
  /**
58
102
  * Call when a task is registered
59
103
  */
@@ -259,6 +303,26 @@ object BackgroundTaskScheduler {
259
303
  }
260
304
  }
261
305
 
306
+ /**
307
+ * Returns diagnostic information about the scheduler state.
308
+ * Useful for debugging background task scheduling issues.
309
+ */
310
+ suspend fun diagnostics(context: Context): Map<String, Any?> {
311
+ val workInfo = getWorkerInfo(context)
312
+
313
+ val result = mutableMapOf<String, Any?>(
314
+ "identifier" to WORKER_IDENTIFIER,
315
+ "registeredConsumers" to numberOfRegisteredTasksOfThisType,
316
+ "intervalMinutes" to intervalMinutes,
317
+ "requiresNetworkConnectivity" to requiresNetwork,
318
+ "inForeground" to inForeground,
319
+ "workerState" to workInfo?.state?.name,
320
+ "hasPendingRequest" to (workInfo?.state == WorkInfo.State.ENQUEUED || workInfo?.state == WorkInfo.State.RUNNING),
321
+ )
322
+
323
+ return result
324
+ }
325
+
262
326
  /**
263
327
  * Returns the worker info object from the WorkManager if the worker has been
264
328
  * registered, otherwise returns null
@@ -35,4 +35,10 @@ class BackgroundTaskWork(context: Context, params: WorkerParameters) : Coroutine
35
35
  }
36
36
  return Result.success()
37
37
  }
38
+
39
+ override fun onStopped() {
40
+ super.onStopped()
41
+ Log.d(TAG, "onStopped: Worker was stopped by the system")
42
+ BackgroundTaskScheduler.onWorkerStopped(applicationContext)
43
+ }
38
44
  }
@@ -1,4 +1,4 @@
1
- import { BackgroundTaskOptions, BackgroundTaskStatus } from './BackgroundTask.types';
1
+ import { BackgroundTaskOptions, BackgroundTaskStatus, SchedulerDiagnostics } from './BackgroundTask.types';
2
2
  /**
3
3
  * Returns the status for the Background Task API. On web, it always returns `BackgroundTaskStatus.Restricted`,
4
4
  * while on native platforms it returns `BackgroundTaskStatus.Available`.
@@ -49,15 +49,31 @@ export declare function unregisterTaskAsync(taskName: string): Promise<void>;
49
49
  */
50
50
  export declare function triggerTaskWorkerForTestingAsync(): Promise<boolean>;
51
51
  /**
52
- * Adds a listener that is called when the background executor expires. On iOS, tasks can run
53
- * for minutes, but the system can interrupt the process at any time. This listener is called
54
- * when the system decides to stop the background tasks and should be used to clean up resources
55
- * or save state. When the expiry handler is called, the main task runner is rescheduled automatically.
56
- * @platform ios
52
+ * Returns diagnostic information about the background task scheduler.
53
+ * Useful for debugging whether tasks are registered and scheduled correctly.
54
+ *
55
+ * @returns A promise that resolves with scheduler diagnostics. Fields vary by platform.
56
+ */
57
+ export declare function getSchedulerDiagnosticsAsync(): Promise<SchedulerDiagnostics>;
58
+ /**
59
+ * Adds a listener that is called when the OS stops a running background task.
60
+ *
61
+ * On iOS, this fires via the BGTask expiration handler when iOS reclaims resources.
62
+ * On Android, this fires when WorkManager stops the worker (e.g., system constraints,
63
+ * memory pressure). If the event cannot be delivered in real-time, it is delivered
64
+ * when the app next enters the foreground.
65
+ *
66
+ * Use this to clean up resources or save state before the task is terminated.
67
+ * The task runner is rescheduled automatically after expiration.
68
+ *
69
+ * **Android note:** This event uses at-least-once delivery — the listener may fire
70
+ * again on next app start if real-time delivery was also successful. Listeners
71
+ * should be idempotent.
72
+ *
57
73
  * @return An object with a `remove` method to unsubscribe the listener.
58
74
  */
59
75
  export declare function addExpirationListener(listener: () => void): {
60
76
  remove: () => void;
61
77
  };
62
- export { BackgroundTaskStatus, BackgroundTaskResult, BackgroundTaskOptions, } from './BackgroundTask.types';
78
+ export { BackgroundTaskStatus, BackgroundTaskResult, BackgroundTaskOptions, SchedulerDiagnostics, } from './BackgroundTask.types';
63
79
  //# sourceMappingURL=BackgroundTask.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"BackgroundTask.d.ts","sourceRoot":"","sources":["../src/BackgroundTask.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAwBrF;;;;;GAKG;AACH,eAAO,MAAM,cAAc,QAAa,OAAO,CAAC,oBAAoB,CAQnE,CAAC;AAGF;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,qBAA0B,GAClC,OAAO,CAAC,IAAI,CAAC,CA0Bf;AAGD;;;;GAIG;AACH,wBAAsB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CASzE;AAGD;;;;;GAKG;AACH,wBAAsB,gCAAgC,IAAI,OAAO,CAAC,OAAO,CAAC,CAUzE;AAGD;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG;IAAE,MAAM,EAAE,MAAM,IAAI,CAAA;CAAE,CAKlF;AAGD,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,qBAAqB,GACtB,MAAM,wBAAwB,CAAC"}
1
+ {"version":3,"file":"BackgroundTask.d.ts","sourceRoot":"","sources":["../src/BackgroundTask.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAwB3G;;;;;GAKG;AACH,eAAO,MAAM,cAAc,QAAa,OAAO,CAAC,oBAAoB,CAQnE,CAAC;AAGF;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,qBAA0B,GAClC,OAAO,CAAC,IAAI,CAAC,CA0Bf;AAGD;;;;GAIG;AACH,wBAAsB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CASzE;AAGD;;;;;GAKG;AACH,wBAAsB,gCAAgC,IAAI,OAAO,CAAC,OAAO,CAAC,CAUzE;AAGD;;;;;GAKG;AACH,wBAAsB,4BAA4B,IAAI,OAAO,CAAC,oBAAoB,CAAC,CAKlF;AAGD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG;IAAE,MAAM,EAAE,MAAM,IAAI,CAAA;CAAE,CAKlF;AAGD,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,qBAAqB,EACrB,oBAAoB,GACrB,MAAM,wBAAwB,CAAC"}
@@ -123,11 +123,33 @@ export async function triggerTaskWorkerForTestingAsync() {
123
123
  }
124
124
  // @needsAudit
125
125
  /**
126
- * Adds a listener that is called when the background executor expires. On iOS, tasks can run
127
- * for minutes, but the system can interrupt the process at any time. This listener is called
128
- * when the system decides to stop the background tasks and should be used to clean up resources
129
- * or save state. When the expiry handler is called, the main task runner is rescheduled automatically.
130
- * @platform ios
126
+ * Returns diagnostic information about the background task scheduler.
127
+ * Useful for debugging whether tasks are registered and scheduled correctly.
128
+ *
129
+ * @returns A promise that resolves with scheduler diagnostics. Fields vary by platform.
130
+ */
131
+ export async function getSchedulerDiagnosticsAsync() {
132
+ if (!ExpoBackgroundTaskModule.getSchedulerDiagnosticsAsync) {
133
+ throw new UnavailabilityError('BackgroundTask', 'getSchedulerDiagnosticsAsync');
134
+ }
135
+ return await ExpoBackgroundTaskModule.getSchedulerDiagnosticsAsync();
136
+ }
137
+ // @needsAudit
138
+ /**
139
+ * Adds a listener that is called when the OS stops a running background task.
140
+ *
141
+ * On iOS, this fires via the BGTask expiration handler when iOS reclaims resources.
142
+ * On Android, this fires when WorkManager stops the worker (e.g., system constraints,
143
+ * memory pressure). If the event cannot be delivered in real-time, it is delivered
144
+ * when the app next enters the foreground.
145
+ *
146
+ * Use this to clean up resources or save state before the task is terminated.
147
+ * The task runner is rescheduled automatically after expiration.
148
+ *
149
+ * **Android note:** This event uses at-least-once delivery — the listener may fire
150
+ * again on next app start if real-time delivery was also successful. Listeners
151
+ * should be idempotent.
152
+ *
131
153
  * @return An object with a `remove` method to unsubscribe the listener.
132
154
  */
133
155
  export function addExpirationListener(listener) {
@@ -1 +1 @@
1
- {"version":3,"file":"BackgroundTask.js","sourceRoot":"","sources":["../src/BackgroundTask.ts"],"names":[],"mappings":"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","sourcesContent":["import { isRunningInExpoGo } from 'expo';\nimport { Platform, UnavailabilityError } from 'expo-modules-core';\nimport * as TaskManager from 'expo-task-manager';\n\nimport { BackgroundTaskOptions, BackgroundTaskStatus } from './BackgroundTask.types';\nimport ExpoBackgroundTaskModule from './ExpoBackgroundTaskModule';\n\n// Flag to warn about running on Apple simulator\nlet warnAboutRunningOniOSSimulator = false;\n\nlet warnedAboutExpoGo = false;\n\nfunction _validate(taskName: unknown) {\n if (isRunningInExpoGo()) {\n if (!warnedAboutExpoGo) {\n const message =\n '`Background Task` functionality is not available in Expo Go:\\n' +\n 'You can use this API and any others in a development build. Learn more: https://expo.fyi/dev-client.';\n console.warn(message);\n warnedAboutExpoGo = true;\n }\n }\n if (!taskName || typeof taskName !== 'string') {\n throw new TypeError('`taskName` must be a non-empty string.');\n }\n}\n\n// @needsAudit\n/**\n * Returns the status for the Background Task API. On web, it always returns `BackgroundTaskStatus.Restricted`,\n * while on native platforms it returns `BackgroundTaskStatus.Available`.\n *\n * @returns A BackgroundTaskStatus enum value or `null` if not available.\n */\nexport const getStatusAsync = async (): Promise<BackgroundTaskStatus> => {\n if (!ExpoBackgroundTaskModule.getStatusAsync) {\n throw new UnavailabilityError('BackgroundTask', 'getStatusAsync');\n }\n\n return isRunningInExpoGo()\n ? BackgroundTaskStatus.Restricted\n : ExpoBackgroundTaskModule.getStatusAsync();\n};\n\n// @needsAudit\n/**\n * Registers a background task with the given name. Registered tasks are saved in persistent storage and restored once the app is initialized.\n * @param taskName Name of the task to register. The task needs to be defined first - see [`TaskManager.defineTask`](task-manager/#taskmanagerdefinetasktaskname-taskexecutor)\n * for more details.\n * @param options An object containing the background task options.\n *\n * @example\n * ```ts\n * import * as TaskManager from 'expo-task-manager';\n *\n * // Register the task outside of the component\n * TaskManager.defineTask(BACKGROUND_TASK_IDENTIFIER, () => {\n * try {\n * await AsyncStorage.setItem(LAST_TASK_DATE_KEY, Date.now().toString());\n * } catch (error) {\n * console.error('Failed to save the last fetch date', error);\n * return BackgroundTaskResult.Failed;\n * }\n * return BackgroundTaskResult.Success;\n * });\n * ```\n *\n * You can now use the `registerTaskAsync` function to register the task:\n *\n * ```ts\n * BackgroundTask.registerTaskAsync(BACKGROUND_TASK_IDENTIFIER, {});\n * ```\n */\nexport async function registerTaskAsync(\n taskName: string,\n options: BackgroundTaskOptions = {}\n): Promise<void> {\n if (!ExpoBackgroundTaskModule.registerTaskAsync) {\n throw new UnavailabilityError('BackgroundTask', 'registerTaskAsync');\n }\n if (!TaskManager.isTaskDefined(taskName)) {\n throw new Error(\n `Task '${taskName}' is not defined. You must define a task using TaskManager.defineTask before registering.`\n );\n }\n\n if ((await ExpoBackgroundTaskModule.getStatusAsync()) === BackgroundTaskStatus.Restricted) {\n if (!warnAboutRunningOniOSSimulator) {\n const message =\n Platform.OS === 'ios'\n ? `Background tasks are not supported on iOS simulators. Skipped registering task: ${taskName}.`\n : `Background tasks are not available in the current environment. Skipped registering task: ${taskName}.`;\n console.warn(message);\n warnAboutRunningOniOSSimulator = true;\n }\n return;\n }\n _validate(taskName);\n if (await TaskManager.isTaskRegisteredAsync(taskName)) {\n return;\n }\n await ExpoBackgroundTaskModule.registerTaskAsync(taskName, options);\n}\n\n// @needsAudit\n/**\n * Unregisters a background task, so the application will no longer be executing this task.\n * @param taskName Name of the task to unregister.\n * @return A promise which fulfils when the task is fully unregistered.\n */\nexport async function unregisterTaskAsync(taskName: string): Promise<void> {\n if (!ExpoBackgroundTaskModule.unregisterTaskAsync) {\n throw new UnavailabilityError('BackgroundTask', 'unregisterTaskAsync');\n }\n _validate(taskName);\n if (!(await TaskManager.isTaskRegisteredAsync(taskName))) {\n return;\n }\n await ExpoBackgroundTaskModule.unregisterTaskAsync(taskName);\n}\n\n// @needsAudit\n/**\n * When in debug mode this function will trigger running the background tasks.\n * This function will only work for apps built in debug mode.\n * This method is only available in development mode. It will not work in production builds.\n * @returns A promise which fulfils when the task is triggered.\n */\nexport async function triggerTaskWorkerForTestingAsync(): Promise<boolean> {\n if (__DEV__) {\n if (!ExpoBackgroundTaskModule.triggerTaskWorkerForTestingAsync) {\n throw new UnavailabilityError('BackgroundTask', 'triggerTaskWorkerForTestingAsync');\n }\n console.log('Calling triggerTaskWorkerForTestingAsync');\n return await ExpoBackgroundTaskModule.triggerTaskWorkerForTestingAsync();\n } else {\n return Promise.resolve(false);\n }\n}\n\n// @needsAudit\n/**\n * Adds a listener that is called when the background executor expires. On iOS, tasks can run\n * for minutes, but the system can interrupt the process at any time. This listener is called\n * when the system decides to stop the background tasks and should be used to clean up resources\n * or save state. When the expiry handler is called, the main task runner is rescheduled automatically.\n * @platform ios\n * @return An object with a `remove` method to unsubscribe the listener.\n */\nexport function addExpirationListener(listener: () => void): { remove: () => void } {\n if (!ExpoBackgroundTaskModule.addListener) {\n throw new UnavailabilityError('BackgroundTask', 'addListener');\n }\n return ExpoBackgroundTaskModule.addListener('onTasksExpired', listener);\n}\n\n// Export types\nexport {\n BackgroundTaskStatus,\n BackgroundTaskResult,\n BackgroundTaskOptions,\n} from './BackgroundTask.types';\n"]}
1
+ {"version":3,"file":"BackgroundTask.js","sourceRoot":"","sources":["../src/BackgroundTask.ts"],"names":[],"mappings":"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","sourcesContent":["import { isRunningInExpoGo } from 'expo';\nimport { Platform, UnavailabilityError } from 'expo-modules-core';\nimport * as TaskManager from 'expo-task-manager';\n\nimport { BackgroundTaskOptions, BackgroundTaskStatus, SchedulerDiagnostics } from './BackgroundTask.types';\nimport ExpoBackgroundTaskModule from './ExpoBackgroundTaskModule';\n\n// Flag to warn about running on Apple simulator\nlet warnAboutRunningOniOSSimulator = false;\n\nlet warnedAboutExpoGo = false;\n\nfunction _validate(taskName: unknown) {\n if (isRunningInExpoGo()) {\n if (!warnedAboutExpoGo) {\n const message =\n '`Background Task` functionality is not available in Expo Go:\\n' +\n 'You can use this API and any others in a development build. Learn more: https://expo.fyi/dev-client.';\n console.warn(message);\n warnedAboutExpoGo = true;\n }\n }\n if (!taskName || typeof taskName !== 'string') {\n throw new TypeError('`taskName` must be a non-empty string.');\n }\n}\n\n// @needsAudit\n/**\n * Returns the status for the Background Task API. On web, it always returns `BackgroundTaskStatus.Restricted`,\n * while on native platforms it returns `BackgroundTaskStatus.Available`.\n *\n * @returns A BackgroundTaskStatus enum value or `null` if not available.\n */\nexport const getStatusAsync = async (): Promise<BackgroundTaskStatus> => {\n if (!ExpoBackgroundTaskModule.getStatusAsync) {\n throw new UnavailabilityError('BackgroundTask', 'getStatusAsync');\n }\n\n return isRunningInExpoGo()\n ? BackgroundTaskStatus.Restricted\n : ExpoBackgroundTaskModule.getStatusAsync();\n};\n\n// @needsAudit\n/**\n * Registers a background task with the given name. Registered tasks are saved in persistent storage and restored once the app is initialized.\n * @param taskName Name of the task to register. The task needs to be defined first - see [`TaskManager.defineTask`](task-manager/#taskmanagerdefinetasktaskname-taskexecutor)\n * for more details.\n * @param options An object containing the background task options.\n *\n * @example\n * ```ts\n * import * as TaskManager from 'expo-task-manager';\n *\n * // Register the task outside of the component\n * TaskManager.defineTask(BACKGROUND_TASK_IDENTIFIER, () => {\n * try {\n * await AsyncStorage.setItem(LAST_TASK_DATE_KEY, Date.now().toString());\n * } catch (error) {\n * console.error('Failed to save the last fetch date', error);\n * return BackgroundTaskResult.Failed;\n * }\n * return BackgroundTaskResult.Success;\n * });\n * ```\n *\n * You can now use the `registerTaskAsync` function to register the task:\n *\n * ```ts\n * BackgroundTask.registerTaskAsync(BACKGROUND_TASK_IDENTIFIER, {});\n * ```\n */\nexport async function registerTaskAsync(\n taskName: string,\n options: BackgroundTaskOptions = {}\n): Promise<void> {\n if (!ExpoBackgroundTaskModule.registerTaskAsync) {\n throw new UnavailabilityError('BackgroundTask', 'registerTaskAsync');\n }\n if (!TaskManager.isTaskDefined(taskName)) {\n throw new Error(\n `Task '${taskName}' is not defined. You must define a task using TaskManager.defineTask before registering.`\n );\n }\n\n if ((await ExpoBackgroundTaskModule.getStatusAsync()) === BackgroundTaskStatus.Restricted) {\n if (!warnAboutRunningOniOSSimulator) {\n const message =\n Platform.OS === 'ios'\n ? `Background tasks are not supported on iOS simulators. Skipped registering task: ${taskName}.`\n : `Background tasks are not available in the current environment. Skipped registering task: ${taskName}.`;\n console.warn(message);\n warnAboutRunningOniOSSimulator = true;\n }\n return;\n }\n _validate(taskName);\n if (await TaskManager.isTaskRegisteredAsync(taskName)) {\n return;\n }\n await ExpoBackgroundTaskModule.registerTaskAsync(taskName, options);\n}\n\n// @needsAudit\n/**\n * Unregisters a background task, so the application will no longer be executing this task.\n * @param taskName Name of the task to unregister.\n * @return A promise which fulfils when the task is fully unregistered.\n */\nexport async function unregisterTaskAsync(taskName: string): Promise<void> {\n if (!ExpoBackgroundTaskModule.unregisterTaskAsync) {\n throw new UnavailabilityError('BackgroundTask', 'unregisterTaskAsync');\n }\n _validate(taskName);\n if (!(await TaskManager.isTaskRegisteredAsync(taskName))) {\n return;\n }\n await ExpoBackgroundTaskModule.unregisterTaskAsync(taskName);\n}\n\n// @needsAudit\n/**\n * When in debug mode this function will trigger running the background tasks.\n * This function will only work for apps built in debug mode.\n * This method is only available in development mode. It will not work in production builds.\n * @returns A promise which fulfils when the task is triggered.\n */\nexport async function triggerTaskWorkerForTestingAsync(): Promise<boolean> {\n if (__DEV__) {\n if (!ExpoBackgroundTaskModule.triggerTaskWorkerForTestingAsync) {\n throw new UnavailabilityError('BackgroundTask', 'triggerTaskWorkerForTestingAsync');\n }\n console.log('Calling triggerTaskWorkerForTestingAsync');\n return await ExpoBackgroundTaskModule.triggerTaskWorkerForTestingAsync();\n } else {\n return Promise.resolve(false);\n }\n}\n\n// @needsAudit\n/**\n * Returns diagnostic information about the background task scheduler.\n * Useful for debugging whether tasks are registered and scheduled correctly.\n *\n * @returns A promise that resolves with scheduler diagnostics. Fields vary by platform.\n */\nexport async function getSchedulerDiagnosticsAsync(): Promise<SchedulerDiagnostics> {\n if (!ExpoBackgroundTaskModule.getSchedulerDiagnosticsAsync) {\n throw new UnavailabilityError('BackgroundTask', 'getSchedulerDiagnosticsAsync');\n }\n return await ExpoBackgroundTaskModule.getSchedulerDiagnosticsAsync();\n}\n\n// @needsAudit\n/**\n * Adds a listener that is called when the OS stops a running background task.\n *\n * On iOS, this fires via the BGTask expiration handler when iOS reclaims resources.\n * On Android, this fires when WorkManager stops the worker (e.g., system constraints,\n * memory pressure). If the event cannot be delivered in real-time, it is delivered\n * when the app next enters the foreground.\n *\n * Use this to clean up resources or save state before the task is terminated.\n * The task runner is rescheduled automatically after expiration.\n *\n * **Android note:** This event uses at-least-once delivery — the listener may fire\n * again on next app start if real-time delivery was also successful. Listeners\n * should be idempotent.\n *\n * @return An object with a `remove` method to unsubscribe the listener.\n */\nexport function addExpirationListener(listener: () => void): { remove: () => void } {\n if (!ExpoBackgroundTaskModule.addListener) {\n throw new UnavailabilityError('BackgroundTask', 'addListener');\n }\n return ExpoBackgroundTaskModule.addListener('onTasksExpired', listener);\n}\n\n// Export types\nexport {\n BackgroundTaskStatus,\n BackgroundTaskResult,\n BackgroundTaskOptions,\n SchedulerDiagnostics,\n} from './BackgroundTask.types';\n"]}
@@ -68,4 +68,68 @@ export type BackgroundTaskOptions = {
68
68
  */
69
69
  requiresExternalPower?: boolean;
70
70
  };
71
+ /**
72
+ * Diagnostic information about the background task scheduler state.
73
+ * Fields vary by platform — iOS-only fields are `undefined` on Android and vice versa.
74
+ */
75
+ export type SchedulerDiagnostics = {
76
+ /** Native worker/task identifier */
77
+ identifier: string;
78
+ /** Number of currently registered task consumers */
79
+ registeredConsumers: number;
80
+ /** Whether a pending background task request exists */
81
+ hasPendingRequest: boolean;
82
+ /** Whether the task requires network connectivity */
83
+ requiresNetworkConnectivity: boolean;
84
+ /**
85
+ * Whether the device supports background tasks.
86
+ * @platform ios
87
+ */
88
+ supportsBackgroundTasks?: boolean;
89
+ /**
90
+ * The BGTask type used ('refresh' or 'processing').
91
+ * @platform ios
92
+ */
93
+ taskType?: string;
94
+ /**
95
+ * Configured interval in seconds.
96
+ * @platform ios
97
+ */
98
+ intervalSeconds?: number;
99
+ /**
100
+ * Whether the task requires external power.
101
+ * @platform ios
102
+ */
103
+ requiresExternalPower?: boolean;
104
+ /**
105
+ * ISO-8601 date string of the earliest time the next task can begin.
106
+ * @platform ios
107
+ */
108
+ earliestBeginDate?: string;
109
+ /**
110
+ * Seconds until the next scheduled run (can be negative if overdue).
111
+ * @platform ios
112
+ */
113
+ secondsUntilNextRun?: number;
114
+ /**
115
+ * The type of the pending request ('refresh' or 'processing').
116
+ * @platform ios
117
+ */
118
+ pendingRequestType?: string;
119
+ /**
120
+ * Configured interval in minutes.
121
+ * @platform android
122
+ */
123
+ intervalMinutes?: number;
124
+ /**
125
+ * Whether the app is currently in the foreground.
126
+ * @platform android
127
+ */
128
+ inForeground?: boolean;
129
+ /**
130
+ * WorkManager worker state (e.g. 'ENQUEUED', 'RUNNING').
131
+ * @platform android
132
+ */
133
+ workerState?: string;
134
+ };
71
135
  //# sourceMappingURL=BackgroundTask.types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"BackgroundTask.types.d.ts","sourceRoot":"","sources":["../src/BackgroundTask.types.ts"],"names":[],"mappings":"AACA;;GAEG;AACH,oBAAY,oBAAoB;IAC9B;;OAEG;IACH,UAAU,IAAI;IACd;;OAEG;IACH,SAAS,IAAI;CACd;AAGD;;GAEG;AACH,oBAAY,oBAAoB;IAC9B;;OAEG;IACH,OAAO,IAAI;IACX;;OAEG;IACH,MAAM,IAAI;CACX;AAGD;;GAEG;AACH,MAAM,MAAM,qBAAqB,GAAG;IAClC;;;;;;;;;OASG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;OAKG;IACH,2BAA2B,CAAC,EAAE,OAAO,CAAC;IACtC;;;;;;;;;;;OAWG;IACH,WAAW,CAAC,EAAE,SAAS,GAAG,YAAY,CAAC;IACvC;;;;;;OAMG;IACH,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC,CAAC"}
1
+ {"version":3,"file":"BackgroundTask.types.d.ts","sourceRoot":"","sources":["../src/BackgroundTask.types.ts"],"names":[],"mappings":"AACA;;GAEG;AACH,oBAAY,oBAAoB;IAC9B;;OAEG;IACH,UAAU,IAAI;IACd;;OAEG;IACH,SAAS,IAAI;CACd;AAGD;;GAEG;AACH,oBAAY,oBAAoB;IAC9B;;OAEG;IACH,OAAO,IAAI;IACX;;OAEG;IACH,MAAM,IAAI;CACX;AAGD;;GAEG;AACH,MAAM,MAAM,qBAAqB,GAAG;IAClC;;;;;;;;;OASG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;OAKG;IACH,2BAA2B,CAAC,EAAE,OAAO,CAAC;IACtC;;;;;;;;;;;OAWG;IACH,WAAW,CAAC,EAAE,SAAS,GAAG,YAAY,CAAC;IACvC;;;;;;OAMG;IACH,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,oBAAoB,GAAG;IACjC,oCAAoC;IACpC,UAAU,EAAE,MAAM,CAAC;IACnB,oDAAoD;IACpD,mBAAmB,EAAE,MAAM,CAAC;IAC5B,uDAAuD;IACvD,iBAAiB,EAAE,OAAO,CAAC;IAC3B,qDAAqD;IACrD,2BAA2B,EAAE,OAAO,CAAC;IACrC;;;OAGG;IACH,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;OAGG;IACH,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC;;;OAGG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B;;;OAGG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;OAGG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"BackgroundTask.types.js","sourceRoot":"","sources":["../src/BackgroundTask.types.ts"],"names":[],"mappings":"AAAA,cAAc;AACd;;GAEG;AACH,MAAM,CAAN,IAAY,oBASX;AATD,WAAY,oBAAoB;IAC9B;;OAEG;IACH,2EAAc,CAAA;IACd;;OAEG;IACH,yEAAa,CAAA;AACf,CAAC,EATW,oBAAoB,KAApB,oBAAoB,QAS/B;AAED,cAAc;AACd;;GAEG;AACH,MAAM,CAAN,IAAY,oBASX;AATD,WAAY,oBAAoB;IAC9B;;OAEG;IACH,qEAAW,CAAA;IACX;;OAEG;IACH,mEAAU,CAAA;AACZ,CAAC,EATW,oBAAoB,KAApB,oBAAoB,QAS/B","sourcesContent":["// @needsAudit\n/**\n * Availability status for background tasks\n */\nexport enum BackgroundTaskStatus {\n /**\n * Background tasks are unavailable.\n */\n Restricted = 1,\n /**\n * Background tasks are available for the app.\n */\n Available = 2,\n}\n\n// @needsAudit\n/**\n * Return value for background tasks.\n */\nexport enum BackgroundTaskResult {\n /**\n * The task finished successfully.\n */\n Success = 1,\n /**\n * The task failed.\n */\n Failed = 2,\n}\n\n// @needsAudit\n/**\n * Options for registering a background task\n */\nexport type BackgroundTaskOptions = {\n /**\n * Inexact interval in minutes between subsequent repeats of the background tasks. The final\n * interval may differ from the specified one to minimize wakeups and battery usage.\n * - Defaults to once every 12 hours (The minimum interval is 15 minutes)\n * - The system controls the background task execution interval and treats the\n * specified value as a minimum delay. Tasks won't run exactly on schedule. On iOS, short\n * intervals are often ignored—the system typically runs background tasks during\n * specific windows, such as overnight.\n *\n */\n minimumInterval?: number;\n /**\n * Whether the task requires network connectivity.\n *\n * @default true\n * @platform ios, android\n */\n requiresNetworkConnectivity?: boolean;\n /**\n * iOS-only: Which BGTask type to use.\n * - `'refresh'` (default): BGAppRefreshTaskRequest — aggressive scheduling (~15-30 min),\n * ~30 s execution limit. Suitable for periodic syncs and content updates.\n * - `'processing'`: BGProcessingTaskRequest — iOS defers at its own discretion\n * (hours/days), but allows several minutes of runtime. Suitable for heavy work\n * (ML training, DB migration). Supports `requiresNetworkConnectivity` and\n * `requiresExternalPower`.\n *\n * @default 'refresh'\n * @platform ios\n */\n iosTaskType?: 'refresh' | 'processing';\n /**\n * iOS-only: Whether the task requires external power.\n * Only relevant when `iosTaskType: 'processing'`.\n *\n * @default false\n * @platform ios\n */\n requiresExternalPower?: boolean;\n};\n"]}
1
+ {"version":3,"file":"BackgroundTask.types.js","sourceRoot":"","sources":["../src/BackgroundTask.types.ts"],"names":[],"mappings":"AAAA,cAAc;AACd;;GAEG;AACH,MAAM,CAAN,IAAY,oBASX;AATD,WAAY,oBAAoB;IAC9B;;OAEG;IACH,2EAAc,CAAA;IACd;;OAEG;IACH,yEAAa,CAAA;AACf,CAAC,EATW,oBAAoB,KAApB,oBAAoB,QAS/B;AAED,cAAc;AACd;;GAEG;AACH,MAAM,CAAN,IAAY,oBASX;AATD,WAAY,oBAAoB;IAC9B;;OAEG;IACH,qEAAW,CAAA;IACX;;OAEG;IACH,mEAAU,CAAA;AACZ,CAAC,EATW,oBAAoB,KAApB,oBAAoB,QAS/B","sourcesContent":["// @needsAudit\n/**\n * Availability status for background tasks\n */\nexport enum BackgroundTaskStatus {\n /**\n * Background tasks are unavailable.\n */\n Restricted = 1,\n /**\n * Background tasks are available for the app.\n */\n Available = 2,\n}\n\n// @needsAudit\n/**\n * Return value for background tasks.\n */\nexport enum BackgroundTaskResult {\n /**\n * The task finished successfully.\n */\n Success = 1,\n /**\n * The task failed.\n */\n Failed = 2,\n}\n\n// @needsAudit\n/**\n * Options for registering a background task\n */\nexport type BackgroundTaskOptions = {\n /**\n * Inexact interval in minutes between subsequent repeats of the background tasks. The final\n * interval may differ from the specified one to minimize wakeups and battery usage.\n * - Defaults to once every 12 hours (The minimum interval is 15 minutes)\n * - The system controls the background task execution interval and treats the\n * specified value as a minimum delay. Tasks won't run exactly on schedule. On iOS, short\n * intervals are often ignored—the system typically runs background tasks during\n * specific windows, such as overnight.\n *\n */\n minimumInterval?: number;\n /**\n * Whether the task requires network connectivity.\n *\n * @default true\n * @platform ios, android\n */\n requiresNetworkConnectivity?: boolean;\n /**\n * iOS-only: Which BGTask type to use.\n * - `'refresh'` (default): BGAppRefreshTaskRequest — aggressive scheduling (~15-30 min),\n * ~30 s execution limit. Suitable for periodic syncs and content updates.\n * - `'processing'`: BGProcessingTaskRequest — iOS defers at its own discretion\n * (hours/days), but allows several minutes of runtime. Suitable for heavy work\n * (ML training, DB migration). Supports `requiresNetworkConnectivity` and\n * `requiresExternalPower`.\n *\n * @default 'refresh'\n * @platform ios\n */\n iosTaskType?: 'refresh' | 'processing';\n /**\n * iOS-only: Whether the task requires external power.\n * Only relevant when `iosTaskType: 'processing'`.\n *\n * @default false\n * @platform ios\n */\n requiresExternalPower?: boolean;\n};\n\n/**\n * Diagnostic information about the background task scheduler state.\n * Fields vary by platform — iOS-only fields are `undefined` on Android and vice versa.\n */\nexport type SchedulerDiagnostics = {\n /** Native worker/task identifier */\n identifier: string;\n /** Number of currently registered task consumers */\n registeredConsumers: number;\n /** Whether a pending background task request exists */\n hasPendingRequest: boolean;\n /** Whether the task requires network connectivity */\n requiresNetworkConnectivity: boolean;\n /**\n * Whether the device supports background tasks.\n * @platform ios\n */\n supportsBackgroundTasks?: boolean;\n /**\n * The BGTask type used ('refresh' or 'processing').\n * @platform ios\n */\n taskType?: string;\n /**\n * Configured interval in seconds.\n * @platform ios\n */\n intervalSeconds?: number;\n /**\n * Whether the task requires external power.\n * @platform ios\n */\n requiresExternalPower?: boolean;\n /**\n * ISO-8601 date string of the earliest time the next task can begin.\n * @platform ios\n */\n earliestBeginDate?: string;\n /**\n * Seconds until the next scheduled run (can be negative if overdue).\n * @platform ios\n */\n secondsUntilNextRun?: number;\n /**\n * The type of the pending request ('refresh' or 'processing').\n * @platform ios\n */\n pendingRequestType?: string;\n /**\n * Configured interval in minutes.\n * @platform android\n */\n intervalMinutes?: number;\n /**\n * Whether the app is currently in the foreground.\n * @platform android\n */\n inForeground?: boolean;\n /**\n * WorkManager worker state (e.g. 'ENQUEUED', 'RUNNING').\n * @platform android\n */\n workerState?: string;\n};\n"]}
@@ -1,5 +1,5 @@
1
1
  import { type NativeModule } from 'expo';
2
- import { BackgroundTaskOptions, BackgroundTaskStatus } from './BackgroundTask.types';
2
+ import { BackgroundTaskOptions, BackgroundTaskStatus, SchedulerDiagnostics } from './BackgroundTask.types';
3
3
  type ExpoBackgroundTaskEvents = {
4
4
  onTasksExpired(): void;
5
5
  };
@@ -8,6 +8,7 @@ declare class ExpoBackgroundTaskModule extends NativeModule<ExpoBackgroundTaskEv
8
8
  registerTaskAsync(name: string, options: BackgroundTaskOptions): Promise<void>;
9
9
  unregisterTaskAsync(name: string): Promise<void>;
10
10
  triggerTaskWorkerForTestingAsync(): Promise<boolean>;
11
+ getSchedulerDiagnosticsAsync(): Promise<SchedulerDiagnostics>;
11
12
  }
12
13
  declare const _default: ExpoBackgroundTaskModule;
13
14
  export default _default;
@@ -1 +1 @@
1
- {"version":3,"file":"ExpoBackgroundTaskModule.d.ts","sourceRoot":"","sources":["../src/ExpoBackgroundTaskModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuB,KAAK,YAAY,EAAE,MAAM,MAAM,CAAC;AAE9D,OAAO,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAErF,KAAK,wBAAwB,GAAG;IAC9B,cAAc,IAAI,IAAI,CAAC;CACxB,CAAC;AAEF,OAAO,OAAO,wBAAyB,SAAQ,YAAY,CAAC,wBAAwB,CAAC;IACnF,cAAc,IAAI,OAAO,CAAC,oBAAoB,CAAC;IAC/C,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC;IAC9E,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAChD,gCAAgC,IAAI,OAAO,CAAC,OAAO,CAAC;CACrD;;AAED,wBAAmF"}
1
+ {"version":3,"file":"ExpoBackgroundTaskModule.d.ts","sourceRoot":"","sources":["../src/ExpoBackgroundTaskModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuB,KAAK,YAAY,EAAE,MAAM,MAAM,CAAC;AAE9D,OAAO,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAE3G,KAAK,wBAAwB,GAAG;IAC9B,cAAc,IAAI,IAAI,CAAC;CACxB,CAAC;AAEF,OAAO,OAAO,wBAAyB,SAAQ,YAAY,CAAC,wBAAwB,CAAC;IACnF,cAAc,IAAI,OAAO,CAAC,oBAAoB,CAAC;IAC/C,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC;IAC9E,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAChD,gCAAgC,IAAI,OAAO,CAAC,OAAO,CAAC;IACpD,4BAA4B,IAAI,OAAO,CAAC,oBAAoB,CAAC;CAC9D;;AAED,wBAAmF"}
@@ -1 +1 @@
1
- {"version":3,"file":"ExpoBackgroundTaskModule.js","sourceRoot":"","sources":["../src/ExpoBackgroundTaskModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAqB,MAAM,MAAM,CAAC;AAe9D,eAAe,mBAAmB,CAA2B,oBAAoB,CAAC,CAAC","sourcesContent":["import { requireNativeModule, type NativeModule } from 'expo';\n\nimport { BackgroundTaskOptions, BackgroundTaskStatus } from './BackgroundTask.types';\n\ntype ExpoBackgroundTaskEvents = {\n onTasksExpired(): void;\n};\n\ndeclare class ExpoBackgroundTaskModule extends NativeModule<ExpoBackgroundTaskEvents> {\n getStatusAsync(): Promise<BackgroundTaskStatus>;\n registerTaskAsync(name: string, options: BackgroundTaskOptions): Promise<void>;\n unregisterTaskAsync(name: string): Promise<void>;\n triggerTaskWorkerForTestingAsync(): Promise<boolean>;\n}\n\nexport default requireNativeModule<ExpoBackgroundTaskModule>('ExpoBackgroundTask');\n"]}
1
+ {"version":3,"file":"ExpoBackgroundTaskModule.js","sourceRoot":"","sources":["../src/ExpoBackgroundTaskModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAqB,MAAM,MAAM,CAAC;AAgB9D,eAAe,mBAAmB,CAA2B,oBAAoB,CAAC,CAAC","sourcesContent":["import { requireNativeModule, type NativeModule } from 'expo';\n\nimport { BackgroundTaskOptions, BackgroundTaskStatus, SchedulerDiagnostics } from './BackgroundTask.types';\n\ntype ExpoBackgroundTaskEvents = {\n onTasksExpired(): void;\n};\n\ndeclare class ExpoBackgroundTaskModule extends NativeModule<ExpoBackgroundTaskEvents> {\n getStatusAsync(): Promise<BackgroundTaskStatus>;\n registerTaskAsync(name: string, options: BackgroundTaskOptions): Promise<void>;\n unregisterTaskAsync(name: string): Promise<void>;\n triggerTaskWorkerForTestingAsync(): Promise<boolean>;\n getSchedulerDiagnosticsAsync(): Promise<SchedulerDiagnostics>;\n}\n\nexport default requireNativeModule<ExpoBackgroundTaskModule>('ExpoBackgroundTask');\n"]}
@@ -69,12 +69,15 @@ public class BackgroundTaskModule: Module {
69
69
  return BackgroundTaskScheduler.supportsBackgroundTasks()
70
70
  ? BackgroundTaskStatus.available : .restricted
71
71
  }
72
+
73
+ AsyncFunction("getSchedulerDiagnosticsAsync") {
74
+ return await BackgroundTaskScheduler.diagnostics()
75
+ }
72
76
  }
73
77
 
78
+ // No payload validation needed — onTasksExpiredNotification is a module-private
79
+ // Notification.Name posted only by BackgroundTaskAppDelegateSubscriber.
74
80
  @objc func handleTasksExpiredNotification(_ notification: Notification) {
75
- guard let url = notification.userInfo?["url"] as? URL else {
76
- return
77
- }
78
81
  self.sendEvent(onTasksExpired, [:])
79
82
  }
80
83
  }
@@ -192,6 +192,38 @@ public class BackgroundTaskScheduler {
192
192
  return requests.contains(where: { $0.identifier == BackgroundTaskConstants.BackgroundWorkerIdentifier })
193
193
  }
194
194
 
195
+ /**
196
+ Returns diagnostic information about the scheduler state.
197
+ Useful for debugging background task scheduling issues.
198
+ */
199
+ public static func diagnostics() async -> [String: Any] {
200
+ let pending = await BGTaskScheduler.shared.pendingTaskRequests()
201
+ let matchingRequest = pending.first(where: {
202
+ $0.identifier == BackgroundTaskConstants.BackgroundWorkerIdentifier
203
+ })
204
+
205
+ var result: [String: Any] = [
206
+ "identifier": BackgroundTaskConstants.BackgroundWorkerIdentifier,
207
+ "registeredConsumers": numberOfRegisteredTasksOfThisType,
208
+ "taskType": taskType,
209
+ "intervalSeconds": intervalSeconds,
210
+ "requiresNetworkConnectivity": requiresNetwork,
211
+ "requiresExternalPower": requiresExternalPower,
212
+ "hasPendingRequest": matchingRequest != nil,
213
+ "supportsBackgroundTasks": supportsBackgroundTasks(),
214
+ ]
215
+
216
+ if let request = matchingRequest {
217
+ if let earliestBeginDate = request.earliestBeginDate {
218
+ result["earliestBeginDate"] = ISO8601DateFormatter().string(from: earliestBeginDate)
219
+ result["secondsUntilNextRun"] = earliestBeginDate.timeIntervalSinceNow
220
+ }
221
+ result["pendingRequestType"] = request is BGProcessingTaskRequest ? "processing" : "refresh"
222
+ }
223
+
224
+ return result
225
+ }
226
+
195
227
  /**
196
228
  Returns true if we're on a device that supports background tasks
197
229
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zykeco/expo-background-task",
3
- "version": "1.0.3",
3
+ "version": "1.2.0",
4
4
  "description": "Expo Android and iOS module for Background Task APIs",
5
5
  "main": "build/BackgroundTask.js",
6
6
  "types": "build/BackgroundTask.d.ts",
@@ -2,7 +2,7 @@ import { isRunningInExpoGo } from 'expo';
2
2
  import { Platform, UnavailabilityError } from 'expo-modules-core';
3
3
  import * as TaskManager from 'expo-task-manager';
4
4
 
5
- import { BackgroundTaskOptions, BackgroundTaskStatus } from './BackgroundTask.types';
5
+ import { BackgroundTaskOptions, BackgroundTaskStatus, SchedulerDiagnostics } from './BackgroundTask.types';
6
6
  import ExpoBackgroundTaskModule from './ExpoBackgroundTaskModule';
7
7
 
8
8
  // Flag to warn about running on Apple simulator
@@ -140,11 +140,34 @@ export async function triggerTaskWorkerForTestingAsync(): Promise<boolean> {
140
140
 
141
141
  // @needsAudit
142
142
  /**
143
- * Adds a listener that is called when the background executor expires. On iOS, tasks can run
144
- * for minutes, but the system can interrupt the process at any time. This listener is called
145
- * when the system decides to stop the background tasks and should be used to clean up resources
146
- * or save state. When the expiry handler is called, the main task runner is rescheduled automatically.
147
- * @platform ios
143
+ * Returns diagnostic information about the background task scheduler.
144
+ * Useful for debugging whether tasks are registered and scheduled correctly.
145
+ *
146
+ * @returns A promise that resolves with scheduler diagnostics. Fields vary by platform.
147
+ */
148
+ export async function getSchedulerDiagnosticsAsync(): Promise<SchedulerDiagnostics> {
149
+ if (!ExpoBackgroundTaskModule.getSchedulerDiagnosticsAsync) {
150
+ throw new UnavailabilityError('BackgroundTask', 'getSchedulerDiagnosticsAsync');
151
+ }
152
+ return await ExpoBackgroundTaskModule.getSchedulerDiagnosticsAsync();
153
+ }
154
+
155
+ // @needsAudit
156
+ /**
157
+ * Adds a listener that is called when the OS stops a running background task.
158
+ *
159
+ * On iOS, this fires via the BGTask expiration handler when iOS reclaims resources.
160
+ * On Android, this fires when WorkManager stops the worker (e.g., system constraints,
161
+ * memory pressure). If the event cannot be delivered in real-time, it is delivered
162
+ * when the app next enters the foreground.
163
+ *
164
+ * Use this to clean up resources or save state before the task is terminated.
165
+ * The task runner is rescheduled automatically after expiration.
166
+ *
167
+ * **Android note:** This event uses at-least-once delivery — the listener may fire
168
+ * again on next app start if real-time delivery was also successful. Listeners
169
+ * should be idempotent.
170
+ *
148
171
  * @return An object with a `remove` method to unsubscribe the listener.
149
172
  */
150
173
  export function addExpirationListener(listener: () => void): { remove: () => void } {
@@ -159,4 +182,5 @@ export {
159
182
  BackgroundTaskStatus,
160
183
  BackgroundTaskResult,
161
184
  BackgroundTaskOptions,
185
+ SchedulerDiagnostics,
162
186
  } from './BackgroundTask.types';
@@ -73,3 +73,68 @@ export type BackgroundTaskOptions = {
73
73
  */
74
74
  requiresExternalPower?: boolean;
75
75
  };
76
+
77
+ /**
78
+ * Diagnostic information about the background task scheduler state.
79
+ * Fields vary by platform — iOS-only fields are `undefined` on Android and vice versa.
80
+ */
81
+ export type SchedulerDiagnostics = {
82
+ /** Native worker/task identifier */
83
+ identifier: string;
84
+ /** Number of currently registered task consumers */
85
+ registeredConsumers: number;
86
+ /** Whether a pending background task request exists */
87
+ hasPendingRequest: boolean;
88
+ /** Whether the task requires network connectivity */
89
+ requiresNetworkConnectivity: boolean;
90
+ /**
91
+ * Whether the device supports background tasks.
92
+ * @platform ios
93
+ */
94
+ supportsBackgroundTasks?: boolean;
95
+ /**
96
+ * The BGTask type used ('refresh' or 'processing').
97
+ * @platform ios
98
+ */
99
+ taskType?: string;
100
+ /**
101
+ * Configured interval in seconds.
102
+ * @platform ios
103
+ */
104
+ intervalSeconds?: number;
105
+ /**
106
+ * Whether the task requires external power.
107
+ * @platform ios
108
+ */
109
+ requiresExternalPower?: boolean;
110
+ /**
111
+ * ISO-8601 date string of the earliest time the next task can begin.
112
+ * @platform ios
113
+ */
114
+ earliestBeginDate?: string;
115
+ /**
116
+ * Seconds until the next scheduled run (can be negative if overdue).
117
+ * @platform ios
118
+ */
119
+ secondsUntilNextRun?: number;
120
+ /**
121
+ * The type of the pending request ('refresh' or 'processing').
122
+ * @platform ios
123
+ */
124
+ pendingRequestType?: string;
125
+ /**
126
+ * Configured interval in minutes.
127
+ * @platform android
128
+ */
129
+ intervalMinutes?: number;
130
+ /**
131
+ * Whether the app is currently in the foreground.
132
+ * @platform android
133
+ */
134
+ inForeground?: boolean;
135
+ /**
136
+ * WorkManager worker state (e.g. 'ENQUEUED', 'RUNNING').
137
+ * @platform android
138
+ */
139
+ workerState?: string;
140
+ };
@@ -1,6 +1,6 @@
1
1
  import { requireNativeModule, type NativeModule } from 'expo';
2
2
 
3
- import { BackgroundTaskOptions, BackgroundTaskStatus } from './BackgroundTask.types';
3
+ import { BackgroundTaskOptions, BackgroundTaskStatus, SchedulerDiagnostics } from './BackgroundTask.types';
4
4
 
5
5
  type ExpoBackgroundTaskEvents = {
6
6
  onTasksExpired(): void;
@@ -11,6 +11,7 @@ declare class ExpoBackgroundTaskModule extends NativeModule<ExpoBackgroundTaskEv
11
11
  registerTaskAsync(name: string, options: BackgroundTaskOptions): Promise<void>;
12
12
  unregisterTaskAsync(name: string): Promise<void>;
13
13
  triggerTaskWorkerForTestingAsync(): Promise<boolean>;
14
+ getSchedulerDiagnosticsAsync(): Promise<SchedulerDiagnostics>;
14
15
  }
15
16
 
16
17
  export default requireNativeModule<ExpoBackgroundTaskModule>('ExpoBackgroundTask');