use crate::transport::TransportKind;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NetworkEnvironment {
Wifi,
Cellular,
Corporate,
Unknown,
}
fn preference_for(env: NetworkEnvironment) -> &'static [TransportKind] {
match env {
NetworkEnvironment::Wifi => &[
TransportKind::Quic,
TransportKind::Udp,
TransportKind::Tcp,
TransportKind::WebSocket,
TransportKind::Grpc,
TransportKind::Http2,
],
NetworkEnvironment::Cellular => &[
TransportKind::Tcp,
TransportKind::Quic,
TransportKind::WebSocket,
TransportKind::Http2,
TransportKind::Grpc,
TransportKind::Udp,
],
NetworkEnvironment::Corporate => &[
TransportKind::Http2,
TransportKind::Grpc,
TransportKind::WebSocket,
TransportKind::Tcp,
TransportKind::Quic,
TransportKind::Udp,
],
NetworkEnvironment::Unknown => &[
TransportKind::Quic,
TransportKind::Tcp,
TransportKind::WebSocket,
TransportKind::Udp,
TransportKind::Grpc,
TransportKind::Http2,
],
}
}
pub struct TransportPolicy {
env: NetworkEnvironment,
}
impl TransportPolicy {
pub fn new(env: NetworkEnvironment) -> Self {
Self { env }
}
pub fn environment(&self) -> NetworkEnvironment {
self.env
}
pub fn set_environment(&mut self, env: NetworkEnvironment) {
self.env = env;
}
pub fn recommend(&self, available: &[TransportKind]) -> Vec<TransportKind> {
let pref = preference_for(self.env);
let mut out: Vec<TransportKind> = pref
.iter()
.copied()
.filter(|k| available.contains(k))
.collect();
for k in available {
if !out.contains(k) {
out.push(*k);
}
}
out
}
pub fn recommend_with_health(
&self,
available: &[TransportKind],
scores: &[(TransportKind, f64)],
) -> Vec<TransportKind> {
let pref = preference_for(self.env);
let pref_rank =
|k: &TransportKind| -> usize { pref.iter().position(|p| p == k).unwrap_or(pref.len()) };
let score_of = |k: &TransportKind| -> f64 {
scores
.iter()
.find(|(sk, _)| sk == k)
.map(|(_, s)| *s)
.unwrap_or(0.5)
};
let mut ranked: Vec<TransportKind> = available.to_vec();
ranked.sort_by(|a, b| {
let sa = score_of(a);
let sb = score_of(b);
sb.partial_cmp(&sa)
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| pref_rank(a).cmp(&pref_rank(b)))
});
ranked
}
}
impl Default for TransportPolicy {
fn default() -> Self {
Self::new(NetworkEnvironment::Unknown)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn wifi_prefers_quic_udp() {
let p = TransportPolicy::new(NetworkEnvironment::Wifi);
let v = p.recommend(&[TransportKind::Tcp, TransportKind::Udp, TransportKind::Quic]);
assert_eq!(v[0], TransportKind::Quic);
assert_eq!(v[1], TransportKind::Udp);
assert_eq!(v[2], TransportKind::Tcp);
}
#[test]
fn cellular_prefers_tcp() {
let p = TransportPolicy::new(NetworkEnvironment::Cellular);
let v = p.recommend(&[TransportKind::Udp, TransportKind::Tcp, TransportKind::Quic]);
assert_eq!(v[0], TransportKind::Tcp);
}
#[test]
fn corporate_prefers_http_grpc() {
let p = TransportPolicy::new(NetworkEnvironment::Corporate);
let v = p.recommend(&[
TransportKind::Tcp,
TransportKind::Http2,
TransportKind::Grpc,
]);
assert_eq!(v[0], TransportKind::Http2);
assert_eq!(v[1], TransportKind::Grpc);
assert_eq!(v[2], TransportKind::Tcp);
}
#[test]
fn unknown_matches_original_order() {
let p = TransportPolicy::new(NetworkEnvironment::Unknown);
let v = p.recommend(&[TransportKind::Udp, TransportKind::Tcp, TransportKind::Quic]);
assert_eq!(
v,
vec![TransportKind::Quic, TransportKind::Tcp, TransportKind::Udp]
);
}
#[test]
fn health_scores_override_preference() {
let p = TransportPolicy::new(NetworkEnvironment::Wifi);
let scores = vec![
(TransportKind::Quic, 0.3),
(TransportKind::Udp, 0.9),
(TransportKind::Tcp, 0.5),
];
let v = p.recommend_with_health(
&[TransportKind::Quic, TransportKind::Udp, TransportKind::Tcp],
&scores,
);
assert_eq!(v[0], TransportKind::Udp);
assert_eq!(v[1], TransportKind::Tcp);
assert_eq!(v[2], TransportKind::Quic);
}
#[test]
fn equal_scores_break_tie_by_env_preference() {
let p = TransportPolicy::new(NetworkEnvironment::Wifi);
let scores = vec![(TransportKind::Quic, 0.8), (TransportKind::Udp, 0.8)];
let v = p.recommend_with_health(&[TransportKind::Udp, TransportKind::Quic], &scores);
assert_eq!(v[0], TransportKind::Quic);
assert_eq!(v[1], TransportKind::Udp);
}
}