222 lines
7.5 KiB
Rust
222 lines
7.5 KiB
Rust
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<String, ComponentNode>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ComponentNode {
|
|
pub component: String,
|
|
#[serde(default)]
|
|
pub key: Option<String>,
|
|
}
|
|
|
|
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<Store>,
|
|
|
|
/// Subscription keys accumulated during this render.
|
|
pub(crate) subscription_keys: Vec<CacheKey>,
|
|
|
|
/// Signal names touched during this render (for demangling tracking).
|
|
pub(crate) touched_signals: Vec<String>,
|
|
}
|
|
|
|
impl RenderContext {
|
|
pub fn new(
|
|
connection_id: Uuid,
|
|
signals: SignalStore,
|
|
tree: ComponentTree,
|
|
store: Arc<Store>,
|
|
) -> 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<Q: Query>(&mut self, query: Q) -> anyhow::Result<Q::Output> {
|
|
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<A: Query, B: Query>(
|
|
&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<A: Query, B: Query, C: Query>(
|
|
&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<serde_json::Value> = cx.kv_run("clock").await;
|
|
/// ```
|
|
pub async fn kv_run(&mut self, key: impl Into<String>) -> Option<serde_json::Value> {
|
|
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<CacheKey>` is the second element of the
|
|
/// `(String, Vec<CacheKey>)` 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<CacheKey> {
|
|
std::mem::take(&mut self.subscription_keys)
|
|
}
|
|
|
|
/// Take the touched signal names.
|
|
pub fn take_touched_signals(&mut self) -> Vec<String> {
|
|
std::mem::take(&mut self.touched_signals)
|
|
}
|
|
} |