Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 40s
reminders.sx bridges calendar + durable rosters to notify: ev/occurrence- reminders (one per booked attendee, fires lead before start, idempotency key occ-key/recipient/lead), ev/agenda-reminders (sorted by fire-at), ev/due-reminders (fire-at <= now), ev/reminder->msg (notify wire shape), ev/agenda-digest + ev/agenda-for-p. 196/196 green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
97 lines
3.1 KiB
Plaintext
97 lines
3.1 KiB
Plaintext
;; lib/events/reminders.sx — derive reminder + digest messages from the agenda.
|
|
;;
|
|
;; Bridges the schedule (calendar) and the durable roster (booking on persist)
|
|
;; to the notification layer (notify.sx). For each booked attendee of each
|
|
;; upcoming occurrence we derive a reminder message that fires `lead` minutes
|
|
;; before the occurrence starts. Each message has a deterministic idempotency
|
|
;; key — occ-key / recipient / lead — so re-deriving over an overlapping window
|
|
;; never produces a duplicate ping (the notify transport dedups on this id).
|
|
;;
|
|
;; A reminder is a dict:
|
|
;; {:id :recipient :event :start :fire-at}
|
|
;; `ev/reminder->msg` projects it to notify's (id recipient body) wire shape.
|
|
|
|
;; Reminders for one occurrence: one per booked attendee (durable roster).
|
|
(define
|
|
ev/occurrence-reminders
|
|
(fn
|
|
(b occ lead)
|
|
(let
|
|
((occ-key (ev-occ-key occ))
|
|
(start (get occ :start))
|
|
(evid (get occ :id)))
|
|
(map (fn (actor) {:id (str occ-key "/" actor "/" lead) :event evid :start start :fire-at (- start lead) :recipient actor}) (ev/roster-occ b occ)))))
|
|
|
|
;; Insertion sort of reminder dicts ascending by :fire-at (then :id for ties).
|
|
(define
|
|
ev-rem-before?
|
|
(fn
|
|
(a c)
|
|
(cond
|
|
((< (get a :fire-at) (get c :fire-at)) true)
|
|
((> (get a :fire-at) (get c :fire-at)) false)
|
|
(else (< (get a :id) (get c :id))))))
|
|
|
|
(define
|
|
ev-rem-insert
|
|
(fn
|
|
(r sorted)
|
|
(cond
|
|
((empty? sorted) (list r))
|
|
((ev-rem-before? r (first sorted)) (cons r sorted))
|
|
(else (cons (first sorted) (ev-rem-insert r (rest sorted)))))))
|
|
|
|
(define
|
|
ev-rem-sort
|
|
(fn (rs) (reduce (fn (acc r) (ev-rem-insert r acc)) (list) rs)))
|
|
|
|
;; All reminders across the agenda in [ws, we), ascending by fire-at.
|
|
(define
|
|
ev/agenda-reminders
|
|
(fn
|
|
(b store ws we lead)
|
|
(let
|
|
((acc (list)))
|
|
(begin
|
|
(for-each
|
|
(fn
|
|
(occ)
|
|
(for-each
|
|
(fn (r) (append! acc r))
|
|
(ev/occurrence-reminders b occ lead)))
|
|
(ev/agenda store ws we))
|
|
(ev-rem-sort acc)))))
|
|
|
|
;; Reminders whose fire-at has arrived (fire-at <= now) — what a scheduler
|
|
;; should hand to the notify transport at time `now`.
|
|
(define
|
|
ev/due-reminders
|
|
(fn
|
|
(reminders now)
|
|
(filter (fn (r) (<= (get r :fire-at) now)) reminders)))
|
|
|
|
;; Project a reminder to notify's (id recipient body) wire shape.
|
|
(define
|
|
ev/reminder->msg
|
|
(fn
|
|
(r)
|
|
(list
|
|
(get r :id)
|
|
(get r :recipient)
|
|
(list :reminder (get r :event) (get r :start)))))
|
|
|
|
;; ---- digests ----
|
|
|
|
;; The occurrences `actor` is booked into (durable roster), within window.
|
|
(define
|
|
ev/agenda-for-p
|
|
(fn
|
|
(b store actor ws we)
|
|
(filter
|
|
(fn (occ) (ev-bk-member? actor (ev/roster-occ b occ)))
|
|
(ev/agenda store ws we))))
|
|
|
|
;; A single digest message summarising an actor's upcoming booked occurrences.
|
|
;; :items is ({:event :start} ...); empty when the actor has nothing booked.
|
|
(define ev/agenda-digest (fn (b store actor ws we) {:items (map (fn (occ) {:event (get occ :id) :start (get occ :start)}) (ev/agenda-for-p b store actor ws we)) :id (str actor "/digest/" ws "-" we) :recipient actor}))
|