Bourse.WS.Subscription.Behaviour behaviour (bourse v0.1.0)

Copy Markdown View Source

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 Bourse.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) requires 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). Bourse.WS.Subscription.build_subscribe/3 passes these through to the caller verbatim rather than wrapping them in {:ok, _}.

Bourse.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 Bourse.WS.Subscription.OpSubscribe do
  @behaviour Bourse.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

Summary

Functions

Builds a single-envelope frame after validating channel-list shape.

Classifies a channel list by element shape.

Types

channel()

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

channel_shape_error()

@type channel_shape_error() :: :multiple_maps_not_supported | :mixed_channel_types

config()

@type config() :: map()

frame()

@type frame() :: map()

Callbacks

subscribe(channels, config)

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

unsubscribe(channels, config)

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

Functions

build_single_envelope(channels, config, action, string_builder)

@spec build_single_envelope([channel()], config(), String.t(), ([channel()],
                                                          config(),
                                                          String.t() ->
                                                            frame())) ::
  frame() | {:error, channel_shape_error()}

Builds a single-envelope frame after validating channel-list shape.

Pattern modules pass their string-channel frame builder. Map-only and mixed channel lists return the shared error atoms used by the dispatcher tests.

classify_channel_list(channels)

@spec classify_channel_list([channel()]) :: :strings | :all_maps | :mixed

Classifies a channel list by element shape.

Empty lists classify as :strings, preserving the default-frame behavior in single-envelope pattern modules.