diff --git a/src/UniGetUI.Avalonia/ViewModels/SoftwarePages/PackagesPageViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/SoftwarePages/PackagesPageViewModel.cs index 30ce1259ca..a834ea28a6 100644 --- a/src/UniGetUI.Avalonia/ViewModels/SoftwarePages/PackagesPageViewModel.cs +++ b/src/UniGetUI.Avalonia/ViewModels/SoftwarePages/PackagesPageViewModel.cs @@ -683,7 +683,7 @@ public void FilterPackages(bool fromQuery = false) UpdateSubtitle(); PackageCountUpdated?.Invoke(); - bool loadingOrPending = Loader.IsLoading || (LoadsOnStart && !Loader.IsLoaded); + bool loadingOrPending = Loader.IsLoading || (LoadsOnStart && Loader.HasPendingInitialLoad); if (loadingOrPending && FilteredPackages.Count == 0) { @@ -1022,7 +1022,7 @@ public void UpdatePackageCount() // ─── Subtitle ───────────────────────────────────────────────────────────── public void UpdateSubtitle() { - if (Loader.IsLoading || (LoadsOnStart && !Loader.IsLoaded)) + if (Loader.IsLoading || (LoadsOnStart && Loader.HasPendingInitialLoad)) { Subtitle = _stillLoadingSubtitle; return; diff --git a/src/UniGetUI.PackageEngine.PackageLoader/AbstractPackageLoader.cs b/src/UniGetUI.PackageEngine.PackageLoader/AbstractPackageLoader.cs index 44e82c43aa..cf5c2eb491 100644 --- a/src/UniGetUI.PackageEngine.PackageLoader/AbstractPackageLoader.cs +++ b/src/UniGetUI.PackageEngine.PackageLoader/AbstractPackageLoader.cs @@ -37,6 +37,8 @@ public abstract class AbstractPackageLoader public bool LastLoadReportedFailures { get; private set; } + public bool HasPendingInitialLoad => !IsLoaded && !LastLoadReportedFailures; + public DateTime? LastLoadFinishedUtc { get; private set; } private TaskCompletionSource? _loadCompletion; @@ -157,6 +159,8 @@ protected void InvokeFinishedLoadingEvent() public virtual async Task ReloadPackages() { TaskCompletionSource? completion = null; + int current_identifier = 0; + bool finishWasAnnounced = false; try { if (DISABLE_RELOAD) @@ -172,7 +176,7 @@ public virtual async Task ReloadPackages() } LoadOperationIdentifier = new Random().Next(); - int current_identifier = LoadOperationIdentifier; + current_identifier = LoadOperationIdentifier; completion = new TaskCompletionSource( TaskCreationOptions.RunContinuationsAsynchronously ); @@ -258,14 +262,32 @@ public virtual async Task ReloadPackages() { LastLoadFinishedUtc = DateTime.UtcNow; IsLoaded = true; + finishWasAnnounced = true; InvokeFinishedLoadingEvent(); } } catch (Exception ex) { Logger.Error(ex); - LastLoadReportedFailures = true; - IsLoading = false; + + if ( + !finishWasAnnounced + && completion is not null + && LoadOperationIdentifier == current_identifier + ) + { + LastLoadReportedFailures = true; + IsLoading = false; + + try + { + InvokeFinishedLoadingEvent(); + } + catch (Exception announceEx) + { + Logger.Error(announceEx); + } + } } finally { diff --git a/src/UniGetUI.PackageEngine.Tests/PackageLoaderPipelineTests.cs b/src/UniGetUI.PackageEngine.Tests/PackageLoaderPipelineTests.cs index d455850bbe..1a29a824d3 100644 --- a/src/UniGetUI.PackageEngine.Tests/PackageLoaderPipelineTests.cs +++ b/src/UniGetUI.PackageEngine.Tests/PackageLoaderPipelineTests.cs @@ -290,4 +290,89 @@ public async Task ReloadPackages_ReportsSettledLoadState_WhenFinishedLoadingIsRa Assert.False(isLoadingWhenFinished); Assert.True(isLoadedWhenFinished); } + + [Fact] + public async Task ReloadPackages_SettlesAndAnnouncesCompletion_WhenTheLoadFails() + { + var manager = new PackageManagerBuilder() + .WithInstalledPackages(testManager => + [ + new PackageBuilder() + .WithManager(testManager) + .WithId("Contoso.Tool") + .WithVersion("1.0.0") + .Build(), + ]) + .Build(); + TestPackageLoader? loaderReference = null; + Task? waitTakenDuringLoad = null; + var loader = new TestPackageLoader( + [manager], + isPackageValid: _ => + { + waitTakenDuringLoad = loaderReference!.WaitForCurrentLoadAsync(); + throw new InvalidOperationException("the load blew up"); + } + ); + loaderReference = loader; + var recorder = new LoaderEventRecorder(loader); + + await loader.ReloadPackages(); + + Assert.Equal(1, recorder.FinishedLoadingCount); + Assert.False(loader.IsLoading); + Assert.True(loader.LastLoadReportedFailures); + Assert.False(loader.HasPendingInitialLoad); + Assert.NotNull(waitTakenDuringLoad); + Assert.True(waitTakenDuringLoad.IsCompleted); + } + + [Fact] + public async Task ReloadPackages_AnnouncesCompletionOnce_WhenAFinishedLoadingSubscriberThrows() + { + var manager = new PackageManagerBuilder().Build(); + var loader = new TestPackageLoader([manager], loadPackages: _ => []); + var recorder = new LoaderEventRecorder(loader); + loader.FinishedLoading += (_, _) => throw new InvalidOperationException("the subscriber blew up"); + + await loader.ReloadPackages(); + + Assert.Equal(1, recorder.FinishedLoadingCount); + Assert.True(loader.IsLoaded); + Assert.NotNull(loader.LastLoadFinishedUtc); + Assert.False(loader.LastLoadReportedFailures); + } + + [Fact] + public async Task ReloadPackages_LeavesAQueuedReloadUntouched_WhenAFinishedLoadingSubscriberThrows() + { + using var release = new ManualResetEventSlim(true); + var manager = new PackageManagerBuilder().Build(); + var loader = new TestPackageLoader( + [manager], + loadPackages: _ => + { + release.Wait(); + return []; + } + ); + + Task? queued = null; + loader.FinishedLoading += (_, _) => + { + if (queued is not null) return; + release.Reset(); + queued = loader.ReloadPackages(); + }; + loader.FinishedLoading += (_, _) => throw new InvalidOperationException("the subscriber blew up"); + + await loader.ReloadPackages(); + + Assert.NotNull(queued); + Assert.True(loader.IsLoading); + Assert.False(loader.LastLoadReportedFailures); + + release.Set(); + await queued; + } }