Handlers & commands¶
One rule decides where a handler runs¶
A handler whose body contains any
awaitis a server command. Otherwise it is local and compiles to client JavaScript.
The transport is inferred from the code's own effect shape — there is no decorator or registry to remember — and it composes with the seam: since only server handlers may await, and database facades are async, DB work cannot land in a client handler.
Local handlers — compiled to the island¶
def validate(draft):
global hint
n = len(draft.strip())
if n == 0:
hint = ""
elif n > 50:
hint = "max 50 chars"
else:
hint = f"{n}/50"
compiles to (real output):
function __l_validate(draft) {
let n = ((draft).trim()).length;
if ((n === 0)) {
S.set("hint", "");
} else if ((n > 50)) {
S.set("hint", "max 50 chars");
} else {
S.set("hint", `${n}/50`);
}
}
Branching, early returns, f-strings, string methods, set/list mutation — the accepted surface is the dialect. A local handler runs with zero network; the demo inspectors label these compiled JS (client, zero network).
Event actions¶
An @event value is a small action program — ;-separated assignments and calls:
<button @click="open_id = t.id; open_msg(t.id)">Open</button>
<form @submit="create">…</form>
<input :bind="draft" @input="validate">
<div @escape="dialog_open = False">…</div>
- Events:
@click,@dblclick,@submit(preventDefaults),@input,@escape. Inside a:eachrow, only@click/@submitare allowed. name = exprassigns a local signal;fn(args)invokes a handler — a compiled call for a local one, a command dispatch for a server one.reset("name", ...)is the one built-in.
Server commands¶
For a server handler, the compiler emits only the dispatch; the body runs server-side, exec'd with the facade injected:
- Positional call args become path segments:
toggle(t.id)→POST …/cmd/toggle/{id}(the id read from the row'sdata-idat click time). - Remaining handler params bind from the request: a
@submitsends its form's fields; local-signal params ride along automatically (add(draft)sends thedraftsignal). ALocal[set]serializes as a JSON array. - The response is the re-rendered region plus an optional signal patch — the full round trip is in the wire protocol.
Pushing values back¶
Server handlers get two injected callables:
async def create(new_title):
await issues.create(new_title)
reset("new_title", "dialog_open") # back to declared defaults
signal("toast", "Issue created") # explicit value into a client signal
Both ride the command response's X-Siting-Signals header and are applied before the region re-binds.
Optimistic updates¶
Lead a server call with local assignments and the compiler pairs them:
The leading assignments are threaded into the dispatch as an apply step — run after a snapshot, rolled back automatically if the command fails. Optimistic UI arrives as a compiler guarantee.
In-flight behavior¶
While a command is outstanding, the invoking control is disabled and the region carries aria-busy (no double-POSTs); a non-2xx response or network error logs, rolls back the optimistic apply, and never swaps an error body into the page.
Reading the derived surface
Every mount knows its own routing. The demo inspectors print it per handler — e.g. add → POST /todos/cmd/add (sends: draft) vs validate → __l_validate() in island.js — which is also exactly what EmittedApp.routes returns if you compile programmatically.