diff --git a/StarMap.API/Mediation/IEventPublisher.cs b/StarMap.API/Mediation/IEventPublisher.cs new file mode 100644 index 0000000..6a2f057 --- /dev/null +++ b/StarMap.API/Mediation/IEventPublisher.cs @@ -0,0 +1,8 @@ +namespace StarMap.API.Mediation +{ + public interface IEventPublisher : IDisposable where T : IStarMapEvent + { + void Publish(T message); + Task PublishParallel(T message); + } +} diff --git a/StarMap.API/Mediation/IStarMapEvent.cs b/StarMap.API/Mediation/IStarMapEvent.cs new file mode 100644 index 0000000..fbbfbf5 --- /dev/null +++ b/StarMap.API/Mediation/IStarMapEvent.cs @@ -0,0 +1,6 @@ +namespace StarMap.API.Mediation +{ + public interface IStarMapEvent + { + } +} diff --git a/StarMap.API/Mediation/ISubscription.cs b/StarMap.API/Mediation/ISubscription.cs new file mode 100644 index 0000000..47f02d1 --- /dev/null +++ b/StarMap.API/Mediation/ISubscription.cs @@ -0,0 +1,6 @@ +namespace StarMap.API.Mediation +{ + public interface ISubscription : IDisposable where T : IStarMapEvent + { + } +} diff --git a/StarMap.API/Services/IMediationService.cs b/StarMap.API/Services/IMediationService.cs new file mode 100644 index 0000000..d8f3450 --- /dev/null +++ b/StarMap.API/Services/IMediationService.cs @@ -0,0 +1,10 @@ +using StarMap.API.Mediation; + +namespace StarMap.API.Services +{ + public interface IMediationService + { + ISubscription Subscribe(Action handler) where T : IStarMapEvent; + IEventPublisher RegisterPublisher() where T : IStarMapEvent; + } +} diff --git a/StarMap.Core/API/MediationService.cs b/StarMap.Core/API/MediationService.cs new file mode 100644 index 0000000..4767293 --- /dev/null +++ b/StarMap.Core/API/MediationService.cs @@ -0,0 +1,29 @@ +using StarMap.API.Mediation; +using StarMap.API.Services; +using StarMap.Core.ModRepository; +using StarMap.Core.Services; + +namespace StarMap.Core.API +{ + internal class MediationService : IMediationService + { + private readonly RuntimeMod _mod; + private readonly IMediator _mediator; + + public MediationService(IModContext modContext, IMediator mediator) + { + _mod = modContext.Mod; + _mediator = mediator; + } + + public IEventPublisher RegisterPublisher() where T : IStarMapEvent + { + return _mediator.RegisterPublisher(_mod); + } + + public ISubscription Subscribe(Action handler) where T : IStarMapEvent + { + return _mediator.Subscribe(handler); + } + } +} diff --git a/StarMap.Core/Services/Mediator.cs b/StarMap.Core/Services/Mediator.cs new file mode 100644 index 0000000..b760c3c --- /dev/null +++ b/StarMap.Core/Services/Mediator.cs @@ -0,0 +1,154 @@ +using StarMap.API.Mediation; +using StarMap.Core.ModRepository; +using System.Collections.Concurrent; +using System.Runtime.Loader; + +namespace StarMap.Core.Services +{ + internal interface IMediator + { + IEventPublisher RegisterPublisher(RuntimeMod mod) where T : IStarMapEvent; + ISubscription Subscribe(Action action) where T : IStarMapEvent; + } + + public interface IEventPublisher { } + + internal class SubscriptionHandle : ISubscription where T : IStarMapEvent + { + private readonly EventPublisher _publisher; + + private readonly Action _action; + + public SubscriptionHandle(EventPublisher publisher, Action action) + { + _publisher = publisher; + _action = action; + } + + public void Invoke(T message) + { + _action.Invoke(message); + } + + public void Dispose() + { + _publisher.Unsubscribe(this); + } + } + + internal class EventPublisher(Mediator mediator) : IEventPublisher, IEventPublisher where T : IStarMapEvent + { + private readonly Lock _lock = new(); + + private bool _isDisposed; + + private readonly HashSet> _handlers = []; + + public ISubscription Subscribe(Action action) + { + var handle = new SubscriptionHandle(this, action); + + lock (_lock) + { + ObjectDisposedException.ThrowIf(_isDisposed, this); + _handlers.Add(handle); + } + + return handle; + } + + public void Unsubscribe(SubscriptionHandle handle) + { + lock (_lock) + { + if (_isDisposed) + return; + + _handlers.Remove(handle); + } + } + + public void Publish(T message) + { + foreach (var handler in GetHandlersSnapshot()) + { + handler.Invoke(message); + } + } + + public Task PublishParallel(T message) + { + var tasks = new List(); + + foreach (var handler in GetHandlersSnapshot()) + { + tasks.Add(Task.Run(() => handler.Invoke(message))); + } + + return Task.WhenAll(tasks); + } + + private SubscriptionHandle[] GetHandlersSnapshot() + { + lock (_lock) + { + if (_isDisposed) + return []; + + return [.. _handlers]; + } + } + + public void Dispose() + { + lock (_lock) + { + if (_isDisposed) + return; + + _isDisposed = true; + + mediator.UnregisterPublisher(this); + _handlers.Clear(); + } + + } + } + + internal class Mediator : IMediator + { + private readonly ConcurrentDictionary _publishers = []; + + public IEventPublisher RegisterPublisher(RuntimeMod mod) where T : IStarMapEvent + { + var type = typeof(T); + if (AssemblyLoadContext.GetLoadContext(typeof(T).Assembly) != mod.ModAssemblyLoadContext) + throw new Exception("Events are only allowed to be published by the owner of the event type"); + + var publisher = new EventPublisher(this); + + if (!_publishers.TryAdd(type, publisher)) + throw new Exception($"A publisher for type '{typeof(T)}' was already registered"); + + return publisher; + } + + public void UnregisterPublisher(IEventPublisher publisher) + { + _publishers.TryRemove(new(typeof(T), publisher)); + } + + public ISubscription Subscribe(Action action) where T : IStarMapEvent + { + if (!_publishers.TryGetValue(typeof(T), out var publisher)) + { + throw new Exception("A publisher for this event was not found"); + } + + if (publisher is not EventPublisher typedPublisher) + throw new Exception("Something went wrong in the type of the publisher"); + + return typedPublisher.Subscribe(action); + } + } +} diff --git a/StarMap.Core/StarMapCore.cs b/StarMap.Core/StarMapCore.cs index 5bda76a..05a0dd7 100644 --- a/StarMap.Core/StarMapCore.cs +++ b/StarMap.Core/StarMapCore.cs @@ -22,10 +22,12 @@ public StarMapCore(AssemblyLoadContext coreAssemblyLoadContext) serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); serviceCollection.AddScoped(); serviceCollection.AddScoped(); serviceCollection.AddScoped(); + serviceCollection.AddScoped(); serviceCollection.AddSingleton();