doc: create readme
This commit is contained in:
@@ -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<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
|
||||||
|
|
||||||
|
```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<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:
|
||||||
|
|
||||||
|
```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!**
|
||||||
Reference in New Issue
Block a user