feat(dashboard): in-process session event subscription feeds the viewer registry — SessionDetailsPage drops its /hubs/events hop
This commit is contained in:
@@ -121,6 +121,140 @@ public sealed class DashboardEventBroadcasterTests
|
||||
Assert.NotNull(sent.OnAlarmTransition.LimitValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An in-process subscriber gets the same redacted clone the hub group gets,
|
||||
/// and the shared source event is still left untouched.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Subscribe_WhenShowTagValuesFalse_DeliversRedactedCloneWithoutMutatingSource()
|
||||
{
|
||||
CapturingHubContext hubContext = new();
|
||||
EventsHubViewerRegistry viewers = new();
|
||||
DashboardEventBroadcaster broadcaster = Create(hubContext, showTagValues: false, viewers);
|
||||
using IDashboardEventSubscription subscription = broadcaster.Subscribe("session-1");
|
||||
MxEvent source = BuildEventWithValue();
|
||||
|
||||
broadcaster.Publish("session-1", source);
|
||||
|
||||
MxEvent received = ReadOne(subscription);
|
||||
Assert.Null(received.Value);
|
||||
Assert.Null(received.OnAlarmTransition.CurrentValue);
|
||||
Assert.Null(received.OnAlarmTransition.LimitValue);
|
||||
Assert.Equal("Tank01.Level.HiHi", received.OnAlarmTransition.AlarmFullReference);
|
||||
|
||||
// One clone feeds both audiences — the hub group and the in-process feed.
|
||||
Assert.Same(hubContext.LastArgument, received);
|
||||
|
||||
// The source is shared with the gRPC stream and the replay ring.
|
||||
Assert.NotSame(source, received);
|
||||
Assert.NotNull(source.Value);
|
||||
Assert.Equal(42.5, source.Value.DoubleValue);
|
||||
Assert.NotNull(source.OnAlarmTransition.CurrentValue);
|
||||
Assert.NotNull(source.OnAlarmTransition.LimitValue);
|
||||
}
|
||||
|
||||
/// <summary>An in-process subscription opens the viewer gate the same way a hub client does.</summary>
|
||||
[Fact]
|
||||
public void Subscribe_OpensTheViewerGate()
|
||||
{
|
||||
CapturingHubContext hubContext = new();
|
||||
EventsHubViewerRegistry viewers = new();
|
||||
DashboardEventBroadcaster broadcaster = Create(hubContext, showTagValues: false, viewers);
|
||||
|
||||
broadcaster.Publish("session-1", BuildEventWithValue());
|
||||
Assert.Equal(0, hubContext.SendCount);
|
||||
Assert.Null(hubContext.LastArgument);
|
||||
|
||||
using IDashboardEventSubscription subscription = broadcaster.Subscribe("session-1");
|
||||
|
||||
Assert.True(viewers.HasViewers("session-1"));
|
||||
|
||||
broadcaster.Publish("session-1", BuildEventWithValue());
|
||||
|
||||
Assert.Equal(1, hubContext.SendCount);
|
||||
Assert.NotNull(hubContext.LastArgument);
|
||||
}
|
||||
|
||||
/// <summary>Disposing the last in-process subscription restores the no-viewers short-circuit.</summary>
|
||||
[Fact]
|
||||
public void Dispose_OfLastInProcessSubscription_RestoresTheShortCircuit()
|
||||
{
|
||||
CapturingHubContext hubContext = new();
|
||||
EventsHubViewerRegistry viewers = new();
|
||||
DashboardEventBroadcaster broadcaster = Create(hubContext, showTagValues: false, viewers);
|
||||
IDashboardEventSubscription subscription = broadcaster.Subscribe("session-1");
|
||||
|
||||
broadcaster.Publish("session-1", BuildEventWithValue());
|
||||
Assert.Equal(1, hubContext.SendCount);
|
||||
Assert.Equal("session-1", ReadOne(subscription).SessionId);
|
||||
|
||||
subscription.Dispose();
|
||||
|
||||
Assert.False(viewers.HasViewers("session-1"));
|
||||
|
||||
broadcaster.Publish("session-1", BuildEventWithValue());
|
||||
|
||||
Assert.Equal(1, hubContext.SendCount);
|
||||
Assert.False(subscription.Reader.TryRead(out _));
|
||||
}
|
||||
|
||||
/// <summary>Hub viewers and in-process subscribers are audiences of their own session only.</summary>
|
||||
[Fact]
|
||||
public void Publish_DeliversOnlyToTheSubscribedSessionsAudience()
|
||||
{
|
||||
CapturingHubContext hubContext = new();
|
||||
EventsHubViewerRegistry viewers = new();
|
||||
viewers.AddViewer("conn-1", "session-1");
|
||||
DashboardEventBroadcaster broadcaster = Create(hubContext, showTagValues: false, viewers);
|
||||
using IDashboardEventSubscription subscription = broadcaster.Subscribe("session-2");
|
||||
|
||||
// The hub viewer's session must not spill into the in-process feed.
|
||||
broadcaster.Publish("session-1", BuildEventWithValue());
|
||||
|
||||
Assert.Equal(1, hubContext.SendCount);
|
||||
Assert.False(subscription.Reader.TryRead(out _));
|
||||
|
||||
broadcaster.Publish("session-2", BuildEventWithValue("session-2"));
|
||||
|
||||
Assert.Equal("session-2", ReadOne(subscription).SessionId);
|
||||
|
||||
// A session with no audience at all still short-circuits.
|
||||
broadcaster.Publish("session-3", BuildEventWithValue("session-3"));
|
||||
|
||||
Assert.Equal(2, hubContext.SendCount);
|
||||
}
|
||||
|
||||
/// <summary>Disposing twice is a no-op and cannot release a sibling subscription's registration.</summary>
|
||||
[Fact]
|
||||
public void Dispose_CalledTwice_IsSafeAndLeavesSiblingSubscriptionsAlone()
|
||||
{
|
||||
CapturingHubContext hubContext = new();
|
||||
EventsHubViewerRegistry viewers = new();
|
||||
DashboardEventBroadcaster broadcaster = Create(hubContext, showTagValues: false, viewers);
|
||||
IDashboardEventSubscription first = broadcaster.Subscribe("session-1");
|
||||
using IDashboardEventSubscription second = broadcaster.Subscribe("session-1");
|
||||
|
||||
first.Dispose();
|
||||
first.Dispose();
|
||||
|
||||
Assert.True(viewers.HasViewers("session-1"));
|
||||
|
||||
broadcaster.Publish("session-1", BuildEventWithValue());
|
||||
|
||||
Assert.Equal(1, hubContext.SendCount);
|
||||
Assert.False(first.Reader.TryRead(out _));
|
||||
Assert.Equal("session-1", ReadOne(second).SessionId);
|
||||
}
|
||||
|
||||
/// <summary>Reads exactly one event from a subscription, failing the test if none is queued.</summary>
|
||||
/// <param name="subscription">The subscription to read from.</param>
|
||||
/// <returns>The event that was read.</returns>
|
||||
private static MxEvent ReadOne(IDashboardEventSubscription subscription)
|
||||
{
|
||||
Assert.True(subscription.Reader.TryRead(out MxEvent? received));
|
||||
return Assert.IsType<MxEvent>(received);
|
||||
}
|
||||
|
||||
private static DashboardEventBroadcaster Create(
|
||||
CapturingHubContext hubContext,
|
||||
bool showTagValues,
|
||||
@@ -147,12 +281,15 @@ public sealed class DashboardEventBroadcasterTests
|
||||
return viewers;
|
||||
}
|
||||
|
||||
private static MxEvent BuildEventWithValue()
|
||||
/// <summary>Builds a value-bearing alarm-transition event for the given session.</summary>
|
||||
/// <param name="sessionId">Session id stamped on the event.</param>
|
||||
/// <returns>The event.</returns>
|
||||
private static MxEvent BuildEventWithValue(string sessionId = "session-1")
|
||||
{
|
||||
return new MxEvent
|
||||
{
|
||||
Family = MxEventFamily.OnAlarmTransition,
|
||||
SessionId = "session-1",
|
||||
SessionId = sessionId,
|
||||
ServerHandle = 7,
|
||||
ItemHandle = 11,
|
||||
Quality = 192,
|
||||
|
||||
Reference in New Issue
Block a user