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) -- aDynamicSupervisorname for the sessions:adapter--{module, conn}; outbound is delivered via itssend_message/3. Or pass:deliver, a(Route.t(), rendered -> any()).:max_sessions(default 1000):idle_timeoutms (default 10 minutes):handler_init_timeout-- passed to each session; ms its handler'sinit/2may 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, androute/3has already replied:ok.:ready-- the handler initialized and the chat can be served. Emitted byRaxol.Gateway.Session; metadata adds:handler.:init_timeout-- the handler never returned frominit/2and the session was killed. Emitted byRaxol.Gateway.Session; metadata adds:handlerand:timeout.:down-- a session died for any reason other than:normalor:shutdown. Metadata adds:reason, which is:noprocwhen the router found the session dead before its:DOWNarrived and so never saw why.:stopped--stop_session/2was called. Metadata addsreason: :explicit.:rejected-- no session was started. Metadata adds:reason, one of:max_sessions,:rate_limitedor: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.
Summary
Functions
Returns a specification to start this module under a supervisor.
The session pid for a key, or nil.
Callback implementation for Raxol.Core.Behaviours.BaseManager.handle_manager_cast/2.
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.
Route an inbound event to the session for route, starting one if needed.
The number of active sessions.
Start (or fetch) the session for route.
Router stats: active sessions and cooldown-map size.
Stop the session for a key.
Functions
Returns a specification to start this module under a supervisor.
See Supervisor.
@spec get_session(GenServer.server(), String.t()) :: pid() | nil
The session pid for a key, or nil.
Callback implementation for Raxol.Core.Behaviours.BaseManager.handle_manager_cast/2.
@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.
@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.
@spec session_count(GenServer.server()) :: non_neg_integer()
The number of active sessions.
@spec start_link(keyword()) :: GenServer.on_start()
@spec start_session(GenServer.server(), Raxol.Gateway.Route.t()) :: {:ok, pid()} | {:error, term()}
Start (or fetch) the session for route.
@spec stats(GenServer.server()) :: %{ sessions: non_neg_integer(), cooldown_entries: non_neg_integer() }
Router stats: active sessions and cooldown-map size.
@spec stop_session(GenServer.server(), String.t()) :: :ok
Stop the session for a key.