How would you handle real-time channel messages initiated by a non-phoenix application

I’m using an umbrella application to give some structure to the code.

Imagine the following general structure

- apps/sales (domain specific application with ecto)
- apps/web_interface (Phoenix application)
  - WebInterface.UserChannel (Phoenix Channel)
  - WebInterface.SaleView (Phoenix View)
  - has a dependency on the sales application

Now when an event such as a new sale occurs in the sales application, I’d like to trigger a message to a user on the WebInterface.UserChannel that is rendered as JSON via the WebInterface.SaleView. But I have to problems:

  1. The web _interface application depends on the sales application so I am unable to make a direct call (since I don’t want a circular dependency). I can create a generic callback method so that I can send any messages I want (e.g. with this approach Best practice for calling a function in another application without defining a dependency) but I don’t really want the sales application to be able to send any message, just a limited subset
  2. Since the Phoenix Views are contained in the web_interface the sales application cannot render the sale to JSON by itself.

Since everything (or nearly) everything in these applications are JSON-based rather than HTML-based I’m also starting to question if Phoenix Views are even necessary. Maybe I should just define modules in the sales application that return a purely map/array based rendering of the data.

Does anyone have any thoughts or suggestions?

1 Like

I had the same concern, but it is difficult to avoid circular dependencies if You want to communicate between.

web -> sales
sales -> web

In order for sales to communicate with web with the least inference, You could configure a notifier in sales.

# game engine config
config :game_engine, notify: GameWeb.Notifier

# game_engine module
def notify(message) do
  module = Application.get_env(:game_engine, :notify)
  module.notify(message)
end

# game_web notifier
defmodule GameWeb.Notifier do
  @moduledoc false
  require Logger

  # Notification Hub for application
  # Used by GameEngine to notify worker's exit

  def notify(%{payload: payload, type: :game_created}) do
    GameWeb.Endpoint.broadcast!("lobby", "game_added", %{game: payload})
  end

  def notify(%{payload: payload, type: :game_stopped}) do
    GameWeb.Endpoint.broadcast!("lobby", "game_removed", %{uuid: payload})
    
    # Notify remaining guest the game has ended!
    GameWeb.Endpoint.broadcast!("game:#{payload}", "game_force_quit", %{uuid: payload})
  end

  def notify(%{type: :request_created} = message) do
    # Do nothing, notification is done in lobby channel
    Logger.debug(fn -> "No op #{inspect(message)}" end)
  end

  def notify(%{payload: payload, type: :request_stopped}) do
    GameWeb.Endpoint.broadcast!("lobby", "request_cancelled", %{uuid: payload})
  end

  def notify(message) do
    Logger.debug(fn -> "Unknown notification #{inspect(message)}" end)
  end
end

So I am just passing an atom in the engine, that points back to web.

Although ultra simplistic, it works well. But for more complex solution, I would have used GenEvent if it was not deprecated.

I have been thinking it would be possible to have a common dependency for all, eg LocalBusSystem, where engines would produces events, and web would consume those.

I am also curious of other people solution, as it seems to be a concern when using umbrellas.

1 Like