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 | Provenance | Actions |
|---|---|---|---|---|
| sillo_wire-1.0.0.tar.gz | 87.6 kB | Without provenance | Details for sillo_wire-1.0.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| sillo_wire-1.0.0-py3-none-any.whl | Python 3 | none | any | Details for sillo_wire-1.0.0-py3-none-any.whl |
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}
|
Security for sillo-wire 1.0.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.