# `Raxol.Gateway.SessionRouter`
[🔗](https://github.com/DROOdotFOO/raxol/blob/raxol_gateway-v0.2.0/lib/raxol/gateway/session_router.ex#L1)

Routes inbound events to per-chat sessions, one process per chat.

Generalizes the Telegram per-chat router: sessions are keyed by
`Raxol.Gateway.Route.key/1` (`agent:main:{platform}:{chat_type}:{chat_id}`),
started lazily under a `DynamicSupervisor`, monitored for cleanup, and bounded
by the same idle-timeout, per-key start cooldown, and max-session limits.

## Options

  * `:handler` (required) -- `{module, opts}` the sessions run. The router
    refuses to start, with `{:handler_not_available, module}`, when the
    module cannot be loaded.
  * `:sessions_sup` (required) -- a `DynamicSupervisor` name for the sessions
  * `:adapter` -- `{module, conn}`; outbound is delivered via its
    `send_message/3`. Or pass `:deliver`, a `(Route.t(), rendered -> any())`.
  * `:max_sessions` (default 1000)
  * `:idle_timeout` ms (default 10 minutes)
  * `:handler_init_timeout` -- passed to each session; ms its handler's
    `init/2` may take before the session is killed (default 30 seconds)
  * `:cooldown_ms` -- minimum ms between starts for one key (default 5000)
  * `:authorize` -- `(Route.t() -> :allow | :deny)`, consulted before a session
    is started or an event delivered. Unset allows everything.
  * `:name` -- the router's registered name

## Authorization

The router is the narrow point every session passes through, so the gate goes
here rather than in each caller. `:authorize` is a function and not a Pairing
server name because the router does not depend on how the decision is made --
`Raxol.Gateway.Pairing` is one implementation, and a deployment with its own
identity system should not have to route around the gate to use it.

Wiring it means no caller can skip it: `route/3` and `start_session/2` are
public, and a feed loop that calls them directly gets the same decision the
documented path gets. Leaving it unset is the open posture, which is why
`Raxol.Console.Boot` always sets it -- an open Console is open because its
Pairing server allows those platforms, not because nothing asked.

The function runs inside the router's own call, so it should be cheap: every
event pays for it, including events for sessions that already exist. A function
that raises or exits is logged and DENIES rather than crashing the router --
a gate that cannot answer is not a reason to serve the chat.

## Telemetry

Every event is `[:raxol_gateway, :session, event]`, measured
`%{system_time: _}`, with the session's `:key` in metadata.

  * `:started` -- a session PROCESS was spawned. Its handler has NOT
    initialized yet, and `route/3` has already replied `:ok`.
  * `:ready` -- the handler initialized and the chat can be served. Emitted by
    `Raxol.Gateway.Session`; metadata adds `:handler`.
  * `:init_timeout` -- the handler never returned from `init/2` and the session
    was killed. Emitted by `Raxol.Gateway.Session`; metadata adds `:handler`
    and `:timeout`.
  * `:down` -- a session died for any reason other than `:normal` or
    `:shutdown`. Metadata adds `:reason`, which is `:noproc` when the router
    found the session dead before its `:DOWN` arrived and so never saw why.
  * `:stopped` -- `stop_session/2` was called. Metadata adds `reason: :explicit`.
  * `:rejected` -- no session was started. Metadata adds `:reason`, one of
    `:max_sessions`, `:rate_limited` or `:unauthorized`.

A `:started` with no `:ready` behind it is a handler that failed or hung at
startup. Since handler init is deferred (see `Raxol.Gateway.Session`), that
pair is the only signal of it -- `route/3` cannot report it.

# `child_spec`

Returns a specification to start this module under a supervisor.

See `Supervisor`.

# `get_session`

```elixir
@spec get_session(GenServer.server(), String.t()) :: pid() | nil
```

The session pid for a key, or nil.

# `handle_manager_cast`

# `handoff`

```elixir
@spec handoff(GenServer.server(), String.t(), Raxol.Gateway.Route.t()) ::
  {:ok, pid()} | {:error, term()}
```

Hand a conversation off to another platform: start a session on `to_route`
reusing the source session's `conversation_id` (and `:log`), so a configured
log resumes the same history. Returns the destination session pid.

The destination is started like any other session, so it passes the same
authorization, `:max_sessions` and per-key cooldown gates; a refusal is
`{:error, :unauthorized | :max_sessions | :rate_limited}` and leaves the source
session as it was. A live destination session keeps its own conversation; a
tracked one that is already dead is replaced, as in `route/3`.

# `route`

```elixir
@spec route(GenServer.server(), Raxol.Gateway.Route.t(), term()) ::
  :ok | {:error, term()}
```

Route an inbound event to the session for `route`, starting one if needed.

`:ok` means the event was ACCEPTED for delivery, not that it was served. A
session's handler initializes asynchronously (see `Raxol.Gateway.Session`), so
a handler that fails or hangs at startup does so after this has replied;
`[:raxol_gateway, :session, :ready]`, `:down` and `:init_timeout` are what
report that. An `{:error, _}` here is only ever a refusal to route --
`:unauthorized`, `:max_sessions` or `:rate_limited`.

A tracked session that is already dead when the event arrives (its `:DOWN`
still queued behind this call) is dropped and replaced by a fresh session,
under the same authorization, `:max_sessions` and per-key cooldown as any other
start. Within the cooldown that is `{:error, :rate_limited}`, never an `:ok` for
an event cast into a dead process. The event is cast, not called, so a session
that dies after the liveness check and before reading its mailbox still loses
it after `:ok`; `:down` telemetry reports that death.

# `session_count`

```elixir
@spec session_count(GenServer.server()) :: non_neg_integer()
```

The number of active sessions.

# `start_link`

```elixir
@spec start_link(keyword()) :: GenServer.on_start()
```

# `start_session`

```elixir
@spec start_session(GenServer.server(), Raxol.Gateway.Route.t()) ::
  {:ok, pid()} | {:error, term()}
```

Start (or fetch) the session for `route`.

# `stats`

```elixir
@spec stats(GenServer.server()) :: %{
  sessions: non_neg_integer(),
  cooldown_entries: non_neg_integer()
}
```

Router stats: active sessions and cooldown-map size.

# `stop_session`

```elixir
@spec stop_session(GenServer.server(), String.t()) :: :ok
```

Stop the session for a key.

---

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