Skip to main content

lib/jidoka/memory/store/in_memory.ex

defmodule Jidoka.Memory.Store.InMemory do
  @moduledoc """
  In-memory memory store for deterministic tests and examples.
  """

  @behaviour Jidoka.Memory.Store

  alias Jidoka.Memory.Entry
  alias Jidoka.Memory.RecallRequest
  alias Jidoka.Memory.RecallResult
  alias Jidoka.Memory.WriteRequest
  alias Jidoka.Memory.WriteResult

  @spec start_link(keyword()) :: Agent.on_start()
  def start_link(opts \\ []) do
    Agent.start_link(fn -> [] end, opts)
  end

  @impl true
  def recall(%RecallRequest{} = request, opts) do
    pid = fetch_pid!(opts)

    entries =
      pid
      |> Agent.get(& &1)
      |> Enum.filter(&matches_request?(&1, request))
      |> Enum.take(request.limit)

    RecallResult.new(request: request, entries: entries)
  end

  @impl true
  def write(%WriteRequest{entry: %Entry{} = entry} = request, opts) do
    pid = fetch_pid!(opts)

    Agent.update(pid, fn entries ->
      [entry | Enum.reject(entries, &(&1.id == entry.id))]
    end)

    WriteResult.new(request: request, entry: entry)
  end

  @impl true
  def list_entries(opts) do
    pid = fetch_pid!(opts)

    entries =
      pid
      |> Agent.get(& &1)
      |> Enum.reverse()

    {:ok, entries}
  end

  defp matches_request?(%Entry{} = entry, %RecallRequest{} = request) do
    entry.agent_id == request.agent_id and
      session_matches?(entry.session_id, request.session_id, request.scope)
  end

  defp session_matches?(nil, _session_id, :agent), do: true
  defp session_matches?(session_id, session_id, _scope), do: true
  defp session_matches?(_entry_session_id, _request_session_id, _scope), do: false

  defp fetch_pid!(opts) do
    case Keyword.fetch(opts, :pid) do
      {:ok, pid} when is_pid(pid) -> pid
      {:ok, name} when is_atom(name) -> name
      :error -> raise ArgumentError, "in-memory memory store requires :pid"
    end
  end
end