events: waitlist + auto-promotion + 21 tests
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 34s

When full, ev/waitlist! queues actors FIFO (:waitlist/:unwaitlist on the
booking stream; waiting fold independent of the seat fold). ev/waitlist,
ev/waitlist-position, ev/leave-waitlist!. ev/cancel-promote! frees a seat and
auto-promotes the head of the queue to a confirmed booking. Idempotent.
240/240 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-07 05:59:19 +00:00
parent 29127d8613
commit 7446c24bde
5 changed files with 175 additions and 7 deletions

View File

@@ -40,6 +40,9 @@
((= (first xs) x) i)
(else (ev-bk-index (rest xs) x (+ i 1))))))
(define ev-bk-append (fn (xs a) (append xs (list a))))
(define ev-bk-remove (fn (xs a) (filter (fn (x) (not (= x a))) xs)))
;; ---- per-actor state association list: ((actor state) ...) in join order ----
(define
@@ -269,3 +272,101 @@
(fn
(b occ-key capacity)
(max 0 (- capacity (ev-booking-count b occ-key)))))
;; ---- waitlist ----
;; When an occurrence is full, actors join a FIFO waitlist (:waitlist /
;; :unwaitlist events on the same stream). Taking a seat (:booking / :hold)
;; removes an actor from the queue, so the waitlist fold is independent of the
;; seat fold. Cancelling/releasing a seat can auto-promote the head of the
;; queue (a :booking appended for them).
(define
ev-fold-waiting
(fn
(events)
(reduce
(fn
(acc e)
(let
((typ (persist/event-type e))
(actor (get (persist/event-data e) :actor)))
(cond
((= typ :waitlist) (if (ev-bk-member? actor acc) acc (ev-bk-append acc actor)))
((= typ :unwaitlist) (ev-bk-remove acc actor))
((= typ :booking) (ev-bk-remove acc actor))
((= typ :hold) (ev-bk-remove acc actor))
(else acc))))
(list)
events)))
;; The current waitlist queue (FIFO, oldest first).
(define
ev/waitlist
(fn (b occ-key) (ev-fold-waiting (persist/read b (ev-booking-stream occ-key)))))
;; 1-based queue position for an actor (0 if not waiting).
(define
ev/waitlist-position
(fn (b occ-key actor) (ev-seat-of (ev/waitlist b occ-key) actor)))
;; Book if a seat is free, else join the waitlist. Idempotent: already seated →
;; :already; already queued → :already-waiting.
(define
ev/waitlist!
(fn
(b occ-key capacity actor)
(let
((seats (ev-booked-actors b occ-key))
(waiting (ev/waitlist b occ-key)))
(cond
((ev-bk-member? actor seats)
{:status :already :seat (ev-seat-of seats actor) :actor actor})
((ev-bk-member? actor waiting)
{:status :already-waiting :position (ev-seat-of waiting actor) :actor actor})
(else
(let
((r (ev/book! b occ-key capacity actor)))
(if
(= (get r :status) :booked)
r
(begin
(persist/append b (ev-booking-stream occ-key) :waitlist 0 {:actor actor})
{:status :waitlisted
:position (+ (len waiting) 1)
:actor actor}))))))))
;; Leave the waitlist. :left or :not-waiting.
(define
ev/leave-waitlist!
(fn
(b occ-key actor)
(if
(ev-bk-member? actor (ev/waitlist b occ-key))
(begin
(persist/append b (ev-booking-stream occ-key) :unwaitlist 0 {:actor actor})
{:status :left :actor actor})
{:status :not-waiting :actor actor})))
;; Cancel a seat and, if that frees capacity, auto-promote the head of the
;; waitlist (a confirmed booking). Returns the cancel result plus :promoted
;; (the actor promoted, or nil).
(define
ev/cancel-promote!
(fn
(b occ-key capacity actor)
(let
((c (ev/cancel! b occ-key actor)))
(if
(= (get c :status) :cancelled)
(let
((waiting (ev/waitlist b occ-key))
(seats (ev-booked-actors b occ-key)))
(if
(and (not (empty? waiting)) (< (len seats) capacity))
(let
((promoted (first waiting)))
(begin
(persist/append b (ev-booking-stream occ-key) :booking 0 {:actor promoted})
{:status :cancelled :actor actor :promoted promoted}))
{:status :cancelled :actor actor :promoted nil}))
c))))