From 39729bfe213df982b15aacac86b91df15db941e1 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Tue, 26 May 2026 05:09:01 -0400 Subject: [PATCH] feat(runtime): VirtualTagActor skeleton (engine wiring tracked as F8) --- .../VirtualTags/VirtualTagActor.cs | 41 +++++++++++++++++++ .../VirtualTags/VirtualTagActorTests.cs | 22 ++++++++++ 2 files changed, 63 insertions(+) create mode 100644 src/Server/ZB.MOM.WW.OtOpcUa.Runtime/VirtualTags/VirtualTagActor.cs create mode 100644 tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/VirtualTags/VirtualTagActorTests.cs diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/VirtualTags/VirtualTagActor.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/VirtualTags/VirtualTagActor.cs new file mode 100644 index 0000000..ffa5adb --- /dev/null +++ b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/VirtualTags/VirtualTagActor.cs @@ -0,0 +1,41 @@ +using Akka.Actor; +using Akka.Event; +using ZB.MOM.WW.OtOpcUa.Commons.Types; + +namespace ZB.MOM.WW.OtOpcUa.Runtime.VirtualTags; + +/// +/// Wraps a single virtual-tag expression. Receives dependency-tag updates, recomputes the +/// expression, and publishes the result to OpcUaPublishActor. +/// +/// Engine wiring (compile expression via VirtualTagEngine, manage subscriptions, +/// emit AttributeValueUpdate) is staged for follow-up F8. This skeleton compiles + has +/// a basic message contract so DriverHostActor can spawn it as a child. +/// +public sealed class VirtualTagActor : ReceiveActor +{ + public sealed record DependencyValueChanged(string TagId, object? Value, DateTime TimestampUtc); + public sealed record EvaluationResult(string VirtualTagId, object? Value, DateTime TimestampUtc, CorrelationId Correlation); + + private readonly string _virtualTagId; + private readonly string _expression; + private readonly ILoggingAdapter _log = Context.GetLogger(); + private readonly Dictionary _dependencies = new(StringComparer.Ordinal); + + public static Props Props(string virtualTagId, string expression) => + Akka.Actor.Props.Create(() => new VirtualTagActor(virtualTagId, expression)); + + public VirtualTagActor(string virtualTagId, string expression) + { + _virtualTagId = virtualTagId; + _expression = expression; + + Receive(msg => + { + _dependencies[msg.TagId] = msg.Value; + // Engine wiring (F8): VirtualTagEngine.Evaluate(_expression, _dependencies) → publish. + _log.Debug("VirtualTag {Id}: dependency {Tag}={Value} buffered (eval staged for F8)", + _virtualTagId, msg.TagId, msg.Value); + }); + } +} diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/VirtualTags/VirtualTagActorTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/VirtualTags/VirtualTagActorTests.cs new file mode 100644 index 0000000..d75e760 --- /dev/null +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/VirtualTags/VirtualTagActorTests.cs @@ -0,0 +1,22 @@ +using Akka.Actor; +using Shouldly; +using Xunit; +using ZB.MOM.WW.OtOpcUa.Runtime.Tests.Harness; +using ZB.MOM.WW.OtOpcUa.Runtime.VirtualTags; + +namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.VirtualTags; + +public sealed class VirtualTagActorTests : RuntimeActorTestBase +{ + [Fact] + public void DependencyValueChanged_is_accepted_and_actor_stays_alive() + { + var actor = Sys.ActorOf(VirtualTagActor.Props("vt-1", "a + b")); + Watch(actor); + actor.Tell(new VirtualTagActor.DependencyValueChanged("tag-a", 10, DateTime.UtcNow)); + actor.Tell(new VirtualTagActor.DependencyValueChanged("tag-b", 20, DateTime.UtcNow)); + + // No crash, no termination. + ExpectNoMsg(TimeSpan.FromMilliseconds(200)); + } +}