Packages

fnord

0.9.30

AI code archaeology

Current section

Files

Jump to
fnord lib services file_cache.ex
Raw

lib/services/file_cache.ex

defmodule Services.FileCache do
@moduledoc """
GenServer-backed file cache used by AI tools and other services that need
to read file contents from the project workspace.
Responsibilities:
- Store {path -> %{sha: sha, content: content}}
- Provide lookup and insert APIs
- Handle concurrent access safely via GenServer
The cache is intentionally simple: it validates freshness by recomputing a
sha256 over the file contents when doing a lookup and updates the stored
entry when a mismatch is detected.
"""
use GenServer
@name __MODULE__
@type entry :: %{sha: String.t(), content: String.t()}
# ----------------------------------------------------------------------------
# Client API
# ----------------------------------------------------------------------------
@spec start_link(keyword()) :: GenServer.on_start()
def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, %{}, Keyword.put_new(opts, :name, @name))
end
@doc """
Lookup a cached file by absolute path. Returns {:ok, content} or :miss. The
caller should provide a fetch_fun that will be called to obtain actual file
contents when the cache is missing or stale.
"""
@spec get_or_fetch(String.t(), (-> {:ok, String.t()} | {:error, any()})) ::
{:ok, String.t()}
| {:error, any()}
| :miss
def get_or_fetch(path, fetch_fun) do
GenServer.call(@name, {:get_or_fetch, path, fetch_fun}, :infinity)
end
@doc "Directly put content into the cache for a path."
@spec put(String.t(), String.t()) :: :ok
def put(path, content) do
GenServer.cast(@name, {:put, path, content})
end
# ----------------------------------------------------------------------------
# Server callbacks
# ----------------------------------------------------------------------------
@impl true
def init(state) do
{:ok, state}
end
@impl true
def handle_call({:get_or_fetch, path, fetch_fun}, _from, state) do
case Map.get(state, path) do
%{sha: sha, content: content} ->
# validate current content by calling fetch_fun
case fetch_fun.() do
{:ok, new_content} ->
new_sha = sha256(new_content)
if new_sha == sha do
{:reply, {:ok, content}, state}
else
new_state = Map.put(state, path, %{sha: new_sha, content: new_content})
{:reply, {:ok, new_content}, new_state}
end
{:error, _} ->
# fallback to cached content on read error
{:reply, {:ok, content}, state}
end
nil ->
case fetch_fun.() do
{:ok, content} ->
entry = %{sha: sha256(content), content: content}
{:reply, {:ok, content}, Map.put(state, path, entry)}
{:error, reason} ->
{:reply, {:error, reason}, state}
end
end
end
@impl true
def handle_cast({:put, path, content}, state) do
entry = %{sha: sha256(content), content: content}
{:noreply, Map.put(state, path, entry)}
end
defp sha256(content) when is_binary(content) do
:crypto.hash(:sha256, content) |> Base.encode16(case: :lower)
end
end