Server-Sent Events (SSE)¶
A Server-Sent Events stream sends events until it is done. Broadcast
sends one event to everyone listening.
A resumable event stream¶
GET /count sends a number a second, and a client that reconnects carries
on where it left off:
(* Server-Sent Events: GET /count sends a number a second, each with its id,
and a client that reconnects carries on after the last number it was sent
instead of starting again. A browser's EventSource reconnects by itself
and says where it was in the Last-Event-ID header, so resuming is the one
thing the route has to do.
What the stream sends is declared on the route, as an answer is, so the
API's document describes each event and the compiler refuses one the
route did not declare. *)
open Spindle.Syntax
let ( let* ) = Result.bind
(* The events one stream may send are a type of their own, and each kind is
declared as one of them. *)
type counting
let count : (int, counting) Spindle.Event.kind =
Spindle.Event.json "count" Wiretype.int
let count_from ~mono_clock n send =
let rec go n =
let* () = send (Spindle.Event.make ~id:(string_of_int n) count n) in
Eio.Time.Mono.sleep_span mono_clock Mtime.Span.s;
go (n + 1)
in
go n
let routes ~mono_clock =
[
Spindle.get
Spindle.Path.(s "count")
(Spindle.Returns.events Spindle.Event.[ declare count ])
(let+ last = Spindle.Header.optional "last-event-id" Spindle.Codec.int in
Ok (count_from ~mono_clock (Option.fold ~none:1 ~some:succ last)));
]
let () =
Spindle.Log.setup ();
Eio_main.run @@ fun env ->
Spindle.serve env (routes ~mono_clock:(Eio.Stdenv.mono_clock env))
- The producer is the function the route answers with,
fun send -> ...(herecount_from ~mono_clock n). It runs after the response’s head is sent, and the stream ends when it returns. - Declare each kind of event:
Event.json "count" Wiretype.int, orEvent.text "token"for plain text.type countingis declared and never defined: it is a label every kind of this stream carries, so the compiler refuses an event declared for another stream. sendanswersError Goneonce the client has gone, so a loop written withlet*ends there. It also answersError Goneafter the producer has returned, so a fiber left holdingsendwrites nothing.- Resuming: give each event an id (
Event.make ~id). A browser sends the last one back asLast-Event-IDwhen it reconnects.Event.retry ~mstells the browser how long to wait before it reconnects. - A quiet stream gets a
: keep-alivecomment every 15 seconds (Returns.events ~keep_alive).Event.commentsends one of your own. - The OpenAPI document describes each kind of event the route declares.
Broadcasting to many clients¶
Broadcast sends one event to every subscriber of a topic, within one
process:
open Spindle.Syntax
let ( let* ) = Result.bind
type room
let count : (int, room) Spindle.Event.kind =
Spindle.Event.json "count" Wiretype.int
let hub : (unit, room Spindle.Event.t) Spindle.Broadcast.t =
Spindle.Broadcast.create ~depth:16 ()
let room = Spindle.Path.str "room"
let events =
Spindle.get
Spindle.Path.(s "rooms" / room / s "events")
(Spindle.Returns.events Spindle.Event.[ declare count ])
(let+ room = Spindle.param room and+ gone = Spindle.client_gone in
Ok
(fun send ->
let sub = Spindle.Broadcast.subscribe hub ~topic:room () in
Fun.protect
~finally:(fun () -> Spindle.Broadcast.unsubscribe hub sub)
(fun () ->
let rec loop () =
match Spindle.Broadcast.next ~until:gone sub with
| Some event ->
let* () = send event in
loop ()
| None -> Ok () (* dropped as slow, unsubscribed, or gone *)
in
loop ())))
(* Anywhere else: make the event once, and everybody gets it. *)
let () =
Spindle.Broadcast.publish hub ~topic:"lobby" (Spindle.Event.make count 1)
- An event is encoded once, however many subscribers get it.
- A client that leaves a quiet topic ends its producer. The keep-alive
it can no longer be sent resolves
Spindle.client_gone, andBroadcast.next ~until:goneanswersNone, so the subscription and the connection are let go without waiting for the next event. Nothing is cancelled: a producer that does not pass~untillearns at its nextsend. - A slow subscriber is dropped, never waited for. One more than
~depthevents (16) behind getsNonefromnext. So send whole states, not changes, and a client that reconnects is up to date. - Each subscriber carries a tag (
()here), whichBroadcast.subscriberslists: who is present, say.Broadcast.closeends every subscription.
To read a stream from OCaml, see the HTTP client; to test one, see Testing event streams.