Call an event and use its result
v0.1.14
Calling an event runs every active plugin attached to it, in order, and returns what they produced. This guide shows how to call an event, pass context to the plugins, keep your own input, choose the answer for an event whose plugins cannot run, ask whether an event has plugins, and read the result without trusting its shape.
Before you begin
- Add
{:mishka_installer, "~> 0.1.14"}to the project that fires the event. - Pick an event name, a string. Plugins attach to it with
use MishkaInstaller.Event.Hook, event: "...". See Write a plugin for an event.
Call an event
Call Hook.call/3 with the event name, the data, and an optional keyword list of arguments:
alias MishkaInstaller.Event.Hook
Hook.call("after_register", %{email: "[email protected]"})The call returns the result of the plugin chain. When the event has no compiled chain on this node, because no plugin has ever started for it, the call returns the data you gave it.
Hook.call!/3 takes the same arguments but skips the check for a compiled chain. It raises UndefinedFunctionError for an event that was never compiled. Use it only where you know a plugin is attached.
The plugins run in the process that makes the call, one after another. A slow plugin delays the caller.
Read the result without trusting its shape
You get your own data back when no plugin is attached, and a plugin may return a different shape from the one you sent. Match the shape you need and fall back when it is missing. This is how Mishka CMS collects the cards of the account security tab:
added =
case MishkaInstaller.Event.Hook.call(@event, %{cards: @core}) do
%{cards: cards} when is_list(cards) -> Enum.filter(cards, &card?/1)
_nothing -> []
endPass read-only context with meta
The meta argument reaches the plugins that define call/2 as their second argument. It is never merged into the result. Plugins that define only call/1 do not see it.
Hook.call("web:request:after_site_resolve", conn, meta: %{host: host})This line is from the site lookup plug of Mishka CMS. A plugin that wants the host defines call(conn, meta) and reads meta.host.
Merge fixed data with private
The private argument is merged into the result after the plugins have run, so no plugin can overwrite it. It merges into a map with Map.merge/2, and into a keyword list with Keyword.merge/2. Give it the same shape as the result. A map or a keyword list inside {:ok, _} is merged too. For any other result shape, private is ignored.
Hook.call("transform_evt", %{n: 5}, private: %{tag: "x"})
# %{n: 12, tag: "x"} when the plugins turn n into 12Keep your input with return
When you pass return: with any value other than nil, the plugins still run, but the call returns the data exactly as you gave it. Use it for events that only announce something. Mishka CMS announces a saved record this way:
MishkaInstaller.Event.Hook.call(event_name(resource, action.name), record,
meta: meta,
return: false
)The library tests the option only against nil, so return: false gives the same result as return: true: the input comes back.
Choose the answer when plugins cannot run
An event cannot run when one of its plugins is started but its module is not loaded. Then no plugin of that event runs. The call answers the value of cannot_run:, or your data when you pass none. An event whose plugins all run ignores the option.
Before 0.1.13 such a call answered {:error, [%{action: :call, field: :event, plugins: [...]}]}. The changes documentation shows how to update code that matched that error.
Pass cannot_run: when "no plugin ran" must not look like "no plugin objected". A decision that plugins take part in should refuse:
Hook.call("auth:gate:check", gate, cannot_run: %{gate | allowed?: false})Mishka CMS asks which producers hold share cards the same way, and turns the answer into an error it can report:
case Hook.call(@targets_event, asked, cannot_run: :cannot_run) do
%{targets: %{} = targets} ->
{:ok, Map.filter(targets, fn {name, list} -> is_binary(name) and is_list(list) end)}
:cannot_run ->
{:error, :cannot_run}
_not_answered ->
{:ok, %{}}
endTo see whether an event is in this state, read mode from Hook.event_health/2, or mode/0 of the compiled module as the next part shows. It is :error for an event that cannot run. Each time the library compiles such an event, it also logs cannot run with the names of the plugins it waits for.
Ask whether an event has plugins
Firing an event only to learn whether anything listens runs the plugins, and a plugin may write something. Read the registry or the compiled chain instead. Neither runs a plugin.
Event.get(:event, name) reads the registry. It returns every plugin record of the event, whatever its status, and [] when there are none. A :stopped or :held plugin is in the list, but it is not in the chain.
The compiled module answers what Hook.call/3 would run on this node. Its initialize/0 returns the chain, and its mode/0 says whether the chain can run:
alias MishkaInstaller.Event.ModuleStateCompiler
def listening?(event) do
module = ModuleStateCompiler.module_event_name(event)
Code.ensure_loaded?(module) and function_exported?(module, :initialize, 0) and
module.mode() == :ok and module.initialize().plugins != []
endAn event that was never compiled has no module, so the function answers false. An event that cannot run still lists its plugins in initialize/0, which is why the function reads mode/0 as well. Mishka CMS asks this way, without the mode/0 test, to learn whether anything is registered to store the submissions of a page form.
Hook.event_health/2 answers more. Its map has compiled?, mode and plugin_count, and it runs the health probe of every plugin, which can take as long as the probe timeout of one second. Use it for a dashboard, not for a check on every request.
What a failing plugin does
A plugin that raises, or returns something other than {:reply, state} or {:reply, :halt, state}, ends the call. The library logs plugin pipeline raised in event and returns the data you gave it. The work of the plugins that ran before the failure is not in the result. Design the caller so that unchanged data is a safe answer.
Next steps
- Hook reference: every argument and the full table of result shapes.
- How events and plugins work: how the chain is built and run.
- Event reference: the registry functions, the compiled module and the errors.
- Manage plugins while the app runs: check health and profile a chain.