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

Implement page compilation on demand #330

Merged
merged 13 commits into from
Aug 21, 2023
4 changes: 4 additions & 0 deletions dev.exs
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,10 @@ seeds = fn ->
<p>From dynamic_helper:</p>
<%= dynamic_helper("upcase", %{name: "beacon"}) %>
</div>

<div>
<p>RANDOM:<%= Enum.random(1..100) %></p>
</div>
</main>
""",
helpers: [
Expand Down
3 changes: 1 addition & 2 deletions lib/beacon/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ defmodule Beacon.Application do
Beacon.Repo
]

# We store routes by order and length so the most visited pages will likely be in the first rows
:ets.new(:beacon_pages, [:ordered_set, :named_table, :public, read_concurrency: true])
Beacon.Router.init()

:ets.new(:beacon_assets, [:set, :named_table, :public, read_concurrency: true])

Expand Down
36 changes: 12 additions & 24 deletions lib/beacon/config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,8 @@ defmodule Beacon.Config do
[
{identifier :: atom(),
fun ::
(Beacon.Template.t(), Beacon.Template.LoadMetadata.t() ->
{:cont, Beacon.Template.t()} | {:halt, Beacon.Template.t()} | {:halt, Exception.t()})}
(template :: String.t(), Beacon.Template.LoadMetadata.t() ->
{:cont, String.t() | Beacon.Template.t()} | {:halt, String.t() | Beacon.Template.t()} | {:halt, Exception.t()})}
]}
]}
| {:render_template,
Expand Down Expand Up @@ -168,14 +168,8 @@ defmodule Beacon.Config do
]

@default_render_template [
{:heex,
[
eval_heex_ast: &Beacon.Template.HEEx.eval_ast/2
]},
{:markdown,
[
eval_heex_ast: &Beacon.Template.HEEx.eval_ast/2
]}
{:heex, []},
{:markdown, []}
]

@default_media_types ["image/jpeg", "image/gif", "image/png", "image/webp", ".pdf"]
Expand Down Expand Up @@ -283,15 +277,14 @@ defmodule Beacon.Config do
load_template: [
{:custom_format,
[
validate: fn template, _metadata -> MyEngine.validate(template) end
validate: fn template, _metadata -> MyEngine.validate(template) end,
build_rendered: fn template, _metadata -> %Phoenix.LiveView.Rendered{static: template} end,
]}
],
render_template: [
{:custom_format,
[
assigns: fn template, %{assigns: assigns} -> MyEngine.parse_to_html(template, assigns) end,
compile: &Beacon.Template.HEEx.compile/2,
eval: &Beacon.Template.HEEx.eval_ast/2
assigns: fn %Phoenix.LiveView.Rendered{static: template} , %{assigns: assigns} -> MyEngine.parse_to_html(template, assigns) end
]}
],
after_publish_page: [
Expand Down Expand Up @@ -329,20 +322,15 @@ defmodule Beacon.Config do
compile_heex: &Beacon.Template.HEEx.compile/2
],
custom_format: [
validate: #Function<41.3316493/2 in :erl_eval.expr/6>
validate: #Function<41.3316493/2 in :erl_eval.expr/6>,
build_rendered: #Function<41.3316494/2 in :erl_eval.expr/6>
]
],
render_template: [
heex: [
eval_heex_ast: &Beacon.Template.HEEx.eval_ast/2
],
markdown: [
eval_heex_ast: &Beacon.Template.HEEx.eval_ast/2
],
heex: [],
markdown: [],
custom_format: [
assigns: #Function<41.3316493/2 in :erl_eval.expr/6>,
compile: &Beacon.Template.HEEx.compile/2,
eval: &Beacon.Template.HEEx.eval_ast/2
assigns: #Function<41.3316493/2 in :erl_eval.expr/6>
]
],
after_create_page: [],
Expand Down
18 changes: 15 additions & 3 deletions lib/beacon/lifecycle/template.ex
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,22 @@ defmodule Beacon.Lifecycle.Template do
@doc """
Render a `page` template using the registered format used on the `page`.

This stage runs in the render callback of the LiveView responsible for displaying the page.
## Notes

- This stage runs in the render callback of the LiveView responsible for displaying the page.
- It will load and compile the page module if it wasn't not loaded yet.

