This document describes how RustView works internally.
┌─────────────────────────────────────────────┐
│ Browser │
│ ┌─────────────────────────────────────┐ │
│ │ DOM (patched by shim) │ │
│ │ ← SSE patches ← Server diffs │ │
│ │ → HTTP POST → widget events │ │
│ └─────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
↕ HTTP / SSE
┌─────────────────────────────────────────────┐
│ Axum Server │
│ ┌──────────┐ ┌────────┐ ┌────────────┐ │
│ │SessionStore│ │ Router │ │ SSE Stream │ │
│ │ (DashMap) │ │ │ │ │ │
│ └──────────┘ └────────┘ └────────────┘ │
│ ↕ │
│ ┌──────────────────────────────────────┐ │
│ │ App Function Re-run │ │
│ │ fn app(ui: &mut Ui) { ... } │ │
│ │ ↓ builds VNode tree │ │
│ └──────────────────────────────────────┘ │
│ ↕ │
│ ┌──────────────────────────────────────┐ │
│ │ VNode Diff (old tree vs new tree) │ │
│ │ Produces: [Patch::Replace, ...] │ │
│ └──────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
RustView maintains a virtual DOM tree (VNode). Each widget call appends one or more VNodes to the tree.
pub struct VNode {
pub id: String, // Unique node ID
pub tag: String, // HTML tag (div, input, button, etc.)
pub attrs: HashMap<String, String>, // HTML attributes
pub children: Vec<VNode>, // Child nodes
pub text: Option<String>, // Text content
}Key operations:
VNode::new(id, tag)— create a node.with_attr(key, value)— add an attribute.with_child(child)— add a child node.with_text(text)— set text content
After each app re-run, RustView diffs the new VNode tree against the previous one stored in the session. The diff produces a list of patches:
- Replace — swap out a node entirely
- UpdateAttrs — change attributes on an existing node
- UpdateText — change text content
- AppendChild / RemoveChild — add or remove children
Only changed nodes are sent to the browser, minimizing data transfer.
Each browser tab gets a unique session:
SessionStore (DashMap<Uuid, Session>)
└── Session
├── id: Uuid
├── widget_state: HashMap<String, Value> // Widget current values
├── user_state: HashMap<String, Value> // User-defined state
├── last_tree: Option<VNode> // Previous render tree
└── last_active: Instant // For TTL expiry
Sessions are stored in a concurrent DashMap and automatically cleaned up when they exceed the TTL.
RustView runs an Axum HTTP server with these routes:
| Route | Method | Purpose |
|---|---|---|
/ |
GET | Serve the HTML page with embedded CSS and JS shim |
/api/session |
POST | Create a new session, return UUID |
/api/event |
POST | Receive widget events (clicks, input changes) |
/api/sse/:id |
GET | SSE stream for pushing DOM patches to the client |
/api/upload |
POST | Receive file uploads |
The HTML page includes a minimal JavaScript shim (~150 lines) that:
- Connects to the SSE endpoint
- Receives DOM patch instructions as JSON
- Applies patches to the live DOM
- Sends user events back via POST
No npm, no bundler, no React — just a small inline script.
- Page Load: Browser requests
/→ receives HTML with CSS + JS shim - Session Init: Shim POSTs to
/api/session→ receives session UUID - SSE Connect: Shim opens SSE connection to
/api/sse/{uuid} - Initial Render: Server runs
app(ui)→ builds VNode tree → diffs against empty tree → sends full tree as patches via SSE - User Interaction: User types in a text input → shim POSTs
{widget_id, value}to/api/event - Re-render: Server updates session state → re-runs
app(ui)→ diffs new tree vs old tree → sends only changed patches via SSE - DOM Patch: Shim receives patches → updates DOM in-place
Each widget gets a unique ID based on render order:
- Auto IDs:
w-0,w-1,w-2, ... (incremented per render) - Keyed IDs:
k-username,k-email(set viaui.with_key("..."))
The ID is used to:
- Match widget values in session state
- Target specific DOM nodes for patching
- Associate events with their source widget
rustview/
├── src/
│ ├── lib.rs # Crate root, re-exports, run() functions
│ ├── ui.rs # Ui struct — the main API surface
│ ├── widgets/
│ │ └── mod.rs # Widget render functions (VNode builders)
│ ├── vdom/
│ │ └── mod.rs # VNode struct, diffing algorithm
│ ├── server/
│ │ └── mod.rs # Axum server, routes, SSE, HTML/CSS
│ ├── session/
│ │ └── mod.rs # Session, SessionStore
│ ├── cache/
│ │ └── mod.rs # @cached decorator support
│ ├── interface.rs # Interface (Gradio-style) API
│ └── testing/
│ └── mod.rs # TestUi test harness
├── rustview-macros/ # Proc macros (#[app], #[cached])
├── examples/ # Example apps
├── docs/ # This documentation
└── KARAR.MD # Decision log (Turkish)
All architectural decisions are logged in KARAR.MD at the project root. Key decisions include:
- Axum over Actix: Chosen for async-first design and Tower ecosystem
- SSE over WebSocket: Simpler, unidirectional, auto-reconnect built-in
- VNode diffing: Minimal data transfer, no full page reloads
- DashMap for sessions: Lock-free concurrent access
- Inline SVG charts: Zero JavaScript chart dependencies
- No Polars dependency for core dataframe: Lightweight built-in DataFrame struct