Skip to content

Broadcast Topics (pub/sub)

A topic is a typed, in-process broadcast channel that decouples where a value comes from from who streams it out. Any handler publishes with Name.send(v); streaming handlers — gRPC server/bidi streams or HTTP Server-Sent Events — subscribe with for x in Name. It's the transport-independent generalization of a WebSocket hub's broadcast.

The classic use is a value posted to one endpoint that fans out to open streams — a gRPC feed that emits whenever an HTTP POST arrives, or vice-versa:

weld
type Order = { id: int @1, item: string @2 }

topic Orders -> Order                     # a typed broadcast topic

route POST "/orders" (body o: Order) -> string {
    Orders.send(o)                        # publish — fans out to every subscriber
    respond "queued"
}

listen g { port 50051 }
service feed.Feed on g {
    rpc Watch(_: Order) -> Stream<Order> {
        for o in Orders { emit o }        # subscribe: blocks, emits each published Order
    }
}

route GET "/live" -> string {             # ...the same topic over HTTP SSE
    type "text/event-stream"
    for o in Orders { emit "data: {o.id}:{o.item}\n\n" }
}

A POST /orders is now delivered to every open Watch gRPC stream and every /live SSE client at once.

Semantics

  • Broadcast (fan-out). Every subscriber receives every message published after it subscribed. There's no replay — a new subscriber only sees messages sent from then on.
  • Name.send(v) publishes one value; v must match the topic's message type. It's valid in any handler — routes, gRPC handlers, hub methods, and every timers — and never fails (see backpressure).
  • for x in Name subscribes and yields each message. It blocks between messages and ends when the client disconnects, so it belongs in a streaming handler (a gRPC stream rpc or an SSE route that emits). x is one message, decoded into the request's memory.
  • Backpressure is drop-oldest. Each subscriber has a bounded buffer; if it can't keep up, the oldest undelivered message is dropped (and counted) rather than blocking the publisher — so one slow gRPC client never stalls an HTTP POST. This mirrors how a hub skips a stuck WebSocket consumer.
  • Disconnect handling. A subscription ends when the peer closes the connection: detected immediately on the next emit (a failed write) and, for an idle topic, within a few seconds by a background sweep that probes liveness.

Scope

Topics are in-process and in-memory (a single server instance), with at-most-once delivery — ideal for live feeds, notifications, and cross-handler events within one service. They are not durable or cross-instance: a restart drops undelivered messages, and two instances don't share a topic. For durable or multi-instance messaging, put a broker (NATS/Kafka) between instances — the same topic handler code is the natural front for it.

Like hubs and timers, topics run on the threaded driver (not --green yet).