How to detect if a user left a Phoenix channel due to a network disconnect?

后端 未结 1 742
后悔当初
后悔当初 2020-12-13 00:55

I have an Elixir/Phoenix server app and the clients connect through the build in channels system via websockets. Now I want to detect when an user leaves a channel.

相关标签:
1条回答
  • 2020-12-13 01:37

    The proper way to do this is to not trap exits in your channel, and instead have another process monitor you. When you go down, it can invoke a callback. Below is a snippet to get you started:

    # lib/my_app.ex
    
    children = [
      ...
      worker(ChannelWatcher, [:rooms])
    ]
    
    # web/channels/room_channel.ex
    
    def join("rooms:", <> id, params, socket) do
      uid = socket.assigns.user_id]
      :ok = ChannelWatcher.monitor(:rooms, self(), {__MODULE__, :leave, [id, uid]})
    
      {:ok, socket}
    end
    
    def leave(room_id, user_id) do
      # handle user leaving
    end
    
    # lib/my_app/channel_watcher.ex
    
    defmodule ChannelWatcher do
      use GenServer
    
      ## Client API
    
      def monitor(server_name, pid, mfa) do
        GenServer.call(server_name, {:monitor, pid, mfa})
      end
    
      def demonitor(server_name, pid) do
        GenServer.call(server_name, {:demonitor, pid})
      end
    
      ## Server API
    
      def start_link(name) do
        GenServer.start_link(__MODULE__, [], name: name)
      end
    
      def init(_) do
        Process.flag(:trap_exit, true)
        {:ok, %{channels: HashDict.new()}}
      end
    
      def handle_call({:monitor, pid, mfa}, _from, state) do
        Process.link(pid)
        {:reply, :ok, put_channel(state, pid, mfa)}
      end
    
      def handle_call({:demonitor, pid}, _from, state) do
        case HashDict.fetch(state.channels, pid) do
          :error       -> {:reply, :ok, state}
          {:ok,  _mfa} ->
            Process.unlink(pid)
            {:reply, :ok, drop_channel(state, pid)}
        end
      end
    
      def handle_info({:EXIT, pid, _reason}, state) do
        case HashDict.fetch(state.channels, pid) do
          :error -> {:noreply, state}
          {:ok, {mod, func, args}} ->
            Task.start_link(fn -> apply(mod, func, args) end)
            {:noreply, drop_channel(state, pid)}
        end
      end
    
      defp drop_channel(state, pid) do
        %{state | channels: HashDict.delete(state.channels, pid)}
      end
    
      defp put_channel(state, pid, mfa) do
        %{state | channels: HashDict.put(state.channels, pid, mfa)}
      end
    end
    
    0 讨论(0)
提交回复
热议问题