Turns the effect list the pure core returns into an ordered list of
instructions for Statifier.Session to perform (ADR-0003). Deciding is
here, where it is a pure function of the effect list; performing is there,
where the process is.
Every effect produces a {:notify, effect} instruction in its original
position, so a subscriber sees the whole stream in order - trace effects
included, since they are ordinary list members and not a side channel.
Effects that also mean something to the session emit their action
immediately after their own :notify.
<send> routing
Every <send>/<send_delayed> effect is planned in this order (6.2.4,
6.2.5, C.1). Steps 1 and 2 are a boundary check, not the primary
enforcement: ADR-0047 decision 4 keeps them because
Statifier.Session.interpret/2 is public (ADR-0029) and an embedder can
hand in an effect the core itself never produced, but an effect the core
did produce never reaches these two arms - the core already rejected an
invalid target or unsupported type in Statifier.Machine.Content.Send
before any {:send, _}/{:send_delayed, _} effect was built.
- An unsupported
type(Statifier.Send.Target.supported_type?/1) ->{:raise, :platform, "error.execution", ...}on the sender's own internal queue. No delivery, no timer. - An unparseable
target(Statifier.Send.Target.parse/1returns{:invalid, _}) -> the sameerror.execution(6.2.4's "not supported or invalid"). :self(notarget) ->{:enqueue_event, event}, delivered straight to the sending session's own external queue.:internal(#_internal/_internal) ->{:deliver, :internal, event, effect}, resolved byStatifier.SessionthroughStatifier.Interpreter.deliver_internal/5(ADR-0039).{:session, _},:parent,{:invoke, _}->{:deliver, route, event, effect};Statifier.Sessionresolves the route (self-addressing needs no registry, decision 10; everything else iserror.communicationuntil a later bead adds one).
A delayed send takes the same route through {:schedule, send_id, delay_ms, route, event, effect} instead of {:enqueue_event, _} /
{:deliver, _, _, _} - the type/target checks above still run at plan
time (6.2.3: arguments are evaluated when <send> is evaluated, not when
the message is dispatched), but the route itself is only resolved when the
timer fires.
<invoke> routing
plan_invoke/3 checks type first, mirroring <send>'s own order
(6.2.5's unsupported-type check ahead of target resolution): an
unregistered type (Statifier.Invoke.Types.registered?/2, judged
against the plan context's :invoke_types snapshot - ADR-0051) plans
{:raise, :platform, "error.execution", {:invoke, state_index, invoke_index}, []} and nothing else - 3.12.2 puts an unregistered type
in error.execution's class ("errors internal to the execution of the
document"), the same class <send>'s own unsupported-type check uses,
because no communication is attempted at all. A registered type looks
up its handler module in the plan context's :invoke_handlers map
(Map.get/3, defaulting to Statifier.Invoke.Handler.Scxml - the
built-in handler is just the default map entry, so no branch here names
scxml specially, ADR-0051 decision 4) and splices module.start/2's
own returned instructions into the plan. The built-in handler's start/2
returns {:start_child, invoke, effect} unchanged from what this module
used to produce directly - Statifier.Session resolves the source, seeds
the child's datamodel, and starts it (ADR-0027 decision 3, ADR-0038).
The plan context
plan/2's second argument is a plain map, not a bare session id:
%{session_id: String.t(), invoke_types: Statifier.Invoke.Types.t() | nil, invoke_handlers: %{String.t() => module()}, invocation_types: %{String.t() => String.t()}} (ADR-0051 decisions 2, 4, and 6).
session_id is what every plan_send/3 / plan_send_delayed/3 call used
to receive directly; invoke_types is the caller-declared registered set
plan_invoke/3 judges against, read off the same %MachineState{} the
core was stamped with, so the planner's answer and the core's
maybe_record_active_invocation/5 answer cannot drift (ADR-0047 decision
4); invoke_handlers is the per-session handler dispatch map
plan_invoke/3 looks a registered type's module up in, and is also
exactly the ctx argument every Statifier.Invoke.Handler planning
callback receives; invocation_types is the live invoke_id => type
snapshot plan_one/2's :cancel_invoke/:autoforward arms look a
tracked invocation's own type up in, before doing the same
invoke_handlers dispatch plan_invoke/3 does at start time (decision
6). Both Statifier.Session and Statifier.Replay build this map from
the %MachineState{}/session state they already hold before calling
plan/2, deriving invoke_types, invoke_handlers, and
invocation_types from the same source so none of the three can diverge
(ADR-0051 decision 3's "one constructor").
:autoforward and :cancel_invoke route through the owning invocation's
own handler (ADR-0051 decision 6), the same dispatch plan_invoke/3 uses
for start/2: the plan context's :invocation_types map (built by
Statifier.Session/Statifier.Replay from the live invocation table,
Statifier.Session.Invocations.types/1) answers invoke_id's own type,
looked up in :invoke_handlers the same way, defaulting to
Statifier.Invoke.Handler.Scxml when invoke_id names nothing tracked -
an invocation the built-in handler started (which never records its own
type, since its dispatch already defaults to it), or one that is no
longer live at all (a cancel_invoke/autoforward naming a
dead/never-started invocation). The built-in handler's cancel/2 and
forward/3 return exactly the {:stop_child, invoke_id}/{:forward, invoke_id, event} instructions this module used to emit directly, so
dispatching through it changes nothing observable for type=scxml.
:autoforward's dispatch carries no type or target check of its own
beyond the handler lookup, since the effect is the core's own decision
about an invocation this session started, not a <send> with
author-written attributes. The built-in handler's {:forward, invoke_id, event} instruction has Statifier.Session look invoke_id up in its
invocation table and forward event unmodified (6.4.2's "All the fields
specified in 5.10.1 ... MUST have the same values in the forwarded
copy"); a miss, or a handler-backed entry with no pid to deliver to, is a
silent no-op, not an error (6.4.3's MUST-ignore for a cancelled
invocation).
:cancel_invoke's dispatch is unconditional for the same reason
:autoforward's is: this is the core's own reaction to a state exiting
while one of its <invoke>s is still live, not an author-addressed
element. The built-in handler's {:stop_child, invoke_id} instruction has
Statifier.Session pop the table entry and, when it held a pid, demonitor
and cancel the child (6.4.3); a miss, or an entry with no pid, is a
silent no-op - the invocation having already been popped by its own
:DOWN or a prior cancel, or never having had a child process at all.
Summary
Types
The pure fold's context (ADR-0051 decisions 2, 4, and 6) - see the
moduledoc's "The plan context" section, and
Statifier.Invoke.Handler.t:ctx/0, which this is (plus invocation_types,
a key no handler callback reads - see its own doc below). session_id is
the sending session's own id (spec 5.10's _sessionid); invoke_types is
the caller-declared registered-type snapshot plan_invoke/3 judges
against, or nil for "no declaration made"; invoke_handlers is the
per-session <invoke type> => module dispatch map plan_invoke/3 looks
a registered type's handler up in; invocation_types is the live
invoke_id => type snapshot plan_one/2's :cancel_invoke/
:autoforward arms judge against before doing the same
invoke_handlers dispatch.
One instruction for Statifier.Session to perform.
Which internal-queue writer {:raise, ...} should use - Statifier.Interpreter.deliver_internal/5's own kind.
Functions
Plans effects, the core's own order preserved, into the instructions
Statifier.Session performs. context is the plan context (context/0
Types
@type context() :: %{ session_id: String.t(), invoke_types: Statifier.Invoke.Types.t() | nil, invoke_handlers: %{required(String.t()) => module()}, invocation_types: %{required(String.t()) => String.t()} }
The pure fold's context (ADR-0051 decisions 2, 4, and 6) - see the
moduledoc's "The plan context" section, and
Statifier.Invoke.Handler.t:ctx/0, which this is (plus invocation_types,
a key no handler callback reads - see its own doc below). session_id is
the sending session's own id (spec 5.10's _sessionid); invoke_types is
the caller-declared registered-type snapshot plan_invoke/3 judges
against, or nil for "no declaration made"; invoke_handlers is the
per-session <invoke type> => module dispatch map plan_invoke/3 looks
a registered type's handler up in; invocation_types is the live
invoke_id => type snapshot plan_one/2's :cancel_invoke/
:autoforward arms judge against before doing the same
invoke_handlers dispatch.
@type instruction() :: {:notify, Statifier.Effect.t()} | {:enqueue_event, Statifier.Event.t()} | {:deliver, Statifier.Send.Target.route(), Statifier.Event.t(), Statifier.Effect.t()} | {:raise, raise_kind(), name :: String.t(), Statifier.Event.Cause.origin(), keyword()} | {:schedule, send_id :: String.t() | nil, delay_ms :: non_neg_integer(), Statifier.Send.Target.route(), Statifier.Event.t(), Statifier.Effect.t()} | {:cancel_timers, send_id :: String.t()} | {:start_child, Statifier.Effect.Invoke.t(), Statifier.Effect.t()} | {:forward, invoke_id :: String.t(), Statifier.Event.t()} | {:stop_child, invoke_id :: String.t()} | {:handler, module(), term()} | {:unroutable, Statifier.Effect.t()} | {:halt, :done | :budget_exhausted}
One instruction for Statifier.Session to perform.
@type raise_kind() :: :internal | :platform
Which internal-queue writer {:raise, ...} should use - Statifier.Interpreter.deliver_internal/5's own kind.
Functions
@spec plan(effects :: [Statifier.Effect.t()], context :: context()) :: [instruction()]
Plans effects, the core's own order preserved, into the instructions
Statifier.Session performs. context is the plan context (context/0
- see the moduledoc's "The plan context" section):
session_idis the sending session's own id (spec 5.10's_sessionid), needed to build a delivered event'sorigin;invoke_typesis the registered-type snapshotplan_invoke/3judges against,invoke_handlersis the dispatch map it looks a registered type's handler module up in, andinvocation_typesis the live snapshot:cancel_invoke/:autoforwardjudge a tracked invocation's own type against before the same dispatch.:log,:datamodel_change,:datamodel_init, and:traceeffects plan to nothing but their own{:notify, effect}.