11use std:: collections:: HashMap ;
22use std:: sync:: Arc ;
3- use tokio:: sync:: RwLock ;
3+ use tokio:: sync:: { broadcast , RwLock } ;
44
55use crate :: core:: reconciler:: Reconciler ;
66use crate :: core:: runtime:: Runtime ;
@@ -15,34 +15,39 @@ pub struct AppSession {
1515}
1616
1717/// Manages per-connection AppSessions, keyed by connection ID.
18- /// Tracks active connections and creates isolated sessions on demand.
18+ /// Stores `Arc<RwLock<AppSession>>` references so both the store and handlers
19+ /// share ownership, enabling admin/monitoring, graceful shutdown, and timeout enforcement.
1920pub struct AppSessionStore {
20- sessions : RwLock < HashMap < String , ( ) > > ,
21+ sessions : RwLock < HashMap < String , Arc < RwLock < AppSession > > > > ,
2122 root_factory : Arc < dyn Fn ( ) -> Box < dyn View > + Send + Sync > ,
23+ shutdown_tx : broadcast:: Sender < ( ) > ,
2224}
2325
2426impl AppSessionStore {
2527 pub fn new ( root_factory : Arc < dyn Fn ( ) -> Box < dyn View > + Send + Sync > ) -> Self {
28+ let ( shutdown_tx, _) = broadcast:: channel ( 16 ) ;
2629 AppSessionStore {
2730 sessions : RwLock :: new ( HashMap :: new ( ) ) ,
2831 root_factory,
32+ shutdown_tx,
2933 }
3034 }
3135
3236 /// Create a new session with an isolated Runtime and Reconciler.
33- /// Registers the connection and returns the session for the handler to own .
34- pub async fn create_session ( & self , connection_id : String ) -> AppSession {
37+ /// Registers the connection and returns an Arc reference to the session .
38+ pub async fn create_session ( & self , connection_id : String ) -> Arc < RwLock < AppSession > > {
3539 let view = ( self . root_factory ) ( ) ;
3640 let runtime = Runtime :: new ( FuncView ( view) ) ;
3741 let reconciler = Reconciler :: new ( ) ;
42+ let session = Arc :: new ( RwLock :: new ( AppSession {
43+ runtime,
44+ reconciler,
45+ } ) ) ;
3846
3947 let mut sessions = self . sessions . write ( ) . await ;
40- sessions. insert ( connection_id, ( ) ) ;
48+ sessions. insert ( connection_id, session . clone ( ) ) ;
4149
42- AppSession {
43- runtime,
44- reconciler,
45- }
50+ session
4651 }
4752
4853 /// Remove a session on disconnect.
@@ -56,6 +61,28 @@ impl AppSessionStore {
5661 let sessions = self . sessions . read ( ) . await ;
5762 sessions. len ( )
5863 }
64+
65+ /// Get a session by connection ID (for admin/monitoring).
66+ pub async fn get_session ( & self , connection_id : & str ) -> Option < Arc < RwLock < AppSession > > > {
67+ let sessions = self . sessions . read ( ) . await ;
68+ sessions. get ( connection_id) . cloned ( )
69+ }
70+
71+ /// Get all active connection IDs (for monitoring/debug).
72+ pub async fn connection_ids ( & self ) -> Vec < String > {
73+ let sessions = self . sessions . read ( ) . await ;
74+ sessions. keys ( ) . cloned ( ) . collect ( )
75+ }
76+
77+ /// Subscribe to the shutdown broadcast channel.
78+ pub fn subscribe_shutdown ( & self ) -> broadcast:: Receiver < ( ) > {
79+ self . shutdown_tx . subscribe ( )
80+ }
81+
82+ /// Broadcast a shutdown signal to all subscribers.
83+ pub fn broadcast_shutdown ( & self ) {
84+ let _ = self . shutdown_tx . send ( ( ) ) ;
85+ }
5986}
6087
6188#[ cfg( test) ]
@@ -110,12 +137,12 @@ mod tests {
110137 } ) ) ;
111138
112139 // Create two sessions — each gets its own Runtime with a different view
113- let mut session_a = store. create_session ( "conn-a" . to_string ( ) ) . await ;
114- let mut session_b = store. create_session ( "conn-b" . to_string ( ) ) . await ;
140+ let session_a = store. create_session ( "conn-a" . to_string ( ) ) . await ;
141+ let session_b = store. create_session ( "conn-b" . to_string ( ) ) . await ;
115142
116143 // Build each session's tree independently
117- let tree_a = session_a. runtime . build ( ) . await ;
118- let tree_b = session_b. runtime . build ( ) . await ;
144+ let tree_a = session_a. write ( ) . await . runtime . build ( ) . await ;
145+ let tree_b = session_b. write ( ) . await . runtime . build ( ) . await ;
119146
120147 let json_a = serde_json:: to_value ( & tree_a) . unwrap ( ) . to_string ( ) ;
121148 let json_b = serde_json:: to_value ( & tree_b) . unwrap ( ) . to_string ( ) ;
@@ -144,9 +171,9 @@ mod tests {
144171 let store = store. clone ( ) ;
145172 let handle = tokio:: spawn ( async move {
146173 let id = format ! ( "conn-{}" , i) ;
147- let mut session = store. create_session ( id. clone ( ) ) . await ;
174+ let session = store. create_session ( id. clone ( ) ) . await ;
148175 // Verify we got a valid session by building its tree
149- let tree = session. runtime . build ( ) . await ;
176+ let tree = session. write ( ) . await . runtime . build ( ) . await ;
150177 let json = serde_json:: to_value ( & tree) . unwrap ( ) . to_string ( ) ;
151178 assert ! ( json. contains( "concurrent" ) ) ;
152179 store. remove_session ( & id) . await ;
@@ -160,4 +187,74 @@ mod tests {
160187
161188 assert_eq ! ( store. session_count( ) . await , 0 ) ;
162189 }
190+
191+ #[ tokio:: test]
192+ async fn test_get_session ( ) {
193+ let store = AppSessionStore :: new ( Arc :: new ( || Box :: new ( TestView :: new ( "get-test" ) ) ) ) ;
194+
195+ store. create_session ( "conn-1" . to_string ( ) ) . await ;
196+
197+ // Should return Some for an active session
198+ assert ! ( store. get_session( "conn-1" ) . await . is_some( ) ) ;
199+
200+ // Should return None for a non-existent session
201+ assert ! ( store. get_session( "conn-999" ) . await . is_none( ) ) ;
202+
203+ // Should return None after removal
204+ store. remove_session ( "conn-1" ) . await ;
205+ assert ! ( store. get_session( "conn-1" ) . await . is_none( ) ) ;
206+ }
207+
208+ #[ tokio:: test]
209+ async fn test_connection_ids ( ) {
210+ let store = AppSessionStore :: new ( Arc :: new ( || Box :: new ( TestView :: new ( "ids-test" ) ) ) ) ;
211+
212+ store. create_session ( "conn-a" . to_string ( ) ) . await ;
213+ store. create_session ( "conn-b" . to_string ( ) ) . await ;
214+ store. create_session ( "conn-c" . to_string ( ) ) . await ;
215+
216+ let mut ids = store. connection_ids ( ) . await ;
217+ ids. sort ( ) ;
218+ assert_eq ! ( ids, vec![ "conn-a" , "conn-b" , "conn-c" ] ) ;
219+
220+ store. remove_session ( "conn-b" ) . await ;
221+ let mut ids = store. connection_ids ( ) . await ;
222+ ids. sort ( ) ;
223+ assert_eq ! ( ids, vec![ "conn-a" , "conn-c" ] ) ;
224+ }
225+
226+ #[ tokio:: test]
227+ async fn test_broadcast_shutdown ( ) {
228+ let store = AppSessionStore :: new ( Arc :: new ( || Box :: new ( TestView :: new ( "shutdown-test" ) ) ) ) ;
229+
230+ let mut rx1 = store. subscribe_shutdown ( ) ;
231+ let mut rx2 = store. subscribe_shutdown ( ) ;
232+ let mut rx3 = store. subscribe_shutdown ( ) ;
233+
234+ store. broadcast_shutdown ( ) ;
235+
236+ // All receivers should get the signal
237+ assert ! ( rx1. recv( ) . await . is_ok( ) ) ;
238+ assert ! ( rx2. recv( ) . await . is_ok( ) ) ;
239+ assert ! ( rx3. recv( ) . await . is_ok( ) ) ;
240+ }
241+
242+ #[ tokio:: test]
243+ async fn test_session_arc_lifecycle ( ) {
244+ let store = AppSessionStore :: new ( Arc :: new ( || Box :: new ( TestView :: new ( "lifecycle-test" ) ) ) ) ;
245+
246+ let session_arc = store. create_session ( "conn-1" . to_string ( ) ) . await ;
247+
248+ // Handler holds a clone — simulates what handle_socket does
249+ let handler_clone = session_arc. clone ( ) ;
250+
251+ // Remove from store — store's reference is dropped
252+ store. remove_session ( "conn-1" ) . await ;
253+ assert ! ( store. get_session( "conn-1" ) . await . is_none( ) ) ;
254+
255+ // Handler's clone is still valid and usable
256+ let tree = handler_clone. write ( ) . await . runtime . build ( ) . await ;
257+ let json = serde_json:: to_value ( & tree) . unwrap ( ) . to_string ( ) ;
258+ assert ! ( json. contains( "lifecycle-test" ) ) ;
259+ }
163260}
0 commit comments