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, Tokio, and Maud 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

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<Todo> = 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

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

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)

[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)

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

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.

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:

pub struct MyQuery;

impl Query for MyQuery {
    type Item = Todo;
    
    async fn run(store: &Store) -> anyhow::Result<Vec<Self::Item>> {
        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:

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)

[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:

[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

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

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!

S
Description
No description provided
Readme MIT
150 KiB
Languages
Rust 99.7%
Nix 0.3%