Skip to content

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:

  1. Return values — Data goes back to the caller only
  2. 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:

notify(player, player.session.players, market_data, event="MarketUpdate")

Listen for specific events in JavaScript:

uproot.onCustomEvent("MarketUpdate", (event) => {
    refreshMarketDisplay(event.detail.data);
});

Default event handling

Without a custom event name, data goes to uproot.receive:

notify(player, other_player, "Hello!")
uproot.receive = (data) => {
    console.log(data);  // "Hello!"
};

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);
});

See the drawing_board example

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)