StatifierBlocks.Core.Invoke (StatifierBlocks v0.7.0)

Copy Markdown View Source

core.invoke: a step that calls the host and waits for it to answer, with an optional subtree for the failure case (ADR-0002 decision 10, as amended 2026-08-29 section D).

This type names an invoke type; it never runs one. Which handler a name resolves to is deployment state supplied per session (st-ADR-0051), and the palette is authoring state supplied per operation - the two-registry seam ADR-0002 decision 2 draws, which this is the first core.* type to stand on. StatifierBlocks.Compiler.InvokeTypes is what a host compares the two against, at the one moment it holds both.

The failure path is a slot, not a port

A failing call is one of the two ways this block can finish, and the way an author says what happens then is by putting blocks in the on_error slot. It is not a second outlet with an author-drawn edge: every edge in a document is a parent/slot/child relationship, which is the invariant the editor's rendered connectors rest on (D13, and ADR-0002's amendment section A2). The slot is a rail beside the step, declared with zero_or_one arity and the :failure slot style ADR-0005 decision 10's 2026-08-29 amendment (10g) names for exactly this slot: an in-band continuation path taken on a bad outcome, rendered on the rail core.group's interrupts already uses but in the error family rather than the interrupt one. The rendering half of 10h is the renderer's, not this type's: all a block type does is declare the style.

Two outcomes

done and error, in that order. Under ADR-0004's outcome amendment each one the block reaches compiles to its own <final>, minted through StatifierBlocks.Compiler.Context.outcome_id/2, whose entry raises done.outcome.<state id>.<outcome>. A parent that does not care which way the call went wires the prefix and never learns an outcome name; one that does names the full event. The block decides nothing about what comes next: its emission ends at the final it enters.

The params field is knowingly provisional

ADR-0002 decision 7's field types are a closed set and none of them is "a list of name/path pairs", so params is a :string holding one name=path per line. That is the amendment's own compromise, recorded in its section D1 as a deferred question: whichever way it is resolved - a new field type, a dedicated control, or the flattening as shipped - it is a decision 7 change rather than a change to this type's row.

Summary

Functions

A compound state that runs the call in an inner state and finishes at the <final> of whichever outcome it reached.

A step with two outcomes, so produces is :unknown for the reason core.branch declares it: joining the type this block's own call produces with the type its on_error subtree produces is the lattice ADR-0003 decision 4 refuses to build.

The params field's rows, as {name, path} pairs in the order the author wrote them.

One optional slot, on_error, holding what runs when the call fails.

Functions

emit(block, context)

A compound state that runs the call in an inner state and finishes at the <final> of whichever outcome it reached.

<state id="s_INV" initial="s_INV__running">
  <state id="s_INV__running">
    <invoke type="myapp:authorize">
      <param expr="order.amount" name="amount"/>
    </invoke>
    <transition event="done.invoke" target="s_INV__o_done">
      <assign expr="_event.data" location="authorization"/>
    </transition>
    <transition event="error.communication.invoke" target="s_blk_PARK"/>
  </state>
  {the on_error child's own subtree}
  <transition event="done.state.s_blk_PARK" target="s_INV__o_error" type="internal"/>
  <final id="s_INV__o_done">
    <onentry><raise event="done.outcome.s_INV.done"/></onentry>
  </final>
  <final id="s_INV__o_error">
    <onentry><raise event="done.outcome.s_INV.error"/></onentry>
  </final>
</state>

Both transitions match by prefix, and neither names an id

The <invoke> carries no id, so the engine mints one, and the two transitions match done.invoke and error.communication.invoke by SCXML's descriptor prefix rule rather than naming the invocation. That is safe for exactly the reason ADR-0004's worked example gives: both transitions sit on the inner state, which is active only while this block's own call is outstanding, so no other invocation's completion can be selected by them.

error.communication.invoke.<invoke_id> is statifier-ex ADR-0068's name - a blessed suffix extension of the error.communication that st-ADR-0051 decision 1 already assigns to this failure - which is what makes the prefix match here and a host chart's existing error.communication handler both correct at once.

An absent on_error emits no failure transition at all

With the slot empty there is nothing to transition to, so neither the failure transition nor the error outcome's <final> is emitted and the error propagates as it does today. That costs a parent nothing: outcome wiring is an event rather than a target, so a parent may transition on an outcome whose final was never emitted and the transition simply never fires (ADR-0004's amendment, 2c).

Who owns what

Everything here is this block's except one transition: the one leaving the on_error child for the error final is attributed to that child, because "what happens after the parking step finishes" is a fact about the child (decision 5, the same rule Emit.chain/2 follows). The type attribute's value is stamped as coming from invoke_type and the location's from assign_to, so an upstream finding inside either is the author's typo rather than a bug in this type.

io(config)

A step with two outcomes, so produces is :unknown for the reason core.branch declares it: joining the type this block's own call produces with the type its on_error subtree produces is the lattice ADR-0003 decision 4 refuses to build.

consumes is absent - an invoke reads its inputs through params, out of the datamodel, rather than through the type flow.

param_rows(value)

@spec param_rows(term()) :: {:ok, [{String.t(), String.t()}]} | {:error, String.t()}

The params field's rows, as {name, path} pairs in the order the author wrote them.

One name=path per line, blank lines ignored. Public because the editor and the tests both need the same reading of the flattened field, and two spellings of it would be two chances for them to disagree - the field is provisional (ADR-0002's amendment, D1) and this function is where the provisionality is contained.

slots(config)

One optional slot, on_error, holding what runs when the call fails.

Its arity is zero_or_one because a failure path is one continuation, not a list of them: an author who wants several steps there puts a core.sequence in it, exactly as anywhere else a single child is asked for.