use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::Arc;
use serde::Serialize;
use tokio::sync::mpsc;
use rainier_support::{Error, Result};
use crate::message::Message;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct SocketId(u64);
impl SocketId {
pub fn next() -> Self {
static NEXT: AtomicU64 = AtomicU64::new(1);
Self(NEXT.fetch_add(1, Ordering::Relaxed))
}
pub fn get(self) -> u64 {
self.0
}
}
impl std::fmt::Display for SocketId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
pub fn instance_id() -> &'static str {
static INSTANCE: std::sync::OnceLock<String> = std::sync::OnceLock::new();
INSTANCE.get_or_init(|| {
let micros = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|since| since.as_micros())
.unwrap_or_default();
format!("{micros:x}")
})
}
pub fn socket_from_identity(identity: &str) -> Option<SocketId> {
let (instance, id) = identity.rsplit_once('.')?;
if instance != instance_id() {
return None;
}
id.parse().ok().map(SocketId)
}
#[derive(Debug, Clone)]
pub struct Socket {
id: SocketId,
path: Arc<str>,
params: Arc<Vec<(String, String)>>,
outbound: mpsc::UnboundedSender<Outbound>,
closed: Arc<AtomicBool>,
}
#[derive(Debug)]
pub enum Outbound {
Send(Message),
Close(Option<String>),
}
impl Socket {
pub fn new(
id: SocketId,
path: impl Into<Arc<str>>,
params: Vec<(String, String)>,
outbound: mpsc::UnboundedSender<Outbound>,
) -> Self {
Self {
id,
path: path.into(),
params: Arc::new(params),
outbound,
closed: Arc::new(AtomicBool::new(false)),
}
}
pub fn id(&self) -> SocketId {
self.id
}
pub fn identity(&self) -> String {
format!("{}.{}", instance_id(), self.id.get())
}
pub fn path(&self) -> &str {
&self.path
}
pub fn param(&self, name: &str) -> Option<&str> {
self.params.iter().find(|(key, _)| key == name).map(|(_, value)| value.as_str())
}
pub fn parse_param<T>(&self, name: &str) -> Result<T>
where
T: std::str::FromStr,
T::Err: std::fmt::Display,
{
let raw = self
.param(name)
.ok_or_else(|| Error::internal(format!("the route captured no `{name}`")))?;
raw.parse::<T>()
.map_err(|e| Error::bad_request(format!("`{name}` in the path is invalid: {e}")))
}
pub fn params(&self) -> &[(String, String)] {
&self.params
}
pub fn send(&self, message: impl Into<Message>) -> Result<()> {
self.push(Outbound::Send(message.into()))
}
pub fn send_json(&self, value: &impl Serialize) -> Result<()> {
self.send(Message::json(value)?)
}
pub fn close(&self) -> Result<()> {
if self.closed.swap(true, Ordering::SeqCst) {
return Ok(());
}
self.push(Outbound::Close(None))
}
pub fn close_with(&self, reason: impl Into<String>) -> Result<()> {
if self.closed.swap(true, Ordering::SeqCst) {
return Ok(());
}
self.push(Outbound::Close(Some(reason.into())))
}
pub fn is_closed(&self) -> bool {
self.closed.load(Ordering::SeqCst) || self.outbound.is_closed()
}
fn push(&self, outbound: Outbound) -> Result<()> {
self.outbound.send(outbound).map_err(|_| {
Error::internal(format!("socket {} has gone away", self.id))
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn socket() -> (Socket, mpsc::UnboundedReceiver<Outbound>) {
let (tx, rx) = mpsc::unbounded_channel();
(Socket::new(SocketId::next(), "/ws/rooms/7", vec![("room".into(), "7".into())], tx), rx)
}
#[test]
fn an_identity_is_the_process_and_the_socket() {
let (first, _rx) = socket();
let (second, _rx2) = socket();
assert_ne!(first.identity(), second.identity(), "two sockets, two identities");
assert!(first.identity().ends_with(&format!(".{}", first.id().get())));
assert_eq!(
first.identity().split('.').next(),
second.identity().split('.').next(),
"one process, one instance id"
);
}
#[test]
fn a_send_is_queued_not_written() {
let (socket, mut rx) = socket();
socket.send("hello").unwrap();
match rx.try_recv().unwrap() {
Outbound::Send(Message::Text(text)) => assert_eq!(text, "hello"),
other => panic!("{other:?}"),
}
}
#[test]
fn route_parameters_come_through() {
let (socket, _rx) = socket();
assert_eq!(socket.param("room"), Some("7"));
assert_eq!(socket.parse_param::<u64>("room").unwrap(), 7);
assert_eq!(socket.param("nope"), None);
}
#[test]
fn a_parameter_that_will_not_parse_is_a_bad_request() {
let (tx, _rx) = mpsc::unbounded_channel();
let socket =
Socket::new(SocketId::next(), "/ws/rooms/x", vec![("room".into(), "x".into())], tx);
assert_eq!(socket.parse_param::<u64>("room").unwrap_err().status(), 400);
}
#[test]
fn closing_twice_is_not_an_error() {
let (socket, mut rx) = socket();
socket.close().unwrap();
socket.close().unwrap();
assert!(matches!(rx.try_recv(), Ok(Outbound::Close(None))));
assert!(rx.try_recv().is_err(), "only one close should be queued");
assert!(socket.is_closed());
}
#[test]
fn sending_to_a_socket_that_went_away_is_an_error_not_a_panic() {
let (socket, rx) = socket();
drop(rx);
let err = socket.send("hello").unwrap_err();
assert!(err.message().contains("gone away"), "{}", err.message());
assert!(socket.is_closed());
}
#[test]
fn ids_are_unique_and_ordered() {
let first = SocketId::next();
let second = SocketId::next();
assert!(second > first);
}
}