Real-time interactions¶
uproot supports real-time communication between participants through WebSockets. This page covers broadcasting updates to multiple participants. For the basics of live methods (the @live decorator and uproot.invoke), see Live methods.
Two patterns: return values vs. notifications¶
There are two ways to send data back to the browser:
- Return values — Data goes back to the caller only
- Notifications — Data is pushed to one or more participants
@live
async def post_offer(page, player, price: float):
player.my_offer = price
# Broadcast to everyone (notification)
notify(player, player.session.players, price, event="NewOffer")
# Return to the caller only
return {"posted": price}
Use return values for request-response patterns. Use notify for broadcasting to multiple participants.
Broadcasting with notify¶
notify sends data to one or more participants. By default it only delivers to
recipients currently on the sender's page (where=sender.show_page):
# Notify one player
notify(player, other_player, data)
# Notify all players in a group
notify(player, player.group.players, data)
# Notify all players in a session
notify(player, player.session.players, data)
# Notify everyone except the sender
notify(player, player.others_in_group, data)
The first argument is the sender (used to determine the current page context).
The second is the recipient(s). Use where=... to deliver regardless of the
recipient's current page, or pass a page index to target a specific page.
Custom events¶
Name your notifications with the event parameter:
Listen for specific events in JavaScript:
Default event handling¶
Without a custom event name, data goes to uproot.receive:
send_to for server-initiated updates¶
Use send_to when you don't have a sender context (like in background tasks):
from uproot.smithereens import send_to
send_to(player, data)
send_to(session.players, data, event="StatusUpdate")
Example: live text observation¶
One participant types while another watches in real-time:
class Diary(Page):
@live
async def typed(page, player, text: str):
observer = player.other_in_group
notify(player, observer, text)
// Writer
textarea.oninput = () => {
uproot.invoke("typed", textarea.value);
};
// Observer
uproot.receive = (text) => {
display.innerText = text;
};
See the observed_diary example
Example: collaborative drawing¶
Multiple participants drawing on a shared canvas:
class Draw(Page):
@live
async def stroke(page, player, points: list, color: str):
with player.session as session:
session.strokes.append({"points": points, "color": color})
# Send to others (not the sender)
notify(player, player.others_in_session, {"points": points, "color": color}, event="NewStroke")
uproot.onCustomEvent("NewStroke", (event) => {
drawStroke(event.detail.data.points, event.detail.data.color);
});
Example: real-time market¶
A trading interface where participants post and accept offers:
class Market(Page):
@live
async def post_offer(page, player, price: float):
player.my_offer = price
# Broadcast updated market to everyone
notify(player, player.session.players, list(player.session.offers), event="MarketUpdate")
return {"posted": price}
@live
async def accept_offer(page, player, seller_id: str):
seller = player.session.players.get(seller_id)
price = seller.my_offer
player.bought_at = price
seller.sold_at = price
seller.my_offer = None
# Notify the seller directly
notify(player, seller, price, event="OfferAccepted")
# Update market for everyone
notify(player, player.session.players, list(player.session.offers), event="MarketUpdate")
return {"bought_at": price}
uproot.onCustomEvent("MarketUpdate", (event) => {
renderMarket(event.detail.data);
});
uproot.onCustomEvent("OfferAccepted", (event) => {
showNotification(`Your offer was accepted at ${event.detail.data}!`);
});
See the double_auction example
Background tasks with spawn¶
Sometimes you need work to continue after a @live method returns — for example, calling an external API or running a loop that pushes updates every few seconds. spawn launches an async function in the background so it keeps running independently:
async def ask_llm(player):
response = await call_api(player.question)
player.answer = response
send_to(player, response, event="Answer")
class Question(Page):
@live
async def submit_question(page, player, text: str):
player.question = text
spawn(ask_llm(player)) # returns immediately; ask_llm keeps running
return {"status": "thinking"}
The spawned task is supervised: exceptions are logged instead of silently swallowed, and all tasks are cancelled when the server shuts down. Tasks do not survive server restarts — if your server restarts and you need the task again, re-launch it from the restart() function in your app module.
async def tick(session):
while True:
with session:
session.counter += 1
send_to(session.players, session.counter)
await asyncio.sleep(1)
async def restart():
# Re-launch background tasks after a server restart
from uproot.storage import Admin
with Admin() as admin:
for session in admin.sessions:
if session.get("counter") is not None:
spawn(tick(session))
Warning
Because spawned tasks run outside a page method, uproot does not auto-track mutations to lists and dicts. Wrap writes in a context manager: with player as p: or with session as s:.
See the continuous example · chat_with_claude example
Summary¶
| Feature | Use case |
|---|---|
notify(sender, recipients, data) |
Broadcasting updates to participants |
notify(..., event="Name") |
Named events for specific handlers |
send_to(recipients, data) |
Server-initiated updates (background tasks) |
uproot.onCustomEvent("Name", fn) |
Listening for named events |
uproot.receive = fn |
Default handler for unnamed notifications |
player.others_in_group |
All group members except sender |
player.others_in_session |
All session members except sender |
spawn(coroutine) |
Run a background task (see above) |