# `CCXT.WS.Subscription.Behaviour`
[🔗](https://github.com/ZenHive/ccxt_client/blob/main/lib/ccxt/ws/subscription/behaviour.ex#L1)

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

# `channel`

```elixir
@type channel() :: String.t() | map()
```

# `config`

```elixir
@type config() :: map()
```

# `frame`

```elixir
@type frame() :: map()
```

# `subscribe`

```elixir
@callback subscribe(channels :: [channel()], config :: config()) ::
  frame() | [frame()] | {:error, term()}
```

# `unsubscribe`
*optional* 

```elixir
@callback unsubscribe(channels :: [channel()], config :: config()) ::
  frame() | [frame()] | {:error, term()}
```

---

*Consult [api-reference.md](api-reference.md) for complete listing*
