AbsintheClient.WebSocket (AbsintheClient v0.2.0)

View Source

Req adapter for Absinthe subscriptions.

The WebSocket does the following:

  • Pushes documents to the GraphQL server and forwards replies to the callers.

  • Manages any subscriptions received, including automatically re-subscribing in the event of a connection loss.

Under the hood, WebSocket connections are Slipstream socket processes which are usually managed by an internal AbsintheClient supervisor.

Examples

Performing a query operation over a WebSocket:

iex> req = Req.new(base_url: "http://localhost:4002") |> AbsintheClient.attach()
iex> ws = req |> AbsintheClient.WebSocket.connect!()
iex> Req.request!(req, web_socket: ws, graphql: ~S|{ __type(name: "Repo") { name } }|).body["data"]
%{"__type" => %{"name" => "Repo"}}

Performing an async query operation and awaiting the reply:

iex> req = Req.new(base_url: "http://localhost:4002") |> AbsintheClient.attach()
iex> ws = req |> AbsintheClient.WebSocket.connect!()
iex> reply =
...>   req
...>   |> Req.request!(web_socket: ws, async: true, graphql: ~S|{ __type(name: "Repo") { name } }|)
...>   |> AbsintheClient.WebSocket.await_reply!()
iex> reply.payload["data"]
%{"__type" => %{"name" => "Repo"}}

Handling messages

Results will be sent to the caller as WebSocket.Message structs.

In a GenServer for instance, you would implement a handle_info/2 callback:

def handle_info(%AbsintheClient.WebSocket.Message{payload: payload}, state) do
  # code...
  {:noreply, state}
end

Summary

Functions

Awaits the server's response to a pushed document.

Awaits the server's response to a pushed document or raises an error.

Dynamically starts (or re-uses already started) AbsintheWs process with the given options

Connects to an Absinthe WebSocket.

Same as connect/1 but raises on error.

Same as connect/2 but raises on error.

Pushes a query to the server via the given socket.

Performs a GraphQL operation.

Types

graphql()

@type graphql() :: String.t() | {String.t(), nil | map()}

web_socket()

@type web_socket() :: GenServer.server()

Functions

await_reply(response_or_ref, timeout \\ 5000)

@spec await_reply(Req.Response.t() | reference(), non_neg_integer()) ::
  {:ok, AbsintheClient.WebSocket.Reply.t()} | {:error, :timeout}

Awaits the server's response to a pushed document.

Examples

iex> req = Req.new(base_url: "http://localhost:4002") |> AbsintheClient.attach(async: true)
iex> {:ok, ws} = AbsintheClient.WebSocket.connect(req)
iex> {:ok, res} = Req.request(req, web_socket: ws, graphql: ~S|{ __type(name: "Repo") { name } }|)
iex> {:ok, reply} = AbsintheClient.WebSocket.await_reply(res)
iex> reply.payload["data"]
%{"__type" => %{"name" => "Repo"}}

await_reply!(response_or_ref, timeout \\ 5000)

Awaits the server's response to a pushed document or raises an error.

Examples

iex> req = Req.new(base_url: "http://localhost:4002") |> AbsintheClient.attach(async: true)
iex> ws = req |> AbsintheClient.WebSocket.connect!()
iex> res = Req.post!(req, web_socket: ws, graphql: ~S|{ __type(name: "Repo") { name } }|)
iex> AbsintheClient.WebSocket.await_reply!(res).payload["data"]
%{"__type" => %{"name" => "Repo"}}

connect(request_or_options)

@spec connect(request_or_options :: Req.Request.t() | keyword()) ::
  {:ok, web_socket()} | {:error, Exception.t()}

Dynamically starts (or re-uses already started) AbsintheWs process with the given options:

  • :url - URL where to make the WebSocket connection. When provided as an option to connect/2 the request's base_url will be prepended to this path. The default value is "/socket/websocket".

  • :headers - headers to send on the initial HTTP request. Defaults to [].

  • :connect_options - list of options given to Mint.HTTP.connect/4 for the initial HTTP request:

    • :timeout - socket connect timeout in milliseconds, defaults to 30_000.

    • :transport_opts - keyword list of options passed to the underlying transport layer (SSL/TCP). Common options include:

      • :verify - :verify_peer or :verify_none for SSL verification
      • :cacertfile - path to CA certificate file
      • :certfile - path to client certificate for mTLS
      • :keyfile - path to client private key for mTLS
      • :versions - list of allowed TLS versions (e.g., [:"tlsv1.2", :"tlsv1.3"])
      • :nodelay - boolean to disable Nagle's algorithm for lower latency
      • :timeout - connection timeout (defaults to 30_000 if not specified)

      See the Erlang :ssl module documentation for a complete list of available options.

  • :connect_params - Optional. Custom params to be sent when the WebSocket connects. Defaults to sending the bearer Authorization token if one is present on the request. The default value is nil.

  • :parent - pid of the process starting the connection. The socket monitors this process and shuts down when the parent process exits. Defaults to self().

