defmodule CCXT.WS.Subscription do @moduledoc """ WebSocket subscription pattern dispatcher. Parallel of `CCXT.WS.Auth`: a function-head dispatcher that routes a pattern atom to the matching per-pattern module. Each pattern module implements the `CCXT.WS.Subscription.Behaviour` callbacks. ## Supported Patterns | Pattern atom | Exchanges | Frame shape | |-------------------------------|-------------------------------------------|--------------------------------------------------| | `:op_subscribe` | bybit, bitmex | `%{"op"=>"subscribe","args"=>[strings]}` | | `:op_subscribe_objects` | okx | `%{"op"=>"subscribe","args"=>[objects]}` | | `:method_subscribe` | binance, xt, aster | `%{"method"=>"SUBSCRIBE","params"=>[strings]}` | | `:method_params_subscribe` | kraken, cryptocom | `%{"method"=>"subscribe","params"=>%{"channel"=>[strings]}}` | | `:method_subscription` | hyperliquid | `%{"method"=>"subscribe","subscription"=>%{"type"=>…}}` | | `:method_topics` | exmo | `%{"method"=>"subscribe","topics"=>[strings]}` | | `:method_as_topic` | coinex, phemex | `%{"method"=>"ticker.subscribe","params"=>[],"id"=>…}` | | `:jsonrpc_subscribe` | deribit | JSON-RPC 2.0 envelope with correlation id | | `:event_subscribe` | gate, bitfinex, woo, bitrue | `%{"event"=>"subscribe","channel"=>…}` variants | | `:type_subscribe` | kucoin, coinbaseexchange | `%{"type"=>"subscribe","topic"=>…}` variants | | `:sub_subscribe` | htx, huobi | `%{"sub"=>…}` — **one frame per channel**, no `id` (see `SubBased` moduledoc) | | `:reqtype_sub` | bingx | `%{"reqType"=>"sub","dataType"=>…}` — **one frame per channel** | | `:action_subscribe` | alpaca, lbank | `%{"action"=>"subscribe","params"=>%{…}}` | | `:custom` | bithumb, upbit, deepcoin, … (escape hatch)| dispatches on `config[:custom_type]` | ## Return shape `build_subscribe/3` returns `{:ok, frame_or_frames}` where `frame_or_frames` is `map() | [map()]`. Single-frame exchanges return a map; multi-frame exchanges (HTX, BingX, Upbit-custom) return a list. Pattern modules may also surface input-shape rejections as `{:error, term()}` (e.g. `:multiple_maps_not_supported` / `:mixed_channel_types` from `:event_subscribe` and `:method_params_subscribe` when callers mix shapes). The dispatcher passes those tuples through verbatim — it never wraps them in `{:ok, _}`. Unknown patterns return `{:error, {:unknown_pattern, atom}}` — matches `CCXT.WS.Auth`'s dispatcher-head behavior. """ alias CCXT.WS.Subscription.ActionSubscribe alias CCXT.WS.Subscription.Custom alias CCXT.WS.Subscription.EventSubscribe alias CCXT.WS.Subscription.JsonRpc alias CCXT.WS.Subscription.MethodAsTopic alias CCXT.WS.Subscription.MethodParams alias CCXT.WS.Subscription.MethodSubscribe alias CCXT.WS.Subscription.MethodSubscription alias CCXT.WS.Subscription.MethodTopics alias CCXT.WS.Subscription.OpSubscribe alias CCXT.WS.Subscription.OpSubscribeObjects alias CCXT.WS.Subscription.ReqtypeSub alias CCXT.WS.Subscription.SubBased alias CCXT.WS.Subscription.TypeSubscribe @type pattern :: :op_subscribe | :op_subscribe_objects | :method_subscribe | :method_params_subscribe | :method_subscription | :method_topics | :method_as_topic | :jsonrpc_subscribe | :event_subscribe | :type_subscribe | :sub_subscribe | :reqtype_sub | :action_subscribe | :custom @type channel :: String.t() | map() @type config :: map() @type frame :: map() @type build_result :: {:ok, frame() | [frame()]} | {:error, term()} @patterns [ :op_subscribe, :op_subscribe_objects, :method_subscribe, :method_params_subscribe, :method_subscription, :method_topics, :method_as_topic, :jsonrpc_subscribe, :event_subscribe, :type_subscribe, :sub_subscribe, :reqtype_sub, :action_subscribe, :custom ] @doc "Lists every supported subscription pattern atom." @spec patterns() :: [pattern()] def patterns, do: @patterns @doc "Returns the implementing module for a pattern, or `nil` if unknown." @spec module_for_pattern(pattern()) :: module() | nil def module_for_pattern(:op_subscribe), do: OpSubscribe def module_for_pattern(:op_subscribe_objects), do: OpSubscribeObjects def module_for_pattern(:method_subscribe), do: MethodSubscribe def module_for_pattern(:method_params_subscribe), do: MethodParams def module_for_pattern(:method_subscription), do: MethodSubscription def module_for_pattern(:method_topics), do: MethodTopics def module_for_pattern(:method_as_topic), do: MethodAsTopic def module_for_pattern(:jsonrpc_subscribe), do: JsonRpc def module_for_pattern(:event_subscribe), do: EventSubscribe def module_for_pattern(:type_subscribe), do: TypeSubscribe def module_for_pattern(:sub_subscribe), do: SubBased def module_for_pattern(:reqtype_sub), do: ReqtypeSub def module_for_pattern(:action_subscribe), do: ActionSubscribe def module_for_pattern(:custom), do: Custom def module_for_pattern(_), do: nil @doc """ Builds the subscribe frame(s) for the pattern and channel list. Returns `{:ok, map()}` for the common single-frame case, or `{:ok, [map()]}` for multi-frame exchanges (HTX, BingX, Upbit-custom). Callers should handle both shapes. """ @spec build_subscribe(pattern(), [channel()], config()) :: build_result() def build_subscribe(pattern, channels, config) when is_list(channels) do case module_for_pattern(pattern) do nil -> {:error, {:unknown_pattern, pattern}} module -> wrap_pattern_result(module.subscribe(channels, config)) end end @doc "Builds the unsubscribe frame(s) — same return shape as `build_subscribe/3`." @spec build_unsubscribe(pattern(), [channel()], config()) :: build_result() def build_unsubscribe(pattern, channels, config) when is_list(channels) do case module_for_pattern(pattern) do nil -> {:error, {:unknown_pattern, pattern}} module -> wrap_pattern_result(module.unsubscribe(channels, config)) end end # Pattern modules may return `{:error, term()}` for input-shape rejections # (`:multiple_maps_not_supported`, `:mixed_channel_types`). Pass those # through unchanged; wrap everything else in `{:ok, _}`. defp wrap_pattern_result({:error, _} = err), do: err defp wrap_pattern_result(frame_or_frames), do: {:ok, frame_or_frames} end