lightninglabs / lightninglabs/wavelength
mobile: push-driven mailbox wake-and-drain lifecycle
@darioAnongba is already working on this.
Since Sep 18, 2026.
- Dominant language
- Go
- Stars
- 47
- Forks
- 14
- Avg merge
- 2d 6h
- Merged PRs (30d)
- 103
Description
Summary
Mobile clients cannot keep the serverconn ingress loop running as an
always-on background goroutine: the OS suspends the app. We need a
wake-and-drain model, where the operator (through a push service such as
Apple Push Notification service (APNs) or Firebase Cloud Messaging (FCM))
wakes the device when its mailbox has new messages, and the app drains its
mailbox once and goes back to sleep.
This is the deferred open question #2 from #713 (the mobile bindings issue,
implemented in #801). The bindings work and run end to end, but they ship the
always-on ingress loop unchanged. This issue scopes the lifecycle and wake
model that should sit on top, what darepo-client must expose, and what lives
server-side in lightninglabs/darepo.
How the client receives today
The ingress loop (serverconn/ingress.go:28, ingressLoop) runs continuously
in a background goroutine: pull a batch (pullBatch, :287) with a long-poll,
dispatch it (dispatchBatch, :325), checkpoint, ack, and back off
exponentially to self-heal across dropped connections. Cursor state is the
four-cursor AckState (mailbox/conn/ack_state.go): PullCursor,
DispatchCommittedTo, AckTarget, AckCommittedTo, with the invariant that
AckCommittedTo never advances past DispatchCommittedTo (never ack past
non-durable local work). It is persisted as a TLV checkpoint, so a process that
wakes cold resumes from the right cursor and the server redelivers any unacked
envelopes. The loop is owned by StartIngress / StopIngress
(serverconn/runtime.go:101, serverconn/actor.go:1154/:1178).
That shape is correct for a daemon. It is wrong for a phone: a frozen app
cannot hold a long-poll, and waking the radio to poll on a timer drains the
battery.
The wake-and-drain model
- The operator appends an envelope to client C's mailbox.
- The operator, or an intermediary push relay, sends a (ideally silent)
push to C's device: APNs on iOS, FCM on Android. - The OS wakes the app for a bounded window.
- The app drains its mailbox once (pull, dispatch, checkpoint, ack until
empty) and returns to sleep.
This needs pieces on both sides.
Client side (this repo)
- Factor the loop body into a reusable step, e.g.
processOnce(ctx, state) -> (state, outcome, error), that both the
background loop and a host-driven pump call. The background loop keeps
running it with backoff; mobile calls it on wake. It must preserve the
existing invariants (checkpoint after dispatch, never ack past
DispatchCommittedTo) and report enough inoutcomefor the host to decide
whether to drain again, back off, or sleep (for example
PulledAndDispatched/Drained/TransientError). - Expose a wake-and-drain entrypoint up the stack:
serverconn->
walletdk-> the mobile facade (aProcessNextMsg/drainMailboxcall,
mirrored in the Kotlin and SwiftWalletClient). The facade should hide
AckStatethreading; the cursor already persists via the checkpoint store,
so the host passes nothing. - Foreground vs background policy: while foregrounded the existing
always-on loop is fine; the host callsStopIngresson background and
drains on push. Decide whether mobile defaults to "loop while foreground,
drain on push in background" or "always drain on push." - Device token registration: the client must hand its push token to the
operator so it can be notified. That is a new RPC the client calls once it
knows its mailbox id, around ingress start, and again on token rotation.
Server side (lightninglabs/darepo)
- A hook that fires when an envelope is appended to a client's mailbox, so
a push can be emitted. The internal post-commit wake pattern in
db/actordelivery/mailbox_wake_context.go(withMailboxEnqueueSet/
noteMailboxEnqueued, used today to wake outbox publishing) is the right
shape to reuse: register a "notify device for mailbox X" callback on commit. - A device-token registry keyed by mailbox id, plus the actual APNs/FCM
sender (or a thin push relay the operator notifies). None of this exists in
darepo-clienttoday (a grep finds no push/apns/fcm), and most of it
belongs in the operator repo.
What exists vs what is new
Exists: the full pull/dispatch/checkpoint/ack loop, the four-cursor
watermark and its invariant, TLV checkpoint persistence with cold resume,
backoff self-heal, and the internal post-commit mailbox-wake pattern.
New: the processOnce factoring plus a host-driven drain entrypoint
(serverconn -> walletdk -> mobile facade -> Kotlin/Swift wrappers); the
device-token registration RPC; the operator-side push hook, token registry,
and sender (in darepo); and the APNs/FCM plumbing in the host apps.
Open questions
- Drain granularity: one batch per call (host loops until
Drained) or
drain-to-empty in one call? The latter is simpler for the host but holds the
wake window longer. - Foreground policy: keep the always-on loop while foregrounded and only
drain on push in the background, or drain on push always? - Token transport: which RPC carries the token (walletdkrpc vs
daemonrpc), and how re-registration on rotation works. - Who runs the push service: the operator directly, or a separate relay
the operator notifies (which keeps APNs/FCM credentials out of the
operator)? A push reveals "this mailbox got a message" to the push
provider, so privacy matters here. - Silent push reliability: iOS silent pushes are throttled and
best-effort. Do we need a foreground fallback or a periodic catch-up drain? - Idempotency on duplicate wakes: the loop is already idempotent
(redelivery plus actor dedup); confirm a double-drain is harmless and cheap. - Layering: does any of
processOncebelong in a lower layer than
serverconn(for examplemailbox/conn) so non-walletdk consumers can
reuse it?
References
- #713 (mobile bindings; open question #2 was this pump) and #801 (bindings PR).
serverconn/ingress.go(ingressLoop/pullBatch/dispatchBatch),
serverconn/runtime.goandserverconn/actor.go(StartIngress/
StopIngress).mailbox/conn/ack_state.go(four-cursor watermark and invariant).db/actordelivery/mailbox_wake_context.go(post-commit mailbox-wake pattern
to reuse server-side).docs/mailbox_transport_serverconn_clientconn.md,
docs/RPC_MAILBOX_CONTRACT.md,docs/mailbox_architecture.md.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.