Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit64401d4

Browse files
committed
Added: Initial implementation of Login Fixes. (#3799)
1 parentaa94e6c commit64401d4

File tree

3 files changed

+120
-1
lines changed

3 files changed

+120
-1
lines changed

‎src/NexusMods.Abstractions.NexusWebApi/ILoginManager.cs‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,4 +77,11 @@ public interface ILoginManager
7777
/// Log out of Nexus Mods
7878
/// </summary>
7979
TaskLogout();
80+
81+
/// <summary>
82+
/// Enables automatic refresh of user info based on an observable boolean trigger.
83+
/// </summary>
84+
/// <param name="triggerObservable">Observable that triggers refresh when true</param>
85+
/// <returns>IDisposable to stop the refresh subscription</returns>
86+
IDisposableRefreshOnObservable(Observable<bool>triggerObservable);
8087
}

‎src/NexusMods.App.UI/Windows/MainWindowViewModel.cs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,11 @@ public MainWindowViewModel(
127127
.BindTo(_windowManager, manager=>manager.ActiveWindow)
128128
.DisposeWith(d);
129129

130+
// Enable automatic UserInfo refresh when window gains focus
131+
loginManager.RefreshOnObservable(
132+
this.WhenAnyValue(vm=>vm.IsActive).ToObservable()
133+
).DisposeWith(d);
134+
130135
overlayController.WhenAnyValue(oc=>oc.CurrentOverlay)
131136
.BindTo(this, vm=>vm.CurrentOverlay)
132137
.DisposeWith(d);

‎src/NexusMods.Networking.NexusWebApi/LoginManager.cs‎

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
usingSystem.Diagnostics;
2+
usingSystem.Net.Http;
23
usingDynamicData.Aggregation;
34
usingJetBrains.Annotations;
45
usingMicrosoft.Extensions.Logging;
@@ -39,6 +40,29 @@ public sealed class LoginManager : IDisposable, ILoginManager
3940

4041
privatereadonlyIDisposable_observeDatomDisposable;
4142

43+
// Timing Constants
44+
45+
/// <summary>
46+
/// How long UserInfo is cached before requiring refresh (in minutes).
47+
/// </summary>
48+
privateconstintCacheExpiryMinutes=60;
49+
50+
/// <summary>
51+
/// How often to proactively refresh UserInfo to prevent cache expiry (in minutes).
52+
/// Must be less than CacheExpiryMinutes to prevent cache expiration.
53+
/// </summary>
54+
privateconstintPeriodicRefreshIntervalMinutes=59;
55+
56+
/// <summary>
57+
/// Maximum number of retry attempts when refreshing UserInfo fails.
58+
/// </summary>
59+
privateconstintMaxRefreshRetries=3;
60+
61+
/// <summary>
62+
/// Initial delay between retry attempts when refreshing UserInfo fails (in seconds).
63+
/// </summary>
64+
privateconstintInitialRetryDelaySeconds=3;
65+
4266
/// <summary>
4367
/// Constructor.
4468
/// </summary>
@@ -76,10 +100,19 @@ public LoginManager(
76100
_userInfo.OnNext(userInfo);
77101
}
78102
},awaitOperation:AwaitOperation.Sequential,configureAwait:false);
103+
104+
// Set up periodic refresh to prevent cache expiry
105+
_periodicRefreshDisposable=Observable
106+
.Timer(TimeSpan.FromMinutes(PeriodicRefreshIntervalMinutes),TimeSpan.FromMinutes(PeriodicRefreshIntervalMinutes))
107+
.SubscribeAwait(async(_,cancellationToken)=>
108+
{
109+
awaitTryRefreshUserInfoSafely(cancellationToken,"periodic update");
110+
},configureAwait:false);
79111
}
80112

81-
privateCachedObject<UserInfo>_cachedUserInfo=new(TimeSpan.FromHours(1));
113+
privateCachedObject<UserInfo>_cachedUserInfo=new(TimeSpan.FromMinutes(CacheExpiryMinutes));
82114
privatereadonlySemaphoreSlim_verifySemaphore=new(initialCount:1,maxCount:1);
115+
privatereadonlyIDisposable_periodicRefreshDisposable;
83116
privatereadonlyIConnection_conn;
84117

