Mob.Wake (mob_wake v0.1.2)

Copy Markdown View Source

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 nil for scheduler triggers (:refresh, :processing) and a map for :push triggers (the APNs / FCM message body).
  • Returning :ok tells 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 to Result.retry() (WorkManager will attempt again). Any other {:error, _} maps to Result.failure() on Android and success: false on 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:

TriggeriOS windowAndroid windowElixir timeout
:refresh~30svaries (WM)25 s
:processing~10 minunlimited†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

dispatch(input)

@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, where payload is the parsed APNs / FCM message body.

Return

  • :ok — handler returned :ok. Silent-APNs completions map this to UIBackgroundFetchResultNewData; scheduler firings map to setTaskCompleted(success: true).
  • {:ok, :no_data} — push-only convention: handler ran successfully but no new data resulted. Maps to UIBackgroundFetchResultNoData. 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; :retry is honored on Android as Result.retry().

pending()

@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.

register(identifier, trigger, mfa)

@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_args is prepended when the handler is invoked.

Returns :ok.

schedule(identifier, opts \\ [])

@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 — a DateTime naming the earliest time the OS should consider firing this task. Maps to BGTaskRequest.earliestBeginDate on iOS and setInitialDelay on Android.
  • :interval — a Duration for :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 to BGProcessingTaskRequest.requiresExternalPower etc. on iOS and Constraints.Builder on Android. Not all constraints are honored on all platforms — check status/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.

status(identifier)

@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() or nil
  • :next_eligible_fire — DateTime.t() or nil. The OS's own hint (not a guarantee); populated by MOB-267's status/1 NIF 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.