defmodule CCXT.WS.Subscription.Behaviour do @moduledoc """ Behaviour for WebSocket subscription pattern implementations. Each pattern module converts a list of pre-formatted channel strings (or per-pattern objects) into the exchange-native subscribe frame. Frame encoding (Jason) happens at the WS boundary in `CCXT.WS.subscribe/3`, not inside pattern modules — modules return plain maps. ## Return shape `subscribe/2` and `unsubscribe/2` return `map() | [map()] | {:error, term()}`. - Most exchanges accept an array of channels in a single frame → return a `map()`. - HTX (`:sub_subscribe`) and BingX (`:reqtype_sub`) require one frame per channel → return a `[map()]` with one frame per channel. - Upbit-style custom (`config[:custom_type] == "array_format"`) also returns a `[map()]`. - Implementations that enforce an input-shape contract may return `{:error, term()}` (e.g. `:multiple_maps_not_supported` or `:mixed_channel_types` from `EventSubscribe` / `MethodParams`). `CCXT.WS.Subscription.build_subscribe/3` passes these through to the caller verbatim rather than wrapping them in `{:ok, _}`. `CCXT.WS.subscribe/3` handles both frame shapes by iterating the list when present and sending one `ZenWebsocket.Client.send_message/2` per frame. ## Channel formatting is the caller's responsibility Pattern modules do **not** format channel strings from unified symbols. Callers pass pre-formatted channels like `"tickers.BTCUSDT"` or `"market.btcusdt.ticker"`. A future task may introduce spec-driven channel templates; this behaviour is deliberately narrow. ## Implementing a Pattern defmodule CCXT.WS.Subscription.OpSubscribe do @behaviour CCXT.WS.Subscription.Behaviour @impl true def subscribe(channels, config) do %{ (config[:op_field] || "op") => "subscribe", (config[:args_field] || "args") => channels } end @impl true def unsubscribe(channels, config) do %{ (config[:op_field] || "op") => "unsubscribe", (config[:args_field] || "args") => channels } end end """ @type channel :: String.t() | map() @type config :: map() @type frame :: map() @callback subscribe(channels :: [channel()], config :: config()) :: frame() | [frame()] | {:error, term()} @callback unsubscribe(channels :: [channel()], config :: config()) :: frame() | [frame()] | {:error, term()} @optional_callbacks [unsubscribe: 2] end