# 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!**