webstation-broker
DeveloperPython reference

session

Single-session state and room fanout.

Single-session state and room fanout.

The container hosts exactly one play session at a time, so session and room state are module globals.

attributelog
= logging.getLogger(__name__)
attributeSESSIONOptional[dict[str, Any]]
= None

The active play session, or None.

Shape:

\{
  "id", "active", "created_at",
  "user": \{...\}, "emulator": "pcsx2", "rom": \{...\}, "rom_file": str | None,
  "save": \{...\} | None, "callback": \{...\} | None, "multiplayer": bool,
  "controller_token", "controller_public_id",
  "invites": \{"participant": str, "readonly": str\},
  "viewers": [\{"token","public_id","user_id","anonymous","last_seen","slot",
               "mk_control","username","permission"\}...],
  "controller_slot", "mk_owner_token", "designated_speaker",
  "save_baseline": float, "emulator_obj": Emulator,
\}

token is each seat's bearer credential; public_id is a non-sensitive stand-in safe to broadcast to the room in its place (see broadcast_state).

attributeLAST_EXITOptional[dict[str, Any]]
= None

What is left of the session that just exited: {"id", "rom", "emulator_obj"}.

RomM files the exit state in its library after the teardown has answered, so the emulator that captured it has to outlive the session for the read routes to find the file. Dropped at the next activate.

attributeLAST_OUTCOMEOptional[dict[str, Any]]
= None

How the last exit of this broker process ended.

\{"session_id", "ended_at", "duration_s", "state_saved", "dump_error", "upload", "archive_path"\}, where upload is the exit report's upload mode.

Unlike LAST_EXIT it survives the next activate, so a save archive that never reached RomM stays visible on the status route until another exit replaces it. None until the first exit of this broker process.

attributeSESSION_END_FLUSH_WAIT
= 2.0

Seconds the session_ended notice gets to reach each member before its socket closes.

attributeKEYFRAME_REQUEST_GAP
= 1.0

Minimum seconds between two keyframe requests to the same member.

attributeROOMdict[str, Any]
= {'controller': None, 'viewers': {}, 'cooldowns': {}}

Live websocket connections for the room.

controller is the controller's connection info dict (or None while offline), viewers maps each online viewer's token to its connection info dict, and cooldowns tracks per-token rate limits.

attributePUBLIC_ID_HEX_CHARS
= 8

Fixed width, in ASCII characters, of every public id the broker mints.

The room's binary media frames carry the sender's id in a prefix of exactly this width and read the frame type from the byte straight after it (see room.MEDIA_ID_BYTES), so an id of any other length would put every frame's type byte at the wrong offset for every recipient.

attributeSEAT_RECLAIM_GRACE_SECONDS
= 60.0

How long a seat is protected from reclaim after its last_seen was stamped.

last_seen is stamped when the seat is minted and again when its socket drops, so the same window covers the gap before a freshly invited browser has finished loading and the gap a reload leaves behind. Without it a burst of joins at the cap keeps reclaiming seats handed out seconds earlier, and every arrival evicts the one before it instead of anybody getting in.

funcnew_public_id() -> str

Mint a broadcast-safe id at the width the room's media wire format assumes.

Returns

str

A hex id exactly PUBLIC_ID_HEX_CHARS characters wide.

func_session_id(raw) -> str

Reduce a caller-supplied id to what is safe in the export filename.

The exit archive is named after the session, and the export routes reject anything with path structure in it, so an id carrying a slash or a dot would produce an archive the parent could never fetch back.

paramrawobject

The session_id from the activate payload, of any type or None.

Returns

str

raw stringified and stripped to [A-Za-z0-9_-], at most 64

funcnew_session(payload, emulator_obj, rom_file) -> dict[str, Any]

Replace the module-level session with a fresh one built from the activate payload.

The controller starts with gamepad 1 and mouse/keyboard control; the viewer list starts empty and the save baseline is the moment of creation.

