lib/loop.ex

defmodule Strom.Loop do
  use GenServer

  @default_timeout 5_000
  defstruct data: [], pid: nil, infinite: false, last_data_at: nil, timeout: @default_timeout

  def start, do: start([])

  def start(%__MODULE__{} = loop), do: loop

  def start(opts) do
    loop = %__MODULE__{
      timeout: Keyword.get(opts, :timeout, @default_timeout)
    }

    {:ok, pid} = GenServer.start_link(__MODULE__, loop)
    __state__(pid)
  end

  @impl true
  def init(%__MODULE__{} = loop), do: {:ok, %{loop | pid: self()}}

  def call(%__MODULE__{} = loop), do: GenServer.call(loop.pid, :get_data)

  def call(%__MODULE__{} = loop, data), do: GenServer.call(loop.pid, {:put_data, data})

  def stop(%__MODULE__{} = loop), do: GenServer.call(loop.pid, :stop)

  def infinite?(%__MODULE__{infinite: infinite}), do: infinite

  def __state__(pid) when is_pid(pid), do: GenServer.call(pid, :__state__)

  @impl true
  def handle_call(:get_data, _from, %__MODULE__{data: data} = loop) do
    last_data_at = if is_nil(loop.last_data_at), do: time_now(), else: loop.last_data_at
    loop = %{loop | data: [], last_data_at: last_data_at}

    case data do
      [] ->
        if time_now() - last_data_at > loop.timeout do
          {:reply, {:error, {:halt, loop}}, loop}
        else
          {:reply, {:ok, {[], loop}}, loop}
        end

      data ->
        {:reply, {:ok, {data, loop}}, loop}
    end
  end

  def handle_call({:put_data, data}, _from, %__MODULE__{} = loop) do
    loop = %{loop | data: loop.data ++ [data], last_data_at: time_now()}
    {:reply, {:ok, {[], loop}}, loop}
  end

  def handle_call(:stop, _from, %__MODULE__{} = loop) do
    {:stop, :normal, :ok, loop}
  end

  def handle_call(:__state__, _from, state), do: {:reply, state, state}

  defp time_now do
    "Etc/UTC"
    |> DateTime.now!()
    |> DateTime.to_unix(:millisecond)
  end
end