565 lines
22 KiB
C#
565 lines
22 KiB
C#
using Akka.Actor;
|
|
using Microsoft.CodeAnalysis.Scripting;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Logging;
|
|
using ScadaLink.Commons.Messages.ScriptExecution;
|
|
using ScadaLink.Commons.Messages.Streaming;
|
|
using ScadaLink.Commons.Types.Flattening;
|
|
using ScadaLink.HealthMonitoring;
|
|
using ScadaLink.SiteEventLogging;
|
|
using ScadaLink.SiteRuntime.Scripts;
|
|
using System.Text.Json;
|
|
|
|
namespace ScadaLink.SiteRuntime.Actors;
|
|
|
|
/// <summary>
|
|
/// WP-15: Script Actor — coordinator actor, child of Instance Actor.
|
|
/// Holds compiled script delegate, manages trigger configuration, and spawns
|
|
/// ScriptExecutionActor children per invocation. Does not block on child completion.
|
|
///
|
|
/// Trigger types:
|
|
/// - Interval: uses Akka timers to fire periodically
|
|
/// - ValueChange: receives attribute change notifications from Instance Actor
|
|
/// - Conditional: evaluates a threshold comparison on attribute change
|
|
/// - Expression: evaluates a compiled boolean expression on attribute change
|
|
/// Conditional and Expression triggers carry a <see cref="TriggerMode"/>:
|
|
/// OnTrue fires as the condition becomes true; WhileTrue additionally re-fires
|
|
/// on a timer (cadence = MinTimeBetweenRuns) while the condition stays true.
|
|
///
|
|
/// Supervision strategy: Resume on exception (coordinator preserves state).
|
|
/// </summary>
|
|
public class ScriptActor : ReceiveActor, IWithTimers
|
|
{
|
|
private readonly string _scriptName;
|
|
private readonly string _instanceName;
|
|
private readonly IActorRef _instanceActor;
|
|
private readonly SharedScriptLibrary _sharedScriptLibrary;
|
|
private readonly SiteRuntimeOptions _options;
|
|
private readonly ILogger _logger;
|
|
private readonly ISiteHealthCollector? _healthCollector;
|
|
private readonly IServiceProvider? _serviceProvider;
|
|
|
|
private Script<object?>? _compiledScript;
|
|
private ScriptTriggerConfig? _triggerConfig;
|
|
private TimeSpan? _minTimeBetweenRuns;
|
|
private DateTimeOffset _lastExecutionTime = DateTimeOffset.MinValue;
|
|
private int _executionCounter;
|
|
private readonly Commons.Types.Scripts.ScriptScope _scope;
|
|
|
|
// Expression trigger state: compiled expression, edge-tracking, and the
|
|
// attribute snapshot the expression evaluates against.
|
|
private readonly Script<object?>? _compiledTriggerExpression;
|
|
private bool _lastExpressionResult;
|
|
private readonly Dictionary<string, object?> _attributeSnapshot = new();
|
|
|
|
// WhileTrue trigger state: the most recent truth value of a Conditional
|
|
// trigger's comparison, used to detect false->true / true->false edges.
|
|
// (Expression triggers reuse _lastExpressionResult for the same purpose.)
|
|
private bool _conditionState;
|
|
|
|
/// <summary>Timer key for the WhileTrue re-fire timer (cadence = MinTimeBetweenRuns).</summary>
|
|
private const string WhileTrueTimerKey = "whiletrue-trigger";
|
|
|
|
/// <summary>
|
|
/// SiteRuntime-017: the exact dictionary instance this actor was seeded from
|
|
/// at construction. The Instance Actor must pass a private snapshot here, not
|
|
/// its live <c>_attributes</c> field — sharing the live dictionary lets this
|
|
/// constructor enumerate it while the Instance Actor mutates it on another
|
|
/// thread. Exposed for regression coverage of that isolation contract.
|
|
/// </summary>
|
|
internal IReadOnlyDictionary<string, object?>? SeedAttributesReference { get; }
|
|
|
|
public ITimerScheduler Timers { get; set; } = null!;
|
|
|
|
public ScriptActor(
|
|
string scriptName,
|
|
string instanceName,
|
|
IActorRef instanceActor,
|
|
Script<object?>? compiledScript,
|
|
ResolvedScript scriptConfig,
|
|
SharedScriptLibrary sharedScriptLibrary,
|
|
SiteRuntimeOptions options,
|
|
ILogger logger,
|
|
Script<object?>? compiledTriggerExpression = null,
|
|
IReadOnlyDictionary<string, object?>? initialAttributes = null,
|
|
ISiteHealthCollector? healthCollector = null,
|
|
IServiceProvider? serviceProvider = null)
|
|
{
|
|
_scriptName = scriptName;
|
|
_instanceName = instanceName;
|
|
_instanceActor = instanceActor;
|
|
_compiledScript = compiledScript;
|
|
_sharedScriptLibrary = sharedScriptLibrary;
|
|
_options = options;
|
|
_logger = logger;
|
|
_healthCollector = healthCollector;
|
|
_serviceProvider = serviceProvider;
|
|
_minTimeBetweenRuns = scriptConfig.MinTimeBetweenRuns;
|
|
_scope = scriptConfig.Scope;
|
|
_compiledTriggerExpression = compiledTriggerExpression;
|
|
|
|
// Seed the trigger-expression attribute snapshot from the instance's
|
|
// initial attribute set so static attributes (which never re-emit an
|
|
// AttributeValueChanged after deploy) evaluate correctly at startup.
|
|
SeedAttributesReference = initialAttributes;
|
|
if (initialAttributes != null)
|
|
{
|
|
foreach (var kvp in initialAttributes)
|
|
_attributeSnapshot[kvp.Key] = kvp.Value;
|
|
}
|
|
|
|
// Parse trigger configuration
|
|
_triggerConfig = ParseTriggerConfig(scriptConfig.TriggerType, scriptConfig.TriggerConfiguration);
|
|
|
|
// Handle script call requests (Ask pattern from Instance Actor or ScriptRuntimeContext)
|
|
Receive<ScriptCallRequest>(HandleScriptCallRequest);
|
|
|
|
// Handle attribute value changes for value-change and conditional triggers
|
|
Receive<AttributeValueChanged>(HandleAttributeValueChanged);
|
|
|
|
// Handle interval tick
|
|
Receive<IntervalTick>(_ => TrySpawnExecution(null));
|
|
|
|
// Handle WhileTrue re-fire tick
|
|
Receive<WhileTrueTick>(_ => FireWhileTrueTick());
|
|
|
|
// Handle execution completion (for logging/metrics)
|
|
Receive<ScriptExecutionCompleted>(HandleExecutionCompleted);
|
|
}
|
|
|
|
protected override void PreStart()
|
|
{
|
|
base.PreStart();
|
|
|
|
// Set up interval trigger if configured
|
|
if (_triggerConfig is IntervalTriggerConfig interval)
|
|
{
|
|
Timers.StartPeriodicTimer(
|
|
"interval-trigger",
|
|
IntervalTick.Instance,
|
|
interval.Interval,
|
|
interval.Interval);
|
|
|
|
_logger.LogDebug(
|
|
"ScriptActor {Script} on {Instance}: interval trigger set to {Interval}",
|
|
_scriptName, _instanceName, interval.Interval);
|
|
}
|
|
|
|
_logger.LogInformation(
|
|
"ScriptActor {Script} started on instance {Instance}",
|
|
_scriptName, _instanceName);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Supervision: Resume on exception — coordinator preserves state.
|
|
/// ScriptExecutionActors are stopped on unhandled exceptions.
|
|
/// </summary>
|
|
protected override SupervisorStrategy SupervisorStrategy()
|
|
{
|
|
return new OneForOneStrategy(
|
|
maxNrOfRetries: -1,
|
|
withinTimeRange: TimeSpan.FromMinutes(1),
|
|
decider: Decider.From(ex =>
|
|
{
|
|
_logger.LogWarning(ex,
|
|
"ScriptExecutionActor for {Script} on {Instance} failed, stopping",
|
|
_scriptName, _instanceName);
|
|
return Directive.Stop;
|
|
}));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles CallScript ask from ScriptRuntimeContext or Instance Actor.
|
|
/// Spawns a ScriptExecutionActor and forwards the sender for reply.
|
|
/// </summary>
|
|
private void HandleScriptCallRequest(ScriptCallRequest request)
|
|
{
|
|
if (_compiledScript == null)
|
|
{
|
|
Sender.Tell(new ScriptCallResult(
|
|
request.CorrelationId,
|
|
false,
|
|
null,
|
|
$"Script '{_scriptName}' is not compiled."));
|
|
return;
|
|
}
|
|
|
|
// Audit Log #23 (ParentExecutionId): carry any inbound-routed
|
|
// ParentExecutionId through to the ScriptExecutionActor so the routed
|
|
// script's ScriptRuntimeContext can record its spawner. Null for normal
|
|
// (tag-change / timer) runs and nested Script.Call invocations.
|
|
SpawnExecution(
|
|
request.Parameters, request.CurrentCallDepth, Sender, request.CorrelationId,
|
|
request.ParentExecutionId);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles attribute value changes — triggers script if configured for
|
|
/// value-change, conditional, or expression. The attribute snapshot is
|
|
/// updated for every change before any trigger logic runs.
|
|
/// </summary>
|
|
private void HandleAttributeValueChanged(AttributeValueChanged changed)
|
|
{
|
|
// Keep the snapshot current for every change, regardless of trigger type.
|
|
_attributeSnapshot[changed.AttributeName] = changed.Value;
|
|
|
|
if (_triggerConfig is ValueChangeTriggerConfig valueTrigger)
|
|
{
|
|
if (valueTrigger.AttributeName == changed.AttributeName)
|
|
{
|
|
TrySpawnExecution(null);
|
|
}
|
|
}
|
|
else if (_triggerConfig is ConditionalTriggerConfig conditional)
|
|
{
|
|
if (conditional.AttributeName == changed.AttributeName)
|
|
{
|
|
var conditionMet = EvaluateCondition(conditional, changed.Value);
|
|
if (conditional.Mode == TriggerMode.WhileTrue)
|
|
{
|
|
// Edge-detect against the prior truth value; the timer does
|
|
// the repeated firing while the condition stays true.
|
|
HandleWhileTrueTransition(conditionMet, _conditionState);
|
|
_conditionState = conditionMet;
|
|
}
|
|
else if (conditionMet)
|
|
{
|
|
// OnTrue: fire on each matching change (existing behavior).
|
|
TrySpawnExecution(null);
|
|
}
|
|
}
|
|
}
|
|
else if (_triggerConfig is ExpressionTriggerConfig)
|
|
{
|
|
EvaluateExpressionTrigger();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Evaluates the compiled trigger expression against the current attribute
|
|
/// snapshot. In <see cref="TriggerMode.OnTrue"/> mode the script runs once
|
|
/// per false→true transition; in <see cref="TriggerMode.WhileTrue"/> mode it
|
|
/// fires on the edge and the re-fire timer is started/stopped with the
|
|
/// expression's truth value. A throwing or non-bool expression is treated as
|
|
/// false and logged as a script error; the actor never crashes.
|
|
/// </summary>
|
|
private void EvaluateExpressionTrigger()
|
|
{
|
|
if (_compiledTriggerExpression == null) return;
|
|
if (_triggerConfig is not ExpressionTriggerConfig exprConfig) return;
|
|
|
|
bool result;
|
|
try
|
|
{
|
|
var globals = new TriggerExpressionGlobals(_attributeSnapshot);
|
|
// Bound evaluation with a short timeout. The CancellationToken
|
|
// covers cooperative/async cases; a pathological CPU-bound
|
|
// expression is not fully interruptible. Acceptable because
|
|
// trigger expressions are authored by trusted Design-role users
|
|
// and are compile-checked pre-deployment.
|
|
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2));
|
|
var state = _compiledTriggerExpression
|
|
.RunAsync(globals, cancellationToken: cts.Token)
|
|
.GetAwaiter().GetResult();
|
|
result = state.ReturnValue is bool b && b;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// OperationCanceledException (timeout) falls through here too,
|
|
// and is correctly treated as false.
|
|
LogExpressionError(ex);
|
|
result = false;
|
|
}
|
|
|
|
if (exprConfig.Mode == TriggerMode.WhileTrue)
|
|
{
|
|
HandleWhileTrueTransition(result, _lastExpressionResult);
|
|
}
|
|
else if (result && !_lastExpressionResult)
|
|
{
|
|
TrySpawnExecution(null);
|
|
}
|
|
|
|
_lastExpressionResult = result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Applies a WhileTrue trigger's condition-state transition: on the
|
|
/// false→true edge, fire once and start the re-fire timer; on the
|
|
/// true→false edge, stop the timer. While the state is unchanged, the
|
|
/// already-running timer continues to drive re-firing.
|
|
/// </summary>
|
|
private void HandleWhileTrueTransition(bool nowTrue, bool wasTrue)
|
|
{
|
|
if (nowTrue && !wasTrue)
|
|
{
|
|
TrySpawnExecution(null);
|
|
StartWhileTrueTimer();
|
|
}
|
|
else if (!nowTrue && wasTrue)
|
|
{
|
|
StopWhileTrueTimer();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Starts the periodic WhileTrue re-fire timer. The cadence is the script's
|
|
/// <c>MinTimeBetweenRuns</c>; with none configured the trigger cannot
|
|
/// re-fire, so it degrades to the single edge fire and logs a warning.
|
|
/// </summary>
|
|
private void StartWhileTrueTimer()
|
|
{
|
|
if (_compiledScript == null) return;
|
|
|
|
if (_minTimeBetweenRuns is not { } interval)
|
|
{
|
|
_logger.LogWarning(
|
|
"ScriptActor {Script} on {Instance}: WhileTrue trigger has no MinTimeBetweenRuns — " +
|
|
"firing once on the edge only, no re-fire timer.",
|
|
_scriptName, _instanceName);
|
|
return;
|
|
}
|
|
|
|
Timers.StartPeriodicTimer(WhileTrueTimerKey, WhileTrueTick.Instance, interval, interval);
|
|
}
|
|
|
|
/// <summary>Cancels the WhileTrue re-fire timer (a no-op if it is not running).</summary>
|
|
private void StopWhileTrueTimer() => Timers.Cancel(WhileTrueTimerKey);
|
|
|
|
/// <summary>
|
|
/// Fires the script for a WhileTrue re-fire tick. The timer interval is
|
|
/// itself the cadence, so this spawns directly — bypassing the
|
|
/// MinTimeBetweenRuns skip-check that gates change-driven spawns (which
|
|
/// could otherwise drop a tick to sub-millisecond timing jitter).
|
|
/// </summary>
|
|
private void FireWhileTrueTick()
|
|
{
|
|
if (_compiledScript == null) return;
|
|
|
|
_lastExecutionTime = DateTimeOffset.UtcNow;
|
|
SpawnExecution(null, 0, ActorRefs.NoSender!, Guid.NewGuid().ToString());
|
|
}
|
|
|
|
/// <summary>
|
|
/// Records a trigger-expression evaluation failure to the site event log,
|
|
/// mirroring how ScriptExecutionActor reports script errors.
|
|
/// </summary>
|
|
private void LogExpressionError(Exception ex)
|
|
{
|
|
_healthCollector?.IncrementScriptError();
|
|
var errorMsg = $"Trigger expression for script '{_scriptName}' on instance '{_instanceName}' failed: {ex.Message}";
|
|
_logger.LogError(ex, "Trigger expression evaluation failed: {Script} on {Instance}", _scriptName, _instanceName);
|
|
|
|
_ = _serviceProvider?.GetService<ISiteEventLogger>()?.LogEventAsync(
|
|
"script", "Error", _instanceName, $"ScriptActor:{_scriptName}", errorMsg, ex.ToString());
|
|
}
|
|
|
|
/// <summary>
|
|
/// Attempts to spawn a script execution, respecting MinTimeBetweenRuns.
|
|
/// </summary>
|
|
private void TrySpawnExecution(IReadOnlyDictionary<string, object?>? parameters)
|
|
{
|
|
if (_compiledScript == null) return;
|
|
|
|
if (_minTimeBetweenRuns.HasValue)
|
|
{
|
|
var elapsed = DateTimeOffset.UtcNow - _lastExecutionTime;
|
|
if (elapsed < _minTimeBetweenRuns.Value)
|
|
{
|
|
_logger.LogDebug(
|
|
"Script {Script} on {Instance}: skipping execution, min time between runs not elapsed ({Elapsed} < {Min})",
|
|
_scriptName, _instanceName, elapsed, _minTimeBetweenRuns.Value);
|
|
return;
|
|
}
|
|
}
|
|
|
|
_lastExecutionTime = DateTimeOffset.UtcNow;
|
|
SpawnExecution(parameters, 0, ActorRefs.NoSender!, Guid.NewGuid().ToString());
|
|
}
|
|
|
|
/// <summary>
|
|
/// Spawns a new ScriptExecutionActor child for this invocation.
|
|
/// Multiple concurrent executions are allowed.
|
|
/// </summary>
|
|
private void SpawnExecution(
|
|
IReadOnlyDictionary<string, object?>? parameters,
|
|
int callDepth,
|
|
IActorRef replyTo,
|
|
string correlationId,
|
|
Guid? parentExecutionId = null)
|
|
{
|
|
var executionId = $"{_scriptName}-exec-{_executionCounter++}";
|
|
|
|
// SiteRuntime-009: the actor's mailbox stays on the default dispatcher, but the
|
|
// script body itself runs on the dedicated ScriptExecutionScheduler (a bounded
|
|
// set of dedicated threads), so blocking script I/O is contained there and
|
|
// cannot starve the shared .NET thread pool.
|
|
var props = Props.Create(() => new ScriptExecutionActor(
|
|
_scriptName,
|
|
_instanceName,
|
|
_compiledScript!,
|
|
parameters,
|
|
callDepth,
|
|
_instanceActor,
|
|
_sharedScriptLibrary,
|
|
_options,
|
|
replyTo,
|
|
correlationId,
|
|
_logger,
|
|
_scope,
|
|
_healthCollector,
|
|
_serviceProvider,
|
|
// Audit Log #23 (ParentExecutionId): null for trigger-driven runs;
|
|
// an inbound-API-routed call supplies the inbound request's id.
|
|
parentExecutionId));
|
|
|
|
Context.ActorOf(props, executionId);
|
|
}
|
|
|
|
private void HandleExecutionCompleted(ScriptExecutionCompleted msg)
|
|
{
|
|
_logger.LogDebug(
|
|
"Script {Script} execution completed on {Instance}: success={Success}",
|
|
_scriptName, _instanceName, msg.Success);
|
|
}
|
|
|
|
private static bool EvaluateCondition(ConditionalTriggerConfig config, object? value)
|
|
{
|
|
if (value == null) return false;
|
|
|
|
try
|
|
{
|
|
var numericValue = Convert.ToDouble(value);
|
|
return config.Operator switch
|
|
{
|
|
">" => numericValue > config.Threshold,
|
|
">=" => numericValue >= config.Threshold,
|
|
"<" => numericValue < config.Threshold,
|
|
"<=" => numericValue <= config.Threshold,
|
|
"==" => Math.Abs(numericValue - config.Threshold) < 0.0001,
|
|
"!=" => Math.Abs(numericValue - config.Threshold) >= 0.0001,
|
|
_ => false
|
|
};
|
|
}
|
|
catch
|
|
{
|
|
return string.Equals(value.ToString(), config.Threshold.ToString(), StringComparison.Ordinal);
|
|
}
|
|
}
|
|
|
|
private static ScriptTriggerConfig? ParseTriggerConfig(string? triggerType, string? triggerConfigJson)
|
|
{
|
|
if (string.IsNullOrEmpty(triggerType)) return null;
|
|
|
|
return triggerType.ToLowerInvariant() switch
|
|
{
|
|
"interval" => ParseIntervalTrigger(triggerConfigJson),
|
|
"valuechange" => ParseValueChangeTrigger(triggerConfigJson),
|
|
"conditional" => ParseConditionalTrigger(triggerConfigJson),
|
|
"expression" => ParseExpressionTrigger(triggerConfigJson),
|
|
"call" => null, // No automatic trigger — invoked only via Instance.CallScript()
|
|
_ => null
|
|
};
|
|
}
|
|
|
|
private static ExpressionTriggerConfig? ParseExpressionTrigger(string? json)
|
|
{
|
|
var expr = TriggerExpressionGlobals.ExtractExpression(json);
|
|
if (expr == null) return null;
|
|
|
|
// ExtractExpression already proved the JSON parses; read the mode too.
|
|
var mode = TriggerMode.OnTrue;
|
|
try
|
|
{
|
|
using var doc = JsonDocument.Parse(json!);
|
|
mode = ParseTriggerMode(doc.RootElement);
|
|
}
|
|
catch (JsonException) { /* keep OnTrue */ }
|
|
|
|
return new ExpressionTriggerConfig(expr, mode);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads the optional <c>mode</c> field (Conditional + Expression triggers).
|
|
/// An absent or unrecognized value (case-insensitive) yields
|
|
/// <see cref="TriggerMode.OnTrue"/>, so pre-WhileTrue configs are unchanged.
|
|
/// </summary>
|
|
private static TriggerMode ParseTriggerMode(JsonElement root)
|
|
{
|
|
var raw = root.TryGetProperty("mode", out var m) ? m.GetString() : null;
|
|
return string.Equals(raw?.Trim(), "WhileTrue", StringComparison.OrdinalIgnoreCase)
|
|
? TriggerMode.WhileTrue
|
|
: TriggerMode.OnTrue;
|
|
}
|
|
|
|
private static IntervalTriggerConfig? ParseIntervalTrigger(string? json)
|
|
{
|
|
if (string.IsNullOrEmpty(json)) return null;
|
|
try
|
|
{
|
|
var doc = JsonDocument.Parse(json);
|
|
var ms = doc.RootElement.GetProperty("intervalMs").GetInt64();
|
|
return new IntervalTriggerConfig(TimeSpan.FromMilliseconds(ms));
|
|
}
|
|
catch { return null; }
|
|
}
|
|
|
|
private static ValueChangeTriggerConfig? ParseValueChangeTrigger(string? json)
|
|
{
|
|
if (string.IsNullOrEmpty(json)) return null;
|
|
try
|
|
{
|
|
var doc = JsonDocument.Parse(json);
|
|
var attr = doc.RootElement.GetProperty("attributeName").GetString()!;
|
|
return new ValueChangeTriggerConfig(attr);
|
|
}
|
|
catch { return null; }
|
|
}
|
|
|
|
private static ConditionalTriggerConfig? ParseConditionalTrigger(string? json)
|
|
{
|
|
if (string.IsNullOrEmpty(json)) return null;
|
|
try
|
|
{
|
|
var doc = JsonDocument.Parse(json);
|
|
var attr = doc.RootElement.GetProperty("attributeName").GetString()!;
|
|
var op = doc.RootElement.GetProperty("operator").GetString()!;
|
|
var threshold = doc.RootElement.GetProperty("threshold").GetDouble();
|
|
return new ConditionalTriggerConfig(
|
|
attr, op, threshold, ParseTriggerMode(doc.RootElement));
|
|
}
|
|
catch { return null; }
|
|
}
|
|
|
|
// ── Internal messages ──
|
|
|
|
internal sealed class IntervalTick
|
|
{
|
|
public static readonly IntervalTick Instance = new();
|
|
private IntervalTick() { }
|
|
}
|
|
|
|
internal sealed class WhileTrueTick
|
|
{
|
|
public static readonly WhileTrueTick Instance = new();
|
|
private WhileTrueTick() { }
|
|
}
|
|
|
|
internal record ScriptExecutionCompleted(string ScriptName, bool Success, string? Error);
|
|
}
|
|
|
|
// ── Trigger config types ──
|
|
|
|
/// <summary>
|
|
/// When a Conditional/Expression trigger fires. <see cref="OnTrue"/> fires once
|
|
/// as the condition becomes true; <see cref="WhileTrue"/> additionally re-fires
|
|
/// on a timer (cadence = the script's MinTimeBetweenRuns) until it goes false.
|
|
/// </summary>
|
|
internal enum TriggerMode { OnTrue, WhileTrue }
|
|
|
|
internal record IntervalTriggerConfig(TimeSpan Interval) : ScriptTriggerConfig;
|
|
internal record ValueChangeTriggerConfig(string AttributeName) : ScriptTriggerConfig;
|
|
internal record ConditionalTriggerConfig(string AttributeName, string Operator, double Threshold, TriggerMode Mode) : ScriptTriggerConfig;
|
|
internal record ExpressionTriggerConfig(string Expression, TriggerMode Mode) : ScriptTriggerConfig;
|
|
internal abstract record ScriptTriggerConfig;
|