Separate delivery receipts from dismissals
The client acks a frame as soon as it arrives — that is how an online device stops being re-told — and the service recorded that ack in acked_at, the column the drain-to-empty rule counts. Measured on an Android 15 emulator: the owner's trigger reached the subscriber at .562 and the alert was destroyed 27 ms later, mid-siren, so the owner's disarm answered ERR_ALERT_NOT_FOUND. alarm_deliveries now carries two timestamps. delivered_at is the receipt (the WS ack frame and the offline check-in): it retires re-delivery and nothing else. acked_at is the human dismissal (ackAlarm): only that ends the alert, once every subscription's row is dismissed. Migration 011 adds the column and replaces the pending index with an undelivered one. ackAlarm also runs the drain check on every explicit dismissal, not only when this call cleared a row: a receipted row still has to be dismissed. Also fixed, found by the same probe run: db.Close left the closed pool as the process singleton (a second probe inherited it), and the lifecycle probe's three subscriptions shared one device id, which migration 010 made illegal. Verified: both probes pass against the live dev database, including the new receipt-vs-dismissal case; migration 011 applied to the dev DB.
This commit is contained in:
@@ -646,15 +646,15 @@ connect (see "Reconnect catch-up"), which reaches the native service even when
|
||||
the JS layer is not running. The 409 is the secondary path for a client that is
|
||||
running and polling but has no socket — both settle the same rows.
|
||||
|
||||
- The rows are marked acknowledged by the act of returning this response, so the
|
||||
same uninterrupted alarm cannot produce a second `409`.
|
||||
- On the live path the WebSocket `ack` frame marks them too, so an online client
|
||||
that actually received the frame is never told again.
|
||||
- The rows are marked **delivered** (`delivered_at`) by the act of returning this
|
||||
response, so the same uninterrupted alarm cannot produce a second `409`. That
|
||||
is a receipt, not a dismissal: it does not end the alert.
|
||||
- On the live path the WebSocket `ack` frame records the same receipt, so an
|
||||
online client that actually received the frame is never told again.
|
||||
- Everything is idempotent: an ack for an event the device never owed, or a
|
||||
repeated ack, updates nothing and is not an error.
|
||||
- A trigger that was already disarmed before the device checked back in is not
|
||||
news: the ledger entries are cleared and the normal body (`active: false`) is
|
||||
returned instead of a stale ring.
|
||||
news: the normal body (`active: false`) is returned instead of a stale ring.
|
||||
- `?deviceId=` selects the subscription (raw value, as sent to `joinAlert`);
|
||||
without it the caller's oldest subscription on the alert is used, mirroring the
|
||||
socket. The service hashes it before comparing.
|
||||
@@ -686,11 +686,21 @@ Response `200`:
|
||||
- Clearing is **ack-driven**, never response-driven: `alarmStatus` only *reports*
|
||||
debt (409 + frame) and clears nothing, so a 409 lost in transit does not
|
||||
forfeit the alarm — the next check-in finds the same row.
|
||||
- **Receipt and dismissal are different facts.** A client acks a frame as soon as
|
||||
it receives it (that is how an online device stops being told about the same
|
||||
alarm on every poll). That writes `delivered_at`, which retires re-delivery but
|
||||
is *not* a dismissal. Only `ackAlarm` writes `acked_at`, and only `acked_at`
|
||||
counts towards the drain-to-empty rule — so the alert survives while phones are
|
||||
still ringing even when everyone happened to be online.
|
||||
- Idempotent: a repeat ack (or an ack for an event the device never owed) returns
|
||||
`acked: 0`, never an error. The WebSocket `ack` frame settles the same rows.
|
||||
- `acked` counts the ledger rows cleared for this device. When the ack clears the
|
||||
last unacked row of the alert, the alert drains and is destroyed (reason
|
||||
`delivered`) — `destroyed: true` reports that intended outcome. Note that a
|
||||
`acked: 0`, never an error. The WebSocket `ack` frame does NOT dismiss an
|
||||
alarm — it only records the receipt.
|
||||
- `acked` counts the ledger rows dismissed for this device. When the last
|
||||
un-dismissed row of the alert is dismissed, the alert drains and is destroyed
|
||||
(reason `delivered`) — `destroyed: true` reports that intended outcome. A
|
||||
receipt (the WS `ack` frame) never counts here: it only sets `delivered_at`.
|
||||
The drain check runs on every explicit dismissal, even when this call cleared
|
||||
no row, because a receipted row is still undismissed. Note that a
|
||||
destroyed alert takes its subscriptions with it, so a device token for it stops
|
||||
resolving (the next call is `401 ERR_UNAUTHORIZED`, not `404`); clients must
|
||||
treat that as "the subscription is over", not as a retryable failure.
|
||||
|
||||
Reference in New Issue
Block a user