arizona_live (arizona v0.4.0)

View Source

The live process: one gen_server per connected client.

Holds the root handler's bindings and snapshot, plus a views map of nested stateful children. Bridges the transport with the render and diff pipeline.

Lifecycle

  1. Mount -- mount/1 or mount_and_render/1 calls the root handler's mount/1, runs on_mount hooks, renders the first template, and stores the resulting snapshot.
  2. Events -- handle_event/4 dispatches a client event to either the root handler or a nested child view (located via ViewId), then diffs the resulting template against the prior snapshot and pushes ops back over the transport.
  3. Info messages -- handle_info/2 invokes the handler's optional handle_info/2 callback, diffs, and pushes the resulting ops.
  4. Navigate -- navigate/3,4 unmounts the old page (embedded child views first, then the root -- the same removal semantics as a diff removal; terminate/2 unmounts in the same order), cancels pending timers, and mounts the new handler. The previous root's final bindings are carried forward as the floor for the new mount's input -- InitBindings (route static config + middleware enrichments) overrides on key overlap. Keys the new handler omits from its mount return are dropped on the next navigate, so handlers control what persists by what they return. Stateful children's state (in views) is wiped.

Process dictionary keys

  • $arizona_connected -- set to true while a transport is attached; consulted by connected/0 so render code can branch on SSR vs live.
  • $arizona_capabilities -- the native-shell capabilities the client advertised at connect (_az_caps); set while a transport is attached and read by capabilities/0 / capability/1. A UI/effect hint only.
  • $arizona_reconnected -- true when this connection is a reconnection (vs the first connect); set while a transport is attached and read by reconnected/0. Erased by navigate/3,4 (the new page's mount is its first), kept across patch/2 (the same view stays mounted; the socket turns a patch to a different handler into a navigate).
  • $arizona_timers -- #{ViewId => {Count, Limit, [Ref]}} from send_after/3. A fired ref is pruned when its message is delivered, and a view's dead refs (fired or cancelled by the app) when an arm takes its list past Limit (twice the live count at the last prune); a removed child view's timers are cancelled synchronously (and its queued view messages flushed) on unmount -- including a child whose id a render keeps but whose handler it swaps; navigate/3,4 cancels them all so stale timers don't fire after a page change.
  • $arizona_deps -- per-dynamic dependency capture set, used by arizona_eval and arizona_template:track/1.

Fingerprint deduplication

Templates carry a base-36 f fingerprint of their statics. Once a fingerprint has been sent to the client, the live process strips the statics from subsequent payloads sharing the same f, sending only the dynamics. seed_fps/2 is used by SSR to pre-populate the set with fingerprints already shipped in the initial HTML.

Summary

Functions

Folds an on_mount hook chain over Bindings. Each hook is either a 1-arity fun or a {Module, Function} tuple whose target has arity 1. Used both internally and exposed for SSR-style rendering paths in arizona_render.

Returns the native-shell capabilities the embedding shell advertised at connect (#{} in a plain browser or during SSR). See arizona_os and capability/1.

Returns true if the embedding native shell advertised capability Key at connect, false otherwise (including a plain browser and during SSR).

Returns true if the calling process is a live process attached to a transport, false otherwise (e.g. during SSR).

Formats arizona_live runtime errors raised with an error_info annotation pointing at this module. Picked up by erl_error:format_exception/3.

Dispatches a client event to a view. If ViewId matches a nested child, the event goes to that view; otherwise it goes to the root handler. Returns {ok, Ops, Effects, Observed}.

Mounts the handler without rendering. Returns {ok, ViewId}.

Mounts and renders the handler. Returns {ok, ViewId, PageContent, Observed} where PageContent is either a fingerprint payload (if the template has f) or an HTML binary, and Observed the render-time az-* observations (see arizona_event_attrs).

SPA navigation: unmounts the current root handler, mounts a new one, and returns fresh page content. Equivalent to navigate(Pid, NewHandler, InitBindings, []).

SPA navigation with on_mount hooks for the new handler.

In-place SPA navigation (patch): keeps the current root view mounted and delivers Params to its handle_update/3, re-rendering through the diff instead of remounting. The root handler, view id, process, and child views all survive; only the changed slots produce ops. Returns the diff ops plus any effects the reaction emitted (handle_update's, folded with children's).

Returns true while the page mounted by a reconnection (the client dropped and re-opened the WebSocket) is the current page, false on the first connect and during SSR.

Renders the current view tree to a complete output binary from the live process's last rendered snapshot and its child views' current snapshots.

Seeds the live process's sent_fps set with fingerprints already shipped to the client (typically by SSR). Subsequent diffs will strip statics for matching fingerprints.

Sends a message to a specific view by id. The message is delivered to the live process's mailbox and routed to the matching child view.

Like send/2 but delivers after Time milliseconds. Returns the timer ref, which is also tracked in the process dictionary (keyed by ViewId) so a removed child view's pending timers can be cancelled on unmount and navigate/3,4 can cancel them all on page change. A fired ref is pruned from the tracked set when its message is delivered. A ref the caller cancelled itself with erlang:cancel_timer/1 delivers nothing, so arming also prunes ViewId's dead refs, once its set has grown to twice the live count found by the previous prune: the set stays within a constant factor of the view's live timers, and each arm costs O(1) amortized.

Starts a live process for a route-level view Handler.

Like start_link/4 but also threads the connection context the transport knows -- #{capabilities => map(), reconnect => boolean(), push_barrier => boolean()} -- into the live process, where capability/1 and reconnected/0 read the first two. Browser/SSR callers use start_link/4 (empty context).

Stops a live process, running its terminate/2 (and thus unmount/1) cleanup.

The state of the views this process holds: the root's handler and bindings, and the same for each child view under views, keyed the way the process keys them.

Types

on_mount()

-nominal on_mount() :: [on_mount_hook()].

on_mount_hook()

-nominal on_mount_hook() :: fun((map()) -> map()) | {module(), atom()}.

route_opts()

-nominal route_opts() ::
             #{bindings => arizona_template:bindings(),
               on_mount => on_mount(),
               layouts => [arizona_render:layout()],
               middlewares => [arizona_middleware:middleware()],
               check_origin => boolean(),
               _ => term()}.

state()

-type state() ::
          #state{handler :: module(),
                 bindings :: map(),
                 snapshot :: map() | undefined,
                 views :: map(),
                 on_mount :: on_mount(),
                 transport_pid :: pid() | undefined,
                 sent_fps :: map(),
                 push_barrier :: boolean(),
                 pending_refresh :: #{binary() => true}}.

Functions

apply_on_mount(OnMount, Bindings)

-spec apply_on_mount(OnMount, Bindings) -> Bindings1
                        when OnMount :: on_mount(), Bindings :: map(), Bindings1 :: map().

Folds an on_mount hook chain over Bindings. Each hook is either a 1-arity fun or a {Module, Function} tuple whose target has arity 1. Used both internally and exposed for SSR-style rendering paths in arizona_render.

capabilities()

-spec capabilities() -> map().

Returns the native-shell capabilities the embedding shell advertised at connect (#{} in a plain browser or during SSR). See arizona_os and capability/1.

capability(Key)

-spec capability(Key) -> boolean() when Key :: binary().

Returns true if the embedding native shell advertised capability Key at connect, false otherwise (including a plain browser and during SSR).

This reflects an unauthenticated, client-advertised claim, so it is a UI/effect hint only -- NEVER branch a server-side authorization decision on it.

connected()

-spec connected() -> boolean().

Returns true if the calling process is a live process attached to a transport, false otherwise (e.g. during SSR).

dedup_fps(Ops, Fps0)

format_error(Reason, Stacktrace)

-spec format_error(Reason, Stacktrace) -> ErrorInfo
                      when
                          Reason :: term(), Stacktrace :: [tuple()], ErrorInfo :: #{general := iolist()}.

Formats arizona_live runtime errors raised with an error_info annotation pointing at this module. Picked up by erl_error:format_exception/3.

handle_call/3

handle_cast/2

handle_continue/2

handle_event(Pid, ViewId, Event, Payload)

-spec handle_event(Pid, ViewId, Event, Payload) -> {ok, Ops, Effects, Observed}
                      when
                          Pid :: pid(),
                          ViewId :: binary(),
                          Event :: binary(),
                          Payload :: map(),
                          Ops :: [list()],
                          Effects :: [term()],
                          Observed :: arizona_event_attrs:observed().

Dispatches a client event to a view. If ViewId matches a nested child, the event goes to that view; otherwise it goes to the root handler. Returns {ok, Ops, Effects, Observed}.

handle_info/2

init/1

-spec init({Handler, InitBindings, TransportPid, OnMount, ParentMetadata, ConnInfo}) -> {ok, state()}
              when
                  Handler :: module(),
                  InitBindings :: map(),
                  TransportPid :: pid() | undefined,
                  OnMount :: on_mount(),
                  ParentMetadata :: logger:metadata() | undefined,
                  ConnInfo ::
                      #{capabilities => map(), reconnect => boolean(), push_barrier => boolean()}.

mount(Pid)

-spec mount(Pid) -> {ok, binary(), arizona_event_attrs:observed()} when Pid :: pid().

Mounts the handler without rendering. Returns {ok, ViewId}.

mount_and_render(Pid)

-spec mount_and_render(Pid) -> {ok, binary(), binary() | map(), arizona_event_attrs:observed()}
                          when Pid :: pid().

Mounts and renders the handler. Returns {ok, ViewId, PageContent, Observed} where PageContent is either a fingerprint payload (if the template has f) or an HTML binary, and Observed the render-time az-* observations (see arizona_event_attrs).

patch(Pid, Params)

-spec patch(Pid, Params) ->
               {ok, [arizona_diff:op()], [arizona_stateful:effect()], arizona_event_attrs:observed()}
               when Pid :: pid(), Params :: map().

In-place SPA navigation (patch): keeps the current root view mounted and delivers Params to its handle_update/3, re-rendering through the diff instead of remounting. The root handler, view id, process, and child views all survive; only the changed slots produce ops. Returns the diff ops plus any effects the reaction emitted (handle_update's, folded with children's).

The caller (the socket) only invokes this when the patched route resolves to the same root handler; a different handler must go through navigate/4.

mount/1 and on_mount do not re-run on a patch -- they belong to the mount phase, and a patch by definition does not remount (it would otherwise clobber the live state the patch exists to preserve: on_mount's output is a mount input fed into mount/1, with no mount/1 here it would land unmediated on the live bindings). The route's middlewares do run (in the socket's do_patch), so per-arrival, request-shaped derivation (session, path params, ...) flows in as Params; handler-specific per-navigation logic goes in handle_update/3, which sees both the new Params and the live state.

reconnected()

-spec reconnected() -> boolean().

Returns true while the page mounted by a reconnection (the client dropped and re-opened the WebSocket) is the current page, false on the first connect and during SSR.

Mirrors connected/0. The live process re-mounts on every reconnect, so connected/0 alone cannot tell first-connect from reconnection. Use this to suppress one-shot OS commands: connected() andalso not reconnected() fires a one-shot (notify, focus) on the first connect only, while a declarative re-assert (set_title, fullscreen) can fire on every connect.

It describes the current page's mount, not the socket: a navigate/3,4 after a reconnect mounts a page this connection has not shown before, so it reads false from that mount on, and its first-connect one-shots fire. A patch/2 keeps the view mounted, so it keeps the value; the socket only patches within the same handler, and a patch to another handler arrives here as a navigate.

render_current(Pid)

-spec render_current(Pid) -> {ok, binary(), arizona_event_attrs:observed()} when Pid :: pid().

Renders the current view tree to a complete output binary from the live process's last rendered snapshot and its child views' current snapshots.

Unlike mount_and_render/1 (which returns a fingerprint payload meant for client-side diff application), this always materializes the full output -- intended for transports that repaint the whole view each frame (e.g. a terminal renderer) instead of applying diff ops. It reflects current root and nested child state without evaluating the template: no mount/1 or handle_update/3 runs, and the process state is left untouched.

seed_fps(Pid, FpList)

-spec seed_fps(Pid, FpList) -> ok when Pid :: pid(), FpList :: [binary()].

Seeds the live process's sent_fps set with fingerprints already shipped to the client (typically by SSR). Subsequent diffs will strip statics for matching fingerprints.

send(ViewId, Msg)

-spec send(ViewId, Msg) -> term() when ViewId :: binary(), Msg :: term().

Sends a message to a specific view by id. The message is delivered to the live process's mailbox and routed to the matching child view.

send_after(ViewId, Time, Msg)

-spec send_after(ViewId, Time, Msg) -> reference()
                    when ViewId :: binary(), Time :: non_neg_integer(), Msg :: term().

Like send/2 but delivers after Time milliseconds. Returns the timer ref, which is also tracked in the process dictionary (keyed by ViewId) so a removed child view's pending timers can be cancelled on unmount and navigate/3,4 can cancel them all on page change. A fired ref is pruned from the tracked set when its message is delivered. A ref the caller cancelled itself with erlang:cancel_timer/1 delivers nothing, so arming also prunes ViewId's dead refs, once its set has grown to twice the live count found by the previous prune: the set stays within a constant factor of the view's live timers, and each arm costs O(1) amortized.

start_link(Handler, InitBindings, TransportPid, OnMount)

-spec start_link(Handler, InitBindings, TransportPid, OnMount) -> gen_server:start_ret()
                    when
                        Handler :: module(),
                        InitBindings :: map(),
                        TransportPid :: pid() | undefined,
                        OnMount :: on_mount().

Starts a live process for a route-level view Handler.

The transport pid receives {arizona_push, RootViewId, Ops, Effects, Observed} messages when the live process diffs and emits updates; RootViewId is the emitting page's root view id, which the transport scopes the ops with (a push the old page queued before a navigate is shipped ahead of the replace, see arizona_socket). OnMount is the route's hook chain. Any request data the view needs is supplied as bindings by the transport layer (e.g. via arizona_middleware:extract/1 middlewares); the live process is transport-agnostic and never sees a request.

start_link(Handler, InitBindings, TransportPid, OnMount, ConnInfo)

-spec start_link(Handler, InitBindings, TransportPid, OnMount, ConnInfo) -> gen_server:start_ret()
                    when
                        Handler :: module(),
                        InitBindings :: map(),
                        TransportPid :: pid() | undefined,
                        OnMount :: on_mount(),
                        ConnInfo ::
                            #{capabilities => map(), reconnect => boolean(), push_barrier => boolean()}.

Like start_link/4 but also threads the connection context the transport knows -- #{capabilities => map(), reconnect => boolean(), push_barrier => boolean()} -- into the live process, where capability/1 and reconnected/0 read the first two. Browser/SSR callers use start_link/4 (empty context).

push_barrier is the transport's own contract, not client data: set it when the transport folds queued {arizona_push, ...} messages into a synchronous event/patch/navigate reply, and the live process marks the boundary between the pushes that preceded that reply and anything emitted after it (see push_barrier/2). Transports that ship every push in its own frame leave it unset.

stop(Pid)

-spec stop(Pid) -> ok when Pid :: pid().

Stops a live process, running its terminate/2 (and thus unmount/1) cleanup.

For transports that manage view lifecycles directly -- e.g. an SSH channel closing one terminal session among many in a long-running daemon. The live process is linked to its transport, but a transport that stops with reason normal would not bring the view down via the link, so the transport stops it explicitly.

terminate/2

view_state(Pid)

-spec view_state(Pid) ->
                    #{handler := module(),
                      bindings := arizona_template:bindings(),
                      views :=
                          #{binary() => #{handler := module(), bindings := arizona_template:bindings()}}}
                    when Pid :: pid().

The state of the views this process holds: the root's handler and bindings, and the same for each child view under views, keyed the way the process keys them.

snapshot is deliberately not included. It is the diff engine's bookkeeping -- the last rendered structure, kept to diff the next render against -- not part of what a view holds, and it is large.