[feat](alarm): add a durable per-device delivery ledger

pg_notify and the socket fan-out are not durable, so an offline phone used to miss a trigger forever and no-auto-stop would leave it ringing at nobody. Write one ledger row per enabled subscription in the trigger transaction, replay what a subscription still owes on every socket connect, and clear a row only on an explicit ack (live socket ack or ackAlarm); alarmStatus is a read-only check-in that answers 409 ERR_ALARM_TRIGGERED_OFFLINE with the missed frame. Ack is idempotent, and a destroyed alert cascades its debt away so a client is never told it missed an alarm that no longer exists.
This commit is contained in:
2026-10-19 02:26:46 +01:00
parent 0334d7fe93
commit 6e6a1e9c8a
12 changed files with 1128 additions and 77 deletions
+12 -14
View File
@@ -36,26 +36,24 @@ const NotifyChannel = "alarm_events"
// the one that served the trigger request — fans the event out to its own
// subscribers.
type NotifyPayload struct {
AlertID string `json:"alertId"`
EventID string `json:"eventId"`
Action string `json:"action"`
Message string `json:"message"`
AutoStopSeconds int `json:"autoStopSeconds"`
CreatedAtMs int64 `json:"createdAtMs"`
AlertID string `json:"alertId"`
EventID string `json:"eventId"`
Action string `json:"action"`
Message string `json:"message"`
CreatedAtMs int64 `json:"createdAtMs"`
}
// Frame converts the transport payload into the wire frame. ServerTime is the
// EVENT time, not the delivery time, so every instance and every client agrees
// on when the alarm started (and therefore on the auto-stop countdown).
// on when the alarm started.
func (p NotifyPayload) Frame() AlarmFrame {
return AlarmFrame{
Type: "alarm",
AlertID: p.AlertID,
EventID: p.EventID,
Action: p.Action,
Message: p.Message,
ServerTime: time.UnixMilli(p.CreatedAtMs).UTC().Format(time.RFC3339Nano),
AutoStopSeconds: p.AutoStopSeconds,
Type: "alarm",
AlertID: p.AlertID,
EventID: p.EventID,
Action: p.Action,
Message: p.Message,
ServerTime: time.UnixMilli(p.CreatedAtMs).UTC().Format(time.RFC3339Nano),
}
}