fix(calendar): fed slots stay fed across a restart

The reminder source skipped an already-fed slot only while the slot
sat in the dispatcher's pending inbox; a claw restart drained that
inbox, and every future lead of the same occurrence re-fed — a
reminder fired two or three times per calendar event. The source
keeps its own fed-slot ledger in the calendar store now (keyed by
calendar, occurrence, and lead), records each slot right after a
successful ingest, and prunes rows whose occurrence is an hour past
every lead, so a pruned row can never resurrect a reminder. The
store-backed source's ledger survives a restart; the static-list
constructor keeps an in-memory one for the process lifetime, said so
in the docs that previously claimed dedup was someone else's problem.
Store failures surface loudly (a silently lost row is a re-fired
reminder); the prune warns and continues. Offline tests cover the
once-only rule, the restart via a tempdir reopen, and the prune.
This commit is contained in:
vhaudiquet committed 2026-11-05 19:06:41 +01:00
1 parent dad99c7664
commit 5a8be19b4c
5 files changed
+371 -40

No files matched your search

@@ -14,8 +14,9 @@
//! its own; every [`crate::CalendarEvent`] carries an id that stays
//! the same across polls of the same occurrence, so any
//! implementation re-polling on its own cadence never re-fires a
//! reminder it already fed. The repetition is the consumer's
//! dispatcher's dedup, not this crate's problem.
//! reminder it already fed. The default source keeps that promise in
//! its own fed-slot ledger; a custom source leans on the stable ids
//! and its dispatcher's dedup.
//!
//! Recurrence is expanded before a poll answers, so a consumer never
//! sees a rule, only starts it can compare against its clock: