feat(core): add IBatchSubscribable batch data-change contract

This commit is contained in:
Joseph Doherty
2026-08-15 12:06:53 -04:00
parent 477cb10ff2
commit b857df8a12
2 changed files with 53 additions and 0 deletions
@@ -51,3 +51,29 @@ public sealed record DataChangeEventArgs(
ISubscriptionHandle SubscriptionHandle,
string FullReference,
DataValueSnapshot Snapshot);
/// <summary>One changed attribute within a <see cref="DataChangeBatchEventArgs"/>.</summary>
/// <param name="FullReference">Driver-side full reference of the changed attribute.</param>
/// <param name="Snapshot">New value + quality + timestamps.</param>
public sealed record DataChangeItem(string FullReference, DataValueSnapshot Snapshot);
/// <summary>Event payload for <see cref="IBatchSubscribable.OnDataChangeBatch"/> — every attribute
/// that changed within ONE production burst (a poll cycle, an inbound message, a stream chunk).</summary>
/// <param name="SubscriptionHandle">The handle returned by the original <see cref="ISubscribable.SubscribeAsync"/> call.</param>
/// <param name="Items">The changed attributes, one entry per changed tag. Never empty.</param>
public sealed record DataChangeBatchEventArgs(
ISubscriptionHandle SubscriptionHandle,
IReadOnlyList<DataChangeItem> Items);
/// <summary>
/// Optional driver capability: data changes delivered one event per production burst instead of
/// one event per tag. A driver implementing this raises <see cref="OnDataChangeBatch"/> once per
/// burst AND continues raising the per-tag <see cref="ISubscribable.OnDataChange"/> for each item
/// (legacy consumers: Galaxy cold reads, driver CLIs). A consumer subscribes to EXACTLY ONE of the
/// two events — subscribing both double-delivers. DriverInstanceActor prefers the batch event.
/// </summary>
public interface IBatchSubscribable : ISubscribable
{
/// <summary>Batched data-change notification — one raise per production burst.</summary>
event EventHandler<DataChangeBatchEventArgs>? OnDataChangeBatch;
}
@@ -0,0 +1,27 @@
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
namespace ZB.MOM.WW.OtOpcUa.Core.Tests;
/// <summary>Contract tests for the batched data-change payload types.</summary>
[Trait("Category", "Unit")]
public sealed class DataChangeBatchContractTests
{
private sealed record FakeHandle(string DiagnosticId) : ISubscriptionHandle;
/// <summary>Verifies the batch payload carries the subscription handle and its per-tag items.</summary>
[Fact]
public void Batch_args_carry_handle_and_items()
{
var handle = new FakeHandle("h1");
var snap = new DataValueSnapshot(42, 0u, DateTime.UtcNow, DateTime.UtcNow);
var args = new DataChangeBatchEventArgs(handle, new[] { new DataChangeItem("plc/tag1", snap) });
args.SubscriptionHandle.ShouldBe(handle);
args.Items.Count.ShouldBe(1);
args.Items[0].FullReference.ShouldBe("plc/tag1");
args.Items[0].Snapshot.ShouldBe(snap);
}
}