Search documentation of Phoenix

# Phoenix.Presence behaviour (Phoenix v1.8.7)

View Source

Provides Presence tracking to processes and channels.

This behaviour provides presence features such as fetching presences for a given topic, as well as handling diffs of join and leave events as they occur in real-time. Using this module defines a supervisor and a module that implements the `Phoenix.Tracker` behaviour that uses `Phoenix.PubSub` to broadcast presence updates.

In case you want to use only a subset of the functionality provided by `Phoenix.Presence`, such as tracking processes but without broadcasting updates, we recommend that you look at the `Phoenix.Tracker` functionality from the `phoenix_pubsub` project.

## Example Usage

Start by defining a presence module within your application which uses `Phoenix.Presence` and provide the `:otp_app` which holds your configuration, as well as the `:pubsub_server`.

    defmodule MyAppWeb.Presence do
      use Phoenix.Presence,
        otp_app: :my_app,
        pubsub_server: MyApp.PubSub
    end

The `:pubsub_server` must point to an existing pubsub server running in your application, which is included by default as `MyApp.PubSub` for new applications.

Next, add the new supervisor to your supervision tree in `lib/my_app/application.ex`. It must be after the PubSub child and before the endpoint:

    children = [
      ...
      {Phoenix.PubSub, name: MyApp.PubSub},
      MyAppWeb.Presence,
      MyAppWeb.Endpoint
    ]

Once added, presences can be tracked in your channel after joining:

    defmodule MyAppWeb.MyChannel do
      use MyAppWeb, :channel
      alias MyAppWeb.Presence

      def join("some:topic", _params, socket) do
        send(self(), :after_join)
        {:ok, assign(socket, :user_id, ...)}
      end

      def handle_info(:after_join, socket) do
        {:ok, _} = Presence.track(socket, socket.assigns.user_id, %{
          online_at: inspect(System.system_time(:second))
        })

        push(socket, "presence_state", Presence.list(socket))
        {:noreply, socket}
      end
    end

In the example above, `Presence.track` is used to register this channel's process as a presence for the socket's user ID, with a map of metadata. Next, the current presence information for the socket's topic is pushed to the client as a `"presence_state"` event.

Finally, a diff of presence join and leave events will be sent to the client as they happen in real-time with the "presence_diff" event. The diff structure will be a map of `:joins` and `:leaves` of the form:

    %{
      joins: %{"123" => %{metas: [%{status: "away", phx_ref: ...}]}},
      leaves: %{"456" => %{metas: [%{status: "online", phx_ref: ...}]}}
    },

See `list/1` for more information on the presence data structure.

## Custom dispatcher

It's possible to customize the dispatcher module used to broadcast. By default, `Phoenix.Channel.Server` is used, which is the same dispatcher used by channels. To customize the dispatcher, pass the `:dispatcher` option when using `Phoenix.Presence`:

    use Phoenix.Presence,
      otp_app: :my_app,
      pubsub_server: MyApp.PubSub,
      dispatcher: MyApp.CustomDispatcher

See `Phoenix.PubSub` for more information on custom dispatchers.

## Fetching Presence Information

Presence metadata should be minimized and used to store small, ephemeral state, such as a user's "online" or "away" status. More detailed information, such as user details that need to be fetched from the database, can be achieved by overriding the `fetch/2` function.

The `fetch/2` callback is triggered when using `list/1` and on every update, and it serves as a mechanism to fetch presence information a single time, before broadcasting the information to all channel subscribers. This prevents N query problems and gives you a single place to group isolated data fetching to extend presence metadata.

The function must return a map of data matching the outlined Presence data structure, including the `:metas` key, but can extend the map of information to include any additional information. For example:

    def fetch(_topic, presences) do
      users = presences |> Map.keys() |> Accounts.get_users_map()

      for {key, %{metas: metas}} <- presences, into: %{} do
        {key, %{metas: metas, user: users[String.to_integer(key)]}}
      end
    end

Where `Account.get_users_map/1` could be implemented like:

    def get_users_map(ids) do
      query =
        from u in User,
          where: u.id in ^ids,
          select: {u.id, u}

      query |> Repo.all() |> Enum.into(%{})
    end

