MonitorEvent

@ObjCName(swiftName = "MonitorEvent")
sealed interface MonitorEvent

Sealed event stream emitted by the library for instrumentation / inspection.

Every internal scheduling, dispatch, deferral, completion, and library-level error path produces one or more MonitorEvents. Consumers observe the stream through BackgroundTaskManager.events as a SharedFlow<MonitorEvent> (Swift sees it as AsyncSequence<MonitorEvent> via SKIE), or — for the imperative callback style — implement BackgrounderEventListener. Both delivery mechanisms are fed from the same emit point and receive the same events.

Delivery guarantees.

  • Synchronous to the producer. BackgrounderEventListener callbacks run inline on the dispatcher thread. Implementations must not block.

  • tryEmit on the flow. Emit into the shared flow is non-suspending — a slow collector cannot pin scheduler dispatch (CLAUDE.md §3). The backing flow uses replay = 0, extraBufferCapacity = 64, BufferOverflow.DROP_OLDEST. Late-attached collectors do not see historical events; long stalls drop the oldest unread events first.

  • Best-effort ordering per taskId. Within one task id the natural order is preserved (scheduled → started → completed → cancelled). Cross-task ordering follows the producer's interleave.

Swift bridging. SKIE renders this sealed interface as a Swift enum usable with onEnum(of:) for exhaustive switch. The payload sealed types (CancelSource, DeferralReason, SkipReason, AttemptFailureReason) are intentionally top-level — nested sealed types inside a sealed parent bridge unreliably (LESSONS.md D-004).

Inheritors

Types

Link copied to clipboard
data class AttemptDeferred(val taskId: String, val at: Instant, val attempt: Int, val reason: DeferralReason) : MonitorEvent

The platform fired the worker but the library deferred execution because a predicate was not met. Followed (after the library converts to retry) by a WorkCompleted with WorkResult.Retry. See DeferralReason for the discriminator.

Link copied to clipboard
data class AttemptFailed(val taskId: String, val at: Instant, val attempt: Int, val reason: AttemptFailureReason) : MonitorEvent

An attempt failed in a way that is not the worker returning WorkResult.Failure: the OS expired the task, the factory threw, the worker threw an uncaught exception. The library converts these to WorkResult.Retry (or WorkResult.Failure on max-attempts exhaustion) internally; this event surfaces the original cause.

Link copied to clipboard
data class Cancelled(val taskId: String, val at: Instant, val source: CancelSource) : MonitorEvent

The task was removed. source discriminates user-initiated cancellation from library-internal teardown (shutdown) and replacement-triggered cancellation.

Link copied to clipboard
data class LibraryError(val taskId: String, val at: Instant, val message: String, val cause: Throwable?, val causeMessage: String? = cause?.message, val causeType: String? = cause?.let { it::class.simpleName }) : MonitorEvent

Library-internal error that does not fit the per-attempt frame — for example BGTaskScheduler.submit rejecting a request, or a platform-level callback throwing. The library has already handled the error (logged, possibly rejected the schedule); this event surfaces what would otherwise be Kermit-only.

Link copied to clipboard
data class RetryScheduled(val taskId: String, val at: Instant, val nextAttempt: Int, val delay: Duration, val nextRunHint: Instant?) : MonitorEvent

The library scheduled a retry for nextAttempt after a failed attempt. Emitted just before resubmitting to the platform.

Link copied to clipboard
data class Scheduled(val taskId: String, val at: Instant, val request: WorkRequest) : MonitorEvent

A new request was accepted by the scheduler. Fires for both fresh registrations and Replace-policy overwrites; for Replace, ScheduleReplaced is also emitted with the previous request.

Link copied to clipboard
data class ScheduleReplaced(val taskId: String, val at: Instant, val policy: ConflictPolicy, val current: WorkRequest) : MonitorEvent

A pre-existing scheduled request for this id was displaced by a new one. Emitted alongside (and immediately before) a fresh Scheduled. Not emitted when policy is ConflictPolicy.Keep and the existing request wins the race — that path is silent (the user-supplied request had no observable effect).

Link copied to clipboard
data class Skipped(val taskId: String, val at: Instant, val reason: SkipReason) : MonitorEvent

The library would have run the worker but skipped entirely — for structural reasons that prevent any future attempt from succeeding (no factory, declined factory, ephemeral wash). No retry follows.

Link copied to clipboard
data class WorkCompleted(val taskId: String, val at: Instant, val attempt: Int, val result: WorkResult, val runtime: Duration) : MonitorEvent

The worker's BackgroundWorker.execute returned (regardless of WorkResult). runtime is wall-clock duration measured from the paired WorkStarted.

Link copied to clipboard
data class WorkStarted(val taskId: String, val at: Instant, val attempt: Int, val expectedAt: Instant?) : MonitorEvent

The platform fired the worker and execution is about to begin.

Properties

Link copied to clipboard
abstract val at: Instant

Wall-clock time the event was emitted.

Link copied to clipboard
abstract val taskId: String

The task this event belongs to.