Skip to main content

lib/http/event_source.ex

defmodule HTTP.EventSource do
  @moduledoc """
  Browser-like EventSource client API for Elixir.

  Events are delivered as messages to the owner process:

      {HTTP.EventSource, source, %HTTP.EventSource.Event.Open{}}
      {HTTP.EventSource, source, %HTTP.EventSource.Event.Message{}}
      {HTTP.EventSource, source, %HTTP.EventSource.Event.Error{}}

  Custom server-sent event names are delivered through the message event's
  `type` field.
  """

  alias HTTP.EventSource.Connection
  alias HTTP.EventSource.Options

  defstruct pid: nil, ref: nil, url: nil, with_credentials: false

  @connecting 0
  @open 1
  @closed 2
  @call_timeout 5_000

  @type t :: %__MODULE__{
          pid: pid() | nil,
          ref: reference() | nil,
          url: String.t() | nil,
          with_credentials: boolean()
        }

  @spec connecting() :: 0
  def connecting, do: @connecting

  @spec open() :: 1
  def open, do: @open

  @spec closed() :: 2
  def closed, do: @closed

  @spec new(String.t() | URI.t(), keyword() | map()) :: t() | {:error, term()}
  def new(url, init \\ []) do
    ref = make_ref()

    with {:ok, options} <- Options.new(url, put_ref(init, ref)),
         {:ok, pid} <-
           DynamicSupervisor.start_child(
             HTTP.EventSource.ConnectionSupervisor,
             {Connection, options}
           ) do
      %__MODULE__{
        pid: pid,
        ref: ref,
        url: options.url,
        with_credentials: options.with_credentials
      }
    end
  end

  @spec url(t()) :: String.t() | nil
  def url(%__MODULE__{url: url}), do: url

  @spec with_credentials(t()) :: boolean()
  def with_credentials(%__MODULE__{with_credentials: with_credentials}), do: with_credentials

  @spec ready_state(t()) :: 0 | 1 | 2
  def ready_state(source), do: connection_call(source, :ready_state, @closed)

  @spec last_event_id(t()) :: String.t()
  def last_event_id(source), do: connection_call(source, :last_event_id, "")

  @spec reconnect_time(t()) :: non_neg_integer()
  def reconnect_time(source), do: connection_call(source, :reconnect_time, 0)

  @spec close(t()) :: :ok
  def close(source), do: connection_call(source, :close, :ok)

  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(_source, _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