LemonCore.RouterBridge (lemon_core v0.1.0)

View Source

Optional bridge to :lemon_router without compile-time coupling.

Channel adapters and other producers can forward inbound messages and submit runs without depending on :lemon_router. :lemon_router configures the bridge at runtime.

submit_run/1 accepts a canonical %LemonCore.RunRequest{}.

Failure contract

Every function here answers rather than raises, because callers are channel adapters and webhook handlers for whom a router problem is not their problem to crash over. Three failure modes, deliberately distinguished:

  • Not configured — no module registered for the role. The documented fallback: {:error, :unavailable}, or the soft value (false, :none, []) for the query functions.
  • Reached and raised — the router ran in the caller's process and threw an exception. Answered {:error, exception}, which is more useful than :unavailable because the router was available; something else failed.
  • Configured but not running — the router module exists, but the process behind it does not answer, so GenServer.call exits. An exit is not an exception and rescue does not catch it: before this was handled, that exit travelled into whatever called the bridge, which for an HTTP handler meant an opaque 500 and, for a mail webhook, a silently dropped message. Now answered {:error, :unavailable} — the same as never having been configured, because from the caller's side it is the same situation: the router did not take this.

Exits are collapsed to :unavailable rather than reported in detail on purpose. A timeout, a dead process and a mid-call crash differ in cause but not in consequence — the work was not accepted and the caller's only real decision is whether to retry. Reporting them separately invites callers to match on :unavailable alone and silently mishandle the rest, which is exactly the bug this contract exists to prevent. Callers that want the detail should log at their own layer, where they know what the failure means.

Throws are not caught: no router throws, and one that did would be a bug worth surfacing rather than flattening into "unavailable".

Summary

Types

config()

@type config() :: %{
  optional(:run_orchestrator) => module(),
  optional(:router) => module()
}

configure_mode()

@type configure_mode() :: :replace | :merge | :safe_merge

Functions

abort_run(run_id, reason \\ :user_requested)

@spec abort_run(binary(), term()) :: :ok | {:error, :unavailable} | {:error, term()}

abort_session(session_key, reason \\ :user_requested)

@spec abort_session(binary(), term()) ::
  :ok | {:error, :unavailable} | {:error, term()}

active_run(session_key)

@spec active_run(binary()) :: {:ok, binary()} | :none

configure(opts)

@spec configure(keyword()) :: :ok | {:error, term()}

configure(opts, config_opts)

@spec configure(keyword(), keyword()) :: :ok | {:error, term()}

Configure bridge modules with merge/guard modes.

Modes:

  • :replace - replace configured keys directly
  • :merge - merge with existing config, preserving unspecified keys
  • :safe_merge - like merge, but rejects conflicting non-nil overrides

configure_guarded(opts)

@spec configure_guarded(keyword()) :: :ok | {:error, term()}

Configure bridge modules with conflict protection.

handle_inbound(msg)

@spec handle_inbound(term()) :: :ok | {:error, :unavailable} | {:error, term()}

keep_run_alive(run_id, decision \\ :continue)

@spec keep_run_alive(binary(), :continue | :cancel) ::
  :ok | {:error, :unavailable} | {:error, term()}

list_active_sessions()

@spec list_active_sessions() :: [%{session_key: binary(), run_id: binary()}]

session_busy?(session_key)

@spec session_busy?(binary()) :: boolean()

submit_run(params)

@spec submit_run(LemonCore.RunRequest.t()) ::
  {:ok, binary()} | {:error, :unavailable} | {:error, term()}