GitHub

Example: Chat rooms

Route parameters, shared server state and live updates over Server-Sent Events.

  • 81 lines of Python
  • 2 pages
  • 2 server functions
  • 657 B page JS (gzip)
This example needs its server (database, sessions or server functions). Run it locally:
Terminal
pyweb dev examples/chat/app.pyweb

#Source

examples/chat/app.pyweb
"""Chat rooms: route parameters, server state and live updates."""

import itertools
import threading

from pyweb import App, RPCError, channel, publish, server, subscribe

app = App(title="Chat", stylesheets=["/static/app.css"])

ROOMS = ["general", "python", "random"]
_messages = {room: [] for room in ROOMS}
_ids = itertools.count(1)
_lock = threading.Lock()


def _room(room):
    if room not in _messages:
        raise RPCError("not_found", f"no room named {room!r}")
    return _messages[room]


@server
def history(room: str, after: int = 0) -> list:
    return [m for m in _room(room) if m["id"] > after][-100:]


@server
def send(room: str, author: str, text: str) -> None:
    text = text.strip()
    if not text:
        return
    with _lock:
        message = {"id": next(_ids), "author": author.strip() or "guest", "text": text[:500]}
        _room(room).append(message)
    publish("chat:" + room, message)  # every open page for this room receives it


@app.page("/", title="Chat rooms")
def Lobby():
    <main>
        <h1>Rooms</h1>
        <ul>
            for room in ROOMS:
                <li><a href={"/chat/" + room}>#{room}</a></li>
        </ul>
    </main>


@app.page("/chat/{room}", title="Chat")
def Room(room: str):
    messages = history(room)
    feed = channel("chat:" + room)  # signed on the server; lets this page listen
    author = "guest"
    draft = ""

    def received(message):
        messages = messages + [message]

    def on_mount():
        subscribe(feed, received)

    def submit():
        if draft.strip():
            send(room, author, draft)
            draft = ""

    <main>
        <p><a href="/">← Rooms</a></p>
        <h1>#{room}</h1>
        <div id="messages">
            for m in messages:
                <div class="msg"><b>{m["author"]}</b>{m["text"]}</div>
            if not messages:
                <p class="empty">No messages yet. Say hi!</p>
        </div>
        <form onsubmit={submit}>
            <input id="author" bind={author} style={{"flex": "0 0 120px"}} />
            <input id="draft" bind={draft} placeholder="Message" />
            <button id="send">Send</button>
        </form>
    </main>

#What runs where

Output of pyweb inspect: every page variable, handler and server function, where it runs and why.

Output
page Lobby  route=/
page Room  route=/chat/{room}
  browser  messages       reactive state: assigned in received(); initial value computed on the server and sent
  server   feed           computed per request on the server; value sent because browser code reads it
  browser  author         reactive state: bound to an input (line 77); literal initial value
  browser  draft          reactive state: bound to an input (line 78); literal initial value
  browser  received       event handler (compiled to JavaScript)
  browser  on_mount       event handler (compiled to JavaScript)
  browser  submit         event handler (compiled to JavaScript)
rpc POST /__pyweb/rpc/history  (room: str, after: int) -> list
rpc POST /__pyweb/rpc/send  (room: str, author: str, text: str) -> None

#Generated JavaScript

The browser modules the compiler wrote (before minification). They import the shared runtime.

JavaScript
// Room.js
import { h as $h, t as $t, dyn as $dyn, list as $list, when as $when, signal as $signal, computed as $computed, mount as $mount, onMount as $onMount, py as $py, rpc as $rpc, subscribe as $subscribe, slot as $slot, onCleanup as $onCleanup, live as $live } from "./runtime.js";
function Room($s) {
  const room = $s["room"];
  const messages = $signal("messages" in $s ? $s["messages"] : null);
  const feed = $s["feed"];
  const author = $signal("author" in $s ? $s["author"] : "guest");
  const draft = $signal("draft" in $s ? $s["draft"] : "");
  function received(message) {
    messages($py.add(messages(), [message]));
  }
  function on_mount() {
    $subscribe(feed, received);
  }
  async function submit() {
    if ($py.truth($py.m(draft(), "strip"))) {
      (await $rpc("send", {"room": room, "author": author(), "text": draft()}));
      draft("");
    }
  }
  $onMount(on_mount);
  return [$h("main", null, () => [
      $h("p", null, () => [$h("a", {"href": "/"}, () => [$t("\u2190 Rooms")])]),
      $h("h1", null, () => [
        $t("#"),
        $t($py.text(room))
      ]),
      $h("div", {"id": "messages"}, () => [
        $list(() => messages(), (m) => [$h("div", {"class": "msg"}, () => [
            $h("b", null, () => [$t($py.text($py.at(m, "author")))]),
            $t($py.text($py.at(m, "text")))
          ])]),
        $when(() => !$py.truth(messages()), () => [$h("p", {"class": "empty"}, () => [$t("No messages yet. Say hi!")])])
      ]),
      $h("form", {"onsubmit": submit}, () => [
        $h("input", {"id": "author", "$bind": author, "style": {"flex": "0 0 120px"}}),
        $h("input", {"id": "draft", "$bind": draft, "placeholder": "Message"}),
        $h("button", {"id": "send"}, () => [$t("Send")])
      ])
    ])];
}
$mount("Room", Room);
Edit this page on GitHub