Skip to content

Commit 3dc8e69

Browse files
committed
Refactor EventBus to retain empty event-type buckets
1 parent b83cad9 commit 3dc8e69

1 file changed

Lines changed: 15 additions & 34 deletions

File tree

src/LoreSoft.Blazor.Controls/Events/EventBus.cs

Lines changed: 15 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ namespace LoreSoft.Blazor.Controls.Events;
1212
/// </para>
1313
/// <para>
1414
/// Events are dispatched based on their type, and all handlers for a given event type are invoked
15-
/// concurrently when the event is published. Empty subscriptions (with no remaining handlers) are
16-
/// automatically removed from the event bus.
15+
/// concurrently when the event is published. Event-type buckets may remain cached after all handlers
16+
/// are removed; individual handler entries are weakly held and pruned by the underlying event store.
1717
/// </para>
1818
/// <para>
1919
/// All operations are thread-safe and use concurrent collections internally.
@@ -39,8 +39,8 @@ public void Subscribe<TEvent>(Func<TEvent, ValueTask> handler)
3939

4040
var messageType = typeof(TEvent);
4141

42-
var subscription = _subscriptions.GetOrAdd(messageType, _ => new WeakEvent<TEvent>()) as WeakEvent<TEvent>;
43-
subscription?.Subscribe(handler);
42+
if (_subscriptions.GetOrAdd(messageType, _ => new WeakEvent<TEvent>()) is WeakEvent<TEvent> subscription)
43+
subscription.Subscribe(handler);
4444
}
4545

4646
/// <summary>
@@ -60,8 +60,8 @@ public void Subscribe<TEvent>(Func<TEvent, CancellationToken, ValueTask> handler
6060

6161
var messageType = typeof(TEvent);
6262

63-
var subscription = _subscriptions.GetOrAdd(messageType, _ => new WeakEvent<TEvent>()) as WeakEvent<TEvent>;
64-
subscription?.Subscribe(handler);
63+
if (_subscriptions.GetOrAdd(messageType, _ => new WeakEvent<TEvent>()) is WeakEvent<TEvent> subscription)
64+
subscription.Subscribe(handler);
6565
}
6666

6767
/// <summary>
@@ -71,21 +71,18 @@ public void Subscribe<TEvent>(Func<TEvent, CancellationToken, ValueTask> handler
7171
/// <param name="handler">The asynchronous function to unsubscribe. Cannot be <see langword="null"/>.</param>
7272
/// <exception cref="ArgumentNullException">Thrown when <paramref name="handler"/> is <see langword="null"/>.</exception>
7373
/// <remarks>
74-
/// If this was the last handler for the event type, the subscription is removed from the event bus.
74+
/// Removing the last handler clears the underlying event store, but the event-type bucket may remain cached.
7575
/// </remarks>
7676
public void Unsubscribe<TEvent>(Func<TEvent, ValueTask> handler)
7777
{
7878
ArgumentNullException.ThrowIfNull(handler);
7979

8080
var messageType = typeof(TEvent);
81-
if (_subscriptions.GetOrAdd(messageType, _ => new WeakEvent<TEvent>()) is not WeakEvent<TEvent> subscription)
82-
return;
8381

84-
var remaining = subscription.Unsubscribe(handler);
85-
if (remaining > 0)
82+
if (!_subscriptions.TryGetValue(messageType, out var weakEvent) || weakEvent is not WeakEvent<TEvent> subscription)
8683
return;
8784

88-
_subscriptions.TryRemove(messageType, out _);
85+
subscription.Unsubscribe(handler);
8986
}
9087

9188
/// <summary>
@@ -95,21 +92,18 @@ public void Unsubscribe<TEvent>(Func<TEvent, ValueTask> handler)
9592
/// <param name="handler">The asynchronous function that accepts a <see cref="CancellationToken"/> to unsubscribe. Cannot be <see langword="null"/>.</param>
9693
/// <exception cref="ArgumentNullException">Thrown when <paramref name="handler"/> is <see langword="null"/>.</exception>
9794
/// <remarks>
98-
/// If this was the last handler for the event type, the subscription is removed from the event bus.
95+
/// Removing the last handler clears the underlying event store, but the event-type bucket may remain cached.
9996
/// </remarks>
10097
public void Unsubscribe<TEvent>(Func<TEvent, CancellationToken, ValueTask> handler)
10198
{
10299
ArgumentNullException.ThrowIfNull(handler);
103100

104101
var messageType = typeof(TEvent);
105-
if (_subscriptions.GetOrAdd(messageType, _ => new WeakEvent<TEvent>()) is not WeakEvent<TEvent> subscription)
106-
return;
107102

108-
var remaining = subscription.Unsubscribe(handler);
109-
if (remaining > 0)
103+
if (!_subscriptions.TryGetValue(messageType, out var weakEvent) || weakEvent is not WeakEvent<TEvent> subscription)
110104
return;
111105

112-
_subscriptions.TryRemove(messageType, out _);
106+
subscription.Unsubscribe(handler);
113107
}
114108

115109
/// <summary>
@@ -119,31 +113,18 @@ public void Unsubscribe<TEvent>(Func<TEvent, CancellationToken, ValueTask> handl
119113
/// <exception cref="ArgumentNullException">Thrown when <paramref name="subscriber"/> is <see langword="null"/>.</exception>
120114
/// <remarks>
121115
/// This method removes all handlers across all event types that belong to the specified subscriber.
122-
/// Empty subscriptions (with no remaining handlers) are automatically removed from the event bus.
116+
/// Event-type buckets may remain cached after all matching handlers are removed.
123117
/// </remarks>
124118
public void Unsubscribe(object subscriber)
125119
{
126120
ArgumentNullException.ThrowIfNull(subscriber);
127121

128-
List<Type>? emptySubscriptions = null;
129122
foreach (var pair in _subscriptions)
130123
{
131-
var subscriberType = pair.Key;
132124
var subscription = pair.Value;
133125

134-
var remaining = subscription.Unsubscribe(subscriber);
135-
if (remaining > 0)
136-
continue;
137-
138-
emptySubscriptions ??= [];
139-
emptySubscriptions.Add(subscriberType);
126+
subscription.Unsubscribe(subscriber);
140127
}
141-
142-
if (emptySubscriptions == null || emptySubscriptions.Count == 0)
143-
return;
144-
145-
foreach (var type in emptySubscriptions)
146-
_subscriptions.TryRemove(type, out _);
147128
}
148129

149130
/// <summary>
@@ -162,7 +143,7 @@ public void Unsubscribe(object subscriber)
162143
/// <exception cref="ArgumentNullException">Thrown when <paramref name="eventData"/> is <see langword="null"/>.</exception>
163144
/// <remarks>
164145
/// <para>
165-
/// Dead handlers (whose targets have been garbage collected) are automatically removed before invocation.
146+
/// Dead handlers (whose targets have been garbage collected) are skipped during invocation and may be pruned asynchronously.
166147
/// </para>
167148
/// <para>
168149
/// All handlers are invoked concurrently, and this method awaits all of them to complete.

0 commit comments

Comments
 (0)