The parent-held invocation table, as a value (ADR-0027 decision 3):
invokeid -> {child_session_id, pid, monitor_ref}, plus the autoforward
flag the delivery half ({:forward, invoke_id, event}) reads. A reverse
index, pid -> invoke_id, is carried alongside so a child's own :DOWN
can be resolved with no scan.
A handler-backed entry (ADR-0051)
Not every live invocation is a child session. An <invoke> dispatched to
a non-scxml Statifier.Invoke.Handler (ADR-0051 decision 4) has no
process of its own, so its entry carries session_id: nil, pid: nil,
and monitor_ref: nil - there is no child to identify, hand a pid for,
or monitor. It still carries type (needed to route a later
cancel_invoke/autoforward effect back to the same handler) and
autoforward, exactly like a child-session entry. put/3 skips the
by_pid reverse index for such an entry (there is no pid to index under),
and pop/2 skips the matching by_pid delete symmetrically;
pop_by_pid/2 needs no change at all - a pid-less entry was never
reachable through it.
Not Statifier.MachineState.active_invocations
MachineState.active_invocations (lib/statifier/machine_state.ex:60-87)
holds the core's view of a live invocation: {state_index, invoke_index} => invoke_id, immutable compiled-document identity, no pid, no monitor
ref, no session id. This module holds the session's view: process
identity the core never touches and never needs to. Statifier.Session
builds this table on top of that one, not in place of it - the two answer
different questions ("which invocations does Appendix D's cancelInvoke
walk still see" versus "which OS process does invocation i1 currently
run as, and who is watching it").
Pure by design (Decision 3)
Every function here is a pure map transformation - no Process.monitor/1,
no send/2, no DynamicSupervisor call. That keeps
Mix.Statifier.AdrGuard's @effect_interpreter_paths at exactly two
entries: lib/statifier/session.ex makes every process call this table's
data describes, and reads the result back through this module's API,
exactly the split Statifier.Session.Timers and Statifier.Session.Inbox
already model for delayed sends and the external queue.
seed_datamodel/2 lives in this module rather than a sibling for the same
reason: it is pure, it is read only from the {:start_child, _, _}
performer that also writes this table, and a third pure module for one
function would be structure without a second caller.
Summary
Types
What the table remembers about one live invocation - process identity the
core never holds. session_id is the child's own sess_ UXID, read back
once the child has started; pid/monitor_ref are the parent's own
handle on it; autoforward is the <invoke autoforward> attribute,
copied off Statifier.Effect.Invoke at start time; type is the
<invoke type> value itself - Statifier.Session writes it into every
entry it records, a built-in scxml entry included, so an
invoke_handlers map that explicitly overrides the literal "scxml"
type is honored on cancel/forward the same way it already is on start.
The key can still be absent (optional/1) on an entry built by older
code or by hand in a test; Statifier.Session.Effects.plan_one/2's own
dispatch treats that the same as an unrecorded type, defaulting to the
built-in handler. session_id, pid, and monitor_ref are nil for a
handler-backed entry (see the moduledoc's "A handler-backed entry"
section) - there is no child process behind it.
The public projection of one live invocation - invoke_id plus the child's
own session id and pid, and deliberately not the parent's monitor_ref or
the <invoke autoforward> flag (ADR-0050 decision 1).
Functions
The number of live invocations.
The whole invoke_id => entry map.
Looks up invoke_id's entry, :error when it names nothing live.
Every live invoke id, in no particular order.
Every live invocation as its public projection, sorted by invoke_id - a
stable order across reads, which invoke_ids/1's map-key order is not
(ADR-0050 decision 1).
Whether invoke_id names a live invocation - the discard predicate a later phase drains against.
An empty invocation table.
Removes invoke_id's entry from both maps, returning it (nil when it
named nothing live) alongside the table with it gone. The by_pid delete
is skipped, symmetrically with put/3, when the popped entry's pid is
nil (a handler-backed entry never occupied by_pid to begin with).
Removes whichever entry pid names, by the reverse index, returning
{invoke_id, entry} (nil when pid names nothing live) alongside the
table with it gone. What a child's own :DOWN resolves through.
Records entry under invoke_id, and indexes it under entry.pid in the
reverse map - skipped when entry.pid is nil (a handler-backed entry,
see the moduledoc's "A handler-backed entry" section), since there is no
pid to index under. A second put/3 for the same invoke_id (an
author-written id on a re-entered <invoke>, Decision 6's residual)
overwrites the first entry in both maps rather than merging with it.
6.4.3's name-matched seeding: keeps only those keys of params (the
Statifier.Effect.Invoke struct's already-coerced <param>/namelist map)
that match a top-level <data> id of child_machine, and drops the rest -
"If the names do not match, the Processor MUST NOT add the value."
Every live invocation's own type, invoke_id => type - what
Statifier.Session.Effects.plan_one/2 looks a cancel_invoke/
autoforward effect's handler module up in (ADR-0051 decision 6). Every
entry Statifier.Session writes carries type (a built-in scxml
entry's own literal type string included, not omitted - so an
invoke_handlers map that explicitly overrides "scxml" is still
honored on cancel/forward, not just on start), but an entry built by
older code or by hand in a test can still leave the key off; such an
entry is left out of this projection entirely rather than included with
a nil value, so the caller's own Map.get(_, invoke_id, ScxmlHandler)-shaped default does the same work either way.
Types
@type entry() :: %{ optional(:type) => String.t() | nil, session_id: String.t() | nil, pid: pid() | nil, monitor_ref: reference() | nil, autoforward: boolean() }
What the table remembers about one live invocation - process identity the
core never holds. session_id is the child's own sess_ UXID, read back
once the child has started; pid/monitor_ref are the parent's own
handle on it; autoforward is the <invoke autoforward> attribute,
copied off Statifier.Effect.Invoke at start time; type is the
<invoke type> value itself - Statifier.Session writes it into every
entry it records, a built-in scxml entry included, so an
invoke_handlers map that explicitly overrides the literal "scxml"
type is honored on cancel/forward the same way it already is on start.
The key can still be absent (optional/1) on an entry built by older
code or by hand in a test; Statifier.Session.Effects.plan_one/2's own
dispatch treats that the same as an unrecorded type, defaulting to the
built-in handler. session_id, pid, and monitor_ref are nil for a
handler-backed entry (see the moduledoc's "A handler-backed entry"
section) - there is no child process behind it.
The public projection of one live invocation - invoke_id plus the child's
own session id and pid, and deliberately not the parent's monitor_ref or
the <invoke autoforward> flag (ADR-0050 decision 1).
@opaque t()
Functions
@spec count(invocations :: t()) :: non_neg_integer()
The number of live invocations.
The whole invoke_id => entry map.
Looks up invoke_id's entry, :error when it names nothing live.
Every live invoke id, in no particular order.
@spec list(invocations :: t()) :: [public_entry()]
Every live invocation as its public projection, sorted by invoke_id - a
stable order across reads, which invoke_ids/1's map-key order is not
(ADR-0050 decision 1).
Whether invoke_id names a live invocation - the discard predicate a later phase drains against.
@spec new() :: t()
An empty invocation table.
Removes invoke_id's entry from both maps, returning it (nil when it
named nothing live) alongside the table with it gone. The by_pid delete
is skipped, symmetrically with put/3, when the popped entry's pid is
nil (a handler-backed entry never occupied by_pid to begin with).
Removes whichever entry pid names, by the reverse index, returning
{invoke_id, entry} (nil when pid names nothing live) alongside the
table with it gone. What a child's own :DOWN resolves through.
Records entry under invoke_id, and indexes it under entry.pid in the
reverse map - skipped when entry.pid is nil (a handler-backed entry,
see the moduledoc's "A handler-backed entry" section), since there is no
pid to index under. A second put/3 for the same invoke_id (an
author-written id on a re-entered <invoke>, Decision 6's residual)
overwrites the first entry in both maps rather than merging with it.
@spec seed_datamodel( params :: map() | :undefined | nil, child_machine :: Statifier.Machine.t() ) :: map()
6.4.3's name-matched seeding: keeps only those keys of params (the
Statifier.Effect.Invoke struct's already-coerced <param>/namelist map)
that match a top-level <data> id of child_machine, and drops the rest -
"If the names do not match, the Processor MUST NOT add the value."
params is :undefined - "no data", ADR-0037's sentinel - when the
invocation carried no <param>/namelist at all
(Statifier.EventData.coerce({:params, []})'s own empty-is-:undefined
rule). That seeds nothing, the same as an empty map would. nil is
accepted for the same outcome but means predicator's null rather than
absence, and no coercion produces it here.
Every live invocation's own type, invoke_id => type - what
Statifier.Session.Effects.plan_one/2 looks a cancel_invoke/
autoforward effect's handler module up in (ADR-0051 decision 6). Every
entry Statifier.Session writes carries type (a built-in scxml
entry's own literal type string included, not omitted - so an
invoke_handlers map that explicitly overrides "scxml" is still
honored on cancel/forward, not just on start), but an entry built by
older code or by hand in a test can still leave the key off; such an
entry is left out of this projection entirely rather than included with
a nil value, so the caller's own Map.get(_, invoke_id, ScxmlHandler)-shaped default does the same work either way.