defmodule Libp2p.PeerSession do @moduledoc """ Manages per-peer state and coordination for a connected peer. Responsibilities: - Track peer metadata (protocol version, agent version, etc.). - Manage scoring, bans, and rate limits. - Coordinate outbound streams and ensure only one connection exists per peer. - Track outstanding requests. """ use GenServer require Logger @type peer_id :: binary() @type state :: %{ peer_id: peer_id(), metadata: map(), score: integer(), connections: MapSet.t(pid()), outstanding_requests: map() } @spec start_link(keyword()) :: GenServer.on_start() def start_link(opts) do peer_id = Keyword.fetch!(opts, :peer_id) GenServer.start_link(__MODULE__, opts, name: via_tuple(peer_id)) end defp via_tuple(peer_id) do {:via, Registry, {Libp2p.PeerRegistry, peer_id}} end @spec get_state(peer_id()) :: {:ok, state()} | {:error, :not_found} def get_state(peer_id) do case Registry.lookup(Libp2p.PeerRegistry, peer_id) do [{pid, _}] -> {:ok, GenServer.call(pid, :get_state)} [] -> {:error, :not_found} end end @spec register_connection(peer_id(), pid()) :: :ok def register_connection(peer_id, conn_pid) do GenServer.cast(via_tuple(peer_id), {:register_connection, conn_pid}) end @impl true def init(opts) do peer_id = Keyword.fetch!(opts, :peer_id) # We might want to monitor connections to clean up if they die {:ok, %{ peer_id: peer_id, metadata: %{}, score: 0, connections: MapSet.new(), outstanding_requests: %{} }} end @impl true def handle_call(:get_state, _from, st) do {:reply, st, st} end @impl true def handle_cast({:register_connection, conn_pid}, st) do Process.monitor(conn_pid) {:noreply, %{st | connections: MapSet.put(st.connections, conn_pid)}} end @impl true def handle_info({:DOWN, _ref, :process, pid, _reason}, st) do if MapSet.member?(st.connections, pid) do connections = MapSet.delete(st.connections, pid) if MapSet.size(connections) == 0 do # Keep the session alive even if all connections are gone; higher layers may want # to retain per-peer metadata/score state. Connection liveness is accounted for by # checking the `connections` set when needed. {:noreply, %{st | connections: connections}} else {:noreply, %{st | connections: connections}} end else {:noreply, st} end end end