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