# The LeanUI server `LeanUI.Server` runs LeanUI programs on a native Lean server (`Std.Http`, Lean 4.34). One `Program` (pure `init`, `update`, `view`) is served three ways from the same code: - **server-rendered, then hydrated by its wasm build**: the page is complete HTML before any script runs; the app's wasm64 module then *adopts* those nodes instead of recreating them; - **server-rendered, then server-driven**: for browsers without WebAssembly Memory64, the page is a thin client and a native *session* on the server runs the program; - **server-rendered only**: no script at all. `LeanUI.boot` picks the first when the browser has Memory64 and the second otherwise. The library also serves static files, and the `leanui` command builds, scaffolds and serves apps. ``` GET /counter/server ─▶ Site.page ─▶ view (init flags) ─▶ Html.render ─▶
…
└─ Sessions.reserve (an id, no thread yet) browser: LeanUI.connect ─▶ POST /leanui/ "h" ─▶ Sessions.route ─▶ Mailbox ─▶ actor thread │ LeanUI.start (hydrate) applies Buf.encode ◀──────────────── 200 [u32 opsBytes][ops][strs] ◀┘ Instance.event click: POST "e\n\n" ─▶ … same actor, same Instance … ─▶ the diff, as bytes ``` ## Contents | Module | What it does | |---|---| | `LeanUI.Server.Http` | `Req`, `Res`, `Route` and its combinators (transport-independent) | | `LeanUI.Server.Url` | path/query decoding, percent-encoding, traversal refusal | | `LeanUI.Server.Mime` | media types (`application/wasm` among them) | | `LeanUI.Server.Static` | static files: index, redirects, ETag/304, HEAD, cache policies, br/gz, COOP/COEP | | `LeanUI.Server.Page` | the HTML document around a rendered view, and the boot script | | `LeanUI.Server.Session` | the remote protocol, session actors, the session table | | `LeanUI.Server.Site` | `program`: serve a `Program` at a URL in every mode | | `LeanUI.Server.Run` | the `Std.Http` adapter, `serve`, clean shutdown on signals | | `LeanUI.Server.Testing` | a native browser stand-in for server-driven pages (not in the umbrella) | | `LeanUI.Server.Cli` | the `leanui` command (`cli/Main.lean` calls it) | | `LeanUI.Server.Demo` | the site of `leanui-demo-server` (`examples/ServerDemo.lean`) | `import LeanUI.Server` brings in everything an app needs (not `Testing`, `Cli` or `Demo`). Server modules are native only: `tools/build-app.sh` leaves every `*/Server*` path out of wasm builds. ## Quick start ```sh lake build # ~/.elan/toolchains/leanprover--lean4---v4.34.0/bin/lake .lake/build/bin/leanui build CounterApp counter --compress # dist/counter/: app.js, app.wasm, leanui.js (+ .br/.gz) .lake/build/bin/leanui-demo-server --port 5340 # http://127.0.0.1:5340/ ``` The demo serves `/counter` (auto), `/counter/wasm`, `/counter/server`, `/counter/ssr`, a server-driven `/ticker/server` (an interval subscription, an HTTP effect, a title effect, flags from `?from=N`) and `/contract/server` (markup the HTML parser rewrites; see the SSR contract below). Without `dist/counter` it still runs: wasm pages answer 404 and `/counter` is server-driven. ## Serving a program ```lean import LeanUI.Server open LeanUI LeanUI.Server def main : IO Unit := do let sessions ← Sessions.new {} -- SessionConfig: caps, timeouts let stopReaper ← sessions.startReaper -- expiry and the stuck-session watchdog let site : Site := { title := "Counter", assets := some "/assets/counter/", flags := fun req => (req.param? "start").getD "" } serve { port := 8080 } (Route.first [ Static.file "/leanui.js" "dist/counter/leanui.js", Route.under "/assets/counter" (Static.route { root := "dist/counter" }), Route.under "/counter" (program "counter" Counter.program site sessions), sessions.route "/leanui"]) (onShutdown := do stopReaper; sessions.closeAll) ``` `program name p site sessions` answers, below its mount point: | path | page | needs | |---|---|---| | `/` | `site.defaultMode` (`auto`), or `?mode=wasm\|server\|ssr\|auto` | | | `/wasm` | server-rendered, hydrated by `app.js` | `site.assets` | | `/server` | server-rendered, driven by a session | `sessions.route` mounted at `site.endpoint` | | `/ssr` | server-rendered only | | | `/auto` | `LeanUI.boot`: wasm with Memory64, else the session (falls back to the session if the wasm fails to start) | both | Every page is the view of `init flags` rendered by `Html.render`; the wasm and the session compute `init flags` again (it is pure), so the client starts from exactly the state the HTML shows. `site.flags` maps the request to the flags string; the same string reaches the wasm through the page. Pages that name a session are sent `Cache-Control: no-store` (a cached page would share its session id); each carries `Server-Timing: ssr;dur=`. ### The page `Page.render` writes `
` + the rendered view + `
` with nothing in between (hydration counts every node, whitespace included), then the boot parameters as JSON in `