dexcord/test/support/fake_gateway_plug.ex
Luna 12689862c8 feat(gateway): phase 2 reliability core (resume/zombie/backoff/fatal) + FakeGateway
- fix heartbeat timer loss in :hello_wait (re-arm during identify-gap wait)
- honor server op 1 in :identifying/:hello_wait, not just :connected/:resuming
- full op 9 handling: d:false in :resuming re-IDENTIFYs on the same socket after
  the mandated 1-5s; d:false elsewhere floors the reconnect; d:true keeps session
- resume-loop cap (5 consecutive failed resumes -> fresh IDENTIFY) via Session ETS
- explicit conn_gen/stale-socket guard backstopping Mint's :unknown
- bounded TCP connect + configurable handshake watchdogs so a stalled reconnect
  self-heals instead of wedging
- FakeGateway scripted Bandit/WebSock test server + TestHandler
- integration suite (happy/zombie/kill/close-codes/op7/op9/replay/cap/backoff/gap/
  stale), fatal-path end-to-end, and unit tests for resume counter + backoff math
2026-07-04 00:09:33 -03:00

54 lines
1.7 KiB
Elixir

defmodule Dexcord.FakeGateway.Plug do
@moduledoc false
# Upgrades every request to a websocket handled by `Dexcord.FakeGateway.Handler`,
# threading the controlling `Dexcord.FakeGateway` server pid into the handler.
@behaviour Plug
@impl true
def init(opts), do: opts
@impl true
def call(conn, opts) do
server = Keyword.fetch!(opts, :server)
conn
|> WebSockAdapter.upgrade(Dexcord.FakeGateway.Handler, %{server: server}, timeout: 60_000)
|> Plug.Conn.halt()
end
end
defmodule Dexcord.FakeGateway.Handler do
@moduledoc false
# Thin WebSock relay. Runs in the Bandit/ThousandIsland connection process, so it
# *is* the connection: killing it drops the raw TCP socket. All logic lives in the
# `Dexcord.FakeGateway` server; this module only forwards frames and pushes what
# the server tells it to.
@behaviour WebSock
@impl true
def init(%{server: server}) do
state = %{server: server}
case Dexcord.FakeGateway.register(server, self()) do
{_conn_id, {:push, msg}} -> {:push, msg, state}
{_conn_id, {:close, code}} -> {:stop, :normal, code, state}
{_conn_id, :none} -> {:ok, state}
end
end
@impl true
def handle_in({data, opcode: :text}, %{server: server} = state) do
send(server, {:client_frame, self(), data})
{:ok, state}
end
def handle_in({_data, opcode: :binary}, state), do: {:ok, state}
@impl true
def handle_info({:push, {:text, _} = msg}, state), do: {:push, msg, state}
def handle_info({:close, {code, reason}}, state), do: {:stop, :normal, {code, reason}, state}
def handle_info({:close, code}, state), do: {:stop, :normal, code, state}
def handle_info(_msg, state), do: {:ok, state}
end