use crate::signal::{Signal, SignalOpts, SignalStore}; use crate::query::{CacheKey, Query}; use crate::store::Store; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::sync::Arc; use uuid::Uuid; // --------------------------------------------------------------------------- // Component tree // --------------------------------------------------------------------------- /// Describes which dynamic components are mounted, parsed from the `tree` /// signal. /// /// Static children (always rendered by their parent) do NOT appear here — /// only dynamic/conditional slots. `page` resolves the root component; /// `children` is the seam for nested dynamic components. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ComponentTree { /// The root page component name, e.g. `"TodoList"`. pub page: String, /// Dynamic child slots. Key is the slot name, value describes the child. #[serde(default)] pub children: HashMap, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ComponentNode { pub component: String, #[serde(default)] pub key: Option, } impl Default for ComponentTree { fn default() -> Self { Self { page: String::new(), children: HashMap::new(), } } } // --------------------------------------------------------------------------- // RenderContext // --------------------------------------------------------------------------- /// The component's window into the framework. /// /// Passed as `&mut cx` to every component function. Provides access to /// signals, DB queries, KV reads, and subscription key collection. /// /// After rendering, call `take_subscription_keys()` to flush subscriptions /// to the `SubscriptionRegistry` for this connection. pub struct RenderContext { /// This connection's UUID. pub connection_id: Uuid, /// Signal values populated from `last_signals_json` (or initial defaults /// on first render). pub signals: SignalStore, /// Parsed component tree from the `tree` signal. pub tree: ComponentTree, /// Access to DB, KV store, and the broadcast event channel. pub store: Arc, /// Subscription keys accumulated during this render. pub(crate) subscription_keys: Vec, /// Signal names touched during this render (for demangling tracking). pub(crate) touched_signals: Vec, } impl RenderContext { pub fn new( connection_id: Uuid, signals: SignalStore, tree: ComponentTree, store: Arc, ) -> Self { Self { connection_id, signals, tree, store, subscription_keys: Vec::new(), touched_signals: Vec::new(), } } // ----------------------------------------------------------------------- // Signals // ----------------------------------------------------------------------- /// Create a signal reference with the given name and scoping options. /// /// Records the signal as "touched" for demangling tracking. /// /// ```ignore /// let open = cx.signal("open", mangle(card.id)); /// let val: bool = open.read_or(&cx.signals, false); /// ``` pub fn signal(&mut self, name: &str, opts: SignalOpts) -> Signal { let mangled = opts.apply(name); self.touched_signals.push(mangled.clone()); Signal::new(mangled) } // ----------------------------------------------------------------------- // DB Queries // ----------------------------------------------------------------------- /// Execute a single DB query, collecting its subscription key. /// /// ```ignore /// let todos = cx.run(ListTodos).await?; /// ``` pub async fn run(&mut self, query: Q) -> anyhow::Result { let key = query.cache_key(); if key != CacheKey::None { self.subscription_keys.push(key); } query.execute(&self.store.db).await } /// Execute two DB queries concurrently, collecting both subscription keys. /// /// ```ignore /// let (board, columns) = cx.run_all( /// GetBoard { id: 1 }, /// ListColumns { board_id: 1 }, /// ).await?; /// ``` pub async fn run_all( &mut self, a: A, b: B, ) -> anyhow::Result<(A::Output, B::Output)> { let key_a = a.cache_key(); let key_b = b.cache_key(); if key_a != CacheKey::None { self.subscription_keys.push(key_a); } if key_b != CacheKey::None { self.subscription_keys.push(key_b); } let db = self.store.db.clone(); let (ra, rb) = tokio::try_join!(a.execute(&db), b.execute(&db))?; Ok((ra, rb)) } /// Three-query concurrent variant. pub async fn run_all3( &mut self, a: A, b: B, c: C, ) -> anyhow::Result<(A::Output, B::Output, C::Output)> { let key_a = a.cache_key(); let key_b = b.cache_key(); let key_c = c.cache_key(); if key_a != CacheKey::None { self.subscription_keys.push(key_a); } if key_b != CacheKey::None { self.subscription_keys.push(key_b); } if key_c != CacheKey::None { self.subscription_keys.push(key_c); } let db = self.store.db.clone(); let (ra, rb, rc) = tokio::try_join!(a.execute(&db), b.execute(&db), c.execute(&db))?; Ok((ra, rb, rc)) } // ----------------------------------------------------------------------- // KV / Channel queries // ----------------------------------------------------------------------- /// Read a value from the KV store and register a subscription to its key. /// /// When `store.kv.set(key, ...)` is called (or `store.publish(...)` for /// that key), all connections that called `kv_run` with that key during /// their last render will be scheduled for rerender. /// /// Returns `None` if the key has not been set yet. /// /// ```ignore /// let tick: Option = cx.kv_run("clock").await; /// ``` pub async fn kv_run(&mut self, key: impl Into) -> Option { let key = key.into(); self.subscription_keys .push(CacheKey::Channel { key: key.clone() }); self.store.kv.get(&key) } // ----------------------------------------------------------------------- // Post-render access // ----------------------------------------------------------------------- /// Take the subscription keys accumulated during this render. /// /// Call this at the top of the component call stack before returning from /// `render_fn`. The returned `Vec` is the second element of the /// `(String, Vec)` tuple — the framework uses it to update the /// `SubscriptionRegistry` for this connection after each successful render. /// /// Keys accumulate naturally as the call stack unwinds: each `cx.run()` /// and `cx.kv_run()` call pushes its key, so the top-level collect gets /// the full set from the entire render. pub fn take_subscription_keys(&mut self) -> Vec { std::mem::take(&mut self.subscription_keys) } /// Take the touched signal names. pub fn take_touched_signals(&mut self) -> Vec { std::mem::take(&mut self.touched_signals) } }