Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

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

The two import paths

sillo.wire and sillo_wire name the same objects. The code lives in the top-level sillo_wire package; sillo.wire is an alias, so it reads as part of the framework:

from sillo.wire import Hub     # both of these
from sillo_wire import Hub     # bind the same class

The alias is a meta-path finder registered by a .pth at interpreter startup — the only hook that runs before an import sillo.wire could fail. Type checkers never run import hooks, so they are served separately by the partial stubs in sillo-stubs/ (PEP 561), which are additive: mypy resolves sillo.wire and still uses the framework's own inline types for the rest of sillo.

Nothing is written into the framework's package directory. Shipping sillo/wire/ in there would be simpler, and it is what this did first — but two distributions sharing one directory goes wrong in both directions. Installing the framework from a checkout moves where sillo resolves and orphans the copy in site-packages; removing or replacing the framework leaves that directory standing with no __init__.py, which is an override rather than an addition. Uninstalling either package here leaves the other exactly as it was.

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 _sillo_wire_bootstrap.py
mypy sillo_wire

Requirements

Python 3.10+, sillo-framework 0.3 or newer. No other dependencies.

Licence

BSD-3-Clause.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

sillo_wire-0.1.0.dev1.tar.gz (90.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

sillo_wire-0.1.0.dev1-py3-none-any.whl (25.0 kB view details)

Uploaded Python 3

File details

Details for the file sillo_wire-0.1.0.dev1.tar.gz.

File metadata

  • Download URL: sillo_wire-0.1.0.dev1.tar.gz
  • Upload date:
  • Size: 90.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.12.10 {"installer":{"name":"uv","version":"0.12.10","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}

File hashes

Hashes for sillo_wire-0.1.0.dev1.tar.gz
Algorithm Hash digest
SHA256 5eb818d8dc4ba67c8acebfdded08f9c830af3f4bb98c8628d64c2c3d08c00f92
MD5 b44d4cd7a5e6281d55ca5b202c89476b
BLAKE2b-256 d3c1321644990ad2c0a62d971f0193d7be90697513ec3b3c790c433e190027c6

See more details on using hashes here.

File details

Details for the file sillo_wire-0.1.0.dev1-py3-none-any.whl.

File metadata

  • Download URL: sillo_wire-0.1.0.dev1-py3-none-any.whl
  • Upload date:
  • Size: 25.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.12.10 {"installer":{"name":"uv","version":"0.12.10","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}

File hashes

Hashes for sillo_wire-0.1.0.dev1-py3-none-any.whl
Algorithm Hash digest
SHA256 ccb3a198c24fc9f69291235b403df787490485de88ed29f1c8f7c5c73ed3f69d
MD5 bb07c94090ada5a7a67329f64ff8fd91
BLAKE2b-256 ba70e874aa9b8104efe043176a116679d60bb356da44348c1caa19d8fc572884

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.1.0.dev1 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page