StatifierBlocks.Core.OnEvent (StatifierBlocks v0.13.0)

Copy Markdown View Source

core.on_event: an interrupt handler, valid inside an interrupts slot and nowhere else (ADR-0002 decision 10).

A leaf with three config fields: the event that fires it, an optional cond that decides whether it fires at all, and the outcome that decides what happens to the group it interrupts.

Placement, in both directions, from one tag

This type declares kinds: [:interrupt_handler] and nothing else. That single tag is the whole placement rule:

  • an on_event dropped into a body slot fails, because body declares [:step] and the two sets do not intersect;
  • an ordinary step dropped into interrupts fails, because interrupts declares [:interrupt_handler] and a step is not one.

ADR-0002 decision 10 originally recorded the first direction as a special-cased validation rule the core types carry, and withdrew it at acceptance in favour of ADR-0003 decision 3's kind tags, which close both directions with one declaration on each side. There is no placement check in this module, and there is not supposed to be one: adding it back would give the editor two code paths to highlight from.

This type also never names the group types it may live inside. A host group with an interrupts slot admits it by declaring "interrupts" => [:interrupt_handler], and a host with a genuinely different notion of interrupt handler mints its own kind and its own group without touching this package.

The outcome values

ADR-0002 decision 10 fixes outcome as a :select and names no values. Two are implemented:

outcomeMeans
"abandon"leave the group and do not come back
"resume"handle the event and re-enter the group

They are the minimal pair ADR-0001 decision 10's compile target needs - transitions on the group's state, with the group's own history mode deciding where a "resume" re-enters. A third value is a config_schema/1 change plus a current_version/0 bump, not a document schema change.

The optional cond guard

cond is an optional :expression field, and when it is set it becomes the cond on the watcher's transition: the handler fires only when the event arrives and the condition holds. A handler with no cond - the key absent, or blank - emits exactly the bytes it emitted before the key existed, which is what keeps it an additive key rather than a document schema change.

The guard belongs here rather than on a core.branch after the handler, which is the shape it would otherwise be spelled as. A core.on_event decides whether to leave the in-flight body at all, and by the time a branch inside the handler could read a condition the body has already been abandoned - so the two spellings do not express the same thing, and only this one expresses a guarded interrupt. See ADR-0002's 2026-08-31 note.

The condition is the author's bytes passed through into predicator's datamodel verbatim. This package ships no expression checking of its own (ADR-0004 decision 9), so validate_config/1 only asks whether the stored value is a string; a typo inside it surfaces as an upstream compile error routed back to the "cond" field by the cond_key this type passes to StatifierBlocks.Core.Emit.transition/2.

Unlike core.branch, this type declares no value_path: its condition is stored at config["cond"], so ADR-0002 decision 7's default path - [key] - already addresses it. And summary/1 is untouched. ADR-0002 amendment H6 fixes this type's card as the outcome word then the event name, and the reason core.branch counts its arms rather than listing their conditions holds here too: an expression is not a chip.

Summary

Functions

A compound state that waits for event and, when it arrives, raises the interrupt-protocol event its outcome names before going final.

One example event payload, so a palette panel can show what _event.data looks like when this handler fires.

The outcome's word, then the event name, as a chip list (ADR-0002 amendment H6).

Functions

emit(block, context)

A compound state that waits for event and, when it arrives, raises the interrupt-protocol event its outcome names before going final.

<state id="s_INT" initial="s_INT__armed">
  <state id="s_INT__armed">
    <transition event="order.cancelled" target="s_INT__done">
      <raise event="statifier_blocks.interrupt.abandon"/>
    </transition>
  </state>
  <final id="s_INT__done"/>
</state>

The group this handler sits in runs it as a region of a <parallel> alongside the body, which is what keeps it live while the body works, and transitions on both protocol events unconditionally - see StatifierBlocks.Core.Emit. The raise is how the outcome crosses that seam: ADR-0004 decision 4 keeps a child's config out of its parent's context on purpose, so the group cannot read outcome and must not try.

A raised event is internal, so it is processed before any external event the queue is holding, and a nested group's handler is selected over an outer group's because SCXML prefers the transition whose source is the deepest active state.

A guarded handler

A cond in config becomes the cond on that one transition, and nothing else about the shape moves:

<state id="s_INT__armed">
  <transition cond="review.parked" event="review.resolved" target="s_INT__done">
    <raise event="statifier_blocks.interrupt.resume"/>
  </transition>
</state>

So the event arriving while the condition is false leaves the handler armed and the body running - the interrupt simply does not happen, and the same event arriving later, once the condition holds, still fires it. A handler with no cond writes no cond attribute at all (StatifierBlocks.Core.Emit.transition/2 drops an absent one), which is why an unguarded handler's bytes are unchanged by this key existing.

The cond_key passed alongside is "cond", the config key the author typed into, so an upstream expression error lands on that field rather than reading as a bug in this type (ADR-0004 decision 9). It is passed unconditionally, guard or no guard: StatifierBlocks.Emission.attribute_from_config/3 records an owner only for an attribute the element actually carries, so an unguarded handler records none without this call site testing for it twice.

fixtures()

One example event payload, so a palette panel can show what _event.data looks like when this handler fires.

Provisional: the accepted spellings are not settled

PROVISIONAL - see ADR-0002 decision 9. The atom-keyed spelling below comes from an amendment to that decision which has not been accepted. Until it is, treat the shape as the intended target rather than a settled contract. That this callback exists, and returns term(), is settled either way.

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.

summary(config)

The outcome's word, then the event name, as a chip list (ADR-0002 amendment H6).

The outcome comes first because it is what the block does; the event is only when. Each half is dropped on its own when it is not there or not well formed, so a handler mid-edit shows the half the author has filled in rather than nothing.

iex> StatifierBlocks.Core.OnEvent.summary(%{"outcome" => "abandon", "event" => "order.cancelled"})
["Abandon", "order.cancelled"]

iex> StatifierBlocks.Core.OnEvent.summary(%{"outcome" => "resume"})
["Resume"]

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