liminal-server 0.21.1

Standalone server for the liminal messaging bus
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
//! The console's records, in the field names of the console's own reader.
//!
//! The reader is `apps/liminal-console/src/live/types.ts` (`TopologyDocument`
//! and `TapBatch`), so a record the server publishes is a record
//! `parseRecord` reads.
//!
//! The bytes are the server's canonical form: the record is turned into a JSON
//! value, whose maps keep their keys sorted, and serialised from there. That is
//! the same output the channel's schema normaliser gives, so `v` arrives in
//! sorted position, not first, and a reader takes every field by key.
//!
//! Left out of the topology because nothing in the server holds them yet:
//! archives, exports, gates, federation and proposed routes, a link's round
//! trip (sent as `null`) and refusals, and a line's product and stops beyond
//! what the roster knows. Nothing is sent as a made-up value in their place.

use std::num::NonZeroU32;

use serde::Serialize;

use super::error::ConsoleError;
use super::wall::WallBase;
use crate::tap::{self, TapIdent, TapOutcome, TapSource};

/// The record version the console reads.
pub const RECORD_VERSION: u64 = 1;

/// A device: this one, or a cluster peer.
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct Device {
    /// The device id (the cluster node name, or `local` without a cluster).
    pub id: String,
    /// The device's name as shown.
    pub name: String,
    /// Always a device: federation has no source.
    pub kind: DeviceKind,
    /// The liminal version running there: known for this device (from the
    /// build), not for a peer, whose version nothing here holds.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub liminal: Option<String>,
    /// When the server's incarnation started, in wall-clock milliseconds
    /// since the epoch: known for this device, not for a peer. A reader takes
    /// the uptime as its own now less this, so the figure never goes stale
    /// between documents (Hermes 7 Oct 01:49, which replaced `uptime`).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub started: Option<u64>,
    /// Why this device's start is missing, in the server's words: the clock
    /// fault met building the document (Buttercup's finding 3, Hermes 03:05).
    /// Present only when `started` is absent on this device, so never both.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub started_fault: Option<String>,
    /// Where the map draws the device's zone (`box` in types.ts), when this
    /// server places it.
    #[serde(rename = "box", skip_serializing_if = "Option::is_none")]
    pub zone: Option<DeviceBox>,
}

/// A device's zone on the console's map, in the map's own units.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
pub struct DeviceBox {
    /// Left edge.
    pub x: i64,
    /// Top edge.
    pub y: i64,
    /// Width.
    pub w: i64,
    /// Height.
    pub h: i64,
}

impl DeviceBox {
    /// This server's own zone: the map's fitted area inset by 40 on each
    /// side, the zone the console draws for a lone device (`NetworkPage`'s
    /// `FIT` of -180, 20, 1420, 600).
    pub const THIS: Self = Self {
        x: -140,
        y: 60,
        w: 1340,
        h: 520,
    };
}

/// What a device is.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum DeviceKind {
    /// A liminal server.
    Device,
}

/// A station: a registered service.
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct Station {
    /// The station id (the responder's subject).
    pub id: String,
    /// Its name as shown.
    pub name: String,
    /// The device it runs on.
    pub device: String,
    /// What kind of station.
    pub kind: StationKind,
    /// Its state.
    pub state: StationState,
    /// SRV-027 R3: the subjects a registered worker serves (its
    /// registration's task queue). Absent on a station that is not a worker.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub subjects: Option<Vec<String>>,
    /// SRV-027 R3: what a registered worker accepts and returns, each
    /// activity's schemas as the exact strings it registered; empty for a
    /// worker that declared none (raw bytes in and out). Absent on a
    /// station that is not a worker.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub activities: Option<Vec<StationActivity>>,
}

/// One activity a worker declared at registration (SRV-027 R3): its name and
/// its input and output schemas, carried as the strings the registration
/// held, never parsed or normalised here.
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct StationActivity {
    /// The activity's name.
    pub name: String,
    /// The input schema as registered.
    pub input_schema_json: String,
    /// The output schema as registered.
    pub output_schema_json: String,
}

