use crate::client::Network;
use crate::live::frame::{BadFrame, Frame, decode};
use crate::live::request::{Post, post, subscribe, unsubscribe};
use crate::live::subscription::{BadSubscription, Subscription};
pub const MAX_CONNECTIONS: usize = 10;
pub const MAX_NEW_CONNECTIONS_PER_MINUTE: usize = 30;
pub const MAX_SUBSCRIPTIONS: usize = 1_000;
pub const MAX_USERS: usize = 10;
pub const MAX_MESSAGES_PER_MINUTE: usize = 2_000;
pub const MAX_INFLIGHT_POSTS: usize = 100;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Limits {
pub subscriptions: usize,
pub users: usize,
pub posts: usize,
}
impl Limits {
pub const VENUE: Limits = Limits { subscriptions: MAX_SUBSCRIPTIONS, users: MAX_USERS, posts: MAX_INFLIGHT_POSTS };
}
impl Default for Limits {
fn default() -> Self {
Limits::VENUE
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Timing {
pub ping_after_ms: u64,
pub silent_ms: u64,
pub steady_ms: u64,
pub first_retry_ms: u64,
pub last_retry_ms: u64,
}
impl Timing {
pub const DEFAULT: Timing = Timing {
ping_after_ms: 30_000,
silent_ms: 60_000,
steady_ms: 30_000,
first_retry_ms: 250,
last_retry_ms: 20_000,
};
}
impl Default for Timing {
fn default() -> Self {
Timing::DEFAULT
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum Refused {
#[error("{limit} subscriptions is the most this session holds; the venue allows an IP 1000")]
Subscriptions { limit: usize },
#[error("{limit} users is the most this session follows; the venue allows an IP 10")]
Users { limit: usize },
#[error("{limit} posts are already waiting for an answer; the venue allows an IP 100")]
Posts { limit: usize },
#[error("the socket is not open, and a post is never held to send later")]
Closed,
#[error(transparent)]
Bad(#[from] BadSubscription),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Tick {
Wait,
Ping,
Reconnect,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Retry {
pub after_ms: u64,
pub unanswered: Vec<u64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Link {
Closed,
Open {
since: u64,
heard: u64,
talked: bool,
pinged: u64,
},
}
#[derive(Debug, Clone)]
pub struct Session {
network: Network,
limits: Limits,
timing: Timing,
wanted: Vec<Subscription>,
link: Link,
failures: u32,
next_id: u64,
waiting: Vec<u64>,
}
impl Session {
#[must_use]
pub fn new(network: Network) -> Self {
Session {
network,
limits: Limits::VENUE,
timing: Timing::DEFAULT,
wanted: Vec::new(),
link: Link::Closed,
failures: 0,
next_id: 1,
waiting: Vec::new(),
}
}
#[must_use]
pub const fn with_limits(mut self, limits: Limits) -> Self {
self.limits = limits;
self
}
#[must_use]
pub const fn with_timing(mut self, timing: Timing) -> Self {
self.timing = timing;
self
}
#[must_use]
pub const fn network(&self) -> Network {
self.network
}
#[must_use]
pub const fn url(&self) -> &'static str {
self.network.ws_url()
}
#[must_use]
pub const fn is_open(&self) -> bool {
matches!(self.link, Link::Open { .. })
}
#[must_use]
pub fn wanted(&self) -> &[Subscription] {
&self.wanted
}
#[must_use]
pub fn users(&self) -> usize {
let mut users: Vec<_> = self.wanted.iter().filter_map(Subscription::user).collect();
users.sort_unstable();
users.dedup();
users.len()
}
#[must_use]
pub fn waiting(&self) -> &[u64] {
&self.waiting
}
pub fn subscribe(&mut self, subscription: Subscription) -> Result<Option<String>, Refused> {
if self.wanted.contains(&subscription) {
return Ok(None);
}
subscription.check()?;
if self.wanted.len() >= self.limits.subscriptions {
return Err(Refused::Subscriptions { limit: self.limits.subscriptions });
}
if let Some(user) = subscription.user()
&& !self.wanted.iter().any(|wanted| wanted.user() == Some(user))
&& self.users() >= self.limits.users
{
return Err(Refused::Users { limit: self.limits.users });
}
let frame = self.is_open().then(|| subscribe(&subscription));
self.wanted.push(subscription);
Ok(frame)
}
pub fn unsubscribe(&mut self, subscription: &Subscription) -> Option<String> {
let at = self.wanted.iter().position(|wanted| wanted == subscription)?;
let gone = self.wanted.remove(at);
self.is_open().then(|| unsubscribe(&gone))
}
pub fn post(&mut self, request: &Post) -> Result<(u64, String), Refused> {
if !self.is_open() {
return Err(Refused::Closed);
}
if self.waiting.len() >= self.limits.posts {
return Err(Refused::Posts { limit: self.limits.posts });
}
let id = self.next_id;
self.next_id = self.next_id.saturating_add(1);
self.waiting.push(id);
Ok((id, post(id, request)))
}
pub fn opened(&mut self, now: u64) -> Vec<String> {
self.link = Link::Open { since: now, heard: now, talked: false, pinged: now };
self.wanted.iter().map(subscribe).collect()
}
pub fn received(&mut self, now: u64, text: &str) -> Result<Frame, BadFrame> {
if let Link::Open { heard, talked, .. } = &mut self.link {
*heard = (*heard).max(now);
*talked = true;
}
let frame = decode(text);
let answered = match &frame {
Ok(Frame::Post(reply)) => Some(reply.id),
Ok(Frame::Error(refusal)) => refusal.post_id(),
_ => None,
};
if let Some(id) = answered {
self.waiting.retain(|waiting| *waiting != id);
}
frame
}
pub fn tick(&mut self, now: u64) -> Tick {
let Link::Open { heard, pinged, .. } = &mut self.link else { return Tick::Wait };
if now.saturating_sub(*heard) >= self.timing.silent_ms {
return Tick::Reconnect;
}
if now.saturating_sub((*heard).max(*pinged)) >= self.timing.ping_after_ms {
*pinged = now;
return Tick::Ping;
}
Tick::Wait
}
#[must_use]
pub fn due(&self) -> Option<u64> {
let Link::Open { heard, pinged, .. } = self.link else { return None };
let ping = heard.max(pinged).saturating_add(self.timing.ping_after_ms);
let silent = heard.saturating_add(self.timing.silent_ms);
Some(ping.min(silent))
}
pub fn closed(&mut self, now: u64, random: u64) -> Retry {
if let Link::Open { since, talked: true, .. } = self.link
&& now.saturating_sub(since) >= self.timing.steady_ms
{
self.failures = 0;
}
self.link = Link::Closed;
let doubled = 1u64.checked_shl(self.failures).unwrap_or(u64::MAX);
let step = self.timing.first_retry_ms.saturating_mul(doubled).min(self.timing.last_retry_ms);
let jitter = random % (step / 4).saturating_add(1);
self.failures = self.failures.saturating_add(1);
Retry { after_ms: step.saturating_add(jitter), unanswered: std::mem::take(&mut self.waiting) }
}
}
#[cfg(test)]
#[path = "tests/session.rs"]
mod tests;