# `Statifier.Invoke.SyncHandler.Adapter`
[🔗](https://github.com/riddler/statifier-ex/blob/v2.6.0/lib/statifier/invoke/sync_handler/adapter.ex#L1)

The `Statifier.Invoke.Handler` every host writing
`Statifier.Invoke.SyncHandler` modules would otherwise write by hand,
plus the two derived registrations those modules feed.

A host `use`s this in one module, naming its sync handlers:

    defmodule MyApp.InvokeHandler do
      use Statifier.Invoke.SyncHandler.Adapter,
        handlers: [MyApp.CardAuth.Handlers, MyApp.Signup.Handlers]
    end

and gets the four `Statifier.Invoke.Handler` callbacks plus three
readings of that list: `invoke_types/0` and `invoke_handlers/0` - the two
sets a host has to declare, both derived from it - and `sync_handlers/0`,
the list itself, for a host driving the pure core with no session to
report to.

    Statifier.Session.start_link(machine, invoke_handlers: MyApp.InvokeHandler.invoke_handlers())

    # a host driving the pure core, with no session to derive the snapshot
    Statifier.MachineState.new(machine,
      invoke_types: Statifier.Invoke.Types.new(types: MyApp.InvokeHandler.invoke_types())
    )

A session needs only `invoke_handlers/0`: `Statifier.Session` derives the
core's registered-type snapshot from that map itself
(`Statifier.Invoke.Types.from_handlers/1`). `invoke_types/0` is the plain
list of strings a host driving the pure core builds that snapshot from by
hand - `:invoke_types` on `Statifier.MachineState.new/2`, or
`Statifier.MachineState.put_invoke_types/2` after a resume, both of which
take a `%Statifier.Invoke.Types{}` rather than the list itself. Nothing is
declared to `Statifier.Compiler.compile/1`, which takes a document and no
options.

## One adapter, not one per handler module

Three sync-handler modules do not need three `Statifier.Invoke.Handler`
implementations. The four callbacks are identical for all of them - the
only difference is which type strings each answers, and that is data, not
code. So `invoke_handlers/0` points **every** registered type at the one
adapter module, and the adapter routes each call back to whichever
handler module claimed that type. Three modules each implementing four
callbacks identically would be this adapter written three times.

Nothing stops a host from having several adapters - one per bounded
context, say - each with its own handler list. What it must not do is
register the same type string in two of them and merge the maps, since
the merge silently keeps one.

## The two registrations cannot come apart

`invoke_handlers/0` is built **from** `invoke_types/0`, not beside it:
one union of `c:Statifier.Invoke.SyncHandler.invoke_types/0` across the
handler list, sorted and deduplicated, then every member mapped to this
adapter. Session start closes the same loop from the other end -
`Statifier.Invoke.Types.from_handlers/1` derives the core's registered-type
snapshot from the `:invoke_handlers` map's own keys (ADR-0051 decision
3's "one constructor"). So the set a host stamps onto a
`%Statifier.MachineState{}`, the set a session dispatches on, and the set
the pure core classifies an `<invoke>` against are three readings of one
list of modules, and registering a new type is one line in one handler
module and nothing anywhere else.

## Routing, and the type claimed twice

`dispatch/4` routes a type to the **first** module in the list that
claims it. A type claimed by two modules is a host bug this module does
not raise on: the union still contains it exactly once, so nothing
observable goes wrong except that the second module never answers. The
list is ordered and the host wrote it, which is the same posture
`Statifier.Send.Routes` and a palette take toward the caller's own
ordering. A type **no** module claims is `{:error, {:unknown_invoke_type,
type}}` - reachable only for a session started with a hand-built
`:invoke_handlers` map that names this adapter for a type its handlers do
not serve, since a map built by `invoke_handlers/0` cannot contain one.

## What the callbacks do

`start/2` plans exactly one `{:handler, __MODULE__, payload}` instruction
carrying the three facts `perform/2` needs: the `invoke_id` the answer is
reported against, the type to route on, and the `<param>` values. Pure.

`cancel/2` and `forward/3` plan nothing, and there is nothing dishonest
about that. A sync call is answered inside the performing turn, so by the
time a cancel could be planned the invocation is either already reported
or never will be - and the contract's "a handler MUST tolerate cancelling
an `invoke_id` it no longer knows" is satisfied for free by a handler
that keeps no table to look one up in. Autoforwarding (6.4.2) delivers
the parent's external events to a running invocation's inbox, and a call
with no inbox has nowhere to put the copy.

`perform/2` is the impure half: it routes the call, then reports the
answer to the session `ctx.session_id` names through
`Statifier.Session.done_invocation/3` or
`Statifier.Session.failed_invocation/3`. Reaching the session needs
`Statifier.Registry` running, which is what `Statifier.Supervisor` places;
a session id that resolves to nothing is
`{:error, {:session_not_registered, id}}` rather than a raise, because a
session that went away while its call was out is an ordinary thing to
observe. Errors are events here too.

## Idempotency

`perform/2` MAY be called more than once for the same `invoke_id`, and
this adapter deduplicates nothing - it has no view of a host's durable
store, exactly as `Statifier.Invoke.Handler` says the library has none. A
second call re-runs `c:Statifier.Invoke.SyncHandler.handle/3` and reports
again; the *reporting* half is harmless twice
(`Statifier.Session.done_invocation/3` for an invocation already popped
is a documented no-op), so the whole obligation lands on the handler,
where the moduledoc of `Statifier.Invoke.SyncHandler` leaves it.

## Failure reporting

`{:error, reason}` from a sync handler is permanent by construction - see
`Statifier.Invoke.SyncHandler`'s "Terminal failure" - so it is reported
through `failed_invocation/3` immediately, with `reason` normalized to
the string a chart reads as `_event.data.reason`: a binary passes
through, anything else is `inspect/1`-ed. No `:attempts` is sent, and
that absence is the honest datum: this adapter is a retry policy that
makes no attempts to count, and `Statifier.Session.failed_invocation/3`
documents an absent `:attempts` as reading `undefined` (ADR-0037), which
is distinct from a host that counted zero.

# `dispatch_ctx`

```elixir
@type dispatch_ctx() :: map()
```

What `dispatch/4` threads to the handler it routes to: any map.

A session's own drive supplies `t:Statifier.Invoke.SyncHandler.ctx/0` -
the plan context, with `session_id` and the two registrations - and that
is still the context `perform/3` requires, because reporting the answer
needs `ctx.session_id` to report it to. Routing needs none of it.
`dispatch/4` reads no key of the context at all; it resolves the module
from `type` and hands the context through untouched.

So the type is `map()` rather than the plan context, and that is the
honest shape rather than a loosening: `dispatch/4` is public *for* the
host driving the pure core with no session (see its doc), and such a
host has no `session_id` to put in a plan context. Typed narrowly, the
only thing the spec achieved was to make the documented use a contract
violation - the host re-implemented the routing beside this function
instead of delegating to it.

The agreement about what the map contains is then between that host and
its own handler modules, which is where it can be kept: a handler
written against `c:Statifier.Invoke.SyncHandler.handle/3`'s declared
`ctx` still gets exactly that from a session, and a handler a host also
drives itself matches on whichever shape it is handed.

# `payload`

```elixir
@type payload() :: %{invoke_id: String.t(), type: String.t(), params: map()}
```

The instruction payload `start/2` plans and `perform/2` consumes. Not part
of the public contract - `Statifier.Session.Effects`'s instruction
vocabulary is opaque outside the library - but named because `perform/2`'s
spec has to say something, and a host reading a trace of planned
instructions will see this shape.

# `__using__`
*macro* 

```elixir
@spec __using__(opts :: Macro.t()) :: Macro.t()
```

Generates the `Statifier.Invoke.Handler` implementation and the two
derived registrations over `:handlers`.

`:handlers` is required and is the list of
`Statifier.Invoke.SyncHandler` modules this adapter serves, in the order
`dispatch/4` resolves a type against.

# `dispatch`

```elixir
@spec dispatch(
  modules :: [module()],
  type :: String.t(),
  params :: map(),
  ctx :: dispatch_ctx()
) :: {:ok, Statifier.Invoke.SyncHandler.donedata()} | {:error, term()}
```

Routes one call to the first module in `modules` claiming `type`.

Public because a host driving the pure core itself - a durable stepper
with no `Statifier.Session` process to report to - performs its
invocations its own way and still wants the routing, without the
reporting half `perform/3` supplies.

`ctx` is whatever that caller has to say about the call, handed to the
handler untouched: `t:dispatch_ctx/0`, any map, and not the plan context
`perform/3` needs. A session's drive passes its plan context; a durable
stepper passes what it knows, typically its own run id. Neither is a
special case here, because routing reads no key of it.

# `invoke_handlers`

```elixir
@spec invoke_handlers(modules :: [module()], adapter :: module()) :: %{
  required(String.t()) =&gt; module()
}
```

The `%{invoke type => module}` map a session is started with: every type
`modules` claim, pointed at `adapter`.

`adapter` is the module implementing `Statifier.Invoke.Handler` - the one
that `use`s this module, not one of the sync handlers, which implement no
`Statifier.Invoke.Handler` callback of their own.

# `invoke_types`

```elixir
@spec invoke_types(modules :: [module()]) :: [String.t()]
```

The union of `c:Statifier.Invoke.SyncHandler.invoke_types/0` across
`modules`, sorted and deduplicated.

The single union in the library: `invoke_handlers/2` is built from this
answer rather than from a second walk of `modules`, so the compiler's set
and the session's map are two readings of one list (see the moduledoc's
"The two registrations cannot come apart").