Note that when connect/2 returns successfully, it indicates that the WebSocket process has started. The process must then connect to the GraphQL server and join the relevant topic(s) before it can send and receive messages.

Examples

From a request:

iex> req = Req.new(base_url: "http://localhost:4002") |> AbsintheClient.attach()
iex> {:ok, ws} = req |> AbsintheClient.WebSocket.connect()
iex> ws |> GenServer.whereis() |> Process.alive?()
true

From keyword options:

iex> {:ok, ws} = AbsintheClient.WebSocket.connect(url: "ws://localhost:4002/socket/websocket")
iex> ws |> GenServer.whereis() |> Process.alive?()
true

Disabling SSL verification for local development:

iex> req = Req.new(base_url: "https://localhost:4002") |> AbsintheClient.attach()
iex> {:ok, ws} = req |> AbsintheClient.WebSocket.connect(
...>   connect_options: [transport_opts: [verify: :verify_none]]
...> )
iex> ws |> GenServer.whereis() |> Process.alive?()
true

Using client certificates for mTLS:

req = Req.new(base_url: "https://example.com") |> AbsintheClient.attach()

{:ok, ws} = req |> AbsintheClient.WebSocket.connect(
  connect_options: [
    transport_opts: [
      verify: :verify_peer,
      cacertfile: "/path/to/ca.pem",
      certfile: "/path/to/client-cert.pem",
      keyfile: "/path/to/client-key.pem"
    ]
  ]
)

ws |> GenServer.whereis() |> Process.alive?()
# ==> true

connect(request, options)

@spec connect(Req.Request.t(), keyword()) ::
  {:ok, web_socket()} | {:error, Exception.t()}

Connects to an Absinthe WebSocket.

Refer to connect/1 for more information.

Examples

With the default URL path:

iex> req = Req.new(base_url: "http://localhost:4002") |> AbsintheClient.attach()
iex> {:ok, ws} = req |> AbsintheClient.WebSocket.connect()
iex> ws |> GenServer.whereis() |> Process.alive?()
true

With a custom URL path:

iex> req = Req.new(base_url: "http://localhost:4002") |> AbsintheClient.attach()
iex> {:ok, ws} = req |> AbsintheClient.WebSocket.connect(url: "/socket/websocket")
iex> ws |> GenServer.whereis() |> Process.alive?()
true

connect!(request_or_options)

@spec connect!(request_or_options :: Req.Request.t() | keyword()) :: web_socket()

Same as connect/1 but raises on error.

Examples

From a request:

iex> ws = Req.new(base_url: "http://localhost:4002") |> AbsintheClient.WebSocket.connect!()
iex> ws |> GenServer.whereis() |> Process.alive?()
true

From keyword options:

iex> ws = AbsintheClient.WebSocket.connect!(url: "ws://localhost:4002/socket/websocket")
iex> ws |> GenServer.whereis() |> Process.alive?()
true

connect!(request, options)

@spec connect!(Req.Request.t(), keyword()) :: web_socket()

Same as connect/2 but raises on error.

Examples

iex> ws =
...>  Req.new(base_url: "http://localhost:4002")
...>  |> AbsintheClient.WebSocket.connect!(url: "/socket/websocket")
iex> ws |> GenServer.whereis() |> Process.alive?()
true

push(request_or_socket, graphql)

@spec push(request_or_socket :: Req.Request.t() | web_socket(), graphql()) ::
  reference()

Pushes a query to the server via the given socket.

Examples

iex> {:ok, req} = AbsintheClient.WebSocket.connect(url: "ws://localhost:4002/socket/websocket")
iex> ref = AbsintheClient.WebSocket.push(req, ~S|{ __type(name: "Repo") { name } }|)
iex> AbsintheClient.WebSocket.await_reply!(ref).payload["data"]
%{"__type" => %{"name" => "Repo"}}

run(request)

Performs a GraphQL operation.

Examples

iex> req = Req.new(base_url: "http://localhost:4002") |> AbsintheClient.attach()
iex> ws = req |> AbsintheClient.WebSocket.connect!()
iex> Req.request!(req, web_socket: ws, graphql: ~S|{ __type(name: "Repo") { name } }|).body["data"]
%{"__type" => %{"name" => "Repo"}}