refactor(alarms): derive the alarm-set notice from the engine alone

The announcer now records only which alarm a user action set, and when.
What the snackbar says is derived from upcoming() while a screen shows
it, re-subscribed per announcement so the first read is from after the
write. That removes the second resolve path through the engine lock
(upcomingFor is gone), the publish lock, and the flash of a pre-write
countdown. The engine is observed only while an announcement stands.

Freshness is judged when a screen picks the announcement up, so one
already showing is not cut off half-way. Consuming matches a sequence
number. Latest action wins across alarms again: the sentence names no
alarm, so a settling announcement must never land on top of a newer
action on another one. Switching an alarm off announces nothing, and a
dismissal retracts before it skips.

Refs #19
This commit is contained in:
makiolaj committed 2026-10-21 19:35:29 +02:00
1 parent ff49667d39
commit 9516dc1198
4 files changed
+106 -141

No files matched your search

+16 -19
View File
@@ -1116,25 +1116,22 @@ its own writes. `reschedule()` is called explicitly: from `ClockulaApp`, from
the receivers, and from the alarm editor. `upcoming()` runs the same resolver in
preview mode and *discards* the state it returns; a read must not write, and a
test asserts the store recorded nothing across a full collection.
`upcomingFor(alarmId)` is the same read for one alarm, on demand: the list and
the editor ask it right after a user's change to say how long until that alarm
rings. `AlarmSetCountdown` (pure, in `domain/alarm/`) decides what that says —
only a natural occurrence of an enabled alarm, rounded as the row's countdown
is, never a snooze or a ring. The `AlarmClock` intents never reach it, since
they already get the status-bar icon (#19). The editor announces a time pick
at once, but waits for repeat-day taps to settle for a second, so Mon–Fri is
one snackbar rather than five. `AlarmSetAnnouncer` is an app-wide singleton
holding the notice as *state* until a screen has shown it to the end: pick →
OK → back disposes the editor's snackbar a moment after it appears, and the
list underneath picks the same notice up. The editor's fire-time-moving
writes, their reschedule and the per-alarm settle all run on the application
scope, so leaving the editor at once neither loses the announcement nor leaves
a stored time unscheduled. A screen shows the notice only while resumed, so a
cross-fade never shows it twice; the editor shows only notices about its own
alarm; a notice is re-checked against the engine before it is shown, so an
alarm switched off or deleted by any path is never announced as set. A notice
older than ten seconds on the monotonic clock is dropped unshown rather than
shown with a stale countdown.
The "Alarm set for … from now" snackbar (#19) is built on `upcoming()` too.
`AlarmSetAnnouncer`, an app-wide singleton, records only *which* alarm a user
action in the list or the editor just set; while a resumed screen shows it,
what it says is derived from `upcoming()`, through `AlarmSetCountdown` (pure,
in `domain/alarm/`): only a natural occurrence of an enabled alarm, rounded as
the row's countdown is, never a snooze or a ring. So an alarm switched off or
deleted by any path is never announced as set, and the `AlarmClock` intents,
which already get the status-bar icon, never reach it. The announcement is
*state*, standing until a screen has shown it to the end: pick → OK → back
disposes the editor's snackbar a moment after it appears, and the list
underneath picks it up. The editor announces a time pick at once and waits a
second for repeat-day taps to settle, so Mon–Fri is one snackbar; the settle,
and the editor's fire-time-moving writes with their reschedule, run in tap
order on the application scope, so leaving the editor at once neither loses
the announcement nor leaves a stored time unscheduled. An announcement older
than ten seconds on the monotonic clock is dropped unshown.
`upcoming(refresh: Flow<Unit>)` takes its **re-read cadence as a parameter**.
The resolver needs a `now`, and `upcoming` reads one inside a `combine` of the