<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,Nhwhere 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=30seach get the full 30 seconds. -
Multiple timers per state-child are legal.
Each
<onTimeout>sibling arms an independent timer with its ownafter=andto=. 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 therule=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
- E-ENGINE-INVALID-TRANSITION — to= target not in surrounding state-child's rule= set
- E-STRUCTURAL-ELEMENT-MISPLACED — outside an engine state-child body
- E-TIMER-NAME-INVALID — name= value is not a valid identifier (S79)
- E-TIMER-NAME-DUPLICATE — two onTimeout elements in the same state-child share a name= (S79)
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.
-
cancelTimeron an unknown name is a runtime no-op (same shape asclearTimeout(undefined)). -
v1 implementation: only the call-ref event-handler form is
supported (e.g.
onclick=cancelTimer("X")). Expression-formonclick=${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.