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_supportedor:mixed_channel_typesfromEventSubscribe/MethodParams).CCXT.WS.Subscription.build_subscribe/3passes 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