arizona_socket (arizona v0.4.0)
View SourceBridges a WebSocket frame stream with the live process.
The server adapter's WebSocket handler creates a socket
via init/4, then forwards inbound text frames to handle_in/2 and
inbox messages to handle_info/2. Each call returns a result tuple
that the transport translates into WebSocket frames or close codes.
Wire protocol
Inbound text frames are JSON arrays:
[~"cached_fps", FpList] %% client tells us which fingerprints it has
[~"navigate", #{~"path" := Path, ~"qs" := Qs}] %% SPA navigation request
[ViewId, Event, Payload] %% UI event
~"0" %% ping (replied with ~"1")Outbound text frames are JSON maps with keys ~"o" (ops), ~"e"
(effects), and/or ~"a" (newly proven az-* attribute names -- the connect
frame carries the compile-time walk of the route's handler and layouts, any
later frame only the per-socket delta; see arizona_event_attrs). ~"o" and
~"e" are arrays produced by arizona_diff and arizona_js respectively.
Exit handling
The live process is linked. Exits map to WebSocket close codes:
normal,shutdown,{shutdown, _other_}-- graceful close1000. Client does NOT auto-reconnect (treats it as a deliberate end of session).{shutdown, drain}-- graceful close1001("going away"). Client auto-reconnects via the form-state-preserving path; new live process mounts against the new server version. A frame whose call into the live process finds it gone (a drain landing while the frame is in flight) is dropped, and the close comes from the queued exit, after any push the process sent before exiting -- a drain's effects among them.- Anything else (including raises in
handle_in/2andhandle_info/2, among them a frame that cannot be encoded) -- crash close4500: the view crashed after it mounted. Client remounts it in place by reconnecting.
In dev mode (arizona_reloader:enabled/0) the socket also joins the reloader
topic and closes 1001 on {arizona_reloader, reload}, whatever the client:
funs held in the view's state (a stream's key fun, a fun prop) point into code
a second reload purges, so the client reconnects and remounts on fresh code.
A failure to mount the view closes 4501 instead, whatever the exit: the
connect's mount (init/4), a reconnect's deferred resync mount, and a
navigate -- each up to the frame that delivers the view.
The page itself may be stale (a deploy changed the route or the assets), so
the client reloads it after a jittered delay.
Summary
Functions
Tells the socket's live process its listener is draining, as
{arizona_drain, Deadline} (Deadline an erlang:monotonic_time(millisecond)
value): the view's handle_drain/2 runs and, by default, the socket closes
1001, which clients reconnect from. A socket built without a live process
(a test fixture) has nothing to tell.
Handles an inbound text frame.
Handles inbox messages forwarded by the transport.
Creates a socket for Handler with the given Bindings and Req,
and starts its live process.
Returns the live process pid backing this socket, or undefined if
the socket was constructed without one (test fixtures).
Types
Functions
Tells the socket's live process its listener is draining, as
{arizona_drain, Deadline} (Deadline an erlang:monotonic_time(millisecond)
value): the view's handle_drain/2 runs and, by default, the socket closes
1001, which clients reconnect from. A socket built without a live process
(a test fixture) has nothing to tell.
Handles an inbound text frame.
While a deferred reconnect resync is pending (fps_follow, see init/4),
the FIRST frame settles it: the promised [~"cached_fps", FpList] seeds
the fingerprints and the resync replies deduped; any other frame flushes
the resync undeduped first, its own reply following in the same result
(reply_many), so no frame ever reaches the still-unmounted live process.
Recognized payloads:
~"0"-- ping, replied with~"1"[~"cached_fps", FpList]-- seeds fingerprints into the live process[~"navigate", #{~"path" := Path, ~"qs" := Qs}]-- SPA navigation (replace)[~"patch", #{~"path" := Path, ~"qs" := Qs}]-- in-place SPA navigation[ViewId, Event, Payload]-- UI event dispatch (Eventa binary,Payloada map)
Unrecognized payloads, and frames whose body is not valid JSON, are
silently dropped (a single bad frame must not crash the socket). An event
frame whose Payload is not a map is dropped the same way, so a crafted
non-map payload can't reach a #{...}-matching handler and crash it.
Handles inbox messages forwarded by the transport.
Routes {arizona_push, ViewId, Ops, Effects} from the live process into
a reply frame, its ops tagged with the view id the push names. A push the old
page emitted before a navigate this socket drives never gets here: its ops are
folded into the navigate's frame ahead of the replace and its effects dropped
(drain_pending_pushes/1). Handles
'EXIT' from the linked live process per the mapping in this module's
"Exit handling" section.
-spec init(Handler, Bindings, Req, Opts) -> result() when Handler :: module(), Bindings :: map(), Req :: az:request(), Opts :: map().
Creates a socket for Handler with the given Bindings and Req,
and starts its live process.
Opts may include:
reconnect-- iftrue, renders the page and replies with an?OP_REPLACEop (used when the client is reconnecting after a network drop)fps_follow-- iftrue(withreconnect), the client promised itscached_fpsframe as the first frame after open, so the resync render is DEFERRED until that frame arrives and its payload dedups statics against the announced fingerprints -- the difference between re-shipping a full page's statics to every client of a deploy-drain reconnect storm and shipping roughly the dynamics. A backstop timer (?RESYNC_TIMEOUT_MS) flushes the resync undeduped for a flagged client that never sends the frame; a client without the flag (a native client, any non-announcing one) keeps the immediate resync -- zero penalty.on_mount-- list ofarizona_live:on_mount/0hookslayouts-- the route'sarizona_render:layout/0list, i.e. what wrapped this page at SSR. A later navigate/patch to a route whose layouts differ cannot be served over the wire and degrades to a full-page navigation
The route adapter (used by SPA navigate to resolve new routes) is
recovered from Req itself via arizona_req:adapter/1.
Returns the live process pid backing this socket, or undefined if
the socket was constructed without one (test fixtures).