Skip to main content

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}