scrml.dev v0.7.1
Reference › Elements

<onTimeout>

Per-state-child timer. Armed on entry to the state, cleared on exit. Fires a transition to a declared target variant when the duration elapses. Sibling of <onTransition> with distinct semantics.

Syntax

<onTimeout after=DURATION to=.Variant [name=IDENT] />

Self-closing only. Both after= and to= are required. The optional name= attribute makes the timer cancellable via cancelTimer("name") (S79; see Named timers below).

after=DURATION

Time before the timer fires. Two forms:

  • Literal: Nms, Ns, Nm, Nh where N is a non-negative integer. Examples: after=500ms, after=30s, after=5m.
  • Computed: after=${expr}<unit> where the expression evaluates to a non-negative number at state-entry time. Example: after=${@retryDelay}s. Computed-form rules opt out of multi-step JSON-encoded chained auto-rearm (per SPEC §51.12.4 S77 amendment).

to=.Variant

Target variant on timer fire. MUST be a legal target per the surrounding state-child's rule= set (single, multi, or wildcard). Strict-by-default. Violations fire E-ENGINE-INVALID-TRANSITION at compile time (the from-state is statically known — it IS the surrounding state-child).

Worked example

Load with a 30-second timeout. The .Loading state arms a timer on entry; if the load returns first the timer is cleared on transition out; if 30 seconds elapses first the engine commits .TimedOut.

type LoadPhase:enum = { Idle, Loading, Done(rows: int), TimedOut, Error(msg: string) }

function load() {
  @loadPhase = .Loading
  const result = fetchItems() !{
    | ::Network msg -> { @loadPhase = .Error(msg); return }
    | ::Empty       -> { @loadPhase = .Done(0);    return }
  }
  @loadPhase = .Done(result.length)
}

<engine for=LoadPhase initial=.Idle>
  <Idle rule=.Loading>
    <button onclick=load()>Load</button>
  </>

  <Loading rule=(.Done | .TimedOut | .Error)>
    <onTimeout after=30s to=.TimedOut/>
    Loading…
  </>

  <Done rows rule=.Idle>: ${rows} rows
  <TimedOut rule=.Idle>: Timed out
  <Error msg rule=.Idle>: ${msg}
</>

Semantics

  • Armed on entry, cleared on exit. A fresh timer is armed when the state-child becomes active. The timer is cleared when the engine leaves the state-child via any path — rule= transition, the timer's own fire, or external write.
  • Reset on re-entry. Per §51.12.4, re-entering the state-child re-arms a fresh timer; the previous timer's elapsed time is discarded. Two consecutive entries with 20 seconds between them and an after=30s each get the full 30 seconds.
  • Multiple timers per state-child are legal. Each <onTimeout> sibling arms an independent timer with its own after= and to=. Independent firing; whichever timer wins drives the transition; the others clear on exit.
  • Compile-time to= validation. The from-state is statically known (it IS the surrounding state-child), so the rule= legality check fires at compile time. Same discipline as §51.0.F direct-write check.
  • Watchdog-induced transitions are real transitions. The fire commits via the same write path as a direct write. Sibling <onTransition> handlers matching the resulting variant fire too. effect= on the destination state-child fires too.

Errors this feature can fire

Named timers and cancelTimer

S79 added an optional name=IDENT attribute. Named timers are cancellable from within the state-child body via the cancelTimer("name") builtin. Unnamed timers are positional and not addressable from scrml; they only fire or clear-on-exit.

<Loading rule=(.Done | .TimedOut)>
  <onTimeout name=slow  after=5s  to=.Loading.history />
  <onTimeout name=abort after=30s to=.TimedOut       />

  <button onclick=cancelTimer("slow")>Don't show the slow banner</button>
  Loading…
</>
  • Name MUST be identifier-shaped: /^[A-Za-z_][A-Za-z0-9_]*$/. Violations fire E-TIMER-NAME-INVALID.
  • Names are scope-local to the state-child — two siblings with the same name fire E-TIMER-NAME-DUPLICATE; the same name in different state-children is fine.
  • cancelTimer on an unknown name is a runtime no-op (same shape as clearTimeout(undefined)).
  • v1 implementation: only the call-ref event-handler form is supported (e.g. onclick=cancelTimer("X")). Expression-form onclick=${cancelTimer("X")} and function-body calls fall through to ordinary emission and runtime-fail with cancelTimer is not defined.

Placement and composition

  • Engine state-child only. Outside that scope — E-STRUCTURAL-ELEMENT-MISPLACED. Inside a <match> block-form state-child — also misplaced (match is rules-inert per §18.0.2).
  • Coexists with effect= and <onTransition>. A state-child may carry an effect= attribute, multiple <onTransition> siblings, and multiple <onTimeout> siblings. All three compose; the timer fire is just one of the events that can cause the transition.
  • Derived engines (§51.0.J) accept <onTimeout>. Legal but rarely useful: derived engines reject direct writes, so the timer can only fire when the source expression's value reaches to='s variant.

Related features

  • <onIdle> — engine-wide event-timeout watchdog. Distinct from onTimeout: armed at module-init, reset on every transition.
  • <onTransition> — sibling effect element; runs after a transition commits.
  • <engine> — the container. onTimeout has no meaning outside an engine state-child.
  • effect= attribute on a state-child — per-rule inline effect (single-target only). Composes with onTimeout fires.

Availability

Surface Since Notes
<onTimeout after=DURATION to=.V/> S67 / S68 SPEC §51.0.M; literal DURATION; armed-on-entry semantics
A5-4 codegen S77 End-to-end runtime emission; tree-shake when zero onTimeout per engine
Computed-form after=${expr}<unit> S67 / S77 SPEC §51.12.3.1; runtime applies negative/NaN → 0 clamp + Math.round
name=IDENT + cancelTimer("name") S79 SPEC §51.0.M.1; identifier-shaped; scope-local to state-child

Specification

This page summarizes SPEC §51.0.M (form / semantics / placement / composition) + §51.0.M.1 (name= and cancelTimer, S79) + §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.