The `fetch/2` function above fetches all users from the database who have registered presences for the given topic. The presences information is then extended with a `:user` key of the user's information, while maintaining the required `:metas` field from the original presence data.

## Using Elixir as a Presence Client

Presence is great for external clients, such as JavaScript applications, but it can also be used from an Elixir client process to keep track of presence changes as they happen on the server. This can be accomplished by implementing the optional `init/1` and `handle_metas/4` callbacks on your presence module. For example, the following callback receives presence metadata changes, and broadcasts to other Elixir processes about users joining and leaving:

    defmodule MyApp.Presence do
      use Phoenix.Presence,
        otp_app: :my_app,
        pubsub_server: MyApp.PubSub

      def init(_opts) do
        {:ok, %{}} # user-land state
      end

      def handle_metas(topic, %{joins: joins, leaves: leaves}, presences, state) do
        # fetch existing presence information for the joined users and broadcast the
        # event to all subscribers
        for {user_id, presence} <- joins do
          user_data = %{user: presence.user, metas: Map.fetch!(presences, user_id)}
          msg = {MyApp.PresenceClient, {:join, user_data}}
          Phoenix.PubSub.local_broadcast(MyApp.PubSub, topic, msg)
        end

        # fetch existing presence information for the left users and broadcast the
        # event to all subscribers
        for {user_id, presence} <- leaves do
          metas =
            case Map.fetch(presences, user_id) do
              {:ok, presence_metas} -> presence_metas
              :error -> []
            end

          user_data = %{user: presence.user, metas: metas}
          msg = {MyApp.PresenceClient, {:leave, user_data}}
          Phoenix.PubSub.local_broadcast(MyApp.PubSub, topic, msg)
        end

        {:ok, state}
      end
    end

The `handle_metas/4` callback receives the topic, presence diff, current presences for the topic with their metadata, and any user-land state accumulated from init and subsequent `handle_metas/4` calls. In our example implementation, we walk the `:joins` and `:leaves` in the diff, and populate a complete presence from our known presence information. Then we broadcast to the local node subscribers about user joins and leaves.

## Testing with Presence

Every time the `fetch` callback is invoked, it is done from a separate process. Given those processes run asynchronously, it is often necessary to guarantee they have been shutdown at the end of every test. This can be done by using ExUnit's `on_exit` hook plus `fetchers_pids` function:

    on_exit(fn ->
      for pid <- MyAppWeb.Presence.fetchers_pids() do
        ref = Process.monitor(pid)
        assert_receive {:DOWN, ^ref, _, _, _}, 1000
      end
    end)

## Summary

### Types

presence()
presences()
topic()

### Callbacks

fetch(topic, presences)

Extend presence information with additional data.

get_by_key(arg1, key)

Returns the map of presence metadata for a socket/topic-key pair.

handle_metas(topic, diff, presences, state)

Receives presence metadata changes.

init(state)

Initializes the presence client state.

list(socket_or_topic)

Returns presences for a socket/topic.

track(socket, key, meta)

Track a channel's process as a presence.

track(pid, topic, key, meta)

Track an arbitrary process as a presence.

untrack(socket, key)

Stop tracking a channel's process.

untrack(pid, topic, key)

Stop tracking a process.

update(socket, key, meta)

Update a channel presence's metadata.

update(pid, topic, key, meta)

Update a process presence's metadata.

## Types

# presence()

    @type presence() :: %{key: String.t(), meta: map()}

# presences()

    @type presences() :: %{required(String.t()) => %{metas: [map()]}}

# topic()

    @type topic() :: String.t()

## Callbacks

# fetch(topic, presences)

    @callback fetch(topic(), presences()) :: presences()

Extend presence information with additional data.

When `list/1` is used to list all presences of the given `topic`, this callback is triggered once to modify the result before it is broadcasted to all channel subscribers. This avoids N query problems and provides a single place to extend presence metadata. You must return a map of data matching the original result, including the `:metas` key, but can extend the map to include any additional information.

The default implementation simply passes `presences` through unchanged.

## Example

    def fetch(_topic, presences) do
      query =
        from u in User,
          where: u.id in ^Map.keys(presences),
          select: {u.id, u}

      users = query |> Repo.all() |> Enum.into(%{})
      for {key, %{metas: metas}} <- presences, into: %{} do
        {key, %{metas: metas, user: users[key]}}
      end
    end

