Skip to content
GenericJamPublic

About

OS-triggered background execution for Mob apps — iOS BGTaskScheduler + silent APNs, Android WorkManager + FCM data. Cross-platform dispatch surface.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

mob_wake

Status: scaffold. The public API contract is stable enough to code against but function bodies raise not yet implemented until the native trigger paths land. Follow MOB-257 for shipping order.

OS-triggered background execution for Mob apps. Cross-platform surface (Mob.Wake), per-platform NIFs, one dispatch mechanism regardless of trigger source: a compile-time identifier → MFA table, native wakes the BEAM, we run to completion, native marks the task done.

What this is (and isn't)

Is: handlers the OS wakes on our behalf.

  • iOS BGTaskScheduler — BGAppRefreshTask, BGProcessingTask
  • iOS silent APNs push — content-available: 1
  • Android WorkManager — OneTimeWorkRequest, PeriodicWorkRequest
  • Android FCM data messages — high-priority, bypass Doze

All four dispatch through the same Mob.Wake.dispatch/1 entry point.

Isn't:

  • Not mob_background (keep-alive pattern — silent-audio session on iOS, foreground service on Android). Different lifecycle, different entitlements, different user-visibility rules.
  • Not the send-side of push. mob_push (in progress) handles the server-to-device push; mob_wake receives it. They coordinate on identifier schemes; they don't overlap.

Read this before you commit to a background flow

The reliability story is not what most people assume.

  • iOS BGTaskScheduler is opportunistic. No guaranteed schedule. iOS learns each user's per-app usage pattern and decides IF and WHEN to fire. Users who open your app once a week get almost nothing. Apple's docs are explicit: not a schedule.
  • Android WorkManager delivers against constraints... until Samsung's DeviceCare, Xiaomi's MIUI Autostart, or Huawei's Protected Apps kill your background work. Default-on on all three. Users have to whitelist manually.
  • Silent push is the more reliable wake mechanism on both platforms. APNs content-available: 1 gives you a real ~30s window; FCM data at priority high bypasses Doze. When timing matters, prefer push over scheduler.
  • Direction of travel: both platforms have been moving away from scheduled polling and toward event-driven wake since ~2015. Scheduler APIs won't disappear but they get more discretionary each release.

If your feature genuinely requires guaranteed periodic execution — accounting reconciliation, medical dose reminders — a background wake plugin is the wrong shape and you should be talking to a server-side scheduler with a push relay.

Install (once shipped)

def deps do
  [
    {:mob,      "~> 0.9 and >= 0.9.15"},
    {:mob_wake, "~> 0.1"}
  ]
end

In mob.exs:

config :mob, :plugins, [:mob_wake]

# The task table drives both codegen (Info.plist, AndroidManifest.xml,
# WorkerFactory registration) and runtime dispatch.
config :mob_wake, tasks: [
  {:sync_notes,   MyApp.BackgroundJobs, :sync_notes,   :refresh},
  {:cleanup,      MyApp.BackgroundJobs, :cleanup,      :processing},
  {:on_new_peer,  MyApp.BackgroundJobs, :handle_peer,  :push}
]

Then write the handlers as normal functions:

defmodule MyApp.BackgroundJobs do
  def sync_notes do
    # Runs when iOS BGAppRefreshTask fires or Android OneTimeWorkRequest fires.
    # Return :ok on success, {:error, term} on failure. The native side reads
    # the return value to call setTaskCompleted(success:) correctly, so iOS's
    # opportunistic scheduler learns to trust (or distrust) us for future fires.
    :ok
  end

  def cleanup, do: :ok

  def handle_peer(%{payload: payload}) do
    # :push-triggered handlers can accept a %{payload: _} map — that's the
    # APNs/FCM message body. Otherwise called with no args, same as
    # :refresh / :processing handlers.
    IO.inspect(payload)
    :ok
  end
end

AppDelegate wiring (iOS, until MOB-265 codegen lands)

Until mob_new's codegen (MOB-265) writes this automatically from your config :mob_wake, :tasks table, add the following to your generated ios/AppDelegate.m:

// Near the top of the file:
#import <BackgroundTasks/BackgroundTasks.h>
@interface MobWakeDispatcher : NSObject
+ (void)registerTaskWithIdentifier:(NSString *)identifier trigger:(NSString *)trigger;
@end

// Inside `application:didFinishLaunchingWithOptions:`, BEFORE `return YES;`:
if (@available(iOS 13.0, *)) {
  // One line per identifier in your `config :mob_wake, :tasks`. iOS
  // rejects registration attempted after `didFinishLaunching` returns.
  [MobWakeDispatcher registerTaskWithIdentifier:@"com.myapp.sync_notes" trigger:@"refresh"];
  [MobWakeDispatcher registerTaskWithIdentifier:@"com.myapp.cleanup"    trigger:@"processing"];
}

// For silent APNs (:push-triggered identifiers), also add:
- (void)application:(UIApplication *)application
    didReceiveRemoteNotification:(NSDictionary *)userInfo
        fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
  // mob_wake routes on the top-level "mob_wake_id" key in userInfo.
  // Your server's silent-push payload MUST include it (mob_push, when
  // shipped, will enforce this convention on the send side).
  [MobWakeDispatcher onPushFired:userInfo completionHandler:completionHandler];
}

And in your Info.plist, add each identifier to BGTaskSchedulerPermittedIdentifiers:

<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
  <string>com.myapp.sync_notes</string>
  <string>com.myapp.cleanup</string>
</array>
<key>UIBackgroundModes</key>
<array>
  <string>fetch</string>                 <!-- for :refresh triggers -->
  <string>processing</string>            <!-- for :processing triggers -->
  <string>remote-notification</string>   <!-- for :push triggers -->
</array>

For silent APNs, the server-side payload must include a top-level mob_wake_id key naming the identifier, plus aps.content-available: 1:

{
  "aps": { "content-available": 1 },
  "mob_wake_id": "com.myapp.sync_from_peer",
  "peer": "abc"
}

Mob.Wake.dispatch/1 will invoke the registered handler with the whole payload as a JSON binary — decode with Jason.decode!/1 or :json.decode/1 (Elixir 1.18+) in your handler if you want a map.

Public API

Under the Mob.Wake namespace. Full contract in the module's @moduledoc.

  • Mob.Wake.register/2 — usually called from generated boot code
  • Mob.Wake.schedule/2 — enqueue a fire (BGTaskScheduler / WorkManager)
  • Mob.Wake.dispatch/1 — called by the native side when the OS fires
  • Mob.Wake.status/1 — health + platform-specific reliability signals
  • Mob.Wake.pending/0 — inventory of currently pending fires

MobWake.SelfTest is the plugin's on-device self-test: run mix mob.selftest from a host app (mob_dev 0.7.17+) to prove the NIF and the Android bridge answer on a device.

Related plugins

  • mob_push — the send side of silent APNs / FCM. When you want deterministic wake timing, mob_push sends the trigger and mob_wake receives it. Use MobWake.wake_payload/2 to build the payload for MobPush.send/3 — same shape on both platforms (see the identifier-and-payload ADR).

    # Server-side
    payload = MobWake.wake_payload(:sync_notes, data: %{"peer" => "abc"})
    MobPush.send(ios_token, :ios, payload)
    MobPush.send(android_token, :android, payload)
  • mob_background (already on Hex, 0.1.0) — the keep-alive pattern. Different concept, different lifecycle. Read its README if you're trying to decide which one you want.

Cross-references

License

MIT.

About

OS-triggered background execution for Mob apps — iOS BGTaskScheduler + silent APNs, Android WorkManager + FCM data. Cross-platform dispatch surface.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages