forked from dynamo/dynamo
-
Notifications
You must be signed in to change notification settings - Fork 1
/
renderer.ex
127 lines (103 loc) · 3.14 KB
/
renderer.ex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
defmodule Dynamo.Views.Renderer do
@moduledoc false
@slots 1_000_000
@max_attempts 1_000
use GenServer.Behaviour
alias Dynamo.Views.Template, as: Template
@doc """
Starts the `Dynamo.Views.Renderer` server.
Usually called internally by Dynamo.
"""
def start_link do
:gen_server.start({ :local, __MODULE__ }, __MODULE__, [], [])
end
@doc """
Stops the `Dynamo.Views.Renderer` server.
"""
def stop do
:gen_server.call(__MODULE__, :stop)
end
@doc """
This function is responsible for rendering the templates.
It supports both pre-compiled and on demand compilation.
The on demand mode needs to be explicitly enabled
by calling start_link/0.
"""
def render(Template[ref: { mod, fun }], assigns) do
apply mod, fun, [assigns]
end
def render(template, assigns) do
module =
get_cached(template) || compile(template) || raise_too_busy(template)
module.render(assigns)
end
## Callbacks
@doc false
def init(args) do
{ :ok, Binary.Dict.new(args) }
end
@doc false
def handle_call({ :get_cached, identifier, updated_at }, _from, dict) do
case Dict.get(dict, identifier) do
{ module, cached } when updated_at > cached ->
spawn fn ->
:code.purge(module)
:code.delete(module)
end
{ :reply, nil, Dict.delete(dict, identifier) }
{ module, _ } ->
{ :reply, module, dict }
nil ->
{ :reply, nil, dict }
end
end
def handle_call({ :register, identifier, updated_at, compiled }, _from, dict) do
if module = generate_module(compiled, identifier, 0) do
{ :reply, module, Dict.put(dict, identifier, { module, updated_at }) }
else
{ :reply, nil, dict }
end
end
def handle_call(:stop, _from, state) do
{ :stop, :normal, :ok, state }
end
def handle_call(_arg, _from, _config) do
super
end
## Helpers
defp get_cached(Template[identifier: identifier, updated_at: updated_at]) do
:gen_server.call(__MODULE__, { :get_cached, identifier, updated_at })
end
defp compile(Template[handler: handler, identifier: identifier, updated_at: updated_at] = template) do
compiled = get_handler(handler).compile(template)
:gen_server.call(__MODULE__, { :register, identifier, updated_at, compiled })
end
defp raise_too_busy(Template[identifier: identifier]) do
raise "Compiling template #{inspect identifier} exceeded the max number of attempts #{@max_attemps}. What gives?"
end
# TODO: Remove hardcoded handler.
defp get_handler("eex") do
Dynamo.Views.EEXHandler
end
defp generate_module(source, identifier, attempts) when attempts < @max_attemps do
random = :random.uniform(@slots)
module = Module.concat(Dynamo.Views, "Template#{random}")
if :code.is_loaded(module) do
generate_module(source, identifier, attempts + 1)
else
source = quote hygiene: false do
_ = assigns
unquote(source)
end
defmodule module do
@file identifier
args = quote hygiene: false, do: [assigns]
def :render, args, [], do: source
end
module
end
end
defp generate_module(_, _, _) do
nil
end
end