//! Read/write helpers for the `active_agents` LWW-map collection. //! //! Active-agent entries are keyed by `agent_id` and track which story and node //! an agent is currently working on, along with when it started. use bft_json_crdt::json_crdt::{JsonValue, *}; use bft_json_crdt::op::ROOT_ID; use serde_json::json; use super::super::state::{apply_and_persist, get_crdt, rebuild_active_agent_index}; use super::super::types::{ActiveAgentCrdt, ActiveAgentView}; use super::list_id_at; /// Write or update an active-agent entry keyed by `agent_id`. pub fn write_active_agent(agent_id: &str, story_id: &str, node_id: &str, started_at: f64) { let Some(state_mutex) = get_crdt() else { return; }; let Ok(mut state) = state_mutex.lock() else { return; }; if let Some(&idx) = state.active_agent_index.get(agent_id) { apply_and_persist(&mut state, |s| { s.crdt.doc.active_agents[idx] .story_id .set(story_id.to_string()) }); apply_and_persist(&mut state, |s| { s.crdt.doc.active_agents[idx] .node_id .set(node_id.to_string()) }); apply_and_persist(&mut state, |s| { s.crdt.doc.active_agents[idx].started_at.set(started_at) }); } else { let entry: JsonValue = json!({ "agent_id": agent_id, "story_id": story_id, "node_id": node_id, "started_at": started_at, }) .into(); apply_and_persist(&mut state, |s| { s.crdt.doc.active_agents.insert(ROOT_ID, entry) }); state.active_agent_index = rebuild_active_agent_index(&state.crdt); } } /// Read all active-agent entries. pub fn read_all_active_agents() -> Option> { let state_mutex = get_crdt()?; let state = state_mutex.lock().ok()?; let mut out = Vec::new(); for entry in state.crdt.doc.active_agents.iter() { if let Some(v) = extract_active_agent_view(entry) { out.push(v); } } Some(out) } /// Read a single active-agent entry by `agent_id`. pub fn read_active_agent(agent_id: &str) -> Option { let state_mutex = get_crdt()?; let state = state_mutex.lock().ok()?; let &idx = state.active_agent_index.get(agent_id)?; extract_active_agent_view(&state.crdt.doc.active_agents[idx]) } /// Tombstone an active-agent entry by `agent_id`. pub fn delete_active_agent(agent_id: &str) -> bool { let Some(state_mutex) = get_crdt() else { return false; }; let Ok(mut state) = state_mutex.lock() else { return false; }; let Some(&idx) = state.active_agent_index.get(agent_id) else { return false; }; let Some(op_id) = list_id_at(&state.crdt.doc.active_agents, idx) else { return false; }; apply_and_persist(&mut state, |s| s.crdt.doc.active_agents.delete(op_id)); state.active_agent_index = rebuild_active_agent_index(&state.crdt); true } /// Convert a CRDT active-agent entry into its read-only view representation. pub(super) fn extract_active_agent_view(entry: &ActiveAgentCrdt) -> Option { let agent_id = match entry.agent_id.view() { JsonValue::String(s) if !s.is_empty() => s, _ => return None, }; let story_id = match entry.story_id.view() { JsonValue::String(s) if !s.is_empty() => Some(s), _ => None, }; let node_id = match entry.node_id.view() { JsonValue::String(s) if !s.is_empty() => Some(s), _ => None, }; let started_at = match entry.started_at.view() { JsonValue::Number(n) => n, _ => 0.0, }; Some(ActiveAgentView { agent_id, story_id, node_id, started_at, }) }