Skip to content

Commit

Permalink
Merge pull request #2693 from erri120/feat/event-bus
Browse files Browse the repository at this point in the history
Event Bus and reacting to downloads
  • Loading branch information
erri120 authored Feb 20, 2025
2 parents 4c8a8c4 + 80ab2c8 commit e438421
Show file tree
Hide file tree
Showing 17 changed files with 277 additions and 16 deletions.
7 changes: 7 additions & 0 deletions NexusMods.App.sln
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NexusMods.Abstractions.Game
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NexusMods.Games.FileHashes", "src\NexusMods.Games.FileHashes\NexusMods.Games.FileHashes.csproj", "{71D8CF63-A287-45AB-B251-676A54576C1D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NexusMods.Abstractions.EventBus", "src\Abstractions\NexusMods.Abstractions.EventBus\NexusMods.Abstractions.EventBus.csproj", "{653CF228-7CD2-4C1B-8B4F-1332B66A33A2}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -782,6 +784,10 @@ Global
{71D8CF63-A287-45AB-B251-676A54576C1D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{71D8CF63-A287-45AB-B251-676A54576C1D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{71D8CF63-A287-45AB-B251-676A54576C1D}.Release|Any CPU.Build.0 = Release|Any CPU
{653CF228-7CD2-4C1B-8B4F-1332B66A33A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{653CF228-7CD2-4C1B-8B4F-1332B66A33A2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{653CF228-7CD2-4C1B-8B4F-1332B66A33A2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{653CF228-7CD2-4C1B-8B4F-1332B66A33A2}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -920,6 +926,7 @@ Global
{9DE1C2AC-927A-4BC6-B2A1-7016902F8BAE} = {0CB73565-1207-4A56-A79F-6A8E9BBD795C}
{E2DB1DF4-9934-4119-BA90-196FDDB0904A} = {0CB73565-1207-4A56-A79F-6A8E9BBD795C}
{71D8CF63-A287-45AB-B251-676A54576C1D} = {70D38D24-79AE-4600-8E83-17F3C11BA81F}
{653CF228-7CD2-4C1B-8B4F-1332B66A33A2} = {0CB73565-1207-4A56-A79F-6A8E9BBD795C}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {9F9F8352-34DD-42C0-8564-EE9AF34A3501}
Expand Down
34 changes: 34 additions & 0 deletions src/Abstractions/NexusMods.Abstractions.EventBus/IEventBus.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using DynamicData.Kernel;
using JetBrains.Annotations;
using R3;

namespace NexusMods.Abstractions.EventBus;

/// <summary>
/// Represents an event bus for sending messages and requests.
/// </summary>
[PublicAPI]
public interface IEventBus
{
/// <summary>
/// Sends a message.
/// </summary>
void Send<T>(T message) where T : IEventBusMessage;

/// <summary>
/// Sends a request as a message and returns task that completes when a handler responds with a result.
/// </summary>
Task<Optional<TResult>> SendAndReceive<TRequest, TResult>(TRequest request, CancellationToken cancellationToken)
where TRequest : IEventBusRequest<TResult>
where TResult : notnull;

/// <summary>
/// Observes incoming messages of type <typeparamref name="T"/>.
/// </summary>
Observable<T> ObserveMessages<T>() where T : IEventBusMessage;

/// <summary>
/// Observes all incoming messages.
/// </summary>
Observable<IEventBusMessage> ObserveAllMessages();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using JetBrains.Annotations;

namespace NexusMods.Abstractions.EventBus;

/// <summary>
/// Represents a message.
/// </summary>
[PublicAPI]
public interface IEventBusMessage;
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using JetBrains.Annotations;

namespace NexusMods.Abstractions.EventBus;

/// <summary>
/// Represents a request.
/// </summary>
[PublicAPI]
public interface IEventBusRequest<TResult> : IEventBusMessage
where TResult : notnull;
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using JetBrains.Annotations;

namespace NexusMods.Abstractions.EventBus;

/// <summary>
/// Handles requests by responding with a result.
/// </summary>
[PublicAPI]
public interface IEventBusRequestHandler<in TRequest, TResult>
where TRequest : IEventBusRequest<TResult>
where TResult : notnull
{
/// <summary>
/// Handles the request.
/// </summary>
Task<TResult> Handle(TRequest request, CancellationToken cancellationToken);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- NuGet Package Shared Details -->
<Import Project="$([MSBuild]::GetPathOfFileAbove('NuGet.Build.props', '$(MSBuildThisFileDirectory)../'))" />
<ItemGroup>
<PackageReference Include="DynamicData"/>
<PackageReference Include="R3"/>
</ItemGroup>

</Project>
11 changes: 11 additions & 0 deletions src/NexusMods.App.Cli/CliMessages.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using NexusMods.Abstractions.EventBus;
using NexusMods.Abstractions.NexusModsLibrary.Models;

namespace NexusMods.CLI;

public static class CliMessages
{
public record AddedCollection(CollectionRevisionMetadata.ReadOnly Revision) : IEventBusMessage;

public record AddedDownload() : IEventBusMessage;
}
1 change: 1 addition & 0 deletions src/NexusMods.App.Cli/NexusMods.App.Cli.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

<ItemGroup>
<ProjectReference Include="..\Abstractions\NexusMods.Abstractions.Cli\NexusMods.Abstractions.Cli.csproj" />
<ProjectReference Include="..\Abstractions\NexusMods.Abstractions.EventBus\NexusMods.Abstractions.EventBus.csproj" />
<ProjectReference Include="..\Abstractions\NexusMods.Abstractions.Games\NexusMods.Abstractions.Games.csproj" />
<ProjectReference Include="..\Abstractions\NexusMods.Abstractions.GOG\NexusMods.Abstractions.GOG.csproj" />
<ProjectReference Include="..\Abstractions\NexusMods.Abstractions.HttpDownloader\NexusMods.Abstractions.HttpDownloader.csproj" />
Expand Down
13 changes: 6 additions & 7 deletions src/NexusMods.App.Cli/Types/IpcHandlers/NxmIpcProtocolHandler.cs
Original file line number Diff line number Diff line change
@@ -1,23 +1,18 @@
using System.Reactive.Linq;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using NexusMods.Abstractions.EventBus;
using NexusMods.Abstractions.GameLocators;
using NexusMods.Abstractions.Games;
using NexusMods.Abstractions.GOG;
using NexusMods.Abstractions.GOG.DTOs;
using NexusMods.Abstractions.Library;
using NexusMods.Abstractions.Loadouts;
using NexusMods.Abstractions.NexusModsLibrary;
using NexusMods.Abstractions.NexusWebApi;
using NexusMods.Abstractions.NexusWebApi.Types;
using NexusMods.Collections;
using NexusMods.Extensions.BCL;
using NexusMods.MnemonicDB.Abstractions;
using NexusMods.MnemonicDB.Abstractions.IndexSegments;
using NexusMods.Networking.HttpDownloader;
using NexusMods.Networking.NexusWebApi;
using NexusMods.Networking.NexusWebApi.Auth;
using NexusMods.Networking.NexusWebApi.V1Interop;
using NexusMods.Paths;

namespace NexusMods.CLI.Types.IpcHandlers;
Expand All @@ -38,6 +33,7 @@ public class NxmIpcProtocolHandler : IIpcProtocolHandler

private readonly IServiceProvider _serviceProvider;
private readonly IClient _client;
private readonly IEventBus _eventBus;

/// <summary>
/// constructor
Expand All @@ -51,6 +47,7 @@ public NxmIpcProtocolHandler(
ILoginManager loginManager)
{
_serviceProvider = serviceProvider;
_eventBus = serviceProvider.GetRequiredService<IEventBus>();

_logger = logger;
_oauth = oauth;
Expand Down Expand Up @@ -156,7 +153,7 @@ private async Task HandleCollectionUrl(NXMCollectionUrl collectionUrl)
}

var collectionRevision = await nexusModsLibrary.GetOrAddCollectionRevision(collectionFile, collectionUrl.Collection.Slug, collectionUrl.Revision, CancellationToken.None);
// var installJob = await InstallCollectionJob.Create(_serviceProvider, loadout, collectionFile);
_eventBus.Send(new CliMessages.AddedCollection(collectionRevision));
}

private async Task HandleModUrl(CancellationToken cancel, NXMModUrl modUrl)
Expand All @@ -173,6 +170,8 @@ private async Task HandleModUrl(CancellationToken cancel, NXMModUrl modUrl)
var library = _serviceProvider.GetRequiredService<ILibraryService>();
var temporaryFileManager = _serviceProvider.GetRequiredService<TemporaryFileManager>();

_eventBus.Send(new CliMessages.AddedDownload());

await using var destination = temporaryFileManager.CreateFile();
var downloadJob = await nexusModsLibrary.CreateDownloadJob(destination, modUrl, cancellationToken: cancel);

Expand Down
94 changes: 94 additions & 0 deletions src/NexusMods.App.UI/EventBus.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
using System.Diagnostics.CodeAnalysis;
using DynamicData.Kernel;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using NexusMods.Abstractions.EventBus;
using R3;

namespace NexusMods.App.UI;

public sealed class EventBus : IEventBus, IDisposable
{
private static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(10);

private static IEventBus? _instance;
public static IEventBus Instance
{
get => _instance ?? throw new InvalidOperationException("Event Bus hasn't been registered yet");
private set
{
if (_instance is not null) throw new InvalidOperationException("Event Bus has already been registered");
_instance = value;
}
}

private readonly IServiceProvider _serviceProvider;
private readonly ILogger _logger;
private readonly Subject<IEventBusMessage> _messages = new();

public EventBus(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
_logger = serviceProvider.GetRequiredService<ILogger<EventBus>>();

Instance = this;
}

[SuppressMessage("ReSharper", "HeapView.PossibleBoxingAllocation")]
public void Send<T>(T message) where T : IEventBusMessage
{
_logger.LogDebug("Received message of type `{Type}`: `{Message}`", typeof(T), message.ToString());
_messages.OnNext(message);
}

public Task<Optional<TResult>> SendAndReceive<TRequest, TResult>(TRequest request, CancellationToken cancellationToken)
where TRequest : IEventBusRequest<TResult>
where TResult : notnull
{
Send(request);

var requestHandler = _serviceProvider.GetService<IEventBusRequestHandler<TRequest, TResult>>();
if (requestHandler is null)
{
_logger.LogError("Found no request handler for request of type `{RequestType}` with result type `{ResultType}`: `{StringRepresentation}`", typeof(TRequest), typeof(TResult), request.ToString());
return Task.FromResult(Optional<TResult>.None);
}

var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
cts.CancelAfter(delay: DefaultTimeout);

return Inner();

async Task<Optional<TResult>> Inner()
{
try
{
return await requestHandler.Handle(request, cts.Token);
}
catch (Exception e)
{
_logger.LogError(e, "Exception running request handler for request of type `{RequestType}` with result type `{ResultType}`: `{StringRepresentation}`", typeof(TRequest), typeof(TResult), request.ToString());
return Optional<TResult>.None;
}
}
}

public Observable<T> ObserveMessages<T>() where T : IEventBusMessage
{
return _messages.OfType<IEventBusMessage, T>();
}

public Observable<IEventBusMessage> ObserveAllMessages()
{
return _messages;
}

private bool _isDisposed;
public void Dispose()
{
if (_isDisposed) return;

_messages.Dispose();
_isDisposed = true;
}
}
2 changes: 2 additions & 0 deletions src/NexusMods.App.UI/NexusMods.App.UI.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Abstractions\NexusMods.Abstractions.EventBus\NexusMods.Abstractions.EventBus.csproj" />
<ProjectReference Include="..\Abstractions\NexusMods.Abstractions.Logging\NexusMods.Abstractions.Logging.csproj" />
<ProjectReference Include="..\Abstractions\NexusMods.Abstractions.NexusModsLibrary\NexusMods.Abstractions.NexusModsLibrary.csproj" />
<ProjectReference Include="..\Abstractions\NexusMods.Abstractions.Resources.Caching\NexusMods.Abstractions.Resources.Caching.csproj" />
Expand All @@ -100,6 +101,7 @@
<ProjectReference Include="..\Networking\NexusMods.Networking.Downloaders\NexusMods.Networking.Downloaders.csproj" />
<ProjectReference Include="..\Networking\NexusMods.Networking.NexusWebApi\NexusMods.Networking.NexusWebApi.csproj" />
<ProjectReference Include="..\NexusMods.App.BuildInfo\NexusMods.App.BuildInfo.csproj" />
<ProjectReference Include="..\NexusMods.App.Cli\NexusMods.App.Cli.csproj" />
<ProjectReference Include="..\NexusMods.Collections\NexusMods.Collections.csproj" />
<ProjectReference Include="..\NexusMods.Icons\NexusMods.Icons.csproj" />
<ProjectReference Include="..\NexusMods.Media\NexusMods.Media.csproj" />
Expand Down
2 changes: 2 additions & 0 deletions src/NexusMods.App.UI/Services.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Text.Json.Serialization;
using Microsoft.Extensions.DependencyInjection;
using NexusMods.Abstractions.Diagnostics;
using NexusMods.Abstractions.EventBus;
using NexusMods.Abstractions.Serialization.ExpressionGenerator;
using NexusMods.Abstractions.Serialization.Json;
using NexusMods.App.UI.Controls.DataGrid;
Expand Down Expand Up @@ -291,6 +292,7 @@ public static IServiceCollection AddUI(this IServiceCollection c)
.AddSingleton<ILibraryDataProvider, NexusModsDataProvider>()
.AddSingleton<ILoadoutDataProvider, NexusModsDataProvider>()
.AddSingleton<ILoadoutDataProvider, BundledDataProvider>()
.AddSingleton<IEventBus, EventBus>()
.AddFileSystem()
.AddImagePipelines();
}
Expand Down
20 changes: 20 additions & 0 deletions src/NexusMods.App.UI/Settings/BehaviorSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using NexusMods.Abstractions.Settings;

namespace NexusMods.App.UI.Settings;

public record BehaviorSettings : ISettings
{
public bool BringWindowToFront { get; set; } = true;

public static ISettingsBuilder Configure(ISettingsBuilder settingsBuilder)
{
return settingsBuilder.AddToUI<BehaviorSettings>(builder => builder
.AddPropertyToUI(x => x.BringWindowToFront, propertyBuilder => propertyBuilder
.AddToSection(Sections.General)
.WithDisplayName("Bring app window to front")
.WithDescription("When enabled, operations like adding a collection will bring the app window to the foreground")
.UseBooleanContainer()
)
);
}
}
3 changes: 2 additions & 1 deletion src/NexusMods.App.UI/Settings/ServiceExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public static IServiceCollection AddUISettings(this IServiceCollection serviceCo
.AddSettings<TextEditorSettings>()
.AddSettings<AlphaSettings>()
.AddSettings<LoginSettings>()
.AddSettings<AlertSettings>();
.AddSettings<AlertSettings>()
.AddSettings<BehaviorSettings>();
}
}
2 changes: 1 addition & 1 deletion src/NexusMods.App.UI/Windows/IWorkspaceWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,5 @@ public interface IWorkspaceWindow
/// <summary>
/// This command is used to bring the window to front.
/// </summary>
ReactiveCommand<Unit, Unit> BringWindowToFront { get; }
ReactiveCommand<Unit, bool> BringWindowToFront { get; }
}
1 change: 1 addition & 0 deletions src/NexusMods.App.UI/Windows/MainWindow.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ public MainWindow()

this.WhenAnyObservable(view => view.ViewModel!.BringWindowToFront)
.OnUI()
.Where(static shouldBringToFront => shouldBringToFront)
.Subscribe(_ => {
if (WindowState == WindowState.Minimized)
WindowState = WindowState.Normal;
Expand Down
Loading

0 comments on commit e438421

Please sign in to comment.