Skip to main content

dartboard_server/
lib.rs

1//! WebSocket transport wrapper for [`dartboard_local`].
2//!
3//! Consumers that only need the in-process server and local client can depend
4//! on `dartboard-local` directly. This crate keeps the websocket listener and
5//! headless `dartboardd` binary while preserving the existing `ServerHandle`
6//! convenience surface for ws-hosting callers.
7
8use dartboard_core::{Canvas, CanvasOp, ClientOpId, UserId};
9
10pub use dartboard_local::{
11    CanvasStore, ColorSelectionMode, ConnectOutcome, Hello, InMemStore, LocalClient, MAX_PLAYERS,
12};
13
14mod ws;
15
16#[derive(Clone)]
17pub struct ServerHandle {
18    local: dartboard_local::ServerHandle,
19}
20
21impl ServerHandle {
22    pub fn spawn_local<S: CanvasStore + 'static>(store: S) -> Self {
23        Self::spawn_local_with_color_selection_mode(store, ColorSelectionMode::default())
24    }
25
26    pub fn spawn_local_with_color_selection_mode<S: CanvasStore + 'static>(
27        store: S,
28        color_selection_mode: ColorSelectionMode,
29    ) -> Self {
30        Self {
31            local: dartboard_local::ServerHandle::spawn_local_with_color_selection_mode(
32                store,
33                color_selection_mode,
34            ),
35        }
36    }
37
38    pub fn try_connect_local(&self, hello: Hello) -> ConnectOutcome {
39        self.local.try_connect_local(hello)
40    }
41
42    pub fn connect_local(&self, hello: Hello) -> LocalClient {
43        self.local.connect_local(hello)
44    }
45
46    pub fn peer_count(&self) -> usize {
47        self.local.peer_count()
48    }
49
50    pub fn canvas_snapshot(&self) -> Canvas {
51        self.local.canvas_snapshot()
52    }
53
54    /// Bind a TCP listener on `addr`, spawn a dedicated tokio runtime thread,
55    /// and accept WebSocket connections. Each accepted connection talks the
56    /// same [`ClientMsg`]/[`ServerMsg`] protocol as [`LocalClient`], framed as
57    /// JSON over ws frames.
58    ///
59    /// Blocks only for the initial bind; returns once the listener is live.
60    /// The accept loop runs until the process exits.
61    pub fn bind_ws(&self, addr: std::net::SocketAddr) -> std::io::Result<()> {
62        let (ready_tx, ready_rx) = std::sync::mpsc::channel();
63        let server = self.clone();
64        std::thread::spawn(move || {
65            let runtime = match tokio::runtime::Builder::new_multi_thread()
66                .enable_all()
67                .build()
68            {
69                Ok(rt) => rt,
70                Err(e) => {
71                    let _ = ready_tx.send(Err(e));
72                    return;
73                }
74            };
75            runtime.block_on(async move {
76                match tokio::net::TcpListener::bind(addr).await {
77                    Ok(listener) => {
78                        let _ = ready_tx.send(Ok(()));
79                        loop {
80                            let Ok((stream, _)) = listener.accept().await else {
81                                break;
82                            };
83                            let server = server.clone();
84                            tokio::spawn(async move {
85                                if let Err(e) = ws::accept_and_run(server, stream).await {
86                                    eprintln!("ws connection ended: {}", e);
87                                }
88                            });
89                        }
90                    }
91                    Err(e) => {
92                        let _ = ready_tx.send(Err(e));
93                    }
94                }
95            });
96        });
97
98        ready_rx
99            .recv()
100            .unwrap_or_else(|_| Err(std::io::Error::other("ws thread disappeared")))
101    }
102
103    pub(crate) fn register_transport(
104        &self,
105        hello: Hello,
106        sender: Box<dyn dartboard_local::ServerSink>,
107    ) -> Result<UserId, String> {
108        self.local.register_transport(hello, sender)
109    }
110
111    pub(crate) fn submit_op_for(&self, user_id: UserId, client_op_id: ClientOpId, op: CanvasOp) {
112        self.local.submit_op_for(user_id, client_op_id, op);
113    }
114    pub(crate) fn disconnect_user(&self, user_id: UserId) {
115        self.local.disconnect_user(user_id);
116    }
117}