@@ -9,6 +9,7 @@ use std::time::Duration;
99use axum:: extract:: ws:: { Message as WsMessage , WebSocket } ;
1010use futures_util:: { Sink , SinkExt , StreamExt } ;
1111use tokio:: sync:: { mpsc, Mutex , RwLock } ;
12+ use tokio:: task:: JoinSet ;
1213use tokio_util:: sync:: CancellationToken ;
1314use tracing:: Instrument as _;
1415use tracing:: { debug, info, trace, warn} ;
@@ -29,6 +30,37 @@ const AUTH_TIMEOUT: Duration = Duration::from_secs(5);
2930/// Shared mutable subscription map for a single WebSocket connection.
3031pub ( crate ) type ConnectionSubscriptions = Arc < Mutex < HashMap < String , Vec < Filter > > > > ;
3132
33+ /// A cancellable lease for one in-flight REQ. The lease is installed before
34+ /// the handler task starts so CLOSE can invalidate a subscription that has not
35+ /// reached registration yet.
36+ #[ derive( Debug ) ]
37+ pub ( crate ) struct PendingSubscription {
38+ cancel : CancellationToken ,
39+ }
40+
41+ impl PendingSubscription {
42+ fn new ( ) -> Self {
43+ Self {
44+ cancel : CancellationToken :: new ( ) ,
45+ }
46+ }
47+
48+ pub ( crate ) fn cancel ( & self ) {
49+ self . cancel . cancel ( ) ;
50+ }
51+
52+ pub ( crate ) fn is_cancelled ( & self ) -> bool {
53+ self . cancel . is_cancelled ( )
54+ }
55+
56+ pub ( crate ) async fn cancelled ( & self ) {
57+ self . cancel . cancelled ( ) . await ;
58+ }
59+ }
60+
61+ /// In-flight REQs keyed by client-supplied subscription ID.
62+ pub ( crate ) type PendingSubscriptions = Arc < Mutex < HashMap < String , Arc < PendingSubscription > > > > ;
63+
3264/// Maximum outbound data frames buffered into the websocket sink before one flush.
3365const MAX_WS_SEND_BATCH : usize = 64 ;
3466
@@ -63,6 +95,8 @@ pub struct ConnectionState {
6395 pub auth_state : RwLock < AuthState > ,
6496 /// Active subscriptions keyed by subscription ID.
6597 pub subscriptions : ConnectionSubscriptions ,
98+ /// REQs that have started but may not yet be registered for fan-out.
99+ pub ( crate ) pending_subscriptions : PendingSubscriptions ,
66100 /// Sender for outbound data messages (EVENT, NOTICE, OK, etc.).
67101 pub send_tx : mpsc:: Sender < WsMessage > ,
68102 /// Sender for outbound control frames (Pong, Close).
@@ -80,6 +114,47 @@ pub struct ConnectionState {
80114}
81115
82116impl ConnectionState {
117+ /// Install a pending REQ before its handler task starts. A repeated REQ
118+ /// with the same subscription ID supersedes and cancels the older task.
119+ pub ( crate ) async fn begin_pending_subscription (
120+ & self ,
121+ sub_id : & str ,
122+ ) -> Arc < PendingSubscription > {
123+ let pending = Arc :: new ( PendingSubscription :: new ( ) ) ;
124+ if let Some ( replaced) = self
125+ . pending_subscriptions
126+ . lock ( )
127+ . await
128+ . insert ( sub_id. to_owned ( ) , Arc :: clone ( & pending) )
129+ {
130+ replaced. cancel ( ) ;
131+ }
132+ pending
133+ }
134+
135+ /// Cancel every in-flight REQ before connection registry cleanup begins.
136+ async fn cancel_all_pending_subscriptions ( & self ) {
137+ let mut pending = self . pending_subscriptions . lock ( ) . await ;
138+ for ( _, request) in pending. drain ( ) {
139+ request. cancel ( ) ;
140+ }
141+ }
142+
143+ /// Forget a completed REQ only if it still owns this subscription ID.
144+ async fn finish_pending_subscription (
145+ & self ,
146+ sub_id : & str ,
147+ completed : & Arc < PendingSubscription > ,
148+ ) {
149+ let mut pending = self . pending_subscriptions . lock ( ) . await ;
150+ if pending
151+ . get ( sub_id)
152+ . is_some_and ( |current| Arc :: ptr_eq ( current, completed) )
153+ {
154+ pending. remove ( sub_id) ;
155+ }
156+ }
157+
83158 /// Sends a data message to this connection's outbound channel.
84159 ///
85160 /// On a full buffer, increments the backpressure counter. The first
@@ -163,6 +238,7 @@ async fn handle_active_connection(
163238
164239 let backpressure_count = Arc :: new ( AtomicU8 :: new ( 0 ) ) ;
165240 let subscriptions = Arc :: new ( Mutex :: new ( HashMap :: new ( ) ) ) ;
241+ let pending_subscriptions = Arc :: new ( Mutex :: new ( HashMap :: new ( ) ) ) ;
166242
167243 let conn = Arc :: new ( ConnectionState {
168244 conn_id,
@@ -172,6 +248,7 @@ async fn handle_active_connection(
172248 challenge : challenge. clone ( ) ,
173249 } ) ,
174250 subscriptions : Arc :: clone ( & subscriptions) ,
251+ pending_subscriptions,
175252 send_tx : tx. clone ( ) ,
176253 ctrl_tx : ctrl_tx. clone ( ) ,
177254 cancel : cancel. clone ( ) ,
@@ -412,6 +489,8 @@ async fn recv_loop(
412489 missed_pongs : Arc < AtomicU8 > ,
413490 cancel : CancellationToken ,
414491) {
492+ let mut req_tasks = JoinSet :: new ( ) ;
493+
415494 loop {
416495 tokio:: select! {
417496 msg = ws_recv. next( ) => {
@@ -433,7 +512,12 @@ async fn recv_loop(
433512 break ;
434513 }
435514 trace!( len = text. len( ) , "frame received" ) ;
436- handle_text_message( text. to_string( ) , Arc :: clone( & conn) , Arc :: clone( & state) ) . await ;
515+ handle_text_message(
516+ text. to_string( ) ,
517+ Arc :: clone( & conn) ,
518+ Arc :: clone( & state) ,
519+ & mut req_tasks,
520+ ) . await ;
437521 }
438522 Some ( Ok ( WsMessage :: Binary ( bytes) ) ) => {
439523 let max_frame_bytes = state. config. max_frame_bytes;
@@ -455,7 +539,12 @@ async fn recv_loop(
455539 // (notably certain Nostr libraries) send text payloads in binary frames.
456540 // NIP-01 is text-only, but accepting binary is a common relay extension.
457541 if let Ok ( text) = String :: from_utf8( bytes. to_vec( ) ) {
458- handle_text_message( text, Arc :: clone( & conn) , Arc :: clone( & state) ) . await ;
542+ handle_text_message(
543+ text,
544+ Arc :: clone( & conn) ,
545+ Arc :: clone( & state) ,
546+ & mut req_tasks,
547+ ) . await ;
459548 }
460549 }
461550 Some ( Ok ( WsMessage :: Pong ( _) ) ) => {
@@ -481,12 +570,31 @@ async fn recv_loop(
481570 }
482571 }
483572 }
573+ completed = req_tasks. join_next( ) , if !req_tasks. is_empty( ) => {
574+ if let Some ( Err ( error) ) = completed {
575+ debug!( conn_id = %conn. conn_id, %error, "REQ handler task failed" ) ;
576+ }
577+ }
484578 _ = cancel. cancelled( ) => break ,
485579 }
486580 }
581+
582+ shutdown_pending_requests ( & conn, & mut req_tasks) . await ;
583+ }
584+
585+ pub ( crate ) async fn shutdown_pending_requests ( conn : & ConnectionState , req_tasks : & mut JoinSet < ( ) > ) {
586+ conn. cancel . cancel ( ) ;
587+ conn. cancel_all_pending_subscriptions ( ) . await ;
588+ req_tasks. abort_all ( ) ;
589+ while req_tasks. join_next ( ) . await . is_some ( ) { }
487590}
488591
489- async fn handle_text_message ( text : String , conn : Arc < ConnectionState > , state : Arc < AppState > ) {
592+ async fn handle_text_message (
593+ text : String ,
594+ conn : Arc < ConnectionState > ,
595+ state : Arc < AppState > ,
596+ req_tasks : & mut JoinSet < ( ) > ,
597+ ) {
490598 let msg = match ClientMessage :: parse ( & text) {
491599 Ok ( m) => m,
492600 Err ( e) => {
@@ -548,10 +656,22 @@ async fn handle_text_message(text: String, conn: Arc<ConnectionState>, state: Ar
548656 return ;
549657 }
550658 } ;
659+ let pending = conn. begin_pending_subscription ( & sub_id) . await ;
551660 let span = tracing:: info_span!( "ws.req" , conn_id = %conn. conn_id, sub_id = %sub_id) ;
552- tokio :: spawn (
661+ req_tasks . spawn (
553662 async move {
554- handlers:: req:: handle_req ( sub_id, filters, conn, state) . await ;
663+ tokio:: select! {
664+ biased;
665+ _ = pending. cancelled( ) => { }
666+ _ = handlers:: req:: handle_req(
667+ sub_id. clone( ) ,
668+ filters,
669+ Arc :: clone( & conn) ,
670+ state,
671+ Arc :: clone( & pending) ,
672+ ) => { }
673+ }
674+ conn. finish_pending_subscription ( & sub_id, & pending) . await ;
555675 drop ( permit) ;
556676 }
557677 . instrument ( span) ,
0 commit comments