StatifierBlocks.Core.Send (StatifierBlocks v0.2.0)

Copy Markdown View Source

core.send: a leaf step that names an event and says when.

PROVISIONAL - not yet in the vocabulary table

core.send is a campaign-013 spike proposal (spike/js/proposed-core.js) that ADR-0002 decision 10's vocabulary table, and its 2026-08-28 amendment, do not yet carry - the amendment's section D promotes core.invoke and core.raise only. This type is shipped under the campaign's filing; the decision-10 row it is owed has not been written.

Two config fields: event, in the same grammar core.raise and core.on_event speak, and delay, an optional duration.

Why this is not core.raise with a delay key

core.raise names an event and hands control on, now: the event goes on the internal queue and is processed before anything else the session is holding, which is the property an enclosing group's interrupt rail depends on. A delayed send goes on the external queue, outlives the step that armed it, has to survive a restart, and is turned into a durable timer by infrastructure outside the interpreter (statifier-ex docs/durable-timers.md, ADR-0054/0059; the sob- lane). A delay key that quietly turned a raise into a durable timer would make core.raise's whole note false half the time, so they are two types.

The block finishes when the send is armed

This is what separates core.send from core.wait. A wait keeps the chart live for the duration; a send arms an event and completes in the same macrostep. So there is still one outcome and still no join to refuse - io/1 leaves produces absent rather than :unknown, the way core.raise and core.wait do.

Two things it deliberately does not declare

No target. What a target may name is not this repo's decision: session identity is statifier-ex's (ADR-0052) and reaching the host is the invoke seam st-ADR-0051 defines, which this vocabulary already spells core.invoke. A config key that validates nothing is a proposal made by accident. Absent a target, spec 6.2.2 puts the event on the running session's own external queue, which is exactly what statifier_oban turns into a durable timer.

No cancel - and that is a recorded gap, not an oversight. A delayed send this block arms is never cancelled by anything this package emits: no <cancel>, and no sendid an author could name. A cancel names the send it cancels, which makes it a cross-subtree reference to another block - the exact shape ADR-0005 decision 13 refused - and the alternative that keeps the tree invariant is scope-shaped rather than reference-shaped: a delayed send is cancelled when the region that armed it is left. Which of those is right is the delayed-send lifetime ruling, filed here as sb-b4f and mirrored to statifier-ex as st-q3ud. It is out of scope for campaign 014 and nothing here should grow a cancel until it is ruled. The consequence a reader needs today: an armed send fires even if the block, its group, or the whole chart moved on.

The delay's stored form

delay is the vocabulary's first optional duration, and "no delay" is neither PT0S nor an unfinished field - an absent or empty delay emits no delay attribute at all, so the event goes out now.

A present delay may be stored in either spelling: ISO-8601 (PT2H), or a predicator duration string (1h30m, 2d, 3d8h). That two-spelling stored form is campaign 014's D4 proposal, filed as sb-709 and implemented in the spike's duration control; it is a recorded proposal, not a ruling, and ADR-0002 decision 7's :duration field type is unchanged. StatifierBlocks.Core.Duration is where the compile lives and where the grammar's one home - predicator's own lexer - is read rather than mirrored.

Summary

Functions

The event first, then the delay.

A compound state whose entry sends the event and immediately goes final.

core.raise's io/1 exactly, and for core.raise's reason: one outcome, so produces is absent rather than :unknown. A send reads nothing through the type flow - its event name and its delay are its own config - so there is no consumes either.

The spike's descriptor declares a static sends badge here. ADR-0005 decision 10's palette_entry/0 key set has no badge, so there is nothing to declare it in yet and the icon carries the whole visual signal - a Phase-B gap the spike's own note already records.

Functions

config_schema(config)

The event first, then the delay.

The delay's default is "" and not a duration: a send with no delay is the ordinary case, and core.wait's PT1H default would author a timer nobody asked for.

emit(block, context)

A compound state whose entry sends the event and immediately goes final.

<state id="s_blk_SND" initial="s_blk_SND__done">
  <onentry><send delay="2h" event="signup.abandoned"/></onentry>
  <final id="s_blk_SND__done"/>
</state>

There is nothing to sequence - arming the send is instantaneous and the block is done the moment it is entered - so initial points straight at the <final> and no auxiliary state is minted. That is core.raise's shape with a different element inside the <onentry>, which is the right way round: the difference between the two types is which queue the event lands on, not how long the block lasts.

With no delay the <send> carries no delay attribute at all, so the event goes out on the current macrostep. Emission.element/3 drops a nil attribute, which is how "no delay" and delay="" stay indistinguishable in the emitted bytes.

event is attributed and delay is not

ADR-0004 decision 9's annotation is for an attribute whose value is the author's verbatim. event is: those bytes came out of the config unchanged. The delay attribute's bytes did not - a stored PT2H emits as 2h, and a stored 1h30m is canonicalised through ISO-8601 before it is rendered - so annotating it would point a runtime finding at a span the author never typed. core.wait leaves its own delay unannotated for the same reason.

No <cancel>, and no id to cancel by

The <send> is deliberately anonymous: minting a sendid here would be half of a cancel mechanism whose other half sb-b4f (mirrored as statifier-ex st-q3ud) has not ruled on, and a half-built one is worse than none. See the moduledoc.

io(config)

core.raise's io/1 exactly, and for core.raise's reason: one outcome, so produces is absent rather than :unknown. A send reads nothing through the type flow - its event name and its delay are its own config - so there is no consumes either.

palette_entry()

The spike's descriptor declares a static sends badge here. ADR-0005 decision 10's palette_entry/0 key set has no badge, so there is nothing to declare it in yet and the icon carries the whole visual signal - a Phase-B gap the spike's own note already records.