85118
privateasyncValueTask<UserInfo?>Verify(CancellationTokencancellationToken)
@@ -101,6 +134,63 @@ public LoginManager(
101134
returnuserInfo;
102135
}
103136

137+
privateasyncTaskRefreshUserInfoWithRetry(CancellationTokencancellationToken)
138+
{
139+
constintmaxRetries=MaxRefreshRetries;
140+
vardelay=TimeSpan.FromSeconds(InitialRetryDelaySeconds);
141+
142+
for(varattempt=0;attempt<maxRetries;attempt++)
143+
{
144+
try
145+
{
146+
// Force cache eviction to trigger a fresh API call
147+
_cachedUserInfo.Evict();
148+
varuserInfo=awaitVerify(cancellationToken);
149+
150+
if(userInfoisnull)
151+
continue;
152+
153+
_cachedUserInfo.Store(userInfo);
154+
_userInfo.OnNext(userInfo);
155+
return;// Success, exit retry loop
156+
}
157+
catch(TaskCanceledException)
158+
{
159+
// Cancellation requested, exit gracefully
160+
return;
161+
}
162+
catch(Exceptionex)when(attempt<maxRetries-1)
163+
{
164+
_logger.LogWarning(ex,"Error refreshing user info, attempt {Attempt}/{MaxAttempts}",
165+
attempt+1,maxRetries);
166+
167+
// Exponential backoff for all retryable exceptions
168+
// Base delay: 3s, multiplied by 2^attempt
169+
// Attempt 0: 3s, Attempt 1: 6s, Attempt 2: 12s
170+
// Total delay if all retries fail: 21 seconds
171+
varexponentialDelay=TimeSpan.FromMilliseconds(
172+
delay.TotalMilliseconds*Math.Pow(2,attempt));
173+
awaitTask.Delay(exponentialDelay,cancellationToken);
174+
}
175+
}
176+
177+
_logger.LogWarning("Failed to refresh user info after {MaxAttempts} attempts",maxRetries);
178+
}
179+
180+
privateasyncTaskTryRefreshUserInfoSafely(CancellationTokencancellationToken,stringcontext)
181+
{
182+
try
183+
{
184+
// Only refresh if we have a cached value (user is logged in)
185+
if(_cachedUserInfo.Get()is notnull)
186+
awaitRefreshUserInfoWithRetry(cancellationToken);
187+
}
188+
catch(Exceptionex)
189+
{
190+
_logger.LogWarning(ex,"Failed to refresh user info during {Context}",context);
191+
}
192+
}
193+
104194
privateasyncValueTaskAddUserToDb(UserInfouserInfo)
105195
{
106196
usingvartx=_conn.BeginTransaction();
@@ -136,6 +226,22 @@ public async Task<bool> GetIsUserLoggedInAsync(CancellationToken token = default
136226
returnawaitGetUserInfoAsync(token)is notnull;
137227
}
138228

229+
/// <summary>
230+
/// Enables automatic refresh of user info based on an observable boolean trigger.
231+
/// </summary>
232+
/// <param name="triggerObservable">Observable that triggers refresh when true</param>
233+
/// <returns>IDisposable to stop the refresh subscription</returns>
234+
publicIDisposableRefreshOnObservable(Observable<bool>triggerObservable)
235+
{
236+
returntriggerObservable
237+
.DistinctUntilChanged()
238+
.Where(isActive=>isActive)
239+
.SubscribeAwait(async(_,cancellationToken)=>
240+
{
241+
awaitTryRefreshUserInfoSafely(cancellationToken,"window focus");
242+
},configureAwait:false);
243+
}
244+
139245
/// <summary>
140246
/// Show a browser and log into Nexus Mods
141247
/// </summary>
@@ -212,5 +318,6 @@ public void Dispose()
212318
{
213319
_verifySemaphore.Dispose();
214320
_observeDatomDisposable.Dispose();
321+
_periodicRefreshDisposable.Dispose();
215322
}
216323
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp