Musubi.Socket behaviour (musubi v0.3.0)

Copy Markdown View Source

Socket struct and assign helpers for Musubi runtimes.

Summary

Types

Keys written into the assigns map.

Application-level denial reasons returned by socket callbacks.

Return shape for Musubi socket lifecycle callbacks.

Phoenix connect_info data captured when the Musubi socket connects.

Parameters supplied when the transport socket connects.

Parameters supplied when a Musubi connection joins.

Path segments identifying a store's parent path.

The private bookkeeping map carried by the socket.

Client params supplied for the current root store mount.

Root store modules that may be mounted through this socket.

Session data shared by all root stores on one Musubi socket.

Runtime identity of a store node — array of local ids from root.

t()

Callbacks

Runs once when the transport socket connects.

Runs once when a Musubi connection joins.

Functions

Declares a Musubi socket module.

Returns whether any assign key is marked as changed since the last render cycle.

Assigns many keys from a keyword list or map.

Assigns one key on the socket and records the change in __changed__.

Assigns a value to key only if key is not already present in the socket's assigns. fun is invoked lazily and receives no arguments.

Returns whether the given assign key is marked as changed.

Returns Phoenix connect_info data captured when the Musubi socket connected.

Returns whether any consumed key appears in the socket's __changed__ map.

Fetches a declared root store module by its client module string.

Reads a private runtime value.

Copies shared Musubi context from one socket to another.

Stores Phoenix connect_info data on the Musubi socket.

Writes a private runtime value.

Stores the current root store mount params in socket private context.

Stores session data shared by all root stores on one Musubi socket.

Clears the LiveView-style __changed__ bookkeeping after a render cycle.

Returns the params supplied when the current root store was mounted.

Returns the session data shared by all root stores on one Musubi socket.

Returns the runtime identity (store_id) of the store node owning this socket.

Updates one assign by applying fun to the current value.

Types

assign_key()

@type assign_key() :: term()

Keys written into the assigns map.

callback_error_reason()

@type callback_error_reason() ::
  :unauthorized | :not_found | :invalid_params | :forbidden

Application-level denial reasons returned by socket callbacks.

callback_result()

@type callback_result() :: {:ok, t()} | :error | {:error, callback_error_reason()}

Return shape for Musubi socket lifecycle callbacks.

connect_info()

@type connect_info() :: map()

Phoenix connect_info data captured when the Musubi socket connects.

connect_params()

@type connect_params() :: map()

Parameters supplied when the transport socket connects.

join_params()

@type join_params() :: map()

Parameters supplied when a Musubi connection joins.

path_segment()

@type path_segment() :: atom() | String.t()

Path segments identifying a store's parent path.

private_key()

@type private_key() :: term()

The private bookkeeping map carried by the socket.

root_params()

@type root_params() :: map()

Client params supplied for the current root store mount.

roots()

@type roots() :: [module()]

Root store modules that may be mounted through this socket.

session()

@type session() :: map()

Session data shared by all root stores on one Musubi socket.

store_id()

@type store_id() :: [String.t()]

Runtime identity of a store node — array of local ids from root.

t()

@type t() :: %Musubi.Socket{
  assigns: map(),
  endpoint: module() | nil,
  id: String.t() | nil,
  module: module() | nil,
  parent_path: [path_segment()],
  private: map(),
  topic: String.t() | nil,
  transport_pid: pid() | nil
}

Callbacks

handle_connect(params, socket)

@callback handle_connect(params :: connect_params(), socket :: t()) :: callback_result()

Runs once when the transport socket connects.

Use this callback for connection-level authentication and assigns that should be visible to every Musubi connection and mounted root store.

handle_join(params, socket)

@callback handle_join(params :: join_params(), socket :: t()) :: callback_result()

Runs once when a Musubi connection joins.

Use this callback for connection-level scope checks such as workspace or tenant authorization. It does not receive root store module or id data; those are validated later when the root store is mounted.

Functions

__using__(opts)

(macro)
@spec __using__(keyword()) :: Macro.t()

Declares a Musubi socket module.

The generated module is a Phoenix socket adapter internally, but application code implements only Musubi callbacks.

