Meeseeks
Connect Store6 mutation drains to Meeseeks scheduling.
How can an offline Store6 edit keep moving after the user leaves the app? The
background-work guide explains why a mutation drain needs
execution opportunities outside the call that recorded the mutation. mutations-drain-meeseeks
connects those opportunities to Meeseeks. The Store6 mutation journal remains the record of what must
be pushed and what has been acknowledged.
Consider one offline profile edit. mutate appends the edit to a durable journal, and the UI can show
the optimistic value. The user then leaves the app. A later host or OS grant lets a
Meeseeks worker run the coordinator, which drains the journal and adopts the server acknowledgement.
When the app opens again, the UI reads the journal-backed pending state and the confirmed value. The
worker finishing does not itself prove that the mutation was acknowledged.
Before you start
Prepare the host-owned pieces before adding the adapter:
- A fully constructed
MutationStorefrom the mutations quickstart, with journal storage that survives process death. The default in-memory journal loses pending intent when the process ends. - One stable coordinator registration name, such as
com.example.users. Meeseeks persists that name inStoreDrainPayload, so the host must register the reconstructed store under the same name on a later launch. - An application-owned
CoroutineScopewhose lifetime covers the coordinator watch. - One host-owned Meeseeks
BGTaskManager. Reuse the manager already initialized by the application instead of creating an adapter-specific manager. - The required Android or iOS host configuration described below.
The candidate module declares Android, JVM, iosArm64, iosSimulatorArm64, iosX64, and JS targets
in its build configuration.
A larger commonMain target matrix cannot resolve this artifact for targets it does not publish.
JVM consumers also need Java 17 because Meeseeks 1.1.1 publishes Java 17 bytecode and exposes a public
inline API. The candidate module sets jvmToolchain(17) for that reason. Its publication metadata
declares mutations-drain-meeseeks as the
artifact id
under the repository's
org.mobilenativefoundation.store group.
How the adapter fits
The adapter has one narrow job. It maps a scheduled activation onto a bounded mutation drain:
- The host or operating system gives Meeseeks an execution opportunity.
- Meeseeks dispatches
StoreDrainWorkerwith aStoreDrainPayloadcontaining the stable store name. - The worker passes that name through
MeeseeksDrainSchedulerto its attachedMutationDrainCoordinator. - The coordinator invokes a bounded
MutationStore.drainpass for the registered store. - The mutation engine writes acknowledgement progress and remaining intent to the Store6 journal. The coordinator then derives whether another activation is needed from that journal state.
mutations-drain-meeseeks never initializes Meeseeks and does not own the mutation transport or
connectivity monitoring. The host owns those concerns. Store6 owns mutation state in its journal,
while Meeseeks owns scheduling state. A successful worker result can mean that no nonterminal intent
remains, or that remaining work already received a follow-up schedule. Dead letters are terminal and
may still exist. Use journal inspection to decide whether the edit settled.
Wire the host
Construct MeeseeksDrainScheduler before initializing Meeseeks, then return the same
BGTaskManager from its manager lambda for the scheduler's entire lifetime. Inside the host's one
Meeseeks.initialize block, register StoreDrainPayload with StoreDrainWorker. If the application
already has a manager and other workers, add this registration to that existing block.
The following is the candidate's compile-only JVM host wiring body. It expects host-provided
appContext, the fully constructed users store, and scope. The
complete source fixture
contains its exact imports. The function is not a runtime test and must not be invoked as one.
Apply @OptIn(ExperimentalStoreApi::class) to the enclosing host function, or use
@file:OptIn(org.mobilenativefoundation.store6.core.ExperimentalStoreApi::class) before the
file's package declaration. The source fixture supplies that file-level opt-in. The annotation
inside the copied region applies only to the following local declaration.
@OptIn(ExperimentalStoreApi::class) // required: the whole module is experimental
lateinit var bgTaskManager: BGTaskManager
val drainScheduler = MeeseeksDrainScheduler(manager = { bgTaskManager })
bgTaskManager =
Meeseeks.initialize(appContext) {
register<StoreDrainPayload> { workerContext ->
StoreDrainWorker(workerContext, drainScheduler)
}
}
val coordinator = mutationDrainCoordinator(drainScheduler)
coordinator.register(
"com.example.users",
users,
DrainPolicy(
constraints = DrainConstraints(
requiresNetwork = false,
requiresCharging = false,
),
),
)
val watch = scope.launch { coordinator.watch("com.example.users") }
scope.launch { coordinator.runActivation("com.example.users") }The two constraint flags are false because Meeseeks does not support Store6 network or charging
constraints on JVM. The last launch represents a host-owned activation such as application start or
reconnect. It is separate from watch, so a foreground reconnect can retry pending work even when
no new mutation is enqueued.
Meeseeks 1.1.1 initializes on JVM, but the candidate records unresolved Quartz execution and
recovery failures. Scheduled tasks do not execute with its bundled Quartz store, and a recovery scan
can fail when the Meeseeks database contains a payload type that the current process did not
register. The candidate's
jvmTest configuration
excludes the two integration suites that exercise these paths unless an opt-in Gradle property is
set. Use InProcessDrainScheduler for a JVM host until those upstream failures are resolved.
Android setup
Place the manager and coordinator at application scope. The Android Application initializes
Meeseeks, registers StoreDrainPayload, implements WorkManager's Configuration.Provider, and adds
MeeseeksWorkerFactory to a DelegatingWorkerFactory. Register the reconstructed mutation store
before starting its coordinator watch.
The candidate README contains the
full Android host example.
It follows the
Meeseeks 1.1.1 Android guide.
The candidate source documents that setup. It does not record an Android device run. Android
supports the adapter's requiresNetwork and requiresCharging constraints. WorkManager may still
defer an eligible request under Doze, App Standby, or other background restrictions. The durable
mutation journal is what lets a later execution opportunity resume from the same pending intent.
iOS setup
Initialize one application-scoped manager and register StoreDrainPayload as shown in the
candidate iOS setup.
Complete the host configuration in the
Meeseeks 1.1.1 iOS guide,
including the app refresh and processing background modes. Add both Meeseeks identifiers to
BGTaskSchedulerPermittedIdentifiers:
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>dev.mattramotar.meeseeks.task.refresh</string>
<string>dev.mattramotar.meeseeks.task.processing</string>
</array>iOS supports the adapter's network and charging constraints, but BGTaskScheduler decides when an
eligible task receives an execution opportunity. Those grants are best-effort, and force-quit can
suppress them until the next user open. Background activations also do not run while the app is in
the foreground. Connect an application-active or reachability signal to
coordinator.runActivation("com.example.users") so a reconnect without a new edit can make progress.
On the next user open, reconstruct the durable store, register the same name, then start the watch or
run reconciliation. No iOS device verification is recorded for this setup.
Platform behavior
| Platform | Scheduler | Store6 constraint handling | Practical boundary |
|---|---|---|---|
| Android | WorkManager | requiresNetwork and requiresCharging are supported. | WorkManager defers work until constraints and system background policy allow it. Doze and app restrictions can extend the delay. |
| iOS | BGTaskScheduler | requiresNetwork and requiresCharging are supported. | The operating system grants best-effort execution opportunities. Foreground reconnect and recovery after force-quit need a host trigger or the next user open. |
| JVM | Quartz | Network and charging constraints are unsupported and fail during registration. | Meeseeks 1.1.1 has known scheduled-execution gaps and recovery gaps with foreign payloads. Prefer InProcessDrainScheduler. |
| JS | Meeseeks runner | Network and charging constraints are unsupported and fail during registration. | There is no background lifetime beyond the live page or process. Prefer InProcessDrainScheduler. |
The adapter forwards the two properties exposed by Store6 DrainConstraints. It does not expose
every Meeseeks precondition or scheduling option. An iOS execution opportunity may be deferred or
never granted, and a platform activation may overlap with another host trigger. Do not treat this
candidate as a verified cross-platform at-least-once delivery guarantee or as proof that overlapping
activations are safe on every platform. The Store6 journal and an idempotent mutation endpoint remain
the correctness boundary.
The server guide defines that endpoint contract.
Recovery and troubleshooting
The worker reports an unknown registration
Reconstruct the MutationStore, construct the scheduler, and initialize Meeseeks with
register<StoreDrainPayload>. Then attach the coordinator and immediately register the store under
the persisted name before starting watch. Without the payload registration, Meeseeks cannot
dispatch StoreDrainWorker. Once the coordinator is attached, a worker that arrives before store
registration receives DrainPassOutcome.Unavailable, which maps to
TaskResult.Failure.Transient. An activation before coordinator attachment instead fails because
the scheduler is not attached. Complete host reconstruction before accepting activations, and
preserve the journal for a later attempt.
An old task no longer finds its store
Registration names are persisted in StoreDrainPayload. Keep the name stable across releases. A
rename does not rewrite already persisted scheduler payloads.
Foreground reconnect does nothing
watch reacts to a new MutationEnqueued event. A reconnect without another edit needs an explicit
runActivation from the host's application-active or reachability hook. Call it outside
MutationServer, mutator, conflict-policy, source-of-truth, and watch-handler code because the drain
path uses a non-reentrant per-store mutex.
Registration rejects the default constraints
DrainConstraints defaults requiresNetwork to true. JVM and JS reject that constraint during
coordinator.register instead of silently dropping it. Set both flags to false only when that is
correct for the host, or use InProcessDrainScheduler.
Pending work disappears after restart
The default mutation journal is in memory. Install durable journal storage before relying on process restart, reboot, or a later OS grant. Meeseeks scheduling persistence cannot recreate mutation intent that Store6 did not persist.
JVM recovery fails while listing tasks
The candidate records a Meeseeks 1.1.1 failure when its database contains a payload type that the
current process has not registered. listTasks throws IllegalArgumentException with
Unknown payload type id, which prevents the adapter recovery scan from reaching its Store6 task.
This is unresolved. Use InProcessDrainScheduler rather than relying on JVM scheduled recovery.
The worker succeeds while work remains
TaskResult.Success can mean the drain cleared or that the coordinator already scheduled a later
pass for remaining work. It can also coexist with dead letters because dead letters are terminal.
Read pendingWrites() and deadLetters() to determine the mutation state. A worker result is neither
a server receipt nor a settlement signal.
Scheduling appears duplicated
The scheduler tracks a logical pending slot per store name, but recovery can find multiple platform tasks for that name. A tracked slot does not establish one worker invocation per mutation. Keep the server idempotent, inspect the Store6 journal after each pass, and verify overlapping activations on each platform the application uses.
Verify your host
Run the complete user journey in the application that owns the manager and mutation store:
- Disable connectivity, enqueue an edit, and confirm that
pendingWrites()contains the durable intent before ending the process. - Restore connectivity while the app is foregrounded without creating another edit. Trigger the application-active or reachability hook and confirm that the existing intent is drained.
- Repeat the offline edit, leave the app, and reopen it. Confirm that reconstruction uses the same journal and registration name before the first activation.
- Cancel or unregister while an activation is pending, then inspect its durable phase,
pendingWrites(), anddeadLetters(). Re-register and activate again to prove recovery without discarding intent. An in-flight pass may have advanced the journal before cancellation. - On an actual Android or iOS device, wait for a background execution opportunity under the intended network and charging conditions. A simulator command or compile check does not prove that the OS will grant background time in production.
- After every activation, inspect
pendingWrites()anddeadLetters(). Backend receipts and the confirmed UI value should agree with those journal-backed facts.
The JVM fixture establishes the wiring shape only. Compilation does not establish scheduled execution, process recovery, cancellation behavior on a device, or an OS background grant.
Use Inspection and observability to make journal-backed mutation state the final check.