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

@ -15,9 +15,9 @@ public sealed class GameEventDispatcherTests
{
byte[] body = new byte[GameEventEnvelope.HeaderSize + payload.Length];
var span = body.AsSpan();
BinaryPrimitives.WriteUInt32LittleEndian(span, GameEventEnvelope.Opcode);
BinaryPrimitives.WriteUInt32LittleEndian(span.Slice(4), playerGuid);
BinaryPrimitives.WriteUInt32LittleEndian(span.Slice(8), sequence);
BinaryPrimitives.WriteUInt32LittleEndian(span, GameEventEnvelope.Opcode);
BinaryPrimitives.WriteUInt32LittleEndian(span.Slice(4), playerGuid);
BinaryPrimitives.WriteUInt32LittleEndian(span.Slice(8), sequence);
BinaryPrimitives.WriteUInt32LittleEndian(span.Slice(12), (uint)type);
payload.CopyTo(span.Slice(GameEventEnvelope.HeaderSize));
return body;
@ -106,6 +106,162 @@ public sealed class GameEventDispatcherTests
Assert.Equal(1, dispatcher.GetUnhandledCount(GameEventType.Tell));
}
[Fact]
public void OwnedRegistration_DisposeRestoresUnownedPredecessor()
{
var dispatcher = new GameEventDispatcher();
var calls = new List<string>();
dispatcher.Register(GameEventType.Tell, _ => calls.Add("baseline"));
IDisposable owned = dispatcher.RegisterOwned(
GameEventType.Tell,
_ => calls.Add("owned"));
dispatcher.Dispatch(new GameEventEnvelope(0, 0, GameEventType.Tell, default));
owned.Dispose();
dispatcher.Dispatch(new GameEventEnvelope(0, 0, GameEventType.Tell, default));
Assert.Equal(["owned", "baseline"], calls);
Assert.Equal(1, dispatcher.RegisteredHandlerCount);
}
[Fact]
public void NestedOwnedRegistrations_DisposeOlderFirst_SkipsRetiredPredecessor()
{
var dispatcher = new GameEventDispatcher();
var calls = new List<string>();
dispatcher.Register(GameEventType.Tell, _ => calls.Add("baseline"));
IDisposable ownerA = dispatcher.RegisterOwned(
GameEventType.Tell,
_ => calls.Add("A"));
IDisposable ownerB = dispatcher.RegisterOwned(
GameEventType.Tell,
_ => calls.Add("B"));
ownerA.Dispose();
dispatcher.Dispatch(new GameEventEnvelope(0, 0, GameEventType.Tell, default));
ownerB.Dispose();
dispatcher.Dispatch(new GameEventEnvelope(0, 0, GameEventType.Tell, default));
Assert.Equal(["B", "baseline"], calls);
}
[Fact]
public void NestedOwnedRegistrations_DisposeNewestFirst_RestoresLiveOlderOwner()
{
var dispatcher = new GameEventDispatcher();
var calls = new List<string>();
dispatcher.Register(GameEventType.Tell, _ => calls.Add("baseline"));
IDisposable ownerA = dispatcher.RegisterOwned(
GameEventType.Tell,
_ => calls.Add("A"));
IDisposable ownerB = dispatcher.RegisterOwned(
GameEventType.Tell,
_ => calls.Add("B"));
ownerB.Dispose();
dispatcher.Dispatch(new GameEventEnvelope(0, 0, GameEventType.Tell, default));
ownerA.Dispose();
ownerA.Dispose();
ownerB.Dispose();
dispatcher.Dispatch(new GameEventEnvelope(0, 0, GameEventType.Tell, default));
Assert.Equal(["A", "baseline"], calls);
}
[Fact]
public void LaterUnownedReplacement_SurvivesOwnedTokenDisposal()
{
var dispatcher = new GameEventDispatcher();
var calls = new List<string>();
IDisposable owned = dispatcher.RegisterOwned(
GameEventType.Tell,
_ => calls.Add("owned"));
dispatcher.Register(GameEventType.Tell, _ => calls.Add("replacement"));
owned.Dispose();
dispatcher.Dispatch(new GameEventEnvelope(0, 0, GameEventType.Tell, default));
Assert.Equal(["replacement"], calls);
}
[Fact]
public void OwnedRegistrationWithoutPredecessor_BecomesUnhandledAfterDispose()
{
var dispatcher = new GameEventDispatcher();
IDisposable owned = dispatcher.RegisterOwned(GameEventType.Tell, _ => { });
owned.Dispose();
owned.Dispose();
dispatcher.Dispatch(new GameEventEnvelope(0, 0, GameEventType.Tell, default));
Assert.Equal(0, dispatcher.RegisteredHandlerCount);
Assert.Equal(1, dispatcher.GetUnhandledCount(GameEventType.Tell));
}
[Fact]
public void ExplicitUnregister_RetiresCurrentChainWithoutResurrection()
{
var dispatcher = new GameEventDispatcher();
dispatcher.Register(GameEventType.Tell, _ => { });
IDisposable ownerA = dispatcher.RegisterOwned(GameEventType.Tell, _ => { });
IDisposable ownerB = dispatcher.RegisterOwned(GameEventType.Tell, _ => { });
dispatcher.Unregister(GameEventType.Tell);
ownerA.Dispose();
ownerB.Dispose();
dispatcher.Dispatch(new GameEventEnvelope(0, 0, GameEventType.Tell, default));
Assert.Equal(0, dispatcher.RegisteredHandlerCount);
Assert.Equal(1, dispatcher.GetUnhandledCount(GameEventType.Tell));
}
[Fact]
public void HandlerCanDisposeItselfAndInstallRawReplacementDuringDispatch()
{
var dispatcher = new GameEventDispatcher();
var calls = new List<string>();
IDisposable? owned = null;
owned = dispatcher.RegisterOwned(GameEventType.Tell, _ =>
{
calls.Add("owned");
owned!.Dispose();
dispatcher.Register(GameEventType.Tell, _ => calls.Add("replacement"));
});
dispatcher.Dispatch(new GameEventEnvelope(0, 0, GameEventType.Tell, default));
dispatcher.Dispatch(new GameEventEnvelope(0, 0, GameEventType.Tell, default));
Assert.Equal(["owned", "replacement"], calls);
Assert.Equal(1, dispatcher.RegisteredHandlerCount);
}
[Fact]
public void RawReplacementAfterNestedOwners_SurvivesEveryOwnedDisposeOrder()
{
var dispatcher = new GameEventDispatcher();
var calls = new List<string>();
IDisposable ownerA = dispatcher.RegisterOwned(GameEventType.Tell, _ => calls.Add("A"));
IDisposable ownerB = dispatcher.RegisterOwned(GameEventType.Tell, _ => calls.Add("B"));
dispatcher.Register(GameEventType.Tell, _ => calls.Add("raw"));
ownerA.Dispose();
ownerB.Dispose();
dispatcher.Dispatch(new GameEventEnvelope(0, 0, GameEventType.Tell, default));
Assert.Equal(["raw"], calls);
}
[Fact]
public void OwnedRegistration_ConcurrentDisposeRetiresExactlyOnce()
{
var dispatcher = new GameEventDispatcher();
IDisposable owned = dispatcher.RegisterOwned(GameEventType.Tell, _ => { });
Parallel.For(0, 64, _ => owned.Dispose());
Assert.Equal(0, dispatcher.RegisteredHandlerCount);
}
// ── Per-event parser tests ───────────────────────────────────────────────
private static byte[] MakeString16L(string s)