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

Feature: add site-wide handle_info callbacks #578

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/beacon/boot.ex
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ defmodule Beacon.Boot do
Task.Supervisor.async(task_supervisor, fn -> Beacon.Loader.reload_layouts_modules(config.site) end),
Task.Supervisor.async(task_supervisor, fn -> Beacon.Loader.reload_error_page_module(config.site) end),
Task.Supervisor.async(task_supervisor, fn -> Beacon.Loader.reload_pages_modules(config.site, per_page: 20) end),
Task.Supervisor.async(task_supervisor, fn -> Beacon.Loader.reload_info_handlers_module(config.site) end),
Task.Supervisor.async(task_supervisor, fn -> Beacon.Loader.reload_event_handlers_module(config.site) end)
# TODO: load main pages (order_by: path, per_page: 10) to avoid SEO issues
]
Expand Down
142 changes: 142 additions & 0 deletions lib/beacon/content.ex
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ defmodule Beacon.Content do
alias Beacon.Content.ComponentSlot
alias Beacon.Content.ComponentSlotAttr
alias Beacon.Content.ErrorPage
alias Beacon.Content.InfoHandler
alias Beacon.Content.EventHandler
alias Beacon.Content.Layout
alias Beacon.Content.LayoutEvent
Expand Down Expand Up @@ -3931,6 +3932,147 @@ defmodule Beacon.Content do
repo(site).delete(live_data_assign)
end

@doc """
Creates a new info handler for creating shared handle_info callbacks.

## Example

iex> create_info_handler(%{site: "my_site", msg: "{:new_msg, arg}", code: "{:noreply, socket}"})
{:ok, %InfoHandler{}}

"""
@doc type: :info_handlers
@spec create_info_handler(map()) :: {:ok, InfoHandler.t()} | {:error, Changeset.t()}
def create_info_handler(attrs) do
changeset = InfoHandler.changeset(%InfoHandler{}, attrs)
site = Changeset.get_field(changeset, :site)

changeset
|> validate_info_handler()
|> repo(site).insert()
|> tap(&maybe_broadcast_updated_content_event(&1, :info_handler))
end

@spec validate_info_handler(Changeset.t(), [String.t()]) :: Changeset.t()
defp validate_info_handler(changeset, imports \\ []) do
code = Changeset.get_field(changeset, :code)
msg = Changeset.get_field(changeset, :msg)
site = Changeset.get_field(changeset, :site)
metadata = %Beacon.Template.LoadMetadata{site: site}
var = ["socket", msg]
imports = ["Phoenix.LiveView"] ++ imports

do_validate_template(changeset, :code, :elixir, code, metadata, var, imports)
end

@doc """
Creates a info handler, raising an error if unsuccessful.

Returns the new info handler if successful, otherwise raises a `RuntimeError`.

## Example

iex> create_info_handler!(%{site: "my_site", msg: "{:new_msg, arg}", code: "{:noreply, socket}"})
%InfoHandler{}
"""
@doc type: :info_handlers
@spec create_info_handler!(map()) :: InfoHandler.t()
def create_info_handler!(attrs \\ %{}) do
case create_info_handler(attrs) do
{:ok, info_handler} -> info_handler
{:error, changeset} -> raise "failed to create info handler, got: #{inspect(changeset.errors)}"
end
end

@doc """
Returns an `%Ecto.Changeset{}` for tracking info handler changes.

## Example

iex> change_info_handler(info_handler, %{code: {:noreply, socket}})
%Ecto.Changeset{data: %InfoHandler{}}

"""
@doc type: :info_handlers
@spec change_info_handler(InfoHandler.t(), map()) :: Changeset.t()
def change_info_handler(%InfoHandler{} = info_handler, attrs \\ %{}) do
InfoHandler.changeset(info_handler, attrs)
end

@doc """
Gets a single info handler by `id`.

## Example

iex> get_single_info_handler(:my_site, "fefebbfe-f732-4119-9116-d031d04f5a2c")
%InfoHandler{}

"""
@doc type: :info_handlers
@spec get_info_handler(Site.t(), UUID.t()) :: InfoHandler.t() | nil
def get_info_handler(site, id) when is_atom(site) and is_binary(id) do
repo(site).get(InfoHandler, id)
end

@doc """
Same as `get_info_handler/2` but raises an error if no result is found.
"""
@doc type: :info_handlers
@spec get_info_handler!(Site.t(), UUID.t()) :: InfoHandler.t()
def get_info_handler!(site, id) when is_atom(site) and is_binary(id) do
repo(site).get!(InfoHandler, id)
end

