Menu 0.6.0 preview

Integrate

Agent Docs: Delivery issues

Delivery issues

Your user’s old laptop runs last year’s version of your app, and their phone runs this week’s. The phone sends a note the laptop can’t read yet. Nothing has gone wrong in a way your app could fix, but the user should hear that the laptop needs an update. That is what the delivery-issue stream is for.

The delivery-issue stream

onDeliveryIssue(listener) registers a listener for every message the library couldn’t deliver or handle, on this device or on its way out of it. It returns a handle that cancels the listener. Listening is optional, and ignoring the stream is safe.

Issues stay on the device and are never sent to another device or to the relay. The relay can still tell that this device runs an older protocol version, from the messages it leaves unacknowledged.

Incoming messages

CaseWhat happenedWhat to do
unreadable(malformed) A message the library can't use was acknowledged and dropped. Its bytes couldn't be decoded or authenticated, or it carried an unknown frame type or message tag. Nothing. Log it if you collect diagnostics.
unreadable(senderOutdated) A message the library can't use was acknowledged and dropped. Another device sent it in an older End-to-End Version than this device supports. Tell the user one of their other devices needs an update.
unreadable(expired) A message the library can't use was acknowledged and dropped. Its Sender Timestamp is older than the Replay Window. Replay Window: 60 days. Nothing. The data is lost on this device. If it matters, ask another device to send its current state again.
unreadable(droppedWhileHeld) A message the library can't use was acknowledged and dropped. More newer-version messages than the Held-Blob cap were waiting for this device, and this was the oldest. Held-Blob cap: 100 Blobs per device. Ask the user to update the app now. Messages are already being lost.
unauthorized A message failed the pairing or membership rules, for example its sender isn't a member, and was acknowledged and dropped. Nothing. Log it if you collect diagnostics.
heldForUpgrade(version) A message in a newer End-to-End Version waits on the relay, unacknowledged, until this device upgrades. Ask the user to update the app. After the upgrade the message is handled, out of order with the messages around it.
handlerGaveUp(messageId, error) The message handler threw on every attempt, and the message was acknowledged and dropped. Handler attempts: 5 attempts. Treat it as a bug in the handler. Log messageId and report error. If the data matters, ask the sending device to send it again.

heldForUpgrade is the first exception to the delivery promise. A held message reaches your handler after messages that were sent later. unreadable(droppedWhileHeld) is what happens if the device doesn’t upgrade in time.

Outgoing messages

The relay answers a refused push with a typed code that carries no content. A temporary refusal (the recipient’s inbox is full, the relay is limiting the rate, or the relay is unavailable or hit an internal error) keeps the message in the outbox, and the library retries it with backoff. A permanent refusal removes it.

CaseWhat happenedWhat to do
sendFailed(messageId, tooLarge) An outbox entry was removed for good, so the recipient never gets that message. Messages sent after it still arrive. The relay refused it because it is over the relay's Relay Size Limit, which an Operator may set below the Protocol Size Limit. Tell the user the message wasn't delivered. Send smaller messages, or pass a matching lower maxPayloadBytes to open() so send() refuses them up front. maxPayloadBytes counts the body, and the Relay Size Limit counts the sealed envelope.
sendFailed(messageId, malformed) An outbox entry was removed for good, so the recipient never gets that message. Messages sent after it still arrive. The relay refused it because it couldn't parse it. Treat it as a bug and report it.
sendFailed(messageId, expired) An outbox entry was removed for good, so the recipient never gets that message. Messages sent after it still arrive. It was a Sync entry still in the outbox after the Outbox expiry, for example because the device was offline all that time. Outbox expiry: 30 days. Tell the user it wasn't delivered, and send it again if it still matters.

Limits and quotas explains why the relay’s limit and the Protocol Size Limit can differ.

sendFailed is the second exception to the delivery promise. The recipient never gets that message, and the messages sent after it still arrive, so the recipient sees a gap.

When the relay speaks a Transport Version this library doesn’t support, connection becomes relayVersionUnsupported{min, max} and no sendFailed is raised. Messages stay in the outbox until your app ships a library that speaks the relay’s version.

Errors when sending

These are thrown by the call, before anything enters the outbox, so they never appear on the delivery-issue stream.

CaseWhat happenedWhat to do
closed A call on a TrueSeal object after close(). Open a new object with TrueSeal.open. A closed object stays closed.
notMember{status} send() while the status is not member. Check status before calling, and hide the send action until it is member. Offer pairing while it is notJoined or pendingJoin.
payloadTooLarge{max} send() with a body over max, the Protocol Size Limit or the lower maxPayloadBytes passed to open(). Nothing enters the outbox. Protocol Size Limit: 61,440 bytes (60 KiB). Send a smaller body. Split the data, or store it elsewhere and send a reference.

What isn’t reported

  • A message that waits on the relay longer than its TTL, 30 days by default, is deleted by the relay. The sender saw the relay accept it, and the recipient never sees it, so neither device gets an issue. See Limits and quotas.
  • A duplicate is dropped without an issue.
  • Messages a device hadn’t sent when it was removed, or when its group was destroyed, are dropped without an issue. See Membership and Destroy Group.

What stays your app’s job

  • Deciding which issues the user should see. Most apps show heldForUpgrade and unreadable(senderOutdated) as an update prompt, show sendFailed as an undelivered message, and log the rest.
  • Fixing the handler when handlerGaveUp appears.
  • Asking for data again when a message was lost and it matters to your app.