44using Configuration ;
55using Flurl ;
66using Flurl . Http ;
7+ using JetBrains . Annotations ;
78using Microsoft . AspNetCore . SignalR . Client ;
89using Microsoft . Extensions . Hosting ;
910using Microsoft . Extensions . Logging ;
1213using Polly . Retry ;
1314using ProReceptionApi ;
1415using Settings ;
15- using Settings . Models . Public ;
1616
17+ [ PublicAPI ]
1718[ UnsupportedOSPlatform ( "browser" ) ] // Proxy support in SignalR is not supported in browser
1819public abstract class SignalRHostedService < T > (
1920 IOptions < ProReceptionApiConfiguration > proReceptionApiConfigurationOptions ,
@@ -23,16 +24,16 @@ public abstract class SignalRHostedService<T>(
2324 ISettingsManagerBase settingsManagerBase )
2425 : IHostedService
2526{
26- private readonly CancellationTokenSource _stoppingCts = new ( ) ;
27+ private readonly CancellationTokenSource stoppingCts = new ( ) ;
2728
28- private Task ? _startUpTask ;
29- private HubConnection ? _hubConnection ;
29+ private Task ? startUpTask ;
30+ private HubConnection ? hubConnection ;
3031
3132 protected abstract string HubPath { get ; }
3233
3334 public Task StartAsync ( CancellationToken cancellationToken )
3435 {
35- _startUpTask = ExecuteStartUp ( _stoppingCts . Token ) ;
36+ startUpTask = ExecuteStartUp ( stoppingCts . Token ) ;
3637
3738 return Task . CompletedTask ;
3839 }
@@ -42,24 +43,24 @@ public async Task StopAsync(CancellationToken cancellationToken)
4243 logger . LogInformation ( $ "Stopping { typeof ( T ) . Name } ...") ;
4344
4445 // Stop called without start
45- if ( _startUpTask == null )
46+ if ( startUpTask == null )
4647 {
4748 return ;
4849 }
4950
5051 try
5152 {
5253 // Signal cancellation to the executing method
53- await _stoppingCts . CancelAsync ( ) ;
54+ await stoppingCts . CancelAsync ( ) ;
5455 }
5556 finally
5657 {
5758 // Wait until the task completes or the stop token triggers
58- await Task . WhenAny ( _startUpTask , Task . Delay ( Timeout . Infinite , cancellationToken ) ) ;
59+ await Task . WhenAny ( startUpTask , Task . Delay ( Timeout . Infinite , cancellationToken ) ) ;
5960
60- if ( _hubConnection != null )
61+ if ( hubConnection != null )
6162 {
62- await _hubConnection . DisposeAsync ( ) ;
63+ await hubConnection . DisposeAsync ( ) ;
6364 }
6465 }
6566 }
@@ -86,7 +87,7 @@ private async Task ExecuteStartUp(CancellationToken cancellationToken)
8687 exceptionMessage += $ " Response body: { await flurlException . GetResponseStringAsync ( ) } ";
8788 }
8889
89- logger . LogWarning ( "Attempt {AttemptNumber} failed: {ExceptionMessage}. Waiting {RetryDelay} before next try. " , args . AttemptNumber , exceptionMessage , args . RetryDelay ) ;
90+ logger . LogWarning ( "Attempt {AttemptNumber} failed: {ExceptionMessage}. Waiting {RetryDelay} before next try" , args . AttemptNumber , exceptionMessage , args . RetryDelay ) ;
9091 }
9192 } )
9293 . Build ( )
@@ -95,67 +96,76 @@ private async Task ExecuteStartUp(CancellationToken cancellationToken)
9596
9697 private async Task LoginAndCreateSignalRConnection ( CancellationToken cancellationToken )
9798 {
98- TokensRecord ? proReceptionTokens ;
99- do
100- {
101- proReceptionTokens = await GetProReceptionTokens ( cancellationToken ) ;
102- } while ( ! cancellationToken . IsCancellationRequested && string . IsNullOrWhiteSpace ( proReceptionTokens ? . AccessToken ) ) ;
103-
10499 if ( ! cancellationToken . IsCancellationRequested )
105100 {
106- if ( string . IsNullOrWhiteSpace ( proReceptionTokens ? . AccessToken ) )
107- {
108- throw new ApplicationException ( "The ProReception access token is null or empty (this should never happen)" ) ;
109- }
110-
111101 logger . LogInformation ( "Establishing SignalR connection..." ) ;
112102
113- _hubConnection = new HubConnectionBuilder ( )
103+ hubConnection = new HubConnectionBuilder ( )
114104 . WithUrl ( proReceptionApiConfigurationOptions . Value . BaseUrl . AppendPathSegment ( HubPath ) , options =>
115105 {
116- options . Headers . Add ( "Authorization" , $ "Bearer { proReceptionTokens . AccessToken } ") ;
106+ // Provide dynamic access token for every (re)connect to avoid stale tokens
107+ options . AccessTokenProvider = async ( ) =>
108+ {
109+ var current = settingsManagerBase . GetTokens ( ) ;
110+ if ( current is null )
111+ {
112+ // No tokens available (e.g., user logged out) -> return null to let reconnect keep retrying
113+ logger . LogInformation ( "SignalR AccessTokenProvider: no tokens available" ) ;
114+ return null ;
115+ }
116+
117+ // Refresh token if expiring soon
118+ if ( current . ExpiresAtUtc . AddMinutes ( - 10 ) < DateTime . UtcNow )
119+ {
120+ try
121+ {
122+ current = await proReceptionApiClient . RefreshAndSaveTokens ( current ) ;
123+ logger . LogInformation ( "SignalR AccessTokenProvider: token refreshed" ) ;
124+ }
125+ catch ( Exception ex )
126+ {
127+ logger . LogWarning ( ex , "SignalR AccessTokenProvider: failed to refresh token" ) ;
128+ // Return existing token (may fail with 401 and trigger reconnect), or null if missing
129+ if ( string . IsNullOrWhiteSpace ( current . AccessToken ) )
130+ {
131+ return null ;
132+ }
133+ }
134+ }
135+
136+ return current . AccessToken ;
137+ } ;
138+
117139 options . Headers . Add ( "X-DistributionServerAppId" , settingsManagerBase . GetDistributionServerAppId ( ) . ToString ( ) ) ;
118140 options . Proxy = proxyConfigurationOptions . Value . GetWebProxy ( ) ;
119141 } )
120142 . WithAutomaticReconnect ( )
121143 . Build ( ) ;
122144
123- ConfigureListeners ( _hubConnection ) ;
145+ ConfigureListeners ( hubConnection ) ;
124146
125- _hubConnection . Closed += async _ =>
147+ hubConnection . Reconnecting += error =>
126148 {
127- logger . LogInformation ( "SignalR connection lost, will retry..." ) ;
128- await _hubConnection . StopAsync ( cancellationToken ) ;
129- await ExecuteStartUp ( cancellationToken ) ;
149+ logger . LogInformation ( error , "SignalR connection reconnecting..." ) ;
150+ return Task . CompletedTask ;
130151 } ;
131152
132- await _hubConnection . StartAsync ( cancellationToken ) ;
133-
134- logger . LogInformation ( "SignalR connection successfully established" ) ;
135- }
136- }
137-
138- private async Task < TokensRecord ? > GetProReceptionTokens ( CancellationToken cancellationToken )
139- {
140- while ( ! cancellationToken . IsCancellationRequested )
141- {
142- var proReceptionTokens = settingsManagerBase . GetTokens ( ) ;
143-
144- if ( ! string . IsNullOrWhiteSpace ( proReceptionTokens ? . AccessToken ) )
153+ hubConnection . Reconnected += connectionId =>
145154 {
146- if ( proReceptionTokens . ExpiresAtUtc . AddMinutes ( - 10 ) < DateTime . UtcNow )
147- {
148- return await proReceptionApiClient . RefreshAndSaveTokens ( proReceptionTokens ) ;
149- }
155+ logger . LogInformation ( "SignalR connection reconnected. ConnectionId={ConnectionId}" , connectionId ) ;
156+ return Task . CompletedTask ;
157+ } ;
150158
151- return proReceptionTokens ;
152- }
159+ hubConnection . Closed += error =>
160+ {
161+ // Do not call StopAsync or recursively restart; rely on automatic reconnect and outer retry/startup logic
162+ logger . LogInformation ( error , "SignalR connection closed. Waiting for background retry logic..." ) ;
163+ return Task . CompletedTask ;
164+ } ;
153165
154- logger . LogInformation ( "Not logged into ProReception, sleeping..." ) ;
166+ await hubConnection . StartAsync ( cancellationToken ) ;
155167
156- await Task . Delay ( 1000 , cancellationToken ) ;
168+ logger . LogInformation ( "SignalR connection successfully established" ) ;
157169 }
158-
159- return null ;
160170 }
161171}
0 commit comments