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);
}