Files
livevue-rs/examples/todo/main.rs
T
2026-03-08 20:10:15 +01:00

112 lines
4.1 KiB
Rust

mod app;
mod queries;
mod todo_list;
mod about;
use livevue_rs::{AppState, CacheKey, RenderContext, Store, 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))
})
});
// -----------------------------------------------------------------------
// Application state
// -----------------------------------------------------------------------
let state = AppState::new(store.clone(), render_fn);
// -----------------------------------------------------------------------
// 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 app = app::router(state);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
println!("Todo example listening on http://localhost:3000");
axum::serve(listener, app).await?;
Ok(())
}