lib/config_cat/cache_policy.ex

defmodule ConfigCat.CachePolicy do
  @moduledoc """
  Represents the [polling mode](https://configcat.com/docs/sdk-reference/elixir#polling-modes) used by ConfigCat.

  The *ConfigCat SDK* supports 3 different polling mechanisms to
  acquire the setting values from *ConfigCat*. After the latest
  setting values are downloaded, they are stored in the internal
  cache and all requests are served from there.

  With the following polling modes, you can customize the SDK to
  best fit to your application's lifecycle.

  ## Auto polling (default)

  The *ConfigCat SDK* downloads the latest values and stores them
  automatically on a regular schedule.

  See `auto/1` below for details.

  ## Lazy loading

  When calling any of the public API functions (like `get_value()`),
  the *ConfigCat SDK* downloads the latest setting values if they are
  not present or have expired. In this case the function will wait
  until the settings have been fetched before returning.

  See `lazy/1` below for details.

  ## Manual polling

  Manual polling gives you full control over when the setting
  values are downloaded. *ConfigCat SDK* will not update them
  automatically. Calling `ConfigCat.force_refresh/1` is your
  application's responsibility.

  See `manual/0` below for details.
  """

  @behaviour ConfigCat.CachePolicy.Behaviour

  alias ConfigCat.CachePolicy.Auto
  alias ConfigCat.CachePolicy.Behaviour
  alias ConfigCat.CachePolicy.Lazy
  alias ConfigCat.CachePolicy.Manual

  require ConfigCat.Constants, as: Constants

  @typedoc "Options for auto-polling mode."
  @type auto_options :: [
          {:max_init_wait_time_seconds, non_neg_integer()}
          | {:poll_interval_seconds, pos_integer()}
        ]

  @typedoc "Options for lazy-polling mode."
  @type lazy_options :: [{:cache_refresh_interval_seconds, non_neg_integer()}]

  @typedoc "Callback to call when configuration changes."
  @type on_changed_callback :: (-> :ok)

  @typedoc false
  @type option ::
          {:cache_policy, t()}
          | {:fetcher, module()}
          | {:instance_id, ConfigCat.instance_id()}
          | {:offline, boolean()}

  @typedoc false
  @type options :: [option]

  @typedoc "The polling mode"
  @opaque t :: Auto.t() | Lazy.t() | Manual.t()

  @doc """
  Auto-polling mode.

  The *ConfigCat SDK* downloads the latest values and stores them
  automatically on a regular schedule.

  Use the `max_init_wait_time_seconds` option to set the maximum waiting time
  between initialization and the first config acquisition. Defaults to 5 seconds
  if not specified.

  ```elixir
  ConfigCat.CachePolicy.auto(max_init_wait_time_seconds: 5)
  ```

  Use the `poll_interval_seconds` option to change the
  polling interval. Defaults to 60 seconds if not specified.

  ```elixir
  ConfigCat.CachePolicy.auto(poll_interval_seconds: 60)
  ```
  """
  @spec auto(auto_options()) :: t()
  def auto(options \\ []) do
    Auto.new(options)
  end

  @doc """
  Lazy polling mode.

  When calling any of the public API functions (like `get_value()`),
  the *ConfigCat SDK* downloads the latest setting values if they are
  not present or have expired. In this case the function will wait
  until the settings have been fetched before returning.

  Use the required `cache_refresh_interval_seconds` option to set the cache
  lifetime.

  ```elixir
  ConfigCat.CachePolicy.lazy(cache_refresh_interval_seconds: 300)
  ```
  """
  @spec lazy(lazy_options()) :: t()
  def lazy(options) do
    Lazy.new(options)
  end

  @doc """
  Manual polling mode.

  Manual polling gives you full control over when the setting
  values are downloaded. *ConfigCat SDK* will not update them
  automatically. Calling `ConfigCat.force_refresh/1` is your
  application's responsibility.

  ```elixir
  ConfigCat.CachePolicy.manual()
  ```
  """
  @spec manual :: t()
  def manual do
    Manual.new()
  end

  @doc false
  @spec child_spec(options()) :: Supervisor.child_spec()
  def child_spec(options) do
    %policy_module{} = Keyword.fetch!(options, :cache_policy)
    policy_module.child_spec(options)
  end

  @impl Behaviour
  def get(instance_id) do
    instance_id
    |> via_tuple()
    |> GenServer.call(:get, Constants.fetch_timeout())
  end

  @impl Behaviour
  def offline?(instance_id) do
    instance_id
    |> via_tuple()
    |> GenServer.call(:offline?, Constants.fetch_timeout())
  end

  @impl Behaviour
  def set_offline(instance_id) do
    instance_id
    |> via_tuple()
    |> GenServer.call(:set_offline, Constants.fetch_timeout())
  end

  @impl Behaviour
  def set_online(instance_id) do
    instance_id
    |> via_tuple()
    |> GenServer.call(:set_online, Constants.fetch_timeout())
  end

  @impl Behaviour
  def force_refresh(instance_id) do
    instance_id
    |> via_tuple()
    |> GenServer.call(:force_refresh, Constants.fetch_timeout())
  end

  @doc false
  @spec via_tuple(ConfigCat.instance_id()) :: {:via, module(), term()}
  def via_tuple(instance_id) do
    {:via, Registry, {ConfigCat.Registry, {__MODULE__, instance_id}}}
  end
end