liminal_server/console/
topology.rs1use liminal::channel::ChannelMode;
4
5use crate::ServerError;
6use crate::server::connection::channel_registry::{ChannelOrigin, ChannelStatus};
7use crate::server::connection::services::LiminalConnectionServices;
8
9#[derive(Clone, Debug, PartialEq, Eq)]
11pub struct ChannelNode {
12 pub name: String,
14 pub mode: &'static str,
16 pub state: &'static str,
18 pub origin: &'static str,
20 pub reason: Option<String>,
22}
23
24#[derive(Clone, Debug, Default, PartialEq, Eq)]
26pub struct TopologySnapshot {
27 pub channels: Vec<ChannelNode>,
29 pub services: Vec<String>,
31}
32
33pub fn snapshot(services: &LiminalConnectionServices) -> Result<TopologySnapshot, ServerError> {
40 let roster_error = |error: &dyn std::fmt::Display| ServerError::ListenerAccept {
41 message: format!("console topology: {error}"),
42 };
43 let census = services
44 .channel_census()
45 .map_err(|error| roster_error(&error))?;
46 let mut channels = Vec::with_capacity(census.len());
47 for (name, status) in census {
48 let node = match status {
49 ChannelStatus::NotRegistered => continue,
50 ChannelStatus::Active { origin, mode, .. } => ChannelNode {
51 name,
52 mode: mode_word(mode),
53 state: "live",
54 origin: origin_word(origin),
55 reason: None,
56 },
57 ChannelStatus::Quiesced {
58 reason,
59 origin,
60 mode,
61 } => ChannelNode {
62 name,
63 mode: mode_word(mode),
64 state: "held",
65 origin: origin_word(origin),
66 reason: Some(reason),
67 },
68 };
69 channels.push(node);
70 }
71 Ok(TopologySnapshot {
72 channels,
73 services: services.responder_subjects()?,
74 })
75}
76
77const fn mode_word(mode: ChannelMode) -> &'static str {
78 match mode {
79 ChannelMode::Durable => "durable",
80 ChannelMode::Ephemeral => "ephemeral",
81 }
82}
83
84const fn origin_word(origin: ChannelOrigin) -> &'static str {
85 match origin {
86 ChannelOrigin::BootConfigured => "config",
87 ChannelOrigin::RuntimeRegistered => "runtime",
88 }
89}
90
91pub fn with_far_stations(
96 payload: &[u8],
97 far: &crate::link::federated::FarTopology,
98) -> Result<Vec<u8>, crate::link::federated::DiscoveryError> {
99 far.project(payload)
100}
101
102pub fn with_sites(
107 payload: &[u8],
108 lifecycle: &crate::services::lifecycle::Lifecycle,
109 device: &str,
110) -> Result<Vec<u8>, crate::services::lifecycle::site::SiteError> {
111 use crate::services::lifecycle::site::SiteError;
112 let mut document: serde_json::Value =
113 serde_json::from_slice(payload).map_err(|_| SiteError::Invalid("topology JSON"))?;
114 let mut stations = document["stations"]
115 .as_array()
116 .ok_or(SiteError::Invalid("topology stations"))?
117 .clone();
118 for station in lifecycle.site_stations(device)? {
119 let id = station["id"]
120 .as_str()
121 .ok_or(SiteError::Invalid("site station id"))?;
122 let resource = station["service"]
123 .as_str()
124 .ok_or(SiteError::Invalid("site resource id"))?;
125 let subjects = station["subjects"]
126 .as_array()
127 .ok_or(SiteError::Invalid("site subjects"))?;
128 let mut removed = Vec::new();
129 stations.retain(|old| {
130 let local = old["device"].as_str() == Some(device);
131 let worker = local
132 && old["kind"] == "worker"
133 && old["subjects"].as_array().is_some_and(|old_subjects| {
134 old_subjects
135 .iter()
136 .any(|subject| subjects.contains(subject))
137 });
138 let replaced = local
139 && (old["id"].as_str() == Some(id)
140 || (old["kind"] == "service" && old["id"].as_str() == Some(resource)));
141 if worker || replaced {
142 if let Some(name) = old["id"].as_str() {
143 removed.push(name.to_owned());
144 }
145 false
146 } else {
147 true
148 }
149 });
150 if let Some(lines) = document["lines"].as_array_mut() {
151 for line in lines {
152 if line["kind"] == "call" && subjects.contains(&line["id"]) {
153 let stops = line["stops"]
154 .as_array_mut()
155 .ok_or(SiteError::Invalid("site line stops"))?;
156 for stop in stops.iter_mut() {
157 if stop
158 .as_str()
159 .is_some_and(|name| removed.iter().any(|old| old == name))
160 {
161 *stop = id.into();
162 }
163 }
164 }
165 }
166 }
167 stations.push(station);
168 }
169 document["stations"] = stations.into();
170 liminal::channel::canonical::to_vec(document)
171 .map_err(|_| SiteError::Invalid("site topology encoding"))
172}
173
174pub fn with_agents(
182 payload: &[u8],
183 agents: &crate::agents::runtime::Agents,
184 device: &str,
185) -> Result<Vec<u8>, crate::agents::error::AgentError> {
186 use crate::agents::error::AgentError;
187 use crate::console::records::{AgentStation, StationState};
188 let mut document: serde_json::Value =
189 serde_json::from_slice(payload).map_err(|_| AgentError::Invalid("topology JSON"))?;
190 let records = agents.registry().station_records()?;
191 for record in records {
192 let connection = agents.presence().connection(&record.name)?;
193 let id = format!("agents.{}.inbox", record.name);
194 if !record.active() {
195 document["stations"]
196 .as_array_mut()
197 .ok_or(AgentError::Invalid("topology stations"))?
198 .retain(|station| station["id"].as_str() != Some(id.as_str()));
199 continue;
200 }
201 let mut stops = connection.map_or(Ok(Vec::new()), |connection| {
202 agents.listened_subjects(connection)
203 })?;
204 if let Some(connection) = connection {
205 let connection = connection.to_string();
206 if let Some(lines) = document["lines"].as_array_mut() {
207 for line in lines {
208 let line_name = line["id"].as_str().map(str::to_owned);
209 if let Some(stations) = line["stops"].as_array_mut() {
210 let listened = stations
211 .iter()
212 .any(|stop| stop.as_str() == Some(&connection));
213 if listened {
214 for stop in stations {
215 if stop.as_str() == Some(&connection) {
216 *stop = id.clone().into();
217 }
218 }
219 if let Some(name) = line_name {
220 stops.push(name);
221 }
222 }
223 }
224 }
225 }
226 }
227 if let Some(lines) = document["lines"].as_array_mut() {
228 for line in lines {
229 if line["id"]
230 .as_str()
231 .is_some_and(|subject| stops.iter().any(|own| own == subject))
232 {
233 let stations = line["stops"]
234 .as_array_mut()
235 .ok_or(AgentError::Invalid("line stops"))?;
236 if !stations
237 .iter()
238 .any(|station| station.as_str() == Some(id.as_str()))
239 {
240 stations.push(id.clone().into());
241 }
242 }
243 if line["id"].as_str() == Some(id.as_str()) {
244 line["kind"] = "stream".into();
245 line["life"] = "kept".into();
246 }
247 }
248 }
249 stops.sort();
250 stops.dedup();
251 let state = agents
252 .retained()
253 .value(&format!("agents.{}.state", record.name))?;
254 let state = match state.as_ref().and_then(|state| state["state"].as_str()) {
255 Some("waiting") => StationState::Waiting,
256 Some("present") if connection.is_some() => StationState::Present,
257 _ => StationState::Gone,
258 };
259 let station = serde_json::to_value(AgentStation::new(&record, device, state, stops))
260 .map_err(|_| AgentError::Invalid("agent station encoding"))?;
261 let stations = document["stations"]
262 .as_array_mut()
263 .ok_or(AgentError::Invalid("topology stations"))?;
264 stations.retain(|station| station["id"].as_str() != Some(&id));
265 stations.push(station);
266 }
267 liminal::channel::canonical::to_vec(document)
268 .map_err(|_| AgentError::Invalid("agent topology encoding"))
269}