Counter — the "hello world" (Phase 2)¶
A real-time counter driven by WebSocket events. Click a button, the
number updates without a page reload. No JavaScript build step, no
npm install, no bundler.
Source: examples/counter/
Run¶
Then open http://127.0.0.1:3000/ — ideally in two browser tabs at once so you can see each tab has its own independent counter (per-connection state; shared state lands in Phase 4).
What each file does¶
fitz.toml— package manifest. Declares this project as a[bin]and pulls in thefitz_liveviewslibrary via apath = "../.."dependency.src/main.fitz— the whole counter:type CounterState— the state (just anInt)fn render(state)— turns state intoHtml@get("/")— initial HTML render + client-side script bundle@ws("/live/counter")— receives events, mutates state, computes the diff against the last snapshot, and sends compact patches@server(3000)— binds the port
The whole source¶
// Fitz LiveViews — real-time counter (Phase 11.6.e migration).
//
// Run: fitz run
// Then open http://127.0.0.1:3000/ in two browser tabs — both stay in
// sync as you click the buttons. **Behaviour change from the original
// per-connection counter**: `component("Counter", "root")` shares one
// server-side state instance across every tab, matching the Phase 4
// dashboard/kanban model. If you want per-tab isolation, use a unique
// `instance_id` per connection (e.g. a UUID passed via query string).
from fitz_liveviews import Html, html, live_layout, html_response, LiveFrame, diff_html, component, dispatch_component_events, flv_register
// Bring the component's type + render/event fns into scope. The
// `.fitzv` loader (Phase 11.6.d) transforms `Counter.fitzv` into
// classic Fitz source that emits `@live_component("Counter")` on the
// type + `@render_for` + `@on` fns.
from Counter import Counter, Counter_render, Counter_increment, Counter_decrement, Counter_reset
// -----------------------------------------------------------------------------
// Boot registration is IMPLICIT (Fitz core v0.21.0+)
// -----------------------------------------------------------------------------
//
// The compiler auto-generates the `flv_register("Counter", Counter { },
// Counter_render, {"increment": Counter_increment, ...})` call from
// the metadata that `@live_component` + `@render_for` + `@on` leave in
// the imported `Counter.fitzv` module. Cross-module auto-inject shipped
// in Fitz core v0.21.0 (Phase 11.6.e §9.bb) — for it to work, `main.fitz`
// must import the state type + render fn + every event handler by their
// canonical names (`from Counter import Counter, Counter_render,
// Counter_<event>...`), which is what the `from Counter import ...`
// line above already does.
// -----------------------------------------------------------------------------
// HTTP GET — initial render + client runtime
// -----------------------------------------------------------------------------
@get("/")
fn counter_page() -> Response {
let initial = component("Counter", "root")
return html_response(live_layout("/live/counter", "counter-app", initial))
}
// -----------------------------------------------------------------------------
// WebSocket — event/patch cycle
// -----------------------------------------------------------------------------
@ws("/live/counter")
async fn counter_socket(ws: WsConn<LiveFrame>) {
let last_html = component("Counter", "root").raw
loop {
let frame = ws.recv()?
let _handled = dispatch_component_events(frame)
// Re-render the component (its state was mutated by the dispatch)
// and ship the incremental patch. Client falls back to `html` if
// the patches don't apply cleanly.
let new_html = component("Counter", "root").raw
let patches = diff_html(last_html, new_html)
ws.send(LiveFrame { html: new_html, patches: patches })?
last_html = new_html
}
}
// -----------------------------------------------------------------------------
// Server config
// -----------------------------------------------------------------------------
@server(3000)
fn main() => 0
Poking under the hood¶
Check the server logs and the network tab of your browser's dev tools. You'll see:
GET /returns the initial HTML.- The browser upgrades a connection at
ws://127.0.0.1:3000/live/counter. - Each click sends a small JSON frame like
{"event":"increment","payload":{},"html":"","patches":[]}. - The server responds with a JSON frame carrying:
patches— compact list like[{"op":"text","path":[0,3,0],"content":"Count: 6"}]html— the full new HTML as a fallback (only used if patches fail)
- The client walks the DOM by path index and applies patches directly.
Only the
<p>element mutates — no re-mount of the surrounding tree.
What's missing (comes later)¶
- Shared state. Two tabs = two counters. Phase 4 adds per-LiveView shared state.
- Forms. No
data-flv-submitneeded here — but chat uses it in Phase 3a. @livedecorator. The current@get+@wspattern is a Phase 2 MVP shape.@live("/counter")as a single decorator is a Phase 6+ language enhancement.
See the Roadmap for the full plan.