A GenServer owning a single CDP WebSocket connection.
This is the heart of cdp_ex's OTP model. It connects and upgrades a
Mint.WebSocket in init/1, then runs in :active mode so every inbound
frame arrives as a process message in handle_info/2. The GenServer:
- matches command replies back to the caller by JSON-RPC
id(concurrent callers are fine — eachcall/4blocks only its own caller), - routes unsolicited events to subscribers and one-shot waiters,
- answers WebSocket pings with pongs,
- fails every pending caller with
{:error, {:ws_closed, reason}}and stops if the socket drops — no caller is left hanging.
One connection backs one socket: the browser endpoint, or a page endpoint at
/devtools/page/<targetId>. A single connection may carry both untagged
browser/page frames and many flattened sessions' frames, demultiplexed by
sessionId. CDPEx.Browser starts and monitors these.
Summary
Types
Error reasons from call/5 — and from every Network/Page op layered on it.
Precisely specced (not term()) so Dialyzer flags drift at the source.
Functions
Blocks until an event for which matcher.(params) returns true, or timeout.
Sends a CDP command and blocks until its reply (or timeout).
Returns a specification to start this module under a supervisor.
Closes the WebSocket and stops the connection. :ok if it is already gone.
Starts a connection to the given ws://host:port/path URL.
Subscribes the calling process to a CDP event method (e.g.
"Page.lifecycleEvent") or to :all events. Delivered as
{:cdp_event, conn_pid, method, params, session_id} — session_id is nil
for browser/page-level events.
Removes a subscription created with subscribe/4. A connection that is gone has
no subscriptions left, so that is :ok too.
Types
@type call_error() :: {:cdp_error, String.t(), term()} | {:timeout, String.t()} | {:ws_closed, term()} | :noproc | {:session_closed, String.t()} | {:invalid_args, term()}
Error reasons from call/5 — and from every Network/Page op layered on it.
Precisely specced (not term()) so Dialyzer flags drift at the source.
Functions
@spec await_event(GenServer.server(), (map() -> boolean()), timeout(), keyword()) :: {:ok, map()} | {:error, {:timeout, :await_event} | :noproc | {:ws_closed, term()} | {:session_closed, String.t()}}
Blocks until an event for which matcher.(params) returns true, or timeout.
matcher receives the event params map. Returns {:ok, params} (the matched
event's params) on a match, or {:error, reason} where reason is
{:timeout, :await_event} (no matching event in time) or :noproc /
{:ws_closed, _} (the connection itself went away) — callers must be able to
tell those apart.
Pass opts with session_id: sid to only match events from that session. Such
a waiter returns {:error, {:session_closed, sid}} if the session detaches while
it waits; one registered after the detach has nothing to end it and times out
(a call/5 on the session first will tell you it is gone).
Keep matcher fast and side-effect-free: it runs inside the connection process
for each candidate event, so a slow or blocking matcher stalls the socket — and,
on a :session-transport connection, every page sharing it.
@spec call(GenServer.server(), String.t(), map(), timeout(), keyword()) :: {:ok, map()} | {:error, call_error()}
Sends a CDP command and blocks until its reply (or timeout).
Returns {:ok, result}, {:error, {:cdp_error, method, error}} on a protocol
error, {:error, {:timeout, method}}, or {:error, {:ws_closed, reason}} /
{:error, :noproc} if the connection drops or is already gone. params that
can't be encoded as JSON (e.g. a binary that isn't valid UTF-8) return
{:error, {:invalid_args, reason}}; the command is not sent. A method that isn't a
string or params that aren't a map raise FunctionClauseError in the caller.
Pass opts with session_id: sid to address a flattened CDP session. A call on
a session that has detached (its target closed, or Target.detachFromTarget) or
that Chrome doesn't know at all returns {:error, {:session_closed, sid}}: one
in flight when the connection sees Target.detachedFromTarget for it fails
then, and a later one fails on Chrome's "session not found" reply rather than
waiting out timeout.
Returns a specification to start this module under a supervisor.
See Supervisor.
@spec close(GenServer.server()) :: :ok
Closes the WebSocket and stops the connection. :ok if it is already gone.
@spec start_link(String.t(), keyword()) :: GenServer.on_start()
Starts a connection to the given ws://host:port/path URL.
Options: :upgrade_timeout (ms, default 15_000) and :name (registers the
GenServer). Returns {:ok, pid} once the WebSocket handshake completes. A failed
connect or upgrade returns its :ws_connect / :ws_upgrade error without
exiting the linked caller.
@spec subscribe(GenServer.server(), String.t() | :all, timeout(), keyword()) :: :ok | {:error, :noproc | {:timeout, :subscribe} | {:ws_closed, term()}}
Subscribes the calling process to a CDP event method (e.g.
"Page.lifecycleEvent") or to :all events. Delivered as
{:cdp_event, conn_pid, method, params, session_id} — session_id is nil
for browser/page-level events.
The subscription is removed automatically if the subscribing process exits, so a crashed subscriber can't accumulate in the connection.
timeout bounds the registration call so a caller can fold it into an overall
deadline (defaults to the standard call timeout).
Pass opts with session_id: sid to receive only events from that session —
the matching that await_event/4 already does for waiters. The default (nil)
receives every session's events (current behaviour); on a :dedicated
connection, where every event is for the one page, the two are equivalent.
"Target.detachedFromTarget" (or :all) scoped to sid also delivers sid's
own detach, which Chrome sends on the parent session (so its session_id
element is the parent's, usually nil, and params["sessionId"] is sid).
Returns {:error, :noproc} if the connection is gone, or
{:error, {:timeout, :subscribe}} if it doesn't register within timeout.
@spec unsubscribe(GenServer.server(), String.t() | :all) :: :ok
Removes a subscription created with subscribe/4. A connection that is gone has
no subscriptions left, so that is :ok too.