Recipe: Broadcast to WebSocket clients

Goal: Fan-out a message to every connected socket.

Suave does not include a built-in bus. Track sockets yourself:

open System.Collections.Concurrent
open System.Text
open System.Threading.Tasks
open Suave
open Suave.Filters
open Suave.Operators
open Suave.WebSocket
open Suave.Sockets
open Suave.Sockets.Control

let clients = ConcurrentDictionary<System.Guid, WebSocket>()

let broadcast (text : string) =
  let data = ByteSegment (Encoding.UTF8.GetBytes text)
  for KeyValue(_, ws) in clients do
    // send returns SocketOp (ValueTask) — fire-and-forget fan-out
    ws.send Text data true |> ignore

let ws (webSocket : WebSocket) (_ : HttpContext) =
  let id = System.Guid.NewGuid()
  clients.[id] <- webSocket
  socket {
    try
      let mutable loop = true
      while loop do
        let! msg = webSocket.read()
        match msg with
        | (Text, data, true) ->
            let s = Encoding.UTF8.GetString data.Span
            // optionally call broadcast s
            do! webSocket.send Text data true
        | (Close, _, _) -> loop <- false
        | _ -> ()
    finally
      clients.TryRemove(id) |> ignore
  }

let app = path "/ws" >=> handShake ws
startWebServer defaultConfig app

For serialized connect/disconnect/send, prefer a MailboxProcessor.