Looking for help in Idiomatic use of Phoenix.PubSub

Hi everyone,

I’m still at the beginning of my Elixir journey, I’ve worked through several tutorials and I’m starting to get to grips with the language itself, but I’m struggling with how to enable reusability in a project. For my current project I’m trying to make up-to-date information from a WebSocket feed available to a subscriber. The idea I have is that I should be able to include this project as a dependency in other projects, so I’m trying to keep it very loosely coupled, but with a usable interface that aids the future developer (probably me!) in using it correctly.

I’ve created some helper functions and my main question here is whether these helper functions are the “right” way of doing things?

Perhaps an example will help. There are several WebSocket feeds and I am successfully consuming them using WebSockEx.

Following advice found elsewhere this forum, I’m creating a struct of the data as soon as it is received:

defmodule Exchange.Ticker do
  defstruct [:event_time, :symbol,:close,:open,:high,:low]

  def new(%{"e" => event_time, s" => symbol,"c" => close,"o" => open,"h" => high,"l" => low }) do
    %__MODULE__{
      event_time: Timex.from_unix(event_time, :milliseconds),
      symbol: symbol,
      close: Decimal.new(close),
      open: Decimal.new(open),
      high: Decimal.new(high),
      low: Decimal.new(low)
    }
  end
end

The data arrives as a list in JSON so which is handled like this:

  def handle_new_ticker_message(data) do
    data
    |> Jason.decode!()
    |> Enum.map(&Exchange.Ticker.new/1)
    |> Enum.map(&Exchange.PubSub.broadcast/1) ## This is one of the helpers.
  end

And the helpers I’ve created are:

defmodule Exchange.PubSub do
  ## Correctly determines the topic from the data type and broadcasts it
  def broadcast(%Exchange.Ticker{} = ticker) do
    Phoenix.PubSub.broadcast!(
      __MODULE__,
      topic(ticker),
      ticker
    )
  end

 ## Subscribe based on a string specifying the desired symbol
  def subscribe(Exchange.Ticker, symbol) do
    Phoenix.PubSub.subscribe(__MODULE__, topic(Exchange.Ticker, symbol))
  end

  # determine the correct topic from the struct
  def topic(%Exchange.Ticker{symbol: symbol}) do
    topic(Exchange.Ticker, symbol)
  end
 
  # determine the correct topic from a string
  def topic(Exchange.Ticker, symbol) when is_binary(symbol) do
    "Exchange.Ticker.#{symbol}"
  end
end

Then if we needed to subscribe to the symbol “EURUSD” we could subscribe possibly in the start_link of a Genserver):

Exchange.PubSub.subscribe(Exchange.Ticker, "EURUSD")

and in subscribed modules we could have:

def handle_info(%Exchange.Ticker{symbol: symbol, close: price}, state) do
  ### do stuff with the message
end

For each additional WebSocket data feed there would be additional structs with their requisite helper functions. I don’t think this has to be limited to incoming data either. For example a PubSub channel notifying about subscription events could have a %Subscription.Event{} struct or similar.

The main reason for doing this is that I’m used to using constants to help my IDE to give me auto complete and avoid typos on magic strings. The PubSub topic feels a bit like a magic string and the example in the Phoenix.PubSub documentation creates a message with what feels a bit like a magic atom :user_update. I don’t think that compile time checks would pick up if you accidentally specified :users_update in a handle_info call or had a typo in the topic string?

Am I creating work for myself here? Am I creating a coupling where there doesn’t need to be one? Do I need to chill out and embrace magic strings/atoms a bit more?

Thanks in advance for any help.

Hey I am new to the elixir, phoenix realm too and I feel your pain of having flexible but defined structs to work with.

I have tested with IntelliJ, Atom, VS Code and all three of them failed with atoms or even module definitions to say this doesn’t exist or you have made a typo.

Maybe there is a way but I still haven’t seen it and none in the example books either.

On the upside, the watcher and compiler will immediately warn you saying no such atom exists.

Elixir being an FP and everything is a function and can be tested, your only defense is to write the mini unit tests and actually do TDD

2 Likes

Thanks, it’s great to know I’m not alone. I think you’re right on TDD, I should fully embrace it as a methodology!