defmodule Nebulex.Cache do
  @moduledoc """
  Cache abstraction layer inspired by
  [Ecto](https://github.com/elixir-ecto/ecto).

  A cache maps to an underlying in-memory storage controlled by the adapter.
  For example, Nebulex ships with an adapter that implements a local
  generational cache.

  The cache expects the `:otp_app` and `:adapter` as options when used.
  The `:otp_app` should point to an OTP application with the cache
  configuration. See the compile time options for more information:

  #{Nebulex.Cache.Options.compile_options_docs()}

  For example, the cache:

      defmodule MyApp.Cache do
        use Nebulex.Cache,
          otp_app: :my_app,
          adapter: Nebulex.Adapters.Local
      end

  Could be configured with:

      config :my_app, MyApp.Cache,
        gc_interval: :timer.hours(12),
        max_size: 1_000_000,
        allocated_memory: 2_000_000_000,
        gc_memory_check_interval: :timer.seconds(10)

  Most of the configuration that goes into the `config` is specific
  to the adapter. For this particular example, you can check
  [`Nebulex.Adapters.Local`][local_adapter] for more information.
  Despite this, the following configuration values are shared
  across all adapters:

  #{Nebulex.Cache.Options.start_link_options_docs()}

  [local_adapter]: http://hexdocs.pm/nebulex_local/3.0.0-rc.1/Nebulex.Adapters.Local.html

  ## Shared options

  All of the cache functions outlined in this module accept the following
  options:

  #{Nebulex.Cache.Options.runtime_shared_options_docs()}

  > #### Adapter-specific options {: .info}
  >
  > In addition to the shared options, each adapter can define its
  > specific options. Therefore, Nebulex recommends reviewing the
  > adapter's documentation.

  ## Telemetry events

  There are two types of telemetry events. The ones emitted by Nebulex and the
  ones that are adapter specific. The ones emitted by Nebulex are divided into
  two categories: cache lifecycle events and cache command events. Let us take
  a closer look at each of them.

  ### Cache lifecycle events

  All Nebulex caches emit the following events:

    * `[:nebulex, :cache, :init]` - It is dispatched whenever a cache starts.
      The only measurement is the current system time in native units from
      calling: `System.system_time()`. The `:opts` key in the metadata
      contains all initialization options.

      * Measurement: `%{system_time: integer()}`
      * Metadata: `%{cache: module(), name: atom(), opts: keyword()}`

  ### Cache command events

  When the option `:telemetry` is set to `true` (the default), Nebulex will
  emit Telemetry span events for each cache command. Those events will use
  the `:telemetry_prefix` outlined in the options above which defaults to
  `[:my_app, :cache]`.

  For instance, to receive all events published for the cache `MyApp.Cache`,
  one could define a module:

      defmodule MyApp.Telemetry do
        def handle_event(
              [:my_app, :cache, :command, event],
              measurements,
              metadata,
              _config
            ) do
          case event do
            :start ->
              # Handle start event ...
              IO.puts("Cache command started: \#{inspect(metadata.command)}")
              IO.puts("Arguments: \#{inspect(metadata.args)}")

            :stop ->
              # Handle stop event ...
              duration = measurements.duration
              IO.puts("Cache command completed: \#{inspect(metadata.command)}")
              IO.puts("Duration: \#{duration} native units")
              IO.puts("Result: \#{inspect(metadata.result)}")

            :exception ->
              # Handle exception event ...
              IO.puts("Cache command failed: \#{inspect(metadata.command)}")
              IO.puts("Error: \#{inspect(metadata.reason)}")
              IO.puts("Stacktrace: \#{inspect(metadata.stacktrace)}")
          end
        end
      end

  Then, in the `Application.start/2` callback, attach the handler to the events
  using a unique handler id:

      :telemetry.attach_many(
        "my-app-handler-id",
        [
          [:my_app, :cache, :command, :start],
          [:my_app, :cache, :command, :stop],
          [:my_app, :cache, :command, :exception]
        ],
        &MyApp.Telemetry.handle_event/4,
        :no_config
      )

  See the [telemetry documentation](http://hexdocs.pm/nebulex/3.0.0-rc.1/telemetry.html)
  for more information.

  The following are the events you should expect from Nebulex. All examples
  below consider a cache named `MyApp.Cache`:

  #### `[:my_app, :cache, :command, :start]`

  This event is emitted before a cache command is executed.

  The `:measurements` map will include the following:

    * `:system_time` - The current system time in native units from calling:
      `System.system_time()`.

  A Telemetry `:metadata` map including the following fields:

    * `:adapter_meta` - The adapter metadata.
    * `:command` - The name of the invoked adapter's command.
    * `:args` - The arguments passed to the invoked adapter, except for the
      first one, since the adapter's metadata is available in the event's
      metadata.
    * `:extra_metadata` - Additional metadata through the runtime option
      `:telemetry_metadata.`

  Example event data:

      %{
        measurements: %{system_time: 1_678_123_456_789},
        metadata: %{
          adapter_meta: %{...},
          command: :put,
          args: ["key", "value", [ttl: :timer.seconds(10)]],
          extra_metadata: %{user_id: 123}
        }
      }

  #### `[:my_app, :cache, :command, :stop]`

  This event is emitted after a cache command is executed.

  The `:measurements` map will include the following:

    * `:duration` - The time spent executing the cache command. The measurement
      is given in the `:native` time unit. You can read more about it in the
      docs for `System.convert_time_unit/3`.

  A Telemetry `:metadata` map including the following fields:

    * `:adapter_meta` - The adapter metadata.
    * `:command` - The name of the invoked adapter's command.
    * `:args` - The arguments passed to the invoked adapter, except for the
      first one, since the adapter's metadata is available in the event's
      metadata.
    * `:extra_metadata` - Additional metadata through the runtime option
      `:telemetry_metadata.`
    * `:result` - The command's result.

  Example event data:

      %{
        measurements: %{duration: 1_234_567},
        metadata: %{
          adapter_meta: %{...},
          command: :put,
          args: ["key", "value", [ttl: :timer.seconds(10)]],
          extra_metadata: %{user_id: 123},
          result: :ok
        }
      }

  #### `[:my_app, :cache, :command, :exception]`

  This event is emitted when an error or exception occurs during the
  cache command execution.

  The `:measurements` map will include the following:

    * `:duration` - The time spent executing the cache command. The measurement
      is given in the `:native` time unit. You can read more about it in the
      docs for `System.convert_time_unit/3`.

  A Telemetry `:metadata` map including the following fields:

    * `:adapter_meta` - The adapter metadata.
    * `:command` - The name of the invoked adapter's command.
    * `:args` - The arguments passed to the invoked adapter, except for the
      first one, since the adapter's metadata is available in the event's
      metadata.
    * `:extra_metadata` - Additional metadata through the runtime option
      `:telemetry_metadata.`
    * `:kind` - The type of the error: `:error`, `:exit`, or `:throw`.
    * `:reason` - The reason of the error.
    * `:stacktrace` - Exception's stack trace.

  Example event data:

      %{
        measurements: %{duration: 1_234_567},
        metadata: %{
          adapter_meta: %{...},
          command: :put,
          args: ["key", "value", [ttl: :timer.seconds(10)]],
          extra_metadata: %{user_id: 123},
          kind: :error,
          reason: %Nebulex.KeyError{key: "key", reason: :not_found},
          stacktrace: [...]
        }
      }

  ### Adapter-specific events

  Regardless of whether Nebulex emits the telemetry events outlined above or
  not, the adapters can and are free to expose their own, but they will be
  out of Nebulex's scope. Therefore, if you are interested in using specific
  adapter events, you should review the adapters' documentation.

  ## Dynamic caches

  Nebulex allows you to start multiple processes from the same cache module.
  This feature is typically useful when you want to have different cache
  instances but access them through the same cache module.

  When you list a cache in your supervision tree, such as `MyApp.Cache`, it will
  start a supervision tree with a process named `MyApp.Cache` under the hood.
  By default, the process has the same name as the cache module. Hence, whenever
  you invoke a function in `MyApp.Cache`, such as `MyApp.Cache.put/3`, Nebulex
  will execute the command in the cache process named `MyApp.Cache`.

  However, with Nebulex, you can start multiple processes from the same cache.
  The only requirement is that they must have different process names, like
  this:

      children = [
        MyApp.Cache,
        {MyApp.Cache, name: MyApp.UsersCache}
      ]

  Now you have two cache instances running: one is named `MyApp.Cache`, and the
  other one is named `MyApp.UsersCache`. You can tell Nebulex which process you
  want to use in your cache operations by calling:

      MyApp.Cache.put_dynamic_cache(MyApp.Cache)
      MyApp.Cache.put_dynamic_cache(MyApp.UsersCache)

  Once you call `MyApp.Cache.put_dynamic_cache(name)`, all invocations made on
  `MyApp.Cache` will use the cache instance denoted by `name`.

  Nebulex also provides a handy function for invoking commands using dynamic
  caches: `c:with_dynamic_cache/2`.

      MyApp.Cache.with_dynamic_cache(MyApp.UsersCache, fn ->
        # all commands here will use MyApp.UsersCache
        MyApp.Cache.put("u1", "joe")
        ...
      end)

  While these functions are handy, you may want to have the ability to pass
  the dynamic cache directly to the command, avoiding the boilerplate logic
  of using `c:put_dynamic_cache/1` or `c:with_dynamic_cache/2`. From **v3.0**,
  all Cache API commands expose an extended callback version that admits a
  dynamic cache at the first argument, so you can directly interact with a
  cache instance.

      MyApp.Cache.put(MyApp.UsersCache, "u1", "joe", ttl: :timer.hours(1))
      MyApp.Cache.get(MyApp.UsersCache, "u1", nil, [])
      MyApp.Cache.delete(MyApp.UsersCache, "u1", [])

  This is another handy way to work with multiple cache instances through
  the same cache module.

  ## Distributed topologies

  One of the goals of Nebulex is also to provide the ability to set up
  distributed cache topologies, but this feature will depend on the adapters.

  See ["Nebulex Distributed"](https://github.com/elixir-nebulex/nebulex_distributed).
  """

  @typedoc "Cache type"
  @type t() :: module()

  @typedoc "Cache entry key"
  @type key() :: any()

  @typedoc "Cache entry value"
  @type value() :: any()

  @typedoc "Dynamic cache value"
  @type dynamic_cache() :: atom() | pid()

  @typedoc "Cache entries"
  @type entries() :: map() | [{key(), value()}]

  @typedoc "Cache action options"
  @type opts() :: keyword()

  @typedoc """
  The data type for a query spec.

  See the ["query-spec"](#c:get_all/2-query-specification) section for more
  information.
  """
  @type query_spec() :: keyword()

  @typedoc "Specification key for the item(s) to include in the returned info"
  @type info_spec() :: :all | atom() | [atom()]

  @typedoc "The type for the info item's value"
  @type info_item() :: any()

  @typedoc "Info map"
  @type info_map() :: %{optional(atom()) => any()}

  @typedoc "The data type for the cache information"
  @type info_data() :: info_map() | info_item()

  @typedoc "Proxy type to a cache event listener"
  @type event_listener() :: Nebulex.Event.listener()

  @typedoc "Proxy type to a cache event  filter"
  @type event_filter() :: Nebulex.Event.filter()

  @typedoc "Proxy type for generic Nebulex error"
  @type nbx_error_reason() :: Nebulex.Error.t()

  @typedoc "Fetch error reason"
  @type fetch_error_reason() :: Nebulex.KeyError.t() | nbx_error_reason()

  @typedoc "Common error type"
  @type error_tuple() :: error_tuple(nbx_error_reason())

  @typedoc "Error type for the given reason"
  @type error_tuple(reason) :: {:error, reason}

  @typedoc "Ok/Error tuple with default error reasons"
  @type ok_error_tuple(ok) :: ok_error_tuple(ok, nbx_error_reason())

  @typedoc "Ok/Error type"
  @type ok_error_tuple(ok, error) :: {:ok, ok} | {:error, error}

  @typedoc "Fetch or store function"
  @type fetch_or_store_fun() :: (-> {:ok, value()} | {:error, any()})

  @typedoc "Get or store function"
  @type get_or_store_fun() :: (-> any())

  ## API

  import __MODULE__.Helpers

  @doc false
  defmacro __using__(opts) do
    quote do
      unquote(prelude(opts))
      unquote(base_defs())
      unquote(kv_defs())

      if Nebulex.Adapter.Queryable in behaviours do
        unquote(queryable_defs())
      end

      if Nebulex.Adapter.Transaction in behaviours do
        unquote(transaction_defs())
      end

      if Nebulex.Adapter.Info in behaviours do
        unquote(info_defs())
      end

      if Nebulex.Adapter.Observable in behaviours do
        unquote(event_defs())
      end
    end
  end

  defp prelude(opts) do
    quote do
      @behaviour Nebulex.Cache

      {otp_app, adapter, behaviours, opts} =
        Nebulex.Cache.Supervisor.compile_config(unquote(opts))

      @otp_app otp_app
      @adapter adapter
      @opts opts
      @default_dynamic_cache @opts[:default_dynamic_cache] || __MODULE__

      @before_compile adapter
    end
  end

  defp base_defs do
    quote do
      ## Config and metadata

      @impl true
      def config do
        {:ok, config} = Nebulex.Cache.Supervisor.runtime_config(__MODULE__, @otp_app, [])

        config
      end

      @impl true
      def __adapter__, do: @adapter

      ## Process lifecycle

      @doc false
      def child_spec(opts) do
        %{
          id: __MODULE__,
          start: {__MODULE__, :start_link, [opts]},
          type: :supervisor
        }
      end

      @impl true
      def start_link(opts \\ []) do
        Nebulex.Cache.Supervisor.start_link(__MODULE__, @otp_app, @adapter, opts)
      end

      @impl true
      def stop(opts \\ []) do
        stop(get_dynamic_cache(), opts)
      end

      @impl true
      def stop(name, opts) do
        Supervisor.stop(name, :normal, Keyword.get(opts, :timeout, 5000))
      end

      # Iniline common instructions
      @compile {:inline, get_dynamic_cache: 0}

      @impl true
      def get_dynamic_cache do
        Process.get({__MODULE__, :dynamic_cache}, @default_dynamic_cache)
      end

      @impl true
      def put_dynamic_cache(dynamic) when is_atom(dynamic) or is_pid(dynamic) do
        Process.put({__MODULE__, :dynamic_cache}, dynamic) || @default_dynamic_cache
      end

      @impl true
      def with_dynamic_cache(name, fun) do
        default_dynamic_cache = get_dynamic_cache()

        try do
          _ = put_dynamic_cache(name)

          fun.()
        after
          _ = put_dynamic_cache(default_dynamic_cache)
        end
      end
    end
  end

  defp kv_defs do
    quote do
      alias Nebulex.Cache.KV

      defcacheapi fetch(key, opts \\ []), to: KV

      defcacheapi fetch!(key, opts \\ []), to: KV

      defcacheapi get(key, default \\ nil, opts \\ []), to: KV

      defcacheapi get!(key, default \\ nil, opts \\ []), to: KV

      defcacheapi put(key, value, opts \\ []), to: KV

      defcacheapi put!(key, value, opts \\ []), to: KV

      defcacheapi put_new(key, value, opts \\ []), to: KV

      defcacheapi put_new!(key, value, opts \\ []), to: KV

      defcacheapi replace(key, value, opts \\ []), to: KV

      defcacheapi replace!(key, value, opts \\ []), to: KV

      defcacheapi put_all(entries, opts \\ []), to: KV

      defcacheapi put_all!(entries, opts \\ []), to: KV

      defcacheapi put_new_all(entries, opts \\ []), to: KV

      defcacheapi put_new_all!(entries, opts \\ []), to: KV

      defcacheapi delete(key, opts \\ []), to: KV

      defcacheapi delete!(key, opts \\ []), to: KV

      defcacheapi take(key, opts \\ []), to: KV

      defcacheapi take!(key, opts \\ []), to: KV

      defcacheapi has_key?(key, opts \\ []), to: KV

      defcacheapi ttl(key, opts \\ []), to: KV

      defcacheapi ttl!(key, opts \\ []), to: KV

      defcacheapi expire(key, ttl, opts \\ []), to: KV

      defcacheapi expire!(key, ttl, opts \\ []), to: KV

      defcacheapi touch(key, opts \\ []), to: KV

      defcacheapi touch!(key, opts \\ []), to: KV

      defcacheapi incr(key, amount \\ 1, opts \\ []), to: KV

      defcacheapi incr!(key, amount \\ 1, opts \\ []), to: KV

      defcacheapi decr(key, amount \\ 1, opts \\ []), to: KV

      defcacheapi decr!(key, amount \\ 1, opts \\ []), to: KV

      defcacheapi get_and_update(key, fun, opts \\ []), to: KV

      defcacheapi get_and_update!(key, fun, opts \\ []), to: KV

      defcacheapi update(key, initial, fun, opts \\ []), to: KV

      defcacheapi update!(key, initial, fun, opts \\ []), to: KV

      defcacheapi fetch_or_store(key, fun, opts \\ []), to: KV

      defcacheapi fetch_or_store!(key, fun, opts \\ []), to: KV

      defcacheapi get_or_store(key, fun, opts \\ []), to: KV

      defcacheapi get_or_store!(key, fun, opts \\ []), to: KV
    end
  end

  defp queryable_defs do
    quote do
      alias Nebulex.Cache.Queryable

      defcacheapi get_all(query_spec \\ [], opts \\ []), to: Queryable

      defcacheapi get_all!(query_spec \\ [], opts \\ []), to: Queryable

      defcacheapi count_all(query_spec \\ [], opts \\ []), to: Queryable

      defcacheapi count_all!(query_spec \\ [], opts \\ []), to: Queryable

      defcacheapi delete_all(query_spec \\ [], opts \\ []), to: Queryable

      defcacheapi delete_all!(query_spec \\ [], opts \\ []), to: Queryable

      defcacheapi stream(query_spec \\ [], opts \\ []), to: Queryable

      defcacheapi stream!(query_spec \\ [], opts \\ []), to: Queryable
    end
  end

  defp transaction_defs do
    quote do
      alias Nebulex.Cache.Transaction

      defcacheapi transaction(fun, opts \\ []), to: Transaction

      defcacheapi in_transaction?(opts \\ []), to: Transaction
    end
  end

  defp info_defs do
    quote do
      alias Nebulex.Cache.Info

      defcacheapi info(spec \\ :all, opts \\ []), to: Info

      defcacheapi info!(spec \\ :all, opts \\ []), to: Info
    end
  end

  defp event_defs do
    quote do
      alias Nebulex.Cache.Observable

      defcacheapi register_event_listener(listener, opts \\ []), to: Observable

      defcacheapi register_event_listener!(listener, opts \\ []), to: Observable

      defcacheapi unregister_event_listener(id, opts \\ []), to: Observable

      defcacheapi unregister_event_listener!(id, opts \\ []), to: Observable
    end
  end

  ## User callbacks

  @optional_callbacks init: 1

  @doc """
  A callback executed when the cache starts or when configuration is read.
  """
  @doc group: "User callbacks"
  @callback init(config) :: {:ok, config} | :ignore when config: keyword()

  ## Nebulex.Adapter

  @doc """
  Returns the adapter tied to the cache.
  """
  @doc group: "Runtime API"
  @callback __adapter__() :: Nebulex.Adapter.t()

  @doc """
  Returns the adapter configuration stored in the `:otp_app` environment.

  If the `c:init/1` callback is implemented in the cache, it will be invoked.
  """
  @doc group: "Runtime API"
  @callback config() :: keyword()

  @doc """
  Starts a supervision and return `{:ok, pid}` or just `:ok` if nothing
  needs to be done.

  Returns `{:error, {:already_started, pid}}` if the cache is already
  started or `{:error, term}` in case anything else goes wrong.

  ## Options

  See the configuration in the moduledoc for options shared between adapters,
  for adapter-specific configuration see the adapter's documentation.
  """
  @doc group: "Runtime API"
  @callback start_link(opts()) ::
              {:ok, pid()}
              | {:error, {:already_started, pid()}}
              | {:error, any()}

  @doc """
  Shuts down the cache.

  ## Options

    `:timeout` - It is an integer that specifies how many milliseconds to wait
    for the cache supervisor process to terminate, or the atom `:infinity` to
    wait indefinitely. Defaults to `5000`. See `Supervisor.stop/3`.

  See the ["Shared options"](#module-shared-options) section in the module
  documentation for more options.
  """
  @doc group: "Runtime API"
  @callback stop(opts()) :: :ok

  @doc """
  Same as `c:stop/1` but stops the cache instance given in the first argument
  `dynamic_cache`.

  See the ["Dynamic caches"](#module-dynamic-caches) section in the
  module documentation for more information.
  """
  @doc group: "Runtime API"
  @callback stop(dynamic_cache(), opts()) :: :ok

  @doc """
  Returns the atom name or pid of the current cache
  (based on Ecto dynamic repo).

  See also `c:put_dynamic_cache/1`.
  """
  @doc group: "Runtime API"
  @callback get_dynamic_cache() :: dynamic_cache()

  @doc """
  Sets the dynamic cache to be used in further commands
  (based on Ecto dynamic repo).

  There are cases where you may want to have different cache instances but
  access them through the same cache module. By default, when you call
  `MyApp.Cache.start_link/1`, it will start a cache with the name
  `MyApp.Cache`. But it is also possible to start multiple caches by using
  a different name for each of them:

      MyApp.Cache.start_link(name: :cache1)
      MyApp.Cache.start_link(name: :cache2)

  You can also start caches without names by explicitly setting the name
  to `nil`:

      MyApp.Cache.start_link(name: nil)

  > **NOTE:** There may be adapters requiring the `:name` option anyway,
    therefore, it is highly recommended to see the adapter's documentation
    you want to use.

  All operations through `MyApp.Cache` are sent by default to the cache named
  `MyApp.Cache`. But you can change the default cache at compile-time:

      use Nebulex.Cache, default_dynamic_cache: :cache_name

  Or anytime at runtime by calling `put_dynamic_cache/1`:

      MyApp.Cache.put_dynamic_cache(:another_cache_name)

  From this moment on, all future commands performed by the current process
  will run on `:another_cache_name`.

  Additionally, all cache commands optionally support passing the wanted
  dynamic cache (name or PID) as the first argument so you can o directly
  interact with a cache instance. See the
  ["Dynamic caches"](#module-dynamic-caches) section in the module
  documentation for more information.
  """
  @doc group: "Runtime API"
  @callback put_dynamic_cache(dynamic_cache()) :: dynamic_cache()

  @doc """
  Invokes the function `fun` using the given dynamic cache.

  ## Example

      MyCache.with_dynamic_cache(:my_cache, fn ->
        MyCache.put("foo", "var")
      end)

  See `c:get_dynamic_cache/0` and `c:put_dynamic_cache/1`.
  """
  @doc group: "Runtime API"
  @callback with_dynamic_cache(dynamic_cache(), fun()) :: any()

  ## Nebulex.Adapter.KV

  @doc """
  Fetches the value for a specific `key` in the cache.

  If the cache contains the given `key`, then its value is returned
  in the shape of `{:ok, value}`.

  If there's an error with executing the command, `{:error, reason}`
  is returned. `reason` is the cause of the error and can be
  `Nebulex.KeyError` if the cache does not contain `key`,
  `Nebulex.Error` otherwise.

  ## Options

  See the ["Shared options"](#module-shared-options) section in the module
  documentation for more options.

  ## Examples

      iex> MyCache.put("foo", "bar")
      :ok
      iex> MyCache.fetch("foo")
      {:ok, "bar"}

      # Key not found error
      iex> {:error, %Nebulex.KeyError{key: "bar"} = e} = MyCache.fetch("bar")
      iex> e.reason
      :not_found

  """
  @doc group: "KV API"
  @callback fetch(key(), opts()) :: ok_error_tuple(value(), fetch_error_reason())

  @doc """
  Same as `c:fetch/2`, but the command is executed on the cache instance
  given at the first argument `dynamic_cache`.

  See the ["Dynamic caches"](#module-dynamic-caches) section in the
  module documentation for more information.

  ## Examples

      iex> MyCache.put("foo", "bar")
      :ok
      iex> MyCache.fetch(MyCache1, "foo", [])
      {:ok, "bar"}

  """
  @doc group: "KV API"
  @callback fetch(dynamic_cache(), key(), opts()) :: ok_error_tuple(value(), fetch_error_reason())

  @doc """
  Same as `c:fetch/2` but raises `Nebulex.KeyError` if the cache doesn't contain
  `key` or `Nebulex.Error` if another error occurs while executing the command.

  ## Examples

      iex> MyCache.put("foo", "bar")
      :ok
      iex>  MyCache.fetch!("foo")
      "bar"

  """
  @doc group: "KV API"
  @callback fetch!(key(), opts()) :: value()

  @doc """
  Same as `c:fetch/3` but raises `Nebulex.KeyError` if the cache doesn't contain
  `key` or `Nebulex.Error` if another error occurs while executing the command.
  """
  @doc group: "KV API"
  @callback fetch!(dynamic_cache(), key(), opts()) :: value()

  @doc """
  Gets a value from the cache where the key matches the given `key`.

  If the cache contains the given `key` its value is returned as
  `{:ok, value}`.

  If the cache does not contain `key`, `{:ok, default}` is returned.

  If there's an error with executing the command, `{:error, reason}`
  is returned, where `reason` is the cause of the error.

  ## Options

  See the ["Shared options"](#module-shared-options) section in the module
  documentation for more options.

  ## Examples

      iex> MyCache.put("foo", "bar")
      :ok
      iex> MyCache.get("foo")
      {:ok, "bar"}
      iex> MyCache.get(:nonexistent)
      {:ok, nil}
      iex> MyCache.get(:nonexistent, :default)
      {:ok, :default}

  """
  @doc group: "KV API"
  @callback get(key(), default :: value(), opts()) :: ok_error_tuple(value())

  @doc """
  Same as `c:get/3`, but the command is executed on the cache instance
  given at the first argument `dynamic_cache`.

  See the ["Dynamic caches"](#module-dynamic-caches) section in the
  module documentation for more information.

  ## Examples

      iex> MyCache.get(MyCache1, "key", nil, [])
      {:ok, nil}

  """
  @doc group: "KV API"
  @callback get(dynamic_cache(), key(), default :: value(), opts()) :: ok_error_tuple(value())

  @doc """
  Same as `c:get/3` but raises an exception if an error occurs.
  """
  @doc group: "KV API"
  @callback get!(key(), default :: value(), opts()) :: value()

  @doc """
  Same as `c:get!/4` but raises an exception if an error occurs.
  """
  @doc group: "KV API"
  @callback get!(dynamic_cache(), key(), default :: value(), opts()) :: value()

  @doc """
  Puts the given `value` under `key` into the cache.

  If `key` already holds an entry, it is overwritten. Any previous TTL
  (time to live) associated with the key is discarded on a successful
  `put` operation.

  Returns `:ok` if successful; `{:error, reason}` otherwise.

  ## Options

  #{Nebulex.Cache.Options.put_options_docs()}

  See the ["Shared options"](#module-shared-options) section in the module
  documentation for more options.

  ## Examples

      iex> MyCache.put("foo", "bar")
      :ok

  Putting entries with specific time-to-live:

      iex> MyCache.put("foo", "bar", ttl: :timer.seconds(10))
      :ok
      iex> MyCache.put("foo", "bar", ttl: :timer.hours(1))
      :ok
      iex> MyCache.put("foo", "bar", ttl: :timer.minutes(1))
      :ok
      iex> MyCache.put("foo", "bar", ttl: :timer.seconds(30))
      :ok

  To keep the current TTL in case the key already exists:

      iex> MyCache.put("foo", "bar", keep_ttl: true)
      :ok

  """
  @doc group: "KV API"
  @callback put(key(), value(), opts()) :: :ok | error_tuple()

  @doc """
  Same as `c:put/3`, but the command is executed on the cache instance
  given at the first argument `dynamic_cache`.

  See the ["Dynamic caches"](#module-dynamic-caches) section in the
  module documentation for more information.

  ## Examples

      iex> MyCache.put(MyCache1, "foo", "bar", [])
      :ok
      iex> MyCache.put(MyCache2, "foo", "bar", ttl: :timer.hours(1))
      :ok

  """
  @doc group: "KV API"
  @callback put(dynamic_cache(), key(), value(), opts()) :: :ok | error_tuple()

  @doc """
  Same as `c:put/3` but raises an exception if an error occurs.
  """
  @doc group: "KV API"
  @callback put!(key(), value(), opts()) :: :ok

  @doc """
  Same as `c:put!/4` but raises an exception if an error occurs.
  """
  @doc group: "KV API"
  @callback put!(dynamic_cache(), key(), value(), opts()) :: :ok

  @doc """
  Puts the given `value` under `key` into the cache only if it does not
  already exist.

  Returns `{:ok, true}` if the value is stored; otherwise, `{:ok, false}`
  is returned.

  If there's an error with executing the command, `{:error, reason}`
  is returned, where `reason` is the cause of the error.

  ## Options

  #{Nebulex.Cache.Options.write_options_docs()}

  See the ["Shared options"](#module-shared-options) section in the module
  documentation for more options.

  ## Examples

      iex> MyCache.put_new("foo", "bar")
      {:ok, true}
      iex> MyCache.put_new("foo", "bar", ttl: :timer.hours(1))
      {:ok, false}

  """
  @doc group: "KV API"
  @callback put_new(key(), value(), opts()) :: ok_error_tuple(boolean())

  @doc """
  Same as `c:put_new/3`, but the command is executed on the cache instance
  given at the first argument `dynamic_cache`.

  See the ["Dynamic caches"](#module-dynamic-caches) section in the
  module documentation for more information.

  ## Examples

      iex> MyCache.put_new(MyCache1, "foo", "bar", [])
      {:ok, true}
      iex> MyCache.put_new(MyCache1, "foo", "bar", ttl: :timer.hours(1))
      {:ok, false}

  """
  @doc group: "KV API"
  @callback put_new(dynamic_cache(), key(), value(), opts()) :: ok_error_tuple(boolean())

  @doc """
  Same as `c:put_new/3` but raises an exception if an error occurs.

  ## Examples

      iex> MyCache.put_new!("foo", "bar")
      true
      iex> MyCache.put_new!("foo", "bar", ttl: :timer.hours(1))
      false

  """
  @doc group: "KV API"
  @callback put_new!(key(), value(), opts()) :: boolean()

  @doc """
  Same as `c:put_new!/4` but raises an exception if an error occurs.
  """
  @doc group: "KV API"
  @callback put_new!(dynamic_cache(), key(), value(), opts()) :: boolean()

  @doc """
  Alters the entry stored under `key`, but only if the entry already exists
  in the cache.

  Returns `{:ok, true}` if the value is replaced. Otherwise, `{:ok, false}`
  is returned.

  If there's an error with executing the command, `{:error, reason}`
  is returned, where `reason` is the cause of the error.

  ## Options

  #{Nebulex.Cache.Options.replace_options_docs()}

  See the ["Shared options"](#module-shared-options) section in the module
  documentation for more options.

  ## Examples

      iex> MyCache.replace("foo", "bar")
      {:ok, false}
      iex> MyCache.put_new("foo", "bar")
      {:ok, true}
      iex> MyCache.replace("foo", "bar2")
      {:ok, true}

  Update current value and TTL:

      iex> MyCache.replace("foo", "bar3", ttl: :timer.seconds(10))
      {:ok, true}

  To keep the current TTL:

      iex> MyCache.replace("foo", "bar4", keep_ttl: true)
      {:ok, true}

  """
  @doc group: "KV API"
  @callback replace(key(), value(), opts()) :: ok_error_tuple(boolean())

  @doc """
  Same as `c:replace/3`, but the command is executed on the cache instance
  given at the first argument `dynamic_cache`.

  See the ["Dynamic caches"](#module-dynamic-caches) section in the
  module documentation for more information.

  ## Examples

      iex> MyCache.replace(MyCache1, "foo", "bar", [])
      {:ok, false}
      iex> MyCache.put_new("foo", "bar")
      {:ok, true}
      iex> MyCache.replace(MyCache1, "foo", "bar", ttl: :timer.hours(1))
      {:ok, true}

  """
  @doc group: "KV API"
  @callback replace(dynamic_cache(), key(), value(), opts()) :: ok_error_tuple(boolean())

  @doc """
  Same as `c:replace/3` but raises an exception if an error occurs.

  ## Examples

      iex> MyCache.replace!("foo", "bar")
      false
      iex> MyCache.put_new!("foo", "bar")
      true
      iex> MyCache.replace!("foo", "bar2")
      true

  """
  @doc group: "KV API"
  @callback replace!(key(), value(), opts()) :: boolean()

  @doc """
  Same as `c:replace!/4` but raises an exception if an error occurs.
  """
  @doc group: "KV API"
  @callback replace!(dynamic_cache(), key(), value(), opts()) :: boolean()

  @doc """
  Puts the given `entries` (key/value pairs) into the cache. It replaces
  existing values with new values (just as regular `put`).

  Returns `:ok` if successful; `{:error, reason}` otherwise.

  ## Options

  #{Nebulex.Cache.Options.write_options_docs()}

  See the ["Shared options"](#module-shared-options) section in the module
  documentation for more options.

  ## Examples

      iex> MyCache.put_all(apples: 3, bananas: 1)
      :ok
      iex> MyCache.put_all(%{apples: 2, oranges: 1}, ttl: :timer.hours(1))
      :ok

  > #### Atomic operation {: .warning}
  >
  > Ideally, this operation should be atomic, so all given keys are put at once.
  > But it depends purely on the adapter's implementation and the backend used
  > internally by the adapter. Hence, reviewing the adapter's documentation is
  > highly recommended.
  """
  @doc group: "KV API"
  @callback put_all(entries(), opts()) :: :ok | error_tuple()

  @doc """
  Same as `c:put_all/2`, but the command is executed on the cache instance
  given at the first argument `dynamic_cache`.

  See the ["Dynamic caches"](#module-dynamic-caches) section in the
  module documentation for more information.

  ## Examples

      iex> MyCache.put_all(MyCache1, [apples: 3, bananas: 1], [])
      :ok
      iex> MyCache.put_all(MyCache1, %{oranges: 1}, ttl: :timer.hours(1))
      :ok

  """
  @doc group: "KV API"
  @callback put_all(dynamic_cache(), entries(), opts()) :: :ok | error_tuple()

  @doc """
  Same as `c:put_all/2` but raises an exception if an error occurs.
  """
  @doc group: "KV API"
  @callback put_all!(entries(), opts()) :: :ok

  @doc """
  Same as `c:put_all!/3` but raises an exception if an error occurs.
  """
  @doc group: "KV API"
  @callback put_all!(dynamic_cache(), entries(), opts()) :: :ok

  @doc """
  Puts the given `entries` (key/value pairs) into the `cache`. It will not
  perform any operation, even if a single key exists.

  Returns `{:ok, true}` if all entries are successfully stored, or
  `{:ok, false}` if no key was set (at least one key already existed).

  If there's an error with executing the command, `{:error, reason}`
  is returned, where `reason` is the cause of the error.

  ## Options

  #{Nebulex.Cache.Options.write_options_docs()}

  See the ["Shared options"](#module-shared-options) section in the module
  documentation for more options.

  ## Examples

      iex> MyCache.put_new_all(apples: 3, bananas: 1)
      {:ok, true}
      iex> MyCache.put_new_all(%{apples: 3, oranges: 1}, ttl: :timer.hours(1))
      {:ok, false}

  > #### Atomic operation {: .warning}
  >
  > Ideally, this operation should be atomic, so all given keys are put at once.
  > But it depends purely on the adapter's implementation and the backend used
  > internally by the adapter. Hence, reviewing the adapter's documentation is
  > highly recommended.
  """
  @doc group: "KV API"
  @callback put_new_all(entries(), opts()) :: ok_error_tuple(boolean())

  @doc """
  Same as `c:put_new_all/2`, but the command is executed on the cache instance
  given at the first argument `dynamic_cache`.

  See the ["Dynamic caches"](#module-dynamic-caches) section in the
  module documentation for more information.

  ## Examples

      iex> MyCache.put_new_all(MyCache1, [apples: 3, bananas: 1], [])
      {:ok, true}
      iex> MyCache.put_new_all(MyCache1, %{apples: 3, oranges: 1}, ttl: :timer.seconds(10))
      {:ok, false}

  """
  @doc group: "KV API"
  @callback put_new_all(dynamic_cache(), entries(), opts()) :: ok_error_tuple(boolean())

  @doc """
  Same as `c:put_new_all/2` but raises an exception if an error occurs.

  ## Examples

      iex> MyCache.put_new_all!(apples: 3, bananas: 1)
      true
      iex> MyCache.put_new_all!(%{apples: 3, oranges: 1}, ttl: :timer.hours(1))
      false

  """
  @doc group: "KV API"
  @callback put_new_all!(entries(), opts()) :: boolean()

  @doc """
  Same as `c:put_new_all!/3` but raises an exception if an error occurs.
  """
  @doc group: "KV API"
  @callback put_new_all!(dynamic_cache(), entries(), opts()) :: boolean()

  @doc """
  Deletes the entry in the cache for a specific `key`.

  If there's an error with executing the command, `{:error, reason}`
  is returned, where `reason` is the cause of the error.

  ## Options

  See the ["Shared options"](#module-shared-options) section in the module
  documentation for more options.

  ## Examples

      iex> MyCache.put(:a, 1)
      :ok
      iex> MyCache.delete(:a)
      :ok
      iex> MyCache.get!(:a)
      nil
      iex> MyCache.delete(:nonexistent)
      :ok

  """
  @doc group: "KV API"
  @callback delete(key(), opts()) :: :ok | error_tuple()

  @doc """
  Same as `c:delete/2`, but the command is executed on the cache instance
  given at the first argument `dynamic_cache`.

  See the ["Dynamic caches"](#module-dynamic-caches) section in the
  module documentation for more information.

  ## Examples

      iex> MyCache.delete(MyCache1, :a, [])
      :ok

  """
  @doc group: "KV API"
  @callback delete(dynamic_cache(), key(), opts()) :: :ok | error_tuple()

  @doc """
  Same as `c:delete/2` but raises an exception if an error occurs.
  """
  @doc group: "KV API"
  @callback delete!(key(), opts()) :: :ok

  @doc """
  Same as `c:delete!/3` but raises an exception if an error occurs.
  """
  @doc group: "KV API"
  @callback delete!(dynamic_cache(), key(), opts()) :: :ok

  @doc """
  Removes and returns the value associated with `key` in the cache.

  If `key` is present in the cache, its value is removed and returned as
  `{:ok, value}`.

  If there's an error with executing the command, `{:error, reason}`
  is returned. `reason` is the cause of the error and can be
  `Nebulex.KeyError` if the cache does not contain `key` or
  `Nebulex.Error` otherwise.

  ## Options

  See the ["Shared options"](#module-shared-options) section in the module
  documentation for more options.

  ## Examples

      iex> MyCache.put(:a, 1)
      :ok
      iex> MyCache.take(:a)
      {:ok, 1}

      iex> {:error, %Nebulex.KeyError{key: :a} = e} = MyCache.take(:a)
      iex> e.reason
      :not_found

  """
  @doc group: "KV API"
  @callback take(key(), opts()) :: ok_error_tuple(value(), fetch_error_reason())

  @doc """
  Same as `c:take/2`, but the command is executed on the cache instance
  given at the first argument `dynamic_cache`.

  See the ["Dynamic caches"](#module-dynamic-caches) section in the
  module documentation for more information.

  ## Examples

      iex> MyCache.put(:a, 1)
      :ok
      iex> MyCache.take(MyCache1, :a, [])
      {:ok, 1}

  """
  @doc group: "KV API"
  @callback take(dynamic_cache(), key(), opts()) :: ok_error_tuple(value(), fetch_error_reason())

  @doc """
  Same as `c:take/2` but raises an exception if an error occurs.

  ## Examples

      iex> MyCache.put(:a, 1)
      :ok
      iex> MyCache.take!(:a)
      1

  """
  @doc group: "KV API"
  @callback take!(key(), opts()) :: value()

  @doc """
  Same as `c:take!/3` but raises an exception if an error occurs.
  """
  @doc group: "KV API"
  @callback take!(dynamic_cache(), key(), opts()) :: value()

  @doc """
  Determines if the cache contains an entry for the specified `key`.

  More formally, it returns `{:ok, true}` if the cache contains the given `key`.
  If the cache doesn't contain `key`, `{:ok, false}` is returned.

  If there's an error with executing the command, `{:error, reason}`
  is returned, where `reason` is the cause of the error.

  ## Options

  See the ["Shared options"](#module-shared-options) section in the module
  documentation for more options.

  ## Examples

      iex> MyCache.put(:a, 1)
      :ok
      iex> MyCache.has_key?(:a)
      {:ok, true}
      iex> MyCache.has_key?(:b)
      {:ok, false}

  """
  @doc group: "KV API"
  @callback has_key?(key(), opts()) :: ok_error_tuple(boolean())

  @doc """
  Same as `c:has_key?/2`, but the command is executed on the cache instance
  given at the first argument `dynamic_cache`.

  See the ["Dynamic caches"](#module-dynamic-caches) section in the
  module documentation for more information.

  ## Examples

      iex> MyCache.has_key?(MyCache1, :a, [])
      {:ok, false}

  """
  @doc group: "KV API"
  @callback has_key?(dynamic_cache(), key(), opts()) :: ok_error_tuple(boolean())

  @doc """
  Returns the remaining time-to-live for the given `key`.

  If `key` is present in the cache, its remaining TTL is returned as
  `{:ok, ttl}`.

  If there's an error with executing the command, `{:error, reason}`
  is returned. `reason` is the cause of the error and can be
  `Nebulex.KeyError` if the cache does not contain `key`,
  `Nebulex.Error` otherwise.

  ## Options

  See the ["Shared options"](#module-shared-options) section in the module
  documentation for more options.

  ## Examples

      iex> MyCache.put(:a, 1, ttl: :timer.seconds(5))
      :ok
      iex> MyCache.put(:b, 2)
      :ok
      iex> MyCache.ttl(:a)
      {:ok, _remaining_ttl}
      iex> MyCache.ttl(:b)
      {:ok, :infinity}

      iex> {:error, %Nebulex.KeyError{key: :c} = e} = MyCache.ttl(:c)
      iex> e.reason
      :not_found

  """
  @doc group: "KV API"
  @callback ttl(key(), opts()) :: ok_error_tuple(timeout(), fetch_error_reason())

  @doc """
  Same as `c:ttl/2`, but the command is executed on the cache instance
  given at the first argument `dynamic_cache`.

  See the ["Dynamic caches"](#module-dynamic-caches) section in the
  module documentation for more information.

  ## Examples

      iex> MyCache.put(:a, 1, ttl: :timer.seconds(5))
      :ok
      iex> MyCache.ttl(MyCache1, :a, [])
      {:ok, _remaining_ttl}

  """
  @doc group: "KV API"
  @callback ttl(dynamic_cache(), key(), opts()) :: ok_error_tuple(timeout(), fetch_error_reason())

  @doc """
  Same as `c:ttl/2` but raises an exception if an error occurs.

  ## Examples

      iex> MyCache.put(:a, 1, ttl: :timer.seconds(5))
      :ok
      iex> MyCache.ttl!(:a)
      _remaining_ttl

  """
  @doc group: "KV API"
  @callback ttl!(key(), opts()) :: timeout()

  @doc """
  Same as `c:ttl!/3` but raises an exception if an error occurs.
  """
  @doc group: "KV API"
  @callback ttl!(dynamic_cache(), key(), opts()) :: timeout()

  @doc """
  Returns `{:ok, true}` if the given `key` exists and the new `ttl` is
  successfully updated; otherwise, `{:ok, false}` is returned.

  If there's an error with executing the command, `{:error, reason}`
  is returned; where `reason` is the cause of the error.

  ## Options

  See the ["Shared options"](#module-shared-options) section in the module
  documentation for more options.

  ## Examples

      iex> MyCache.put(:a, 1)
      :ok
      iex> MyCache.expire(:a, :timer.hours(1))
      {:ok, true}
      iex> MyCache.expire(:a, :infinity)
      {:ok, true}
      iex> MyCache.expire(:b, 5)
      {:ok, false}

  """
  @doc group: "KV API"
  @callback expire(key(), ttl :: timeout(), opts()) :: ok_error_tuple(boolean())

  @doc """
  Same as `c:expire/3`, but the command is executed on the cache instance
  given at the first argument `dynamic_cache`.

  See the ["Dynamic caches"](#module-dynamic-caches) section in the
  module documentation for more information.

  ## Examples

      iex> MyCache.expire(MyCache1, :a, :timer.hours(1), [])
      {:ok, false}

  """
  @doc group: "KV API"
  @callback expire(dynamic_cache(), key(), ttl :: timeout(), opts()) :: ok_error_tuple(boolean())

  @doc """
  Same as `c:expire/3` but raises an exception if an error occurs.

  ## Examples

      iex> MyCache.put(:a, 1)
      :ok
      iex> MyCache.expire!(:a, :timer.hours(1))
      true

  """
  @doc group: "KV API"
  @callback expire!(key(), ttl :: timeout(), opts()) :: boolean()

  @doc """
  Same as `c:expire!/4` but raises an exception if an error occurs.
  """
  @doc group: "KV API"
  @callback expire!(dynamic_cache(), key(), ttl :: timeout(), opts()) :: boolean()

  @doc """
  Returns `{:ok, true}` if the given `key` exists and the last access time is
  successfully updated; otherwise, `{:ok, false}` is returned.

  If there's an error with executing the command, `{:error, reason}`
  is returned, where `reason` is the cause of the error.

  ## Options

  See the ["Shared options"](#module-shared-options) section in the module
  documentation for more options.

  ## Examples

      iex> MyCache.put(:a, 1)
      :ok
      iex> MyCache.touch(:a)
      {:ok, true}
      iex> MyCache.ttl(:b)
      {:ok, false}

  """
  @doc group: "KV API"
  @callback touch(key(), opts()) :: ok_error_tuple(boolean())

  @doc """
  Same as `c:touch/2`, but the command is executed on the cache instance
  given at the first argument `dynamic_cache`.

  See the ["Dynamic caches"](#module-dynamic-caches) section in the
  module documentation for more information.

  ## Examples

      iex> MyCache.touch(MyCache1, :a, [])
      {:ok, false}

  """
  @doc group: "KV API"
  @callback touch(dynamic_cache(), key(), opts()) :: ok_error_tuple(boolean())

  @doc """
  Same as `c:touch/2` but raises an exception if an error occurs.

  ## Examples

      iex> MyCache.put(:a, 1)
      :ok
      iex> MyCache.touch!(:a)
      true

  """
  @doc group: "KV API"
  @callback touch!(key(), opts()) :: boolean()

  @doc """
  Same as `c:touch!/3` but raises an exception if an error occurs.
  """
  @doc group: "KV API"
  @callback touch!(dynamic_cache(), key(), opts()) :: boolean()

  @doc """
  Increments the counter stored at `key` by the given `amount` and returns
  the current count as `{:ok, count}`.

  If `amount < 0`, the value is decremented by that `amount` instead.

  If there's an error with executing the command, `{:error, reason}`
  is returned, where `reason` is the cause of the error.

  If the key doesn't exist, the TTL is set. Otherwise, only the counter value
  is updated, keeping the TTL set for the first time.

  ## Options

  #{Nebulex.Cache.Options.update_counter_options_docs()}

  See the ["Shared options"](#module-shared-options) section in the module
  documentation for more options.

  ## Examples

      iex> MyCache.incr(:a)
      {:ok, 1}
      iex> MyCache.incr(:a, 2)
      {:ok, 3}
      iex> MyCache.incr(:a, -1)
      {:ok, 2}
      iex> MyCache.incr(:missing_key, 2, default: 10)
      {:ok, 12}

      # Initialize the counter with a TTL
      iex> MyCache.incr(:new_counter, 10, ttl: :timer.seconds(1))
      {:ok, 10}

  """
  @doc group: "KV API"
  @callback incr(key(), amount :: integer(), opts()) :: ok_error_tuple(integer())

  @doc """
  Same as `c:incr/3`, but the command is executed on the cache instance
  given at the first argument `dynamic_cache`.

  See the ["Dynamic caches"](#module-dynamic-caches) section in the
  module documentation for more information.

  ## Examples

      iex> MyCache.incr(MyCache1, :a, 1, [])
      {:ok, 1}

  """
  @doc group: "KV API"
  @callback incr(dynamic_cache(), key(), amount :: integer(), opts()) :: ok_error_tuple(integer())

  @doc """
  Same as `c:incr/3` but raises an exception if an error occurs.

  ## Examples

      iex> MyCache.incr!(:a)
      1
      iex> MyCache.incr!(:a, 2)
      3

  """
  @doc group: "KV API"
  @callback incr!(key(), amount :: integer(), opts()) :: integer()

  @doc """
  Same as `c:incr!/4` but raises an exception if an error occurs.
  """
  @doc group: "KV API"
  @callback incr!(dynamic_cache(), key(), amount :: integer(), opts()) :: integer()

  @doc """
  Decrements the counter stored at `key` by the given `amount` and returns
  the current count as `{:ok, count}`.

  If `amount < 0`, the value is incremented by that `amount` instead
  (opposite to `incr/3`).

  If there's an error with executing the command, `{:error, reason}`
  is returned, where `reason` is the cause of the error.

  If the key doesn't exist, the TTL is set. Otherwise, only the counter value
  is updated, keeping the TTL set for the first time.

  ## Options

  #{Nebulex.Cache.Options.update_counter_options_docs()}

  See the ["Shared options"](#module-shared-options) section in the module
  documentation for more options.

  ## Examples

      iex> MyCache.decr(:a)
      {:ok, -1}
      iex> MyCache.decr(:a, 2)
      {:ok, -3}
      iex> MyCache.decr(:a, -1)
      {:ok, -2}

      iex> MyCache.decr(:missing_key, 2, default: 10)
      {:ok, 8}

  """
  @doc group: "KV API"
  @callback decr(key(), amount :: integer(), opts()) :: ok_error_tuple(integer())

  @doc """
  Same as `c:decr/3`, but the command is executed on the cache instance
  given at the first argument `dynamic_cache`.

  See the ["Dynamic caches"](#module-dynamic-caches) section in the
  module documentation for more information.

  ## Examples

      iex> MyCache.decr(MyCache1, :a, 1, [])
      {:ok, -1}

  """
  @doc group: "KV API"
  @callback decr(dynamic_cache(), key(), amount :: integer(), opts()) :: ok_error_tuple(integer())

  @doc """
  Same as `c:decr/3` but raises an exception if an error occurs.

  ## Examples

      iex> MyCache.decr!(:a)
      -1

  """
  @doc group: "KV API"
  @callback decr!(key(), amount :: integer(), opts()) :: integer()

  @doc """
  Same as `c:decr!/4` but raises an exception if an error occurs.
  """
  @doc group: "KV API"
  @callback decr!(dynamic_cache(), key(), amount :: integer(), opts()) :: integer()

  @doc """
  Gets the value from `key` and updates it, all in one pass.

  `fun` is called with the current cached value under `key` (or `nil` if `key`
  hasn't been cached) and must return a two-element tuple: the current value
  (the retrieved value, which can be operated on before being returned) and
  the new value to be stored under `key`. `fun` may also return `:pop`, which
  means the current value shall be removed from the cache and returned.

  This function returns:

    * `{:ok, {current_value, new_value}}` - The `current_value` is the current
      cached value and `new_value` the updated one returned by `fun`.

    * `{:error, reason}` - An error occurred executing the command.
      `reason` is the cause of the error.

  ## Options

  #{Nebulex.Cache.Options.put_options_docs()}

  See the ["Shared options"](#module-shared-options) section in the module
  documentation for more options.

  > #### `get_and_update` atomicity {: .warning}
  >
  > This operation is not atomic. It uses `get` and `put` (or `delete` for
  > `:pop`) under the hood, but the function is executed outside of the cache
  > transaction. If you need to ensure atomicity, consider wrapping the function
  > in a `c:transaction/2` call.

  ## Examples

  Update nonexistent key:

      iex> MyCache.get_and_update(:a, fn current_value ->
      ...>   {current_value, "value!"}
      ...> end)
      {:ok, {nil, "value!"}}

  Update existing key:

      iex> MyCache.get_and_update(:a, fn current_value ->
      ...>   {current_value, "new value!"}
      ...> end)
      {:ok, {"value!", "new value!"}}

  Pop/remove value if exist:

      iex> MyCache.get_and_update(:a, fn _ -> :pop end)
      {:ok, {"new value!", nil}}

  Pop/remove nonexistent key:

      iex> MyCache.get_and_update(:b, fn _ -> :pop end)
      {:ok, {nil, nil}}

  """
  @doc group: "KV API"
  @callback get_and_update(key(), (value() -> {current_value, new_value} | :pop), opts()) ::
              ok_error_tuple({current_value, new_value})
            when current_value: value(), new_value: value()

  @doc """
  Same as `c:get_and_update/3`, but the command is executed on the cache
  instance given at the first argument `dynamic_cache`.

  See the ["Dynamic caches"](#module-dynamic-caches) section in the
  module documentation for more information.

  ## Examples

      iex> MyCache.get_and_update(MyCache1, :a, &{&1, "value!"}, [])
      {:ok, {nil, "value!"}}

  """
  @doc group: "KV API"
  @callback get_and_update(
              dynamic_cache(),
              key(),
              (value() -> {current_value, new_value} | :pop),
              opts()
            ) :: ok_error_tuple({current_value, new_value})
            when current_value: value(), new_value: value()

  @doc """
  Same as `c:get_and_update/3` but raises an exception if an error occurs.

  ## Examples

      iex> MyCache.get_and_update!(:a, &{&1, "value!"})
      {nil, "value!"}

  """
  @doc group: "KV API"
  @callback get_and_update!(key(), (value() -> {current_value, new_value} | :pop), opts()) ::
              {current_value, new_value}
            when current_value: value(), new_value: value()

  @doc """
  Same as `c:get_and_update!/4` but raises an exception if an error occurs.
  """
  @doc group: "KV API"
  @callback get_and_update!(
              dynamic_cache(),
              key(),
              (value() -> {current_value, new_value} | :pop),
              opts()
            ) :: {current_value, new_value}
            when current_value: value(), new_value: value()

  @doc """
  Updates the `key` in the cache with the given function.

  If `key` is present in the cache, the existing value is passed to `fun` and
  its result is used as the updated value of `key`. If `key` is not present in
  the cache, `default` is inserted as the value of `key`. The default value
  will not be passed through the update function.

  This function returns:

    * `{:ok, value}` - The value associated with the `key` is updated.

    * `{:error, reason}` - An error occurred executing the command.
      `reason` is the cause.

  ## Options

  #{Nebulex.Cache.Options.put_options_docs()}

  See the ["Shared options"](#module-shared-options) section in the module
  documentation for more options.

  > #### `update` atomicity {: .warning}
  >
  > This operation is not atomic. It uses `fetch` and `put` under the hood,
  > but the function is executed outside of the cache transaction. If you need
  > to ensure atomicity, consider wrapping the function in a `c:transaction/2`
  > call.

  ## Examples

      iex> MyCache.update(:a, 1, &(&1 * 2))
      {:ok, 1}
      iex> MyCache.update(:a, 1, &(&1 * 2))
      {:ok, 2}

  """
  @doc group: "KV API"
  @callback update(key(), initial :: value(), (value() -> value()), opts()) ::
              ok_error_tuple(value())

  @doc """
  Same as `c:update/4`, but the command is executed on the cache instance
  given at the first argument `dynamic_cache`.

  See the ["Dynamic caches"](#module-dynamic-caches) section in the
  module documentation for more information.

  ## Examples

      iex> MyCache.update(MyCache1, :a, 1, &(&1 * 2), [])
      {:ok, 1}

  """
  @doc group: "KV API"
  @callback update(dynamic_cache(), key(), initial :: value(), (value() -> value()), opts()) ::
              ok_error_tuple(value())

  @doc """
  Same as `c:update/4` but raises an exception if an error occurs.

  ## Examples

      iex> MyCache.update!(:a, 1, &(&1 * 2))
      1

  """
  @doc group: "KV API"
  @callback update!(key(), initial :: value(), (value() -> value()), opts()) :: value()

  @doc """
  Same as `c:update/5` but raises an exception if an error occurs.
  """
  @doc group: "KV API"
  @callback update!(dynamic_cache(), key(), initial :: value(), (value() -> value()), opts()) ::
              value()

  @doc """
  Fetches the value for the given `key` from the cache. If the key is not
  present, the provided anonymous function is executed.

  If the function returns `{:ok, value}`, the value is cached under the given
  `key` and returned as the result. If it returns `{:error, reason}`, the value
  is **not** cached, and the error is returned as is.

  If the function returns any other value, a `RuntimeError` is raised.

  This function is useful when you want to cache only successful computations,
  such as API calls or database queries that may fail. Failed operations are
  not cached, allowing subsequent calls to retry the operation.

  ## Options

  Since the `put` operation is used under the hood, the following options are
  supported:

  #{Nebulex.Cache.Options.put_options_docs()}

  See the ["Shared options"](#module-shared-options) section in the module
  documentation for a list of supported options.

  > #### `fetch_or_store` atomicity {: .warning}
  >
  > This operation is not atomic. It uses `fetch` and `put` under the hood,
  > but the function is executed outside of the cache transaction. If you need
  > to ensure atomicity, consider wrapping the function in a `c:transaction/2`
  > call.

  ## Examples

  ### Basic usage

      iex> MyCache.fetch_or_store("foo", fn -> {:ok, "bar"} end)
      {:ok, "bar"}

      iex> MyCache.fetch_or_store("foo", fn -> {:ok, "new value"} end)
      {:ok, "bar"}  # Returns cached value, function not executed

  ### Error handling

  When the function returns an error, the value is not cached:

      iex> MyCache.fetch_or_store("user", fn -> {:error, "not found"} end)
      {:error, %Nebulex.Error{reason: "not found"}}

      iex> MyCache.fetch("user")
      {:error, %Nebulex.KeyError{key: "user"}}  # Key was not cached

  ### Invalid return values

  The function must return either `{:ok, value}` or `{:error, reason}`:

      iex> MyCache.fetch_or_store("foo", fn -> :invalid end)
      ** (RuntimeError) the supplied lambda function must return ..

  ### With TTL

      iex> MyCache.fetch_or_store(
      ...>   "session",
      ...>   fn -> {:ok, "data"} end,
      ...>   ttl: :timer.minutes(5)
      ...> )
      {:ok, "data"}

  ### Real-world example: API calls

      def get_user_from_api(user_id) do
        MyCache.fetch_or_store(
          "user:\#{user_id}",
          fn ->
            case HTTPClient.get("/users/\#{user_id}") do
              {:ok, %{status: 200, body: data}} -> {:ok, data}
              {:ok, %{status: 404}} -> {:error, :not_found}
              {:error, reason} -> {:error, reason}
            end
          end,
          ttl: :timer.minutes(10)
        )
      end

  ### Real-world example: Database queries

      def fetch_product(id) do
        MyCache.fetch_or_store(
          "product:\#{id}",
          fn ->
            case Repo.get(Product, id) do
              %Product{} = product -> {:ok, product}
              nil -> {:error, :not_found}
            end
          end,
          ttl: :timer.minutes(5)
        )
      end

  ## When to use `fetch_or_store`

  Use `fetch_or_store/3` when:

  - The computation may fail and you don't want to cache errors.
  - Working with external APIs that may return error responses.
  - Fetching from databases where records might not exist.
  - Performing validations where only valid results should be cached.
  - Any scenario where caching failures would be problematic.

  For computations that always produce a valid result to cache (even if it's
  an error tuple), consider using `get_or_store/3` instead.
  """
  @doc group: "KV API"
  @callback fetch_or_store(key(), fetch_or_store_fun(), opts()) :: ok_error_tuple(value())

  @doc """
  Same as `c:fetch_or_store/3`, but the command is executed on the cache
  instance given at the first argument `dynamic_cache`.

  See the ["Dynamic caches"](#module-dynamic-caches) section in the
  module documentation for more information.

  ## Examples

      iex> MyCache.fetch_or_store(MyCache1, "key", fn -> {:ok, "value"} end, [])
      {:ok, "value"}

  """
  @doc group: "KV API"
  @callback fetch_or_store(dynamic_cache(), key(), fetch_or_store_fun(), opts()) ::
              ok_error_tuple(value())

  @doc """
  Same as `c:fetch_or_store/3` but raises an exception if an error occurs.

  Returns the unwrapped value on success, or raises `Nebulex.Error` if the
  function returns `{:error, reason}`.

  ## Examples

      iex> MyCache.fetch_or_store!("key", fn -> {:ok, "value"} end)
      "value"

      iex> MyCache.fetch_or_store!("key", fn -> {:error, :error} end)
      ** (Nebulex.Error) fetch_or_store command failed with reason: :error

      iex> MyCache.fetch_or_store!("key", fn -> :invalid end)
      ** (RuntimeError) the supplied lambda function must return ...

  """
  @doc group: "KV API"
  @callback fetch_or_store!(key(), fetch_or_store_fun(), opts()) :: value()

  @doc """
  Same as `c:fetch_or_store!/3` but the command is executed on the cache
  instance given at the first argument `dynamic_cache`.
  """
  @doc group: "KV API"
  @callback fetch_or_store!(dynamic_cache(), key(), fetch_or_store_fun(), opts()) :: value()

  @doc """
  Gets the value for the given `key` from the cache. If the key is not
  present, the provided anonymous function is executed and its result
  is always cached under the given `key`.

  Unlike `fetch_or_store/3`, this function always caches the result
  regardless of whether it's a success or error tuple. This makes it
  ideal for caching expensive computations, implementing negative caching
  patterns, or caching error states temporarily.

  ## Options

  Since the `put` operation is used under the hood, the following options are
  supported:

  #{Nebulex.Cache.Options.put_options_docs()}

  See the ["Shared options"](#module-shared-options) section in the module
  documentation for a list of supported options.

  > #### `get_or_store` atomicity {: .warning}
  >
  > This operation is not atomic. It uses `fetch` and `put` under the hood,
  > but the function is executed outside of the cache transaction. If you need
  > to ensure atomicity, consider wrapping the function in a `c:transaction/2`
  > call.

  ## Examples

  ### Basic usage - caching any value

  The function can return any value, and it will be cached:

      iex> MyCache.get_or_store("api_result", fn -> "data" end)
      {:ok, "data"}

      iex> MyCache.get_or_store("api_result", fn -> "new data" end)
      {:ok, "data"}  # Returns cached value, function not executed

  ### Caching tuples

  This function caches the exact return value, including tuples:

      iex> MyCache.get_or_store("api_result_tuple", fn -> {:ok, "data"} end)
      {:ok, {:ok, "data"}}

      iex> MyCache.get_or_store("api_error", fn -> {:error, "rate_limited"} end)
      {:ok, {:error, "rate_limited"}}

  ### With TTL

      iex> MyCache.get_or_store(
      ...>   "with_ttl",
      ...>   fn -> "value" end,
      ...>   ttl: :timer.minutes(5)
      ...> )
      {:ok, "value"}

  ### Real-world example: Negative caching

  Cache "not found" results to avoid repeated database queries:

      def get_user(user_id) do
        MyCache.get_or_store!(
          "user:\#{user_id}",
          fn ->
            case Repo.get(User, user_id) do
              %User{} = user ->
                {:ok, user}

              nil ->
              # Cache the "not found" result
              {:error, :not_found}
            end
          end,
          ttl: :timer.minutes(5)
        )
      end

      # First call - queries database, caches {:error, :not_found}
      get_user(999)  #=> {:error, :not_found}

      # Second call - returns cached error, no database query
      get_user(999)  #=> {:error, :not_found}

  ### Real-world example: Rate limit protection

  Cache error responses temporarily to prevent hammering external services:

      def fetch_from_api(endpoint) do
        MyCache.get_or_store(
          "api:\#{endpoint}",
          fn ->
            case ExternalAPI.call(endpoint) do
              {:ok, data} ->
                {:ok, data}

              {:error, :rate_limited} = error ->
                # Cache rate limit error
                error

              {:error, _} = error ->
                error
            end
          end,
          ttl: :timer.seconds(30)
        )
      end

  ### Real-world example: Expensive computations

  Cache results of expensive operations regardless of outcome:

      def calculate_report(params) do
        MyCache.get_or_store!(
          "report:\#{hash(params)}",
          fn ->
            %{
              total_revenue: calculate_revenue(params),
              user_count: count_users(params),
              metrics: compute_metrics(params)
            }
          end,
          ttl: :timer.hours(1)
        )
      end

  ## When to use `get_or_store` vs `fetch_or_store`

  **Use `get_or_store/3` when:**

  - You want to cache all results, including errors.
  - Implementing negative caching (caching "not found" states).
  - The computation is expensive and you want to avoid repeating it.
  - Caching error states temporarily for rate limiting or backoff strategies.
  - Working with pure functions that always produce valid output.

  **Use `fetch_or_store/3` when:**

  - The function may fail and you want to retry on subsequent calls.
  - Errors are transient and should not be cached.
  - Working with external APIs where temporary failures should trigger retries.
  - You only want to cache successful results.

  ## Comparison

  | Feature | `get_or_store` | `fetch_or_store` |
  |---------|----------------|------------------|
  | Caches all values | ✅ Yes | ❌ No |
  | Caches errors | ✅ Yes | ❌ No |
  | Function return type | Any value | `{:ok, value}` or `{:error, reason}` |
  | Type validation | None | Validates return type |
  | Best for | Negative caching, pure computations | Fallible operations |

  """
  @doc group: "KV API"
  @callback get_or_store(key(), get_or_store_fun(), opts()) :: ok_error_tuple(value())

  @doc """
  Same as `c:get_or_store/3`, but the command is executed on the cache
  instance given at the first argument `dynamic_cache`.

  See the ["Dynamic caches"](#module-dynamic-caches) section in the
  module documentation for more information.

  ## Examples

      iex> MyCache.get_or_store(MyCache, "key", fn -> "value" end, [])
      {:ok, "value"}

  """
  @doc group: "KV API"
  @callback get_or_store(dynamic_cache(), key(), get_or_store_fun(), opts()) ::
              ok_error_tuple(value())

  @doc """
  Same as `c:get_or_store/3` but raises an exception if a cache error occurs
  (e.g., the adapter failed executing the command and returns an error).

  Note that this function returns the unwrapped cached value, which may itself
  be an error tuple if that's what the function returned.

  ## Examples

      iex> MyCache.get_or_store!("key", fn -> "value" end)
      "value"

      iex> MyCache.get_or_store!("key", fn -> {:ok, "value"} end)
      {:ok, "value"}

      iex> MyCache.get_or_store!("key", fn -> {:error, "error"} end)
      {:error, "error"}

  ## Difference from fetch_or_store!

  Unlike `fetch_or_store!/3` which raises when the function returns an error,
  `get_or_store!/3` only raises when there's a cache operation error. The
  function's return value (even if it's an error tuple) is cached and returned.

      # get_or_store! caches and returns error tuples
      iex> MyCache.get_or_store!("key", fn -> {:error, :not_found} end)
      {:error, :not_found}

      # fetch_or_store! raises when function returns error
      iex> MyCache.fetch_or_store!("key", fn -> {:error, :not_found} end)
      ** (Nebulex.Error) fetch_or_store command failed with reason: :not_found

  """
  @doc group: "KV API"
  @callback get_or_store!(key(), get_or_store_fun(), opts()) :: value()

  @doc """
  Same as `c:get_or_store!/3` but the command is executed on the cache
  instance given at the first argument `dynamic_cache`.
  """
  @doc group: "KV API"
  @callback get_or_store!(dynamic_cache(), key(), get_or_store_fun(), opts()) :: value()

  ## Nebulex.Adapter.Queryable

  @optional_callbacks get_all: 2,
                      get_all: 3,
                      get_all!: 2,
                      get_all!: 3,
                      count_all: 2,
                      count_all: 3,
                      count_all!: 2,
                      count_all!: 3,
                      delete_all: 2,
                      delete_all: 3,
                      delete_all!: 2,
                      delete_all!: 3,
                      stream: 2,
                      stream: 3,
                      stream!: 2,
                      stream!: 3

  @doc """
  Fetches all entries from the cache matching the given query specified through
  the ["query-spec"](#c:get_all/2-query-specification).

  This function returns:

    * `{:ok, result}` - The cache executes the query successfully. The `result`
      is a list with the matched entries.

    * `{:error, reason}` - An error occurred executing the command.
      `reason` is the cause.

  May raise `Nebulex.QueryError` if query validation fails.

  ## Query specification

  There are two ways to use the Query API:

    * Fetch multiple keys (all at once), like a bulk fetch.
    * Fetch all entries from the cache matching a given query, more like a
      search (this is the most generic option).

  Here is where the `query_spec` argument comes in to specify the type of query
  to run.

  The `query_spec` argument is a `t:keyword/0` with options defining the desired
  query. The `query_spec` fields or options are:

  #{Nebulex.Cache.QuerySpec.options_docs()}

  ### Fetching multiple keys

  While you can perform any query using the `:query` option (even fetching
  multiple keys), the option `:in` is preferable. For example:

      MyCache.get_all(in: ["a", "list", "of", "keys"])

  ### Fetching all entries matching a given query

  As mentioned above, the option `:query` is the most generic way to match
  entries in a cache. This option allows users to write custom queries
  to be executed by the underlying adapter.

  For matching all cached entries, you can skip the `:query` option or set it
  to `nil` instead (the default). For example:

      MyCache.get_all() #=> Equivalent to MyCache.get_all(query: nil)

  Using a custom query:

      MyCache.get_all(query: query_supported_by_the_adapter)

  > _Nebulex recommends to see the adapter documentation when using this option._

  ## Options

  See the ["Shared options"](#module-shared-options) section in the module
  documentation for more options.

  ## Examples

  Populate the cache with some entries:

      iex> MyCache.put_all(a: 1, b: 2, c: 3)
      :ok

  Fetch all entries in the cache:

      iex> MyCache.get_all()
      {:ok, [a: 1, b: 2, c: 3]}

  Fetch all entries returning only the keys:

      iex> MyCache.get_all(select: :key)
      {:ok, [:a, :b, :c]}

  Fetch all entries returning only the values:

      iex> MyCache.get_all(select: :value)
      {:ok, [1, 2, 3]}

  Fetch only the requested keys (bulk fetch):

      iex> MyCache.get_all(in: [:a, :b, :d])
      {:ok, [a: 1, b: 2]}

  Fetch the requested keys returning only the keys or values:

      iex> MyCache.get_all(in: [:a, :b, :d], select: :key)
      {:ok, [:a, :b]}
      iex> MyCache.get_all(in: [:a, :b, :d], select: :value)
      {:ok, [1, 2]}

  ### Query examples for `Nebulex.Adapters.Local` adapter

  The `Nebulex.Adapters.Local` adapter supports **"ETS Match Spec"** as query
  values (in addition to `nil` or the option `:in`).

  You must know the adapter's entry structure for match-spec queries, which is
  `{:entry, key, value, touched, ttl}`. For example, one may write the following
  query:

      iex> match_spec = [
      ...>   {
      ...>     {:entry, :"$1", :"$2", :_, :_},
      ...>     [{:>, :"$2", 1}],
      ...>     [{{:"$1", :"$2"}}]
      ...>   }
      ...> ]
      iex> MyCache.get_all(query: match_spec)
      {:ok, [b: 1, c: 3]}

  """
  @doc group: "Query API"
  @callback get_all(query_spec(), opts()) :: ok_error_tuple([any()])

  @doc """
  Same as `c:get_all/2`, but the command is executed on the cache instance
  given at the first argument `dynamic_cache`.

  See the ["Dynamic caches"](#module-dynamic-caches) section in the
  module documentation for more information.

  ## Examples

      iex> MyCache.get_all(MyCache1, [], [])
      {:ok, _matched_entries}

  """
  @doc group: "Query API"
  @callback get_all(dynamic_cache(), query_spec(), opts()) :: ok_error_tuple([any()])

  @doc """
  Same as `c:get_all/2` but raises an exception if an error occurs.

  ## Examples

      iex> MyCache.put_all(a: 1, b: 2, c: 3)
      :ok
      iex> MyCache.get_all!()
      [a: 1, b: 2, c: 3]
      iex> MyCache.get_all!(in: [:a, :b])
      [a: 1, b: 2]

  """
  @doc group: "Query API"
  @callback get_all!(query_spec(), opts()) :: [any()]

  @doc """
  Same as `c:get_all!/3` but raises an exception if an error occurs.
  """
  @doc group: "Query API"
  @callback get_all!(dynamic_cache(), query_spec(), opts()) :: [any()]

  @doc """
  Deletes all entries matching the query specified by the given `query_spec`.

  See `c:get_all/2` for more information about the `query_spec`.

  This function returns:

    * `{:ok, deleted_count}` - The cache executes the query successfully and
      returns the deleted entries count.

    * `{:error, reason}` - An error occurred executing the command.
      `reason` is the cause.

  May raise `Nebulex.QueryError` if query validation fails.

  ## Options

  See the ["Shared options"](#module-shared-options) section in the module
  documentation for more options.

  ## Examples

  Populate the cache with some entries:

      iex> Enum.each(1..5, &MyCache.put(&1, &1 * 2))
      :ok

  Delete all (default args):

      iex> MyCache.delete_all()
      {:ok, 5}

  Delete only the requested keys (bulk delete):

      iex> MyCache.delete_all(in: [1, 2, 10])
      {:ok, 2}

  Delete all entries that match with the given query, assuming we are using
  `Nebulex.Adapters.Local` adapter:

      iex> query = [{{:_, :"$1", :"$2", :_, :_}, [{:>, :"$2", 5}], [true]}]
      iex> {:ok, deleted_count} = MyCache.delete_all(query: query)

  See `c:get_all/2` for more query examples.
  """
  @doc group: "Query API"
  @callback delete_all(query_spec(), opts()) :: ok_error_tuple(non_neg_integer())

  @doc """
  Same as `c:delete_all/2`, but the command is executed on the cache instance
  given at the first argument `dynamic_cache`.

  See the ["Dynamic caches"](#module-dynamic-caches) section in the
  module documentation for more information.

  ## Examples

      iex> MyCache.delete_all(MyCache1, [], [])
      {:ok, 0}

  """
  @doc group: "Query API"
  @callback delete_all(dynamic_cache(), query_spec(), opts()) :: ok_error_tuple(non_neg_integer())

  @doc """
  Same as `c:delete_all/2` but raises an exception if an error occurs.

  ## Examples

      iex> MyCache.delete_all!()
      0

  """
  @doc group: "Query API"
  @callback delete_all!(query_spec(), opts()) :: integer()

  @doc """
  Same as `c:delete_all!/3` but raises an exception if an error occurs.
  """
  @doc group: "Query API"
  @callback delete_all!(dynamic_cache(), query_spec(), opts()) :: integer()

  @doc """
  Counts all entries matching the query specified by the given `query_spec`.

  See `c:get_all/2` for more information about the `query_spec`.

  This function returns:

    * `{:ok, count}` - The cache executes the query successfully and
      returns the `count` of the matched entries.

    * `{:error, reason}` - An error occurred executing the command.
      `reason` is the cause.

  May raise `Nebulex.QueryError` if query validation fails.

  ## Options

  See the ["Shared options"](#module-shared-options) section in the module
  documentation for more options.

  ## Examples

  Populate the cache with some entries:

      iex> Enum.each(1..5, &MyCache.put(&1, &1 * 2))
      :ok

  Count all entries in cache (cache size):

      iex> MyCache.count_all()
      {:ok, 5}

  Count all entries that match with the given query, assuming we are using
  `Nebulex.Adapters.Local` adapter:

      iex> query = [{{:_, :"$1", :"$2", :_, :_}, [{:>, :"$2", 5}], [true]}]
      iex> {:ok, count} = MyCache.count_all(query: query)

  See `c:get_all/2` for more query examples.
  """
  @doc group: "Query API"
  @callback count_all(query_spec(), opts()) :: ok_error_tuple(non_neg_integer())

  @doc """
  Same as `c:count_all/2`, but the command is executed on the cache instance
  given at the first argument `dynamic_cache`.

  See the ["Dynamic caches"](#module-dynamic-caches) section in the
  module documentation for more information.

  ## Examples

      iex> MyCache.count_all(MyCache1, [], [])
      {:ok, 0}

  """
  @doc group: "Query API"
  @callback count_all(dynamic_cache(), query_spec(), opts()) :: ok_error_tuple(non_neg_integer())

  @doc """
  Same as `c:count_all/2` but raises an exception if an error occurs.

  ## Examples

      iex> MyCache.count_all!()
      0

  """
  @doc group: "Query API"
  @callback count_all!(query_spec(), opts()) :: non_neg_integer()

  @doc """
  Same as `c:count_all!/3` but raises an exception if an error occurs.
  """
  @doc group: "Query API"
  @callback count_all!(dynamic_cache(), query_spec(), opts()) :: non_neg_integer()

  @doc """
  Similar to `c:get_all/2`, but returns a lazy enumerable that emits all entries
  matching the query specified by the given `query_spec`.

  See `c:get_all/2` for more information about the `query_spec`.

  This function returns:

    * `{:ok, stream}` - It returns a `stream` of values.

    * `{:error, reason}` - An error occurred executing the command.
      `reason` is the cause.

  May raise `Nebulex.QueryError` if query validation fails.

  ## Options

  #{Nebulex.Cache.Options.stream_options_docs()}

  See the ["Shared options"](#module-shared-options) section in the module
  documentation for more options.

  ## Examples

  Populate the cache with some entries:

      iex> MyCache.put_all(a: 1, b: 2, c: 3)
      :ok

  Stream all (default args):

      iex> {:ok, stream} = MyCache.stream()
      iex> Enum.to_list(stream)
      [a: 1, b: 2, c: 3]

  Stream all entries returning only the keys (with :max_entries option):

      iex> {:ok, stream} = MyCache.stream([select: :key], max_entries: 2)
      iex> Enum.to_list(stream)
      [:a, :b, :c]

  Stream all entries returning only the values:

      iex> {:ok, stream} = MyCache.stream(select: :value)
      iex> Enum.to_list(stream)
      [1, 2, 3]

  Stream only the requested keys (lazy bulk-fetch):

      iex> {:ok, stream} = MyCache.stream(in: [:a, :b, :d])
      iex> Enum.to_list(stream)
      [a: 1, b: 2]
      iex> {:ok, stream} = MyCache.stream(in: [:a, :b, :d], select: :key)
      iex> Enum.to_list(stream)
      [:a, :b]

  """
  @doc group: "Query API"
  @callback stream(query_spec(), opts()) :: ok_error_tuple(Enum.t())

  @doc """
  Same as `c:stream/2`, but the command is executed on the cache instance
  given at the first argument `dynamic_cache`.

  See the ["Dynamic caches"](#module-dynamic-caches) section in the
  module documentation for more information.

  ## Examples

      iex> = MyCache.stream(MyCache1, [], [])
      {:ok, _stream}

  """
  @doc group: "Query API"
  @callback stream(dynamic_cache(), query_spec(), opts()) :: ok_error_tuple(Enum.t())

  @doc """
  Same as `c:stream/2` but raises an exception if an error occurs.

  ## Examples

      iex> MyCache.put_all(a: 1, b: 2, c: 3)
      :ok
      iex> MyCache.stream!() |> Enum.to_list()
      [a: 1, b: 2, c: 3]

  """
  @doc group: "Query API"
  @callback stream!(query_spec(), opts()) :: Enum.t()

  @doc """
  Same as `c:stream!/3` but raises an exception if an error occurs.
  """
  @doc group: "Query API"
  @callback stream!(dynamic_cache(), query_spec(), opts()) :: Enum.t()

  ## Nebulex.Adapter.Transaction

  @optional_callbacks transaction: 2, transaction: 3, in_transaction?: 1, in_transaction?: 2

  @doc """
  Runs the given function inside a transaction.

  If an Elixir exception occurs, the exception will bubble up from the
  transaction function. If the cache aborts the transaction, it returns
  `{:error, reason}`.

  A successful transaction returns the value returned by the function wrapped
  in a tuple as `{:ok, value}`.

  ### Nested transactions

  If `transaction/2` is called inside another transaction, the cache executes
  the function without wrapping the new transaction call in any way.

  ## Options

  See the ["Shared options"](#module-shared-options) section in the module
  documentation for more options.

  ## Examples

      MyCache.transaction(fn ->
        alice = MyCache.get(:alice)
        bob = MyCache.get(:bob)
        MyCache.put(:alice, %{alice | balance: alice.balance + 100})
        MyCache.put(:bob, %{bob | balance: bob.balance + 100})
      end)

  We can provide the keys to lock when using the `Nebulex.Adapters.Local`
  adapter (or any other adapter that supports key locking):

      MyCache.transaction(
        fn ->
          alice = MyCache.get(:alice)
          bob = MyCache.get(:bob)
          MyCache.put(:alice, %{alice | balance: alice.balance + 100})
          MyCache.put(:bob, %{bob | balance: bob.balance + 100})
        end,
        keys: [:alice, :bob]
      )

  """
  @doc group: "Transaction API"
  @callback transaction(fun(), opts()) :: ok_error_tuple(any())

  @doc """
  Same as `c:transaction/2`, but the command is executed on the cache instance
  given at the first argument `dynamic_cache`.

  See the ["Dynamic caches"](#module-dynamic-caches) section in the
  module documentation for more information.

  ## Examples

      MyCache.transaction(
        MyCache1,
        fn ->
          alice = MyCache.get(:alice)
          bob = MyCache.get(:bob)
          MyCache.put(:alice, %{alice | balance: alice.balance + 100})
          MyCache.put(:bob, %{bob | balance: bob.balance + 100})
        end,
        keys: [:alice, :bob]
      )

  """
  @doc group: "Transaction API"
  @callback transaction(dynamic_cache(), fun(), opts()) :: ok_error_tuple(any())

  @doc """
  Returns `{:ok, true}` if the current process is inside a transaction;
  otherwise, `{:ok, false}` is returned.

  If there's an error with executing the command, `{:error, reason}`
  is returned, where `reason` is the cause of the error.

  ## Options

  See the ["Shared options"](#module-shared-options) section in the module
  documentation for more options.

  ## Examples

      MyCache.in_transaction?()
      #=> {:ok, false}

      MyCache.transaction(fn ->
        MyCache.in_transaction? #=> {:ok, true}
      end)

  """
  @doc group: "Transaction API"
  @callback in_transaction?(opts()) :: ok_error_tuple(boolean())

  @doc """
  Same as `c:in_transaction?/1`, but the command is executed on the cache instance
  given at the first argument `dynamic_cache`.

  See the ["Dynamic caches"](#module-dynamic-caches) section in the
  module documentation for more information.

  ## Examples

      MyCache.in_transaction?(MyCache1, [])

  """
  @doc group: "Transaction API"
  @callback in_transaction?(dynamic_cache(), opts()) :: ok_error_tuple(boolean())

  ## Nebulex.Adapter.Info

  @optional_callbacks info: 2, info: 3, info!: 2, info!: 3

  @doc """
  Returns `{:ok, info}` where `info` contains the requested cache information,
  as specified by the `spec`.

  If there's an error with executing the command, `{:error, reason}`
  is returned, where `reason` is the cause of the error.

  The `spec` (information specification key) can be:

    * **The atom `:all`** - returns a map with all information items.
    * **An atom** - returns the value for the requested information item.
    * **A list of atoms** - returns a map only with the requested information
      items.

  If the argument `spec` is omitted, all information items are returned;
  same as if the `spec` was the atom `:all`.

  The adapters are free to add the information specification keys they want.
  However, Nebulex suggests the adapters add the following keys:

    * `:server` - General information about the cache server (e.g., cache name,
      adapter, PID, etc.).
    * `:memory` - Memory consumption information (e.g., used memory,
      allocated memory, etc.).
    * `:stats` - Cache statistics (e.g., hits, misses, etc.).

  ## Examples

  The following examples assume the underlying adapter uses the implementation
  provided by `Nebulex.Adapters.Common.Info`.

      iex> {:ok, info} = MyCache.info()
      iex> info
      %{
        server: %{
          nbx_version: "3.0.0",
          cache_module: "MyCache",
          cache_adapter: "Nebulex.Adapters.Local",
          cache_name: "MyCache",
          cache_pid: #PID<0.111.0>
        },
        memory: %{
          total: 1_000_000,
          used: 0
        },
        stats: %{
          deletions: 0,
          evictions: 0,
          expirations: 0,
          hits: 0,
          misses: 0,
          updates: 0,
          writes: 0
        }
      }

      iex> {:ok, info} = MyCache.info(:server)
      iex> info
      %{
        nbx_version: "3.0.0",
        cache_module: "MyCache",
        cache_adapter: "Nebulex.Adapters.Local",
        cache_name: "MyCache",
        cache_pid: #PID<0.111.0>
      }

      iex> {:ok, info} = MyCache.info([:server, :stats])
      iex> info
      %{
        server: %{
          nbx_version: "3.0.0",
          cache_module: "MyCache",
          cache_adapter: "Nebulex.Adapters.Local",
          cache_name: "MyCache",
          cache_pid: #PID<0.111.0>
        },
        stats: %{
          deletions: 0,
          evictions: 0,
          expirations: 0,
          hits: 0,
          misses: 0,
          updates: 0,
          writes: 0
        }
      }

  """
  @doc group: "Info API"
  @callback info(spec :: info_spec(), opts()) :: ok_error_tuple(info_data())

  @doc """
  Same as `c:info/2`, but the command is executed on the cache
  instance given at the first argument `dynamic_cache`.

  See the ["Dynamic caches"](#module-dynamic-caches) section in the
  module documentation for more information.

  ## Examples

      MyCache.info(MyCache1, :all, [])

  """
  @doc group: "Info API"
  @callback info(dynamic_cache(), spec :: info_spec(), opts()) :: ok_error_tuple(info_data())

  @doc """
  Same as `c:info/2` but raises an exception if an error occurs.
  """
  @doc group: "Info API"
  @callback info!(spec :: info_spec(), opts()) :: info_data()

  @doc """
  Same as `c:info/3` but raises an exception if an error occurs.
  """
  @doc group: "Info API"
  @callback info!(dynamic_cache(), spec :: info_spec(), opts()) :: info_data()

  ## Nebulex.Adapter.Observable

  @optional_callbacks register_event_listener: 2,
                      register_event_listener: 3,
                      register_event_listener!: 2,
                      register_event_listener!: 3,
                      unregister_event_listener: 2,
                      unregister_event_listener: 3,
                      unregister_event_listener!: 2,
                      unregister_event_listener!: 3

  @doc """
  Register a cache event listener `event_listener`.

  Returns `:ok` if successful; `{:error, reason}` otherwise.

  Listeners should be implemented with care. In particular, it is important
  to consider their impact on performance and latency.

  Listeners:

    * are fired after the entry is mutated in the cache.
    * block the calling process until the listener returns,
      when the listener is invoked synchronously; depends on
      the adapter's implementation.

  Listeners follow the observer pattern. An exception raised by a listener does
  not cause the cache operation to fail.

  Listeners can only raise `Nebulex.Error` exception. Caching implementations
  must catch any other exception from a listener, then wrap and reraise it as
  a `Nebulex.Error` exception.

  ## Options

  #{Nebulex.Cache.Options.observable_options_docs()}

  See the ["Shared options"](#module-shared-options) section in the module
  documentation for more options.

  ## Examples

      iex> MyApp.Cache.register_event_listener(&MyApp.handle/1)
      :ok
      iex> MyApp.Cache.register_event_listener(&MyApp.handle/1,
      ...>   filter: &MyApp.filter/1
      ...> )
      :ok
      iex> MyApp.Cache.register_event_listener(&MyApp.handle/2,
      ...>   filter: &MyApp.filter/2
      ...>   metadata: [foo: :bar]
      ...> )
      :ok

      # Register with `:id` (must be unregistered using the same `:id` value)
      iex> MyApp.Cache.register_event_listener(&MyApp.handle/2,
      ...>   id: :my_listener,
      ...>   filter: &MyApp.filter/2
      ...>   metadata: [foo: :bar]
      ...> )
      :ok

  """
  @doc group: "Observable API"
  @callback register_event_listener(event_listener(), opts()) :: :ok | error_tuple()

  @doc """
  Same as `c:register_event_listener/2`, but the command is executed on the cache
  instance given at the first argument `dynamic_cache`.

  See the ["Dynamic caches"](#module-dynamic-caches) section in the
  module documentation for more information.

  ## Examples

      MyApp.Cache.register_event_listener(:my_cache, &MyApp.handle/1, [])

  """
  @doc group: "Observable API"
  @callback register_event_listener(dynamic_cache(), event_listener(), opts()) ::
              :ok | error_tuple()

  @doc """
  Same as `c:register_event_listener/2` but raises an exception if an error
  occurs.
  """
  @doc group: "Observable API"
  @callback register_event_listener!(event_listener(), opts()) :: :ok

  @doc """
  Same as `c:register_event_listener/3` but raises an exception if an error
  occurs.
  """
  @doc group: "Observable API"
  @callback register_event_listener!(dynamic_cache(), event_listener(), opts()) :: :ok

  @doc """
  Un-register a cache event listener.

  Returns `:ok` if successful; `{:error, reason}` otherwise.

  ## Options

  See the ["Shared options"](#module-shared-options) section in the module
  documentation for more options.

  ## Examples

      # Register with default ID
      iex> MyApp.Cache.register_event_listener(&MyApp.handle/1)
      :ok
      # Unregister with default ID
      iex> MyApp.Cache.unregister_event_listener(&MyApp.handle/1)
      :ok

      # Register with `:id`
      iex> MyApp.Cache.register_event_listener(&MyApp.handle/1, id: :listener)
      :ok
      # Unregister using the previously registered `:id`
      iex> MyApp.Cache.unregister_event_listener(:listener)
      :ok

  """
  @doc group: "Observable API"
  @callback unregister_event_listener(id :: any(), opts()) :: :ok | error_tuple()

  @doc """
  Same as `c:unregister_event_listener/2`, but the command is executed on the
  cache instance given at the first argument `dynamic_cache`.

  See the ["Dynamic caches"](#module-dynamic-caches) section in the
  module documentation for more information.

  ## Examples

      MyApp.Cache.unregister_event_listener(:my_cache, &MyApp.handle/1, [])

  """
  @doc group: "Observable API"
  @callback unregister_event_listener(dynamic_cache(), id :: any(), opts()) ::
              :ok | error_tuple()

  @doc """
  Same as `c:unregister_event_listener/2` but raises an exception if an error
  occurs.
  """
  @doc group: "Observable API"
  @callback unregister_event_listener!(id :: any(), opts()) :: :ok

  @doc """
  Same as `c:unregister_event_listener/3` but raises an exception if an error
  occurs.
  """
  @doc group: "Observable API"
  @callback unregister_event_listener!(dynamic_cache(), id :: any(), opts()) :: :ok
end
