defmodule Supabase.Storage.Cache do
@moduledoc """
Provides caching mechanisms for Supabase Storage Buckets.
This module acts as a GenServer that offers caching capabilities, especially for bucket-related operations in Supabase Storage. The caching is backed by the `:ets` (Erlang Term Storage) to provide in-memory storage and fast retrieval of cached data.
## Features
- **Bucket Caching**: Store and retrieve buckets by their unique identifier.
- **Cache Flushing**: Clear the cache when necessary.
- **Configurable Cache Size**: Limit the number of items that can be stored in the cache.
## Usage
### Starting the Cache Server
Supabase.Storage.Cache.start_link(%{cache_max_size: 200})
### Caching Buckets
buckets = [%{id: "bucket_1", ...}, %{id: "bucket_2", ...}]
Supabase.Storage.Cache.cache_buckets(buckets)
### Retrieving a Cached Bucket by ID
Supabase.Storage.Cache.find_bucket_by_id("bucket_1")
### Clearing the Cache
Supabase.Storage.Cache.flush()
## Implementation Details
The cache uses the `:ets` module for in-memory storage of buckets. The number of buckets cached is controlled by the `:cache_max_size` option (default: 100). When the cache is close to exceeding its maximum size, older entries are removed to accommodate new ones.
"""
use GenServer
## Client
def start_link(args) do
GenServer.start_link(__MODULE__, args, name: __MODULE__)
end
def find_bucket_by_id(id) do
GenServer.call(__MODULE__, {:find_bucket, id: id})
end
def cache_buckets(buckets) do
GenServer.cast(__MODULE__, {:cache_buckets, buckets})
end
def flush do
GenServer.cast(__MODULE__, :flush)
end
## API
@impl true
def init(args) do
Process.flag(:trap_exit, true)
table = :ets.new(:buckets_cache, [:set, :public, :named_table])
max_size = Keyword.get(args, :cache_max_size, 100)
{:ok, %{table: table, max_size: max_size, size: 0}}
end
@impl true
def handle_cast(:flush, table) do
:ets.delete_all_objects(table)
{:noreply, table}
end
def handle_cast({:cache_buckets, buckets}, state) do
if overflowed_max_size?(state, buckets) do
:ets.delete_all_objects(state.table)
end
# prefer atomic operations
for bucket <- buckets do
:ets.insert_new(state.table, {bucket.id, bucket})
end
{:noreply, %{state | size: length(buckets)}}
end
defp overflowed_max_size?(state, buckets) do
state.size + length(buckets) > state.max_size
end
@impl true
def handle_call({:find_bucket, id: id}, _from, state) do
bucket = :ets.lookup_element(state.table, id, 2)
{:reply, bucket, state}
rescue
_ -> {:reply, nil, state}
end
end