Implement per-connection subscription lifecycle and EOSE semantics

This commit is contained in:
2026-03-13 20:03:14 +01:00
parent 0c04859b97
commit 73811c0772
3 changed files with 133 additions and 24 deletions

View File

@@ -8,16 +8,27 @@ defmodule Parrhesia.Web.Connection do
alias Parrhesia.Protocol
alias Parrhesia.Protocol.Filter
defstruct subscriptions: MapSet.new(), authenticated_pubkeys: MapSet.new()
@default_max_subscriptions_per_connection 32
defstruct subscriptions: %{},
authenticated_pubkeys: MapSet.new(),
max_subscriptions_per_connection: @default_max_subscriptions_per_connection
@type subscription :: %{
filters: [map()],
eose_sent?: boolean()
}
@type t :: %__MODULE__{
subscriptions: MapSet.t(String.t()),
authenticated_pubkeys: MapSet.t(String.t())
subscriptions: %{String.t() => subscription()},
authenticated_pubkeys: MapSet.t(String.t()),
max_subscriptions_per_connection: pos_integer()
}
@impl true
def init(_opts) do
{:ok, %__MODULE__{}}
def init(opts) do
state = %__MODULE__{max_subscriptions_per_connection: max_subscriptions_per_connection(opts)}
{:ok, state}
end
@impl true
@@ -38,19 +49,7 @@ defmodule Parrhesia.Web.Connection do
{:push, {:text, response}, state}
{:ok, {:req, subscription_id, filters}} ->
case Filter.validate_filters(filters) do
:ok ->
next_state = put_subscription(state, subscription_id)
response = Protocol.encode_relay({:eose, subscription_id})
{:push, {:text, response}, next_state}
{:error, reason} ->
response =
Protocol.encode_relay({:closed, subscription_id, Filter.error_message(reason)})
{:push, {:text, response}, state}
end
handle_req(state, subscription_id, filters)
{:ok, {:close, subscription_id}} ->
next_state = drop_subscription(state, subscription_id)
@@ -79,13 +78,79 @@ defmodule Parrhesia.Web.Connection do
{:ok, state}
end
defp put_subscription(%__MODULE__{} = state, subscription_id) do
subscriptions = MapSet.put(state.subscriptions, subscription_id)
defp handle_req(%__MODULE__{} = state, subscription_id, filters) do
with :ok <- Filter.validate_filters(filters),
{:ok, next_state} <- upsert_subscription(state, subscription_id, filters) do
response = Protocol.encode_relay({:eose, subscription_id})
{:push, {:text, response}, next_state}
else
{:error, :subscription_limit_reached} ->
response =
Protocol.encode_relay({
:closed,
subscription_id,
"rate-limited: maximum subscriptions per connection exceeded"
})
{:push, {:text, response}, state}
{:error, reason} ->
response = Protocol.encode_relay({:closed, subscription_id, Filter.error_message(reason)})
{:push, {:text, response}, state}
end
end
defp upsert_subscription(%__MODULE__{} = state, subscription_id, filters) do
subscription = %{filters: filters, eose_sent?: true}
cond do
Map.has_key?(state.subscriptions, subscription_id) ->
{:ok, put_subscription(state, subscription_id, subscription)}
map_size(state.subscriptions) < state.max_subscriptions_per_connection ->
{:ok, put_subscription(state, subscription_id, subscription)}
true ->
{:error, :subscription_limit_reached}
end
end
defp put_subscription(%__MODULE__{} = state, subscription_id, subscription) do
subscriptions = Map.put(state.subscriptions, subscription_id, subscription)
%__MODULE__{state | subscriptions: subscriptions}
end
defp drop_subscription(%__MODULE__{} = state, subscription_id) do
subscriptions = MapSet.delete(state.subscriptions, subscription_id)
subscriptions = Map.delete(state.subscriptions, subscription_id)
%__MODULE__{state | subscriptions: subscriptions}
end
defp max_subscriptions_per_connection(opts) when is_list(opts) do
opts
|> Keyword.get(:max_subscriptions_per_connection)
|> normalize_max_subscriptions_per_connection()
end
defp max_subscriptions_per_connection(opts) when is_map(opts) do
opts
|> Map.get(:max_subscriptions_per_connection)
|> normalize_max_subscriptions_per_connection()
end
defp max_subscriptions_per_connection(_opts), do: configured_max_subscriptions_per_connection()
defp normalize_max_subscriptions_per_connection(value) when is_integer(value) and value > 0,
do: value
defp normalize_max_subscriptions_per_connection(_value),
do: configured_max_subscriptions_per_connection()
defp configured_max_subscriptions_per_connection do
:parrhesia
|> Application.get_env(:limits, [])
|> Keyword.get(
:max_subscriptions_per_connection,
@default_max_subscriptions_per_connection
)
end
end