@doc """
Lists all info handlers for a given site.

## Example

iex> list_info_handlers()

"""
@doc type: :info_handlers
@spec list_info_handlers(Site.t()) :: [InfoHandler.t()]
def list_info_handlers(site) do
repo(site).all(
from h in InfoHandler,
where: h.site == ^site,
order_by: [asc: h.inserted_at]
)
end

@doc """
Updates a info handler.

## Example

iex> update_info_handler(info_handler, %{msg: "{:new_msg, arg}"})
{:ok, %InfoHandler{}}

"""
@doc type: :info_handlers
@spec update_info_handler(InfoHandler.t(), map()) :: {:ok, InfoHandler.t()}
def update_info_handler(%InfoHandler{} = info_handler, attrs) do
changeset = InfoHandler.changeset(info_handler, attrs)
site = Changeset.get_field(changeset, :site)

changeset
|> validate_info_handler(["Phoenix.Component"])
|> repo(site).update()
|> tap(&maybe_broadcast_updated_content_event(&1, :info_handler))
end

@doc """
Deletes info handler.
"""
@doc type: :info_handlers
@spec delete_info_handler(InfoHandler.t()) :: {:ok, InfoHandler.t()} | {:error, Changeset.t()}
def delete_info_handler(info_handler) do
info_handler
|> repo(info_handler).delete()
|> tap(&maybe_broadcast_updated_content_event(&1, :info_handler))
end

## Utils

defp do_validate_template(changeset, field, format, template, metadata, vars \\ [], imports \\ [])
Expand Down
46 changes: 46 additions & 0 deletions lib/beacon/content/info_handler.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
defmodule Beacon.Content.InfoHandler do
@moduledoc """
Beacon's representation of a LiveView [handle_info/2](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#c:handle_info/2)
that applies to all of a site's pages.

This is the Elixir code which will handle messages from other Elixir processes.

> #### Do not create or edit info handlers manually {: .warning}
>
> Use the public functions in `Beacon.Content` instead.
> The functions in that module guarantee that all dependencies
> are created correctly and all processes are updated.
> Manipulating data manually will most likely result
> in inconsistent behavior and crashes.
"""

use Beacon.Schema

import Ecto.Changeset

@type t :: %__MODULE__{
id: Ecto.UUID.t(),
site: Beacon.Types.Site.t(),
msg: binary(),
code: binary(),
inserted_at: DateTime.t(),
updated_at: DateTime.t()
}

schema "beacon_info_handlers" do
field :site, Beacon.Types.Site
field :msg, :string
field :code, :string

timestamps()
end

@doc false
def changeset(%__MODULE__{} = info_handler, attrs) do
fields = ~w(site msg code)a

info_handler
|> cast(attrs, fields)
|> validate_required(fields)
end
end
13 changes: 13 additions & 0 deletions lib/beacon/loader.ex
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,10 @@ defmodule Beacon.Loader do
Loader.Page.module_name(site, page_id)
end

def fetch_info_handlers_module(site) do
Loader.InfoHandlers.module_name(site)
end

def maybe_reload_page_module(site, page_id) do
maybe_reload(Loader.Page.module_name(site, page_id), fn -> reload_page_module(site, page_id) end)
end
Expand Down Expand Up @@ -218,6 +222,10 @@ defmodule Beacon.Loader do
GenServer.call(worker(site), {:unload_page_module, page_id}, @timeout)
end

def reload_info_handlers_module(site) do
GenServer.call(worker(site), :reload_info_handlers_module, @timeout)
end

defp maybe_reload(module, reload_fun) do
if :erlang.module_loaded(module) do
{:ok, module}
Expand Down Expand Up @@ -296,6 +304,11 @@ defmodule Beacon.Loader do
{:noreply, config}
end

def handle_info({:content_updated, :info_handler, %{site: site}}, config) do
reload_info_handlers_module(site)
{:noreply, config}
end

def handle_info({:content_updated, :event_handler, %{site: site}}, config) do
reload_event_handlers_module(site)
{:noreply, config}
Expand Down
32 changes: 32 additions & 0 deletions lib/beacon/loader/info_handlers.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
defmodule Beacon.Loader.InfoHandlers do
@moduledoc false
alias Beacon.Loader

def module_name(site), do: Loader.module_name(site, "InfoHandlers")

def build_ast(site, info_handlers) do
module = module_name(site)
functions = Enum.map(info_handlers, &build_fn/1)

quote do
defmodule unquote(module) do
import Phoenix.LiveView
import Phoenix.Component, except: [assign: 2, assign: 3, assign_new: 3]
import Beacon.Web, only: [assign: 2, assign: 3, assign_new: 3]

(unquote_splicing(functions))
end
end
end