parampayloaddict[str, Any]

The activate request body; emulator is required, the rest (session_id, user, rom, save, callback, multiplayer) is optional.

paramemulator_objEmulator

The emulator instance driving this session.

paramrom_fileOptional[str]

The resolved path of the ROM file being played, or None for a session with nothing to boot from (a resumed state, or an emulator launched without a ROM). Only ever read back out for the status routes to report.

Returns

dict

The new session dict, which is also stored in SESSION.

funcretire_session() -> None

End the session, keeping what the state routes still have to answer with.

Exit captures a state and then tears the session down, but RomM only asks for that state once the teardown has replied, so clearing outright is what made every post-exit read a 409. Only the emulator is kept, and only the read routes consult it: nothing here can be played, written to or resumed.

Returns

None
funcfind_viewer(token) -> Optional[dict[str, Any]]

Look up a viewer entry in the active session by its token.

paramtokenstr

The viewer's streaming token.

Returns

typing.Optional

The viewer dict, or None when there is no session or no viewer holds

funcpublic_id_for(token) -> Optional[str]

Resolve a seat's real token to the non-sensitive id safe to broadcast for it.

paramtokenstr

A controller or viewer token already known to be valid.

Returns

typing.Optional

The matching public id, or None when there is no session or no seat

funcresolve_public_id(public_id) -> Optional[str]

Resolve a broadcast-safe public id back to the seat's real token.

The inverse of public_id_for, used to translate a room-management target (e.g. who a gamepad slot goes to) that a client can only name by its public id back into the token the rest of the session logic keys on.

parampublic_idOptional[str]

The publicId a client sent to name a room member, or None.

Returns

typing.Optional

The matching seat token, or None when there is no session, no seat

funcinvite_token(permission) -> str

Return the session's shareable invite token for a permission, minting it on first use.

The token identifies the session and the permission, not a person: every arrival on it is seated separately by add_viewer, so the same link can go to any number of friends and stays valid until the session ends.

parampermissionstr

The permission the link grants, participant or readonly.

Returns

str

The invite token for that permission.

funcfind_invite(token) -> Optional[str]

Resolve an invite token to the permission it grants.

paramtokenstr

The invite query parameter from a landing URL.

Returns

typing.Optional

The permission, or None when no session is active or the token is unknown.

func_release_seat(token) -> Optional[dict[str, Any]]

Drop a removed seat's rate-limit cooldowns and role, and detach its live connection.

