lib/async_action/base.ex

defmodule Rephex.AsyncAction.Base do
  @moduledoc """
  Define behavior of AsyncAction.
  """

  alias Phoenix.LiveView.Socket

  # Path to target AsyncResult
  @type result_path :: [term()]
  @type state :: map()
  @type payload :: map()
  @type progress :: any()
  @type success_result :: any()
  @type exit_reason :: any()
  @type failed_value :: any()

  @doc """
  Get initial progress. This value will be set synchronously before the async action starts.

  ## Example

      @impl true
      def initial_progress(_path, _payload) do
        # optional but recommended
        # `start/4` apply this progress synchronously.
        # AsyncResult.loading will be `{progress, _meta_values}` before start_async.
        {0, 100}
      end
  """
  @callback initial_progress(result_path(), payload()) :: progress()

  @doc """
  Before the async action starts, this callback will be called.

  ## Example

      @impl true
      def before_start(socket, _result_path, %{amount: _amount} = _payload) do
        # optional
        # This function will be called before `start_async`.
        socket |> LiveView.put_flash(:info, "Add twice start")
      end
  """
  @callback before_start(Socket.t(), result_path(), payload()) :: Socket.t()

  @doc ~S"""
  After the async action is resolved, this callback will be called.

  ## Example

      @impl true
      def after_resolve(socket, _result_path, result) do
        # optional
        # This function will be called after `start_async` is finished.
        case result do
          {:ok, amount} ->
            socket
            |> State.add_count(%{amount: amount})
            |> LiveView.put_flash(:info, "Add twice done: #{amount}")

          {:exit, _reason} ->
            socket
            |> LiveView.put_flash(:error, "Add twice failed")
        end
      end
  """
  @callback after_resolve(
              Socket.t(),
              result_path(),
              {:ok, success_result()} | {:exit, exit_reason()}
            ) :: Socket.t()

  @doc ~S"""
  Generate failed value. This value will be set to AsyncResult via `AsyncResult.failed/2`.

  ## Example

      @impl true
      def generate_failed_value(_result_path, exit_reason) do
        # optional
        # You can customize the failed value.
        case exit_reason do
          {:shutdown, :cancel} -> "canceled by no-reason"
          {:shutdown, {:cancel, text}} when is_bitstring(text) -> "canceled by #{text}"
          _ -> "unknown reason"
        end
      end

  """
  @callback generate_failed_value(result_path(), exit_reason()) :: failed_value()

  @doc """
  Start async action.

  ## Example

      @impl true
      def start_async(_state, _path, %{amount: amount} = _payload, progress) do
        # required
        # This function will be passed to Phoenix's `start_async`.
        max = 500
        progress.({0, max})

        1..max
        |> Enum.each(fn i ->
          :timer.sleep(2)
          progress.({i, max})
        end)

        amount
      end
  """
  @callback start_async(
              state(),
              result_path(),
              payload(),
              (progress() -> nil)
            ) :: success_result()

  @doc """
  This callback will be implemented by __using__.
  """
  @callback options() :: %{optional(:throttle) => non_neg_integer()}

  @optional_callbacks initial_progress: 2,
                      before_start: 3,
                      after_resolve: 3,
                      generate_failed_value: 2
end