refactor(net): own session wiring subscriptions

This commit is contained in:
Erik 2026-07-21 10:45:15 +02:00
parent 88fe1db37b
commit 7d452aa6a2
8 changed files with 577 additions and 61 deletions

View file

@ -24,7 +24,27 @@ public sealed class GameEventDispatcher
{
public delegate void EventHandler(GameEventEnvelope envelope);
private readonly Dictionary<GameEventType, EventHandler> _handlers = new();
private sealed class RegistrationNode(
GameEventType type,
EventHandler handler,
RegistrationNode? previous)
{
public GameEventType Type { get; } = type;
public EventHandler Handler { get; } = handler;
public RegistrationNode? Previous { get; } = previous;
public bool Retired { get; set; }
}
private sealed class OwnedRegistration(
GameEventDispatcher owner,
RegistrationNode node) : IDisposable
{
private Action? _retire = () => owner.Retire(node);
public void Dispose() => Interlocked.Exchange(ref _retire, null)?.Invoke();
}
private readonly Dictionary<GameEventType, RegistrationNode> _handlers = new();
private readonly Dictionary<GameEventType, int> _unhandledCounts = new();
/// <summary>
@ -34,13 +54,34 @@ public sealed class GameEventDispatcher
public void Register(GameEventType type, EventHandler handler)
{
ArgumentNullException.ThrowIfNull(handler);
_handlers[type] = handler;
// Legacy replacement intentionally severs the predecessor chain. An
// exact owned token installed earlier must never remove or expose a
// later unowned replacement when that token is disposed.
_handlers[type] = new RegistrationNode(type, handler, previous: null);
}
/// <summary>
/// Installs one exact owned handler. Disposal restores the nearest live
/// predecessor only when this registration is still current. Nested A/B
/// ownership is safe in either disposal order.
/// </summary>
public IDisposable RegisterOwned(GameEventType type, EventHandler handler)
{
ArgumentNullException.ThrowIfNull(handler);
_handlers.TryGetValue(type, out RegistrationNode? previous);
var node = new RegistrationNode(type, handler, previous);
_handlers[type] = node;
return new OwnedRegistration(this, node);
}
/// <summary>
/// Remove the registered handler for a sub-opcode.
/// </summary>
public void Unregister(GameEventType type) => _handlers.Remove(type);
public void Unregister(GameEventType type)
{
if (_handlers.Remove(type, out RegistrationNode? current))
current.Retired = true;
}
/// <summary>
/// Route an envelope to its handler, or log as unhandled. Exceptions
@ -49,11 +90,11 @@ public sealed class GameEventDispatcher
/// </summary>
public void Dispatch(GameEventEnvelope envelope)
{
if (_handlers.TryGetValue(envelope.EventType, out var handler))
if (_handlers.TryGetValue(envelope.EventType, out RegistrationNode? registration))
{
try
{
handler(envelope);
registration.Handler(envelope);
}
catch (Exception ex)
{
@ -87,4 +128,21 @@ public sealed class GameEventDispatcher
/// <summary>How many distinct sub-opcodes have a handler registered.</summary>
public int RegisteredHandlerCount => _handlers.Count;
private void Retire(RegistrationNode node)
{
node.Retired = true;
if (!_handlers.TryGetValue(node.Type, out RegistrationNode? current)
|| !ReferenceEquals(current, node))
return;
RegistrationNode? predecessor = node.Previous;
while (predecessor?.Retired == true)
predecessor = predecessor.Previous;
if (predecessor is null)
_handlers.Remove(node.Type);
else
_handlers[node.Type] = predecessor;
}
}