Sync a UI to a server over transports¶
This guide shows you how to keep a served spaday UI in sync with a server-side state model using transports — so a two-way control’s change is applied on the server and fanned to every connected browser. For delivering the page itself (the integration ladder), see Serve and embed; for the zero-server version, the notebook guide.
The split to keep in mind: spaday owns the UI (the tree and its reactive Store); transports
owns the wire (a Client that mirrors a model and sends edits); a single adapter, connectStore, is the
only place they meet — and serve(wire="transports", …) generates that adapter for you, so there is
no hand-written browser glue.
pip install "spaday[examples]" # spaday + transports + starlette + uvicorn
Host a model and wire the page¶
Host a model in a transports Session, author a tree whose controls two-way bind to its fields, and
serve it with wire="transports" — supply the websocket route and run autosync as a background task:
import transports, uvicorn
from pydantic import BaseModel
from starlette.routing import WebSocketRoute
from spaday import element
from spaday.backends.starlette import serve
from spaday_webawesome import WaInput, WaSwitch
class Controls(BaseModel):
label: str = "hello"
on: bool = True
session = transports.Session()
session.host(Controls())
server = transports.Server(session)
def page():
return (
element("div")
.child(WaInput().bind("value", "label", mode="two-way"))
.child(WaSwitch().bind("checked", "on", mode="two-way"))
)
app = serve(
page,
packages=["webawesome"],
wire="transports",
routes=[WebSocketRoute("/ws", transports.ws_endpoint(server))],
background=[transports.autosync(server)], # fan host-side changes to every client
)
if __name__ == "__main__":
uvicorn.run(app, host="127.0.0.1", port=8000)
There are no event handlers in the tree — the two-way bindings carry every control→model edit.
Inbound model patches flow model → store → bound props; a two-way control’s change becomes a
server-authoritative client.edit, which takes effect when the server echoes it back, so two browser
tabs stay in sync. A complete, runnable version is spaday/examples/reactive.py.
Go multi-tenant¶
Swap the Session for a Hub, which routes each connection to its
own tenant session (and can share models across tenants). The UI code is unchanged — connectStore and
the bindings don’t know whether the model is private or shared.
Several models on one page¶
Pass a list of Wire specs to mirror several models into one store at once, each under its own
namespace so their fields don’t collide (two Chart models would both have data/type):
from spaday import Wire, field
from spaday.backends.starlette import serve
app = serve(
page,
wire=[
Wire("/ws", namespace="global"), # a shared model
Wire("/ws/session", namespace="session", session=True), # a fresh per-tab tenant (a Hub)
Wire("/ws/cfg", namespace="cfg", flatten=False), # an opaque map/dict field, mirrored whole
],
routes=[...], # one WebSocketRoute per wire
background=[...], # one autosync per Server/Hub
)
The tree then binds against namespaced fields — bind("value", "global.type"),
compute("data", field("global.data")). A Wire:
namespace— mirror the model’s fields under<namespace>.(omit for bare fields, e.g. a form).session— append?session=<uuid>, making the model a fresh per-page-load tenant.flatten— recurse nested sub-models into dottedparent.childfields (the default, what a form binds); setFalsefor an opaque map/dict field (a chart’s time-keyeddata, a Perspective layout) so it’s mirrored whole instead of one store field per key.
A raw {"url": …, "namespace": …} dict works anywhere a Wire does. The omnibus
(python -m spaday.examples) wires four models this way.
Perspective (Mode B)¶
A live Perspective table streams its data over Perspective’s own websocket; spaday/transports sync
only a small config model (server, tables, layout). Mirror the config with flatten=False and feed it
to the panel with a computed config prop, so a server push re-restores the workspace for every tab:
from spaday import field, obj
from spaday_perspective import PerspectivePanel
PerspectivePanel().compute("config", obj({
"ws_url": field("cfg.ws_url"), "tables": field("cfg.tables"), "layout": field("cfg.layout"),
}))
Install spaday-perspective and select its assets with packages=["perspective"] when serving the page.
See spaday/examples/gateway.py (no transports — REST + Perspective’s own ws) and the omnibus for the
full pattern.