# `Statifier.Session.Effects`
[🔗](https://github.com/riddler/statifier-ex/blob/v2.0.0/lib/statifier/session/effects.ex#L1)

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.

1. An unsupported `type` (`Statifier.Send.Target.supported_type?/1`) ->
   `{:raise, :platform, "error.execution", ...}` on the sender's own
   internal queue. No delivery, no timer.
2. An unparseable `target` (`Statifier.Send.Target.parse/1` returns
   `{:invalid, _}`) -> the same `error.execution` (6.2.4's "not supported
   or invalid").
3. `:self` (no `target`) -> `{:enqueue_event, event}`, delivered straight
   to the sending session's own external queue.
4. `:internal` (`#_internal`/`_internal`) -> `{:deliver, :internal, event,
   effect}`, resolved by `Statifier.Session` through
   `Statifier.Interpreter.deliver_internal/5` (ADR-0039).
5. `{:session, _}`, `:parent`, `{:invoke, _}` -> `{:deliver, route, event,
   effect}`; `Statifier.Session` resolves the route (self-addressing needs
   no registry, decision 10; everything else is `error.communication`
   until 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.

# `context`

```elixir
@type context() :: %{
  session_id: String.t(),
  invoke_types: Statifier.Invoke.Types.t() | nil,
  invoke_handlers: %{required(String.t()) =&gt; module()},
  invocation_types: %{required(String.t()) =&gt; 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.

# `instruction`

```elixir
@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.

# `raise_kind`

```elixir
@type raise_kind() :: :internal | :platform
```

Which internal-queue writer `{:raise, ...}` should use - `Statifier.Interpreter.deliver_internal/5`'s own `kind`.

# `plan`

```elixir
@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 (`t:context/0`
- see the moduledoc's "The plan context" section): `session_id` is the
sending session's own id (spec 5.10's `_sessionid`), needed to build a
delivered event's `origin`; `invoke_types` is the registered-type
snapshot `plan_invoke/3` judges against, `invoke_handlers` is the
dispatch map it looks a registered type's handler module up in, and
`invocation_types` is the live snapshot `:cancel_invoke`/`:autoforward`
judge a tracked invocation's own type against before the same dispatch.
`:log`,
`:datamodel_change`,
`:datamodel_init`, and `:trace` effects plan to nothing but their own
`{:notify, effect}`.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