"""
def render_template(site, template, format, context) do
lifecycle = Lifecycle.execute(__MODULE__, site, :render_template, template, sub_key: format, context: context)
@spec render_template(Beacon.Content.Page.t(), module(), map(), Macro.Env.t()) :: Beacon.Template.t()
def render_template(page, page_module, assigns, env) do
template =
case page_module.render(assigns) do
%Phoenix.LiveView.Rendered{} = rendered -> rendered
:not_loaded -> Beacon.Loader.PageModuleLoader.load_page_template!(page, page_module, assigns)
end

context = [path: page.path, assigns: assigns, env: env]
lifecycle = Lifecycle.execute(__MODULE__, page.site, :render_template, template, sub_key: page.format, context: context)
lifecycle.output
end
end
30 changes: 16 additions & 14 deletions lib/beacon/loader.ex
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ defmodule Beacon.Loader do
def load_page(%Content.Page{} = page) do
page = Repo.preload(page, :event_handlers)
config = Beacon.Config.fetch!(page.site)
GenServer.call(name(config.site), {:load_page, page}, 60_000)
GenServer.call(name(config.site), {:load_page, page}, 30_000)
end

@doc false
Expand Down Expand Up @@ -172,7 +172,7 @@ defmodule Beacon.Loader do
|> Content.list_published_pages()
|> Enum.map(fn page ->
Task.async(fn ->
{:ok, _ast} = Beacon.Loader.PageModuleLoader.load_page!(page)
{:ok, _module, _ast} = Beacon.Loader.PageModuleLoader.load_page!(page)
:ok
end)
end)
Expand All @@ -197,20 +197,22 @@ defmodule Beacon.Loader do
end

@doc false
def layout_module_for_site(site, layout_id) do
prefix = Macro.camelize("layout_#{layout_id}")
module_for_site(site, prefix)
def layout_module_for_site(layout_id) do
module_for_site("Layout#{layout_id}")
end

@doc false
def page_module_for_site(site, page_id) do
prefix = Macro.camelize("page_#{page_id}")
module_for_site(site, prefix)
def page_module_for_site(page_id) do
module_for_site("Page#{page_id}")
end

defp module_for_site(site, prefix) do
site_hash = :crypto.hash(:md5, Atom.to_string(site)) |> Base.encode16()
Module.concat([BeaconWeb.LiveRenderer, "#{prefix}#{site_hash}"])
defp module_for_site(resource) do
Module.concat([BeaconWeb.LiveRenderer, resource])
end

defp module_for_site(site, resource) do
site_hash = :md5 |> :crypto.hash(Atom.to_string(site)) |> Base.encode16()
Module.concat([BeaconWeb.LiveRenderer, "#{site_hash}#{resource}"])
end

# This retry logic exists because a module may be in the process of being reloaded, in which case we want to retry
Expand Down Expand Up @@ -358,8 +360,8 @@ defmodule Beacon.Loader do
:ok <- load_snippet_helpers(page.site),
{:ok, _ast} <- Beacon.Loader.LayoutModuleLoader.load_layout!(layout),
:ok <- load_stylesheets(page.site),
{:ok, _ast} <- Beacon.Loader.PageModuleLoader.load_page!(page) do
:ok = Beacon.PubSub.page_loaded(page)
{:ok, _module, _ast} <- Beacon.Loader.PageModuleLoader.load_page!(page),
:ok <- Beacon.PubSub.page_loaded(page) do
:ok
else
_ -> raise Beacon.LoaderError, message: "failed to load resources for page #{page.title} of site #{page.site}"
Expand All @@ -368,7 +370,7 @@ defmodule Beacon.Loader do

@doc false
def do_unload_page(page) do
module = page_module_for_site(page.site, page.id)
module = page_module_for_site(page.id)
:code.delete(module)
:code.purge(module)
Beacon.Router.del_page(page.site, page.path)
Expand Down
2 changes: 1 addition & 1 deletion lib/beacon/loader/layout_module_loader.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ defmodule Beacon.Loader.LayoutModuleLoader do

def load_layout!(%Content.Layout{} = layout) do
component_module = Loader.component_module_for_site(layout.site)
module = Loader.layout_module_for_site(layout.site, layout.id)
module = Loader.layout_module_for_site(layout.id)
render_function = render_layout(layout)
ast = render(module, render_function, component_module)
:ok = Loader.reload_module!(module, ast)
Expand Down
Loading