scrml.dev v0.7.1
Reference › Elements

<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_set or _scrml_engine_advance commit triggers reset (clear + re-arm). Includes self-writes that pass through commit-path (idempotent self-writes detected by W-ENGINE-SELF-WRITE-DETECTED are 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_watchdog only ship when at least one engine in the file declares <onIdle>. Engines without <onIdle> pass null for the idleEntry arg at every commit-path call site; the runtime no-ops.

Errors this feature can fire

<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.