API Layer
The FRB API layer in crates/api is deliberately thin. It exposes three operations per modality — open, dispatch, close — and delegates everything else to Session. No routing, child loading, or persistence logic lives in the API layer.
Session Registry
Section titled “Session Registry”session_api.rs maintains a global registry of open sessions:
// Simplified -- actual implementation uses a typed mapstatic SESSIONS: Lazy<Mutex<HashMap<String, Arc<Mutex<Session<M, I>>>>>> = /* ... */;Each open session is held behind Arc<Mutex<Session<M, I>>>. The session ID (a string) is the key used by Dart to reference a session.
Typed Open + Subscribe
Section titled “Typed Open + Subscribe”Each modality has a typed open function that loads or creates a session, subscribes a sink, and returns the session ID:
pub fn open_whiteboard_session( id: String, sink: StreamSink<WhiteboardSnapshot>,) -> String { // 1. Load from backend (or create new) // 2. Create Session<Whiteboard, SessionIntent<WhiteboardIntent>> // 3. Subscribe sink for WhiteboardSnapshot emissions // 4. Register in session registry // 5. Return session ID}
pub fn open_worksheet_session( id: String, sink: StreamSink<WorksheetSnapshot>,) -> String { /* ... */ }
pub fn open_lesson_plan_session( id: String, sink: StreamSink<LessonPlanSnapshot>,) -> String { /* ... */ }The StreamSink is FRB’s mechanism for pushing typed snapshots from Rust to Dart. After every dispatch() lifecycle (reduce —> recompile —> commit —> emit), the session pushes its M::Snapshot to all subscribed sinks.
Dispatch
Section titled “Dispatch”Each modality has a typed dispatch function:
pub fn dispatch_whiteboard( session_id: String, intent: WhiteboardIntent,) { let session = get_session(&session_id); let mut session = session.lock(); session.dispatch(Command::Intent(SessionIntent::Modality(intent)));}For parent modalities, dispatch accepts the parent intent type:
pub fn dispatch_lesson_plan( session_id: String, intent: ParentSessionIntent<LessonPlanIntent, SessionIntent<WhiteboardIntent>>,) { let session = get_session(&session_id); let mut session = session.lock(); session.dispatch(Command::Intent(intent));}All routing, child loading, and lifecycle logic happens inside Session::dispatch() and the ReduceIntent impls. The API layer just forwards.
pub fn close_session(session_id: String) { // 1. Remove from registry // 2. Persist final state (Doc::export_snapshot) // 3. Drop session (cleans up channels, sinks)}Close always persists the final state before dropping the session.
Auto-Save
Section titled “Auto-Save”A background loop periodically flushes dirty sessions:
pub fn start_autosave_loop(interval_secs: u64) { // Spawns a thread that every `interval_secs`: // 1. Iterates all registered sessions // 2. Exports updates for sessions with changes since last save // 3. Writes to backend}Typical interval is 30 seconds. close_session always persists final state regardless of the auto-save timer.
Session-Level Operations
Section titled “Session-Level Operations”Undo and redo are session-level operations exposed through the API:
pub fn session_undo(session_id: String) { /* ... */ }pub fn session_redo(session_id: String) { /* ... */ }These operate on the Doc<M::Synced> inside the session, reverting or replaying Loro CRDT operations.
AI Dispatch
Section titled “AI Dispatch”AI initialization and message dispatch are separate API functions:
pub fn init_ai(api_key: String, base_url: String, model: String) { // Configure the AI runtime (rig model, API credentials)}
pub fn dispatch_ai(session_id: String, intent: AiIntent) { let session = get_session(&session_id); let mut session = session.lock(); session.dispatch(Command::Intent(SessionIntent::Ai(intent)));}AiIntent variants include SendMessage { text } and CancelGeneration. The AI agent thread is spawned per-session in session_api.rs::spawn_whiteboard_agent():
- Session holds
self_ref: Option<Weak<Mutex<Self>>>andagent_tx: Option<Sender<String>> - The agent thread loops on an
mpsc::Receiver<String>, callingrunner::chat_stream()per message - Feedback flows via
CommandSender— periodicFlushdispatches drain events and emit snapshots - Tools are created via
bridge::create_tools::<M>(session_arc), dispatching through the sameSession::dispatchpath as Dart
API Surface Summary
Section titled “API Surface Summary”| Function | Modality | Direction |
|---|---|---|
open_*_session(id, sink) | Per-modality | Rust —> Dart (stream) |
dispatch_*(session_id, intent) | Per-modality | Dart —> Rust |
close_session(session_id) | Generic | Dart —> Rust |
session_undo(session_id) | Generic | Dart —> Rust |
session_redo(session_id) | Generic | Dart —> Rust |
init_ai(key, url, model) | Global | Dart —> Rust |
dispatch_ai(session_id, intent) | Generic | Dart —> Rust |
start_autosave_loop(interval) | Global | Dart —> Rust |
Related
Section titled “Related”FRB Patterns— annotation decisions for types crossing the FFI boundaryFRB Codegen— regenerating bindings after API changesWhiteboard— leaf modality session exampleLesson Plan— parent modality session with child dispatch