From 439c8e3eaa76292aa9904959fde76810fb21440f Mon Sep 17 00:00:00 2001 From: Tomas Koutsky Date: Mon, 16 Apr 2018 15:00:58 +0200 Subject: [PATCH 1/3] Add init_channels/0 function. init_channels/0 allows to subscribe to given list of channels after startup --- lib/pushest.ex | 10 +++++++--- lib/pushest/api.ex | 2 +- lib/pushest/socket.ex | 25 +++++++++++++++++++++---- lib/pushest/socket/data/state.ex | 3 ++- lib/pushest/supervisor.ex | 10 +++++++--- mix.exs | 2 +- test/pushest_test.exs | 16 ++++++++++++++++ 7 files changed, 55 insertions(+), 13 deletions(-) diff --git a/lib/pushest.ex b/lib/pushest.ex index 5c9ceef..d2dd5d3 100644 --- a/lib/pushest.ex +++ b/lib/pushest.ex @@ -114,11 +114,11 @@ defmodule Pushest do """ @spec start_link(pusher_opts) :: {:ok, pid} | {:error, term} def start_link(pusher_opts) when is_map(pusher_opts) do - Pushest.Supervisor.start_link(pusher_opts, __MODULE__) + Pushest.Supervisor.start_link(pusher_opts, __MODULE__, init_channels()) end def start_link(_) do - Pushest.Supervisor.start_link(@config, __MODULE__) + Pushest.Supervisor.start_link(@config, __MODULE__, init_channels()) end def child_spec(opts) do @@ -129,6 +129,10 @@ defmodule Pushest do } end + def init_channels do + [] + end + @doc ~S""" Subscribe to a channel with user_data as a map. When subscribing to a presence- channel user_id key with unique identifier as a value has to be @@ -221,7 +225,7 @@ defmodule Pushest do ) end - defoverridable handle_event: 2 + defoverridable handle_event: 2, init_channels: 0 end end end diff --git a/lib/pushest/api.ex b/lib/pushest/api.ex index 102b2ac..ee0ebd9 100644 --- a/lib/pushest/api.ex +++ b/lib/pushest/api.ex @@ -15,7 +15,7 @@ defmodule Pushest.Api do @client Pushest.Client.for_env() @version Mix.Project.config()[:version] - def start_link({pusher_opts, _callback_module}) do + def start_link({pusher_opts, _callback_module, _init_channels}) do GenServer.start_link( __MODULE__, %State{url: Utils.url(pusher_opts), options: %Options{} |> Map.merge(pusher_opts)}, diff --git a/lib/pushest/socket.ex b/lib/pushest/socket.ex index 8747e3f..789f23b 100644 --- a/lib/pushest/socket.ex +++ b/lib/pushest/socket.ex @@ -105,13 +105,21 @@ defmodule Pushest.Socket do """ def handle_info( {:gun_ws, _conn_pid, {:text, raw_frame}}, - state = %State{channels: channels, presence: presence, callback_module: callback_module} + state = %State{ + channels: channels, + presence: presence, + callback_module: callback_module, + init_channels: init_channels + } ) do frame = Frame.decode!(raw_frame) case frame.event do "pusher:connection_established" -> Logger.debug("Socket | pusher:connection_established") + + do_init_channels(init_channels) + {:noreply, %{state | socket_info: SocketInfo.decode(frame.data)}} "pusher_internal:subscription_succeeded" -> @@ -166,12 +174,21 @@ defmodule Pushest.Socket do @client.ws_send(conn_pid, {:text, Frame.encode!(frame)}) end - @spec init_state({map, module}) :: %State{} - defp init_state({pusher_opts, callback_module}) do + @spec init_state({map, module, list}) :: %State{} + defp init_state({pusher_opts, callback_module, init_channels}) do %State{ options: %Options{} |> Map.merge(pusher_opts), url: Utils.url(pusher_opts), - callback_module: callback_module + callback_module: callback_module, + init_channels: init_channels } end + + defp do_init_channels([[name: channel, user_data: user_data] | other_channels]) do + GenServer.cast(__MODULE__, {:subscribe, channel, user_data}) + Logger.debug("do_init_channels: #{channel}") + do_init_channels(other_channels) + end + + defp do_init_channels([]), do: nil end diff --git a/lib/pushest/socket/data/state.ex b/lib/pushest/socket/data/state.ex index ac3ae63..b076e71 100644 --- a/lib/pushest/socket/data/state.ex +++ b/lib/pushest/socket/data/state.ex @@ -12,5 +12,6 @@ defmodule Pushest.Socket.Data.State do channels: [], presence: %Presence{}, conn_pid: nil, - callback_module: nil + callback_module: nil, + init_channels: [] end diff --git a/lib/pushest/supervisor.ex b/lib/pushest/supervisor.ex index feb9357..8b69481 100644 --- a/lib/pushest/supervisor.ex +++ b/lib/pushest/supervisor.ex @@ -7,9 +7,13 @@ defmodule Pushest.Supervisor do alias Pushest.{Api, Socket} use Supervisor - @spec start_link(map, module) :: {:ok, pid} | {:error, term} - def start_link(pusher_opts, callback_module) do - Supervisor.start_link(__MODULE__, {pusher_opts, callback_module}, name: __MODULE__) + @spec start_link(map, module, list) :: {:ok, pid} | {:error, term} + def start_link(pusher_opts, callback_module, init_channels) do + Supervisor.start_link( + __MODULE__, + {pusher_opts, callback_module, init_channels}, + name: __MODULE__ + ) end def init(opts) do diff --git a/mix.exs b/mix.exs index 96e89d1..6af03c5 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule Pushest.MixProject do def project do [ app: :pushest, - version: "0.2.0", + version: "0.2.1", elixir: "~> 1.6", start_permanent: Mix.env() == :prod, deps: deps(), diff --git a/test/pushest_test.exs b/test/pushest_test.exs index 0abc3f1..a6a15dd 100644 --- a/test/pushest_test.exs +++ b/test/pushest_test.exs @@ -12,6 +12,14 @@ defmodule PushestTest do @moduledoc false use Pushest, otp_app: :pushest + + def init_channels do + [ + [name: "public-init-channel", user_data: %{}], + [name: "private-init-channel", user_data: %{}], + [name: "presence-init-channel", user_data: %{user_id: 123}], + ] + end end def child_pid(mod_name) do @@ -62,6 +70,14 @@ defmodule PushestTest do start() end + describe "subscription to init_channels" do + test "it subscribes to list of init_channels after startup" do + assert Enum.member?(TestPushest.subscribed_channels(), "public-init-channel") + assert Enum.member?(TestPushest.subscribed_channels(), "private-init-channel") + assert Enum.member?(TestPushest.subscribed_channels(), "presence-init-channel") + end + end + describe "subscribe" do @channel "test-channel" test "to a public channel", context do From 8699b3d7f6d46e32987a8258a67618c2f4557325 Mon Sep 17 00:00:00 2001 From: Tomas Koutsky Date: Tue, 17 Apr 2018 00:09:22 +0200 Subject: [PATCH 2/3] Fix the comments --- README.md | 9 ++++++ lib/pushest.ex | 70 ++++++++++++++++++++++++++++++++----------- lib/pushest/api.ex | 2 +- lib/pushest/socket.ex | 2 +- 4 files changed, 64 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 9e64c00..afc28fa 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,15 @@ config :simple_client, SimpleClient, defmodule SimpleClient do use Pushest, otp_app: :simple_client + # Subscribe to these channels right after application startup. + def init_channels do + [ + [name: "public-init-channel", user_data: %{}], + [name: "private-init-channel", user_data: %{}], + [name: "presence-init-channel", user_data: %{user_id: 123}], + ] + end + # handle_event/2 is user-defined callback which is triggered whenever an event # occurs on the channel. def handle_event({:ok, "public-channel", "some-event"}, frame) do diff --git a/lib/pushest.ex b/lib/pushest.ex index d2dd5d3..a329ef0 100644 --- a/lib/pushest.ex +++ b/lib/pushest.ex @@ -2,13 +2,13 @@ defmodule Pushest do @moduledoc ~S""" Pushest is a Pusher library leveraging Elixir/OTP to combine server and client-side Pusher features. Abstracts un/subscription, client-side triggers, private/presence channel authorizations. - Keeps track of subscribed channels and users presence when subscribed to presence channel. - Pushest is meant to be used in your module where you can define callbacks for + Keeps track of subscribed channels and users presence when subscribed to a presence channel. + Pushest is meant to be `use`d in your module where you can define callbacks for events you're interested in. A simple implementation in an OTP application would be: ``` - # Add necessary pusher configuration to your application config: + # Add necessary pusher configuration to your application config (assuming an OTP app): # simple_client/config/config.exs config :simple_client, SimpleClient, pusher_app_id: System.get_env("PUSHER_APP_ID"), @@ -19,18 +19,33 @@ defmodule Pushest do # simple_client/simple_client.ex defmodule SimpleClient do + # :otp_app option is needed for Pushest to get a config. use Pushest, otp_app: :simple_client + # Subscribe to these channels right after application startup. + def init_channels do + [ + [name: "public-init-channel", user_data: %{}], + [name: "private-init-channel", user_data: %{}], + [name: "presence-init-channel", user_data: %{user_id: 123}], + ] + end + + # handle incoming events. + def handle_event({:ok, "public-init-channel", "some-event"}, frame) do + # do something with public-init-channel frame + end + def handle_event({:ok, "public-channel", "some-event"}, frame) do - # do something with public frame + # do something with public-channel frame end def handle_event({:ok, "private-channel", "some-other-event"}, frame) do - # do something with private frame + # do something with private-channel frame end end - # Now you can start your application with Pushest as a part of your supervision tree: + # Now you can start your application as a part of your supervision tree: # simple_client/lib/simple_client/application.ex def start(_type, _args) do children = [ @@ -55,16 +70,22 @@ defmodule Pushest do {:ok, pid} = SimpleClient.start_link(config) ``` - Now you can interact with Pusher: + Now you can interact with Pusher using methods injected in your module: ``` SimpleClient.trigger("private-channel", "event", %{message: "via api"}) SimpleClient.channels() - # => %{"channels" => %{"public-channel" => %{}}} + # => %{ + "channels" => %{ + "presence-init-channel" => %{}, + "private-init-channel" => %{}, + "public-init-channel" => %{} + } SimpleClient.subscribe("private-channel") SimpleClient.trigger("private-channel", "event", %{message: "via ws"}) SimpleClient.trigger("private-channel", "event", %{message: "via api"}, force_api: true) # ... ``` + For full list of injected methods please check the README. """ alias Pushest.Router @@ -112,7 +133,6 @@ defmodule Pushest do For available pusher_opts values see `t:pusher_opts/0`. """ - @spec start_link(pusher_opts) :: {:ok, pid} | {:error, term} def start_link(pusher_opts) when is_map(pusher_opts) do Pushest.Supervisor.start_link(pusher_opts, __MODULE__, init_channels()) end @@ -129,10 +149,6 @@ defmodule Pushest do } end - def init_channels do - [] - end - @doc ~S""" Subscribe to a channel with user_data as a map. When subscribing to a presence- channel user_id key with unique identifier as a value has to be @@ -140,7 +156,6 @@ defmodule Pushest do informations about user. E.g.: %{user_id: "1", user_info: %{name: "Tomas Koutsky"}} """ - @spec subscribe(String.t(), map) :: term def subscribe(channel, user_data) do Router.cast({:subscribe, channel, user_data}) end @@ -148,7 +163,6 @@ defmodule Pushest do @doc ~S""" Subscribe to a channel without any user data, like any public channel. """ - @spec subscribe(String.t()) :: term def subscribe(channel) do Router.cast({:subscribe, channel, %{}}) end @@ -157,7 +171,6 @@ defmodule Pushest do Trigger on given channel/event combination - sends given data to Pusher. data has to be a map. """ - @spec trigger(String.t(), String.t(), map) :: term def trigger(channel, event, data) do Router.cast({:trigger, channel, event, data}) end @@ -169,7 +182,6 @@ defmodule Pushest do For trigger_opts values see `t:trigger_opts/0`. """ - @spec trigger(String.t(), String.t(), map, trigger_opts) :: term def trigger(channel, event, data, opts) do Router.cast({:trigger, channel, event, data}, opts) end @@ -203,6 +215,29 @@ defmodule Pushest do Router.cast({:unsubscribe, channel}) end + @doc ~S""" + Function meant to be overwritten in user module, e.g.: + ``` + defmodule MyMod do + use Pushest, otp_app: :my_mod + + def init_channels do + [ + [name: "public-init-channel", user_data: %{}], + [name: "private-init-channel", user_data: %{}], + [name: "presence-init-channel", user_data: %{user_id: 123}], + ] + end + end + ``` + Subscribes to given list of channels right after application startup. + Each element has to be a keyword list in exact format of: + [name: String.t(), user_data: map] + """ + def init_channels do + [] + end + @doc ~S""" Function meant to be overwritten in user module, e.g.: ``` @@ -214,6 +249,7 @@ defmodule Pushest do end end ``` + Catches events sent to a channels the client is subscribed to. """ def handle_event({status, channel, event}, frame) do require Logger diff --git a/lib/pushest/api.ex b/lib/pushest/api.ex index ee0ebd9..ad7a296 100644 --- a/lib/pushest/api.ex +++ b/lib/pushest/api.ex @@ -78,7 +78,7 @@ defmodule Pushest.Api do end def handle_info( - {:gun_response, conn_pid, stream_ref, :nofin, status, _headers}, + {:gun_response, conn_pid, stream_ref, :fin, status, _headers}, state = %State{conn_pid: conn_pid} ) do case status do diff --git a/lib/pushest/socket.ex b/lib/pushest/socket.ex index 789f23b..0acfbd8 100644 --- a/lib/pushest/socket.ex +++ b/lib/pushest/socket.ex @@ -184,9 +184,9 @@ defmodule Pushest.Socket do } end + @spec do_init_channels(list) :: term defp do_init_channels([[name: channel, user_data: user_data] | other_channels]) do GenServer.cast(__MODULE__, {:subscribe, channel, user_data}) - Logger.debug("do_init_channels: #{channel}") do_init_channels(other_channels) end From c4da10211ac019c7ea7be5d3b2e1a05e5529e42c Mon Sep 17 00:00:00 2001 From: Tomas Koutsky Date: Tue, 17 Apr 2018 00:35:08 +0200 Subject: [PATCH 3/3] Update the readme --- README.md | 54 +++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 49 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index afc28fa..3ec7ca7 100644 --- a/README.md +++ b/README.md @@ -56,12 +56,16 @@ defmodule SimpleClient do # handle_event/2 is user-defined callback which is triggered whenever an event # occurs on the channel. + def handle_event({:ok, "public-init-channel", "some-event"}, frame) do + # do something with public-init-channel frame + end + def handle_event({:ok, "public-channel", "some-event"}, frame) do - # do something with public frame + # do something with public-channel frame end def handle_event({:ok, "private-channel", "some-other-event"}, frame) do - # do something with private frame + # do something with private-channel frame end # We can also catch errors. @@ -97,6 +101,14 @@ config = %{ ### Now you can use various functions injected in your module ```elixir +SimpleClient.channels() +# => %{ +"channels" => %{ + "presence-init-channel" => %{}, + "private-init-channel" => %{}, + "public-init-channel" => %{} +} +# ... SimpleClient.subscribe("public-channel") :ok # ... @@ -117,8 +129,9 @@ SimpleClient.presence() SimpleClient.trigger("private-channel", "first-event", %{message: "Ahoj"}) :ok # ... -SimpleClient.channels() -["presence-channel", "private-channel", "public-channel"] +SimpleClient.subscribed_channels() +["presence-channel", "private-channel", "public-channel", + "presence-init-channel", "private-init-channel", "public-init-channel"] # ... SimpleClient.unsubscribe("public-channel") :ok @@ -174,7 +187,7 @@ SimpleClient.channels() #### subscribed_channels/0 Returns list of all the subscribed channels for current instance. ```elixir -SimpleClient.channels() +SimpleClient.subscribed_channels() ["private-channel"] ``` @@ -196,6 +209,37 @@ Unsubscribes from given channel SimpleClient.unsubscribe("public-channel") ``` +### Overridable functions +These functions are meant to be overridden in a module using Pushest +#### handle_event/2 +Callback being triggered when there is a WebSocket event on a subscribed channel. +```elixir +defmodule MyApp.MyModule + use Pushest, otp_app: :my_app + + def handle_event({:ok, "my-channel", "my-event"}, frame) do + IO.inspect frame + end +end +``` + +#### init_channels/0 +Subscribes to given list of channels right after application startup. +Each element has to be a keyword list in exact format of: `[name: String.t(), user_data: map]` +```elixir +defmodule MyApp.MyModule + use Pushest, otp_app: :my_app + + def init_channels do + [ + [name: "public-init-channel", user_data: %{}], + [name: "private-init-channel", user_data: %{}], + [name: "presence-init-channel", user_data: %{user_id: 123}], + ] + end +end +``` + #### `frame` example `frame` is a `Pushest.Socket.Data.Frame` or `Pushest.Api.Data.Frame` struct with data payload as a map. ```elixir