Skip to main content
A model’s life has two nested scopes. The session is the model serving traffic: it starts before anyone is watching and ends when the runtime tears it down. A connection is one client inside that session, and a session can hold several at once. Reactor gives you a hook for each scope, so a model can tell “a new viewer joined” apart from “the session began”.

The four hooks

Each hook can be async def or plain def, and a model declares at most one of each.

Ordering

For a session serving two clients in sequence, the hooks fire in this order:
self.connected is set before the first @connected hook runs and cleared before the @disconnected hook for the last client to leave, so a run() loop gating on it always agrees with the hooks.
When a session ends, its connections are torn down wholesale. @session_ended fires, but the per-client @disconnected hooks do not. Anything that must happen for every client, however the session ends, belongs in @session_ended.

Choosing a hook

Put work in the scope whose lifetime it matches. @session_started / @session_ended for anything shared across everyone the session serves: a scene the viewers all observe, a scoreboard, a conversation history, a cache you want warm before the first client arrives. @connected / @disconnected for anything belonging to one viewer: their camera pose, their cursor, their entry in a roster. Anything that costs real money or memory — loading a checkpoint, allocating GPU buffers — belongs in load(), which runs once at startup, well before either hook.

The client handle

Add a client: ClientInfo parameter to any hook or @event handler and the runtime injects the client it concerns. It never appears in your schema, and clients never send it.
ClientInfo carries the connection id, the joined_at timestamp, and a send() coroutine that messages that one client:
The handle stays valid for as long as the client is connected, including inside its own @disconnected hook, so you can say goodbye before it goes.

Serving several clients

self.connected tracks occupancy, not identity — it is set while any client is connected, and clears only when the last one leaves. A single run() loop produces one stream that every connected client receives. That makes the common multi-client pattern a counter:
Resetting unconditionally in @connected would yank the scene out from under everyone already watching whenever somebody new joined. Guarding on the counter keeps the second viewer from disturbing the first. If you need to address clients individually, keep the handles:
Then self.send() broadcasts to everyone and client.send() reaches exactly one. See Per-client messages.

Next

Events & Messages

Commands, replies, and outbound messages.

Managing State

One event loop, shared by your loop and your handlers.