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}