feat(ablegacy): implement IBatchSubscribable

Review 02 P1 / Wave 2 rule (d): AbLegacy runs PollGroupEngine but raised only
the per-tag OnDataChange, so every changed tag paid its own trip through the
actor chain. The engine's per-cycle batch is now raised as OnDataChangeBatch
(one per poll cycle), alongside the per-tag event for legacy consumers.

Refs #738
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
This commit is contained in:
Joseph Doherty
2026-10-22 14:42:23 -04:00
parent 530f4bdcd4
commit 582e82899b
2 changed files with 59 additions and 1 deletions
@@ -12,7 +12,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy;
/// <see cref="IDriver"/> only at PR 1 time; read / write / discovery / subscribe / probe /
/// host-resolver capabilities ship in PRs 2 and 3.
/// </summary>
public sealed partial class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscovery, ISubscribable,
public sealed partial class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscovery, IBatchSubscribable,
IHostConnectivityProbe, IPerCallHostResolver, ISkippedTagReporter, IDisposable, IAsyncDisposable
{
/// <summary>Authored tags this driver could not build into its poll table.</summary>
@@ -64,6 +64,12 @@ public sealed partial class AbLegacyDriver : IDriver, IReadable, IWritable, ITag
/// </summary>
public event EventHandler<DataChangeEventArgs>? OnDataChange;
/// <summary>
/// One raise per poll cycle carrying every tag that changed in it (review 02 P1). Raised alongside the per-tag
/// <see cref="OnDataChange"/>; a consumer subscribes to exactly one of the two.
/// </summary>
public event EventHandler<DataChangeBatchEventArgs>? OnDataChangeBatch;
/// <summary>
/// Occurs when host status changes.
/// </summary>
@@ -99,6 +105,8 @@ public sealed partial class AbLegacyDriver : IDriver, IReadable, IWritable, ITag
OnDataChange?.Invoke(this, new DataChangeEventArgs(handle, tagRef, snapshot)),
onError: HandlePollError,
backoffCap: PollBackoffCap,
onBatch: (handle, items) =>
OnDataChangeBatch?.Invoke(this, new DataChangeBatchEventArgs(handle, items)),
// The engine owns no Meter (Core.Abstractions takes no metrics dependency), so this
// closure IS the production consumer of its overrun signal.
onOverrun: _ => OtOpcUaTelemetry.PollOverruns.Add(1,