Called for a seat leaving SESSION["viewers"] outside the normal disconnect path (a same-user rejoin replacing it, or the reclaim of a stale seat), both of which can happen while the seat still holds the speaker or MK role from the unvalidated set_designated_speaker/assign_mk actions, or a cooldowns entry, both cleared here. A rejoin can also evict a seat whose socket is still connected (a second tab, a reload racing the old tab's close): that connection is detached from ROOM["viewers"] here, synchronously, so nothing can look it up as the seat's connection anymore, but closing it is left to the caller. It must stay a synchronous pop rather than an awaited close here, since add_viewer's cap check right after this needs the whole admission decision to run without yielding control, or a concurrent join could interleave mid-decision and seat past the cap.

paramtokenstr

The removed seat's token.

Returns

typing.Optional

The token's live connection info, for the caller to close, or None if

funcadd_viewer(permission, user=None) -> Optional[dict[str, Any]]

Mint a viewer token and add the viewer to the active session.

A re-join by the same user (matched by id, else username) replaces the old entry and invalidates its token. If that old seat still has a live room connection (a second tab, a reload racing the first tab's close), that connection is force-closed rather than left running on a token nothing recognizes anymore. The close is deferred until every seat has already been decided (see _release_seat), so it never interleaves with a concurrent join's own admission decision.

parampermissionstr

The viewer's permission level, e.g. readonly.

paramuserOptional[dict[str, Any]]
= None

The joining user's details (id, display_name, username), or None for an anonymous viewer who gets a generated username.

Returns

typing.Optional

The new viewer dict: `{"token", "public_id", "user_id", "anonymous",

func_room_connections() -> list[dict[str, Any]]

List every live room connection, the controller's first.

Returns

list

The connection-info dicts currently registered in ROOM.

funcbroadcast_to_room(payload) -> None

Queue a JSON message for every connected room member.

Each member's outbox sends it on its own time, so a member whose connection is backed up delays nobody but itself (see outbox).

parampayloaddict[str, Any]

The JSON-serializable message, normally carrying a type key.

Returns

None
funcbroadcast_binary_to_room(payload, sender_ws) -> None

Queue a binary media frame for every connected room member except its sender.

Runs inline in the sender's receive loop, so it never waits on a recipient.

parampayloadbytes

The raw frame in the room's binary wire format.

paramsender_wsWebSocket

The websocket the frame arrived on; it is skipped.

Returns

None
funcrequest_keyframe(media_id) -> None

Ask the member streaming under media_id to encode its next video frame as a keyframe.

Called by a recipient's outbox after it dropped one of that member's video frames. Requests to one member are spaced KEYFRAME_REQUEST_GAP apart, since keyframes are the largest frames and would add to the congestion.

parammedia_idbytes

The sender's per-connection media id, as stamped on its frames.

Returns

None
funcbroadcast_state() -> None

Broadcast a state_update describing every room member to the room.

The controller is listed first, then each viewer with its online status and mouse/keyboard ownership. Members are identified by publicId, a non-sensitive stand-in for their seat token: broadcasting the real token would hand every room member, including anonymous viewers, the bearer credential needed to reconnect as (or impersonate) anyone else. While a member is connected its mediaId, the per-connection id its live socket tags binary media frames with, is included too. Does nothing when there is no session.

Each entry is assembled field by field rather than copied from the seat: everyone in the room reads this, so a seat's RomM user_id and the bookkeeping the room has no use for stay on the server side.

Returns

None
funchandle_assign_slot(viewer_token, slot) -> None

Assign a gamepad slot to a room member, or take theirs away.

A slot can only be held by one member, so whoever held it before is unassigned first. The new token map is pushed to selkies and a gamepad_change notification is broadcast for every change made, followed by a state update. Unknown tokens are logged and ignored.

A push selkies refuses is logged and the room is still told: the notification describes what the session now holds, and the next successful push carries the whole map, so nothing here is worth leaving the room's view of itself out of step with the broker's over.

paramviewer_tokenOptional[str]

The token of the member to change; the controller's own token targets the controller.

paramslotOptional[int]

The gamepad slot to assign, or None to unassign.

Returns

None
funchandle_assign_mk(target_token) -> None

Hand mouse and keyboard control to a room member.

The controller's own token and None both mean control returns to the controller. A no-op when the target already holds it; otherwise the token map is pushed to selkies and an mk_change notification and a state update are broadcast. A push selkies refuses is logged, since input keeps routing by the map it already has until some later push lands.

paramtarget_tokenOptional[str]

The token of the viewer to receive control, or None (or the controller's token) to give it back to the controller.

Returns

None
funcnotify_session_ended() -> None

Tell the room the session has ended, then close and forget every connection.

Forgetting a socket does not end it: the handler behind it stays parked in receive() forever, so every connection of every ended session would survive as a live socket and a running task. Each one is detached from ROOM first and closed after, which also means a handler waking on the close finds itself already replaced and skips its per-member cleanup: the seat, role and token-map releases it would do are moot against a session being torn down, and its departure notice would go to a room that has already been told the whole session ended. Broadcasts only queue, so the notice is given up to SESSION_END_FLUSH_WAIT to go out before the sockets close under it.

Returns

None
func_close_at_session_end(conn) -> None

Close one room socket, giving up after outbox.CLOSE_WAIT on one that cannot take it.

paramconndict[str, Any]

The room connection to close.

Returns

None