Packages
fnord
0.9.12
0.9.40
0.9.39
0.9.38
0.9.37
0.9.36
0.9.35
0.9.34
0.9.33
0.9.32
0.9.31
0.9.30
0.9.29
0.9.28
0.9.27
0.9.26
0.9.25
0.9.24
0.9.23
0.9.22
0.9.21
0.9.20
0.9.19
0.9.18
0.9.17
0.9.16
0.9.15
0.9.14
0.9.13
0.9.12
0.9.11
0.9.10
0.9.9
0.9.8
0.9.7
0.9.6
0.9.5
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.99
0.8.98
0.8.97
0.8.96
0.8.95
0.8.94
0.8.93
0.8.92
0.8.91
0.8.90
0.8.89
0.8.88
0.8.87
0.8.86
0.8.85
0.8.84
0.8.83
0.8.82
0.8.81
0.8.80
0.8.79
0.8.78
0.8.77
0.8.76
0.8.75
0.8.74
0.8.73
0.8.72
0.8.71
0.8.70
0.8.69
0.8.68
0.8.67
0.8.66
0.8.65
0.8.64
0.8.63
0.8.62
0.8.61
0.8.60
0.8.59
0.8.58
0.8.57
0.8.56
0.8.55
0.8.54
0.8.53
0.8.52
0.8.51
0.8.50
0.8.49
0.8.48
0.8.47
0.8.46
0.8.45
0.8.44
0.8.43
0.8.42
0.8.41
0.8.40
0.8.39
0.8.38
0.8.37
0.8.36
0.8.35
0.8.34
0.8.33
0.8.32
0.8.31
0.8.30
0.8.29
0.8.27
0.8.26
0.8.25
0.8.24
0.8.23
0.8.22
0.8.21
0.8.20
0.8.19
0.8.18
0.8.17
0.8.16
0.8.15
0.8.14
0.8.13
0.8.12
0.8.11
0.8.1
0.8.0
0.7.24
0.7.23
0.7.22
0.7.21
0.7.20
0.7.19
0.7.18
0.7.17
0.7.16
0.7.15
0.7.14
0.7.13
0.7.12
0.7.11
0.7.10
0.7.9
0.7.8
0.7.7
0.7.6
0.7.5
0.7.3
0.7.2
0.7.1
0.7.0
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.1
0.6.0
0.5.9
0.5.8
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.44
0.4.43
0.4.42
0.4.41
0.4.40
0.4.39
0.4.38
0.4.37
0.4.36
0.4.35
0.4.34
0.4.33
0.4.32
0.4.30
0.4.29
0.4.28
0.4.27
0.4.26
0.4.25
0.4.24
0.4.23
0.4.22
0.4.21
0.4.20
0.4.19
0.4.18
0.4.17
0.4.16
0.4.15
0.4.14
0.4.13
0.4.12
0.4.11
0.4.10
0.4.9
0.4.8
0.4.7
0.4.6
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.0
0.2.0
0.1.0
AI code archaeology
Current section
Files
Jump to
Current section
Files
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