<onIdle>
Engine-wide event-timeout watchdog. Armed at module-init, reset on every successful transition. Fires after N ms of silence across the whole engine.
Syntax
<onIdle after=DURATION to=.Variant />
Self-closing only. Both
after=
and
to=
are required.
<onIdle>
is engine-wide: it sits at engine root (sibling of state-children),
not inside any state-child body.
after=DURATION
Time of silence before the watchdog fires. Same shape as
<onTimeout>
after=:
literal
Nms /
Ns /
Nm /
Nh
or computed
${expr}<unit>.
See SPEC §51.12.3 / §51.12.3.1.
to=.Variant
Target variant on watchdog fire. MUST be a variant of the engine's
for=
enum. Strict-by-default. Unknown variants fire
E-IDLE-INVALID-VARIANT
at compile time.
Note: the
to=
target is NOT checked against the current state's
rule=
at compile time (the current state at fire time is dynamic). The
watchdog write commits via the same write path as a direct write;
if the current state's
rule=
does not permit the target, E-ENGINE-INVALID-TRANSITION fires at
runtime.
Worked example
Session-idle detection. After 5 minutes with no variant-changing
transition, the watchdog moves the engine to
.Idle;
from there, clicking Resume transitions
.Idle → .Active
(a real transition, which re-arms the watchdog).
${
type Phase:enum = { Active, Idle }
function resume() { @phase = .Active }
}
<engine for=Phase initial=.Active>
<Active rule=.Idle>
Working…
</>
<Idle rule=.Active>
<button onclick=resume()>Resume</button>
</>
<onIdle after=5m to=.Idle/>
</>
Module-init counts as the first event — the watchdog arms
with the full 5 minutes. Every successful (variant-changing)
transition resets it to a fresh 5 minutes; a self-write to the
current variant is a no-op (§51.0.F.1) and does NOT reset it.
After 5 minutes of no transitions, the watchdog fires
@phase = .Idle.
Semantics
- Armed at module-init. Alongside the engine variant cell. Module-init counts as the "first event"; the timer arms with the full duration remaining.
-
Reset on every successful transition.
Any
_scrml_engine_direct_setor_scrml_engine_advancecommit triggers reset (clear + re-arm). Includes self-writes that pass through commit-path (idempotent self-writes detected byW-ENGINE-SELF-WRITE-DETECTEDare runtime no-ops and do NOT reset). -
rule=-honoring fire.
When the watchdog fires, the resulting transition write goes
through the same write path as a direct write. The current
state's
rule=validation applies per §51.0.F. If the current state's rule= does not permit the target, E-ENGINE-INVALID-TRANSITION fires at runtime. -
One per engine maximum.
Multiple
<onIdle>declarations fire E-IDLE-DUPLICATE. -
Tree-shakeable.
Runtime helpers
_scrml_engine_arm_idle_watchdog/_scrml_engine_reset_idle_watchdogonly ship when at least one engine in the file declares<onIdle>. Engines without<onIdle>passnullfor the idleEntry arg at every commit-path call site; the runtime no-ops.
Errors this feature can fire
- E-IDLE-DUPLICATE — engine declares more than one <onIdle>
- E-IDLE-INVALID-VARIANT — to= references a variant not in the engine's for= enum, or to= is missing/malformed
- E-IDLE-MISPLACED — <onIdle> appears inside a state-child body
- E-ENGINE-INVALID-TRANSITION — runtime fire targets a variant the current state's rule= forbids
<onIdle>
vs
<onTimeout>
Both are temporal triggers on an engine, but they have different scope and timing semantics.
| Axis | <onTimeout> | <onIdle> |
|---|---|---|
| Scope | Per state-child | Engine-wide |
| Placement | Inside a state-child body | At engine root (sibling of state-children) |
| Armed | On entry to the state-child | At module-init |
| Cleared | On exit from the state-child | Never (resets, doesn't clear) |
| Reset trigger | Re-entry to the state-child | Every successful transition (including self-writes that commit) |
| Max per engine | Any (per state-child) | One |
| to= validation | Compile-time vs surrounding state-child's rule= | Compile-time vs engine for= enum; runtime vs current rule= |
| Typical use | Per-state timeout (e.g. .Loading times out after 30s) | Whole-app idle (e.g. lock the screen after 5m of inactivity) |
They compose. An engine may declare both: per-state timers AND a
machine-wide watchdog. Each occupies a distinct slot in the
runtime's
_scrml_machine_timers
map (composite key
<varName>::<stateName>::<index>
for per-state;
<varName>::__idle
for the watchdog).
Placement and composition
- Engine root only. Inside a state-child body — E-IDLE-MISPLACED. For per-state timer semantics, use <onTimeout> instead.
- Composes with <onTransition>. A watchdog-induced transition IS a legal transition event; matching <onTransition> handlers fire too. Same write-path semantics as direct writes.
-
Derived engines (§51.0.J) accept <onIdle>.
Legal but rarely useful: derived engines reject direct writes,
so the watchdog can only fire when the source expression's
value reaches
to='s target.
Related features
- <onTimeout> — per-state-child timer. Sibling temporal trigger with distinct scope and timing.
- <onTransition> — engine effect element. Fires when a transition commits (including watchdog-induced transitions).
- <engine> — the container. onIdle has no meaning outside an engine.
Availability
| Surface | Since | Notes |
|---|---|---|
| <onIdle after=DURATION to=.V/> | S77 A5-6 | SPEC §51.0.R; armed-at-module-init + reset-on-every-transition semantics |
| Computed-form after=${expr}<unit> | S77 | SPEC §51.12.3.1; rides parseAfterDuration helper |
| Tree-shake when no <onIdle> per engine | S77 | Runtime helpers only ship when at least one engine declares onIdle |
Specification
This page summarizes SPEC §51.0.R (form / semantics / placement / composition) + §51.12 (runtime backbone, shared with legacy machine temporal) + §51.12.3 / §51.12.3.1 (DURATION grammar). The normative text lives at compiler/SPEC.md in the scrmlTS repository.