Raises `ArgumentError` for a module that does not export `invoke_types/0`
- a host naming a module that is not a sync handler learns it here rather
than at the `UndefinedFunctionError` a session would raise mid-drive.

# `perform`

```elixir
@spec perform(
  modules :: [module()],
  instruction :: payload(),
  ctx :: Statifier.Invoke.SyncHandler.ctx()
) :: :ok | {:error, {:session_not_registered, String.t()}}
```

Runs one planned call and reports the answer to the session
`ctx.session_id` names. The impure half; the `perform/2` a `use`-ing
module delegates to.

# `plan_cancel`

```elixir
@spec plan_cancel(invoke_id :: String.t(), ctx :: Statifier.Invoke.SyncHandler.ctx()) ::
  {:ok, []}
```

Plans nothing. Pure; see the moduledoc on why a sync call has nothing to
cancel.

# `plan_forward`

```elixir
@spec plan_forward(
  invoke_id :: String.t(),
  event :: Statifier.Event.t(),
  ctx :: Statifier.Invoke.SyncHandler.ctx()
) :: {:ok, []}
```

Plans nothing. Pure; see the moduledoc on why a sync call has no inbox to
autoforward into.

# `plan_start`

```elixir
@spec plan_start(
  adapter :: module(),
  invoke :: Statifier.Effect.Invoke.t(),
  ctx :: Statifier.Invoke.SyncHandler.ctx()
) :: {:ok, [{:handler, module(), payload()}]}
```

Plans the one `{:handler, adapter, payload}` instruction a sync call
needs. Pure; the `start/2` a `use`-ing module delegates to.

Named `plan_*` rather than `start`/`cancel`/`forward` because a
`use`-ing module's generated callbacks carry those names at those
arities, and one module defining `cancel/2` twice - once as the callback,
once as the helper it delegates to - reads as a mistake even where the
compiler is fine with it.

---

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