Jev.Server is a GenServer that talks to Jev by replying. It owns the real
GenServer callbacks and delegates to your module, the way GenStage and
Agent are built, so every GenServer facility keeps working.
Callbacks
Your module implements init/1, any of the usual handle_call/3,
handle_cast/2, handle_info/2, handle_continue/2, and one new callback:
handle_answer(reply | {:error, reason}, tag, state)Message, tag, state mirrors handle_call's message, from, state.
Sending to Jev
From any callback, return
{:reply, {tag, state, questions}, your_state}and the request is posted under Jev.TaskSupervisor without blocking the
server. tag is any term; it comes back with the answer. state is what the
questions are about. questions is a keyword list or map in any of the
shorthands.
Per-request options such as model: go in a fourth element. The questions
then need their own brackets:
{:reply, {tag, state, [kind: {"Which?", %{a: nil, b: nil}}], [model: "jev-preview"]}, s}The other return values are the GenServer ones: {:noreply, s},
{:noreply, s, timeout | :hibernate | {:continue, term}}, and
{:stop, reason, s}.
Answering callers
Because the answer is not known when handle_call/3 returns, callers are
answered later with GenServer.reply/2. Passing from as the tag is the
common case:
def handle_call({:labels, issue}, from, s), do: {:reply, {from, issue, @questions}, s}
def handle_answer(%{kind: k}, from, s) do
GenServer.reply(from, [k])
{:noreply, s}
endSet the call timeout with the API latency in mind. A budget of 30 seconds covers the default receive timeout plus retries.
Requests in flight
A server can have any number of requests outstanding. Each reply carries its
tag, so a clause knows which caller, page, or step it belongs to. The
pending map in the wrapper state holds the outstanding task references, and
:sys.get_status/1 shows your inner state rather than the wrapper.
Errors
Every failure arrives at handle_answer/3 as {:error, reason}:
- a non-2xx response after retries, as
{:error, %Jev.Error{status: status, body: body, request_id: id}} - a transport failure, as
{:error, %Req.TransportError{}}or another exception - a crashed request, for example state that cannot be encoded, as
{:error, {exception, stacktrace}}
The request runs under Task.Supervisor.async_nolink/4, so the crash never
reaches the server. A retry is just a clause that replies again:
def handle_answer({:error, %Jev.Error{status: 529}}, {attempt, issue} = tag, s) when attempt < 3 do
{:reply, {{attempt + 1, issue}, issue, @questions}, s}
endNote that Jev.HTTP already retries 429 and 529 with backoff before the error
reaches you, so this is for the rare case that outlives those retries.
Routing on the reply
The reply is a plain map, so routing is pattern matching in clause order:
# A security flag wins regardless of the rest.
def handle_answer(%{security: p}, from, s) when p > 0.5, do: done(from, [:security], s)
# Confident and serious.
def handle_answer(%{kind: :bug, severity: sev, confidence: %{kind: c}}, from, s)
when c > 0.85 and sev >= 2,
do: done(from, [:bug, :"priority:high"], s)
# Confident enough to act.
def handle_answer(%{kind: k, confidence: %{kind: c}}, from, s) when c > 0.6, do: done(from, [k], s)
# Too close to call: hand the top two to a human.
def handle_answer(%{probabilities: %{kind: ps}}, from, s) do
[a, b | _] = ps |> Enum.sort_by(&elem(&1, 1), :desc) |> Enum.map(&elem(&1, 0))
done(from, [:"needs-triage", {:one_of, a, b}], s)
endThe TypeSafe confidence guide suggests acting above 0.9, confirming between 0.5 and 0.9, and escalating below 0.5, with stricter thresholds for actions that are harder to reverse. Those are three guards.
Starting a server
use Jev.Server defines a child_spec/1, so a server goes in a supervision
tree like any other child:
children = [
{Triage, []},
{Jev.Server, [Locate, page]} # or explicitly
]Jev.Server.start_link/3 takes the module, the init argument, and
GenServer.start_link/3 options such as name:.
The :jev application starts Jev.TaskSupervisor, which the servers post
through. Nothing else needs to be started.