Broadcasting¶
Introduction¶
In many modern web applications, WebSockets are used to implement real-time, live-updating interfaces. When some data is updated on the server, a message is sent over a WebSocket connection to be handled by the client. Arvel's broadcasting publishes named events on named channels through a pluggable driver, so your frontend can subscribe and react in real time.
Configuration¶
Broadcasting reads config/broadcasting.py when present; the BROADCASTING_* environment variables are the fallback for any key the file doesn't set (see the cascade):
Drivers¶
| Driver | Behavior |
|---|---|
log | Writes broadcasts to the log — good for local development |
null | Discards broadcasts (default) |
redis-pubsub | Publishes over Redis pub/sub |
pusher | Pusher-compatible service |
Warning
The pusher driver is stubbed. Use redis for real-time delivery, or log / null in development and tests.
Registering the Provider¶
Broadcasting is opt-in. Add BroadcastServiceProvider to bootstrap/providers.py. It binds the Broadcast facade; without it, the facade raises a runtime error.
Broadcasting Events¶
The ShouldBroadcast Contract¶
To make an event broadcastable, implement the ShouldBroadcast contract. It defines what channels to broadcast on, the event name, and the payload:
from collections.abc import Mapping, Sequence
from arvel.events.event import Event
from arvel.broadcasting.should_broadcast import ShouldBroadcast
class OrderShipped(Event, ShouldBroadcast):
order_id: int
def broadcast_on(self) -> Sequence[str]:
return [f"orders.{self.order_id}"]
def broadcast_as(self) -> str:
return "order.shipped"
def broadcast_with(self) -> Mapping[str, object]:
return {"order_id": self.order_id}
Broadcasting From an Event¶
When you dispatch an event that implements ShouldBroadcast, the dispatcher pushes it to the broadcast driver automatically — after the synchronous listeners finish:
from arvel.facades.event import Event
await Event.dispatch(OrderShipped(order_id=1)) # listeners run, then it broadcasts
Note
Auto-broadcast only fires when the Broadcast facade is bound. If broadcasting isn't registered, the event still dispatches to listeners and the broadcast is silently skipped.
Broadcasting Directly¶
You can also publish without an event, straight through the facade:
from arvel.facades.broadcast import Broadcast
await Broadcast.send(
channels=["orders.1"],
event="order.shipped",
payload={"order_id": 1},
)
# Or push a ShouldBroadcast object explicitly:
await Broadcast.event(OrderShipped(order_id=1))
Channels & Authorization¶
Register an authorization callback for a channel pattern with the Broadcast.channel decorator. The callback decides whether a given user may listen on a channel:
@Broadcast.channel("orders.{order_id}")
async def authorize_order(user: User, order_id: int) -> bool:
return await Order.where(id=order_id, user_id=user.id).exists()
Testing¶
BroadcasterFake records every broadcast(...) call so you can assert what was published, with no real driver involved. It exposes calls and assert_broadcasted(event_name):
from arvel.testing.broadcasting import BroadcasterFake
fake = BroadcasterFake()
await fake.broadcast(["orders.1"], "order.shipped", {"order_id": 1})
fake.assert_broadcasted("order.shipped")
assert fake.calls[0].payload == {"order_id": 1}
Note
BroadcasterFake is a driver-level fake, not a manager — Broadcast.set_manager(...) expects a BroadcastManager. To route the Broadcast facade through the fake, wrap it in a test BroadcastManager whose driver() returns the fake.