sillo-wire
Rooms, presence and fan-out for Sillo WebSockets.
pip install sillo-wire
Installs as sillo-wire, imports as sillo_wire.
from sillo import SilloApp
from sillo_wire import Hub, Peer
app = SilloApp()
hub = Hub()
@app.ws_route("/ws/room/{name}")
async def room(socket, name: str):
await socket.accept()
peer = Peer(socket, identity=socket.query_params.get("user"))
await hub.join(peer, name)
try:
async for message in socket.iter_json():
await hub.broadcast(name, message)
finally:
await hub.disconnect(peer)
Why this exists
Three things differ from the obvious implementation, and they are the whole point of the package.
A broadcast never blocks. Writing straight to each socket in turn means the slowest member of a room sets the pace for everyone else — a client that has stopped reading fills its kernel buffer, the write blocks, and the rest of the room waits behind it. Here every peer has a bounded queue and a writer task, so a broadcast only ever enqueues:
report = await hub.broadcast("lobby", {"msg": "hello"})
report.delivered # 41
report.dropped # 2 queues were full
report.failed # 1 socket was already gone
You get a DeliveryReport rather than nothing, because a fan-out you cannot
measure is a fan-out you cannot operate.
Nothing is global. A Hub is an ordinary object. Two of them are two
independent worlds, so tests get a fresh one per case instead of remembering to
flush shared state, and a multi-tenant application keeps traffic apart without
a naming convention.
History is replayable. Every envelope carries a monotonic sequence, so a client that reconnects asks for what it missed rather than for everything or for nothing:
await hub.replay(peer, "lobby", since=last_seq_the_client_saw)
Slow consumers
When a peer's queue fills, what happens is a choice, not a default:
from sillo_wire import Overflow, Peer
Peer(socket, overflow=Overflow.DROP_OLDEST) # keep current — prices, cursors
Peer(socket, overflow=Overflow.DROP_NEWEST) # keep order — reconcile later
Peer(socket, overflow=Overflow.CLOSE) # disconnect and let it reconnect
Presence
@hub.on_join
async def joined(room, peer):
await hub.broadcast(room, {"event": "joined", "who": peer.identity})
hub.identities("lobby") # ["ada", "bob"] — people, not sockets
hub.count("lobby") # 5 — subscriptions
Two peers can share an identity — the same person with a phone and two tabs —
and send_to reaches all of them:
await hub.send_to("ada", {"notice": "your export is ready"})
Consumers
RoomConsumer is the class-based form. It accepts the socket, builds the peer,
joins the rooms, pumps messages, and guarantees the peer is removed from every
room when the connection ends — including when a hook raises.
from sillo_wire import Hub, RoomConsumer
hub = Hub()
class Chat(RoomConsumer):
hub = hub
async def identify(self, ctx):
return ctx.query_params.get("user")
async def rooms(self, ctx):
return [ctx.path_params["room"]]
async def on_message(self, data):
await self.broadcast({"from": self.peer.identity, "text": data})
app.add_ws_route(path="/ws/{room}", handler=Chat.as_handler())
Backlog
Retention is per room and capped by payload bytes, evicting oldest first:
from sillo_wire import Hub, MemoryBacklog, NullBacklog
Hub(backlog=MemoryBacklog(capacity_bytes=4 * 1024 * 1024))
Hub(backlog=NullBacklog()) # keep nothing — typing indicators, telemetry
Backlog is a Protocol, so a Redis or Postgres store satisfies it without
importing anything from here.
Testing
sillo_wire.testing ships the piece unit tests are missing — a socket:
from sillo_wire import Hub, Peer
from sillo_wire.testing import FakeSocket, drain
async def test_a_broadcast_reaches_the_room():
hub, socket = Hub(), FakeSocket()
peer = Peer(socket)
await hub.join(peer, "lobby")
await hub.broadcast("lobby", {"hello": True})
await drain(peer) # broadcasts enqueue; this waits for the write
assert socket.sent == [{"hello": True}]
FakeSocket(delay=…) simulates a client that is slow to read, and
FakeSocket(fail=True) one that has gone away — the two cases that are hardest
to reproduce against a real server and the two most worth testing.
Reference
Hub |
join leave leave_all disconnect broadcast send_to replay history clear_history on_join on_leave rooms members identities count prune close |
Peer |
offer send start close is_idle closed pending identity |
Envelope |
payload room seq sent_at size() |
DeliveryReport |
delivered dropped failed attempted |
Backlog |
MemoryBacklog NullBacklog, or your own |
Overflow |
DROP_OLDEST DROP_NEWEST CLOSE |
Working on it
The alias works under an editable install too — the .pth is shipped by the
editable build target as well as the wheel.
pip install -e ".[dev]"
pytest --cov # 100% required, bootstrap included
ruff check sillo_wire tests
mypy sillo_wire
Requirements
Python 3.10+, sillo-framework 1.0 or newer. No other dependencies.
Licence
BSD-3-Clause.
Metadata
Release files for sillo-wire 1.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| sillo_wire-1.0.0.tar.gz | 87.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| sillo_wire-1.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 109.1 kB
Release files / sillo_wire-1.0.0.tar.gz
| Download URL | sillo_wire-1.0.0.tar.gz |
|---|---|
| Size | 87.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
493d06a112634c9c7ed78950acf00e09dabd6def861b72559c89f03671d28bab
|
|
BLAKE2b-256 checksum How to use checksums |
bf1007014772b9211979cd54f310ce621e69df79914863a31e60b63b4ffd6fff
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.19 {"installer":{"name":"uv","version":"0.12.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / sillo_wire-1.0.0-py3-none-any.whl
| Download URL | sillo_wire-1.0.0-py3-none-any.whl |
|---|---|
| Size | 21.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
1d9495d05106283e0753e3efaf0feccead94a498770f5adab95f7cf2e3aa904c
|
|
BLAKE2b-256 checksum How to use checksums |
2a8461099ee3434b3396cce6a3eafb468816c0577f589d84e7d5658132f68ac5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.19 {"installer":{"name":"uv","version":"0.12.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|