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
//! What the transport is doing right now.
//!
//! Pure: a plain state value with no machinery behind it. The watch cell
//! that publishes transitions, and the aggregation that turns several
//! peers' connection types into one of these, live with the handles.
/// What the transport is doing right now.
///
/// The `Relayed` case is worth surfacing to users: it explains latency
/// and is expected under carrier-grade or strict corporate NAT. `Closed`
/// is the terminal state, and `status_changed` guarantees to deliver it —
/// a watcher never blocks forever on a pipe that is already gone.
///
/// One listener can serve several peers at once — a phone and a laptop
/// holding the same ticket — so on the serve side this is an aggregate,
/// and it reports **the worst active path**: `Relayed` if any connected
/// peer is relayed, `Direct` only when all of them are direct. Reporting
/// the best path instead would hide exactly what `Relayed` exists to
/// explain, leaving the owner of the slow device with no way to find out
/// why. The cost is accepted and stated plainly: with a mixed set this
/// value describes no single peer, and a per-peer accessor is the
/// additive change that would fix that if the need proves real.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum PipeStatus {
/// No peer is connected — waiting for the first, or between
/// connections.
Idle,
/// Every connected peer has a direct hole-punched connection.
Direct,
/// At least one connected peer is falling back through an (encrypted,
/// unreadable) relay.
Relayed,
/// The pipe is gone — shut down, dropped, or dead after an
/// unrecoverable transport failure. Terminal: no transition follows.
/// Carried as a bare state rather than a reason so this type stays
/// `Copy`; a diagnostic accessor on the handle can be added
/// compatibly if the need proves real.
Closed,
}
impl PipeStatus {
/// A stable lowercase identifier: `"idle"`, `"direct"`, `"relayed"`,
/// `"closed"`.
///
/// For status output, log fields and anything else that wants to name
/// the state without matching on it. Deliberately an identifier and
/// not a sentence — freezing `"relayed"` costs nothing, while freezing
/// "falling back through a relay" would make every wording improvement
/// a breaking change for whoever grepped for it.
///
/// A variant added later returns its own new identifier, so a caller
/// rendering this string keeps working; one *matching* on the string
/// has the same obligation it would have had matching on the enum.
pub const fn as_str(self) -> &'static str {
match self {
Self::Idle => "idle",
Self::Direct => "direct",
Self::Relayed => "relayed",
Self::Closed => "closed",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
/// The identifiers are frozen surface once anything greps for them, so
/// pin the spellings and the distinctness in one place.
#[test]
fn every_status_renders_a_distinct_stable_identifier() {
let all = [
PipeStatus::Idle,
PipeStatus::Direct,
PipeStatus::Relayed,
PipeStatus::Closed,
];
let rendered: Vec<&str> = all.iter().map(|s| s.as_str()).collect();
assert_eq!(rendered, ["idle", "direct", "relayed", "closed"]);
let mut deduped = rendered.clone();
deduped.sort_unstable();
deduped.dedup();
assert_eq!(
deduped.len(),
rendered.len(),
"identifiers must be distinct"
);
}
}