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(()) }