use std::sync::Arc;
use std::sync::RwLock;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use serde_json::{Value, json};
use tokio::sync::{broadcast, mpsc};
use tokio::time::{Duration, interval};
pub const BROADCAST_CAPACITY: usize = 256;
const TICK_INTERVAL_MS: u64 = 33;
#[derive(Debug, Clone)]
pub enum StateOp {
Set,
Merge,
Delete,
}
#[derive(Debug, Clone)]
pub enum EmitTarget {
All,
Session(String),
Others(String),
}
#[derive(Debug)]
pub enum RoomCmd {
PatchState {
op: StateOp,
path: String,
value: Option<Value>,
},
PatchStateSilent {
op: StateOp,
path: String,
value: Option<Value>,
},
Emit {
event: String,
payload: Value,
to: EmitTarget,
},
Shutdown,
}
#[derive(Debug, Clone)]
pub struct RoomBroadcast {
pub text: Arc<str>,
pub to: EmitTarget,
}
impl RoomBroadcast {
fn all(text: String) -> Self {
Self { text: text.into(), to: EmitTarget::All }
}
pub fn is_for(&self, session_id: &str) -> bool {
match &self.to {
EmitTarget::All => true,
EmitTarget::Session(id) => id == session_id,
EmitTarget::Others(id) => id != session_id,
}
}
}
pub struct SessionGuard {
count: Arc<AtomicUsize>,
}
impl Drop for SessionGuard {
fn drop(&mut self) {
self.count.fetch_sub(1, Ordering::Relaxed);
}
}
pub struct RoomHandle {
cmd_tx: mpsc::UnboundedSender<RoomCmd>,
broadcast_tx: broadcast::Sender<RoomBroadcast>,
session_count: Arc<AtomicUsize>,
state: Arc<RwLock<Value>>,
}
impl RoomHandle {
pub fn spawn() -> Arc<Self> {
let (cmd_tx, cmd_rx) = mpsc::unbounded_channel();
let (broadcast_tx, _) = broadcast::channel(BROADCAST_CAPACITY);
let session_count = Arc::new(AtomicUsize::new(0));
let state = Arc::new(RwLock::new(json!({})));
let dirty = Arc::new(AtomicBool::new(false));
let handle = Arc::new(Self {
cmd_tx,
broadcast_tx: broadcast_tx.clone(),
session_count: session_count.clone(),
state: state.clone(),
});
tokio::spawn(run_room(cmd_rx, broadcast_tx, state, dirty));
handle
}
pub fn subscribe(&self) -> broadcast::Receiver<RoomBroadcast> {
self.broadcast_tx.subscribe()
}
pub fn send_cmd(&self, cmd: RoomCmd) {
let _ = self.cmd_tx.send(cmd);
}
pub fn get_state(&self) -> Value {
self.state.read().unwrap_or_else(|e| e.into_inner()).clone()
}
pub(crate) fn join_session(&self) -> SessionGuard {
self.session_count.fetch_add(1, Ordering::Relaxed);
SessionGuard {
count: self.session_count.clone(),
}
}
pub fn session_count(&self) -> usize {
self.session_count.load(Ordering::Relaxed)
}
}
async fn run_room(
mut cmd_rx: mpsc::UnboundedReceiver<RoomCmd>,
broadcast_tx: broadcast::Sender<RoomBroadcast>,
state: Arc<RwLock<Value>>,
dirty: Arc<AtomicBool>,
) {
let mut tick = interval(Duration::from_millis(TICK_INTERVAL_MS));
loop {
tokio::select! {
cmd = cmd_rx.recv() => {
match cmd {
None | Some(RoomCmd::Shutdown) => break,
Some(RoomCmd::PatchState { op, path, value }) => {
{
let mut w = state.write().unwrap_or_else(|e| e.into_inner());
apply_state_op(&mut w, &op, &path, value);
}
let snapshot = state.read().unwrap_or_else(|e| e.into_inner()).clone();
let msg = json!({ "type": "state_patch", "state": snapshot });
let _ = broadcast_tx.send(RoomBroadcast::all(msg.to_string()));
}
Some(RoomCmd::PatchStateSilent { op, path, value }) => {
let mut w = state.write().unwrap_or_else(|e| e.into_inner());
apply_state_op(&mut w, &op, &path, value);
dirty.store(true, Ordering::Relaxed);
}
Some(RoomCmd::Emit { event, payload, to }) => {
let msg = json!({ "type": "event", "event": event, "payload": payload });
let _ = broadcast_tx.send(RoomBroadcast { text: msg.to_string().into(), to });
}
}
}
_ = tick.tick() => {
if dirty.swap(false, Ordering::Relaxed) {
let snapshot = state.read().unwrap_or_else(|e| e.into_inner()).clone();
let msg = json!({ "type": "state_patch", "state": snapshot });
let _ = broadcast_tx.send(RoomBroadcast::all(msg.to_string()));
}
}
}
}
}
fn apply_state_op(state: &mut Value, op: &StateOp, path: &str, value: Option<Value>) {
match op {
StateOp::Set => {
if path.is_empty() || path == "/" {
if let Some(v) = value {
*state = v;
}
} else {
json_ptr_set(state, path, value.unwrap_or(Value::Null));
}
}
StateOp::Merge => {
if let Some(Value::Object(patch)) = value {
if path.is_empty() || path == "/" {
if let Value::Object(current) = state {
for (k, v) in patch {
current.insert(k, v);
}
}
} else {
json_ptr_merge(state, path, patch);
}
}
}
StateOp::Delete => {
json_ptr_delete(state, path);
}
}
}
fn json_ptr_set(root: &mut Value, path: &str, value: Value) {
let parts: Vec<&str> = path
.trim_start_matches('/')
.split('/')
.filter(|s| !s.is_empty())
.collect();
if parts.is_empty() {
*root = value;
return;
}
let mut cur = root;
for (i, part) in parts.iter().enumerate() {
if i == parts.len() - 1 {
if let Value::Object(map) = cur {
map.insert((*part).to_string(), value);
}
return;
}
if let Value::Object(map) = cur {
cur = map.entry(part.to_string()).or_insert_with(|| json!({}));
} else {
return;
}
}
}
fn json_ptr_merge(root: &mut Value, path: &str, patch: serde_json::Map<String, Value>) {
let parts: Vec<&str> = path
.trim_start_matches('/')
.split('/')
.filter(|s| !s.is_empty())
.collect();
if parts.is_empty() {
if let Value::Object(current) = root {
for (k, v) in patch {
current.insert(k, v);
}
}
return;
}
let mut cur = root;
for part in &parts {
if let Value::Object(map) = cur {
cur = map.entry(part.to_string()).or_insert_with(|| json!({}));
} else {
return;
}
}
if let Value::Object(target) = cur {
for (k, v) in patch {
target.insert(k, v);
}
}
}
fn json_ptr_delete(root: &mut Value, path: &str) {
let parts: Vec<&str> = path
.trim_start_matches('/')
.split('/')
.filter(|s| !s.is_empty())
.collect();
if parts.is_empty() {
return;
}
let mut cur = root;
for part in &parts[..parts.len() - 1] {
if let Value::Object(map) = cur {
if let Some(next) = map.get_mut(*part) {
cur = next;
} else {
return;
}
} else {
return;
}
}
if let Value::Object(map) = cur {
map.remove(*parts.last().unwrap());
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_session_targeted_message_is_only_for_that_session() {
let b = RoomBroadcast { text: "{}".into(), to: EmitTarget::Session("s1".into()) };
assert!(b.is_for("s1"));
assert!(!b.is_for("s2"));
}
#[test]
fn an_others_message_skips_only_the_sender() {
let b = RoomBroadcast { text: "{}".into(), to: EmitTarget::Others("s1".into()) };
assert!(!b.is_for("s1"));
assert!(b.is_for("s2"));
}
#[tokio::test]
async fn an_emit_reaches_subscribers_with_its_target() {
let room = RoomHandle::spawn();
let mut rx = room.subscribe();
room.send_cmd(RoomCmd::Emit { event: "hi".into(), payload: json!({ "a": 1 }), to: EmitTarget::Session("s1".into()) });
let got = rx.recv().await.expect("broadcast");
assert!(got.is_for("s1") && !got.is_for("s2"));
let v: Value = serde_json::from_str(&got.text).unwrap();
assert_eq!(v["event"], "hi");
assert!(v.get("target_session").is_none(), "targets no longer travel to clients");
}
}