# `CDPEx.Protocol`
[🔗](https://github.com/patrols/cdp_ex/blob/v0.9.0/lib/cdp_ex/protocol.ex#L1)

Pure CDP wire helpers: JSON-RPC encoding, frame decoding, and message
classification. No process state, no I/O — every function here is referentially
transparent and unit-testable without a running Chrome.

> Module note: this module aliases `Mint.WebSocket` as `WebSocket`.

The Chrome DevTools Protocol is JSON-RPC 2.0-ish over a WebSocket:

  * a **command** is `%{"id" => n, "method" => "Domain.method", "params" => %{}}`
  * a **reply** echoes the `"id"` and carries either `"result"` or `"error"`
  * an **event** has a `"method"` and `"params"` but no `"id"`

`CDPEx.Connection` owns the socket and drives these helpers.

# `classification`

```elixir
@type classification() ::
  {:reply, id :: pos_integer(), session_id :: String.t() | nil,
   {:ok, map()} | {:error, map()}}
  | {:event, method :: String.t(), session_id :: String.t() | nil,
     params :: map()}
  | {:ping, binary()}
  | {:close, code :: integer() | nil, reason :: binary()}
  | :ignore
```

The result of classifying a single decoded frame.

# `frame`

```elixir
@type frame() :: Mint.WebSocket.frame() | {:close, integer() | nil, binary()}
```

A decoded `Mint.WebSocket` frame.

# `bracket_host`

```elixir
@spec bracket_host(String.t()) :: String.t()
```

Brackets an IPv6 host literal so it can be interpolated into a URL
(`::1` -> `[::1]`); other hosts pass through unchanged.

# `classify`

```elixir
@spec classify(frame()) :: classification()
```

Classifies one decoded frame into a CDP-level action.

Text frames are parsed as JSON and split into replies (by `"id"`) and events
(by `"method"`), each carrying any `"sessionId"` (from flattened CDP sessions)
so the connection can route per session. Ping frames surface so the connection can pong, and a peer
`close` frame surfaces so the connection can shut down and fail pending
callers. Everything else (`pong`, unrecognised JSON) is `:ignore`.

For an error reply, the raw CDP error object is returned; the connection wraps
it with the originating method as `{:cdp_error, method, error}`.

## Examples

    iex> CDPEx.Protocol.classify({:text, ~s({"id":1,"result":{"frameId":"A"}})})
    {:reply, 1, nil, {:ok, %{"frameId" => "A"}}}

    iex> CDPEx.Protocol.classify({:text, ~s({"id":2,"error":{"code":-32000,"message":"boom"}})})
    {:reply, 2, nil, {:error, %{"code" => -32000, "message" => "boom"}}}

    iex> CDPEx.Protocol.classify({:text, ~s({"method":"Page.loadEventFired","params":{"t":1}})})
    {:event, "Page.loadEventFired", nil, %{"t" => 1}}

    iex> CDPEx.Protocol.classify({:text, ~s({"method":"Inspector.detached"})})
    {:event, "Inspector.detached", nil, %{}}

    iex> CDPEx.Protocol.classify({:text, ~s({"method":"Page.lifecycleEvent","sessionId":"S","params":{"x":1}})})
    {:event, "Page.lifecycleEvent", "S", %{"x" => 1}}

    iex> CDPEx.Protocol.classify({:ping, "hi"})
    {:ping, "hi"}

    iex> CDPEx.Protocol.classify({:close, 1000, "bye"})
    {:close, 1000, "bye"}

    iex> CDPEx.Protocol.classify({:pong, "hi"})
    :ignore

# `decode_frames`

```elixir
@spec decode_frames(Mint.WebSocket.t(), [term()], reference()) ::
  {:ok, Mint.WebSocket.t(), [frame()]} | {:error, term()}
```

Decodes `Mint.WebSocket` stream responses for `ref` into a flat list of frames.

Non-matching responses (other refs, status/headers) are ignored. Returns the
advanced websocket along with the frames so the caller can thread state.

# `encode`

```elixir
@spec encode(String.t(), map(), pos_integer(), String.t() | nil) :: iodata()
```

Encodes a CDP command to JSON iodata.

Pass a `session_id` to target a flattened session; omit it (the default) for
commands sent on a page or browser socket directly.

## Examples

    iex> CDPEx.Protocol.encode("Page.navigate", %{"url" => "https://x.test"}, 1)
    ...> |> IO.iodata_to_binary()
    ...> |> Jason.decode!()
    %{"id" => 1, "method" => "Page.navigate", "params" => %{"url" => "https://x.test"}}

    iex> CDPEx.Protocol.encode("Page.enable", %{}, 7, "SID")
    ...> |> IO.iodata_to_binary()
    ...> |> Jason.decode!()
    %{"id" => 7, "method" => "Page.enable", "params" => %{}, "sessionId" => "SID"}

# `evaluate_result`

```elixir
@spec evaluate_result(map()) :: {:ok, term()} | {:error, term()}
```

Unwraps a `Runtime.evaluate` result.

A thrown JS exception becomes `{:error, {:evaluate_exception, details}}`; a
returned value (with `returnByValue: true`) becomes `{:ok, value}`; `undefined`
becomes `{:ok, nil}`. A result Chrome can only express as an `unserializableValue`
— `NaN`, `Infinity`, `-0`, or a `BigInt` — has no by-value `value` and becomes
`{:error, {:unserializable_value, uv}}`, carrying the raw `unserializableValue`
string. Anything else (an unrecognized result envelope) falls through to
`{:error, {:unexpected_evaluate, _}}`. (Inputs Chrome can't serialize at all,
like `window` or a circular object, never reach here — the `Runtime.evaluate`
call fails first; see `CDPEx.Page.evaluate/3`.)

## Examples

    iex> CDPEx.Protocol.evaluate_result(%{"result" => %{"type" => "string", "value" => "<html>"}})
    {:ok, "<html>"}

    iex> CDPEx.Protocol.evaluate_result(%{"result" => %{"type" => "number", "value" => 42}})
    {:ok, 42}

    iex> CDPEx.Protocol.evaluate_result(%{"result" => %{"type" => "undefined"}})
    {:ok, nil}

    iex> {:error, {:evaluate_exception, _}} =
    ...>   CDPEx.Protocol.evaluate_result(%{"exceptionDetails" => %{"text" => "Uncaught"}})

    iex> CDPEx.Protocol.evaluate_result(%{"result" => %{"type" => "bigint", "unserializableValue" => "10n"}})
    {:error, {:unserializable_value, "10n"}}

# `parse_ws_url`

```elixir
@spec parse_ws_url(String.t()) :: {String.t(), String.t(), pos_integer(), String.t()}
```

Splits a Chrome DevTools `ws://` or `wss://` URL into `{scheme, host, port, target}`.

Uses `URI.parse/1`, so IPv6 hosts, explicit ports, and paths are handled
correctly; a non-`ws(s)://` URL or one missing a host/port raises `ArgumentError`.
`wss://` denotes a remote/TLS DevTools endpoint (see `CDPEx.connect/2`).

The fourth element is the WebSocket-upgrade **request target** — the path *plus
any `?query`* — so a token-bearing cloud endpoint
(`wss://host/cdp?token=…`) reaches the provider with its query intact.

## Examples

    iex> CDPEx.Protocol.parse_ws_url("ws://127.0.0.1:9222/devtools/browser/abc-123")
    {"ws", "127.0.0.1", 9222, "/devtools/browser/abc-123"}

    iex> CDPEx.Protocol.parse_ws_url("wss://[::1]:9222/devtools/browser/abc")
    {"wss", "::1", 9222, "/devtools/browser/abc"}

    iex> CDPEx.Protocol.parse_ws_url("wss://chrome.example.com/cdp?token=abc")
    {"wss", "chrome.example.com", 443, "/cdp?token=abc"}

# `prevent_alerts_js`

```elixir
@spec prevent_alerts_js() :: String.t()
```

JavaScript that neutralises `alert`/`confirm`/`prompt` so modal dialogs can't
block automation. Injected via `Page.addScriptToEvaluateOnNewDocument`.

# `sni`

```elixir
@spec sni(String.t()) :: charlist() | :disable
```

The `:server_name_indication` value for a TLS connection to `host`: the host as a
charlist for a DNS name, or `:disable` for an IP literal (RFC 6066 forbids an IP
address as an SNI server name, and some TLS peers reject it).

---

*Consult [api-reference.md](api-reference.md) for complete listing*
