Current section
Files
Jump to
Current section
Files
lib/playwright_ex/processes/websocket_transport.ex
if Code.ensure_loaded?(WebSockex) do
defmodule PlaywrightEx.WebSocketTransport do
@moduledoc """
WebSocket-based transport for connecting to a remote Playwright server.
Unlike `PlaywrightEx.PortTransport` which spawns a local Node.js process via Erlang Port,
this module connects to an existing Playwright server via WebSocket.
This is useful for:
- Alpine Linux containers (glibc issues with local Playwright driver)
- Containerized CI environments with a separate Playwright server
- Connecting to remote/shared Playwright instances
Message format: JSON (no binary framing - WebSocket handles framing)
Run server via docker:
```bash
docker run -p 3000:3000 --rm --init -it mcr.microsoft.com/playwright:v1.58.0-noble \
npx -y playwright@1.58.0 run-server --port 3000 --host 0.0.0.0
```
"""
@behaviour PlaywrightEx.Transport
use WebSockex
alias PlaywrightEx.Connection
alias PlaywrightEx.Serialization
require Logger
@default_name __MODULE__
@max_retries 30
@retry_interval 1_000
defstruct [:ws_endpoint, connection_name: Connection]
@doc """
Start the WebSocket client and connect to the Playwright server.
Blocks until connected or max retries exhausted. This ensures the supervisor
doesn't proceed to start dependent services until the connection is ready.
## Options
- `:ws_endpoint` - Required. The WebSocket URL to connect to (e.g. `ws://localhost:3000`)
- `:name` - Optional. Process name for this transport. Defaults to `PlaywrightEx.WebSocketTransport`.
- `:connection_name` - Optional. Name of the Connection process to forward messages to.
"""
def start_link(opts) do
opts = Keyword.validate!(opts, [:ws_endpoint, :name, :connection_name])
ws_endpoint = Keyword.fetch!(opts, :ws_endpoint)
name = Keyword.get(opts, :name, @default_name)
connection_name = Keyword.get(opts, :connection_name, Connection)
connect_with_retry(ws_endpoint, name, connection_name, 0)
end
defp connect_with_retry(ws_endpoint, name, connection_name, retries) do
state = %__MODULE__{ws_endpoint: ws_endpoint, connection_name: connection_name}
case WebSockex.start_link(ws_endpoint, __MODULE__, state, name: name) do
{:ok, pid} ->
{:ok, pid}
{:error, %WebSockex.ConnError{}} when retries < @max_retries ->
Process.sleep(@retry_interval)
connect_with_retry(ws_endpoint, name, connection_name, retries + 1)
{:error, error} ->
{:error, error}
end
end
@impl PlaywrightEx.Transport
def post(name \\ @default_name, msg) do
frame = to_json(msg)
WebSockex.send_frame(name, {:text, frame})
end
# WebSockex Callbacks
@impl WebSockex
def handle_connect(_conn, state), do: {:ok, state}
@impl WebSockex
def handle_frame({:text, frame}, state) do
msg = frame |> from_json() |> tap(&log_unknown_browser_error/1)
Connection.handle_playwright_msg(state.connection_name, msg)
{:ok, state}
end
@impl WebSockex
def handle_frame({:binary, _data}, state), do: {:ok, state}
@impl WebSockex
def handle_disconnect(_status, state), do: {:reconnect, state}
@impl WebSockex
def terminate(_reason, _state), do: :ok
# JSON Serialization (same as PortTransport)
defp to_json(msg) do
msg
|> Map.update(:method, nil, &Serialization.camelize/1)
|> Serialization.deep_key_camelize()
|> JSON.encode!()
end
defp from_json(frame) do
frame
|> JSON.decode!()
|> Serialization.deep_key_underscore()
|> Map.update(:method, nil, &Serialization.underscore/1)
end
defp log_unknown_browser_error(%{
error: %{error: %{message: "Cannot read properties of undefined (reading 'launch')"}}
}) do
Logger.error("""
Failed to launch browser.
Ensure you have passed a valid browser as `ws_endpoint` query param,
e.g. `ws://localhost:3000?browser=chromium`.
""")
end
defp log_unknown_browser_error(_), do: :ok
end
end