defp build_fn(info_handler) do
%{site: site, msg: msg, code: code} = info_handler
Beacon.safe_code_check!(site, code)

quote do
def handle_info(unquote(Code.string_to_quoted!(msg)), var!(socket)) do
unquote(Code.string_to_quoted!(code))
end
end
end
end
20 changes: 19 additions & 1 deletion lib/beacon/loader/page.ex
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ defmodule Beacon.Loader.Page do

# Group function heads together to avoid compiler warnings
functions = [
for fun <- [&page/1, &page_assigns/1, &handle_event/1, &helper/1] do
for fun <- [&page/1, &page_assigns/1, &handle_event/1, &handle_info/1, &helper/1] do
fun.(page)
end,
render(page),
Expand All @@ -37,9 +37,11 @@ defmodule Beacon.Loader.Page do
import PhoenixHTMLHelpers.Link
import PhoenixHTMLHelpers.Tag
import PhoenixHTMLHelpers.Format
import Phoenix.LiveView
leandrocp marked this conversation as resolved.
Show resolved Hide resolved
import Phoenix.Component, except: [assign: 2, assign: 3, assign_new: 3]
import Beacon.Web, only: [assign: 2, assign: 3, assign_new: 3]
import Beacon.Router, only: [beacon_asset_path: 2, beacon_asset_url: 2]
use Gettext, backend: Beacon.Gettext
import unquote(routes_module)
import unquote(components_module)

Expand Down Expand Up @@ -138,6 +140,22 @@ defmodule Beacon.Loader.Page do
end)
end

defp handle_info(page) do
%{site: site} = page

info_handlers = Content.list_info_handlers(site)

Enum.map(info_handlers, fn info_handler ->
Beacon.safe_code_check!(site, info_handler.code)

quote do
def handle_info(unquote(Code.string_to_quoted!(info_handler.msg)), var!(socket)) do
unquote(Code.string_to_quoted!(info_handler.code))
end
end
end)
end

# TODO: validate fn name and args
def helper(%{site: site, helpers: helpers}) do
Enum.map(helpers, fn helper ->
Expand Down
8 changes: 8 additions & 0 deletions lib/beacon/loader/worker.ex
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,14 @@ defmodule Beacon.Loader.Worker do
end
end

def handle_call(:reload_info_handlers_module, _from, config) do
%{site: site} = config
info_handlers = Content.list_info_handlers(site)
ast = Loader.InfoHandlers.build_ast(site, info_handlers)
result = compile_module(site, ast, "info_handlers")
stop(result, config)
end

def handle_info(msg, config) do
Logger.warning("Beacon.Loader.Worker can not handle the message: #{inspect(msg)}")
{:noreply, config}
Expand Down
11 changes: 11 additions & 0 deletions lib/beacon/migrations/v002.ex
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@ defmodule Beacon.Migrations.V002 do
|> then(&repo().insert_all("beacon_event_handlers", &1, []))

drop_if_exists table(:beacon_page_event_handlers)

create_if_not_exists table(:beacon_info_handlers, primary_key: false) do
add :id, :binary_id, primary_key: true
add :site, :text, null: false
add :msg, :text, null: false
add :code, :text, null: false

timestamps(type: :utc_datetime_usec)
end
end

def down do
Expand All @@ -50,5 +59,7 @@ defmodule Beacon.Migrations.V002 do
# global event handlers can't be converted back into page event handlers

drop_if_exists table(:beacon_event_handlers)

drop_if_exists table(:beacon_info_handlers)
end
end
3 changes: 3 additions & 0 deletions lib/beacon/web/beacon_assigns.ex
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ defmodule Beacon.Web.BeaconAssigns do
private: %{
page_module: nil,
components_module: nil,
info_handlers_module: nil,
event_handlers_module: nil,
live_data_keys: [],
live_path: []
Expand Down Expand Up @@ -66,6 +67,7 @@ defmodule Beacon.Web.BeaconAssigns do
path_params = Beacon.Router.path_params(page.path, path_info)
page_title = Beacon.Web.DataSource.page_title(site, page.id, live_data)
components_module = Beacon.Loader.Components.module_name(site)
info_handlers_module = Beacon.Loader.InfoHandlers.module_name(site)
event_handlers_module = Beacon.Loader.EventHandlers.module_name(site)

%__MODULE__{
Expand All @@ -76,6 +78,7 @@ defmodule Beacon.Web.BeaconAssigns do
private: %{
page_module: page_module,
components_module: components_module,
info_handlers_module: info_handlers_module,
event_handlers_module: event_handlers_module,
live_data_keys: Map.keys(live_data),
live_path: path_info
Expand Down
Loading
Loading