@@ -89,8 +89,9 @@ private async Task OnAuthenticationChangedAsync(LexboxServer server)
8989 // live connection AND the project is registered on it, so a periodic call is a true no-op once subscribed;
9090 // a logged-out server no-ops on the token check). The per-project check matters: single-project paths
9191 // (post-sync, project-open) subscribe only the requesting project when they rebuild a connection, leaving
92- // sibling projects unsubscribed until this pass heals them. Used by connectivity-regained and periodic
93- // recovery so a listener that failed to start while offline comes back without a manual sync or edit.
92+ // sibling projects unsubscribed until this pass heals them. Used by connectivity-regained, app-resume
93+ // and periodic recovery so a listener that failed to start while offline comes back without a manual
94+ // sync or edit.
9495 public Task EnsureListenersForTrackedProjects ( bool kickReconnecting = false ) => EnsureListenersForTrackedProjects ( null , kickReconnecting ) ;
9596
9697 private async Task EnsureListenersForTrackedProjects ( Func < ProjectData , bool > ? filter , bool kickReconnecting = false )
@@ -458,10 +459,10 @@ internal static async Task EvictAndStopIfCached(string cacheKey, IMemoryCache ca
458459 }
459460 }
460461
461- // Internal for unit tests. SignalR's InfiniteRetryPolicy retries forever ; this breaks the loop when the
462- // user is logged out. We treat "no token" as "logged out" — distinguishing a transient token failure
463- // from a real logout is OAuthClient's job (see its failure classifier), not ours. Once the user signs
464- // back in, OnAuthenticationChangedAsync rebuilds the listener.
462+ // Internal for unit tests. The retry policy never gives up ; this breaks the loop when the user is
463+ // logged out. We treat "no token" as "logged out" — distinguishing a transient token failure from a
464+ // real logout is OAuthClient's job (see its failure classifier), not ours. Once the user signs back
465+ // in, OnAuthenticationChangedAsync rebuilds the listener.
465466 internal static async Task HandleReconnecting (
466467 string cacheKey ,
467468 IMemoryCache cache ,
@@ -574,7 +575,16 @@ internal class AdaptiveRetryPolicy(INetworkStatus networkStatus) : IRetryPolicy
574575 ( Guid projectId , Guid ? clientId ) =>
575576 {
576577 logger . LogInformation ( "Received project update for {ProjectId}, triggering sync" , projectId ) ;
577- backgroundSyncService . TriggerSync ( projectId , clientId ) ;
578+ try
579+ {
580+ backgroundSyncService . TriggerSync ( projectId , clientId ) ;
581+ }
582+ catch ( Exception e )
583+ {
584+ // TriggerSync throws if the background sync service isn't running (e.g. during shutdown);
585+ // don't let that bubble into SignalR's dispatcher.
586+ logger . LogWarning ( e , "Failed to trigger sync for {ProjectId}" , projectId ) ;
587+ }
578588 return Task . CompletedTask ;
579589 } ) ;
580590
@@ -586,7 +596,6 @@ internal class AdaptiveRetryPolicy(INetworkStatus networkStatus) : IRetryPolicy
586596 var hasValidToken = await clientFactory . GetClient ( server ) . GetCurrentToken ( ) is not null ;
587597 await HandleReconnecting ( cacheKey , cache , logger , connection , ( ) => connection . StopAsync ( ) , hasValidToken , exception ) ;
588598 } ;
589- // TODO: If StartAsync fails due to transient network, consider retrying on next sync/on-demand
590599 await connection . StartAsync ( stoppingToken ) ;
591600 // intentionally AFTER StartAsync (see catch comment)
592601 connection . Closed += async ( exception ) =>
0 commit comments