Examples

defmodule MyAppWeb.UserSocket do
  use Musubi.Socket,
    roots: [
      MyApp.Stores.DashboardStore,
      MyApp.Stores.PollRoomStore
    ]

  @impl Musubi.Socket
  def handle_connect(%{"token" => token}, socket) do
    {:ok, Musubi.Socket.assign(socket, :token, token)}
  end

  @impl Musubi.Socket
  def handle_join(_params, socket), do: {:ok, socket}
end

any_changed?(socket)

@spec any_changed?(t()) :: boolean()

Returns whether any assign key is marked as changed since the last render cycle.

Examples

iex> Musubi.Socket.any_changed?(%Musubi.Socket{})
false
iex> socket = Musubi.Socket.assign(%Musubi.Socket{}, :title, "Inbox")
iex> Musubi.Socket.any_changed?(socket)
true

assign(socket, attrs)

@spec assign(t(), keyword(term()) | map()) :: t()

Assigns many keys from a keyword list or map.

Examples

iex> socket = %Musubi.Socket{}
iex> socket = Musubi.Socket.assign(socket, %{title: "Inbox", count: 2})
iex> socket.assigns.title
"Inbox"
iex> socket.assigns.count
2

assign(socket, key, value)

@spec assign(t(), assign_key(), term()) :: t()

Assigns one key on the socket and records the change in __changed__.

Examples

iex> socket = %Musubi.Socket{}
iex> socket = Musubi.Socket.assign(socket, :title, "Inbox")
iex> socket.assigns.title
"Inbox"
iex> socket.assigns.__changed__
%{title: true}

assign_new(socket, key, fun)

@spec assign_new(t(), assign_key(), (-> term())) :: t()

Assigns a value to key only if key is not already present in the socket's assigns. fun is invoked lazily and receives no arguments.

Useful for setting defaults that should not overwrite parent-supplied values. When key is already present the socket is returned unchanged and the __changed__ map is not touched.

Examples

iex> socket = Musubi.Socket.assign_new(%Musubi.Socket{}, :count, fn -> 0 end)
iex> socket.assigns.count
0
iex> socket = Musubi.Socket.assign_new(socket, :count, fn -> 99 end)
iex> socket.assigns.count
0

changed?(socket, key)

@spec changed?(t(), assign_key()) :: boolean()

Returns whether the given assign key is marked as changed.

Examples

iex> socket = Musubi.Socket.assign(%Musubi.Socket{}, :title, "Inbox")
iex> Musubi.Socket.changed?(socket, :title)
true
iex> Musubi.Socket.changed?(socket, :count)
false

connect_info(socket)

@spec connect_info(t()) :: connect_info()

Returns Phoenix connect_info data captured when the Musubi socket connected.

Examples

iex> Musubi.Socket.connect_info(%Musubi.Socket{})
%{}

consumed_keys_changed?(socket, keys)

@spec consumed_keys_changed?(t(), [assign_key()]) :: boolean()

Returns whether any consumed key appears in the socket's __changed__ map.

Examples

iex> socket = Musubi.Socket.assign(%Musubi.Socket{}, :title, "Inbox")
iex> Musubi.Socket.consumed_keys_changed?(socket, [:title, :count])
true
iex> Musubi.Socket.consumed_keys_changed?(socket, [:count])
false

fetch_root_by_module(socket_module, module_str)

@spec fetch_root_by_module(module(), String.t()) :: {:ok, module()} | :error

Fetches a declared root store module by its client module string.

The string is compared against modules already declared in the socket; Musubi does not convert arbitrary strings into atoms.

Examples

iex> defmodule SocketFetchRootByModuleDoc do
...>   defmodule Store do
...>     use Musubi.Store, root: true
...>
...>     state do
...>       field :ok, boolean()
...>     end
...>
...>     def render(_socket), do: %{ok: true}
...>     def handle_command(_name, _payload, socket), do: {:noreply, socket}
...>   end
...>
...>   use Musubi.Socket, roots: [Store]
...> end
iex> Musubi.Socket.fetch_root_by_module(SocketFetchRootByModuleDoc, "SocketFetchRootByModuleDoc.Store")
{:ok, SocketFetchRootByModuleDoc.Store}
iex> Musubi.Socket.fetch_root_by_module(SocketFetchRootByModuleDoc, "Missing.Store")
:error

