1use crate::transport::TransportKind;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum NetworkEnvironment {
14 Wifi,
15 Cellular,
16 Corporate,
17 Unknown,
18}
19
20fn preference_for(env: NetworkEnvironment) -> &'static [TransportKind] {
22 match env {
23 NetworkEnvironment::Wifi => &[
24 TransportKind::Quic,
25 TransportKind::Udp,
26 TransportKind::Tcp,
27 TransportKind::WebSocket,
28 TransportKind::Grpc,
29 TransportKind::Http2,
30 ],
31 NetworkEnvironment::Cellular => &[
32 TransportKind::Tcp,
33 TransportKind::Quic,
34 TransportKind::WebSocket,
35 TransportKind::Http2,
36 TransportKind::Grpc,
37 TransportKind::Udp,
38 ],
39 NetworkEnvironment::Corporate => &[
40 TransportKind::Http2,
41 TransportKind::Grpc,
42 TransportKind::WebSocket,
43 TransportKind::Tcp,
44 TransportKind::Quic,
45 TransportKind::Udp,
46 ],
47 NetworkEnvironment::Unknown => &[
48 TransportKind::Quic,
49 TransportKind::Tcp,
50 TransportKind::WebSocket,
51 TransportKind::Udp,
52 TransportKind::Grpc,
53 TransportKind::Http2,
54 ],
55 }
56}
57
58pub struct TransportPolicy {
61 env: NetworkEnvironment,
62}
63
64impl TransportPolicy {
65 pub fn new(env: NetworkEnvironment) -> Self {
66 Self { env }
67 }
68
69 pub fn environment(&self) -> NetworkEnvironment {
71 self.env
72 }
73
74 pub fn set_environment(&mut self, env: NetworkEnvironment) {
76 self.env = env;
77 }
78
79 pub fn recommend(&self, available: &[TransportKind]) -> Vec<TransportKind> {
82 let pref = preference_for(self.env);
83 let mut out: Vec<TransportKind> = pref
84 .iter()
85 .copied()
86 .filter(|k| available.contains(k))
87 .collect();
88 for k in available {
89 if !out.contains(k) {
90 out.push(*k);
91 }
92 }
93 out
94 }
95
96 pub fn recommend_with_health(
102 &self,
103 available: &[TransportKind],
104 scores: &[(TransportKind, f64)],
105 ) -> Vec<TransportKind> {
106 let pref = preference_for(self.env);
107
108 let pref_rank =
109 |k: &TransportKind| -> usize { pref.iter().position(|p| p == k).unwrap_or(pref.len()) };
110 let score_of = |k: &TransportKind| -> f64 {
111 scores
112 .iter()
113 .find(|(sk, _)| sk == k)
114 .map(|(_, s)| *s)
115 .unwrap_or(0.5)
116 };
117
118 let mut ranked: Vec<TransportKind> = available.to_vec();
119 ranked.sort_by(|a, b| {
120 let sa = score_of(a);
121 let sb = score_of(b);
122 sb.partial_cmp(&sa)
124 .unwrap_or(std::cmp::Ordering::Equal)
125 .then_with(|| pref_rank(a).cmp(&pref_rank(b)))
126 });
127 ranked
128 }
129}
130
131impl Default for TransportPolicy {
132 fn default() -> Self {
133 Self::new(NetworkEnvironment::Unknown)
134 }
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140
141 #[test]
142 fn wifi_prefers_quic_udp() {
143 let p = TransportPolicy::new(NetworkEnvironment::Wifi);
144 let v = p.recommend(&[TransportKind::Tcp, TransportKind::Udp, TransportKind::Quic]);
145 assert_eq!(v[0], TransportKind::Quic);
146 assert_eq!(v[1], TransportKind::Udp);
147 assert_eq!(v[2], TransportKind::Tcp);
148 }
149
150 #[test]
151 fn cellular_prefers_tcp() {
152 let p = TransportPolicy::new(NetworkEnvironment::Cellular);
153 let v = p.recommend(&[TransportKind::Udp, TransportKind::Tcp, TransportKind::Quic]);
154 assert_eq!(v[0], TransportKind::Tcp);
155 }
156
157 #[test]
158 fn corporate_prefers_http_grpc() {
159 let p = TransportPolicy::new(NetworkEnvironment::Corporate);
160 let v = p.recommend(&[
161 TransportKind::Tcp,
162 TransportKind::Http2,
163 TransportKind::Grpc,
164 ]);
165 assert_eq!(v[0], TransportKind::Http2);
166 assert_eq!(v[1], TransportKind::Grpc);
167 assert_eq!(v[2], TransportKind::Tcp);
168 }
169
170 #[test]
171 fn unknown_matches_original_order() {
172 let p = TransportPolicy::new(NetworkEnvironment::Unknown);
173 let v = p.recommend(&[TransportKind::Udp, TransportKind::Tcp, TransportKind::Quic]);
174 assert_eq!(
175 v,
176 vec![TransportKind::Quic, TransportKind::Tcp, TransportKind::Udp]
177 );
178 }
179
180 #[test]
181 fn health_scores_override_preference() {
182 let p = TransportPolicy::new(NetworkEnvironment::Wifi);
183 let scores = vec![
185 (TransportKind::Quic, 0.3),
186 (TransportKind::Udp, 0.9),
187 (TransportKind::Tcp, 0.5),
188 ];
189 let v = p.recommend_with_health(
190 &[TransportKind::Quic, TransportKind::Udp, TransportKind::Tcp],
191 &scores,
192 );
193 assert_eq!(v[0], TransportKind::Udp);
194 assert_eq!(v[1], TransportKind::Tcp);
195 assert_eq!(v[2], TransportKind::Quic);
196 }
197
198 #[test]
199 fn equal_scores_break_tie_by_env_preference() {
200 let p = TransportPolicy::new(NetworkEnvironment::Wifi);
201 let scores = vec![(TransportKind::Quic, 0.8), (TransportKind::Udp, 0.8)];
202 let v = p.recommend_with_health(&[TransportKind::Udp, TransportKind::Quic], &scores);
203 assert_eq!(v[0], TransportKind::Quic);
205 assert_eq!(v[1], TransportKind::Udp);
206 }
207}