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 registered conversation responder.
136    Service,
137    /// A registered worker.
138    Worker,
139    /// This server's persistent store, which a durable line's messages
140    /// reach before the fan-out (lane brief ruling 5).
141    Depot,
142    /// This server's exchange, the fan-out every line passes; its id is the
143    /// device's (lane brief ruling 2).
144    Exchange,
145}
146
147/// A station's state. A registered responder is live; nothing in the server
148/// is asleep, held or retired as a station yet.
149#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
150#[serde(rename_all = "lowercase")]
151pub enum StationState {
152    /// Registered and answering.
153    Live,
154}
155
156/// A line: a stream on the roster.
157#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
158pub struct Line {
159    /// The channel name.
160    pub id: String,
161    /// A channel stream or a live serving subject.
162    pub kind: LineKind,
163    /// How long its messages are kept.
164    pub life: LineLife,
165    /// Whether it admits.
166    pub state: LineState,
167    /// The stations it passes, in order.
168    pub stops: Vec<String>,
169}
170
171/// What a line is.
172#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
173#[serde(rename_all = "lowercase")]
174pub enum LineKind {
175    /// A channel.
176    Stream,
177    /// A subject served by a live responder or worker.
178    Call,
179}
180
181/// How long a line keeps its messages, from the channel's mode.
182#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
183#[serde(rename_all = "lowercase")]
184pub enum LineLife {
185    /// Ephemeral: messages pass and are not stored.
186    Passing,
187    /// Durable: messages are stored.
188    Kept,
189}
190
191/// Whether a line admits.
192#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
193#[serde(rename_all = "lowercase")]
194pub enum LineState {
195    /// Admitting publishes and subscribes.
196    Live,
197    /// Quiesced: refusing new publishes and subscribes.
198    Quiesced,
199}
200
201/// A cluster or federated link, with only observed network facts.
202#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
203pub struct Link {
204    /// Carrier refusals retained across later observations.
205    pub carrier_refusals: Vec<String>,
206    /// The device carrying a relayed link.
207    #[serde(skip_serializing_if = "Option::is_none")]
208    pub carried_by: Option<String>,
209    /// The link id, `<this device>~<peer>`.
210    pub id: String,
211    /// The peer's name as shown.
212    pub name: String,
213    /// How the two devices are joined.
214    pub kind: LinkKind,
215    /// The link's state.
216    pub state: LinkState,
217    /// The transport carrying this link, when observed.
218    #[serde(skip_serializing_if = "Option::is_none")]
219    pub carrier: Option<Carrier>,
220    /// The relaying device, only for a relay carrier.
221    #[serde(skip_serializing_if = "Option::is_none")]
222    pub relayed: Option<String>,
223    /// The cause of a down link, when observed.
224    #[serde(skip_serializing_if = "Option::is_none")]
225    pub reason: Option<String>,
226    /// Observed crossings; absent when no source counts them.
227    #[serde(skip_serializing_if = "Option::is_none")]
228    pub crossed: Option<u64>,
229    /// Observed refusals; absent when no source counts them.
230    #[serde(skip_serializing_if = "Option::is_none")]
231    pub refused: Option<u64>,
232    /// Crossings answered from an existing receipt.
233    #[serde(skip_serializing_if = "Option::is_none")]
234    pub already_crossed: Option<u64>,
235    /// Observed opens and ends by carrier cause.
236    #[serde(flatten)]
237    pub lifecycle: Option<crate::link::LinkLifeCounters>,
238    /// Round trip in milliseconds. No source: always `null`, never `0`.
239    pub rtt_ms: Option<u64>,
240    /// The two device ids it joins, in sorted order, so the same link has
241    /// the same bytes whichever end publishes it.
242    pub ends: [String; 2],
243}
244
245impl Link {
246    /// The link from `local` to `peer`, up, with no round trip measured.
247    #[must_use]
248    pub fn to_peer(local: &str, peer: &str) -> Self {
249        let ends = if peer < local {
250            [peer.to_owned(), local.to_owned()]
251        } else {
252            [local.to_owned(), peer.to_owned()]
253        };
254        Self {
255            id: format!("{local}~{peer}"),
256            name: peer.to_owned(),
257            kind: LinkKind::SameNetwork,
258            state: LinkState::Up,
259            rtt_ms: None,
260            carrier: None,
261            carrier_refusals: Vec::new(),
262            carried_by: None,
263            relayed: None,
264            reason: None,
265            crossed: None,
266            refused: None,
267            already_crossed: None,
268            lifecycle: None,
269            ends,
270        }
271    }
272    /// A federated observation; an up link carries no end reason.
273    #[must_use]
274    pub fn federated(
275        local: &str,
276        peer: &str,
277        carrier: Option<Carrier>,
278        state: LinkState,
279        reason: Option<String>,
280        counters: LinkCounters,
281    ) -> Self {
282        let relayed = match &carrier {
283            Some(Carrier::Relay(device)) => Some(device.clone()),
284            _ => None,
285        };
286        Self {
287            kind: LinkKind::Federated,
288            state,
289            carrier,
290            carried_by: relayed.clone(),
291            carrier_refusals: reason.clone().into_iter().collect(),
292            relayed,
293            reason: if state == LinkState::Down {
294                reason
295            } else {
296                None
297            },
298            crossed: counters.crossed,
299            refused: counters.refused,
300            already_crossed: counters.already_crossed,
301            lifecycle: counters.lifecycle,
302            ..Self::to_peer(local, peer)
303        }
304    }
305}
306
307/// The transport and, for a relay, the device carrying the link.
308#[derive(Clone, Debug, PartialEq, Eq)]
309pub enum Carrier {
310    /// A direct wire connection.
311    Wire,
312    /// A direct UDP connection.
313    Direct,
314    /// A connection carried by the named relaying device.
315    Relay(String),
316}
317
318impl Serialize for Carrier {
319    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
320        serializer.serialize_str(match self {
321            Self::Wire => "wire",
322            Self::Direct => "direct",
323            Self::Relay(_) => "relay",
324        })
325    }
326}
327
328/// Independently observed counters; unknown is absent, known zero is zero.
329#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
330pub struct LinkCounters {
331    /// Crossings answered from an existing receipt.
332    pub already_crossed: Option<u64>,
333    /// Opens and ends counted by the lifecycle owner.
334    pub lifecycle: Option<crate::link::LinkLifeCounters>,
335    /// Crossings counted by the source.
336    pub crossed: Option<u64>,
337    /// Refusals counted by the source.
338    pub refused: Option<u64>,
339}
340
341/// How two devices are joined.
342#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
343pub enum LinkKind {
344    /// Members of one cluster.
345    #[serde(rename = "same network")]
346    SameNetwork,
347    /// Machines joined across networks.
348    #[serde(rename = "federated")]
349    Federated,
350}
351
352/// A link's observed state. A departed cluster peer has no down record.
353#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
354#[serde(rename_all = "lowercase")]
355pub enum LinkState {
356    /// The peer is a member.
357    Up,
358    /// A federated link ended.
359    Down,
360}
361
362/// A live connection to this server.
363#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
364pub struct Connection {
365    /// The connection id, as a tap message's `from` names it.
366    pub id: String,
367    /// The transport it arrived on.
368    pub mount: Mount,
369    /// The participant its pass names, when it presented one.
370    #[serde(skip_serializing_if = "Option::is_none")]
371    pub participant: Option<String>,
372}
373
374/// The transport a connection arrived on.
375#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
376#[serde(rename_all = "lowercase")]
377pub enum Mount {
378    /// The TCP wire listener.
379    Tcp,
380    /// The WebSocket listener.
381    Websocket,
382    /// The in-process transport.
383    Loopback,
384}
385
386impl From<crate::server::mount::MountKind> for Mount {
387    fn from(mount: crate::server::mount::MountKind) -> Self {
388        match mount {
389            crate::server::mount::MountKind::Tcp => Self::Tcp,
390            crate::server::mount::MountKind::WebSocket => Self::Websocket,
391            crate::server::mount::MountKind::Loopback => Self::Loopback,
392        }
393    }
394}
395
396/// One `console.topology` record: the whole document as it stands.
397#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
398pub struct TopologyDocument {
399    v: u64,
400    #[serde(rename = "type")]
401    record_type: &'static str,
402    /// One more than the previous document's, from 1 at start.
403    pub rev: u64,
404    /// The id of the device this server is, one of `devices`' ids (lane
405    /// brief ruling 1, Hermes 7 Oct 03:51): named once here, never as a flag
406    /// on each device.
407    pub this_device: String,
408    /// This device, then each peer.
409    pub devices: Vec<Device>,
410    /// The registered services.
411    pub stations: Vec<Station>,
412    /// Every roster channel outside `console.`.
413    pub lines: Vec<Line>,
414    /// Cluster peer links, then known federated links.
415    pub links: Vec<Link>,
416    /// One per live connection.
417    pub connections: Vec<Connection>,
418}
419
420impl TopologyDocument {
421    /// A document at `rev`, from the server that is device `this_device`.
422    #[must_use]
423    pub const fn new(
424        rev: u64,
425        this_device: String,
426        devices: Vec<Device>,
427        stations: Vec<Station>,
428        lines: Vec<Line>,
429        peer_links: Vec<Link>,
430        connections: Vec<Connection>,
431    ) -> Self {
432        Self {
433            v: RECORD_VERSION,
434            record_type: "topology",
435            rev,
436            this_device,
437            devices,
438            stations,
439            lines,
440            links: peer_links,
441            connections,
442        }
443    }
444}
445
446mod tap_records;
447pub use tap_records::{Hop, Outcome, TapBatch, TapMessage, tap_batch_record};
448
449/// The canonical bytes of a record: through a JSON value, so every key is in
450/// sorted order at every depth, as the schema normaliser writes them.
451///
452/// # Errors
453/// Returns [`ConsoleError::Encode`] naming the record when serialisation fails.
454pub fn canonical_bytes<Record: Serialize>(
455    record: &Record,
456    kind: &'static str,
457) -> Result<Vec<u8>, ConsoleError> {
458    serde_json::to_value(record)
459        .and_then(|value| serde_json::to_vec(&value))
460        .map_err(|source| ConsoleError::Encode {
461            record: kind,
462            source,
463        })
464}