Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ Targets: `make test` (build + run tests + build examples), `make unit-tests` (te

| Package | Version | Use path | Role |
|---------|---------|----------|------|
| mare | 0.1.1 | `"mare"` | WebSocket server (connection actor implements `WebSocketServerActor`) |
| mare | 0.2.0 | `"mare"` | WebSocket server (connection actor implements `WebSocketServerActor`) |
| templates | 0.3.0 | `"templates"` | HTML rendering (`HtmlTemplate` auto-escapes by default) |
| json-ng | 0.3.0 | `"json"` | Wire protocol serialization (persistent `JsonObject`/`JsonArray`) |
| hobby | 0.2.1 | `"hobby"` | HTTP server (used in SSR example for dynamic first paint) |
| lori | (transitive via mare) | `"lori"` | TCP networking, idle timeout |

## Architecture
Expand Down Expand Up @@ -71,7 +72,7 @@ examples/
counter/ # Increment/decrement counter
ticker/ # PubSub-driven ticker (server push via re-render + push_event)
form/ # Registration form with live validation (lv-change + lv-submit)
ssr/ # Server-rendered first paint (pre-rendered counter)
ssr/ # Server-rendered first paint (hobby HTTP + PageRenderer)
client/ # JavaScript client library
src/ # Source modules (wire, events, socket, live-view)
test/ # vitest tests
Expand Down
20 changes: 10 additions & 10 deletions examples/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Examples

Each subdirectory is a self-contained Pony program demonstrating a different part of the livery library. Each example includes an `index.html` file that connects to the running server via the JavaScript client. The ssr example's `index.html` contains pre-rendered content that the JS client takes over on connect.
Each subdirectory is a self-contained Pony program demonstrating a different part of the livery library. The counter, ticker, and form examples each include an `index.html` file that connects to the running server via the JavaScript client. The ssr example dynamically renders its HTML via an HTTP server using `PageRenderer`.

## Running examples

Expand All @@ -18,14 +18,14 @@ make ssl=openssl_3.0.x examples

1. Start the Pony server for the example you want to run:

| Example | Command | Port |
|---------|---------|------|
| counter | `./build/release/counter` | 8081 |
| ticker | `./build/release/ticker` | 8082 |
| form | `./build/release/form` | 8083 |
| ssr | `./build/release/ssr` | 8084 |
| Example | Command | WebSocket port | HTTP port |
|---------|---------|----------------|-----------|
| counter | `./build/release/counter` | 8081 | — |
| ticker | `./build/release/ticker` | 8082 | — |
| form | `./build/release/form` | 8083 | — |
| ssr | `./build/release/ssr` | 8084 | 8085 |

1. Serve the repo root with a static file server (the HTML shells use relative paths to load the JS client bundle):
1. For the counter, ticker, and form examples, serve the repo root with a static file server (the HTML shells use relative paths to load the JS client bundle):

```sh
python3 -m http.server 8080
Expand All @@ -36,7 +36,7 @@ Then visit the example in your browser:
- Counter: `http://localhost:8080/examples/counter/index.html`
- Ticker: `http://localhost:8080/examples/ticker/index.html`
- Form: `http://localhost:8080/examples/form/index.html`
- SSR: `http://localhost:8080/examples/ssr/index.html`
- SSR: `http://localhost:8085/` (served directly by hobby, no static file server needed)

## [counter](counter/)

Expand All @@ -52,4 +52,4 @@ Demonstrates form handling with live validation. A registration form uses `lv-ch

## [ssr](ssr/)

Demonstrates server-rendered first paint. The `index.html` contains the counter's initial HTML pre-rendered inside the `lv-root` container — the user sees content immediately on page load with no empty-page flash. When the WebSocket connects, the server mounts the same `CounterView` (producing identical HTML), and morphdom silently takes over the pre-rendered DOM. Compare with the counter example to see what server-rendered first paint adds. In production, `PageRenderer.render(factory)` would generate the HTML dynamically in an HTTP handler; this example uses static HTML to demonstrate the client-side takeover without requiring an HTTP server dependency.
Demonstrates server-rendered first paint with a dynamic HTTP handler. A hobby HTTP server on port 8085 calls `PageRenderer.render(factory)` to produce the counter's initial HTML at request time, embeds it in a full page, and serves it along with the JS client bundle. When the browser loads the page, the user sees the counter immediately — no empty-page flash. The JS client then connects to the WebSocket server on port 8084, morphdom takes over the pre-rendered DOM, and the counter becomes interactive. Compare with the counter example to see what server-rendered first paint adds.
20 changes: 0 additions & 20 deletions examples/ssr/index.html

This file was deleted.

62 changes: 59 additions & 3 deletions examples/ssr/main.pony
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
use "files"
use "templates"
use "json"
use hobby = "hobby"
use lori = "lori"
use stallion = "stallion"
use "../../livery"

class CounterView is LiveView
Expand Down Expand Up @@ -35,11 +38,64 @@ class CounterView is LiveView
fun box render(assigns: Assigns box): String ? =>
_template.render(assigns.template_values())?

class val IndexHandler is hobby.Handler
"""
Serves the initial HTML page with server-rendered content.

Calls `PageRenderer.render` to produce the counter's HTML at request time,
embeds it in a full page shell, and responds with `text/html`. The JS client
takes over when the WebSocket connects — morphdom patches the pre-rendered
DOM without a visible flash.
"""
let _factory: Factory

new val create(factory: Factory) =>
_factory = factory

fun apply(ctx: hobby.Context ref) =>
let body = match PageRenderer.render(_factory)
| let html: String val =>
"<!DOCTYPE html>\n"
+ "<html>\n"
+ "<head><title>SSR Counter - Livery Example</title></head>\n"
+ "<body>\n"
+ " <div id=\"lv-root\">" + html + "</div>\n"
+ " <script src=\"/client/livery.iife.js\"></script>\n"
+ " <script>\n"
+ " new LiveView({\n"
+ " url: \"ws://localhost:8084/ssr\",\n"
+ " target: document.getElementById(\"lv-root\")\n"
+ " }).connect();\n"
+ " </script>\n"
+ "</body>\n"
+ "</html>"
| let err: PageRenderError =>
ctx.respond(stallion.StatusInternalServerError, err.string())
return
end
let headers = recover val
let h = stallion.Headers
h.set("Content-Type", "text/html; charset=utf-8")
h.set("Content-Length", body.size().string())
h
end
ctx.respond_with_headers(stallion.StatusOK, headers, consume body)

actor Main
new create(env: Env) =>
let router = Router
router.route("/ssr",
{(): LiveView ref^ ? => CounterView.create()?} val)
let factory: Factory =
{(): LiveView ref^ ? => CounterView.create()?} val

// WebSocket server for live updates
let router = Router
router.route("/ssr", factory)
Listener(lori.TCPListenAuth(env.root), "0.0.0.0", "8084",
router.build(), PubSub, env.err)

// HTTP server for initial page load and static assets
let client_root = FilePath(FileAuth(env.root), "client/dist")
hobby.Application
.>get("/", IndexHandler(factory))
.>get("/client/*filepath", hobby.ServeFiles(client_root))
.serve(lori.TCPListenAuth(env.root), stallion.ServerConfig("0.0.0.0",
"8085"), env.err)
Loading