Skip to main content

nautilus_derive/websocket/
handler.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Inner I/O feed handler for the Derive WebSocket transport.
17//!
18//! The handler owns the [`WebSocketClient`] exclusively and runs in a dedicated
19//! Tokio task. The outer [`super::client::DeriveWebSocketClient`] talks to it
20//! via a command channel and consumes a stream of [`DeriveWsMessage`] events.
21//!
22//! Each outbound JSON-RPC request is registered in a `pending` map keyed by the
23//! correlator `id`. When the venue echoes the id on a response frame, the
24//! matching oneshot is fulfilled with `result` or the JSON-RPC error.
25
26use std::{
27    sync::{
28        Arc,
29        atomic::{AtomicBool, AtomicU64, Ordering},
30    },
31    time::Duration,
32};
33
34use ahash::AHashMap;
35use nautilus_live::task::{SharedTaskSlot, TaskJoinOutcome};
36use nautilus_network::{
37    RECONNECTED,
38    websocket::{AuthTracker, WebSocketClient},
39};
40use serde_json::Value;
41use tokio_tungstenite::tungstenite::Message;
42
43use super::{
44    client::UNAUTHENTICATED_CONNECTION_EPOCH,
45    error::DeriveWsError,
46    messages::{DeriveWsChannel, DeriveWsFrame, WsSubscribeParams, WsSubscriptionPayload},
47};
48use crate::http::models::JsonRpcRequest;
49
50/// Outbound commands the outer client sends to the inner handler.
51#[derive(Debug)]
52pub(super) enum HandlerCommand {
53    /// Hand the active [`WebSocketClient`] to the handler.
54    SetClient(WebSocketClient),
55    /// Send a JSON-RPC request and resolve the oneshot when the venue replies.
56    /// `params` is a pre-serialized `Value` so the handler stays agnostic to the
57    /// per-method param types (login, subscribe, signed `private/*` bodies).
58    Request {
59        method: &'static str,
60        params: Value,
61        connection_epoch: Option<u64>,
62        response_tx: tokio::sync::oneshot::Sender<Result<Value, DeriveWsError>>,
63    },
64    /// Gracefully tear down the WebSocket connection.
65    Disconnect,
66}
67
68/// Events emitted by the handler for the outer client and downstream consumers.
69#[derive(Debug, Clone)]
70pub enum DeriveWsMessage {
71    /// `public/login` succeeded. Consumed by the client's spawn loop to drive
72    /// resubscription; not forwarded to data/execution layers.
73    Authenticated,
74    /// Underlying transport reconnected; outer client triggers re-login and
75    /// resubscribes the tracked channels.
76    Reconnected,
77    /// Re-login or subscription replay exhausted its retry budget.
78    SessionRecoveryFailed(String),
79    /// Channel update pushed by the venue.
80    Subscription(WsSubscriptionPayload),
81}
82
83#[derive(Debug)]
84struct SendCommand {
85    id: u64,
86    token: u64,
87    connection_epoch: u64,
88    payload: String,
89}
90
91#[derive(Debug)]
92struct SendFailure {
93    id: u64,
94    token: u64,
95    reason: String,
96}
97
98#[derive(Debug)]
99struct PendingRequest {
100    token: u64,
101    response_tx: tokio::sync::oneshot::Sender<Result<Value, DeriveWsError>>,
102}
103
104/// Inner I/O loop. Lives in a Tokio task spawned by
105/// [`super::client::DeriveWebSocketClient::connect`].
106pub(super) struct FeedHandler {
107    signal: Arc<AtomicBool>,
108    client: Option<Arc<WebSocketClient>>,
109    cmd_rx: tokio::sync::mpsc::UnboundedReceiver<HandlerCommand>,
110    cmd_closed: bool,
111    raw_rx: tokio::sync::mpsc::UnboundedReceiver<Message>,
112    raw_closed: bool,
113    next_id: Arc<AtomicU64>,
114    next_send_token: u64,
115    pending: AHashMap<u64, PendingRequest>,
116    send_tx: Option<tokio::sync::mpsc::UnboundedSender<SendCommand>>,
117    send_failure_rx: tokio::sync::mpsc::UnboundedReceiver<SendFailure>,
118    send_task: Arc<SharedTaskSlot<()>>,
119    auth_tracker: AuthTracker,
120    authenticated_epoch: Arc<AtomicU64>,
121}
122
123impl FeedHandler {
124    pub(super) fn new_with_send_task(
125        signal: Arc<AtomicBool>,
126        cmd_rx: tokio::sync::mpsc::UnboundedReceiver<HandlerCommand>,
127        raw_rx: tokio::sync::mpsc::UnboundedReceiver<Message>,
128        next_id: Arc<AtomicU64>,
129        auth_tracker: AuthTracker,
130        authenticated_epoch: Arc<AtomicU64>,
131        send_task: Arc<SharedTaskSlot<()>>,
132    ) -> Self {
133        let (_, send_failure_rx) = tokio::sync::mpsc::unbounded_channel();
134        Self {
135            signal,
136            client: None,
137            cmd_rx,
138            cmd_closed: false,
139            raw_rx,
140            raw_closed: false,
141            next_id,
142            next_send_token: 0,
143            pending: AHashMap::new(),
144            send_tx: None,
145            send_failure_rx,
146            send_task,
147            auth_tracker,
148            authenticated_epoch,
149        }
150    }
151
152    /// Drains the next event from the underlying channels, processes it, and
153    /// returns the resulting outbound message (if any). Returns `None` when
154    /// the handler is shutting down or both channels closed.
155    pub(super) async fn next(&mut self) -> Option<DeriveWsMessage> {
156        loop {
157            tokio::select! {
158                cmd = self.cmd_rx.recv(), if !self.cmd_closed => {
159                    match cmd {
160                        None => {
161                            self.cmd_closed = true;
162
163                            if self.raw_closed {
164                                self.shutdown_send_path(
165                                    "WebSocket handler stopped before response was received",
166                                )
167                                .await;
168                                return None;
169                            }
170                        }
171                        Some(HandlerCommand::SetClient(client)) => {
172                            log::debug!("Setting WebSocket client in Derive handler");
173                            let client = Arc::new(client);
174                            self.stop_send_worker().await;
175                            self.start_send_worker(Arc::clone(&client));
176                            self.client = Some(client);
177                        }
178                        Some(HandlerCommand::Request {
179                            method,
180                            params,
181                            connection_epoch,
182                            response_tx,
183                        }) => {
184                            self.dispatch_request(method, params, connection_epoch, response_tx);
185                        }
186                        Some(HandlerCommand::Disconnect) => {
187                            log::debug!("Derive handler received disconnect command");
188                            self.shutdown_send_path(
189                                "WebSocket disconnected before response was received",
190                            )
191                            .await;
192
193                            if let Some(ref client) = self.client {
194                                client.disconnect().await;
195                            }
196                            self.signal.store(true, Ordering::SeqCst);
197                            return None;
198                        }
199                    }
200                }
201
202                raw = self.raw_rx.recv(), if !self.raw_closed => {
203                    match raw {
204                        None => {
205                            self.raw_closed = true;
206
207                            if self.cmd_closed {
208                                self.shutdown_send_path(
209                                    "WebSocket handler stopped before response was received",
210                                )
211                                .await;
212                                return None;
213                            }
214                        }
215                        Some(Message::Text(text)) => {
216                            if text.as_str() == RECONNECTED {
217                                log::info!("Derive WebSocket reconnected sentinel received");
218                                self.auth_tracker.invalidate();
219                                self.authenticated_epoch.store(
220                                    UNAUTHENTICATED_CONNECTION_EPOCH,
221                                    Ordering::Release,
222                                );
223                                self.restart_send_worker(
224                                    "WebSocket reconnected before response was received",
225                                )
226                                .await;
227                                return Some(DeriveWsMessage::Reconnected);
228                            }
229
230                            match DeriveWsFrame::parse(&text) {
231                                Ok(DeriveWsFrame::Response { id, result, error }) => {
232                                    if let Some(pending) = self.pending.remove(&id) {
233                                        let outcome = match (result, error) {
234                                            (_, Some(err)) => Err(DeriveWsError::JsonRpc {
235                                                code: err.code,
236                                                message: err.message,
237                                                data: err.data,
238                                            }),
239                                            (Some(value), None) => Ok(value),
240                                            (None, None) => Ok(Value::Null),
241                                        };
242                                        let _ = pending.response_tx.send(outcome);
243                                    } else {
244                                        log::debug!(
245                                            "Derive WebSocket response with unknown id={id} dropped",
246                                        );
247                                    }
248                                }
249                                Ok(DeriveWsFrame::Subscription(payload)) => {
250                                    return Some(DeriveWsMessage::Subscription(payload));
251                                }
252                                Ok(DeriveWsFrame::UncorrelatedError(error)) => {
253                                    self.fail_uncorrelated_error(error);
254                                }
255                                Ok(DeriveWsFrame::Unknown(value)) => {
256                                    log::debug!("Derive WebSocket unknown frame: {value}");
257                                }
258                                Err(e) => {
259                                    log::error!(
260                                        "Derive WebSocket frame parse error: {e}, text: {text}",
261                                    );
262                                }
263                            }
264                        }
265                        Some(Message::Ping(data)) => {
266                            if let Some(ref client) = self.client
267                                && let Err(e) = client.send_pong(data.to_vec()).await {
268                                log::error!("Derive WebSocket send_pong failed: {e}");
269                            }
270                        }
271                        Some(Message::Close(_)) => {
272                            log::debug!("Derive WebSocket close frame received");
273                            self.shutdown_send_path(
274                                "WebSocket closed before response was received",
275                            )
276                            .await;
277                            return None;
278                        }
279                        Some(_) => {}
280                    }
281                }
282
283                Some(failure) = self.send_failure_rx.recv() => {
284                    self.handle_send_failure(failure);
285                }
286
287                else => {
288                    log::debug!("Derive handler shutting down: channels closed");
289                    self.shutdown_send_path(
290                        "WebSocket handler stopped before response was received",
291                    )
292                    .await;
293                    return None;
294                }
295            }
296        }
297    }
298
299    fn dispatch_request(
300        &mut self,
301        method: &'static str,
302        params: Value,
303        connection_epoch: Option<u64>,
304        response_tx: tokio::sync::oneshot::Sender<Result<Value, DeriveWsError>>,
305    ) {
306        let Some(client) = self.client.as_ref() else {
307            let _ = response_tx.send(Err(DeriveWsError::NotConnected));
308            return;
309        };
310        let current_epoch = client.connection_epoch();
311        let connection_epoch = if method.starts_with("private/") {
312            let authenticated_epoch = self.authenticated_epoch.load(Ordering::Acquire);
313            if !client.connection_mode().is_active() || authenticated_epoch != current_epoch {
314                let _ = response_tx.send(Err(DeriveWsError::Authentication {
315                    operation: method.to_string(),
316                    reason: "WebSocket session is not authenticated".to_string(),
317                }));
318                return;
319            }
320            authenticated_epoch
321        } else {
322            connection_epoch.unwrap_or(current_epoch)
323        };
324
325        if !client.connection_mode().is_active() || connection_epoch != current_epoch {
326            let _ = response_tx.send(Err(DeriveWsError::transport(format!(
327                "connection changed before `{method}` was sent",
328            ))));
329            return;
330        }
331
332        self.enqueue_request(method, params, connection_epoch, response_tx);
333    }
334
335    fn enqueue_request(
336        &mut self,
337        method: &'static str,
338        params: Value,
339        connection_epoch: u64,
340        response_tx: tokio::sync::oneshot::Sender<Result<Value, DeriveWsError>>,
341    ) {
342        let Some(send_tx) = self.send_tx.clone() else {
343            let _ = response_tx.send(Err(DeriveWsError::NotConnected));
344            return;
345        };
346        let id = self.next_id.fetch_add(1, Ordering::Relaxed);
347        let token = self.next_send_token;
348        self.next_send_token = self.next_send_token.wrapping_add(1);
349        let request = JsonRpcRequest::new(id, method, params);
350        let payload = match serde_json::to_string(&request) {
351            Ok(p) => p,
352            Err(e) => {
353                let _ = response_tx.send(Err(DeriveWsError::Serde(e)));
354                return;
355            }
356        };
357        self.pending
358            .insert(id, PendingRequest { token, response_tx });
359        log::debug!("Derive WebSocket sending `{method}` id={id}");
360        if let Err(e) = send_tx.send(SendCommand {
361            id,
362            token,
363            connection_epoch,
364            payload,
365        }) && self
366            .pending
367            .get(&id)
368            .is_some_and(|pending| pending.token == token)
369            && let Some(pending) = self.pending.remove(&id)
370        {
371            let _ = pending
372                .response_tx
373                .send(Err(DeriveWsError::transport(format!(
374                    "failed to queue WebSocket request: {e}",
375                ))));
376        }
377    }
378
379    fn start_send_worker(&mut self, client: Arc<WebSocketClient>) {
380        if !self.send_task.is_empty() {
381            log::error!("Cannot start Derive WebSocket send worker while the prior task is owned");
382            return;
383        }
384
385        let (send_tx, send_rx) = tokio::sync::mpsc::unbounded_channel();
386        let (failure_tx, failure_rx) = tokio::sync::mpsc::unbounded_channel();
387        self.send_tx = Some(send_tx);
388        self.send_failure_rx = failure_rx;
389
390        if let Err(e) = self
391            .send_task
392            .spawn(run_send_worker(client, send_rx, failure_tx))
393        {
394            self.send_tx.take();
395            log::error!("Failed to start Derive WebSocket send worker: {e}");
396        }
397    }
398
399    async fn stop_send_worker(&mut self) {
400        self.send_tx.take();
401
402        if let Some(outcome) = self
403            .send_task
404            .finish(Duration::from_secs(1), Duration::from_secs(2))
405            .await
406        {
407            match outcome {
408                TaskJoinOutcome::Completed(()) | TaskJoinOutcome::Aborted => {}
409                TaskJoinOutcome::Failed(e) => {
410                    log::error!("Derive WebSocket send worker failed: {e}");
411                }
412                TaskJoinOutcome::Incomplete => {
413                    log::error!("Derive WebSocket send worker did not stop after abort");
414                }
415            }
416        }
417    }
418
419    async fn restart_send_worker(&mut self, reason: &str) {
420        self.stop_send_worker().await;
421        self.fail_pending(reason);
422
423        if let Some(client) = self.client.clone() {
424            self.start_send_worker(client);
425        }
426    }
427
428    async fn shutdown_send_path(&mut self, reason: &str) {
429        self.stop_send_worker().await;
430        self.fail_pending(reason);
431    }
432
433    fn handle_send_failure(&mut self, failure: SendFailure) {
434        if !self
435            .pending
436            .get(&failure.id)
437            .is_some_and(|pending| pending.token == failure.token)
438        {
439            return;
440        }
441
442        if let Some(pending) = self.pending.remove(&failure.id) {
443            let _ = pending
444                .response_tx
445                .send(Err(DeriveWsError::transport(failure.reason)));
446        }
447    }
448
449    fn fail_uncorrelated_error(&mut self, error: crate::http::models::JsonRpcError) {
450        if self.pending.len() != 1 {
451            log::warn!(
452                "Derive WebSocket uncorrelated JSON-RPC error with {} pending requests: code={}, message={}",
453                self.pending.len(),
454                error.code,
455                error.message,
456            );
457            return;
458        }
459
460        if let Some((_, pending)) = self.pending.drain().next() {
461            let _ = pending.response_tx.send(Err(DeriveWsError::JsonRpc {
462                code: error.code,
463                message: error.message,
464                data: error.data,
465            }));
466        }
467    }
468
469    fn fail_pending(&mut self, reason: &str) {
470        if self.pending.is_empty() {
471            return;
472        }
473        log::debug!(
474            "Failing {} pending Derive WebSocket request(s): {reason}",
475            self.pending.len(),
476        );
477
478        for (_, pending) in self.pending.drain() {
479            let _ = pending
480                .response_tx
481                .send(Err(DeriveWsError::transport(reason.to_string())));
482        }
483    }
484}
485
486impl Drop for FeedHandler {
487    fn drop(&mut self) {
488        self.send_tx.take();
489        self.send_task.abort();
490    }
491}
492
493async fn run_send_worker(
494    client: Arc<WebSocketClient>,
495    mut send_rx: tokio::sync::mpsc::UnboundedReceiver<SendCommand>,
496    failure_tx: tokio::sync::mpsc::UnboundedSender<SendFailure>,
497) {
498    while let Some(command) = send_rx.recv().await {
499        if let Err(e) = client
500            .send_text_on_connection(command.payload, None, command.connection_epoch)
501            .await
502            && failure_tx
503                .send(SendFailure {
504                    id: command.id,
505                    token: command.token,
506                    reason: e.to_string(),
507                })
508                .is_err()
509        {
510            return;
511        }
512    }
513}
514
515/// Builds `subscribe` params from a single channel topic.
516#[must_use]
517pub(super) fn subscribe_params(channel: DeriveWsChannel) -> WsSubscribeParams {
518    WsSubscribeParams {
519        channels: vec![channel],
520    }
521}
522
523/// Convenience wrapper that produces the `subscribe` params for the
524/// `ticker_slim.{instrument_name}.{interval}` channel.
525#[must_use]
526pub(super) fn ticker_subscribe_params(instrument_name: &str, interval: &str) -> WsSubscribeParams {
527    subscribe_params(DeriveWsChannel::ticker_slim(instrument_name, interval))
528}
529
530/// Convenience wrapper that produces the `subscribe` params for the
531/// `orderbook.{instrument_name}.{group}.{depth}` channel.
532#[must_use]
533pub(super) fn orderbook_subscribe_params(
534    instrument_name: &str,
535    group: &str,
536    depth: &str,
537) -> WsSubscribeParams {
538    subscribe_params(DeriveWsChannel::orderbook(instrument_name, group, depth))
539}
540
541/// Convenience wrapper that produces the `subscribe` params for the
542/// `trades.{instrument_type}.{currency}` channel.
543#[must_use]
544pub(super) fn trades_subscribe_params(instrument_type: &str, currency: &str) -> WsSubscribeParams {
545    subscribe_params(DeriveWsChannel::trades(instrument_type, currency))
546}
547
548#[cfg(test)]
549mod tests {
550    use rstest::rstest;
551    use serde_json::json;
552
553    use super::*;
554
555    fn unauthenticated_epoch() -> Arc<AtomicU64> {
556        Arc::new(AtomicU64::new(u64::MAX))
557    }
558
559    fn feed_handler(
560        signal: Arc<AtomicBool>,
561        cmd_rx: tokio::sync::mpsc::UnboundedReceiver<HandlerCommand>,
562        raw_rx: tokio::sync::mpsc::UnboundedReceiver<Message>,
563        next_id: Arc<AtomicU64>,
564        auth_tracker: AuthTracker,
565        authenticated_epoch: Arc<AtomicU64>,
566    ) -> FeedHandler {
567        FeedHandler::new_with_send_task(
568            signal,
569            cmd_rx,
570            raw_rx,
571            next_id,
572            auth_tracker,
573            authenticated_epoch,
574            Arc::new(SharedTaskSlot::new()),
575        )
576    }
577
578    #[rstest]
579    fn test_subscribe_params_carries_single_channel() {
580        let params = subscribe_params(DeriveWsChannel::ticker_slim("ETH-PERP", "1000"));
581        assert_eq!(
582            params.channels,
583            vec![DeriveWsChannel::ticker_slim("ETH-PERP", "1000")],
584        );
585    }
586
587    #[rstest]
588    fn test_ticker_subscribe_params_formats_topic() {
589        let params = ticker_subscribe_params("ETH-PERP", "1000");
590        assert_eq!(
591            params.channels,
592            vec![DeriveWsChannel::ticker_slim("ETH-PERP", "1000")],
593        );
594    }
595
596    #[rstest]
597    fn test_orderbook_subscribe_params_formats_topic() {
598        let params = orderbook_subscribe_params("ETH-PERP", "1", "10");
599        assert_eq!(
600            params.channels,
601            vec![DeriveWsChannel::orderbook("ETH-PERP", "1", "10")],
602        );
603    }
604
605    #[rstest]
606    fn test_trades_subscribe_params_formats_topic() {
607        let params = trades_subscribe_params("perp", "ETH");
608        assert_eq!(
609            params.channels,
610            vec![DeriveWsChannel::trades("perp", "ETH")],
611        );
612    }
613
614    #[rstest]
615    #[tokio::test]
616    async fn test_dispatch_request_without_client_returns_not_connected() {
617        // Requests issued before SetClient must fail fast rather than hang.
618        let signal = Arc::new(AtomicBool::new(false));
619        let (_cmd_tx, cmd_rx) = tokio::sync::mpsc::unbounded_channel();
620        let (_raw_tx, raw_rx) = tokio::sync::mpsc::unbounded_channel();
621        let next_id = Arc::new(AtomicU64::new(1));
622        let auth_tracker = AuthTracker::new();
623        let mut handler = feed_handler(
624            signal,
625            cmd_rx,
626            raw_rx,
627            next_id,
628            auth_tracker,
629            unauthenticated_epoch(),
630        );
631
632        let (response_tx, response_rx) = tokio::sync::oneshot::channel();
633        let params = serde_json::to_value(WsSubscribeParams { channels: vec![] }).unwrap();
634        handler.dispatch_request("public/login", params, None, response_tx);
635
636        let outcome = response_rx.await.expect("oneshot resolved");
637        match outcome {
638            Err(DeriveWsError::NotConnected) => {}
639            other => panic!("expected NotConnected, was {other:?}"),
640        }
641    }
642
643    #[rstest]
644    #[tokio::test]
645    async fn test_dispatch_registers_pending_requests_before_ordered_queueing() {
646        let signal = Arc::new(AtomicBool::new(false));
647        let (_cmd_tx, cmd_rx) = tokio::sync::mpsc::unbounded_channel();
648        let (_raw_tx, raw_rx) = tokio::sync::mpsc::unbounded_channel();
649        let next_id = Arc::new(AtomicU64::new(1));
650        let auth_tracker = AuthTracker::new();
651        let mut handler = feed_handler(
652            signal,
653            cmd_rx,
654            raw_rx,
655            next_id,
656            auth_tracker,
657            unauthenticated_epoch(),
658        );
659        let (send_tx, mut send_rx) = tokio::sync::mpsc::unbounded_channel();
660        handler.send_tx = Some(send_tx);
661
662        let (first_tx, _first_rx) = tokio::sync::oneshot::channel();
663        let (second_tx, _second_rx) = tokio::sync::oneshot::channel();
664        handler.enqueue_request("first", json!({"sequence": 1}), 0, first_tx);
665        handler.enqueue_request("second", json!({"sequence": 2}), 0, second_tx);
666
667        let first = send_rx.recv().await.expect("first queued send");
668        let second = send_rx.recv().await.expect("second queued send");
669        let first_payload: Value = serde_json::from_str(&first.payload).unwrap();
670        let second_payload: Value = serde_json::from_str(&second.payload).unwrap();
671
672        assert_eq!(first.id, 1);
673        assert_eq!(second.id, 2);
674        assert_eq!(first_payload["method"], "first");
675        assert_eq!(second_payload["method"], "second");
676        assert_eq!(handler.pending.get(&first.id).unwrap().token, first.token);
677        assert_eq!(handler.pending.get(&second.id).unwrap().token, second.token);
678    }
679
680    #[rstest]
681    #[tokio::test]
682    async fn test_late_send_failure_does_not_remove_reused_request_id() {
683        let signal = Arc::new(AtomicBool::new(false));
684        let (_cmd_tx, cmd_rx) = tokio::sync::mpsc::unbounded_channel();
685        let (_raw_tx, raw_rx) = tokio::sync::mpsc::unbounded_channel();
686        let next_id = Arc::new(AtomicU64::new(1));
687        let auth_tracker = AuthTracker::new();
688        let mut handler = feed_handler(
689            signal,
690            cmd_rx,
691            raw_rx,
692            Arc::clone(&next_id),
693            auth_tracker,
694            unauthenticated_epoch(),
695        );
696        let (send_tx, mut send_rx) = tokio::sync::mpsc::unbounded_channel();
697        handler.send_tx = Some(send_tx);
698
699        let (old_tx, old_rx) = tokio::sync::oneshot::channel();
700        handler.enqueue_request("first", json!({}), 0, old_tx);
701        let old_send = send_rx.recv().await.expect("old queued send");
702        let old_pending = handler.pending.remove(&old_send.id).unwrap();
703        old_pending.response_tx.send(Ok(Value::Null)).unwrap();
704        old_rx.await.unwrap().unwrap();
705
706        next_id.store(old_send.id, Ordering::Relaxed);
707        let (new_tx, new_rx) = tokio::sync::oneshot::channel();
708        handler.enqueue_request("second", json!({}), 0, new_tx);
709        let new_send = send_rx.recv().await.expect("new queued send");
710        handler.handle_send_failure(SendFailure {
711            id: old_send.id,
712            token: old_send.token,
713            reason: "late failure".to_string(),
714        });
715
716        assert_eq!(old_send.id, new_send.id);
717        assert_ne!(old_send.token, new_send.token);
718        assert_eq!(
719            handler.pending.get(&new_send.id).unwrap().token,
720            new_send.token,
721        );
722
723        handler.handle_send_failure(SendFailure {
724            id: new_send.id,
725            token: new_send.token,
726            reason: "current failure".to_string(),
727        });
728        let error = new_rx.await.unwrap().expect_err("current request failed");
729        assert!(error.to_string().contains("current failure"));
730    }
731
732    #[rstest]
733    #[tokio::test]
734    async fn test_shutdown_aborts_send_worker_and_drains_pending_requests() {
735        let signal = Arc::new(AtomicBool::new(false));
736        let (_cmd_tx, cmd_rx) = tokio::sync::mpsc::unbounded_channel();
737        let (_raw_tx, raw_rx) = tokio::sync::mpsc::unbounded_channel();
738        let next_id = Arc::new(AtomicU64::new(1));
739        let auth_tracker = AuthTracker::new();
740        let mut handler = feed_handler(
741            signal,
742            cmd_rx,
743            raw_rx,
744            next_id,
745            auth_tracker,
746            unauthenticated_epoch(),
747        );
748        let (send_tx, _send_rx) = tokio::sync::mpsc::unbounded_channel();
749        handler.send_tx = Some(send_tx);
750
751        let task = tokio::spawn(std::future::pending::<()>());
752        let abort_handle = task.abort_handle();
753        handler.send_task.insert(task);
754
755        let (first_tx, first_rx) = tokio::sync::oneshot::channel();
756        let (second_tx, second_rx) = tokio::sync::oneshot::channel();
757        handler.enqueue_request("first", json!({}), 0, first_tx);
758        handler.enqueue_request("second", json!({}), 0, second_tx);
759        handler.shutdown_send_path("disconnect requested").await;
760        tokio::task::yield_now().await;
761
762        let first_error = first_rx.await.unwrap().expect_err("first request failed");
763        let second_error = second_rx.await.unwrap().expect_err("second request failed");
764        assert!(abort_handle.is_finished());
765        assert!(handler.send_tx.is_none());
766        assert!(handler.send_task.is_empty());
767        assert!(handler.pending.is_empty());
768        assert!(first_error.to_string().contains("disconnect requested"));
769        assert!(second_error.to_string().contains("disconnect requested"));
770    }
771
772    #[rstest]
773    #[tokio::test]
774    async fn test_next_stops_when_input_channels_close_with_failure_channel_open() {
775        let signal = Arc::new(AtomicBool::new(false));
776        let (cmd_tx, cmd_rx) = tokio::sync::mpsc::unbounded_channel();
777        let (raw_tx, raw_rx) = tokio::sync::mpsc::unbounded_channel();
778        let next_id = Arc::new(AtomicU64::new(1));
779        let auth_tracker = AuthTracker::new();
780        let mut handler = feed_handler(
781            signal,
782            cmd_rx,
783            raw_rx,
784            next_id,
785            auth_tracker,
786            unauthenticated_epoch(),
787        );
788        let (_failure_tx, failure_rx) = tokio::sync::mpsc::unbounded_channel();
789        handler.send_failure_rx = failure_rx;
790        drop(cmd_tx);
791        drop(raw_tx);
792
793        let outcome = tokio::time::timeout(std::time::Duration::from_millis(100), handler.next())
794            .await
795            .expect("handler stopped after both input channels closed");
796
797        assert!(outcome.is_none());
798    }
799
800    #[rstest]
801    #[tokio::test]
802    async fn test_uncorrelated_error_fails_only_pending_request() {
803        let signal = Arc::new(AtomicBool::new(false));
804        let (_cmd_tx, cmd_rx) = tokio::sync::mpsc::unbounded_channel();
805        let (_raw_tx, raw_rx) = tokio::sync::mpsc::unbounded_channel();
806        let next_id = Arc::new(AtomicU64::new(1));
807        let auth_tracker = AuthTracker::new();
808        let mut handler = feed_handler(
809            signal,
810            cmd_rx,
811            raw_rx,
812            next_id,
813            auth_tracker,
814            unauthenticated_epoch(),
815        );
816        let (send_tx, _send_rx) = tokio::sync::mpsc::unbounded_channel();
817        handler.send_tx = Some(send_tx);
818        let (response_tx, response_rx) = tokio::sync::oneshot::channel();
819        handler.enqueue_request("request", json!({}), 0, response_tx);
820
821        handler.fail_uncorrelated_error(crate::http::models::JsonRpcError {
822            code: -32700,
823            message: "Parse error".to_string(),
824            data: Some(json!("invalid JSON")),
825        });
826        let error = response_rx.await.unwrap().expect_err("request failed");
827
828        match error {
829            DeriveWsError::JsonRpc {
830                code,
831                message,
832                data,
833            } => {
834                assert_eq!(code, -32700);
835                assert_eq!(message, "Parse error");
836                assert_eq!(data, Some(json!("invalid JSON")));
837            }
838            other => panic!("expected JsonRpc, was {other:?}"),
839        }
840        assert!(handler.pending.is_empty());
841    }
842
843    #[rstest]
844    #[tokio::test]
845    async fn test_uncorrelated_error_does_not_guess_between_pending_requests() {
846        let signal = Arc::new(AtomicBool::new(false));
847        let (_cmd_tx, cmd_rx) = tokio::sync::mpsc::unbounded_channel();
848        let (_raw_tx, raw_rx) = tokio::sync::mpsc::unbounded_channel();
849        let next_id = Arc::new(AtomicU64::new(1));
850        let auth_tracker = AuthTracker::new();
851        let mut handler = feed_handler(
852            signal,
853            cmd_rx,
854            raw_rx,
855            next_id,
856            auth_tracker,
857            unauthenticated_epoch(),
858        );
859        let (send_tx, _send_rx) = tokio::sync::mpsc::unbounded_channel();
860        handler.send_tx = Some(send_tx);
861        let (first_tx, first_rx) = tokio::sync::oneshot::channel();
862        let (second_tx, second_rx) = tokio::sync::oneshot::channel();
863        handler.enqueue_request("first", json!({}), 0, first_tx);
864        handler.enqueue_request("second", json!({}), 0, second_tx);
865
866        handler.fail_uncorrelated_error(crate::http::models::JsonRpcError {
867            code: -32600,
868            message: "Invalid Request".to_string(),
869            data: None,
870        });
871
872        assert_eq!(handler.pending.len(), 2);
873        handler.fail_pending("test cleanup");
874        assert!(first_rx.await.unwrap().is_err());
875        assert!(second_rx.await.unwrap().is_err());
876    }
877}