Refactor: untangle framework library from todo example
The codebase is now split into two clear layers: Framework library (src/): - lib.rs — public API surface, re-exports all framework types - signal.rs — SignalOpts, Signal, SignalStore, mangle/global/client_only helpers - query.rs — Query trait, CacheKey, query_one!/query_all! macros (macro paths updated from $crate::framework::* to $crate::*) - context.rs — RenderContext, ComponentTree, ComponentNode - connection.rs — ConnectionManager, SubscriptionRegistry, ConnectionState - sse.rs — format_patch_elements, format_patch_signals - server.rs — AppState, SharedState, sse_handler (pure framework plumbing) Todo example (examples/todo/): - main.rs — SQLite setup, seeding, server bootstrap - app.rs — router, document_shell, resolve_and_render, render_page, action_then_render, and all HTTP handlers - queries.rs — Todo model, ListTodos/GetTodo queries, mutation helpers - todo_list.rs — TodoList page component - about.rs — About page component Run the example independently with: cargo run --example todo https://claude.ai/code/session_01JThiQbp3Bn9J1VhBpRuz4u
This commit is contained in:
+200
@@ -0,0 +1,200 @@
|
||||
use crate::signal::{Signal, SignalOpts, SignalStore};
|
||||
use crate::query::{CacheKey, Query};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
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. In v0 we primarily use `page` to resolve
|
||||
/// the root component. The `children` map is the seam for nested dynamic
|
||||
/// components in Phase 2.
|
||||
#[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, database queries, and subscription key collection.
|
||||
///
|
||||
/// Phase 2 adds: cache handle, auth/session info.
|
||||
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).
|
||||
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,
|
||||
|
||||
/// Subscription keys accumulated during this render. After render
|
||||
/// completes, these are flushed to the SubscriptionRegistry for this
|
||||
/// connection.
|
||||
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,
|
||||
db: sqlx::SqlitePool,
|
||||
) -> Self {
|
||||
Self {
|
||||
connection_id,
|
||||
signals,
|
||||
tree,
|
||||
db,
|
||||
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)
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Queries
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
/// Execute a single 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.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).
|
||||
///
|
||||
/// ```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)> {
|
||||
// Collect subscription keys synchronously (no borrow conflict)
|
||||
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);
|
||||
}
|
||||
|
||||
// Execute concurrently against a cloned pool handle
|
||||
let db = self.db.clone();
|
||||
let (ra, rb) = tokio::try_join!(a.execute(&db), b.execute(&db))?;
|
||||
Ok((ra, rb))
|
||||
}
|
||||
|
||||
/// Three-query variant of `run_all`.
|
||||
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.db.clone();
|
||||
let (ra, rb, rc) = tokio::try_join!(a.execute(&db), b.execute(&db), c.execute(&db))?;
|
||||
Ok((ra, rb, rc))
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Post-render access
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
/// Take the subscription keys collected during this render.
|
||||
/// Called by the handler after rendering to flush them to the registry.
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user