@@ -9,6 +9,10 @@ var SignalROpenApiPlugin = function (system) {
99 // Active hub connections keyed by hub path (e.g. "/hubs/chat")
1010 var _hubs = { } ;
1111
12+ // Auth fingerprint at connection time, keyed by hub path.
13+ // Used to detect credential changes so the connection can be recycled.
14+ var _hubAuthFingerprints = { } ;
15+
1216 // Active stream subscriptions keyed by operation path (e.g. "/hubs/Chat/Countdown")
1317 var _streams = { } ;
1418
@@ -101,6 +105,26 @@ var SignalROpenApiPlugin = function (system) {
101105 return headers ;
102106 } ;
103107
108+ // Compute a fingerprint of the current auth state (token + apiKey headers).
109+ // Returns a stable string that changes whenever credentials change.
110+ var _computeAuthFingerprint = function ( ) {
111+ var parts = [ ] ;
112+ var token = _getAccessToken ( ) ;
113+ if ( token ) {
114+ parts . push ( "token:" + token ) ;
115+ }
116+
117+ var apiKeys = _getApiKeyHeaders ( ) ;
118+ if ( apiKeys ) {
119+ var sortedKeys = Object . keys ( apiKeys ) . sort ( ) ;
120+ for ( var i = 0 ; i < sortedKeys . length ; i ++ ) {
121+ parts . push ( "apiKey:" + sortedKeys [ i ] + "=" + apiKeys [ sortedKeys [ i ] ] ) ;
122+ }
123+ }
124+
125+ return parts . join ( "|" ) ;
126+ } ;
127+
104128 // Subscribe to all client events on a hub connection
105129 var _subscribeClientEvents = function ( hubPath , hub ) {
106130 var specJson = system . specSelectors . specJson ( ) . toJS ( ) ;
@@ -224,8 +248,62 @@ var SignalROpenApiPlugin = function (system) {
224248 } ) ;
225249 } ;
226250
227- // Get or create a HubConnection for the given hub path
251+ // Disconnect and clean up a hub connection.
252+ // Cancels active streams, removes the cached connection and fingerprint,
253+ // and notifies UI listeners so components re-render.
254+ var _disconnectHub = function ( hubPath ) {
255+ // Cancel all active streams on this hub
256+ Object . keys ( _streams ) . forEach ( function ( streamPath ) {
257+ var specIm = system . specSelectors . specJson ( ) ;
258+ var ext = specIm . getIn ( [ "paths" , streamPath , "post" , "x-signalr" ] ) ;
259+ if ( ! ext ) {
260+ return ;
261+ }
262+
263+ var streamHubPath = ext . get ( "hubPath" ) || ( "/" + ext . get ( "hub" ) . toLowerCase ( ) ) ;
264+ if ( streamHubPath === hubPath && _streams [ streamPath ] ) {
265+ _streams [ streamPath ] . dispose ( ) ;
266+ delete _streams [ streamPath ] ;
267+ delete _streamItems [ streamPath ] ;
268+ }
269+ } ) ;
270+
271+ var hub = _hubs [ hubPath ] ;
272+ delete _hubs [ hubPath ] ;
273+ delete _hubAuthFingerprints [ hubPath ] ;
274+
275+ var _notifyListeners = function ( ) {
276+ if ( _eventListeners [ hubPath ] ) {
277+ _eventListeners [ hubPath ] . forEach ( function ( fn ) { fn ( ) ; } ) ;
278+ }
279+ } ;
280+
281+ if ( hub ) {
282+ return hub . stop ( ) . then ( _notifyListeners ) . catch ( function ( err ) {
283+ console . error ( "[SignalR OpenAPI] Disconnect error:" , err ) ;
284+ _notifyListeners ( ) ;
285+ } ) ;
286+ }
287+
288+ _notifyListeners ( ) ;
289+ return Promise . resolve ( ) ;
290+ } ;
291+
292+ // Get or create a HubConnection for the given hub path.
293+ // If auth credentials changed since the connection was established,
294+ // the existing connection is torn down and a fresh one is created.
228295 var _getOrCreateHub = function ( hubPath ) {
296+ var currentFingerprint = _computeAuthFingerprint ( ) ;
297+
298+ // If a connection exists but auth changed, disconnect first
299+ if ( _hubs [ hubPath ]
300+ && _hubs [ hubPath ] . state === signalR . HubConnectionState . Connected
301+ && _hubAuthFingerprints [ hubPath ] !== currentFingerprint ) {
302+ return _disconnectHub ( hubPath ) . then ( function ( ) {
303+ return _getOrCreateHub ( hubPath ) ;
304+ } ) ;
305+ }
306+
229307 if ( _hubs [ hubPath ] && _hubs [ hubPath ] . state === signalR . HubConnectionState . Connected ) {
230308 return Promise . resolve ( _hubs [ hubPath ] ) ;
231309 }
@@ -274,6 +352,9 @@ var SignalROpenApiPlugin = function (system) {
274352
275353 _hubs [ hubPath ] = connection ;
276354
355+ // Store the auth fingerprint so we can detect changes later
356+ _hubAuthFingerprints [ hubPath ] = currentFingerprint ;
357+
277358 // Track connection state changes for UI updates
278359 connection . onreconnecting ( function ( ) {
279360 if ( _eventListeners [ hubPath ] ) {
@@ -325,6 +406,50 @@ var SignalROpenApiPlugin = function (system) {
325406 return signalrExt . hubPath || ( "/" + signalrExt . hub . toLowerCase ( ) ) ;
326407 } ;
327408
409+ // Discover all unique hub paths from the spec's x-signalr extensions.
410+ // Returns an array of { hubPath, hubName } objects.
411+ var _getHubPaths = function ( ) {
412+ var specIm = system . specSelectors . specJson ( ) ;
413+ var pathsIm = specIm . get ( "paths" ) ;
414+ if ( ! pathsIm ) {
415+ return [ ] ;
416+ }
417+
418+ var hubSet = { } ;
419+ pathsIm . keySeq ( ) . forEach ( function ( path ) {
420+ if ( ! _isSignalRPath ( path ) ) {
421+ return ;
422+ }
423+
424+ var methods = [ "post" , "get" ] ;
425+ for ( var mi = 0 ; mi < methods . length ; mi ++ ) {
426+ var ext = specIm . getIn ( [ "paths" , path , methods [ mi ] , "x-signalr" ] ) ;
427+ if ( ext ) {
428+ var hubName = ext . get ( "hub" ) ;
429+ var hubPath = ext . get ( "hubPath" ) || ( "/" + hubName . toLowerCase ( ) ) ;
430+ hubSet [ hubPath ] = hubName ;
431+ }
432+ }
433+ } ) ;
434+
435+ return Object . keys ( hubSet ) . map ( function ( hp ) {
436+ return { hubPath : hp , hubName : hubSet [ hp ] } ;
437+ } ) ;
438+ } ;
439+
440+ // Get the hub path associated with a given tag name.
441+ // Tags correspond to hub names in the generated OpenAPI spec.
442+ var _getHubPathForTag = function ( tagName ) {
443+ var hubs = _getHubPaths ( ) ;
444+ for ( var i = 0 ; i < hubs . length ; i ++ ) {
445+ if ( hubs [ i ] . hubName === tagName ) {
446+ return hubs [ i ] . hubPath ;
447+ }
448+ }
449+
450+ return null ;
451+ } ;
452+
328453 // Parse request body from the SwaggerUI OAS3 state.
329454 // The executeRequest wrapper intercepts before the original action reads
330455 // the request body, so we must read it from the OAS3 selectors directly.
@@ -416,6 +541,95 @@ var SignalROpenApiPlugin = function (system) {
416541 return JSON . stringify ( result , null , 2 ) ;
417542 } ;
418543
544+ // React component for hub connection control bar.
545+ // Renders inside each tag header to show connection status
546+ // and provide Connect / Disconnect buttons.
547+ function SignalRHubConnectionBar ( props ) {
548+ var React = system . React ;
549+ var hubPath = props . hubPath ;
550+
551+ var stateHook = React . useState ( 0 ) ;
552+ var forceUpdate = stateHook [ 1 ] ;
553+
554+ var hub = _hubs [ hubPath ] ;
555+ var connectionState = hub ? hub . state : null ;
556+ var isConnected = connectionState === signalR . HubConnectionState . Connected ;
557+ var isConnecting = connectionState === signalR . HubConnectionState . Connecting
558+ || connectionState === signalR . HubConnectionState . Reconnecting ;
559+
560+ var connectingHook = React . useState ( false ) ;
561+ var isManualConnecting = connectingHook [ 0 ] ;
562+ var setManualConnecting = connectingHook [ 1 ] ;
563+
564+ React . useEffect ( function ( ) {
565+ var listener = function ( ) { forceUpdate ( function ( n ) { return n + 1 ; } ) ; } ;
566+
567+ if ( ! _eventListeners [ hubPath ] ) {
568+ _eventListeners [ hubPath ] = [ ] ;
569+ }
570+
571+ _eventListeners [ hubPath ] . push ( listener ) ;
572+
573+ return function ( ) {
574+ var idx = _eventListeners [ hubPath ] . indexOf ( listener ) ;
575+ if ( idx >= 0 ) {
576+ _eventListeners [ hubPath ] . splice ( idx , 1 ) ;
577+ }
578+ } ;
579+ } , [ hubPath ] ) ;
580+
581+ var handleConnect = function ( ) {
582+ setManualConnecting ( true ) ;
583+ _getOrCreateHub ( hubPath )
584+ . then ( function ( ) {
585+ setManualConnecting ( false ) ;
586+ forceUpdate ( function ( n ) { return n + 1 ; } ) ;
587+ } )
588+ . catch ( function ( err ) {
589+ setManualConnecting ( false ) ;
590+ console . error ( "[SignalR OpenAPI] Manual connect failed:" , err ) ;
591+ forceUpdate ( function ( n ) { return n + 1 ; } ) ;
592+ } ) ;
593+ } ;
594+
595+ var handleDisconnect = function ( ) {
596+ _disconnectHub ( hubPath ) . then ( function ( ) {
597+ forceUpdate ( function ( n ) { return n + 1 ; } ) ;
598+ } ) ;
599+ } ;
600+
601+ var showConnecting = isConnecting || isManualConnecting ;
602+
603+ var statusClass = isConnected
604+ ? "signalr-status--connected"
605+ : showConnecting
606+ ? "signalr-status--connecting"
607+ : "signalr-status--disconnected" ;
608+
609+ var statusText = isConnected
610+ ? "Connected"
611+ : showConnecting
612+ ? "Connecting\u2026"
613+ : "Disconnected" ;
614+
615+ return React . createElement ( "div" , {
616+ className : "signalr-hub-connection-bar" ,
617+ onClick : function ( e ) { e . stopPropagation ( ) ; } ,
618+ } ,
619+ React . createElement ( "span" , {
620+ className : "signalr-status " + statusClass ,
621+ } , statusText ) ,
622+ ! isConnected && ! showConnecting && React . createElement ( "button" , {
623+ className : "btn signalr-connect-btn" ,
624+ onClick : handleConnect ,
625+ } , "Connect" ) ,
626+ isConnected && React . createElement ( "button" , {
627+ className : "btn signalr-disconnect-btn" ,
628+ onClick : handleDisconnect ,
629+ } , "Disconnect" )
630+ ) ;
631+ }
632+
419633 // React component for client event log panel
420634 function SignalREventLog ( props ) {
421635 var React = system . React ;
@@ -709,6 +923,37 @@ var SignalROpenApiPlugin = function (system) {
709923 return React . createElement ( Original , props ) ;
710924 } ;
711925 } ,
926+ // Inject a connection status bar into each SignalR hub tag header.
927+ // OperationTag renders the collapsible tag section; we append the
928+ // SignalRHubConnectionBar below the original tag header content.
929+ OperationTag : function ( Original , system ) {
930+ return function ( props ) {
931+ var React = system . React ;
932+ var result = React . createElement ( Original , props ) ;
933+
934+ // props.tag is the tag name (ImmutableJS or plain string)
935+ var tagName = props . tag ;
936+ if ( tagName && tagName . toJS ) {
937+ tagName = tagName . toJS ( ) ;
938+ } else if ( tagName && typeof tagName . get === "function" ) {
939+ tagName = tagName . get ( 0 ) || tagName ;
940+ }
941+
942+ if ( typeof tagName !== "string" ) {
943+ return result ;
944+ }
945+
946+ var hubPath = _getHubPathForTag ( tagName ) ;
947+ if ( ! hubPath ) {
948+ return result ;
949+ }
950+
951+ return React . createElement ( "div" , null ,
952+ React . createElement ( SignalRHubConnectionBar , { hubPath : hubPath } ) ,
953+ result
954+ ) ;
955+ } ;
956+ } ,
712957 // Hide curl command for SignalR operations
713958 curl : function ( Original , system ) {
714959 return function ( props ) {
0 commit comments