From 4d05eebe00440b08a7c8830ad9d9a3ca125eb078 Mon Sep 17 00:00:00 2001 From: Mark Kalsbeek Date: Tue, 26 May 2026 23:29:29 +0200 Subject: [PATCH] doc: create readme --- README.md | 341 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 341 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..4c88242 --- /dev/null +++ b/README.md @@ -0,0 +1,341 @@ +# livevue-rs + +A reactive server-side rendering framework for Rust that powers live-updating UIs with real-time data synchronization via Server-Sent Events (SSE). Built with [Axum](https://github.com/tokio-rs/axum), [Tokio](https://tokio.rs/), and [Maud](https://maud.lambda.world/) for efficient, async HTML rendering. + +**Status**: Early development (v0.1.0) — actively under development with frequent improvements. + +## Key Features + +- **Server-Driven Reactivity**: Stream UI updates directly from the server to clients via SSE +- **Reactive Signals**: Type-safe signal system with client-side synchronization +- **Query Caching & Invalidation**: Automatic cache key subscription during render, with smart fanout +- **PostgreSQL Logical Replication** (opt-in): Real-time cache invalidation via PostgreSQL's logical replication protocol +- **Hot-Reloadable Config**: `livevue.toml` configuration with live reload on file changes (via `notify` crate) +- **Streaming Brotli Compression**: Configurable compression for SSE streams with custom window sizes +- **SQLite & PostgreSQL Support**: Query any SQL database seamlessly +- **Per-Connection Task Handling**: Debounced rerenders with reconnection grace periods + +## Architecture + +``` +┌─────────────────────────────────────────────────────┐ +│ Client (Browser) │ +│ • DataStar morphing │ +│ • Signals (frontend state) │ +│ • SSE listener │ +└─────────────────────┬─────────────────────────────┘ + │ SSE + │ +┌─────────────────────▼─────────────────────────────┐ +│ Axum Server (livevue-rs) │ +├─────────────────────────────────────────────────┤ +│ HTTP Routes: │ +│ • GET /route?signals={json} → Initial HTML │ +│ • POST /action → Signal updates │ +│ • GET /sse?connId={uuid} → SSE stream │ +├─────────────────────────────────────────────────┤ +│ Internal Systems: │ +│ • RenderContext: Tracks signal subscriptions │ +│ • ConnectionManager: Per-connection state │ +│ • Fanout Task: Publishes cache invalidations │ +│ • Store: KV cache + DB pool │ +└─────────────────────┬─────────────────────────────┘ + │ + ┌─────────────┴──────────────┐ + │ │ + ┌───▼───────┐ ┌──────▼─────┐ + │ SQLite │ │ PostgreSQL │ + │ DB │ │ (+ PgR) │ + └───────────┘ └─────────────┘ +``` + +## Quick Start + +### 1. Create a Render Function + +```rust +use livevue_rs::RenderContext; +use std::sync::Arc; + +let render_fn: livevue_rs::RenderFn = Arc::new(|mut cx: RenderContext| { + Box::pin(async move { + // Run queries subscribed automatically to cache keys + let todos: Vec = cx.run(MyQuery::list_todos()).await?; + + // Render HTML (using Maud) + let html = html! { + ul { + @for todo in &todos { + li { (todo.title) } + } + } + }; + + let keys = cx.take_subscription_keys(); + Ok((html, keys)) + }) +}); +``` + +### 2. Set Up Application State + +```rust +use livevue_rs::{AppState, Store, load_and_watch_config, spawn_fanout}; + +let db = sqlx::SqlitePool::connect("sqlite:app.db").await?; +let store = Store::new(db); +let config = load_and_watch_config("livevue.toml")?; +let state = AppState::new(store.clone(), render_fn, config.clone()); + +// Start the fanout task for server-initiated rerenders +spawn_fanout(state.clone()); +``` + +### 3. Create Routes + +```rust +use livevue_rs::sse_handler; +use axum::{Router, routing::{get, post}, middleware}; + +let app = Router::new() + .route("/", get(/* initial render handler */)) + .route("/sse", get(sse_handler)) + .route("/action", post(/* action handler */)) + .with_state(state) + .layer(middleware::from_fn_with_state( + state.clone(), + brotli_compression, + )); +``` + +### 4. Configure (livevue.toml) + +```toml +[server] +bind_addr = "0.0.0.0" +bind_port = 3000 + +[compression] +enabled = true +window_bits = 22 # Window size (20-24) +quality = 11 # Compression level (0-11) +``` + +## Examples + +### Todo App (SQLite) + +```bash +cargo run --example todo +# Open http://localhost:3000 +``` + +Features: Add/complete/delete todos, real-time list updates via SSE. + +**Files**: +- `examples/todo/main.rs` — Server setup & database initialization +- `examples/todo/queries.rs` — Query definitions with cache keys +- `examples/todo/todo_list.rs` — Component rendering logic + +### PostgreSQL Replication Example + +```bash +cargo run --example pg_replication --features pg_replication +``` + +Demonstrates logical replication for real-time cache invalidation directly from the database. + +## Core Concepts + +### Signals + +Type-safe reactive values synchronized between server and client. + +```rust +use livevue_rs::{Signal, SignalOpts}; + +#[derive(Signal)] +#[signal(opts = "SignalOpts { mangle: true, ..Default::default() }")] +struct MySignal { + count: i32, + name: String, +} +``` + +Signals are automatically mangled for client safety and synced via SSE. + +### Queries & Cache Keys + +Queries register cache subscription keys during render: + +```rust +pub struct MyQuery; + +impl Query for MyQuery { + type Item = Todo; + + async fn run(store: &Store) -> anyhow::Result> { + sqlx::query_as::<_, Todo>("SELECT * FROM todos") + .fetch_all(&store.db) + .await + .map_err(Into::into) + } + + fn cache_key() -> CacheKey { + CacheKey::Channel { key: "todos" } + } +} +``` + +When the cache key is invalidated, all subscribed connections are scheduled for rerender. + +### Fanout & Invalidation + +The fanout task: +1. Monitors `AppState::fanout_rx` for cache invalidation events +2. Looks up all connections subscribed to that key +3. Schedules a render for each connection +4. Publishes patches via SSE with debouncing (configurable grace period) + +### Brotli Compression + +Streams are compressed on-the-fly with configurable settings: + +```rust +use livevue_rs::brotli_compression; + +let app = app.layer(middleware::from_fn_with_state( + state.clone(), + brotli_compression, +)); +``` + +Window size (20-24 bits) and quality (0-11) are configurable in `livevue.toml`. + +## Configuration (livevue.toml) + +```toml +[server] +bind_addr = "127.0.0.1" # Bind address +bind_port = 3000 # Bind port + +[compression] +enabled = true # Enable brotli compression +window_bits = 22 # Window size (20-24, default 22) +quality = 11 # Compression level (0-11, default 11) + +[connection] +rerender_debounce_ms = 100 # Debounce duration for rerenders +reconnect_grace_period_ms = 5000 # Grace period for reconnection +``` + +Hot-reloaded on file changes; trigger with `SIGHUP` to force reload. + +## Features + +### Default + +- Basic server-side rendering with SQLite support +- Signal management and caching +- Brotli compression + +### `pg_replication` + +Enable PostgreSQL logical replication for real-time cache invalidation: + +```toml +[dependencies] +livevue-rs = { features = ["pg_replication"] } +``` + +Use `cx.run_pg(pool, query)` in render functions to execute `PgQuery` objects with automatic replication-based invalidation. + +## API Overview + +### Key Exports + +| Module | Purpose | +|--------|---------| +| `signal` | Signal trait & macros for reactive values | +| `query` | `Query` trait & `CacheKey` types | +| `store` | `Store` (DB pool + KV cache) & `KVStore` trait | +| `context` | `RenderContext` for render functions | +| `connection` | Connection manager & subscription registry | +| `server` | HTTP handlers, fanout, config | +| `config` | Config loading, hot-reload | +| `brotli_layer` | Compression middleware | +| `pg_replication` | PostgreSQL replication (feature-gated) | +| `pg_query` | PostgreSQL query macros (feature-gated) | + +### Main Functions + +- **`sse_handler()`** — Axum handler for SSE connections +- **`spawn_fanout()`** — Start the fanout task +- **`ingest_signals()`** — Update signals from action payloads +- **`load_and_watch_config()`** — Load config with hot-reload +- **`brotli_compression()`** — Middleware for compression + +## Development + +### Running Tests & Examples + +```bash +# Run the todo example +cargo run --example todo + +# Run with PostgreSQL replication +cargo run --example pg_replication --features pg_replication + +# Build release +cargo build --release +``` + +### Development Environment (Nix) + +```bash +nix-shell shell.nix +cargo build +``` + +### Dependencies + +- **Runtime**: Axum, Tokio, Maud, SQLx, Serde, Dashmap, Brotli, Notify +- **Async**: Tokio, Tokio-stream, Futures +- **DB**: SQLx (SQLite + PostgreSQL), optional `tokio-postgres` for replication +- **Compression**: Brotli with configurable streaming +- **Config**: TOML parsing with hot-reload via `notify` + +## Known Limitations & TODO + +See `todo.md` for pending improvements: + +- [ ] Refactor query contract for better pg_replication + SQLite differentiation +- [ ] Clean up SSE module (currently inlined in `server.rs`) +- [ ] Fix pg_replication example to defer signal delivery over SSE +- [ ] Implement cache-backed query examples +- [ ] Move `connId` to HTTP headers instead of query params +- [ ] Extract common example logic into framework + +## Contributing + +This is an early-stage project. Contributions welcome! Key areas: + +1. **Testing** — Expand test coverage for fanout & compression +2. **Documentation** — Add more examples (auth, streaming uploads, etc.) +3. **Performance** — Optimize render scheduling, compression tuning +4. **PostgreSQL** — Refine replication reliability + +## License + +Check the repository for license details. + +## Similar Projects + +- **Phoenix LiveView** (Elixir) — Inspirational real-time rendering approach +- **Leptos** (Rust) — Full-stack reactive framework +- **Dioxus** (Rust) — Component-based with SSR support +- **HTMX** (JavaScript) — Client-side hypermedia enhancement + +--- + +**Start building live-updating Rust UIs with livevue-rs!**