/// What a station is. Only services have a source until the services
/// registry exists.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum StationKind {
    /// A registered conversation responder.
    Service,
    /// A registered worker.
    Worker,
    /// This server's persistent store, which a durable line's messages
    /// reach before the fan-out (lane brief ruling 5).
    Depot,
    /// This server's exchange, the fan-out every line passes; its id is the
    /// device's (lane brief ruling 2).
    Exchange,
}

/// A station's state. A registered responder is live; nothing in the server
/// is asleep, held or retired as a station yet.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum StationState {
    /// Registered and answering.
    Live,
}

/// A line: a stream on the roster.
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct Line {
    /// The channel name.
    pub id: String,
    /// Always a stream: calls, archives and exports have no source.
    pub kind: LineKind,
    /// How long its messages are kept.
    pub life: LineLife,
    /// Whether it admits.
    pub state: LineState,
    /// The stations it passes, in order.
    pub stops: Vec<String>,
}

/// What a line is.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum LineKind {
    /// A channel.
    Stream,
}

/// How long a line keeps its messages, from the channel's mode.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum LineLife {
    /// Ephemeral: messages pass and are not stored.
    Passing,
    /// Durable: messages are stored.
    Kept,
}

/// Whether a line admits.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum LineState {
    /// Admitting publishes and subscribes.
    Live,
    /// Quiesced: refusing new publishes and subscribes.
    Quiesced,
}

/// A link to a cluster peer, present while the peer is.
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct Link {
    /// The link id, `<this device>~<peer>`.
    pub id: String,
    /// The peer's name as shown.
    pub name: String,
    /// How the two devices are joined.
    pub kind: LinkKind,
    /// The link's state.
    pub state: LinkState,
    /// Round trip in milliseconds. No source: always `null`, never `0`.
    pub rtt_ms: Option<u64>,
    /// The two device ids it joins, in sorted order, so the same link has
    /// the same bytes whichever end publishes it.
    pub ends: [String; 2],
}

impl Link {
    /// The link from `local` to `peer`, up, with no round trip measured.
    #[must_use]
    pub fn to_peer(local: &str, peer: &str) -> Self {
        let ends = if peer < local {
            [peer.to_owned(), local.to_owned()]
        } else {
            [local.to_owned(), peer.to_owned()]
        };
        Self {
            id: format!("{local}~{peer}"),
            name: peer.to_owned(),
            kind: LinkKind::SameNetwork,
            state: LinkState::Up,
            rtt_ms: None,
            ends,
        }
    }
}

/// How two devices are joined.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
pub enum LinkKind {
    /// Members of one cluster.
    #[serde(rename = "same network")]
    SameNetwork,
}

/// A link's state. A departed peer's link is absent, not down: membership
/// keeps no record of why it left.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum LinkState {
    /// The peer is a member.
    Up,
}

/// A live connection to this server.
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct Connection {
    /// The connection id, as a tap message's `from` names it.
    pub id: String,
    /// The transport it arrived on.
    pub mount: Mount,
    /// The participant its pass names, when it presented one.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub participant: Option<String>,
}

/// The transport a connection arrived on.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum Mount {
    /// The TCP wire listener.
    Tcp,
    /// The WebSocket listener.
    Websocket,
    /// The in-process transport.
    Loopback,
}

impl From<crate::server::mount::MountKind> for Mount {
    fn from(mount: crate::server::mount::MountKind) -> Self {
        match mount {
            crate::server::mount::MountKind::Tcp => Self::Tcp,
            crate::server::mount::MountKind::WebSocket => Self::Websocket,
            crate::server::mount::MountKind::Loopback => Self::Loopback,
        }
    }
}

