defmodule HTTP.WebSocket do @moduledoc """ Browser-like WebSocket client API for Elixir. Events are delivered as messages to the owner process: {HTTP.WebSocket, socket, %HTTP.WebSocket.Event.Open{}} {HTTP.WebSocket, socket, %HTTP.WebSocket.Event.Message{}} {HTTP.WebSocket, socket, %HTTP.WebSocket.Event.Error{}} {HTTP.WebSocket, socket, %HTTP.WebSocket.Event.Close{}} Plain Elixir binaries are sent as text frames. Use `array_buffer/1` or `HTTP.Blob` for binary frames. """ alias HTTP.WebSocket.ArrayBuffer alias HTTP.WebSocket.Connection alias HTTP.WebSocket.Frame alias HTTP.WebSocket.Options defstruct pid: nil, ref: nil, url: nil @connecting 0 @open 1 @closing 2 @closed 3 @call_timeout 5_000 @type t :: %__MODULE__{pid: pid() | nil, ref: reference() | nil, url: String.t() | nil} @spec connecting() :: 0 def connecting, do: @connecting @spec open() :: 1 def open, do: @open @spec closing() :: 2 def closing, do: @closing @spec closed() :: 3 def closed, do: @closed @spec new(String.t() | URI.t(), String.t() | [String.t()], keyword() | map()) :: t() | {:error, term()} def new(url, protocols \\ [], init \\ []) do ref = make_ref() with {:ok, options} <- Options.new(url, protocols, put_ref(init, ref)), {:ok, pid} <- DynamicSupervisor.start_child( HTTP.WebSocket.ConnectionSupervisor, {Connection, options} ) do %__MODULE__{pid: pid, ref: ref, url: options.url} end end @spec array_buffer(binary()) :: ArrayBuffer.t() | {:error, :invalid_array_buffer} def array_buffer(data) when is_binary(data), do: ArrayBuffer.new(data) def array_buffer(_data), do: {:error, :invalid_array_buffer} @spec url(t()) :: String.t() | nil def url(%__MODULE__{url: url}), do: url @spec ready_state(t()) :: 0 | 1 | 2 | 3 def ready_state(socket), do: connection_call(socket, :ready_state, @closed) @spec buffered_amount(t()) :: non_neg_integer() def buffered_amount(socket), do: connection_call(socket, :buffered_amount, 0) @spec extensions(t()) :: String.t() def extensions(socket), do: connection_call(socket, :extensions, "") @spec protocol(t()) :: String.t() def protocol(socket), do: connection_call(socket, :protocol, "") @spec binary_type(t()) :: :blob | :array_buffer def binary_type(socket), do: connection_call(socket, :binary_type, :blob) @spec set_binary_type(t(), :blob | :array_buffer) :: :ok | {:error, term()} def set_binary_type(socket, binary_type) when binary_type in [:blob, :array_buffer] do connection_call(socket, {:set_binary_type, binary_type}, {:error, :closed}) end def set_binary_type(_socket, _binary_type), do: {:error, :invalid_binary_type} @spec send(t(), String.t() | HTTP.Blob.t() | ArrayBuffer.t()) :: :ok | {:error, term()} def send(socket, data), do: connection_call(socket, {:send, data}, {:error, :closed}) @spec close(t()) :: :ok | {:error, term()} def close(socket), do: close(socket, nil, "") @spec close(t(), non_neg_integer()) :: :ok | {:error, term()} def close(socket, code), do: close(socket, code, "") @spec close(t(), non_neg_integer() | nil, String.t()) :: :ok | {:error, term()} def close(socket, code, reason) when is_binary(reason) do with {:ok, payload} <- Frame.close_payload(code, reason) do connection_call(socket, {:close, code, reason, payload}, {:error, :closed}) end end def close(_socket, _code, _reason), do: {:error, :invalid_close_reason} defp connection_call(%__MODULE__{pid: pid}, request, default) when is_pid(pid) do GenServer.call(pid, request, @call_timeout) catch :exit, _reason -> default end defp connection_call(_socket, _request, default), do: default defp put_ref(init, ref) when is_map(init), do: Map.put(init, :ref, ref) defp put_ref(init, ref) when is_list(init), do: Keyword.put(init, :ref, ref) end