Musubi.Lifecycle (musubi v0.13.0)

Copy Markdown View Source

Lifecycle hook helpers for Musubi runtime stages.

Stages

StageArityHook arguments
:before_command3(command_name, payload, socket)
:after_command4(command_name, payload, reply, socket)
:handle_async3(name, async_result, socket)
:handle_info2(message, socket)
:after_render2(frame, socket)
:after_serialize2(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

hook_entry()

@type hook_entry() :: %{id: hook_id(), fun: hook_fun()}

hook_fun()

@type hook_fun() :: function()

hook_id()

@type hook_id() :: term()

hook_result()

@type hook_result() ::
  {:cont, Musubi.Socket.t()}
  | {:halt, Musubi.Socket.t()}
  | {:halt, term(), Musubi.Socket.t()}

hook_table()

@type hook_table() :: %{optional(stage()) => [hook_entry()]}

stage()

@type stage() ::
  :before_command
  | :after_command
  | :handle_async
  | :handle_info
  | :after_render
  | :after_serialize

Functions

attach_default_hooks(socket)

@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.

attach_hook(socket, id, stage, fun)

@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

detach_hook(socket, id, stage)

@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)
%{}

run_hooks(socket, stage, hook_args, halt_payloads_allowed?)

@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

run_transform_hooks(socket, stage, datum)

@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

stage_arity(stage)

@spec stage_arity(stage()) :: 2 | 3 | 4

Returns the required hook function arity for a lifecycle stage.

StageArityHook arguments
:before_command3(command_name, payload, socket)
:after_command4(command_name, payload, reply, socket)
:handle_async3(name, async_result, socket)
:handle_info2(message, socket)
:after_render2(frame, socket)
:after_serialize2(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

stages()

@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]