Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding Phoenix.Presence functionality #87

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
30 changes: 30 additions & 0 deletions lib/absinthe/phoenix/channel.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
defmodule Absinthe.Phoenix.Channel do
use Phoenix.Channel
require Logger
alias Absinthe.Phoenix.Presence

@moduledoc false

Expand Down Expand Up @@ -34,6 +35,8 @@ defmodule Absinthe.Phoenix.Channel do
absinthe_config =
Map.put(absinthe_config, :pipeline, pipeline || {__MODULE__, :default_pipeline})

send(self(), :after_join)

socket = socket |> assign(:absinthe, absinthe_config)
{:ok, socket}
end
Expand All @@ -56,6 +59,17 @@ defmodule Absinthe.Phoenix.Channel do

{reply, socket} = run_doc(socket, query, config, opts)

## Message ourselves to tell the server the client is ready to receive data now
## Note, the below message will not be executed until this current message is finished
## since they are part of the same GenServer.
handler =
socket.assigns
|> Map.get(:absinthe, %{})
|> Map.get(:opts, [])
|> Keyword.get(:context, %{handler: nil})

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need %{handler: nil} here?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because you cannot do Map.get(nil, :handler) which will happen if the context is missing from opts

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This whole chain feels like it would be improved with use of Access since it is nil resilient:

socket.assigns[:absinthe][:opts][:context][:handler]

|> Map.get(:handler)

send(self(), {:ready_for_data, handler})
Logger.debug(fn ->
"""
-- Absinthe Phoenix Reply --
Expand Down Expand Up @@ -142,6 +156,22 @@ defmodule Absinthe.Phoenix.Channel do
|> Absinthe.Pipeline.for_document(options)
end

def handle_info(
:after_join,
socket = %{assigns: %{__absinthe_presence_config__: presence_config}}
)
when is_map(presence_config) do
Presence.track(socket)
push(socket, "presence_state", Presence.list(socket))
{:noreply, socket}
end

## This handler is used to tell our handler it can send its data
def handle_info({:ready_for_data, handler}, socket) when is_pid(handler) do
send(handler, :send_updates)
{:noreply, socket}
end

def handle_info(_, state) do
{:noreply, state}
end
Expand Down
105 changes: 105 additions & 0 deletions lib/absinthe/phoenix/presence.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
defmodule Absinthe.Phoenix.Presence do
@moduledoc """
This module is to allow the use of the Phoenix.Presence behaviour (https://hexdocs.pm/phoenix/Phoenix.Presence.html) while using Absinthe.Phoenix.

To use it, do the following:
1. Set up your implementation of Phoenix.Presence as per the official documentation: https://hexdocs.pm/phoenix/Phoenix.Presence.html
2. When 'using' Absinthe.Phoenix.Socket, simply add the presence_config option like so:
'''
defmodule MyAppWeb.SocketUserModule do
use Absinthe.Phoenix.Socket,
presence_config: %{
module: MyAppWeb.Presence,
meta_fn: {MyAppWeb, :some_meta_logic_fn},
key_fn: {MyAppWeb, :some_key_logic_fn}
}

...
end
'''
* The :some_meta_logic atom is a function that returns the `meta` argument that Phoenix.Presence.track/3 expects (see https://hexdocs.pm/phoenix/Phoenix.Presence.html#c:track/3 for more info).
The single argument this function should handle is the socket itself, which is of type `Phoenix.Socket`. The reason it is a function and not a single data item is that it allows the devloper to be
flexible and apply/get whatever meta logic/data they want from the socket
* The :some_key_logic_fn atom is a function that returns the `key` argument that Phoenix.Presence.track/3 expects (see https://hexdocs.pm/phoenix/Phoenix.Presence.html#c:track/3 for more info).
The single argument this function should handle is the socket itself, which is of type `Phoenix.Socket`. The reason it is a function and not a single data item is that it allows the devloper to be
flexible and apply/get whatever key logic/data they want from the socket
"""
require Logger
@presence_topic "__absinthe__:control"

def presence_topic() do
@presence_topic
end

@doc """
Function to call the Phoenix.Presence.track/3 callback from the module that the user has configured in __absinthe_presence_config__.
"""
def track(socket = %{assigns: %{__absinthe_presence_config__: presence_config}})
when is_map(presence_config) do
module = Map.get(presence_config, :module, __MODULE__.Defaults)
meta_fn = Map.get(presence_config, :meta_fn, {__MODULE__.Defaults, :meta_fn})
key_fn = Map.get(presence_config, :key_fn, {__MODULE__.Defaults, :key_fn})

meta = execute(meta_fn, socket)
key = execute(key_fn, socket)

{:ok, _} = module.track(socket, key, meta)
end

def track(_socket) do
Logger.debug(
"Cannot track as socket.assigns does not contain a valid :__abinthe_presence_config__ key!"
)

nil
end

@doc """
Function to call the Phoenix.Presence.list/1 callback from the module that the user has configured in __absinthe_presence_config__.
"""
def list(socket = %{assigns: %{__absinthe_presence_config__: presence_config}})

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Elixir can pattern match at multiple levels

Suggested change
def list(socket = %{assigns: %{__absinthe_presence_config__: presence_config}})
def list(socket = %{assigns: %{__absinthe_presence_config__: %{module: module}}}) when is_binary(module) do
module.list(socket)
end

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is_binary module is wrong through, would need to be is_atom.

when is_map(presence_config) do
module = Map.get(presence_config, :module)

if module == nil do
Logger.debug(
"Cannot list as the :__abinthe_presence_config__ map does not contain a :module key!"
)

nil
else
module.list(socket)
end
end

def list(_socket) do
Logger.debug(
"Cannot list as socket.assigns does not contain a valid :__abinthe_presence_config__ key!"
)

nil
end

defp execute({module, function}, args) do
apply(module, function, [args])
end

defmodule Defaults do
@moduledoc """
Module for housing the default functions if none are given
"""
def track(_socket, _key, _meta) do
{:ok, ""}
end

def meta_fn(_socket) do
%{
online_at: inspect(System.system_time(:second))
}
end

def key_fn(_socket) do
""
end
end
end
4 changes: 3 additions & 1 deletion lib/absinthe/phoenix/socket.ex
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,16 @@ defmodule Absinthe.Phoenix.Socket do
defmacro __using__(opts) do
schema = Keyword.get(opts, :schema)
pipeline = Keyword.get(opts, :pipeline)
presence_config = Keyword.get(opts, :presence_config)

quote do
channel(
"__absinthe__:*",
Absinthe.Phoenix.Channel,
assigns: %{
__absinthe_schema__: unquote(schema),
__absinthe_pipeline__: unquote(pipeline)
__absinthe_pipeline__: unquote(pipeline),
__absinthe_presence_config__: unquote(presence_config),
}
)
end
Expand Down