CDPEx.Connection (CDPEx v0.12.0)

Copy Markdown View Source

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 — each call/4 blocks 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.

t()

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

call_error()

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

t()

@type t() :: %CDPEx.Connection{
  all_subscribers: term(),
  conn: term(),
  monitors: term(),
  next_id: term(),
  pending: term(),
  ref: term(),
  subscribers: term(),
  waiters: term(),
  websocket: term(),
  ws_send_error: term()
}

Functions

await_event(conn, matcher, timeout \\ 10000, opts \\ [])

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

call(conn, method, params \\ %{}, timeout \\ 10000, opts \\ [])

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

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

close(conn)

@spec close(GenServer.server()) :: :ok

Closes the WebSocket and stops the connection. :ok if it is already gone.

start_link(ws_url, opts \\ [])

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

subscribe(conn, method, timeout \\ 10000, opts \\ [])

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

unsubscribe(conn, method)

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