StatifierBlocks.Core.Await (StatifierBlocks v0.31.0)

Copy Markdown View Source

core.await: an in-flow leaf that holds until a named event arrives, with an optional deadline (ADR-0002 decision 10, the 2026-09-05 amendment).

No slots, two config fields, and two outcomes. event is required and reads the event-name grammar core.on_event and core.send already read; timeout is an optional :duration in the one grammar core.wait's duration and core.send's delay are written in. The two outcomes, received and timed_out, are the two ways an await can end.

An await is not a wait

The names are one word apart because the things are close, so this moduledoc says which is meant rather than leaning on the verb. A wait (core.wait) holds for a duration and ends one way. An await holds for an event and ends one of two ways, one of which is a duration elapsing. io/1 is core.wait's declaration byte for byte, which is the whole of what "valid wherever a wait is" means: both are kinds: [:step], and both leave consumes and produces to ADR-0003 decision 5's permissive default, because holding transforms no data.

Why this is a type rather than an arrangement

The nearest arrangement is a core.group whose interrupts rail carries a core.on_event for the awaited event and, for the deadline, the deadline recipe's core.send and a second handler. It does not express this type, because a handler's outcome word becomes one of exactly two package-owned interrupt events (StatifierBlocks.Core.Emit.interrupt_events/0) and a group declares no outcomes at all: two abandon handlers on one rail are indistinguishable to everything downstream. This type declares the two outcomes directly, which is the seam the arrangement is missing.

Both outcomes are declared whether or not a deadline is stored

outcomes/1 returns received and timed_out for every config, including one with no timeout. An outcome's wiring is an event rather than a target (ADR-0004's outcome amendment, 2c), so a parent may transition on an outcome whose <final> was never emitted and the transition simply never fires; nothing in the compiler cross-checks a declaration against an emitted final either. Deriving the list from timeout instead would take a declared seam away from an author who cleared the field, which is the failure ADR-0002 decision 6's slots/1 stability rule exists to avoid on the slot side. The emitted bytes stay honest either way: with no timeout, no timer is armed and no timed_out final is written.

The deadline's timer cannot outlive the await

When timeout holds a duration, the compiled waiting state arms a delayed <send> whose id is minted with StatifierBlocks.Compiler.Context.role_id/2 under StatifierBlocks.Compiler.Cancels.armed_role/0 - the reserved role core.wait and core.send mint under. StatifierBlocks.Compiler.Cancels therefore reaches it like any other armed send and cancels it in the enclosing scope's <onexit>, so an await left before its deadline - because the awaited event arrived, because an interrupt fired, because a losing lane exited, because a group was abandoned - leaves no timer behind. Nothing in that module changes for this type; an await with no timeout arms nothing and there is nothing to cancel.

Summary

Functions

The event first, then the deadline.

A compound state whose one waiting child transitions to the received outcome when the event arrives, and - when a timeout is stored - arms a delayed send on entry and transitions to the timed_out outcome when that send's event comes back.

One example event payload, so a palette panel can show what _event.data looks like when the awaited event arrives.

core.wait's declaration byte for byte: a step that constrains nothing and is constrained by nothing beyond being one.

The two ways an await can finish, for every config.

The event name, then the deadline when one is stored (ADR-0002 amendment H6).

Functions

config_schema(config)

The event first, then the deadline.

timeout's default is "" and not a duration: an await with no deadline is the ordinary case, and a default that armed a timer would be this type deciding an author's policy for them.

emit(block, context)

A compound state whose one waiting child transitions to the received outcome when the event arrives, and - when a timeout is stored - arms a delayed send on entry and transitions to the timed_out outcome when that send's event comes back.

<state id="s_AWT" initial="s_AWT__waiting">
  <state id="s_AWT__waiting">
    <onentry><send delay="48h" event="statifier_blocks.await.blk_AWT" id="s_AWT__send"/></onentry>
    <transition event="order.approved" target="s_AWT__o_received"/>
    <transition event="statifier_blocks.await.blk_AWT" target="s_AWT__o_timed_out"/>
  </state>
  <final id="s_AWT__o_received">
    <onentry><raise event="done.outcome.s_AWT.received"/></onentry>
  </final>
  <final id="s_AWT__o_timed_out">
    <onentry><raise event="done.outcome.s_AWT.timed_out"/></onentry>
  </final>
</state>

The timer event carries the block id, so two awaits in the same chart never wake each other, and the <send> names no target, which under spec 6.2.2 is the running session's own external queue - the queue a durable host turns into a durable timer. This type does not know durable timers exist; it emits an ordinary delayed send.

With no timeout stored, the <onentry>, the timer transition and the timed_out <final> are all absent, so an await with no deadline compiles to the awaited transition and one final. The timed_out outcome stays declared: a parent wiring done.outcome.s_AWT.timed_out gets a transition that never fires rather than an unresolved target (ADR-0004's outcome amendment, 2c).

What is annotated and what is not

The awaited event attribute is stamped as coming from the event config key, so an upstream finding inside it reads as the author's typo rather than a bug in this type (ADR-0004 decision 9). The delay attribute is not, for the reason core.wait and core.send both give: those bytes are not the author's verbatim, since a repeated unit accumulates and a fraction expands.

fixtures()

One example event payload, so a palette panel can show what _event.data looks like when the awaited event arrives.

Under statifier-ui's docs/fixture-bundles.md, events is one sample _event.data payload per event name. The name here is an example, not this block's configured event - fixtures/0 takes no config and could not read one.

io(config)

core.wait's declaration byte for byte: a step that constrains nothing and is constrained by nothing beyond being one.

outcomes(config)

The two ways an await can finish, for every config.

See the moduledoc: the list does not follow timeout, because an unreached outcome costs a parent nothing and a disappearing one costs an author their wiring.

summary(config)

The event name, then the deadline when one is stored (ADR-0002 amendment H6).

The event comes first because it is what the block is for; the deadline is only the other way out. Each half is dropped on its own when it is absent or not well formed, so an await mid-edit shows the half the author has filled in rather than nothing, and the stored bytes are shown rather than the compiled ones for core.wait's reason - the card reads back what the inspector field beside it holds.

iex> StatifierBlocks.Core.Await.summary(%{"event" => "order.approved"})
["order.approved"]

iex> StatifierBlocks.Core.Await.summary(%{"event" => "order.approved", "timeout" => "48h"})
["order.approved", "by 48h"]

iex> StatifierBlocks.Core.Await.summary(%{})
[]