Conditional and Expression script triggers gain an optional `mode` field in their TriggerConfiguration JSON: - OnTrue (default): unchanged edge/per-change firing. An absent mode field parses as OnTrue, so every existing trigger config behaves identically. - WhileTrue: fires on the false->true edge, then re-fires on a periodic timer while the condition holds; stops on the true->false edge. The re-fire cadence is the script's MinTimeBetweenRuns; with none configured the trigger degrades to a single edge fire and logs a warning. ScriptActor tracks condition truth state and manages a dedicated "whiletrue-trigger" timer. ScriptTriggerConfigCodec and ScriptTriggerEditor round-trip the mode and expose an OnTrue/WhileTrue selector for the two trigger kinds. Design: docs/plans/2026-05-18-whiletrue-trigger-mode-design.md Tests: 7 ScriptActor runtime tests (edge fire, timer re-fire, stop, re-arm, no-MinTimeBetweenRuns degrade, OnTrue regressions) + 14 codec / editor tests. SiteRuntime suite 206 green, CentralUI suite 295 green.
135 lines
6.2 KiB
Markdown
135 lines
6.2 KiB
Markdown
# WhileTrue Trigger Mode for Conditional & Expression Script Triggers — Design
|
|
|
|
**Date:** 2026-05-18
|
|
**Status:** Implemented
|
|
|
|
## Context
|
|
|
|
Template script triggers of type `Conditional` and `Expression` currently fire
|
|
*once* when their condition first holds:
|
|
|
|
- **Expression** is edge-triggered — the script runs once per `false→true`
|
|
transition of the boolean expression.
|
|
- **Conditional** runs the script on each change of the monitored attribute for
|
|
which the `{operator, threshold}` comparison is true.
|
|
|
|
There is no way to keep a script running *while* a condition remains true. This
|
|
design adds a second mode, **WhileTrue**, alongside the existing behavior
|
|
(referred to as **OnTrue**).
|
|
|
|
### Decisions taken during brainstorming
|
|
|
|
- **WhileTrue is a timer re-fire cadence.** On the `false→true` edge the script
|
|
fires immediately; while the condition stays true it re-fires on a periodic
|
|
timer; on the `true→false` edge the timer stops. It re-fires even if no
|
|
attribute updates arrive (e.g. a static condition).
|
|
- **The re-fire interval is the script's existing `MinTimeBetweenRuns`** — no
|
|
new configuration field. One value serves as both the WhileTrue cadence and
|
|
the general per-script throttle.
|
|
- **OnTrue is unchanged.** A trigger config with no `mode` field parses as
|
|
`OnTrue`, so every existing deployed template behaves identically.
|
|
- **Alarms are out of scope** — alarm triggers are already level-based ("active
|
|
while true").
|
|
|
|
## Design
|
|
|
|
### 1. Trigger model & storage
|
|
|
|
`Conditional` and `Expression` trigger `TriggerConfiguration` JSON gains an
|
|
optional `mode` field:
|
|
|
|
| Trigger | JSON shape |
|
|
|-------------|---------------------------------------------------------|
|
|
| Conditional | `{ attributeName, operator, threshold, mode? }` |
|
|
| Expression | `{ expression, mode? }` |
|
|
|
|
`mode` is `"OnTrue"` (default; absent ⇒ `OnTrue`) or `"WhileTrue"`. Entity
|
|
types are unchanged — `TemplateScript.TriggerConfiguration` stays `string?`.
|
|
|
|
### 2. Runtime evaluation (`ScriptActor`)
|
|
|
|
The internal trigger-config records gain a `Mode`:
|
|
|
|
```csharp
|
|
internal enum TriggerMode { OnTrue, WhileTrue }
|
|
internal record ConditionalTriggerConfig(
|
|
string AttributeName, string Operator, double Threshold, TriggerMode Mode) : ScriptTriggerConfig;
|
|
internal record ExpressionTriggerConfig(string Expression, TriggerMode Mode) : ScriptTriggerConfig;
|
|
```
|
|
|
|
**OnTrue** — current behavior, unchanged:
|
|
- Conditional: fire on each monitored-attribute change for which the comparison
|
|
is true.
|
|
- Expression: fire once per `false→true` transition.
|
|
|
|
**WhileTrue** — the actor tracks the condition's current truth state:
|
|
- **`false→true` edge:** fire once immediately (via `TrySpawnExecution`, which
|
|
still respects `MinTimeBetweenRuns` against any prior run), then start a
|
|
periodic Akka timer with both initial delay and interval set to
|
|
`MinTimeBetweenRuns`.
|
|
- **Each timer tick:** spawn an execution **directly** — the timer interval is
|
|
itself the cadence, so ticks bypass the `MinTimeBetweenRuns` skip-check (which
|
|
could otherwise drop a tick to sub-millisecond timing jitter).
|
|
- **`true→false` edge:** cancel the timer.
|
|
- The state transitions naturally re-arm: a later `true` after a `false` starts
|
|
a fresh cycle.
|
|
|
|
Truth-state sources:
|
|
- Conditional depends only on the monitored attribute, so it is re-evaluated on
|
|
each change of that attribute (unchanged gate).
|
|
- Expression is re-evaluated on every attribute change (unchanged).
|
|
|
|
A throwing/non-bool expression is treated as `false` (existing behavior); for
|
|
WhileTrue that cleanly stops the timer.
|
|
|
|
**Edge case — WhileTrue with no `MinTimeBetweenRuns`:** a periodic timer has no
|
|
interval to use, so the trigger degrades to a single edge fire and logs a
|
|
warning. Deployment-time validation of this combination is a deliberate
|
|
non-goal of this change.
|
|
|
|
New surface in `ScriptActor`: a `TriggerMode` on the two config records, one
|
|
`WhileTrueTick` internal message, and start/stop timer helpers (the actor
|
|
already implements `IWithTimers` for Interval triggers). Timer key
|
|
`"whiletrue-trigger"`.
|
|
|
|
### 3. Editors & codec
|
|
|
|
- **`ScriptTriggerConfigCodec`:** `ScriptTriggerModel` gains a `Mode`
|
|
(`ScriptTriggerMode` enum, default `OnTrue`). `Parse` reads `mode` for the
|
|
Conditional and Expression kinds; `Serialize` writes it for those kinds.
|
|
Absent/unrecognized `mode` ⇒ `OnTrue`.
|
|
- **`ScriptTriggerEditor.razor`:** the Conditional and Expression panels gain a
|
|
mode `<select>` — "When the condition becomes true — once" (`OnTrue`) vs
|
|
"Repeatedly while true" (`WhileTrue`) — with a hint that WhileTrue re-fires at
|
|
the script's *Min time between runs* interval. `BuildHint()` is updated to
|
|
describe the selected mode.
|
|
|
|
### 4. Error handling
|
|
|
|
- Expression throws / returns non-bool → treated as `false` (logged as a script
|
|
error, as today); for WhileTrue this stops the re-fire timer.
|
|
- Malformed trigger JSON → trigger parses as inert (existing behavior).
|
|
- WhileTrue with no `MinTimeBetweenRuns` → single fire + warning (see above).
|
|
|
|
### 5. Testing & verification
|
|
|
|
- **Runtime (`ScriptActorTests`)** — a script body that calls
|
|
`Instance.SetAttribute(...)` is observable as a `SetStaticAttributeCommand` on
|
|
the instance-actor `TestProbe`; with a short `MinTimeBetweenRuns`:
|
|
- WhileTrue conditional & expression: fire on the `false→true` edge, re-fire
|
|
on the timer, stop on `true→false`, re-arm on a later `true`.
|
|
- OnTrue regression: conditional & expression fire exactly as before.
|
|
- WhileTrue with no `MinTimeBetweenRuns`: a single fire, no repeats.
|
|
- **Codec (`ScriptTriggerConfigCodecTests`, new)** — `mode` round-trips for
|
|
Conditional and Expression; absent `mode` ⇒ `OnTrue`; `WhileTrue` serializes.
|
|
|
|
## Affected files
|
|
|
|
- `src/ScadaLink.SiteRuntime/Actors/ScriptActor.cs`
|
|
- `src/ScadaLink.CentralUI/Components/Shared/ScriptTriggerConfigCodec.cs`
|
|
- `src/ScadaLink.CentralUI/Components/Shared/ScriptTriggerEditor.razor`
|
|
- `tests/ScadaLink.SiteRuntime.Tests/Actors/ScriptActorTests.cs`
|
|
- `tests/ScadaLink.CentralUI.Tests/Shared/ScriptTriggerConfigCodecTests.cs` (new)
|
|
- Docs: `docs/requirements/Component-SiteRuntime.md`,
|
|
`docs/requirements/Component-TemplateEngine.md` (note the new mode).
|