/// One `console.topology` record: the whole document as it stands.
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct TopologyDocument {
    v: u64,
    #[serde(rename = "type")]
    record_type: &'static str,
    /// One more than the previous document's, from 1 at start.
    pub rev: u64,
    /// The id of the device this server is, one of `devices`' ids (lane
    /// brief ruling 1, Hermes 7 Oct 03:51): named once here, never as a flag
    /// on each device.
    pub this_device: String,
    /// This device, then each peer.
    pub devices: Vec<Device>,
    /// The registered services.
    pub stations: Vec<Station>,
    /// Every roster channel outside `console.`.
    pub lines: Vec<Line>,
    /// One per peer.
    pub links: Vec<Link>,
    /// One per live connection.
    pub connections: Vec<Connection>,
}

impl TopologyDocument {
    /// A document at `rev`, from the server that is device `this_device`.
    #[must_use]
    pub const fn new(
        rev: u64,
        this_device: String,
        devices: Vec<Device>,
        stations: Vec<Station>,
        lines: Vec<Line>,
        peer_links: Vec<Link>,
        connections: Vec<Connection>,
    ) -> Self {
        Self {
            v: RECORD_VERSION,
            record_type: "topology",
            rev,
            this_device,
            devices,
            stations,
            lines,
            links: peer_links,
            connections,
        }
    }
}

/// What became of a tapped message, in the console's words.
///
/// Only the words the server has an honest cause for: the console's call words (reply,
/// died, deadline), deferred, dropped and redelivered have none here.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum Outcome {
    /// Taken by at least one subscriber.
    Delivered,
    /// Admitted, and no subscriber matched.
    Nobody,
    /// Admitted; subscribers matched and none took it.
    Unaccepted,
    /// Refused at admission, by the schema, or by the library.
    Refused,
    /// The store could not say whether the append landed.
    Unresolved,
}

impl From<TapOutcome> for Outcome {
    fn from(outcome: TapOutcome) -> Self {
        match outcome {
            TapOutcome::Delivered => Self::Delivered,
            TapOutcome::NoSubscriber => Self::Nobody,
            TapOutcome::NoneAccepted => Self::Unaccepted,
            TapOutcome::Unresolved => Self::Unresolved,
            TapOutcome::Refused => Self::Refused,
        }
    }
}

/// One point a message crossed.
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct Hop {
    /// The device id.
    pub node: String,
    /// Milliseconds after the message's `t`.
    pub at_ms: u64,
}

/// The facts a tap carries about one message. Payloads never ride here.
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct TapMessage {
    /// `<device>:<message id>` when admitted, `<device>:x<observation>`
    /// otherwise.
    pub id: String,
    /// The subject (channel name).
    pub subject: String,
    /// Payload size in bytes.
    pub size: u64,
    /// What became of it.
    pub outcome: Outcome,
    /// Wall time it was first seen, in milliseconds since the epoch.
    pub t: u64,
    /// The publishing connection's id, or `liminal.<device>` for the server.
    pub from: String,
    /// The station that published it: the registered worker identity of the
    /// publishing connection. Absent when that connection is not a registered
    /// worker, and for the server's own records.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub from_station: Option<String>,
    /// This device's id.
    pub to: String,
    /// The stations that received it: each registered worker whose
    /// subscription's inbox on this device queued the message, once, in
    /// delivery order. A subscriber whose inbox refused it is not here; nor is
    /// one that is no registered worker (it is counted in the outcome, never
    /// named), nor a receiver on another device. `[]` when the fan-out was
    /// recorded and reached no registered worker here; absent when it was not
    /// recorded (a tap went live while the publish ran, or the tap could not
    /// lend it a buffer), so a reader shows its receivers as not known rather
    /// than as none.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub to_stations: Option<Vec<String>>,
    /// Where it went, in order.
    pub hops: Vec<Hop>,
    /// The id of the message it answers (its causal parent), when it has one.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub reply_to: Option<String>,
    /// Held by a rule. Rules do not exist yet, so always false.
    pub held: bool,
    /// Where a rule diverted it. Rules do not exist yet, so always absent.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub diverted: Option<String>,
}

