Packages
electric_client
0.9.2
0.10.3
0.10.2
0.10.1
0.10.1-beta-1
0.10.0
0.9.5-beta-1
0.9.4
0.9.4-beta-1
0.9.3
0.9.2
0.9.1
0.9.0
0.8.3
0.8.3-beta-1
0.8.2
0.8.1
0.8.0
0.8.0-beta-1
0.7.3
0.7.2
0.7.1
0.7.0
0.6.5
0.6.5-beta-5
0.6.5-beta-4
0.6.5-beta-3
0.6.5-beta-2
0.6.5-beta-1
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.0
0.5.0-beta-1
0.4.1
0.4.0
0.3.2
0.3.1
0.3.0
0.3.0-beta.4
0.3.0-beta.3
0.3.0-beta.2
0.2.6-pre-1
retired
0.2.6-beta.1
0.2.6-beta.0
0.2.5
0.2.4
0.2.4-pre-8
0.2.4-pre-7
0.2.4-pre-6
0.2.4-pre-5
0.2.4-pre-4
0.2.4-pre-3
0.2.4-pre-2
0.2.4-pre-1
0.2.3
0.2.3-rc-1
0.2.2
0.2.2-rc-1
0.2.1
0.2.1-rc-3
0.2.1-rc-2
0.2.1-rc-1
0.2.0
0.1.2
0.1.1
0.1.0
0.1.0-dev-9
0.1.0-dev-8
0.1.0-dev-7
0.1.0-dev-6
0.1.0-dev-5
0.1.0-dev-4
0.1.0-dev-3
0.1.0-dev-2
0.1.0-dev-17
0.1.0-dev-16
0.1.0-dev-15
0.1.0-dev-14
0.1.0-dev-13
0.1.0-dev-12
0.1.0-dev-11
0.1.0-dev-10
0.1.0-dev
Elixir client for ElectricSQL
Current section
Files
Jump to
Current section
Files
lib/electric/client/expired_shapes_cache.ex
defmodule Electric.Client.ExpiredShapesCache do
@moduledoc """
LRU cache for tracking expired shape handles.
This cache stores shape handles that have been marked as expired (typically after
receiving a 409 response). When making subsequent requests for the same shape,
the client includes the expired handle as a query parameter to help bypass
stale CDN/proxy caches.
The cache uses ETS for fast concurrent reads and a GenServer to manage
LRU eviction when the cache exceeds the maximum number of entries.
"""
use GenServer
@table_name :electric_expired_shapes
@max_entries 250
# Public API
@doc """
Get the expired handle for a shape key, if one exists.
Updates the last_used timestamp to maintain LRU ordering.
"""
@spec get_expired_handle(String.t()) :: String.t() | nil
def get_expired_handle(shape_key) do
case :ets.lookup(@table_name, shape_key) do
[{^shape_key, %{expired_handle: handle}}] ->
# Update last_used timestamp asynchronously
GenServer.cast(__MODULE__, {:touch, shape_key})
handle
[] ->
nil
end
end
@doc """
Mark a shape handle as expired for the given shape key.
If the cache exceeds the maximum number of entries, the least recently
used entry will be evicted.
"""
@spec mark_expired(String.t(), String.t()) :: :ok
def mark_expired(shape_key, handle) do
GenServer.call(__MODULE__, {:mark_expired, shape_key, handle})
end
@doc """
Clear all entries from the cache.
"""
@spec clear() :: :ok
def clear do
GenServer.call(__MODULE__, :clear)
end
@doc """
Get the current number of entries in the cache.
Primarily for testing purposes.
"""
@spec size() :: non_neg_integer()
def size do
:ets.info(@table_name, :size)
end
# GenServer implementation
def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
@impl true
def init(_opts) do
table =
:ets.new(@table_name, [
:set,
:public,
:named_table,
read_concurrency: true
])
{:ok, %{table: table}}
end
@impl true
def handle_call({:mark_expired, shape_key, handle}, _from, state) do
timestamp = System.monotonic_time()
:ets.insert(@table_name, {shape_key, %{expired_handle: handle, last_used: timestamp}})
# Evict oldest entries if we exceed the limit
evict_if_needed()
{:reply, :ok, state}
end
@impl true
def handle_call(:clear, _from, state) do
:ets.delete_all_objects(@table_name)
{:reply, :ok, state}
end
@impl true
def handle_cast({:touch, shape_key}, state) do
timestamp = System.monotonic_time()
case :ets.lookup(@table_name, shape_key) do
[{^shape_key, entry}] ->
:ets.insert(@table_name, {shape_key, %{entry | last_used: timestamp}})
[] ->
:ok
end
{:noreply, state}
end
defp evict_if_needed do
size = :ets.info(@table_name, :size)
if size > @max_entries do
# Find and evict the oldest entry
oldest =
:ets.foldl(
fn {key, %{last_used: ts}}, acc ->
case acc do
nil -> {key, ts}
{_oldest_key, oldest_ts} when ts < oldest_ts -> {key, ts}
_ -> acc
end
end,
nil,
@table_name
)
case oldest do
{oldest_key, _ts} ->
:ets.delete(@table_name, oldest_key)
nil ->
:ok
end
end
end
end