liminal_server/console/records.rs
1//! The console's records, in the field names of the console's own reader.
2//!
3//! The reader is `apps/liminal-console/src/live/types.ts` (`TopologyDocument`
4//! and `TapBatch`), so a record the server publishes is a record
5//! `parseRecord` reads.
6//!
7//! The bytes are the server's canonical form: the record is turned into a JSON
8//! value, whose maps keep their keys sorted, and serialised from there. That is
9//! the same output the channel's schema normaliser gives, so `v` arrives in
10//! sorted position, not first, and a reader takes every field by key.
11//!
12//! Left out of the topology because nothing in the server holds them yet:
13//! archives, exports, gates and proposed routes, a link's round
14//! trip (sent as `null`), and a line's product and stops beyond
15//! what the roster knows. Nothing is sent as a made-up value in their place.
16
17mod carrier_refusals;
18
19use serde::Serialize;
20
21use super::error::ConsoleError;
22
23/// The record version the console reads.
24pub const RECORD_VERSION: u64 = 1;
25
26/// A device: this one, a cluster peer, or a federated peer.
27#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
28pub struct Device {
29 /// The device id (the cluster node name, or `local` without a cluster).
30 pub id: String,
31 /// The device's name as shown.
32 pub name: String,
33 /// Always a device: federation has no source.
34 pub kind: DeviceKind,
35 /// The liminal version running there: known for this device (from the
36 /// build), not for a peer, whose version nothing here holds.
37 #[serde(skip_serializing_if = "Option::is_none")]
38 pub liminal: Option<String>,
39 /// When the server's incarnation started, in wall-clock milliseconds
40 /// since the epoch: known for this device, not for a peer. A reader takes
41 /// the uptime as its own now less this, so the figure never goes stale
42 /// between documents (Hermes 7 Oct 01:49, which replaced `uptime`).
43 #[serde(skip_serializing_if = "Option::is_none")]
44 pub started: Option<u64>,
45 /// Why this device's start is missing, in the server's words: the clock
46 /// fault met building the document (Buttercup's finding 3, Hermes 03:05).
47 /// Present only when `started` is absent on this device, so never both.
48 #[serde(skip_serializing_if = "Option::is_none")]
49 pub started_fault: Option<String>,
50 /// The device this one is a district of, when configured.
51 #[serde(skip_serializing_if = "Option::is_none")]
52 pub parent: Option<String>,
53 /// Where the map draws the device's zone (`box` in types.ts), when this
54 /// server places it.
55 #[serde(rename = "box", skip_serializing_if = "Option::is_none")]
56 pub zone: Option<DeviceBox>,
57}
58
59/// A device's zone on the console's map, in the map's own units.
60#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
61pub struct DeviceBox {
62 /// Left edge.
63 pub x: i64,
64 /// Top edge.
65 pub y: i64,
66 /// Width.
67 pub w: i64,
68 /// Height.
69 pub h: i64,
70}
71
72impl DeviceBox {
73 /// This server's own zone: the map's fitted area inset by 40 on each
74 /// side, the zone the console draws for a lone device (`NetworkPage`'s
75 /// `FIT` of -180, 20, 1420, 600).
76 pub const THIS: Self = Self {
77 x: -140,
78 y: 60,
79 w: 1340,
80 h: 520,
81 };
82}
83
84/// What a device is.
85#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
86#[serde(rename_all = "lowercase")]
87pub enum DeviceKind {
88 /// A liminal server.
89 Device,
90}
91
92/// A station: a registered service.
93#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
94pub struct Station {
95 /// The station id (the responder's subject).
96 pub id: String,
97 /// Its name as shown.
98 pub name: String,
99 /// The device it runs on.
100 pub device: String,
101 /// What kind of station.
102 pub kind: StationKind,
103 /// Its state.
104 pub state: StationState,
105 /// SRV-027 R3: the subjects a registered worker serves (its
106 /// registration's task queue). Absent on a station that is not a worker.
107 #[serde(skip_serializing_if = "Option::is_none")]
108 pub subjects: Option<Vec<String>>,
109 /// SRV-027 R3: what a registered worker accepts and returns, each
110 /// activity's schemas as the exact strings it registered; empty for a
111 /// worker that declared none (raw bytes in and out). Absent on a
112 /// station that is not a worker.
113 #[serde(skip_serializing_if = "Option::is_none")]
114 pub activities: Option<Vec<StationActivity>>,
115}
116
117/// One activity a worker declared at registration (SRV-027 R3): its name and
118/// its input and output schemas, carried as the strings the registration
119/// held, never parsed or normalised here.
120#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
121pub struct StationActivity {
122 /// The activity's name.
123 pub name: String,
124 /// The input schema as registered.
125 pub input_schema_json: String,
126 /// The output schema as registered.
127 pub output_schema_json: String,
128}
129
130/// What a station is. Only services have a source until the services
131/// registry exists.
132#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
133#[serde(rename_all = "lowercase")]
134pub enum StationKind {
135 /// A Lys agent connected through its own ticket.
136 Agent,
137 /// A registered conversation responder.
138 Service,
139 /// A registered worker.
140 Worker,
141 /// This server's persistent store, which a durable line's messages
142 /// reach before the fan-out (lane brief ruling 5).
143 Depot,
144 /// This server's exchange, the fan-out every line passes; its id is the
145 /// device's (lane brief ruling 2).
146 Exchange,
147}
148
149/// A station's state. A registered responder is live; nothing in the server
150/// is asleep, held or retired as a station yet.
151#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
152#[serde(rename_all = "lowercase")]
153pub enum StationState {
154 /// Approved contracts are being built by the bound agent.
155 Building,
156 /// A builder's worker is fenced to its site's approvers.
157 Trial,
158 /// Completed and ready for its template process to wake.
159 Asleep,
160 /// Deliberately stopped, with its history kept.
161 Held,
162 /// Permanently retired, with its history kept.
163 Retired,
164 /// A verified agent ticket is connected and listening.
165 Present,
166 /// The agent has a deliberate act held in Lys.
167 Waiting,
168 /// The agent's ticket is absent.
169 Gone,
170 /// Registered and answering.
171 Live,
172}
173
174/// A line: a stream on the roster.
175#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
176pub struct Line {
177 /// The channel name.
178 pub id: String,
179 /// A channel stream or a live serving subject.
180 pub kind: LineKind,
181 /// How long its messages are kept.
182 pub life: LineLife,
183 /// Whether it admits.
184 pub state: LineState,
185 /// The stations it passes, in order.
186 pub stops: Vec<String>,
187}
188
189/// What a line is.
190#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
191#[serde(rename_all = "lowercase")]
192pub enum LineKind {
193 /// A channel.
194 Stream,
195 /// A subject served by a live responder or worker.
196 Call,
197}
198
199/// How long a line keeps its messages, from the channel's mode.
200#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
201#[serde(rename_all = "lowercase")]
202pub enum LineLife {
203 /// Ephemeral: messages pass and are not stored.
204 Passing,
205 /// Durable: messages are stored.
206 Kept,
207}
208
209/// Whether a line admits.
210#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
211#[serde(rename_all = "lowercase")]
212pub enum LineState {
213 /// Admitting publishes and subscribes.
214 Live,
215 /// Quiesced: refusing new publishes and subscribes.
216 Quiesced,
217}
218
219/// A cluster or federated link, with only observed network facts.
220#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
221pub struct Link {
222 /// Carrier refusals retained across later observations.
223 pub carrier_refusals: Vec<String>,
224 /// The device carrying a relayed link.
225 #[serde(skip_serializing_if = "Option::is_none")]
226 pub carried_by: Option<String>,
227 /// The link id, `<this device>~<peer>`.
228 pub id: String,
229 /// The peer's name as shown.
230 pub name: String,
231 /// How the two devices are joined.
232 pub kind: LinkKind,
233 /// The link's state.
234 pub state: LinkState,
235 /// The transport carrying this link, when observed.
236 #[serde(skip_serializing_if = "Option::is_none")]
237 pub carrier: Option<Carrier>,
238 /// The relaying device, only for a relay carrier.
239 #[serde(skip_serializing_if = "Option::is_none")]
240 pub relayed: Option<String>,
241 /// The cause of a down link, when observed.
242 #[serde(skip_serializing_if = "Option::is_none")]
243 pub reason: Option<String>,
244 /// Observed crossings; absent when no source counts them.
245 #[serde(skip_serializing_if = "Option::is_none")]
246 pub crossed: Option<u64>,
247 /// Observed refusals; absent when no source counts them.
248 #[serde(skip_serializing_if = "Option::is_none")]
249 pub refused: Option<u64>,
250 /// Crossings answered from an existing receipt.
251 #[serde(skip_serializing_if = "Option::is_none")]
252 pub already_crossed: Option<u64>,
253 /// Observed opens and ends by carrier cause.
254 #[serde(flatten)]
255 pub lifecycle: Option<crate::link::LinkLifeCounters>,
256 /// Round trip in milliseconds. No source: always `null`, never `0`.
257 pub rtt_ms: Option<u64>,
258 /// The two device ids it joins, in sorted order, so the same link has
259 /// the same bytes whichever end publishes it.
260 pub ends: [String; 2],
261}
262
263impl Link {
264 /// The link from `local` to `peer`, up, with no round trip measured.
265 #[must_use]
266 pub fn to_peer(local: &str, peer: &str) -> Self {
267 let ends = if peer < local {
268 [peer.to_owned(), local.to_owned()]
269 } else {
270 [local.to_owned(), peer.to_owned()]
271 };
272 Self {
273 id: format!("{local}~{peer}"),
274 name: peer.to_owned(),
275 kind: LinkKind::SameNetwork,
276 state: LinkState::Up,
277 rtt_ms: None,
278 carrier: None,
279 carrier_refusals: Vec::new(),
280 carried_by: None,
281 relayed: None,
282 reason: None,
283 crossed: None,
284 refused: None,
285 already_crossed: None,
286 lifecycle: None,
287 ends,
288 }
289 }
290 /// A federated observation; an up link carries no end reason.
291 #[must_use]
292 pub fn federated(
293 local: &str,
294 peer: &str,
295 carrier: Option<Carrier>,
296 state: LinkState,
297 reason: Option<String>,
298 counters: LinkCounters,
299 ) -> Self {
300 let relayed = match &carrier {
301 Some(Carrier::Relay(device)) => Some(device.clone()),
302 _ => None,
303 };
304 Self {
305 kind: LinkKind::Federated,
306 state,
307 carrier,
308 carried_by: relayed.clone(),
309 carrier_refusals: reason.clone().into_iter().collect(),
310 relayed,
311 reason: if state == LinkState::Down {
312 reason
313 } else {
314 None
315 },
316 crossed: counters.crossed,
317 refused: counters.refused,
318 already_crossed: counters.already_crossed,
319 lifecycle: counters.lifecycle,
320 ..Self::to_peer(local, peer)
321 }
322 }
323}
324
325/// The transport and, for a relay, the device carrying the link.
326#[derive(Clone, Debug, PartialEq, Eq)]
327pub enum Carrier {
328 /// A direct wire connection.
329 Wire,
330 /// A direct UDP connection.
331 Direct,
332 /// A connection carried by the named relaying device.
333 Relay(String),
334}
335
336impl Serialize for Carrier {
337 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
338 serializer.serialize_str(match self {
339 Self::Wire => "wire",
340 Self::Direct => "direct",
341 Self::Relay(_) => "relay",
342 })
343 }
344}
345
346/// Independently observed counters; unknown is absent, known zero is zero.
347#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
348pub struct LinkCounters {
349 /// Crossings answered from an existing receipt.
350 pub already_crossed: Option<u64>,
351 /// Opens and ends counted by the lifecycle owner.
352 pub lifecycle: Option<crate::link::LinkLifeCounters>,
353 /// Crossings counted by the source.
354 pub crossed: Option<u64>,
355 /// Refusals counted by the source.
356 pub refused: Option<u64>,
357}
358
359/// How two devices are joined.
360#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
361pub enum LinkKind {
362 /// Members of one cluster.
363 #[serde(rename = "same network")]
364 SameNetwork,
365 /// Machines joined across networks.
366 #[serde(rename = "federated")]
367 Federated,
368}
369
370/// A link's observed state. A departed cluster peer has no down record.
371#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
372#[serde(rename_all = "lowercase")]
373pub enum LinkState {
374 /// The peer is a member.
375 Up,
376 /// A federated link ended.
377 Down,
378}
379
380/// A live connection to this server.
381#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
382pub struct Connection {
383 /// The connection id, as a tap message's `from` names it.
384 pub id: String,
385 /// The transport it arrived on.
386 pub mount: Mount,
387 /// The participant its pass names, when it presented one.
388 #[serde(skip_serializing_if = "Option::is_none")]
389 pub participant: Option<String>,
390}
391
392/// The transport a connection arrived on.
393#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
394#[serde(rename_all = "lowercase")]
395pub enum Mount {
396 /// The TCP wire listener.
397 Tcp,
398 /// The WebSocket listener.
399 Websocket,
400 /// The in-process transport.
401 Loopback,
402}
403
404impl From<crate::server::mount::MountKind> for Mount {
405 fn from(mount: crate::server::mount::MountKind) -> Self {
406 match mount {
407 crate::server::mount::MountKind::Tcp => Self::Tcp,
408 crate::server::mount::MountKind::WebSocket => Self::Websocket,
409 crate::server::mount::MountKind::Loopback => Self::Loopback,
410 }
411 }
412}
413
414/// One `console.topology` record: the whole document as it stands.
415#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
416pub struct TopologyDocument {
417 v: u64,
418 #[serde(rename = "type")]
419 record_type: &'static str,
420 /// One more than the previous document's, from 1 at start.
421 pub rev: u64,
422 /// The id of the device this server is, one of `devices`' ids (lane
423 /// brief ruling 1, Hermes 7 Oct 03:51): named once here, never as a flag
424 /// on each device.
425 pub this_device: String,
426 /// This device, then each peer.
427 pub devices: Vec<Device>,
428 /// The registered services.
429 pub stations: Vec<Station>,
430 /// Every roster channel outside `console.`.
431 pub lines: Vec<Line>,
432 /// Cluster peer links, then known federated links.
433 pub links: Vec<Link>,
434 /// One per live connection.
435 pub connections: Vec<Connection>,
436}
437
438impl TopologyDocument {
439 /// A document at `rev`, from the server that is device `this_device`.
440 #[must_use]
441 pub const fn new(
442 rev: u64,
443 this_device: String,
444 devices: Vec<Device>,
445 stations: Vec<Station>,
446 lines: Vec<Line>,
447 peer_links: Vec<Link>,
448 connections: Vec<Connection>,
449 ) -> Self {
450 Self {
451 v: RECORD_VERSION,
452 record_type: "topology",
453 rev,
454 this_device,
455 devices,
456 stations,
457 lines,
458 links: peer_links,
459 connections,
460 }
461 }
462}
463
464mod tap_records;
465pub use crate::agents::station::AgentStation;
466pub use tap_records::{Hop, Outcome, TapBatch, TapMessage, tap_batch_record};
467
468/// A worker refusal uses the terminal call's existing refused outcome. Its
469/// stable code belongs in the call fields; application words never enter a tap.
470pub const WORKER_REFUSAL_OUTCOME: Outcome = Outcome::Refused;
471
472/// The canonical bytes of a record: through a JSON value rebuilt in sorted key
473/// order at every depth, as the schema normaliser writes them, whatever map
474/// type `serde_json` was built with.
475///
476/// # Errors
477/// Returns [`ConsoleError::Encode`] naming the record when serialisation fails.
478pub fn canonical_bytes<Record: Serialize>(
479 record: &Record,
480 kind: &'static str,
481) -> Result<Vec<u8>, ConsoleError> {
482 serde_json::to_value(record)
483 .and_then(liminal::channel::canonical::to_vec)
484 .map_err(|source| ConsoleError::Encode {
485 record: kind,
486 source,
487 })
488}