lightninglabs / lightninglabs/wavelength

mobile: push-driven mailbox wake-and-drain lifecycle

Open
#802 0 comments 0 reactions 1 assignee View on GitHub

@darioAnongba is already working on this.

Since Sep 18, 2026.

enhancement mailbox P2
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

  1. The operator appends an envelope to client C's mailbox.
  2. The operator, or an intermediary push relay, sends a (ideally silent)
    push to C's device: APNs on iOS, FCM on Android.
  3. The OS wakes the app for a bounded window.
  4. 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 in outcome for 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 (a ProcessNextMsg / drainMailbox call,
    mirrored in the Kotlin and Swift WalletClient). The facade should hide
    AckState threading; 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 calls StopIngress on 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-client today (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

  1. 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.
  2. Foreground policy: keep the always-on loop while foregrounded and only
    drain on push in the background, or drain on push always?
  3. Token transport: which RPC carries the token (walletdkrpc vs
    daemonrpc), and how re-registration on rotation works.
  4. 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.
  5. Silent push reliability: iOS silent pushes are throttled and
    best-effort. Do we need a foreground fallback or a periodic catch-up drain?
  6. Idempotency on duplicate wakes: the loop is already idempotent
    (redelivery plus actor dedup); confirm a double-drain is harmless and cheap.
  7. Layering: does any of processOnce belong in a lower layer than
    serverconn (for example mailbox/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.go and serverconn/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

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.