lib/aino/chunked_handler.ex

defmodule Aino.ChunkedHandler do
  @moduledoc """
  Chunked Response Handler

  In order to [send chunked data](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Transfer-Encoding)
  with Aino, you must use a ChunkedHandler.

  To use chunked response, you must return `chunk: true` and `handler: YourHandler` in the token.

  # EventStream Example

  In the example below, a Ping route starts a chunked response.

  ```elixir
  defmodule Web.Ping do
    def index(token) do
      token
      |> Token.response_status(200)
      |> Token.response_header("Content-Type", "text/event-stream")
      |> Map.put(:chunk, true)
      |> Map.put(:handler, Web.Ping.Handler)
    end
  end

  defmodule Web.Ping.Handler do
    @behaviour Aino.ChunkedHandler

    @impl true
    def init(token) do
      :timer.send_interval(1000, :ping)

      {:ok, token}
    end

    @impl true
    def handle(:ping, token) do
      json = Jason.encode!(%{time: DateTime.utc_now()})
      response = "event: ping\ndata: #\{json}\n\n"
      {:ok, response, token}
    end
  end
  ```
  """

  @doc """
  Initialize your handler

  Called when the chunked response initializes.
  """
  @callback init(Aino.Token.t()) :: {:ok, Aino.Token.t()}

  @doc """
  Handle incoming messages

  Called when any incoming messages are sent to the GenServer processing your chunked response
  """
  @callback handle(any(), Aino.Token.t()) :: {:ok, Aino.Token.t()}
end

defmodule Aino.ChunkedHandler.Server do
  @moduledoc false

  # An internal GenServer to send messages to the elli process which
  # is receiving `:chunk` messages.
  #
  # See the github link below for specifics:
  #
  # https://github.com/elli-lib/elli/blob/067909111326ca96609d89643771a29fede052d7/src/elli_http.erl#L358

  use GenServer

  @doc false
  def start_link(token) do
    GenServer.start_link(__MODULE__, token)
  end

  @impl true
  def init(token) do
    # If the client disconnects, terminate the chunked handler
    Process.flag(:trap_exit, true)

    {:ok, token} = token.handler.init(token)

    {:ok, token}
  end

  @impl true
  def handle_info(message, token) do
    case token.handler.handle(message, token) do
      {:ok, %Aino.Event{} = response, token} ->
        send(token.request.pid, {:chunk, to_string(response)})
        {:noreply, token}

      {:ok, response, token} ->
        send(token.request.pid, {:chunk, response})
        {:noreply, token}

      {:ok, token} ->
        {:noreply, token}
    end
  end
end