/// One `console.tap.<target>` record: one batch, every message in it admitted
/// at the one divisor `sampled`.
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct TapBatch {
    v: u64,
    #[serde(rename = "type")]
    record_type: &'static str,
    /// The node or stream the tap is on.
    pub target: String,
    /// One more than this tap's previous batch, from 1.
    pub seq: u64,
    /// N in "sampled 1 in N" for every message in this batch.
    pub sampled: NonZeroU32,
    /// Messages evicted from the tap's ring since its previous batch.
    pub dropped: u64,
    /// The messages, oldest first.
    pub messages: Vec<TapMessage>,
}

impl TapBatch {
    /// A batch for `target`.
    #[must_use]
    pub const fn new(
        target: String,
        seq: u64,
        sampled: NonZeroU32,
        dropped: u64,
        messages: Vec<TapMessage>,
    ) -> Self {
        Self {
            v: RECORD_VERSION,
            record_type: "tap.batch",
            target,
            seq,
            sampled,
            dropped,
            messages,
        }
    }
}

/// The canonical bytes of a record: through a JSON value, so every key is in
/// sorted order at every depth, as the schema normaliser writes them.
///
/// # Errors
/// Returns [`ConsoleError::Encode`] naming the record when serialisation fails.
pub fn canonical_bytes<Record: Serialize>(
    record: &Record,
    kind: &'static str,
) -> Result<Vec<u8>, ConsoleError> {
    serde_json::to_value(record)
        .and_then(|value| serde_json::to_vec(&value))
        .map_err(|source| ConsoleError::Encode {
            record: kind,
            source,
        })
}

/// The `console.tap.<target>` record for one tap batch on `device`.
///
/// Every message is named, timed and attributed in the console's terms. A
/// payload, which only a test grant can carry today, does not ride the
/// record: the console's reader names no field for one.
///
/// # Errors
/// Returns [`ConsoleError::Clock`] when a message's time passes u64
/// milliseconds.
pub fn tap_batch_record(
    device: &str,
    target: &str,
    batch: &tap::TapBatch,
    wall: &WallBase,
) -> Result<TapBatch, ConsoleError> {
    let messages = batch
        .messages
        .iter()
        .map(|message| tap_message(device, message, wall))
        .collect::<Result<Vec<_>, _>>()?;
    Ok(TapBatch::new(
        target.to_owned(),
        batch.seq,
        batch.sampled,
        batch.dropped,
        messages,
    ))
}

fn tap_message(
    device: &str,
    message: &tap::TapMessage,
    wall: &WallBase,
) -> Result<TapMessage, ConsoleError> {
    Ok(TapMessage {
        id: match message.ident {
            TapIdent::Message(id) => format!("{device}:{id}"),
            TapIdent::Observation(observation) => format!("{device}:x{observation}"),
        },
        subject: message.subject.clone(),
        size: message.size,
        outcome: message.outcome.into(),
        t: wall.unix_ms_at(message.seen_at)?,
        from: match message.source {
            TapSource::Connection(connection) => connection.to_string(),
            TapSource::Server => message
                .station
                .as_deref()
                .map_or_else(|| format!("liminal.{device}"), str::to_owned),
        },
        from_station: message.station.as_deref().map(str::to_owned),
        to: device.to_owned(),
        to_stations: message.to_stations.as_deref().map(|stations| {
            stations
                .iter()
                .map(|reached| reached.station.to_string())
                .collect()
        }),
        hops: message
            .hops
            .iter()
            .map(|hop| Hop {
                node: hop.node.to_string(),
                at_ms: hop.at_ms,
            })
            .collect(),
        reply_to: message.reply_to.clone(),
        held: message.held,
        diverted: None,
    })
}