using ZB.MOM.WW.ScadaBridge.Commons.Types; namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts; /// /// Scope-aware view onto the instance's attributes, anchored at a path prefix. /// Attributes["X"] on the root scope resolves to canonical name "X"; /// on a composition with prefix "TempSensor" it resolves to "TempSensor.X". /// /// /// Thread-model note: the indexer get/set block synchronously /// on the Instance Actor Ask (and, for data-connected attributes, the DCL /// round-trip). This is safe because script bodies execute on the dedicated /// threads, not the /// shared — so a blocked accessor /// cannot starve unrelated Akka dispatchers or HTTP request handling. The async /// variants (/) are still preferred /// where the script can await, as they avoid holding a dedicated thread idle for /// the duration of each round-trip. /// /// public class AttributeAccessor { private readonly ScriptRuntimeContext _ctx; /// Canonical-name prefix, e.g. "" for root or "TempSensor" for a composition. public string ScopePrefix { get; } /// /// Initializes a new instance of the AttributeAccessor with the specified context and prefix. /// /// The script runtime context. /// The canonical-name prefix for attribute resolution. public AttributeAccessor(ScriptRuntimeContext ctx, string prefix) { _ctx = ctx; ScopePrefix = prefix; } /// /// Resolves a key to its full canonical name by applying the scope prefix. /// /// The attribute key to resolve. /// The fully qualified canonical name (e.g. "TempSensor.X" or "X" for the root scope). public string Resolve(string key) => ScopePrefix.Length == 0 ? key : ScopePrefix + "." + key; /// /// Gets or sets an attribute value synchronously by canonical name. /// /// The attribute key. public object? this[string key] { // Both reads and writes block on the actor Ask; the write also blocks // on the DCL round-trip for data-connected attributes. The async // variants (GetAsync/SetAsync) are preferred where awaiting is possible. get => _ctx.GetAttribute(Resolve(key)).GetAwaiter().GetResult(); set => _ctx.SetAttribute(Resolve(key), AttributeValueCodec.Encode(value) ?? string.Empty).GetAwaiter().GetResult(); } /// /// Gets an attribute value asynchronously. /// /// The attribute key. /// A task that resolves to the attribute value, or null if not set. public Task GetAsync(string key) => _ctx.GetAttribute(Resolve(key)); /// /// Sets an attribute value asynchronously. /// /// The attribute key. /// The value to set. /// A task that represents the asynchronous operation. public Task SetAsync(string key, object? value) => _ctx.SetAttribute(Resolve(key), AttributeValueCodec.Encode(value) ?? string.Empty); /// /// Writes a SET of data-sourced attributes to the device in ONE gateway batch /// round-trip, then writes =, /// then waits EVENT-DRIVEN (reusing the existing WaitForAttribute waiter — not a /// poll) for to reach , /// bounded by . Replaces N sequential per-attribute writes /// with one batched call before the wait. All keys are scope-resolved () /// and all values codec-encoded just like the other accessors. Returns true if /// the response was observed within the timeout, false on timeout (no throw on /// timeout); throws if the batch/trigger /// write itself fails. /// /// Attribute key → value to batch-write (keys scope-resolved, values codec-encoded). /// Trigger attribute key written AFTER the batch. /// Value to write to the trigger. /// Attribute key to wait on. /// Target value to wait for (null ⇒ any change). /// How long to wait before returning false. /// true on response match within the timeout; false on timeout. public Task WriteBatchAndWaitAsync( IReadOnlyDictionary values, string flagKey, object? flagValue, string responseKey, object? responseValue, TimeSpan timeout) { var encoded = new Dictionary(values.Count); foreach (var kv in values) encoded[Resolve(kv.Key)] = AttributeValueCodec.Encode(kv.Value); return _ctx.WriteBatchAndWait( encoded, Resolve(flagKey), AttributeValueCodec.Encode(flagValue), Resolve(responseKey), AttributeValueCodec.Encode(responseValue), timeout); } /// /// WaitForAttribute (spec §3-§5): waits event-driven until the attribute equals /// (value-equality, codec-normalized), bounded by /// . Returns true if matched within the timeout, /// false on timeout (no throw). Honors the script's execution-timeout token. /// Scope/composition path resolution () is applied just like /// / . /// /// /// Quality-agnostic by default (spec §4.2): matching tests the VALUE, not /// the quality — a value arriving at Bad quality still satisfies the wait. Pass /// :true for quality-gated ("Good"-only) /// matching: a value reaching the target at Bad/Uncertain quality is ignored and /// the wait holds until the target is reached at "Good" quality (or times out). /// /// /// /// Passing a null means "match on any change": /// the wait then matches the next value the attribute receives — and matches /// IMMEDIATELY (fast-path) if the attribute already holds any value at registration. /// /// /// The attribute key (scope-resolved before the wait is registered). /// /// The value to wait for (codec-encoded for comparison); null means /// "match on any change" (matches immediately if the attribute already has a value). /// /// How long to wait before returning false. /// /// true for quality-gated ("Good"-only) matching (spec §4.2); defaults to /// false (quality-agnostic — Bad/Uncertain-quality transients still match). /// /// true on match within the timeout; false on timeout. public Task WaitAsync(string key, object? targetValue, TimeSpan timeout, bool requireGoodQuality = false) => _ctx.WaitAttribute(Resolve(key), AttributeValueCodec.Encode(targetValue), null, timeout, requireGoodQuality); /// /// WaitForAttribute (spec §3-§5): predicate form — waits event-driven until /// returns true for the attribute's current /// value, bounded by . Site-local only (the predicate /// is an in-process delegate). Returns true if matched within the timeout, /// false on timeout (no throw). Scope/composition path resolution applies. /// /// /// Quality-agnostic by default (spec §4.2): the predicate is tested against /// the VALUE, regardless of quality — a value arriving at Bad quality still /// satisfies the wait if the predicate passes. Pass /// :true for quality-gated ("Good"-only) matching: a value satisfying the /// predicate at Bad/Uncertain quality is ignored until it does so at "Good" quality. /// /// /// The attribute key (scope-resolved before the wait is registered). /// The site-local predicate tested against the current value. /// How long to wait before returning false. /// /// true for quality-gated ("Good"-only) matching (spec §4.2); defaults to /// false (quality-agnostic). /// /// true on match within the timeout; false on timeout. public Task WaitAsync(string key, Func predicate, TimeSpan timeout, bool requireGoodQuality = false) => _ctx.WaitAttribute(Resolve(key), null, predicate, timeout, requireGoodQuality); /// /// WaitForAttribute (spec §3): richer value-equality form — like /// but returns the full /// (matched flag + matched value + quality + timed-out /// flag) instead of a bare bool. Scope/composition path resolution /// () is applied to just like the /// other accessors. Never throws on timeout — a timeout yields /// WaitResult { Matched = false, TimedOut = true }. /// /// The attribute key (scope-resolved before the wait is registered). /// /// The value to wait for (codec-encoded for comparison); null means /// "match on any change". /// /// How long to wait before returning a timed-out result. /// /// true for quality-gated ("Good"-only) matching (spec §4.2); defaults to false. /// /// The full for the wait. public Task WaitForAsync(string key, object? targetValue, TimeSpan timeout, bool requireGoodQuality = false) => _ctx.WaitAttributeFull(Resolve(key), AttributeValueCodec.Encode(targetValue), null, timeout, requireGoodQuality); /// /// WaitForAttribute (spec §3): richer predicate form — like /// but returns /// the full . Site-local only (the predicate is an /// in-process delegate). Scope/composition path resolution applies. Never throws /// on timeout (WaitResult { Matched = false, TimedOut = true }). /// /// The attribute key (scope-resolved before the wait is registered). /// The site-local predicate tested against the current value. /// How long to wait before returning a timed-out result. /// /// true for quality-gated ("Good"-only) matching (spec §4.2); defaults to false. /// /// The full for the wait. public Task WaitForAsync(string key, Func predicate, TimeSpan timeout, bool requireGoodQuality = false) => _ctx.WaitAttributeFull(Resolve(key), null, predicate, timeout, requireGoodQuality); } /// /// A view of one composition at a path. Exposes its attributes via /// and an invokable CallScript. /// public class CompositionAccessor { private readonly ScriptRuntimeContext _ctx; /// Canonical-name path this composition is rooted at. public string Path { get; } /// /// Accessor for attributes within this composition. /// public AttributeAccessor Attributes { get; } /// /// Initializes a new instance of the CompositionAccessor with the specified context and path. /// /// The script runtime context. /// The canonical-name path for the composition. public CompositionAccessor(ScriptRuntimeContext ctx, string path) { _ctx = ctx; Path = path; Attributes = new AttributeAccessor(ctx, path); } /// /// Resolves a script name to its full canonical name by applying the composition path. /// /// The script name to resolve. /// The fully qualified canonical script name (e.g. "Module.MyScript" or "MyScript" at root). public string ResolveScript(string scriptName) => Path.Length == 0 ? scriptName : Path + "." + scriptName; /// /// Calls a script within this composition. /// /// The name of the script to call. /// Optional parameters to pass to the script. /// A task that resolves to the script's return value, or null if none. public Task CallScript(string scriptName, object? parameters = null) => _ctx.CallScript(ResolveScript(scriptName), parameters); } /// /// Dictionary-style accessor for the script's child compositions. Indexing /// returns a rooted at the child's path. /// public class ChildrenAccessor { private readonly ScriptRuntimeContext _ctx; private readonly string _selfPath; /// /// Initializes a new instance of the ChildrenAccessor with the specified context and path. /// /// The script runtime context. /// The canonical-name path of the parent composition. public ChildrenAccessor(ScriptRuntimeContext ctx, string selfPath) { _ctx = ctx; _selfPath = selfPath; } /// /// Gets a composition accessor for a child by name. /// /// The name of the child composition. public CompositionAccessor this[string compositionName] { get { var path = _selfPath.Length == 0 ? compositionName : _selfPath + "." + compositionName; return new CompositionAccessor(_ctx, path); } } } internal static class ScopeAccessorFactory { /// /// Creates an AttributeAccessor for the specified context and path. /// /// The script runtime context. /// The canonical-name path. /// A new rooted at . public static AttributeAccessor AttributesFor(ScriptRuntimeContext ctx, string selfPath) => new(ctx, selfPath); /// /// Creates a ChildrenAccessor for the specified context and path. /// /// The script runtime context. /// The canonical-name path. /// A new rooted at . public static ChildrenAccessor ChildrenFor(ScriptRuntimeContext ctx, string selfPath) => new(ctx, selfPath); /// /// Creates a CompositionAccessor for the specified context and path, or null if no parent exists. /// /// The script runtime context. /// The parent path, or null if no parent. /// A for the parent, or null when is null. public static CompositionAccessor? ParentFor(ScriptRuntimeContext ctx, string? parentPath) => parentPath == null ? null : new CompositionAccessor(ctx, parentPath); }