lib/portal/portal_monitor.ex

defmodule Sorcery.PortalMonitor do
  use GenServer

  def monitor(pid, mod) do
    GenServer.call(:portal_monitor, {:monitor, pid, mod})
  end

  def start_link(_default) do
    GenServer.start_link(__MODULE__, %{}, name: :portal_monitor)
  end

  @impl true
  def init(_) do
    {:ok, %{}}
  end

  @impl true
  def handle_call({:monitor, pid, mod}, _, state) do
    Process.monitor(pid)
    {:reply, :ok, Map.put(state, pid, mod)}
  end

  @impl true
  def handle_info({:DOWN, _ref, :process, pid, _reason}, state) do
    # Sometimes due to race conditions, mod is nil so this doesn't work
    # When that happens, we need to not lose the pid from state, but save it for later.
    # Next time a process unmounts, we'll handle both of them. 
    # The cost should be negligible considering how rare it seems to be.
    mod = state[pid]
    if mod do
      orphans = Map.get(state, :orphaned, [])
      pids = [pid | orphans]
      mod.sorcery_unmount(pids)
      new_state = Map.delete(state, pid)
      {:noreply, new_state}
    else
      state =
        state
        |> Map.update(:orphaned, [pid], fn orphans -> [pid | orphans] end)
        |> Map.delete(pid)
      {:noreply, state}
    end
    
  end
end