# 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](https://uproot.science/building/live-methods.md).

## 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.

```python
@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`):

```python
# 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:

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

Listen for specific events in JavaScript:

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

### Default event handling

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

```python
notify(player, other_player, "Hello!")
```

```javascript
uproot.receive = (data) => {
    console.log(data);  // "Hello!"
};
```

## `send_to` for server-initiated updates

Use `send_to` when you do not have a sender context (like in background tasks):

```python
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:

```python
class Diary(Page):
    @live
    async def typed(page, player, text: str):
        observer = player.other_in_group
        notify(player, observer, text)
```

```javascript
// Writer
textarea.oninput = () => {
    uproot.invoke("typed", textarea.value);
};

// Observer
uproot.receive = (text) => {
    display.innerText = text;
};
```

[See the observed_diary example](https://github.com/mrpg/uproot-examples/tree/main/observed_diary)

## Example: collaborative drawing

Multiple participants drawing on a shared canvas:

```python
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")
```

```javascript
uproot.onCustomEvent("NewStroke", (event) => {
    drawStroke(event.detail.data.points, event.detail.data.color);
});
```

[See the drawing_board example](https://github.com/mrpg/uproot-examples/tree/main/drawing_board)

## Example: real-time market

A trading interface where participants post and accept offers:

```python
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}
```

```javascript
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](https://github.com/mrpg/uproot-examples/tree/main/double_auction)

## Background tasks with `spawn`

Sometimes you need work to continue after a `@live` method returns. Examples are 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:

```python
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.

```python
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](https://github.com/mrpg/uproot-examples/tree/main/continuous) · [chat_with_claude example](https://github.com/mrpg/uproot-examples/tree/main/chat_with_claude)

## 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) |
