Public dispatch surface for mob_wake.
This module is what user code touches: register/3 to register a
handler dynamically, schedule/2 to enqueue a wake fire, dispatch/1
for the native side to call when the OS fires, and status/1 /
pending/0 for observability.
See MobWake's @moduledoc for the honest-reliability discussion,
configuration format, and cross-references to mob_push /
mob_background.
Handler contract
Handlers are functions the plugin invokes when the OS wakes us. Signature:
def my_handler(payload_or_nil), do: :ok | {:error, reason}
# or with extra_args baked in at registration time:
def my_handler(extra_arg_1, extra_arg_2, ..., payload_or_nil), do: ...- The payload is
nilfor scheduler triggers (:refresh,:processing) and amapfor:pushtriggers (the APNs / FCM message body). - Returning
:oktells the native side to mark the task successful (setTaskCompleted(success: true)/Result.success()). iOS's opportunistic scheduler learns from this — trust yourself to the truth. - Returning
{:error, :retry}on Android maps toResult.retry()(WorkManager will attempt again). Any other{:error, _}maps toResult.failure()on Android andsuccess: falseon iOS. - A crash or timeout is mapped to
{:error, :crashed}/{:error, :timeout}respectively — the OS is notified honestly.
Trigger sources and timeouts
Timeout enforcement runs on the Elixir side just below the OS's own
window, so dispatch/1 returns {:error, :timeout} before the OS
force-kills us and mis-attributes the failure:
| Trigger | iOS window | Android window | Elixir timeout |
|---|---|---|---|
:refresh | ~30s | varies (WM) | 25 s |
:processing | ~10 min | unlimited† | 4 min |
:push | ~30s | ~10s (FCM) | 8 s |
† Under WorkManager's constraints; OEM battery killers may terminate
sooner. status/1's :platform_signal surfaces the signal.
Overrideable per-call via dispatch/1's options (see below).
Summary
Functions
Dispatch a fire from the native side.
Inventory of all pending fires.
Register a task at runtime.
Schedule a wake fire.
Current state for one identifier.
Functions
@spec dispatch( MobWake.identifier_t() | %{identifier: MobWake.identifier_t(), payload: map()} ) :: :ok | {:ok, :no_data} | {:error, term()}
Dispatch a fire from the native side.
Called by the platform-specific NIF when the OS wakes us. Looks up
the identifier's registered MFA and invokes it inside a
Task.Supervisor with a timeout matched to the trigger's platform
window (see @moduledoc).
Return values map to the native completion API — see @moduledoc's handler contract for the shape.
Input
- A bare
identifier— scheduler firings pass this shape. - A
%{identifier: id, payload: payload}— push firings pass this, wherepayloadis the parsed APNs / FCM message body.
Return
:ok— handler returned:ok. Silent-APNs completions map this toUIBackgroundFetchResultNewData; scheduler firings map tosetTaskCompleted(success: true).{:ok, :no_data}— push-only convention: handler ran successfully but no new data resulted. Maps toUIBackgroundFetchResultNoData. Passed through from scheduler triggers as-is (they don't use it).{:error, :unknown_identifier}— no MFA registered.{:error, :timeout}— handler ran past the trigger's Elixir timeout.{:error, {:crashed, error}}— handler raised.{:error, reason}— whatever{:error, reason}the handler returned;:retryis honored on Android asResult.retry().
@spec pending() :: [map()]
Inventory of all pending fires.
Returns a list of %{identifier: id, trigger: t, state: s, last_fired_at: dt, next_eligible_fire: dt} maps, one per registered
task. Includes tasks in every state (:pending, :running, :idle)
— the caller filters if they only want pending.
The :next_eligible_fire field is populated by MOB-267; before that
it is nil for every entry.
@spec register( MobWake.identifier_t(), MobWake.trigger(), {module(), atom()} | {module(), atom(), [any()]} ) :: :ok
Register a task at runtime.
Usually not needed — config :mob_wake, :tasks seeds the table at
boot. Reach for register/3 when a plugin needs to add its own
handlers dynamically or for tests.
trigger— one of:refresh,:processing,:push.mfa—{module, function}or{module, function, extra_args}.extra_argsis prepended when the handler is invoked.
Returns :ok.
@spec schedule(MobWake.identifier_t(), keyword()) :: :ok | {:error, term()}
Schedule a wake fire.
On iOS submits a BGTaskScheduler request; on Android enqueues a
WorkManager request. :push-triggered identifiers cannot be
scheduled locally — the wake comes from a server-sent APNs / FCM
message; schedule/2 on a :push identifier raises ArgumentError.
Options
:earliest— aDateTimenaming the earliest time the OS should consider firing this task. Maps toBGTaskRequest.earliestBeginDateon iOS andsetInitialDelayon Android.:interval— aDurationfor:processing-triggered periodic work only. iOS accepts as a hint, Android as a hard interval floor.:constraints— a keyword list:charging: true,unmetered: true,idle: true. Maps toBGProcessingTaskRequest.requiresExternalPoweretc. on iOS andConstraints.Builderon Android. Not all constraints are honored on all platforms — checkstatus/1.
Return
:ok— request accepted by the OS.{:error, :not_yet_implemented}— the NIF for this platform hasn't landed yet (MOB-261..264 in progress). Elixir-side registry state is unchanged.{:error, :unknown_identifier}— nothing registered under that identifier.{:error, :cannot_schedule_push}— attempted to schedule a:push-triggered identifier.{:error, reason}— anything else the native side reported.
@spec status(MobWake.identifier_t()) :: map() | {:error, :unknown_identifier}
Current state for one identifier.
Returns a map with keys:
:state—:pending | :running | :idle:last_fired_at—DateTime.t()ornil:next_eligible_fire—DateTime.t()ornil. The OS's own hint (not a guarantee); populated by MOB-267'sstatus/1NIF enrichment.:platform_signal— per-platform reliability info; empty map until MOB-267 lands. Then Android gets%{battery_optimized: bool, work_state: term()}and iOS gets%{background_refresh_status: :available | :denied | :restricted}.
Returns {:error, :unknown_identifier} for an identifier that is not
registered.