Skip to content

Commit

Permalink
add site-wide handle_info callbacks
Browse files Browse the repository at this point in the history
  • Loading branch information
ddink committed Aug 23, 2024
1 parent ce916b5 commit ff037d9
Show file tree
Hide file tree
Showing 17 changed files with 482 additions and 22 deletions.
2 changes: 2 additions & 0 deletions lib/beacon/boot.ex
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ defmodule Beacon.Boot do
Beacon.Loader.populate_default_error_pages(config.site)
Beacon.Loader.populate_default_home_page(config.site)

Beacon.Loader.reload_site_info_handlers(config.site)

assets = [
Task.Supervisor.async(task_supervisor, fn -> Beacon.Loader.reload_runtime_js(config.site) end),
Task.Supervisor.async(task_supervisor, fn -> Beacon.Loader.reload_runtime_css(config.site) end)
Expand Down
181 changes: 181 additions & 0 deletions lib/beacon/content.ex
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ defmodule Beacon.Content do
alias Beacon.Content.PageField
alias Beacon.Content.PageSnapshot
alias Beacon.Content.PageVariant
alias Beacon.Content.SiteInfoHandler
alias Beacon.Content.Snippets
alias Beacon.Content.Stylesheet
alias Beacon.Lifecycle
Expand Down Expand Up @@ -3931,6 +3932,186 @@ defmodule Beacon.Content do
repo(site).delete(live_data_assign)
end

@doc """
Creates a new SiteInfoHandler for creating site-wide handle_info callbacks.
## Example
iex> create_site_info_handler(%{site: "my_site", msg: "{:new_msg, arg}", code: "{:noreply, socket}"})
{:ok, %SiteInfoHandler{}}
"""
@doc type: :site_info_handlers
@spec create_site_info_handler(map()) :: {:ok, SiteInfoHandler.t()} | {:error, Changeset.t()}
def create_site_info_handler(attrs) do
variable_names =
attrs
|> ExUtils.Map.atomize_keys()
|> Map.get(:msg)
|> list_variable_names()

changeset =
%SiteInfoHandler{}
|> SiteInfoHandler.changeset(attrs)
|> validate_site_info_handler(variable_names)

site = Changeset.get_field(changeset, :site)

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

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

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

@spec list_variable_names(String.t()) :: [String.t()]
defp list_variable_names(msg) do
{_msg, variables} =
msg
|> String.replace("{", "")
|> String.replace("}", "")
|> String.split(", ")
|> List.pop_at(0)

variables
end

@doc """
Creates a SiteInfoHandler, raising an error if unsuccessful.
Returns the new SiteInfoHandler if successful, otherwise raises a `RuntimeError`.
## Example
iex> create_site_info_handler!(%{site: "my_site", msg: "{:new_msg, arg}", code: "{:noreply, socket}"})
%SiteInfoHandler{}
"""
@doc type: :site_info_handlers
@spec create_site_info_handler!(map()) :: SiteInfoHandler.t()
def create_site_info_handler!(attrs \\ %{}) do
case create_site_info_handler(attrs) do
{:ok, info_handler} -> info_handler
{:error, changeset} -> raise "failed to create site info handler, got: #{inspect(changeset.errors)}"
end
end

@doc """
Returns an `%Ecto.Changeset{}` for tracking SiteInfoHandler changes.
## Example
iex> change_site_info_handler(info_handler, %{code: {:noreply, socket}})
%Ecto.Changeset{data: %SiteInfoHandler{}}
"""
@doc type: :site_info_handlers
@spec change_site_info_handler(SiteInfoHandler.t(), map()) :: Changeset.t()
def change_site_info_handler(%SiteInfoHandler{} = info_handler, attrs \\ %{}) do
SiteInfoHandler.changeset(info_handler, attrs)
end

@doc """
Gets a single SiteInfoHandler by `id`.
## Example
iex> get_single_info_handler(:my_site, "fefebbfe-f732-4119-9116-d031d04f5a2c")
%SiteInfoHandler{}
"""
@doc type: :site_info_handlers
@spec get_site_info_handler(Site.t(), UUID.t()) :: SiteInfoHandler.t() | nil
def get_site_info_handler(site, id) when is_atom(site) and is_binary(id) do
repo(site).get(SiteInfoHandler, id)
end

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

@doc """
Lists all SiteInfoHandlers for a given site.
## Example
iex> list_site_info_handlers()
"""
@doc type: :site_info_handlers
@spec list_site_info_handlers(Site.t()) :: [SiteInfoHandler.t()]
def list_site_info_handlers(site) do
repo(site).all(
from h in SiteInfoHandler,
where: h.site == ^site,
order_by: [asc: h.inserted_at]
)
end

@doc """
Updates a SiteInfoHandler.
## Example
iex> update_site_info_handler(info_handler, %{msg: "{:new_msg, arg}"})
{:ok, %SiteInfoHandler{}}
"""
@doc type: :site_info_handlers
@spec update_site_info_handler(SiteInfoHandler.t(), map()) :: {:ok, SiteInfoHandler.t()}
def update_site_info_handler(%SiteInfoHandler{} = info_handler, attrs) do
variable_names =
info_handler
|> retrieve_msg(attrs)
|> list_variable_names()

changeset =
info_handler
|> SiteInfoHandler.changeset(attrs)
|> validate_site_info_handler(variable_names, ["Phoenix.Component"])

site = Changeset.get_field(changeset, :site)

changeset
|> repo(site).update()
|> tap(&maybe_broadcast_updated_content_event(&1, :site_info_handler))
end

@spec retrieve_msg(SiteInfoHandler.t(), map()) :: String.t()
defp retrieve_msg(info_handler, attrs) do
attrs = ExUtils.Map.atomize_keys(attrs)

case Map.get(attrs, :msg) do
nil ->
info_handler.msg

msg ->
msg
end
end

@doc """
Deletes SiteInfoHandler.
"""
@doc type: :site_info_handlers
@spec delete_site_info_handler(SiteInfoHandler.t()) :: {:ok, SiteInfoHandler.t()} | {:error, Changeset.t()}
def delete_site_info_handler(info_handler) do
repo(info_handler.site).delete(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/site_info_handler.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
defmodule Beacon.Content.SiteInfoHandler 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 site 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_site_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
10 changes: 10 additions & 0 deletions lib/beacon/loader.ex
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,10 @@ defmodule Beacon.Loader do
GenServer.call(worker(site), {:unload_page_module, page_id}, @timeout)
end

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

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

def handle_info({:content_updated, :site_info_handler, %{site: site}}, config) do
reload_site_info_handlers(site)
reload_runtime_css(site)
{:noreply, config}
end

def handle_info(msg, config) do
raise inspect(msg)
Logger.warning("Beacon.Loader can not handle the message: #{inspect(msg)}")
Expand Down
20 changes: 19 additions & 1 deletion lib/beacon/loader/page.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,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 @@ -36,9 +36,11 @@ defmodule Beacon.Loader.Page do
import PhoenixHTMLHelpers.Link
import PhoenixHTMLHelpers.Tag
import PhoenixHTMLHelpers.Format
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]
import Beacon.Router, only: [beacon_asset_path: 2, beacon_asset_url: 2]
import Beacon.Web.Gettext
import unquote(routes_module)
import unquote(components_module)

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

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

site_info_handlers = Beacon.Content.list_site_info_handlers(site)

Enum.map(site_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
4 changes: 4 additions & 0 deletions lib/beacon/loader/worker.ex
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,10 @@ defmodule Beacon.Loader.Worker do
end
end

def handle_call(:reload_site_info_handlers, _from, config) do
stop(Beacon.Content.list_site_info_handlers(config.site), config)
end

def handle_info(msg, config) do
Logger.warning("Beacon.Loader.Worker can not handle the message: #{inspect(msg)}")
{:noreply, config}
Expand Down
10 changes: 10 additions & 0 deletions lib/beacon/migrations/v001.ex
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,15 @@ defmodule Beacon.Migrations.V001 do

create_if_not_exists index(:beacon_live_data_assigns, [:live_data_id])

create_if_not_exists table(:beacon_site_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

create_if_not_exists table(:beacon_snippet_helpers, primary_key: false) do
add :id, :binary_id, primary_key: true
add :site, :text, null: false
Expand Down Expand Up @@ -250,6 +259,7 @@ defmodule Beacon.Migrations.V001 do
drop_if_exists table(:beacon_assets)
drop_if_exists table(:beacon_stylesheets)
drop_if_exists table(:beacon_live_data_assigns)
drop_if_exists table(:beacon_site_info_handlers)
drop_if_exists table(:beacon_live_data)
drop_if_exists table(:beacon_snippet_helpers)
drop_if_exists table(:beacon_component_attrs)
Expand Down
11 changes: 0 additions & 11 deletions lib/beacon/web/components/layouts/app.html.heex
Original file line number Diff line number Diff line change
@@ -1,16 +1,5 @@
<main class="px-4 py-20 sm:px-6 lg:px-8">
<div class="mx-auto max-w-6xl">
<Beacon.Web.CoreComponents.flash kind={:info} title="Success!" flash={@flash} />
<Beacon.Web.CoreComponents.flash kind={:error} title="Error!" flash={@flash} />
<Beacon.Web.CoreComponents.flash
id="disconnected"
kind={:error}
title="We can't find the internet"
phx-disconnected={Beacon.Web.CoreComponents.show("#disconnected")}
phx-connected={Beacon.Web.CoreComponents.hide("#disconnected")}
>
Attempting to reconnect <Beacon.Web.CoreComponents.icon name="hero-arrow-path" class="ml-1 w-3 h-3 inline animate-spin" />
</Beacon.Web.CoreComponents.flash>
<%= @inner_content %>
</div>
</main>
1 change: 1 addition & 0 deletions lib/beacon/web/components/layouts/dynamic.html.heex
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
<Beacon.Web.CoreComponents.flash_group id="flash_group" flash={@flash} />
<%= render_dynamic_layout(assigns) %>
Loading

0 comments on commit ff037d9

Please sign in to comment.