# get_by_key(arg1, key)

    @callback get_by_key(Phoenix.Socket.t() | topic(), key :: String.t()) :: [presence()]

Returns the map of presence metadata for a socket/topic-key pair.

## Examples

Uses the same data format as each presence in `list/1`, but only returns metadata for the presences under a topic and key pair. For example, a user with key `"user1"`, connected to the same chat room `"room:1"` from two devices, could return:

    iex> MyPresence.get_by_key("room:1", "user1")
    [%{name: "User 1", metas: [%{device: "Desktop"}, %{device: "Mobile"}]}]

Like `list/1`, the presence metadata is passed to the `fetch` callback of your presence module to fetch any additional information.

# handle_metas(topic, diff, presences, state)

(optional)

    @callback handle_metas(
      topic :: String.t(),
      diff :: map(),
      presences :: map(),
      state :: term()
    ) ::
      {:ok, term()}

Receives presence metadata changes.

# init(state)

(optional)

    @callback init(state :: term()) :: {:ok, new_state :: term()}

Initializes the presence client state.

Invoked when your presence module starts, allows dynamically providing initial state for handling presence metadata.

# list(socket_or_topic)

    @callback list(socket_or_topic :: Phoenix.Socket.t() | topic()) :: presences()

Returns presences for a socket/topic.

## Presence data structure

The presence information is returned as a map with presences grouped by key, cast as a string, and accumulated metadata, with the following form:

    %{key => %{metas: [%{phx_ref: ..., ...}, ...]}}

For example, imagine a user with id `123` online from two different devices, as well as a user with id `456` online from just one device. The following presence information might be returned:

    %{"123" => %{metas: [%{status: "away", phx_ref: ...},
                         %{status: "online", phx_ref: ...}]},
      "456" => %{metas: [%{status: "online", phx_ref: ...}]}}

The keys of the map will usually point to a resource ID. The value will contain a map with a `:metas` key containing a list of metadata for each resource. Additionally, every metadata entry will contain a `:phx_ref` key which can be used to uniquely identify metadata for a given key. In the event that the metadata was previously updated, a `:phx_ref_prev` key will be present containing the previous `:phx_ref` value.

# track(socket, key, meta)

    @callback track(socket :: Phoenix.Socket.t(), key :: String.t(), meta :: map()) ::
      {:ok, ref :: binary()} | {:error, reason :: term()}

Track a channel's process as a presence.

Tracked presences are grouped by `key`, cast as a string. For example, to group each user's channels together, use user IDs as keys. Each presence can be associated with a map of metadata to store small, ephemeral state, such as a user's online status. To store detailed information, see `fetch/2`.

## Example

    alias MyApp.Presence
    def handle_info(:after_join, socket) do
      {:ok, _} = Presence.track(socket, socket.assigns.user_id, %{
        online_at: inspect(System.system_time(:second))
      })
      {:noreply, socket}
    end

# track(pid, topic, key, meta)

    @callback track(pid(), topic(), key :: String.t(), meta :: map()) ::
      {:ok, ref :: binary()} | {:error, reason :: term()}

Track an arbitrary process as a presence.

Same with `track/3`, except track any process by `topic` and `key`.

# untrack(socket, key)

    @callback untrack(socket :: Phoenix.Socket.t(), key :: String.t()) :: :ok

Stop tracking a channel's process.

# untrack(pid, topic, key)

    @callback untrack(pid(), topic(), key :: String.t()) :: :ok

Stop tracking a process.

# update(socket, key, meta)

    @callback update(
      socket :: Phoenix.Socket.t(),
      key :: String.t(),
      meta :: map() | (map() -> map())
    ) :: {:ok, ref :: binary()} | {:error, reason :: term()}

Update a channel presence's metadata.

Replace a presence's metadata by passing a new map or a function that takes the current map and returns a new one.

# update(pid, topic, key, meta)

    @callback update(pid(), topic(), key :: String.t(), meta :: map() | (map() -> map())) ::
      {:ok, ref :: binary()} | {:error, reason :: term()}

Update a process presence's metadata.

Same as `update/3`, but with an arbitrary process.