bodged refactor

This commit is contained in:
2026-03-08 20:10:15 +01:00
parent 71e63e4410
commit b10326b242
12 changed files with 801 additions and 339 deletions
+54 -32
View File
@@ -1,7 +1,9 @@
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;
// ---------------------------------------------------------------------------
@@ -12,9 +14,8 @@ use uuid::Uuid;
/// signal.
///
/// Static children (always rendered by their parent) do NOT appear here —
/// only dynamic/conditional slots. In v0 we primarily use `page` to resolve
/// the root component. The `children` map is the seam for nested dynamic
/// components in Phase 2.
/// 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"`.
@@ -47,26 +48,25 @@ impl Default for ComponentTree {
/// The component's window into the framework.
///
/// Passed as `&mut cx` to every component function. Provides access to
/// signals, database queries, and subscription key collection.
/// signals, DB queries, KV reads, and subscription key collection.
///
/// Phase 2 adds: cache handle, auth/session info.
/// 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 the incoming POST body (or initial
/// defaults on first page load).
/// 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,
/// Database pool. Cloneable (it's an Arc internally).
pub db: sqlx::SqlitePool,
/// Access to DB, KV store, and the broadcast event channel.
pub store: Arc<Store>,
/// Subscription keys accumulated during this render. After render
/// completes, these are flushed to the SubscriptionRegistry for this
/// connection.
/// Subscription keys accumulated during this render.
pub(crate) subscription_keys: Vec<CacheKey>,
/// Signal names touched during this render (for demangling tracking).
@@ -78,13 +78,13 @@ impl RenderContext {
connection_id: Uuid,
signals: SignalStore,
tree: ComponentTree,
db: sqlx::SqlitePool,
store: Arc<Store>,
) -> Self {
Self {
connection_id,
signals,
tree,
db,
store,
subscription_keys: Vec::new(),
touched_signals: Vec::new(),
}
@@ -109,10 +109,10 @@ impl RenderContext {
}
// -----------------------------------------------------------------------
// Queries
// DB Queries
// -----------------------------------------------------------------------
/// Execute a single query, collecting its subscription key.
/// Execute a single DB query, collecting its subscription key.
///
/// ```ignore
/// let todos = cx.run(ListTodos).await?;
@@ -122,14 +122,10 @@ impl RenderContext {
if key != CacheKey::None {
self.subscription_keys.push(key);
}
query.execute(&self.db).await
query.execute(&self.store.db).await
}
/// Execute two queries concurrently, collecting both subscription keys.
///
/// This avoids the `&mut self` borrow conflict by collecting keys
/// synchronously first, then running the async executions against a
/// cloned pool handle (pool clones are cheap — they share the inner Arc).
/// Execute two DB queries concurrently, collecting both subscription keys.
///
/// ```ignore
/// let (board, columns) = cx.run_all(
@@ -142,7 +138,6 @@ impl RenderContext {
a: A,
b: B,
) -> anyhow::Result<(A::Output, B::Output)> {
// Collect subscription keys synchronously (no borrow conflict)
let key_a = a.cache_key();
let key_b = b.cache_key();
if key_a != CacheKey::None {
@@ -151,14 +146,12 @@ impl RenderContext {
if key_b != CacheKey::None {
self.subscription_keys.push(key_b);
}
// Execute concurrently against a cloned pool handle
let db = self.db.clone();
let db = self.store.db.clone();
let (ra, rb) = tokio::try_join!(a.execute(&db), b.execute(&db))?;
Ok((ra, rb))
}
/// Three-query variant of `run_all`.
/// Three-query concurrent variant.
pub async fn run_all3<A: Query, B: Query, C: Query>(
&mut self,
a: A,
@@ -177,18 +170,47 @@ impl RenderContext {
if key_c != CacheKey::None {
self.subscription_keys.push(key_c);
}
let db = self.db.clone();
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 collected during this render.
/// Called by the handler after rendering to flush them to the registry.
/// 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)
}
@@ -197,4 +219,4 @@ impl RenderContext {
pub fn take_touched_signals(&mut self) -> Vec<String> {
std::mem::take(&mut self.touched_signals)
}
}
}