Lifecycle hook helpers for Musubi runtime stages.
Stages
| Stage | Arity | Hook arguments |
|---|---|---|
:before_command | 3 | (command_name, payload, socket) |
:after_command | 4 | (command_name, payload, reply, socket) |
:handle_async | 3 | (name, async_result, socket) |
:handle_info | 2 | (message, socket) |
:after_render | 2 | (frame, socket) |
:after_serialize | 2 | (frame, socket) |
:after_render and :after_serialize are the two outbound stages: they run
per store socket each render cycle over that socket's Musubi.Page.Frame — its
render output plus the push events (Musubi.Event) it queued.
:after_render sees the Elixir-form frame (atom keys, structs, atom values,
native event payloads); :after_serialize sees the wire-form frame after
Musubi.Wire.to_wire/1 (string keys, plain maps, atoms-as-strings).
Both are transform stages — run them with run_transform_hooks/3, and each
hook returns {:cont | :halt, frame, socket} so it may rewrite, drop, or enrich
the outbound frame (render redaction, event audit/validation, telemetry).
Default render and push-event payload validation are attached at
:after_serialize.
Summary
Functions
Attaches the application's configured default hooks
(config :musubi, :default_hooks, {id, stage, fun} entries) to a socket.
Attaches a lifecycle hook for the given stage.
Detaches a lifecycle hook when one is present.
Runs every socket-only hook registered for a stage until one halts or all
continue. For the transform stages (:after_render, :after_serialize) use
run_transform_hooks/3 instead — they thread a data frame, not just the socket.
Runs a transform stage (:after_render / :after_serialize) over an outbound
datum (a Musubi.Page.Frame).
Returns the required hook function arity for a lifecycle stage.
Returns the supported lifecycle stages in execution order.
Types
@type hook_fun() :: function()
@type hook_id() :: term()
@type hook_result() :: {:cont, Musubi.Socket.t()} | {:halt, Musubi.Socket.t()} | {:halt, term(), Musubi.Socket.t()}
@type hook_table() :: %{optional(stage()) => [hook_entry()]}
@type stage() ::
:before_command
| :after_command
| :handle_async
| :handle_info
| :after_render
| :after_serialize
Functions
@spec attach_default_hooks(Musubi.Socket.t()) :: Musubi.Socket.t()
Attaches the application's configured default hooks
(config :musubi, :default_hooks, {id, stage, fun} entries) to a socket.
Applied to the root socket at mount and to every child socket at creation, so
each store runs the validators. Hooks self-scope: command/reply/event
validators skip what the store does not declare, and ValidateRender skips
non-root sockets.
@spec attach_hook(Musubi.Socket.t(), hook_id(), stage(), hook_fun()) :: Musubi.Socket.t()
Attaches a lifecycle hook for the given stage.
Examples
iex> socket = %Musubi.Socket{}
iex> socket =
...> Musubi.Lifecycle.attach_hook(socket, :audit, :after_render, fn _output, socket ->
...> {:cont, socket}
...> end)
iex> Musubi.Socket.get_private(socket, :hooks)[:after_render] |> length()
1
@spec detach_hook(Musubi.Socket.t(), hook_id(), stage()) :: Musubi.Socket.t()
Detaches a lifecycle hook when one is present.
Examples
iex> socket =
...> Musubi.Lifecycle.attach_hook(%Musubi.Socket{}, :audit, :after_render, fn _output, socket ->
...> {:cont, socket}
...> end)
iex> socket = Musubi.Lifecycle.detach_hook(socket, :audit, :after_render)
iex> Musubi.Socket.get_private(socket, :hooks)
%{}
@spec run_hooks(Musubi.Socket.t(), stage(), list(), boolean()) :: {:cont, Musubi.Socket.t()} | {:halt, Musubi.Socket.t()} | {:halt, term(), Musubi.Socket.t()}
Runs every socket-only hook registered for a stage until one halts or all
continue. For the transform stages (:after_render, :after_serialize) use
run_transform_hooks/3 instead — they thread a data frame, not just the socket.
Examples
iex> socket =
...> Musubi.Lifecycle.attach_hook(%Musubi.Socket{}, :mark, :handle_info, fn _msg, socket ->
...> {:cont, Musubi.Socket.assign(socket, :seen?, true)}
...> end)
iex> {:cont, socket} = Musubi.Lifecycle.run_hooks(socket, :handle_info, [:ping], false)
iex> socket.assigns.seen?
true
@spec run_transform_hooks(Musubi.Socket.t(), stage(), term()) :: {term(), Musubi.Socket.t()}
Runs a transform stage (:after_render / :after_serialize) over an outbound
datum (a Musubi.Page.Frame).
Unlike run_hooks/4 (which only threads the socket), this folds the datum
through each hook: a hook receives (datum, socket) and returns
{:cont, datum, socket} to continue with a possibly-rewritten datum, or
{:halt, datum, socket} to stop the chain early. Returns the final
{datum, socket}. With no hooks attached it returns the datum unchanged.
Examples
iex> socket =
...> Musubi.Lifecycle.attach_hook(%Musubi.Socket{}, :bump, :after_render, fn frame, socket ->
...> {:cont, Map.put(frame, :seen, true), socket}
...> end)
iex> {frame, _socket} = Musubi.Lifecycle.run_transform_hooks(socket, :after_render, %{render: %{}})
iex> frame.seen
true
@spec stage_arity(stage()) :: 2 | 3 | 4
Returns the required hook function arity for a lifecycle stage.
| Stage | Arity | Hook arguments |
|---|---|---|
:before_command | 3 | (command_name, payload, socket) |
:after_command | 4 | (command_name, payload, reply, socket) |
:handle_async | 3 | (name, async_result, socket) |
:handle_info | 2 | (message, socket) |
:after_render | 2 | (frame, socket) |
:after_serialize | 2 | (frame, socket) |
Examples
iex> Musubi.Lifecycle.stage_arity(:before_command)
3
iex> Musubi.Lifecycle.stage_arity(:after_command)
4
iex> Musubi.Lifecycle.stage_arity(:after_serialize)
2
@spec stages() :: [stage()]
Returns the supported lifecycle stages in execution order.
Examples
iex> Musubi.Lifecycle.stages()
[:before_command, :after_command, :handle_async, :handle_info, :after_render, :after_serialize]