Files
livevue-rs/examples/todo/main.rs
T
Claude 6d91fd9c36 Add streaming brotli compression and hot-reloadable config (livevue.toml)
Key changes:

- **src/config.rs** – `LiveVueConfig` (server host/port, brotli quality +
  window size), loaded from `livevue.toml`. A notify-based file watcher
  reloads the config on any file change; a SIGHUP handler does the same on
  demand. Falls back to compiled-in defaults when the file is absent.

- **src/brotli_layer.rs** – `BrotliBody`, a custom `http_body::Body` wrapper
  that streams brotli-compressed data through a single persistent
  `brotli::CompressorWriter`. The encoder survives across SSE event flushes,
  so its sliding window accumulates context from all prior events —
  progressively better compression as the stream grows. Both quality (0–11)
  and window size (lgwin 10–24, i.e. 1 KB – 16 MB) are taken from config.
  `brotli_compression` is an axum `from_fn_with_state` middleware that
  activates only when the client sends `Accept-Encoding: br`.

- **src/server.rs** – `AppState` gains a `config: SharedConfig` field.
  `AppState::new` takes the config; `AppState::new_default` is a zero-config
  convenience constructor.

- **livevue.toml** – documented example config with inline comments
  explaining each field and the brotli window-size tradeoff table.

- **examples/todo** – loads config via `load_and_watch_config`, derives the
  bind address from `config.server`, and applies `brotli_compression` as a
  router layer.

https://claude.ai/code/session_01UnQQkkwts64FPUsSzFfdQb
2026-03-12 07:06:10 +00:00

119 lines
4.5 KiB
Rust

mod app;
mod queries;
mod todo_list;
mod about;
use livevue_rs::{AppState, RenderContext, Store, load_and_watch_config, spawn_fanout};
use crate::app::{app_wrapper, resolve_and_render};
use std::sync::Arc;
use std::time::Duration;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// -----------------------------------------------------------------------
// Database setup
// -----------------------------------------------------------------------
let db = sqlx::SqlitePool::connect("sqlite:todo.db?mode=rwc").await?;
sqlx::query(
"CREATE TABLE IF NOT EXISTS todos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
completed BOOLEAN NOT NULL DEFAULT FALSE
)",
)
.execute(&db)
.await?;
let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM todos")
.fetch_one(&db)
.await?;
if count.0 == 0 {
for title in &["Learn Rust", "Build LiveVue.rs", "Ship v0"] {
sqlx::query("INSERT INTO todos (title, completed) VALUES (?, false)")
.bind(title)
.execute(&db)
.await?;
}
}
// -----------------------------------------------------------------------
// Store
// -----------------------------------------------------------------------
let store = Store::new(db);
// -----------------------------------------------------------------------
// Render function
// -----------------------------------------------------------------------
// The render_fn is the app's half of the framework contract. It receives
// a RenderContext (with signals, tree, store access) and returns the
// rendered inner HTML plus the accumulated subscription keys.
//
// The fanout task calls this for every server-initiated rerender.
let render_fn: livevue_rs::RenderFn = Arc::new(|mut cx: RenderContext| {
Box::pin(async move {
println!("Render Called");
let signals_json = cx.signals.to_json_string();
let conn_id = cx.connection_id;
let html = resolve_and_render(&mut cx).await?;
let keys = cx.take_subscription_keys();
// Wrap in app_wrapper so DataStar morphs the full #app element.
// Signals are re-emitted from the stored json so the client stays
// in sync even on server-initiated rerenders.
let wrapped = app_wrapper(conn_id, &signals_json, &html);
Ok((wrapped, keys))
})
});
// -----------------------------------------------------------------------
// Configuration (livevue.toml, hot-reloaded on change or SIGHUP)
// -----------------------------------------------------------------------
let config = load_and_watch_config("livevue.toml")?;
// -----------------------------------------------------------------------
// Application state
// -----------------------------------------------------------------------
let state = AppState::new(store.clone(), render_fn, config.clone());
// -----------------------------------------------------------------------
// Fanout task
// -----------------------------------------------------------------------
spawn_fanout(state.clone());
// -----------------------------------------------------------------------
// Clock ticker
//
// Publishes CacheKey::Channel { key: "clock" } every second. Any
// connection whose last render called cx.kv_run("clock") will be
// scheduled for a rerender by the fanout task.
// -----------------------------------------------------------------------
let clock_store = store.clone();
tokio::spawn(async move {
let mut interval = tokio::time::interval(Duration::from_secs(1));
loop {
interval.tick().await;
let now = chrono::Utc::now().format("%H:%M:%S UTC").to_string();
// set() writes the value and publishes the invalidation event.
clock_store.kv.set("clock", serde_json::json!(now));
}
});
// -----------------------------------------------------------------------
// Server
// -----------------------------------------------------------------------
let bind_addr = config.read().await.server.bind_addr();
let app = app::router(state, config);
let listener = tokio::net::TcpListener::bind(&bind_addr).await?;
println!("Todo example listening on http://{bind_addr}");
axum::serve(listener, app).await?;
Ok(())
}