Rooms, membership and fan-out — join, leave, broadcast, send_to, and why a Hub is an object rather than a global.
A Hub is a set of rooms, and a room is a set of peers. Everything that
reaches more than one connection goes through it.
from sillo.wire import Hub
hub = Hub()It is an object, deliberately
Section titled “It is an object, deliberately”The predecessor was a class with class methods over a process-wide dictionary. Two consequences followed from that and neither was wanted: tests shared state with each other, and two tenants in one process could not be kept apart.
Two hubs are two independent worlds. Nothing is registered anywhere, so a hub created in a test is collected with the test.
tenants = {name: Hub() for name in ("acme", "globex")}await tenants["acme"].broadcast("lobby", {"msg": "only acme sees this"})Most applications have exactly one, defined at module scope beside the app.
Membership
Section titled “Membership”await hub.join(peer, "lobby") # True if newly addedawait hub.leave(peer, "lobby") # True if it was a memberawait hub.leave_all(peer) # -> ["lobby", "support"]await hub.disconnect(peer) # leave everything, then close the socketjoin starts the peer’s writer task if it is not already running, so you never
have to remember to. An empty room name raises ValueError — a room named ""
is a bug that otherwise stays invisible until nobody receives anything.
leave on a room the peer is not in is not an error. A disconnect racing a
cleanup produces exactly that, routinely, and raising would turn a normal race
into a log full of tracebacks.
disconnect is what a finally block wants: it leaves every room, fires the
leave listeners, and closes the peer.
try: async for message in socket.iter_json(): await hub.broadcast(room, message)finally: await hub.disconnect(peer)Fan-out
Section titled “Fan-out”report = await hub.broadcast("lobby", {"msg": "hello"})Every member’s queue is offered the same Envelope,
concurrently. The call does not wait for any socket to accept it — that is the
point, and it is what stops one stalled client from holding up forty others.
What comes back says what happened:
report.delivered # queued for writingreport.dropped # queue was full; see the overflow policyreport.failed # peer was already closedreport.attempted # the sum of the threeA room nobody is in is not an error. broadcast returns a report of zeroes,
and — unless you say otherwise — the message still goes to the backlog, so a
client that joins in a moment can replay it.
await hub.broadcast("lobby", {"typing": "ada"}, retain=False)retain=False keeps a message out of the backlog. Use it for anything whose
value expires: typing indicators, cursor positions, presence churn. Replaying
“ada is typing” to someone who connects four minutes later is worse than
sending nothing.
Reaching a person rather than a room
Section titled “Reaching a person rather than a room”await hub.send_to("ada", {"notice": "your export is ready"})send_to delivers to every peer with that identity, across every room — one
person with a phone and two tabs is three peers and one identity. It returns
the same DeliveryReport. See Presence.
Inspecting
Section titled “Inspecting”hub.rooms() # ["lobby", "support"]hub.members("lobby") # [Peer, Peer, ...]hub.identities("lobby") # ["ada", "bob"] — deduplicated, Nones droppedhub.count("lobby") # 12hub.count() # every distinct peer in the hubAll four are synchronous and return snapshots — plain lists you can iterate without holding anything.
Housekeeping
Section titled “Housekeeping”await hub.prune() # remove closed peers; returns the ones removedawait hub.close() # close every peer and empty every roomA peer whose socket closed without a disconnect stays in its rooms until
something notices. Broadcasts to it count as failed rather than raising, so
nothing breaks — but the membership counts drift. prune is what a periodic
task calls to tidy that up.
close is for shutdown. It clears the rooms first and then closes every peer
concurrently: closing a thousand sockets one after another makes shutdown
as slow as the slowest one.
@app.on_shutdownasync def stop(): await hub.close()Presence hooks
Section titled “Presence hooks”@hub.on_joinasync def joined(room, peer): await hub.broadcast(room, {"event": "joined", "who": peer.identity}, retain=False)Covered in full under Presence.
History
Section titled “History”A hub with a backlog can answer what a room has recently seen, and replay it into a reconnecting peer.
await hub.history("lobby", limit=50) # newest 50 envelopesawait hub.replay(peer, "lobby", since=1042) # -> how many were sentawait hub.clear_history("lobby") # or clear_history() for allSee Backlog & Replay.