From f3a2a5789599603ed039f10825f29a16cf81cbda Mon Sep 17 00:00:00 2001 From: alanj853 Date: Tue, 22 Jun 2021 14:36:44 +0000 Subject: [PATCH 1/7] First pass at adding Phoenix.Presence --- lib/absinthe/phoenix/channel.ex | 13 +++++ lib/absinthe/phoenix/presence.ex | 98 ++++++++++++++++++++++++++++++++ lib/absinthe/phoenix/socket.ex | 4 +- 3 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 lib/absinthe/phoenix/presence.ex diff --git a/lib/absinthe/phoenix/channel.ex b/lib/absinthe/phoenix/channel.ex index 5e32a8b..87d1764 100644 --- a/lib/absinthe/phoenix/channel.ex +++ b/lib/absinthe/phoenix/channel.ex @@ -1,6 +1,7 @@ defmodule Absinthe.Phoenix.Channel do use Phoenix.Channel require Logger + alias Absinthe.Phoenix.Presence @moduledoc false @@ -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 @@ -142,6 +145,16 @@ 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 + def handle_info(_, state) do {:noreply, state} end diff --git a/lib/absinthe/phoenix/presence.ex b/lib/absinthe/phoenix/presence.ex new file mode 100644 index 0000000..926611d --- /dev/null +++ b/lib/absinthe/phoenix/presence.ex @@ -0,0 +1,98 @@ +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: &some_meta_logic_fn/1, + key_fn: &some_key_logic_fn/1 + } + + ... + end + ''' + * The some_meta_logic/1 function 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 argument should be 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/1 function 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 argument should be 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/1) + key_fn = Map.get(presence_config, :key_fn, &__MODULE__.Defaults.key_fn/1) + + {:ok, _} = module.track(socket, key_fn.(socket), meta_fn.(socket)) + end + + def track(_socket) do + Logger.warn( + "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}}) + when is_map(presence_config) do + module = Map.get(presence_config, :module) + + if module == nil do + Logger.warn( + "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.warn( + "Cannot list as socket.assigns does not contain a valid :__abinthe_presence_config__ key!" + ) + + nil + 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 diff --git a/lib/absinthe/phoenix/socket.ex b/lib/absinthe/phoenix/socket.ex index 8c30c34..a541c90 100644 --- a/lib/absinthe/phoenix/socket.ex +++ b/lib/absinthe/phoenix/socket.ex @@ -30,6 +30,7 @@ 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( @@ -37,7 +38,8 @@ defmodule Absinthe.Phoenix.Socket do Absinthe.Phoenix.Channel, assigns: %{ __absinthe_schema__: unquote(schema), - __absinthe_pipeline__: unquote(pipeline) + __absinthe_pipeline__: unquote(pipeline), + __absinthe_presence_config__: unquote(presence_config), } ) end From b1b0386c4dc38c025de0757f058a27e947ead79e Mon Sep 17 00:00:00 2001 From: alanj853 Date: Tue, 22 Jun 2021 15:49:52 +0100 Subject: [PATCH 2/7] tidy up --- lib/absinthe/phoenix/presence.ex | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/absinthe/phoenix/presence.ex b/lib/absinthe/phoenix/presence.ex index 926611d..950a311 100644 --- a/lib/absinthe/phoenix/presence.ex +++ b/lib/absinthe/phoenix/presence.ex @@ -10,8 +10,8 @@ defmodule Absinthe.Phoenix.Presence do use Absinthe.Phoenix.Socket, presence_config: %{ module: MyAppWeb.Presence, - meta_fn: &some_meta_logic_fn/1, - key_fn: &some_key_logic_fn/1 + meta_fn: &MyAppWeb.some_meta_logic_fn/1, + key_fn: &MyAppWeb.some_key_logic_fn/1 } ... @@ -44,7 +44,7 @@ defmodule Absinthe.Phoenix.Presence do end def track(_socket) do - Logger.warn( + Logger.debug( "Cannot track as socket.assigns does not contain a valid :__abinthe_presence_config__ key!" ) @@ -59,7 +59,7 @@ defmodule Absinthe.Phoenix.Presence do module = Map.get(presence_config, :module) if module == nil do - Logger.warn( + Logger.debug( "Cannot list as the :__abinthe_presence_config__ map does not contain a :module key!" ) @@ -70,7 +70,7 @@ defmodule Absinthe.Phoenix.Presence do end def list(_socket) do - Logger.warn( + Logger.debug( "Cannot list as socket.assigns does not contain a valid :__abinthe_presence_config__ key!" ) From 96bb98a991c9cafa15d834a2783f4e50b7b44546 Mon Sep 17 00:00:00 2001 From: alanj853 Date: Tue, 22 Jun 2021 15:14:53 +0000 Subject: [PATCH 3/7] follow previous function-passing structure --- lib/absinthe/phoenix/presence.ex | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/lib/absinthe/phoenix/presence.ex b/lib/absinthe/phoenix/presence.ex index 950a311..57adfa2 100644 --- a/lib/absinthe/phoenix/presence.ex +++ b/lib/absinthe/phoenix/presence.ex @@ -10,18 +10,18 @@ defmodule Absinthe.Phoenix.Presence do use Absinthe.Phoenix.Socket, presence_config: %{ module: MyAppWeb.Presence, - meta_fn: &MyAppWeb.some_meta_logic_fn/1, - key_fn: &MyAppWeb.some_key_logic_fn/1 + meta_fn: {MyAppWeb, :some_meta_logic_fn}, + key_fn: {MyAppWeb, :some_key_logic_fn} } ... end ''' - * The some_meta_logic/1 function 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 argument should be 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 + * 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/1 function 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 argument should be 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 + * 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 @@ -37,10 +37,13 @@ defmodule Absinthe.Phoenix.Presence do 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/1) - key_fn = Map.get(presence_config, :key_fn, &__MODULE__.Defaults.key_fn/1) + meta_fn = Map.get(presence_config, :meta_fn, {__MODULE__.Defaults, :meta_fn}) + key_fn = Map.get(presence_config, :key_fn, {__MODULE__.Defaults, :key_fn}) - {:ok, _} = module.track(socket, key_fn.(socket), meta_fn.(socket)) + meta = execute(meta_fn, socket) + key = execute(key_fn, socket) + + {:ok, _} = module.track(socket, key, meta) end def track(_socket) do @@ -77,6 +80,10 @@ defmodule Absinthe.Phoenix.Presence do 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 From 802741c27f1d911edf5c848f0966a151b73b3cb9 Mon Sep 17 00:00:00 2001 From: alanj853 Date: Wed, 22 Sep 2021 14:48:51 +0000 Subject: [PATCH 4/7] Add ready_for_data clause This allows us to wait for our current handler to execute fully and the client to prepare itself before it receives any data --- lib/absinthe/phoenix/channel.ex | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/lib/absinthe/phoenix/channel.ex b/lib/absinthe/phoenix/channel.ex index 87d1764..498b749 100644 --- a/lib/absinthe/phoenix/channel.ex +++ b/lib/absinthe/phoenix/channel.ex @@ -59,6 +59,10 @@ 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. + send(self(), :ready_for_data) Logger.debug(fn -> """ -- Absinthe Phoenix Reply -- @@ -155,6 +159,19 @@ defmodule Absinthe.Phoenix.Channel do {:noreply, socket} end + ## This handler is used to tell our handler it can send its data + def handle_info(:ready_for_data, socket) do + handler = + socket.assigns + |> Map.get(:absinthe, %{}) + |> Map.get(:opts, []) + |> Keyword.get(:context, %{handler: nil}) + |> Map.get(:handler) + + send(handler, :send_updates) + {:noreply, socket} + end + def handle_info(_, state) do {:noreply, state} end From 1f4d027005482c523c4f28d0c07812f4bae6fadc Mon Sep 17 00:00:00 2001 From: alanj853 Date: Wed, 22 Sep 2021 15:20:47 +0000 Subject: [PATCH 5/7] check for pid --- lib/absinthe/phoenix/channel.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/absinthe/phoenix/channel.ex b/lib/absinthe/phoenix/channel.ex index 498b749..8f072ca 100644 --- a/lib/absinthe/phoenix/channel.ex +++ b/lib/absinthe/phoenix/channel.ex @@ -168,7 +168,7 @@ defmodule Absinthe.Phoenix.Channel do |> Keyword.get(:context, %{handler: nil}) |> Map.get(:handler) - send(handler, :send_updates) + if is_pid(handler), do: send(handler, :send_updates) {:noreply, socket} end From be9960a330e9ad0fe6d3923faeb01ea066fe342c Mon Sep 17 00:00:00 2001 From: alanj853 Date: Wed, 22 Sep 2021 17:11:54 +0100 Subject: [PATCH 6/7] make sure we send to the right pid --- lib/absinthe/phoenix/channel.ex | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/absinthe/phoenix/channel.ex b/lib/absinthe/phoenix/channel.ex index 8f072ca..965cb4d 100644 --- a/lib/absinthe/phoenix/channel.ex +++ b/lib/absinthe/phoenix/channel.ex @@ -62,7 +62,14 @@ defmodule Absinthe.Phoenix.Channel do ## 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. - send(self(), :ready_for_data) + handler = + socket.assigns + |> Map.get(:absinthe, %{}) + |> Map.get(:opts, []) + |> Keyword.get(:context, %{handler: nil}) + |> Map.get(:handler) + + send(self(), {:ready_for_data, handler}) Logger.debug(fn -> """ -- Absinthe Phoenix Reply -- @@ -160,15 +167,8 @@ defmodule Absinthe.Phoenix.Channel do end ## This handler is used to tell our handler it can send its data - def handle_info(:ready_for_data, socket) do - handler = - socket.assigns - |> Map.get(:absinthe, %{}) - |> Map.get(:opts, []) - |> Keyword.get(:context, %{handler: nil}) - |> Map.get(:handler) - - if is_pid(handler), do: send(handler, :send_updates) + def handle_info({:ready_for_data, handler}, socket) when is_pid(handler) do + send(handler, :send_updates) {:noreply, socket} end From 9730ab7bb1042296825ce54f8064c7bc088c245b Mon Sep 17 00:00:00 2001 From: alanj853 Date: Fri, 21 Jan 2022 09:17:30 +0000 Subject: [PATCH 7/7] update .tool-versions --- .tool-versions | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.tool-versions b/.tool-versions index a3865e3..a13c60c 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1,2 @@ -elixir 1.10.0-otp-22 -erlang 22.0.7 +erlang 24.1.3 +elixir 1.12.1-otp-24