-
Notifications
You must be signed in to change notification settings - Fork 43
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
Adds support for allowing PubSub subscription and events handling #189
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -213,6 +213,15 @@ defmodule Drab do | |
{:noreply, state} | ||
end | ||
|
||
# pass any other handle_info to the Commander `handle_info_message/2` callbacks, | ||
# which expect `message` and `socket` params (no `state` reply). | ||
def handle_info(message, state) do | ||
state.socket | ||
|> get_commander() | ||
|> apply(:handle_info_message, [message, state.socket]) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is synchronous! Definitely shouldn't be! Drab has async calls in this module, should follow the same pattern as them. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also, why is this name of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it is hardcoded as well as GenServer name |
||
{:noreply, state} | ||
end | ||
|
||
@doc false | ||
@spec handle_cast(tuple, t) :: {:noreply, t} | ||
def handle_cast({:onconnect, socket, payload}, %Drab{commander: commander} = state) do | ||
|
@@ -240,10 +249,18 @@ defmodule Drab do | |
def handle_cast({:onload, socket, payload}, %Drab{commander: commander} = state) do | ||
socket = transform_socket(payload["payload"], socket, state) | ||
|
||
# onload_init sync call with arity/0 | ||
handle_callback_sync(commander, commander_config(commander).onload_init) | ||
|
||
# onload async call | ||
onload = commander_config(commander).onload | ||
handle_callback(socket, commander, onload) | ||
|
||
for shared_commander <- state.commanders do | ||
# onload_init sync call with arity/0 | ||
handle_callback_sync(commander, commander_config(commander).onload_init) | ||
|
||
# onload async call | ||
onload = commander_config(shared_commander).onload | ||
handle_callback(socket, shared_commander, onload) | ||
end | ||
|
@@ -277,6 +294,7 @@ defmodule Drab do | |
end | ||
end) | ||
|
||
# Handles callbacks asynchronously | ||
@spec handle_callback(Phoenix.Socket.t(), atom, atom) :: Phoenix.Socket.t() | ||
defp handle_callback(socket, commander, callback) do | ||
if callback do | ||
|
@@ -294,6 +312,19 @@ defmodule Drab do | |
socket | ||
end | ||
|
||
# Handles callbacks synchronously | ||
# Temporary disabled until it will be needed, for avoiding warning | ||
# @spec handle_callback_sync(Phoenix.Socket.t(), atom, atom) :: Phoenix.Socket.t() | ||
# defp handle_callback_sync(socket, commander, callback) do | ||
# if callback, do: apply(commander, callback, [socket]) | ||
# socket | ||
# end | ||
|
||
@spec handle_callback_sync(atom, atom) :: any | ||
defp handle_callback_sync(commander, callback) do | ||
if callback, do: apply(commander, callback, []) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This really shouldn't exist, like really really shouldn't exist. If someone wants the pid to drab they can just call the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is necessary to let to subscribe to PubSub in the Commander. This because it is deprecated to pass a PID to the PubSub.subscribe function, otherwise we could just use |
||
end | ||
|
||
@spec transform_payload(map, t) :: map | ||
defp transform_payload(payload, state) do | ||
all_modules = DrabModule.all_modules_for(state.commander.__drab__().modules) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -132,13 +132,22 @@ defmodule Drab.Commander do | |
defmodule DrabExample.PageCommander do | ||
use Drab.Commander | ||
|
||
onload_init: do_init | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should not exist, it is sync. |
||
onload :page_loaded | ||
onconnect :connected | ||
ondisconnect :disconnected | ||
|
||
before_handler :check_status | ||
after_handler :clean_up, only: [:perform_long_process] | ||
|
||
def do_init do | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ditto, no sync calls. |
||
# notice that this callback is called synchronously, so | ||
# it is not suitable for longer operations that could | ||
# slow down the main process. | ||
# Any operation here inherits the main process PID | ||
... | ||
end | ||
|
||
def page_loaded(socket) do | ||
... | ||
end | ||
|
@@ -169,6 +178,14 @@ defmodule Drab.Commander do | |
Launched every time client browser connects to the server, including reconnects after server | ||
crash, network broken etc | ||
|
||
#### `onload_init` | ||
Launched synchronously just before the `onload` callback. Unlike all the others callbacks | ||
that runs in their own process, this callback runs in the main process, and therefore is useful | ||
to call those operations who needs the main process PID to operate correctly, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No it is not useful for that because you can just |
||
as e.g. the subscription to PubSub events. | ||
This callback is not suitable for hosting longer or harmful operations, since this | ||
could slow down or broke the main process. Use with caution. | ||
|
||
#### `onload` | ||
Launched only once after page loaded and connects to the server - exactly the same like | ||
`onconnect`, but launches only once, not after every reconnect | ||
|
@@ -191,6 +208,11 @@ defmodule Drab.Commander do | |
Runs after the event handler. Gets return value of the event handler function as a third argument. | ||
Can be filtered by `:only` or `:except` options, analogically to `before_handler` | ||
|
||
|
||
### `handle_info_message` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems to focus a lot on 'pubsub' when it is nothing of the sort, only thing it does is pass in otherwise unhandled messages, I.E it's an info message handler, no need to focus on pubsub stuff and just say that when an unknown message is sent to the drab pid then it is received here. Phoenix.PubSub already clarifies how Phoenix.PubSub works. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes it is focused on PubSub because this is the main and most useful reason to add this stuff to Drab, but of course we change the documentation. |
||
Launched every time a GenServer/PubSub event to which you have subscribed is fired. | ||
See "PubSub support" for more informations. | ||
|
||
### Using callbacks to check user permissions | ||
Callbacks are handy for security. You may retrieve controller name and action name from the | ||
socket with `controller/1` and `action/1`. | ||
|
@@ -240,6 +262,92 @@ defmodule Drab.Commander do | |
Drab injects function `render_to_string/2` into your Commander. It is a shorthand for | ||
`Phoenix.View.render_to_string/3` - Drab automatically chooses the current View. | ||
|
||
## PubSub support | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Technically Drab already has PubSub support as of https://github.com/grych/drab/releases/tag/v0.8.3 although not of the form you are expecting, rather it passes messages to the javascript to then be handled there. Messages to the Drab process should already be web-ready regardless, unknown pubsub messages should never reach drab regardless. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It seems that Drab hasn't any kind of PubSub support. It just use PubSub for its internal uses, that it is a different things to let the developer uses Phoenix.PubSub directly inside a live page if needed, as for example LiveView does and suggest. |
||
|
||
You can use the PubSub mechanism to automatically update your pages on data changes. | ||
|
||
Suppose for example you have a backend module that interfaces the database, and you | ||
whishes to automatically update your page every time a change occours in a | ||
database table by some operations made in other part of your application. You can | ||
implement | ||
|
||
Suppose you need to automatically update your page every time a change occours in a | ||
database table by some operations made in other part of your application. You can | ||
implement a PubSub mechanism to react from the commander to any events you have | ||
subscribed to: add the subscription to the desidered `PubSub` in the | ||
`onload_init` event handler, then add a `handle_info_message/2` handler for any | ||
event you need to react to. | ||
|
||
Please note that the `handle_info_message/2` handler is a simplified version of the | ||
GenServer/PubSub `handle_info/2` handler, it expects a message parameter, identical | ||
to the standard `handle_info/2` one, and a `socket` parameter, instead of the | ||
`state` parameters, and it doesn't have to return anything. | ||
|
||
For example: | ||
|
||
# the backend: | ||
defmodule MyApp.Backend do | ||
# PubSub stuff | ||
@topic inspect(__MODULE__) | ||
|
||
def subscribe() do | ||
Phoenix.PubSub.subscribe(MyApp.PubSub, @topic) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This isn't taking a PID, that's very weird... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is not possible to pass the pid to a PubSub.subscription function. Otherwise we could just call This because, although documented, This is the exact reason because I've elaborate a solution through the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
https://hexdocs.pm/phoenix_pubsub/Phoenix.PubSub.html#subscribe/3 Besides, Drab already has a method to subscribe to pubsub topics via it's own There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
end | ||
|
||
defp notify_subscribers({:ok, result}, event) do | ||
Phoenix.PubSub.broadcast(MyApp.PubSub, @topic, {__MODULE__, event, result}) | ||
{:ok, result} | ||
end | ||
defp notify_subscribers({:error, reason}, _event), do: {:error, reason} | ||
|
||
# CRUD stuff | ||
... | ||
def create(...) do | ||
... | ||
|> Repo.insert() | ||
|> notify_subscribers([:data, :created]) | ||
end | ||
|
||
def update(...) do | ||
... | ||
|> Repo.update() | ||
|> notify_subscribers([:data, :updated]) | ||
end | ||
|
||
def delete(...) do | ||
... | ||
|> Repo.delete() | ||
|> notify_subscribers([:data, :deleted]) | ||
end | ||
end | ||
|
||
# the Commander | ||
defmodule MyApp.Commander do | ||
use Drab.Commander | ||
|
||
onload_init(:do_init) | ||
|
||
def do_init() do | ||
# subscribe to any desidered PubSub | ||
MyApp.Backend.subscribe() | ||
end | ||
|
||
... | ||
|
||
# handles the events | ||
def handle_info_message({MyApp.Backend, [:data, :created], result}, socket) do | ||
poke(socket, data: result) | ||
end | ||
|
||
# handles the events | ||
def handle_info_message({MyApp.Backend, [:data, :updated], result}, socket) do | ||
poke(socket, data: result) | ||
end | ||
|
||
... | ||
end | ||
|
||
|
||
### Examples: | ||
|
||
buttons = render_to_string("waiter_example.html", []) | ||
|
@@ -379,7 +487,7 @@ defmodule Drab.Commander do | |
end | ||
end | ||
|
||
Enum.each([:onload, :onconnect, :ondisconnect], fn macro_name -> | ||
Enum.each([:onload_init, :onload, :onconnect, :ondisconnect], fn macro_name -> | ||
@doc """ | ||
Sets up the callback for #{macro_name}. Receives handler function name as an atom. | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,6 +4,7 @@ defmodule Drab.Commander.Config do | |
defstruct commander: nil, | ||
controller: nil, | ||
view: nil, | ||
onload_init: nil, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a synchronous call, should not exist. |
||
onload: nil, | ||
onconnect: nil, | ||
ondisconnect: nil, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
defmodule DrabTestApp.PubsubTest do | ||
import Drab.Live | ||
use DrabTestApp.IntegrationCase | ||
|
||
defp pubsub_index do | ||
pubsub_url(DrabTestApp.Endpoint, :index) | ||
end | ||
|
||
setup do | ||
pubsub_index() |> navigate_to() | ||
# wait for the Drab to initialize | ||
find_element(:id, "page_loaded_indicator") | ||
[socket: drab_socket()] | ||
end | ||
|
||
describe "PubSub" do | ||
test "Subscribed PubSub events should trigger `handle_info_message` in the Commander" do | ||
socket = drab_socket() | ||
|
||
# Changes some data in the db, this will trigger a PubSub event | ||
DrabTestApp.Backend.append_element(42) | ||
|
||
# Above operation is async, so wait a little bit | ||
# for its completion before checking the result | ||
Process.sleep(500) | ||
|
||
# Check if the `handle_info_message` had changed the | ||
# page assigns according to the new data | ||
assert peek(socket, :status) == {:ok, "updated"} | ||
assert peek(socket, :data) == {:ok, [1, 2, 3, 42]} | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
defmodule DrabTestApp.Backend do | ||
@moduledoc false | ||
|
||
# PubSub support | ||
@topic inspect(__MODULE__) | ||
|
||
def subscribe() do | ||
Phoenix.PubSub.subscribe(DrabTestApp.PubSub, @topic) | ||
end | ||
|
||
defp notify_subscribers({:ok, result}, event) do | ||
Phoenix.PubSub.broadcast(DrabTestApp.PubSub, @topic, {__MODULE__, event, result}) | ||
{:ok, result} | ||
end | ||
|
||
defp notify_subscribers({:error, reason}, _event), do: {:error, reason} | ||
|
||
# Fake db access | ||
@fake_db [1, 2, 3] | ||
|
||
def get_data() do | ||
@fake_db | ||
end | ||
|
||
def append_element(element) do | ||
get_data() | ||
|> List.insert_at(-1, element) | ||
|> (&({:ok, &1})).() | ||
|> notify_subscribers([:data, :updated]) | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -49,6 +49,8 @@ defmodule DrabTestApp.Router do | |
get("/tests/live/broadcasting", LiveController, :broadcasting, as: :broadcasting) | ||
|
||
get("/tests/element", ElementController, :index, as: :element) | ||
|
||
get("/tests/pubsub", PubsubController, :index, as: :pubsub) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Drab already has pubsub functionality, although it's tested in the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't understand this comment. Here we are just testing the new added functionalities with a PubSub lifecycle case, thus the name 'pubsub' for this test. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh I was just saying that the little bit of pubsub use that drab uses is testing in the |
||
end | ||
|
||
# Other scopes may use custom stacks. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
defmodule DrabTestApp.PubsubCommander do | ||
@moduledoc false | ||
|
||
use Drab.Commander | ||
|
||
onload_init(:do_init) | ||
onload(:page_loaded) | ||
|
||
def do_init() do | ||
DrabTestApp.Backend.subscribe() | ||
end | ||
|
||
def page_loaded(socket) do | ||
DrabTestApp.IntegrationCase.add_page_loaded_indicator(socket) | ||
DrabTestApp.IntegrationCase.add_pid(socket) | ||
end | ||
|
||
def handle_info_message({DrabTestApp.Backend, [:data, :updated], result}, socket) do | ||
poke(socket, status: "updated", data: result ) | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
defmodule DrabTestApp.PubsubController do | ||
@moduledoc false | ||
|
||
use DrabTestApp.Web, :controller | ||
require Logger | ||
|
||
def index(conn, _params) do | ||
render(conn, "index.html", status: "initilised", data: DrabTestApp.Backend.get_data()) | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
<h3 id="header">Drab Tests</h3> | ||
|
||
<div id="begin"></div> | ||
<div id="drab_pid"></div> | ||
|
||
<div>status_: <%= @status %> data: <%= inspect @data %></div> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
defmodule DrabTestApp.PubsubView do | ||
@moduledoc false | ||
|
||
use DrabTestApp.Web, :view | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure this should be in
Drab
, it should probably be inDrab.Channel
, honestlyDrab
shouldn't be a genserver at all, everything it does and all the information it holds really should be inDrab.Channel
, but that's a refactor for another day. ^.^;For note, Drab.Channel already handles phoenix messages, although it passes them to the client for bouncing back 'through' drab, that should probably be restricted to a specific set of 'types' of topics for such javascript bouncing, so normal phoenix events can be handled differently. Also,
Drab.Commander
hassubscribe
andunsubscribe
calls for registering to phoenix pubsub topics through the endpoint viaDrab.Channel
. I wonder what messages it handles through here actually, I.E. I wonder what the purpose of it is to send only to the JS side, hmm... This needs to be researched. Maybe Drab already has this functionality but it could just be 'fleshed out' a little bit...There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe, for now I haven't found a better way to support PubSub natively in a Commander