# 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
`