[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:
@@ -0,0 +1,50 @@
|
||||
// Copyright (C) 2026 Dcntrlized
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package actions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// completeAlertDelivery is the decision point for the second half of the
|
||||
// delivery design: "once every client is removed for an alarm, the alarm is then
|
||||
// deleted with whichever cascade deletions are relevant".
|
||||
//
|
||||
// It is DELIBERATELY UNWIRED — nothing calls it. That sentence has two readings
|
||||
// and they are different products, so the user has been asked which one is meant:
|
||||
//
|
||||
// (a) delete the ALERT. The definition, both tokens, the subscriptions and the
|
||||
// event history go with it (ON DELETE CASCADE). An alert becomes a
|
||||
// one-shot event: once every device has acknowledged, it cannot be
|
||||
// triggered again and the admin token becomes dead.
|
||||
// (b) retire only the DELIVERY record (and/or the event). The alert survives,
|
||||
// stays armed, and can be triggered again later; only the ledger bookkeeping
|
||||
// about this alarm is completed.
|
||||
//
|
||||
// Implementing either now would delete or preserve data on a reading nobody
|
||||
// confirmed, so this function reports that the decision is pending. When the
|
||||
// answer arrives it gets implemented in exactly one place and called from the
|
||||
// paths that observe the last outstanding delivery (the alarmStatus check-in and
|
||||
// the WebSocket ack), where "every client has been told" is decided atomically
|
||||
// from the ledger rather than from a client's claim.
|
||||
func completeAlertDelivery(ctx context.Context, alertID uuid.UUID) error {
|
||||
_ = ctx
|
||||
_ = alertID
|
||||
return errors.New("completeAlertDelivery: pending user decision between (a) deleting the alert as a one-shot and (b) retiring only the delivery record")
|
||||
}
|
||||
Reference in New Issue
Block a user