Skip to main content

wscall_server/
server_runtime.rs

1use std::collections::HashMap;
2use std::future::Future;
3use std::panic::AssertUnwindSafe;
4use std::sync::Arc;
5use std::sync::atomic::Ordering;
6use std::time::Duration;
7
8use bytes::Bytes;
9use futures_util::{FutureExt, SinkExt, StreamExt, future::BoxFuture, future::join_all};
10use serde::de::DeserializeOwned;
11use serde_json::{Map, Value, json};
12use tokio::net::{TcpListener, TcpStream};
13use tokio::sync::{Semaphore, mpsc};
14use tokio::time::{MissedTickBehavior, interval, timeout};
15use tokio_tungstenite::{
16    WebSocketStream, accept_async_with_config, tungstenite::Message,
17    tungstenite::protocol::WebSocketConfig,
18};
19use uuid::Uuid;
20use validator::Validate;
21use wscall_protocol::{
22    DEFAULT_MAX_FRAME_BYTES, EcdhKeypair, EncryptionKind, ErrorPayload, FileAttachment, FrameCodec,
23    PacketBody, PacketEnvelope, ProtocolError, parse_peer_public,
24};
25
26use crate::server_types::{
27    ApiContext, ApiError, ClientEntry, EventContext, ExceptionContext, ServerConnectionContext,
28    ServerDisconnectContext, ServerError, ServerHandle, ServerOutbound, ServerState,
29};
30use crate::validation;
31
32const SERVER_IDLE_TIMEOUT: Duration = Duration::from_secs(45);
33const SERVER_HEARTBEAT_INTERVAL: Duration = Duration::from_secs(15);
34const SERVER_OUTBOUND_QUEUE_CAPACITY: usize = 256;
35/// Default cap on concurrently in-flight request/event handlers per connection.
36const SERVER_DEFAULT_MAX_IN_FLIGHT: usize = 64;
37/// Grace window given to the writer task to flush a close frame before abort.
38const SERVER_CLOSE_GRACE: Duration = Duration::from_millis(500);
39
40type ApiHandler =
41    Arc<dyn Fn(ApiContext) -> BoxFuture<'static, Result<Value, ApiError>> + Send + Sync>;
42type Filter =
43    Arc<dyn Fn(ApiContext) -> BoxFuture<'static, Result<ApiContext, ApiError>> + Send + Sync>;
44type EventHandler =
45    Arc<dyn Fn(EventContext) -> BoxFuture<'static, Result<Value, ApiError>> + Send + Sync>;
46type ConnectionHandler =
47    Arc<dyn Fn(ServerConnectionContext) -> BoxFuture<'static, ()> + Send + Sync>;
48type DisconnectHandler =
49    Arc<dyn Fn(ServerDisconnectContext) -> BoxFuture<'static, ()> + Send + Sync>;
50type ExceptionHandler =
51    Arc<dyn Fn(ExceptionContext) -> BoxFuture<'static, ErrorPayload> + Send + Sync>;
52
53struct ApiRequestInput {
54    request_id: u64,
55    route: String,
56    params: Value,
57    attachments: Vec<FileAttachment>,
58    metadata: Value,
59}
60
61struct EventEmitInput {
62    event_id: u64,
63    name: String,
64    data: Map<String, Value>,
65    attachments: Vec<FileAttachment>,
66    metadata: Value,
67}
68
69impl ServerHandle {
70    /// Broadcasts an event to every live connection.
71    ///
72    /// The frame is encoded exactly once and shared as [`Bytes`] across all
73    /// recipients, so the cost no longer scales with the connection count times
74    /// the payload size. A full outbound queue for a single connection drops
75    /// that one delivery instead of failing the whole broadcast.
76    pub async fn broadcast_event(
77        &self,
78        name: impl Into<String>,
79        data: Map<String, Value>,
80        attachments: Vec<FileAttachment>,
81    ) -> Result<(), ApiError> {
82        let event_id = self.state.next_event_id.fetch_add(1, Ordering::Relaxed) + 1;
83        let packet = PacketEnvelope::with_encryption(
84            PacketBody::EventEmit {
85                event_id,
86                name: name.into(),
87                data,
88                attachments,
89                metadata: json!({ "source": "server" }),
90                expect_ack: true,
91            },
92            self.default_encryption,
93        );
94
95        if self.is_ecdh {
96            // ECDH mode: each connection has a unique session key, so the
97            // writer task must encode per-connection.
98            for entry in self.state.clients.iter() {
99                if entry
100                    .value()
101                    .sender
102                    .try_send(ServerOutbound::Packet(packet.clone()))
103                    .is_err()
104                {
105                    tracing::warn!(
106                        "broadcast: outbound queue full, dropping event for a connection"
107                    );
108                }
109            }
110        } else {
111            // PSK mode: all connections share the same key, so encode once.
112            let encoded = self
113                .codec
114                .encode(&packet)
115                .map_err(|_| ApiError::internal("failed to encode broadcast event"))?;
116            let encoded = Bytes::from(encoded);
117
118            for entry in self.state.clients.iter() {
119                if entry
120                    .value()
121                    .sender
122                    .try_send(ServerOutbound::PreEncoded(encoded.clone()))
123                    .is_err()
124                {
125                    tracing::warn!(
126                        "broadcast: outbound queue full, dropping event for a connection"
127                    );
128                }
129            }
130        }
131        Ok(())
132    }
133
134    /// Sends an event to a single connection by id.
135    ///
136    /// The frame is pre-encoded once so the writer task only ships bytes.
137    pub async fn send_event_to(
138        &self,
139        connection_id: &str,
140        name: impl Into<String>,
141        data: Map<String, Value>,
142        attachments: Vec<FileAttachment>,
143    ) -> Result<(), ApiError> {
144        let event_id = self.state.next_event_id.fetch_add(1, Ordering::Relaxed) + 1;
145        let packet = PacketEnvelope::with_encryption(
146            PacketBody::EventEmit {
147                event_id,
148                name: name.into(),
149                data,
150                attachments,
151                metadata: json!({ "source": "server" }),
152                expect_ack: true,
153            },
154            self.default_encryption,
155        );
156
157        let entry = self
158            .state
159            .clients
160            .get(connection_id)
161            .ok_or_else(|| ApiError::not_found("target connection not found"))?;
162
163        if self.is_ecdh {
164            // ECDH mode: send unencoded packet; the writer encodes it.
165            entry
166                .sender
167                .try_send(ServerOutbound::Packet(packet))
168                .map_err(|_| ApiError::internal("failed to queue direct event"))
169        } else {
170            let encoded = self
171                .codec
172                .encode(&packet)
173                .map_err(|_| ApiError::internal("failed to encode direct event"))?;
174            let encoded = Bytes::from(encoded);
175            entry
176                .sender
177                .try_send(ServerOutbound::PreEncoded(encoded))
178                .map_err(|_| ApiError::internal("failed to queue direct event"))
179        }
180    }
181
182    /// Returns the current number of live connections.
183    pub async fn connection_count(&self) -> usize {
184        self.state.clients.len()
185    }
186}
187
188pub struct WscallServer {
189    state: Arc<ServerState>,
190    routes: HashMap<String, ApiHandler>,
191    filters: Vec<Filter>,
192    event_handlers: HashMap<String, EventHandler>,
193    connection_handlers: Vec<ConnectionHandler>,
194    disconnect_handlers: Vec<DisconnectHandler>,
195    exception_handler: Option<ExceptionHandler>,
196    codec: FrameCodec,
197    default_encryption: EncryptionKind,
198    /// Whether ECDH dynamic key agreement is enabled (no pre-shared key).
199    is_ecdh: bool,
200    /// Optional global cap on concurrent accepted connections.
201    max_connections: Option<usize>,
202    /// Per-connection cap on concurrently running request/event handlers.
203    max_in_flight: usize,
204    /// Maximum total frame size (including 4-byte length prefix).
205    max_frame_bytes: usize,
206}
207
208impl Default for WscallServer {
209    fn default() -> Self {
210        Self::new()
211    }
212}
213
214impl WscallServer {
215    pub fn new() -> Self {
216        Self {
217            state: Arc::new(ServerState::new()),
218            routes: HashMap::new(),
219            filters: Vec::new(),
220            event_handlers: HashMap::new(),
221            connection_handlers: Vec::new(),
222            disconnect_handlers: Vec::new(),
223            exception_handler: None,
224            codec: FrameCodec::plaintext(),
225            default_encryption: EncryptionKind::None,
226            is_ecdh: false,
227            max_connections: None,
228            max_in_flight: SERVER_DEFAULT_MAX_IN_FLIGHT,
229            max_frame_bytes: DEFAULT_MAX_FRAME_BYTES,
230        }
231    }
232
233    /// Enables ECDH dynamic key agreement.
234    ///
235    /// Instead of a pre-shared symmetric key, each connection performs an
236    /// X25519 handshake right after the WebSocket upgrade. The negotiated
237    /// 32-byte session key is used with ChaCha20-Poly1305 for all subsequent
238    /// frames. This is mutually exclusive with `with_chacha20_key` /
239    /// `with_aes256_key`.
240    pub fn with_ecdh(mut self) -> Self {
241        self.is_ecdh = true;
242        // The global codec stays plaintext; per-connection codecs carry the
243        // negotiated session keys.
244        self.default_encryption = EncryptionKind::ChaCha20;
245        self
246    }
247
248    pub fn with_chacha20_key(mut self, key: [u8; 32]) -> Self {
249        self.codec = self.codec.clone().with_chacha20_key(key);
250        self.default_encryption = EncryptionKind::ChaCha20;
251        self
252    }
253
254    pub fn with_aes256_key(mut self, key: [u8; 32]) -> Self {
255        self.codec = self.codec.clone().with_aes256_key(key);
256        self.default_encryption = EncryptionKind::Aes256;
257        self
258    }
259
260    /// Caps the number of concurrently accepted connections.
261    ///
262    /// When the cap is reached, new `accept` calls block until an existing
263    /// connection drops, providing natural backpressure against connection
264    /// flooding instead of spawning unbounded tasks.
265    pub fn with_max_connections(mut self, max: usize) -> Self {
266        self.max_connections = Some(max);
267        self
268    }
269
270    /// Caps the number of concurrently in-flight request/event handlers per
271    /// single connection, bounding per-connection CPU and memory usage.
272    pub fn with_max_in_flight(mut self, max: usize) -> Self {
273        self.max_in_flight = max;
274        self
275    }
276
277    /// Sets the maximum total frame size (including the 4-byte length prefix).
278    ///
279    /// Frames exceeding this limit cause the server to send a 413 error response
280    /// frame (`request_id=0`, `code="frame_too_large"`); the connection stays
281    /// open. Default: 100 MiB.
282    pub fn with_max_frame_bytes(mut self, max: usize) -> Self {
283        self.max_frame_bytes = max;
284        self
285    }
286
287    pub fn handle(&self) -> ServerHandle {
288        ServerHandle {
289            state: Arc::clone(&self.state),
290            codec: self.codec.clone(),
291            default_encryption: self.default_encryption,
292            is_ecdh: self.is_ecdh,
293        }
294    }
295
296    pub fn route<F, Fut>(&mut self, route: impl Into<String>, handler: F)
297    where
298        F: Fn(ApiContext) -> Fut + Send + Sync + 'static,
299        Fut: Future<Output = Result<Value, ApiError>> + Send + 'static,
300    {
301        let handler = Arc::new(move |ctx: ApiContext| {
302            Box::pin(handler(ctx)) as BoxFuture<'static, Result<Value, ApiError>>
303        });
304        self.routes.insert(route.into(), handler);
305    }
306
307    pub fn typed_route<T, F, Fut>(&mut self, route: impl Into<String>, handler: F)
308    where
309        T: DeserializeOwned + Send + 'static,
310        F: Fn(ApiContext, T) -> Fut + Send + Sync + 'static,
311        Fut: Future<Output = Result<Value, ApiError>> + Send + 'static,
312    {
313        let handler = Arc::new(handler);
314        self.route(route, move |mut ctx| {
315            let handler = Arc::clone(&handler);
316            // Zero-copy bind: moves the params out of the context instead of
317            // deep-cloning the whole JSON tree on every request.
318            let params = ctx.bind_take::<T>();
319            async move {
320                let params = params?;
321                handler(ctx, params).await
322            }
323        });
324    }
325
326    pub fn validated_route<T, F, Fut>(&mut self, route: impl Into<String>, handler: F)
327    where
328        T: DeserializeOwned + Validate + Send + 'static,
329        F: Fn(ApiContext, T) -> Fut + Send + Sync + 'static,
330        Fut: Future<Output = Result<Value, ApiError>> + Send + 'static,
331    {
332        let handler = Arc::new(handler);
333        self.route(route, move |mut ctx| {
334            let handler = Arc::clone(&handler);
335            // Zero-copy bind (see `typed_route`); validation runs on the owned
336            // value exactly as before.
337            let params: Result<T, ApiError> = ctx.bind_take::<T>().and_then(|params: T| {
338                params.validate().map_err(|source| {
339                    ApiError::bad_request("params validation failed").with_details(json!({
340                        "validation_errors": validation::errors_to_details(&source),
341                    }))
342                })?;
343                Ok(params)
344            });
345            async move {
346                let params = params?;
347                handler(ctx, params).await
348            }
349        });
350    }
351
352    pub fn filter<F, Fut>(&mut self, filter: F)
353    where
354        F: Fn(ApiContext) -> Fut + Send + Sync + 'static,
355        Fut: Future<Output = Result<ApiContext, ApiError>> + Send + 'static,
356    {
357        let filter = Arc::new(move |ctx: ApiContext| {
358            Box::pin(filter(ctx)) as BoxFuture<'static, Result<ApiContext, ApiError>>
359        });
360        self.filters.push(filter);
361    }
362
363    pub fn event_handler<F, Fut>(&mut self, name: impl Into<String>, handler: F)
364    where
365        F: Fn(EventContext) -> Fut + Send + Sync + 'static,
366        Fut: Future<Output = Result<Value, ApiError>> + Send + 'static,
367    {
368        let handler = Arc::new(move |ctx: EventContext| {
369            Box::pin(handler(ctx)) as BoxFuture<'static, Result<Value, ApiError>>
370        });
371        self.event_handlers.insert(name.into(), handler);
372    }
373
374    pub fn on_connected<F, Fut>(&mut self, handler: F)
375    where
376        F: Fn(ServerConnectionContext) -> Fut + Send + Sync + 'static,
377        Fut: Future<Output = ()> + Send + 'static,
378    {
379        let handler = Arc::new(move |ctx: ServerConnectionContext| {
380            Box::pin(handler(ctx)) as BoxFuture<'static, ()>
381        });
382        self.connection_handlers.push(handler);
383    }
384
385    pub fn on_disconnected<F, Fut>(&mut self, handler: F)
386    where
387        F: Fn(ServerDisconnectContext) -> Fut + Send + Sync + 'static,
388        Fut: Future<Output = ()> + Send + 'static,
389    {
390        let handler = Arc::new(move |ctx: ServerDisconnectContext| {
391            Box::pin(handler(ctx)) as BoxFuture<'static, ()>
392        });
393        self.disconnect_handlers.push(handler);
394    }
395
396    pub fn exception_handler<F, Fut>(&mut self, handler: F)
397    where
398        F: Fn(ExceptionContext) -> Fut + Send + Sync + 'static,
399        Fut: Future<Output = ErrorPayload> + Send + 'static,
400    {
401        self.exception_handler = Some(Arc::new(move |ctx: ExceptionContext| {
402            Box::pin(handler(ctx)) as BoxFuture<'static, ErrorPayload>
403        }));
404    }
405
406    pub async fn listen(self, address: &str) -> Result<(), ServerError> {
407        let listener = TcpListener::bind(address).await?;
408        tracing::info!(%address, "wscall server listening on ws://{address}/socket");
409
410        let shared = Arc::new(self);
411        let conn_sem = shared
412            .max_connections
413            .map(|max| Arc::new(Semaphore::new(max)));
414
415        loop {
416            // Acquire a connection permit (when capped) before accepting so a
417            // connection flood turns into backpressure rather than unbounded
418            // task spawning.
419            let permit = match &conn_sem {
420                Some(sem) => match sem.clone().acquire_owned().await {
421                    Ok(permit) => Some(permit),
422                    Err(_) => continue,
423                },
424                None => None,
425            };
426
427            let (stream, peer) = listener.accept().await?;
428            // Disable Nagle's algorithm for low-latency RPC frames.
429            let _ = TcpStream::set_nodelay(&stream, true);
430
431            let server = Arc::clone(&shared);
432            tokio::spawn(async move {
433                let _permit = permit;
434                if let Err(error) = server.serve_connection(stream, peer).await {
435                    tracing::warn!(?error, ?peer, "connection failed");
436                }
437            });
438        }
439    }
440
441    async fn serve_connection(
442        self: Arc<Self>,
443        stream: TcpStream,
444        peer: std::net::SocketAddr,
445    ) -> Result<(), ServerError> {
446        // Configure WebSocket-level message size limit slightly above the
447        // WSCALL limit so that oversized frames can still be received and
448        // answered with a 413 error response instead of an abrupt close.
449        let mut ws_config = WebSocketConfig::default();
450        ws_config.max_message_size = Some(self.max_frame_bytes + 1024 * 1024);
451        ws_config.max_frame_size = Some(self.max_frame_bytes + 1024 * 1024);
452        let mut websocket = accept_async_with_config(stream, Some(ws_config)).await?;
453        let connection_id = Uuid::now_v7().to_string();
454        let peer_addr = Some(peer);
455
456        // ECDH handshake: exchange X25519 public keys and derive a per-
457        // connection session key before splitting the stream.
458        let session_codec = if self.is_ecdh {
459            let (codec, _encryption) = self.perform_ecdh_handshake(&mut websocket).await?;
460            codec.with_max_frame_bytes(self.max_frame_bytes)
461        } else {
462            self.codec
463                .clone()
464                .with_max_frame_bytes(self.max_frame_bytes)
465        };
466
467        let (mut sink, mut stream) = websocket.split();
468        let (tx, mut rx) = mpsc::channel::<ServerOutbound>(SERVER_OUTBOUND_QUEUE_CAPACITY);
469
470        // Store the per-connection entry.
471        self.state
472            .clients
473            .insert(connection_id.clone(), ClientEntry { sender: tx.clone() });
474
475        self.notify_connected(&connection_id, peer_addr).await;
476
477        // The writer needs the per-connection codec to encode `Packet`
478        // variants (ECDH mode).
479        let writer_codec = session_codec.clone();
480        let mut writer = tokio::spawn(async move {
481            let mut ticker = interval(SERVER_HEARTBEAT_INTERVAL);
482            ticker.set_missed_tick_behavior(MissedTickBehavior::Delay);
483            loop {
484                tokio::select! {
485                    maybe_outbound = rx.recv() => {
486                        let Some(outbound) = maybe_outbound else {
487                            break Ok::<(), ServerError>(());
488                        };
489                        match outbound {
490                            ServerOutbound::PreEncoded(bytes) => {
491                                // Zero-copy: `bytes` is already a shared `Bytes`,
492                                // and tungstenite 0.30 accepts `Bytes` directly, so
493                                // broadcasts encoded once are shipped to every
494                                // recipient without a per-recipient frame copy.
495                                sink.send(Message::Binary(bytes)).await?;
496                            }
497                            ServerOutbound::Packet(packet) => {
498                                let encoded = writer_codec.encode(&packet)?;
499                                sink.send(Message::Binary(encoded.into())).await?;
500                            }
501                            ServerOutbound::Pong(payload) => {
502                                sink.send(Message::Pong(payload.into())).await?;
503                            }
504                            ServerOutbound::Close => {
505                                let _ = sink.send(Message::Close(None)).await;
506                                break Ok(());
507                            }
508                        }
509                    }
510                    _ = ticker.tick() => {
511                        if let Err(error) = sink.send(Message::Ping(Bytes::new())).await {
512                            break Err(ServerError::WebSocket(error));
513                        }
514                    }
515                }
516            }
517        });
518
519        // The reader uses the per-connection codec (session key in ECDH mode,
520        // global codec in PSK mode).
521        let reader_codec = session_codec.clone();
522
523        // Per-connection concurrency limit for handler execution. The reader
524        // stays non-blocking under normal load; under overload it pauses reading
525        // new frames, which is the desired backpressure signal.
526        let in_flight = Arc::new(Semaphore::new(self.max_in_flight));
527
528        let result = async {
529            self.handle()
530                .send_event_to(
531                    &connection_id,
532                    "system.notice",
533                    json!({ "message": "connected", "connection_id": connection_id })
534                        .as_object()
535                        .unwrap()
536                        .clone(),
537                    Vec::new(),
538                )
539                .await
540                .map_err(ServerError::Api)?;
541
542            loop {
543                let next_message = timeout(SERVER_IDLE_TIMEOUT, stream.next()).await;
544                let Some(message) =
545                    next_message.map_err(|_| ServerError::IdleTimeout(connection_id.clone()))?
546                else {
547                    break Ok(());
548                };
549
550                match message? {
551                    Message::Binary(bytes) => {
552                        // WSCALL-level frame size check: reject oversized frames
553                        // with a 413 error response; the connection stays open.
554                        if bytes.len() > self.max_frame_bytes {
555                            let error_packet = PacketEnvelope::with_encryption(
556                                PacketBody::ApiResponse {
557                                    request_id: 0,
558                                    ok: false,
559                                    status: 413,
560                                    data: json!({}),
561                                    error: Some(ErrorPayload {
562                                        code: "frame_too_large".to_string(),
563                                        message: format!(
564                                            "frame size {} exceeds limit {}",
565                                            bytes.len(),
566                                            self.max_frame_bytes
567                                        ),
568                                        status: 413,
569                                        details: None,
570                                    }),
571                                    metadata: json!({}),
572                                },
573                                self.default_encryption,
574                            );
575                            let _ = self.queue_for(&connection_id, error_packet).await;
576                            continue;
577                        }
578
579                        let packet = reader_codec.decode(bytes)?;
580
581                        let spawn_handler = matches!(
582                            &packet.body,
583                            PacketBody::ApiRequest { .. } | PacketBody::EventEmit { .. }
584                        );
585
586                        if spawn_handler {
587                            let permit = in_flight.clone().acquire_owned().await.map_err(|_| {
588                                ServerError::Api(ApiError::internal("in-flight semaphore closed"))
589                            })?;
590                            let server = Arc::clone(&self);
591                            let conn_id = connection_id.clone();
592                            tokio::spawn(async move {
593                                let _permit = permit;
594                                if let Err(error) =
595                                    server.process_packet(&conn_id, peer_addr, packet).await
596                                {
597                                    tracing::warn!(%error, "packet processing failed");
598                                }
599                            });
600                        } else {
601                            // Trivial control messages (acks / responses from
602                            // clients) are handled inline without spawning.
603                            self.process_packet(&connection_id, peer_addr, packet)
604                                .await?;
605                        }
606                    }
607                    Message::Close(_) => break Ok(()),
608                    Message::Ping(payload) => {
609                        if tx
610                            .send(ServerOutbound::Pong(payload.to_vec()))
611                            .await
612                            .is_err()
613                        {
614                            break Ok(());
615                        }
616                    }
617                    Message::Pong(_) => {}
618                    Message::Text(_) => {}
619                    Message::Frame(_) => {}
620                }
621            }
622        }
623        .await;
624
625        // Teardown: remove from the live table, attempt a graceful close, then
626        // abort the writer. Aborting the writer drops the channel receiver so
627        // any in-flight handler `queue_for` calls fail fast instead of hanging.
628        self.state.clients.remove(&connection_id);
629        let _ = tx.try_send(ServerOutbound::Close);
630        let _ = timeout(SERVER_CLOSE_GRACE, &mut writer).await;
631        writer.abort();
632        self.notify_disconnected(&connection_id, peer_addr, Self::disconnect_reason(&result))
633            .await;
634        result
635    }
636
637    /// Performs the X25519 ECDH handshake over the combined WebSocket stream.
638    ///
639    /// 1. Generate a server keypair.
640    /// 2. Wait for the client's 32-byte public key (raw binary message).
641    /// 3. Send the server's 32-byte public key back.
642    /// 4. Derive the 32-byte ChaCha20-Poly1305 session key.
643    ///
644    /// Must be called before `websocket.split()` because it uses both
645    /// the read and write halves of the stream.
646    async fn perform_ecdh_handshake(
647        &self,
648        websocket: &mut WebSocketStream<TcpStream>,
649    ) -> Result<(FrameCodec, EncryptionKind), ServerError> {
650        // 1. Generate server keypair.
651        let keypair = EcdhKeypair::generate()?;
652
653        // 2. Read the client's public key (first binary WebSocket message).
654        let next = timeout(Duration::from_secs(10), websocket.next()).await;
655        let client_public = match next {
656            Ok(Some(Ok(Message::Binary(bytes)))) => parse_peer_public(&bytes).map_err(|_| {
657                ServerError::Protocol(ProtocolError::EcdhHandshake(
658                    "client sent invalid public key length".to_string(),
659                ))
660            })?,
661            _ => {
662                return Err(ProtocolError::EcdhHandshake(
663                    "client did not send a valid public key".to_string(),
664                )
665                .into());
666            }
667        };
668
669        // 3. Send the server's public key.
670        websocket
671            .send(Message::Binary(keypair.public_bytes().to_vec().into()))
672            .await?;
673
674        // 4. Derive the session key and build the per-connection codec.
675        let session_key = keypair.derive_session_key(&client_public);
676        Ok((
677            FrameCodec::plaintext().with_chacha20_key(session_key),
678            EncryptionKind::ChaCha20,
679        ))
680    }
681
682    async fn process_packet(
683        &self,
684        connection_id: &str,
685        peer_addr: Option<std::net::SocketAddr>,
686        packet: PacketEnvelope,
687    ) -> Result<(), ServerError> {
688        match packet.body {
689            PacketBody::ApiRequest {
690                request_id,
691                route,
692                params,
693                attachments,
694                metadata,
695            } => {
696                let response = self
697                    .run_api_request(
698                        connection_id,
699                        peer_addr,
700                        ApiRequestInput {
701                            request_id,
702                            route,
703                            params,
704                            attachments,
705                            metadata,
706                        },
707                    )
708                    .await;
709                self.queue_for(connection_id, response).await?;
710            }
711            PacketBody::EventEmit {
712                event_id,
713                name,
714                data,
715                attachments,
716                metadata,
717                ..
718            } => {
719                let ack = self
720                    .run_event(
721                        connection_id,
722                        peer_addr,
723                        EventEmitInput {
724                            event_id,
725                            name,
726                            data,
727                            attachments,
728                            metadata,
729                        },
730                    )
731                    .await;
732                self.queue_for(connection_id, ack).await?;
733            }
734            PacketBody::EventAck {
735                event_id,
736                ok,
737                receipt,
738                error,
739            } => {
740                tracing::debug!(
741                    connection_id,
742                    event_id,
743                    ok,
744                    ?receipt,
745                    ?error,
746                    "received event ack from client",
747                );
748            }
749            PacketBody::ApiResponse { .. } => {}
750        }
751        Ok(())
752    }
753
754    /// Queues a response packet for a connection.
755    ///
756    /// The frame is pre-encoded here (offloading the writer task and enabling
757    /// parallel encoding across concurrent handlers) and sent with backpressure
758    /// semantics: a slow consumer pauses the producer rather than being dropped.
759    async fn queue_for(
760        &self,
761        connection_id: &str,
762        packet: PacketEnvelope,
763    ) -> Result<(), ServerError> {
764        // Clone the sender out of the DashMap guard before awaiting so the
765        // shard lock is never held across an await point.
766        let sender = match self.state.clients.get(connection_id) {
767            Some(entry) => entry.sender.clone(),
768            None => {
769                return Err(ServerError::Api(ApiError::not_found(
770                    "connection is closed",
771                )));
772            }
773        };
774
775        if self.is_ecdh {
776            // ECDH mode: the writer task encodes with the per-connection codec.
777            sender
778                .send(ServerOutbound::Packet(packet))
779                .await
780                .map_err(|_| {
781                    ServerError::Api(ApiError::internal("failed to queue outbound packet"))
782                })
783        } else {
784            // PSK mode: pre-encode with the shared global codec.
785            let encoded = self.codec.encode(&packet)?;
786            let encoded = Bytes::from(encoded);
787            sender
788                .send(ServerOutbound::PreEncoded(encoded))
789                .await
790                .map_err(|_| {
791                    ServerError::Api(ApiError::internal("failed to queue outbound packet"))
792                })
793        }
794    }
795
796    async fn run_api_request(
797        &self,
798        connection_id: &str,
799        peer_addr: Option<std::net::SocketAddr>,
800        request: ApiRequestInput,
801    ) -> PacketEnvelope {
802        let ApiRequestInput {
803            request_id,
804            route,
805            params,
806            attachments,
807            metadata,
808        } = request;
809
810        let mut ctx = ApiContext {
811            connection_id: connection_id.to_string(),
812            peer_addr,
813            request_id,
814            route: route.clone(),
815            params,
816            attachments,
817            metadata,
818            server: self.handle(),
819        };
820
821        for filter in &self.filters {
822            match filter(ctx).await {
823                Ok(next_ctx) => ctx = next_ctx,
824                Err(error) => {
825                    return self
826                        .api_error_packet(connection_id, Some(request_id), route, error)
827                        .await;
828                }
829            }
830        }
831
832        let Some(handler) = self.routes.get(&ctx.route) else {
833            return self
834                .api_error_packet(
835                    connection_id,
836                    Some(request_id),
837                    route,
838                    ApiError::not_found("route not found"),
839                )
840                .await;
841        };
842
843        match AssertUnwindSafe(handler(ctx)).catch_unwind().await {
844            Ok(Ok(data)) => PacketEnvelope::with_encryption(
845                PacketBody::ApiResponse {
846                    request_id,
847                    ok: true,
848                    status: 200,
849                    data,
850                    error: None,
851                    metadata: json!({}),
852                },
853                self.default_encryption,
854            ),
855            Ok(Err(error)) => {
856                self.api_error_packet(connection_id, Some(request_id), route, error)
857                    .await
858            }
859            Err(_) => {
860                self.api_error_packet(
861                    connection_id,
862                    Some(request_id),
863                    route,
864                    ApiError::internal("handler panicked"),
865                )
866                .await
867            }
868        }
869    }
870
871    async fn run_event(
872        &self,
873        connection_id: &str,
874        peer_addr: Option<std::net::SocketAddr>,
875        event: EventEmitInput,
876    ) -> PacketEnvelope {
877        let EventEmitInput {
878            event_id,
879            name,
880            data,
881            attachments,
882            metadata,
883        } = event;
884
885        let ctx = EventContext {
886            connection_id: connection_id.to_string(),
887            peer_addr,
888            event_id,
889            name: name.clone(),
890            data,
891            attachments,
892            metadata,
893            server: self.handle(),
894        };
895
896        let Some(handler) = self.event_handlers.get(&name) else {
897            return PacketEnvelope::with_encryption(
898                PacketBody::EventAck {
899                    event_id,
900                    ok: false,
901                    receipt: json!({}),
902                    error: Some(ApiError::not_found("event handler not found").into_payload()),
903                },
904                self.default_encryption,
905            );
906        };
907
908        match AssertUnwindSafe(handler(ctx)).catch_unwind().await {
909            Ok(Ok(receipt)) => PacketEnvelope::with_encryption(
910                PacketBody::EventAck {
911                    event_id,
912                    ok: true,
913                    receipt,
914                    error: None,
915                },
916                self.default_encryption,
917            ),
918            Ok(Err(error)) => PacketEnvelope::with_encryption(
919                PacketBody::EventAck {
920                    event_id,
921                    ok: false,
922                    receipt: json!({}),
923                    error: Some(
924                        self.map_exception(ExceptionContext {
925                            connection_id: connection_id.to_string(),
926                            request_id: Some(event_id),
927                            target: name,
928                            message_kind: "event",
929                            error,
930                        })
931                        .await,
932                    ),
933                },
934                self.default_encryption,
935            ),
936            Err(_) => PacketEnvelope::with_encryption(
937                PacketBody::EventAck {
938                    event_id,
939                    ok: false,
940                    receipt: json!({}),
941                    error: Some(
942                        self.map_exception(ExceptionContext {
943                            connection_id: connection_id.to_string(),
944                            request_id: Some(event_id),
945                            target: name,
946                            message_kind: "event",
947                            error: ApiError::internal("event handler panicked"),
948                        })
949                        .await,
950                    ),
951                },
952                self.default_encryption,
953            ),
954        }
955    }
956
957    async fn api_error_packet(
958        &self,
959        connection_id: &str,
960        request_id: Option<u64>,
961        route: String,
962        error: ApiError,
963    ) -> PacketEnvelope {
964        let request_id = request_id.unwrap_or(0);
965        let status = error.status;
966        let payload = self
967            .map_exception(ExceptionContext {
968                connection_id: connection_id.to_string(),
969                request_id: Some(request_id),
970                target: route,
971                message_kind: "api",
972                error,
973            })
974            .await;
975
976        PacketEnvelope::with_encryption(
977            PacketBody::ApiResponse {
978                request_id,
979                ok: false,
980                status,
981                data: json!({}),
982                error: Some(payload),
983                metadata: json!({}),
984            },
985            self.default_encryption,
986        )
987    }
988
989    async fn notify_connected(&self, connection_id: &str, peer_addr: Option<std::net::SocketAddr>) {
990        let handlers = self.connection_handlers.clone();
991        // Run lifecycle handlers concurrently so a slow handler cannot delay
992        // connection establishment (the reader loop starts right after this).
993        let futures = handlers.into_iter().map(|handler| {
994            let context = ServerConnectionContext {
995                connection_id: connection_id.to_string(),
996                peer_addr,
997                server: self.handle(),
998            };
999            AssertUnwindSafe(handler(context)).catch_unwind()
1000        });
1001        for result in join_all(futures).await {
1002            if result.is_err() {
1003                tracing::error!("server connected handler panicked");
1004            }
1005        }
1006    }
1007
1008    async fn notify_disconnected(
1009        &self,
1010        connection_id: &str,
1011        peer_addr: Option<std::net::SocketAddr>,
1012        reason: String,
1013    ) {
1014        let handlers = self.disconnect_handlers.clone();
1015        let futures = handlers.into_iter().map(|handler| {
1016            let context = ServerDisconnectContext {
1017                connection_id: connection_id.to_string(),
1018                peer_addr,
1019                reason: reason.clone(),
1020                server: self.handle(),
1021            };
1022            AssertUnwindSafe(handler(context)).catch_unwind()
1023        });
1024        for result in join_all(futures).await {
1025            if result.is_err() {
1026                tracing::error!("server disconnected handler panicked");
1027            }
1028        }
1029    }
1030
1031    fn disconnect_reason(result: &Result<(), ServerError>) -> String {
1032        match result {
1033            Ok(()) => "connection closed".to_string(),
1034            Err(ServerError::IdleTimeout(_)) => "idle timeout".to_string(),
1035            Err(error) => error.to_string(),
1036        }
1037    }
1038
1039    async fn map_exception(&self, context: ExceptionContext) -> ErrorPayload {
1040        match &self.exception_handler {
1041            Some(handler) => handler(context).await,
1042            None => context.error.into_payload(),
1043        }
1044    }
1045}