1use crate::channel::{Channel, ChannelContext, ChannelName};
7use crate::protocol::{CableFrame, ServerFrame};
8use crate::pubsub::PubSub;
9use axum::extract::ws::{Message, WebSocket, WebSocketUpgrade};
10use axum::extract::State;
11use axum::response::Response;
12use axum::routing::get;
13use axum::Router;
14use futures_util::{SinkExt, StreamExt};
15use std::collections::HashMap;
16use std::sync::Arc;
17use std::time::Duration;
18
19const DEFAULT_HEARTBEAT: Duration = Duration::from_secs(3);
21
22pub struct ChannelRegistry {
25 channels: HashMap<String, Arc<dyn Channel>>,
26 pubsub: Arc<dyn PubSub>,
27 heartbeat: Duration,
28}
29
30impl ChannelRegistry {
31 pub fn new(pubsub: Arc<dyn PubSub>) -> Self {
32 Self {
33 channels: HashMap::new(),
34 pubsub,
35 heartbeat: DEFAULT_HEARTBEAT,
36 }
37 }
38
39 pub fn with_heartbeat(mut self, interval: Duration) -> Self {
41 self.heartbeat = interval;
42 self
43 }
44
45 pub fn register(&mut self, name: impl Into<String>, channel: Arc<dyn Channel>) -> &mut Self {
47 self.channels.insert(name.into(), channel);
48 self
49 }
50
51 pub fn register_channel<C>(&mut self, channel: C) -> &mut Self
53 where
54 C: Channel + ChannelName + 'static,
55 {
56 self.register(C::channel_name(), Arc::new(channel))
57 }
58
59 pub fn get(&self, name: &str) -> Option<Arc<dyn Channel>> {
61 self.channels.get(name).cloned()
62 }
63
64 pub fn pubsub(&self) -> Arc<dyn PubSub> {
66 self.pubsub.clone()
67 }
68}
69
70fn channel_name_of(identifier: &str) -> Option<String> {
73 serde_json::from_str::<serde_json::Value>(identifier)
74 .ok()?
75 .get("channel")?
76 .as_str()
77 .map(str::to_string)
78}
79
80fn send_frame(tx: &tokio::sync::mpsc::UnboundedSender<String>, frame: &ServerFrame) {
82 if let Ok(json) = frame.to_json() {
83 let _ = tx.send(json);
84 }
85}
86
87async fn handle_frame(
90 frame: CableFrame,
91 registry: &ChannelRegistry,
92 tx: &tokio::sync::mpsc::UnboundedSender<String>,
93 subs: &mut HashMap<String, ChannelContext>,
94) {
95 match frame {
96 CableFrame::Subscribe { identifier } => {
97 let channel = channel_name_of(&identifier).and_then(|name| registry.get(&name));
98 let Some(channel) = channel else {
99 send_frame(tx, &ServerFrame::RejectSubscription { identifier });
100 return;
101 };
102 let ctx = ChannelContext::new(identifier.clone(), tx.clone(), registry.pubsub());
103 match channel.subscribed(&ctx).await {
104 Ok(()) => {
105 send_frame(
106 tx,
107 &ServerFrame::ConfirmSubscription {
108 identifier: identifier.clone(),
109 },
110 );
111 subs.insert(identifier, ctx);
112 }
113 Err(_) => send_frame(tx, &ServerFrame::RejectSubscription { identifier }),
114 }
115 }
116 CableFrame::Message { identifier, data } => {
117 if let Some(ctx) = subs.get(&identifier) {
118 if let Some(channel) = channel_name_of(&identifier).and_then(|n| registry.get(&n)) {
119 let _ = channel.received(ctx, data).await;
120 }
121 }
122 }
123 CableFrame::Unsubscribe { identifier } => {
124 if let Some(ctx) = subs.remove(&identifier) {
125 if let Some(channel) = channel_name_of(&identifier).and_then(|n| registry.get(&n)) {
126 let _ = channel.unsubscribed(&ctx).await;
127 }
128 ctx.stop_all_streams().await;
129 }
130 }
131 }
132}
133
134pub async fn handle_socket(socket: WebSocket, registry: Arc<ChannelRegistry>) {
137 let (mut sink, mut stream) = socket.split();
138 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<String>();
139
140 let writer = tokio::spawn(async move {
142 while let Some(msg) = rx.recv().await {
143 if sink.send(Message::text(msg)).await.is_err() {
144 break;
145 }
146 }
147 });
148
149 let _ = tx.send(ServerFrame::Welcome.to_json().unwrap_or_default());
150
151 let heartbeat_tx = tx.clone();
153 let heartbeat_interval = registry.heartbeat;
154 let heartbeat = tokio::spawn(async move {
155 let mut ticker = tokio::time::interval(heartbeat_interval);
156 ticker.tick().await; loop {
158 ticker.tick().await;
159 let ping = crate::heartbeat::ping_now().to_json().unwrap_or_default();
160 if heartbeat_tx.send(ping).is_err() {
161 break;
162 }
163 }
164 });
165
166 let mut subs: HashMap<String, ChannelContext> = HashMap::new();
167 while let Some(Ok(message)) = stream.next().await {
168 match message {
169 Message::Text(text) => {
170 if let Ok(frame) = CableFrame::parse(text.as_str()) {
171 handle_frame(frame, ®istry, &tx, &mut subs).await;
172 }
173 }
174 Message::Close(_) => break,
175 _ => {}
176 }
177 }
178
179 heartbeat.abort();
180 writer.abort();
181 for (_, ctx) in subs {
182 ctx.stop_all_streams().await;
183 }
184}
185
186pub async fn ws_handler(
188 ws: WebSocketUpgrade,
189 State(registry): State<Arc<ChannelRegistry>>,
190) -> Response {
191 ws.on_upgrade(move |socket| handle_socket(socket, registry))
192}
193
194pub fn route(registry: Arc<ChannelRegistry>) -> Router {
196 Router::new()
197 .route("/cable", get(ws_handler))
198 .with_state(registry)
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204 use crate::pubsub::MemoryPubSub;
205 use std::sync::atomic::{AtomicBool, Ordering};
206 use tokio::sync::mpsc;
207
208 struct EchoChannel {
209 subscribed: Arc<AtomicBool>,
210 received: Arc<AtomicBool>,
211 }
212
213 #[async_trait::async_trait]
214 impl Channel for EchoChannel {
215 async fn subscribed(&self, _ctx: &ChannelContext) -> doido_core::Result<()> {
216 self.subscribed.store(true, Ordering::SeqCst);
217 Ok(())
218 }
219 async fn unsubscribed(&self, _ctx: &ChannelContext) -> doido_core::Result<()> {
220 Ok(())
221 }
222 async fn received(
223 &self,
224 ctx: &ChannelContext,
225 data: serde_json::Value,
226 ) -> doido_core::Result<()> {
227 self.received.store(true, Ordering::SeqCst);
228 ctx.transmit(data);
229 Ok(())
230 }
231 }
232
233 impl ChannelName for EchoChannel {
234 fn channel_name() -> &'static str {
235 "EchoChannel"
236 }
237 }
238
239 fn registry_with(subscribed: Arc<AtomicBool>, received: Arc<AtomicBool>) -> ChannelRegistry {
240 let mut registry = ChannelRegistry::new(Arc::new(MemoryPubSub::new()));
241 registry.register_channel(EchoChannel {
242 subscribed,
243 received,
244 });
245 registry
246 }
247
248 #[test]
249 fn channel_name_of_reads_the_channel_field() {
250 assert_eq!(
251 channel_name_of(r#"{"channel":"EchoChannel","room":"1"}"#).as_deref(),
252 Some("EchoChannel")
253 );
254 assert_eq!(channel_name_of("not json"), None);
255 }
256
257 #[tokio::test]
258 async fn subscribe_confirms_and_calls_subscribed() {
259 let subscribed = Arc::new(AtomicBool::new(false));
260 let registry = registry_with(subscribed.clone(), Arc::new(AtomicBool::new(false)));
261 let (tx, mut rx) = mpsc::unbounded_channel();
262 let mut subs = HashMap::new();
263
264 let identifier = r#"{"channel":"EchoChannel"}"#.to_string();
265 handle_frame(
266 CableFrame::Subscribe {
267 identifier: identifier.clone(),
268 },
269 ®istry,
270 &tx,
271 &mut subs,
272 )
273 .await;
274
275 assert!(subscribed.load(Ordering::SeqCst), "subscribed() ran");
276 assert!(subs.contains_key(&identifier), "subscription tracked");
277 let frame = ServerFrame::parse(&rx.recv().await.unwrap()).unwrap();
278 assert_eq!(frame, ServerFrame::ConfirmSubscription { identifier });
279 }
280
281 #[tokio::test]
282 async fn subscribe_to_unknown_channel_is_rejected() {
283 let registry = registry_with(
284 Arc::new(AtomicBool::new(false)),
285 Arc::new(AtomicBool::new(false)),
286 );
287 let (tx, mut rx) = mpsc::unbounded_channel();
288 let mut subs = HashMap::new();
289
290 let identifier = r#"{"channel":"NopeChannel"}"#.to_string();
291 handle_frame(
292 CableFrame::Subscribe {
293 identifier: identifier.clone(),
294 },
295 ®istry,
296 &tx,
297 &mut subs,
298 )
299 .await;
300
301 assert!(subs.is_empty());
302 let frame = ServerFrame::parse(&rx.recv().await.unwrap()).unwrap();
303 assert_eq!(frame, ServerFrame::RejectSubscription { identifier });
304 }
305
306 #[tokio::test]
307 async fn message_dispatches_to_received() {
308 let received = Arc::new(AtomicBool::new(false));
309 let registry = registry_with(Arc::new(AtomicBool::new(false)), received.clone());
310 let (tx, mut rx) = mpsc::unbounded_channel();
311 let mut subs = HashMap::new();
312 let identifier = r#"{"channel":"EchoChannel"}"#.to_string();
313
314 handle_frame(
315 CableFrame::Subscribe {
316 identifier: identifier.clone(),
317 },
318 ®istry,
319 &tx,
320 &mut subs,
321 )
322 .await;
323 let _confirm = rx.recv().await.unwrap();
324
325 handle_frame(
326 CableFrame::Message {
327 identifier: identifier.clone(),
328 data: serde_json::json!({ "text": "hi" }),
329 },
330 ®istry,
331 &tx,
332 &mut subs,
333 )
334 .await;
335
336 assert!(received.load(Ordering::SeqCst), "received() ran");
337 let echoed = crate::protocol::ServerMessage::parse(&rx.recv().await.unwrap()).unwrap();
339 assert_eq!(echoed.message["text"], "hi");
340 }
341}