get_private(socket, key, default \\ nil)

@spec get_private(t(), private_key(), term()) :: term()

Reads a private runtime value.

Examples

iex> socket = %Musubi.Socket{private: %{hooks: %{}}}
iex> Musubi.Socket.get_private(socket, :hooks)
%{}
iex> Musubi.Socket.get_private(socket, :missing, :fallback)
:fallback

inherit_context(source, target)

@spec inherit_context(t(), t()) :: t()

Copies shared Musubi context from one socket to another.

The copied context includes session and connect_info. Root params are intentionally per-root and are not copied.

Examples

iex> source = Musubi.Socket.put_session(%Musubi.Socket{}, %{"user_id" => "u1"})
iex> target = Musubi.Socket.inherit_context(source, %Musubi.Socket{})
iex> Musubi.Socket.session(target)
%{"user_id" => "u1"}

put_connect_info(socket, connect_info)

@spec put_connect_info(t(), connect_info()) :: t()

Stores Phoenix connect_info data on the Musubi socket.

Examples

iex> socket = Musubi.Socket.put_connect_info(%Musubi.Socket{}, %{peer_data: %{address: {127, 0, 0, 1}}})
iex> Musubi.Socket.connect_info(socket)
%{peer_data: %{address: {127, 0, 0, 1}}}

put_private(socket, key, value)

@spec put_private(t(), private_key(), term()) :: t()

Writes a private runtime value.

Examples

iex> socket = Musubi.Socket.put_private(%Musubi.Socket{}, :hooks, %{})
iex> socket.private.hooks
%{}

put_root_params(socket, params)

@spec put_root_params(t(), root_params()) :: t()

Stores the current root store mount params in socket private context.

Examples

iex> socket = Musubi.Socket.put_root_params(%Musubi.Socket{}, %{"poll_id" => "p1"})
iex> Musubi.Socket.root_params(socket)
%{"poll_id" => "p1"}

put_session(socket, session)

@spec put_session(t(), session()) :: t()

Stores session data shared by all root stores on one Musubi socket.

Examples

iex> socket = Musubi.Socket.put_session(%Musubi.Socket{}, %{"user_id" => "u1"})
iex> Musubi.Socket.session(socket)
%{"user_id" => "u1"}

reset_changed(socket)

@spec reset_changed(t()) :: t()

Clears the LiveView-style __changed__ bookkeeping after a render cycle.

Examples

iex> socket = Musubi.Socket.assign(%Musubi.Socket{}, :title, "Inbox")
iex> Musubi.Socket.reset_changed(socket).assigns.__changed__
%{}

root_params(socket)

@spec root_params(t()) :: root_params()

Returns the params supplied when the current root store was mounted.

Examples

iex> Musubi.Socket.root_params(%Musubi.Socket{})
%{}

session(socket)

@spec session(t()) :: session()

Returns the session data shared by all root stores on one Musubi socket.

Examples

iex> Musubi.Socket.session(%Musubi.Socket{})
%{}

store_id(socket)

@spec store_id(t()) :: store_id()

Returns the runtime identity (store_id) of the store node owning this socket.

The store_id is the array of local id strings from the root down to this node. The root has store_id = []. Each non-root node has store_id = parent_path ++ [id].

Examples

iex> Musubi.Socket.store_id(%Musubi.Socket{parent_path: [], id: ""})
[]

iex> Musubi.Socket.store_id(%Musubi.Socket{parent_path: [], id: "filters"})
["filters"]

iex> Musubi.Socket.store_id(%Musubi.Socket{parent_path: ["filters"], id: "primary"})
["filters", "primary"]

update(socket, key, fun)

@spec update(t(), assign_key(), (term() -> term())) :: t()

Updates one assign by applying fun to the current value.

Examples

iex> socket = Musubi.Socket.assign(%Musubi.Socket{}, :count, 1)
iex> socket = Musubi.Socket.update(socket, :count, &(&1 + 1))
iex> socket.assigns.count
2