Repository navigation
Improve event handling and the eventing API #102
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
KlaasWhite
wants to merge
1
commit into
main
Choose a base branch
from
feature/events
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| namespace StarMap.API.Events | ||
| { | ||
| public interface IEventPublisher<T> : IDisposable | ||
| { | ||
| void Publish(T message); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| namespace StarMap.API.Events | ||
| { | ||
| public interface ISubscription<T> : IDisposable | ||
| { | ||
| } | ||
| } |
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| using StarMap.API.Events; | ||
|
|
||
| namespace StarMap.API.Services | ||
| { | ||
| public interface IEventBus | ||
| { | ||
| IEventPublisher<T> GetPublisher<T>(); | ||
| IEventPublisher<T> GetPublisher<T>(string eventName); | ||
|
|
||
| ISubscription<T> Subscribe<T>(Action<T> handler); | ||
| ISubscription<T> Subscribe<T>(Action<T> handler, string eventName); | ||
| } | ||
| } |
This file was deleted.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| using StarMap.API.Events; | ||
| using StarMap.API.Services; | ||
| using StarMap.Core.ModRepository; | ||
| using StarMap.Core.Services; | ||
|
|
||
| namespace StarMap.Core.API | ||
| { | ||
| internal class EventBus : IEventBus | ||
| { | ||
| private readonly RuntimeMod _mod; | ||
| private readonly IMediator _mediator; | ||
|
|
||
| public EventBus(IModContext modContext, IMediator mediator) | ||
| { | ||
| _mod = modContext.Mod; | ||
| _mediator = mediator; | ||
| } | ||
|
|
||
| public IEventPublisher<T> GetPublisher<T>() | ||
| { | ||
| return _mediator.GetPublisher<T>(); | ||
| } | ||
|
|
||
| public IEventPublisher<T> GetPublisher<T>(string eventName) | ||
| { | ||
| return _mediator.GetPublisher<T>(eventName); | ||
| } | ||
|
|
||
| public ISubscription<T> Subscribe<T>(Action<T> handler) | ||
| { | ||
| return _mediator.Subscribe(handler); | ||
| } | ||
|
|
||
| public ISubscription<T> Subscribe<T>(Action<T> handler, string eventName) | ||
| { | ||
| return _mediator.Subscribe(handler, eventName); | ||
| } | ||
| } | ||
| } |
This file was deleted.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,154 +1,164 @@ | ||
| using StarMap.API.Mediation; | ||
| using StarMap.Core.ModRepository; | ||
| using StarMap.API.Events; | ||
| using System.Collections.Concurrent; | ||
| using System.Runtime.Loader; | ||
|
|
||
| namespace StarMap.Core.Services | ||
| { | ||
| internal interface IMediator | ||
| { | ||
| IEventPublisher<T> RegisterPublisher<T>(RuntimeMod mod) where T : IStarMapEvent; | ||
| ISubscription<T> Subscribe<T>(Action<T> action) where T : IStarMapEvent; | ||
| } | ||
| IEventPublisher<T> GetPublisher<T>(); | ||
| IEventPublisher<T> GetPublisher<T>(string eventName); | ||
|
|
||
| public interface IEventPublisher { } | ||
| ISubscription<T> Subscribe<T>(Action<T> handler); | ||
| ISubscription<T> Subscribe<T>(Action<T> handler, string eventName); | ||
| } | ||
|
|
||
| internal class SubscriptionHandle<T> : ISubscription<T> where T : IStarMapEvent | ||
| internal class Mediator : IMediator | ||
| { | ||
| private readonly EventPublisher<T> _publisher; | ||
| private interface IEventBus { } | ||
|
|
||
| private readonly Action<T> _action; | ||
| private readonly ConcurrentDictionary<Type, IEventBus> _busses = []; | ||
|
|
||
| public SubscriptionHandle(EventPublisher<T> publisher, Action<T> action) | ||
| private TypeEventBus<T> GetTypeBus<T>() | ||
| { | ||
| _publisher = publisher; | ||
| _action = action; | ||
| var bus = _busses.GetOrAdd(typeof(T), _ => new TypeEventBus<T>()); | ||
|
|
||
| if (bus is TypeEventBus<T> typeBus) | ||
| return typeBus; | ||
|
|
||
| throw new InvalidOperationException("Something went wrong in bus management"); | ||
| } | ||
|
|
||
| public void Invoke(T message) | ||
| public IEventPublisher<T> GetPublisher<T>() | ||
| { | ||
| _action.Invoke(message); | ||
| return GetTypeBus<T>().CreatePublisher(); | ||
| } | ||
|
|
||
| public void Dispose() | ||
| public IEventPublisher<T> GetPublisher<T>(string eventName) | ||
| { | ||
| _publisher.Unsubscribe(this); | ||
| return GetTypeBus<T>().GetNamedBus(eventName).CreatePublisher(); | ||
| } | ||
| } | ||
|
|
||
| internal class EventPublisher<T>(Mediator mediator) : IEventPublisher<T>, IEventPublisher where T : IStarMapEvent | ||
| { | ||
| private readonly Lock _lock = new(); | ||
|
|
||
| private bool _isDisposed; | ||
|
|
||
| private readonly HashSet<SubscriptionHandle<T>> _handlers = []; | ||
|
|
||
| public ISubscription<T> Subscribe(Action<T> action) | ||
| public ISubscription<T> Subscribe<T>(Action<T> handler) | ||
| { | ||
| var handle = new SubscriptionHandle<T>(this, action); | ||
|
|
||
| lock (_lock) | ||
| { | ||
| ObjectDisposedException.ThrowIf(_isDisposed, this); | ||
| _handlers.Add(handle); | ||
| } | ||
|
|
||
| return handle; | ||
| return GetTypeBus<T>().Subscribe(handler); | ||
| } | ||
|
|
||
| public void Unsubscribe(SubscriptionHandle<T> handle) | ||
| public ISubscription<T> Subscribe<T>(Action<T> handler, string eventName) | ||
| { | ||
| lock (_lock) | ||
| { | ||
| if (_isDisposed) | ||
| return; | ||
|
|
||
| _handlers.Remove(handle); | ||
| } | ||
| return GetTypeBus<T>().GetNamedBus(eventName).Subscribe(handler); | ||
| } | ||
|
|
||
| public void Publish(T message) | ||
| private class TypeEventBus<T>() : EventBus<T>() | ||
| { | ||
| foreach (var handler in GetHandlersSnapshot()) | ||
| private readonly ConcurrentDictionary<string, NamedEventBus<T>> _namedBusses = []; | ||
|
|
||
| public EventBus<T> GetNamedBus(string eventName) | ||
| { | ||
| handler.Invoke(message); | ||
| return _namedBusses.GetOrAdd(eventName, (_) => new NamedEventBus<T>()); | ||
| } | ||
| } | ||
|
|
||
| public Task PublishParallel(T message) | ||
| private class NamedEventBus<T>() : EventBus<T>() { } | ||
|
|
||
| private abstract class EventBus<T>() : IEventBus | ||
| { | ||
| var tasks = new List<Task>(); | ||
| private readonly Lock _lock = new(); | ||
|
|
||
|
|
||
| foreach (var handler in GetHandlersSnapshot()) | ||
| private HashSet<SubscriptionHandle<T>> _subscription = []; | ||
| private HashSet<IEventPublisher<T>> _publishers = []; | ||
|
|
||
| public IEventPublisher<T> CreatePublisher() | ||
| { | ||
| tasks.Add(Task.Run(() => handler.Invoke(message))); | ||
| lock (_lock) | ||
| { | ||
| var publisher = new Publisher<T>(this); | ||
| _publishers.Add(publisher); | ||
| return publisher; | ||
| } | ||
| } | ||
|
|
||
| return Task.WhenAll(tasks); | ||
| } | ||
| public void RemovePublisher(IEventPublisher<T> publisher) | ||
| { | ||
| lock (_lock) | ||
| { | ||
| _publishers.Remove(publisher); | ||
| } | ||
| } | ||
|
|
||
| private SubscriptionHandle<T>[] GetHandlersSnapshot() | ||
| { | ||
| lock (_lock) | ||
| public ISubscription<T> Subscribe(Action<T> action) | ||
| { | ||
| if (_isDisposed) | ||
| return []; | ||
| lock (_lock) | ||
| { | ||
| var subHandle = new SubscriptionHandle<T>(this, action); | ||
| _subscription.Add(subHandle); | ||
| return subHandle; | ||
| } | ||
| } | ||
|
|
||
| return [.. _handlers]; | ||
| public void Unsubscribe(SubscriptionHandle<T> handle) | ||
| { | ||
| lock (_lock) | ||
| { | ||
| _subscription.Remove(handle); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| public void Dispose() | ||
| { | ||
| lock (_lock) | ||
| public void Publish(T message) | ||
| { | ||
| if (_isDisposed) | ||
| return; | ||
| SubscriptionHandle<T>[] subscriptions; | ||
|
|
||
| _isDisposed = true; | ||
| lock (_lock) | ||
| { | ||
| subscriptions = [.. _subscription]; | ||
| } | ||
|
|
||
| mediator.UnregisterPublisher<T>(this); | ||
| _handlers.Clear(); | ||
| foreach (var sub in subscriptions) | ||
| { | ||
| sub.Invoke(message); | ||
| } | ||
| } | ||
|
|
||
| } | ||
| } | ||
|
|
||
| internal class Mediator : IMediator | ||
| { | ||
| private readonly ConcurrentDictionary<Type, IEventPublisher> _publishers = []; | ||
|
|
||
| public IEventPublisher<T> RegisterPublisher<T>(RuntimeMod mod) where T : IStarMapEvent | ||
| private class Publisher<T>(EventBus<T> bus) : IEventPublisher<T> | ||
| { | ||
| 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"); | ||
| private bool _isDisposed; | ||
|
|
||
| var publisher = new EventPublisher<T>(this); | ||
| public void Publish(T message) | ||
| { | ||
| ObjectDisposedException.ThrowIf(Volatile.Read(ref _isDisposed), this); | ||
| bus.Publish(message); | ||
| } | ||
|
|
||
| if (!_publishers.TryAdd(type, publisher)) | ||
| throw new Exception($"A publisher for type '{typeof(T)}' was already registered"); | ||
| public void Dispose() | ||
| { | ||
| if (Interlocked.Exchange(ref _isDisposed, true)) | ||
| return; | ||
|
|
||
| return publisher; | ||
| bus.RemovePublisher(this); | ||
| } | ||
| } | ||
|
|
||
| public void UnregisterPublisher<T>(IEventPublisher publisher) | ||
| private class SubscriptionHandle<T> : ISubscription<T> | ||
| { | ||
| _publishers.TryRemove(new(typeof(T), publisher)); | ||
| } | ||
| private readonly EventBus<T> _bus; | ||
| private readonly Action<T> _action; | ||
|
|
||
| public ISubscription<T> Subscribe<T>(Action<T> action) where T : IStarMapEvent | ||
| { | ||
| if (!_publishers.TryGetValue(typeof(T), out var publisher)) | ||
| public SubscriptionHandle(EventBus<T> bus, Action<T> action) | ||
| { | ||
| throw new Exception("A publisher for this event was not found"); | ||
| _bus = bus; | ||
| _action = action; | ||
| } | ||
|
|
||
| if (publisher is not EventPublisher<T> typedPublisher) | ||
| throw new Exception("Something went wrong in the type of the publisher"); | ||
| public void Invoke(T message) | ||
| { | ||
| _action.Invoke(message); | ||
| } | ||
|
|
||
| return typedPublisher.Subscribe(action); | ||
| public void Dispose() | ||
| { | ||
| _bus.Unsubscribe(this); | ||
| } | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Currently busses remain in memory forever to reduce any thread risk coming with disposing and creation. In a future update this can be added to reduce memory, but the retained memory should be minimal. With no disposal, busses cannot be disconnected so no issues should arise