;; lib/events/booking.sx — transactional, capacity-safe booking on persist. ;; ;; Each bookable occurrence has an append-only booking stream. A booking is an ;; event in that stream; the roster is the stream replayed. Capacity safety is ;; the contract: two bookers racing for the last seat must NEVER both succeed. ;; The guarantee is delegated to persist's optimistic concurrency — ;; `persist/append-expect` appends only if the stream's last-seq still equals ;; what the booker observed; otherwise it returns a conflict value and the ;; booker retries against the advanced roster. So the capacity check + append ;; are atomic at the persist boundary, with no overbooking and no lock. ;; ;; A booking decision is made against an OBSERVED snapshot (roster + last-seq): ;; two concurrent bookers each see the same free seat, both attempt, and ;; append-expect lets exactly one win — the loser gets a conflict it retries. (define ev-booking-stream (fn (occ-key) (str "booking:" occ-key))) (define ev-bk-member? (fn (x xs) (cond ((empty? xs) false) ((= x (first xs)) true) (else (ev-bk-member? x (rest xs)))))) (define ev-bk-index (fn (xs x i) (cond ((empty? xs) -1) ((= (first xs) x) i) (else (ev-bk-index (rest xs) x (+ i 1)))))) ;; Booked actors for an occurrence, oldest first. (define ev-booked-actors (fn (b occ-key) (map (fn (e) (get (persist/event-data e) :actor)) (persist/read b (ev-booking-stream occ-key))))) (define ev-actor-booked? (fn (b occ-key actor) (ev-bk-member? actor (ev-booked-actors b occ-key)))) (define ev-booking-count (fn (b occ-key) (persist/count b (ev-booking-stream occ-key)))) ;; 1-based seat number for an actor on the roster (0 if not booked). (define ev-seat-of (fn (actors actor) (let ((i (ev-bk-index actors actor 0))) (if (< i 0) 0 (+ i 1))))) ;; One booking attempt decided against an OBSERVED snapshot: `observed-actors` ;; (the roster the booker saw) and `expected` (the last-seq it saw). Returns ;; :already / :full / :booked / :conflict. :conflict means a concurrent append ;; landed since the snapshot — the caller must re-observe and retry. (define ev/book-with-observed (fn (b occ-key capacity actor observed-actors expected) (cond ((ev-bk-member? actor observed-actors) {:seat (ev-seat-of observed-actors actor) :actor actor :status :already}) ((>= (len observed-actors) capacity) {:actor actor :capacity capacity :status :full}) (else (let ((r (persist/append-expect b (ev-booking-stream occ-key) expected :booking 0 {:actor actor}))) (if (persist/conflict? r) {:actual (persist/conflict-actual r) :actor actor :status :conflict} {:seat (+ (len observed-actors) 1) :actor actor :status :booked})))))) ;; Capacity-safe booking with retry. Observes a consistent (roster, last-seq) ;; snapshot, attempts, and retries on conflict (a concurrent booker won the ;; race) — bounded by capacity, since each successful append moves the roster ;; one seat toward full. (define ev/book! (fn (b occ-key capacity actor) (let ((res (ev/book-with-observed b occ-key capacity actor (ev-booked-actors b occ-key) (persist/last-seq b (ev-booking-stream occ-key))))) (if (= (get res :status) :conflict) (ev/book! b occ-key capacity actor) res)))) ;; The roster as a plain list of actors (oldest first). (define ev/roster (fn (b occ-key) (ev-booked-actors b occ-key))) ;; Seats remaining for an occurrence of the given capacity. (define ev/seats-left (fn (b occ-key capacity) (max 0 (- capacity (ev-booking-count b occ-key)))))