Packages
tokenio_client
1.0.0
Production-grade Elixir client for the Token.io Open Banking platform (Payments v2, VRP, AIS, Banks, Refunds, Payouts, Settlement, Transfers, Tokens, Token Requests, Account on File, Sub-TPPs, Auth Keys, Reports, Webhooks, Verification).
Current section
Files
Jump to
Current section
Files
lib/tokenio/http/token_cache.ex
defmodule TokenioClient.HTTP.TokenCache do
@moduledoc """
ETS-backed OAuth2 token cache with TTL expiry.
Tokens are cached per `client_id` and automatically refreshed
60 seconds before expiry to avoid races at TTL boundaries.
Supervised automatically by `TokenioClient.Application`.
"""
use GenServer
@table :tokenio_client_token_cache
# Refresh 60 s before the token actually expires
@refresh_buffer_s 60
@typep fetch_fn :: (-> {:ok, String.t(), pos_integer()} | {:error, TokenioClient.Error.t()})
# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------
@doc """
Return a valid cached token for `client_id`, or call `fetch_fn` to obtain one.
`fetch_fn` must return `{:ok, token, ttl_seconds}` on success.
"""
@spec get_or_fetch(String.t(), fetch_fn()) ::
{:ok, String.t()} | {:error, TokenioClient.Error.t()}
def get_or_fetch(client_id, fetch_fn) do
case lookup(client_id) do
{:ok, token} ->
{:ok, token}
:miss ->
case fetch_fn.() do
{:ok, token, ttl_s} ->
store(client_id, token, ttl_s)
{:ok, token}
{:error, _} = err ->
err
end
end
end
@doc false
@spec start_link(keyword()) :: GenServer.on_start()
def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
# ---------------------------------------------------------------------------
# GenServer callbacks
# ---------------------------------------------------------------------------
@spec init(keyword()) :: {:ok, map()}
@impl GenServer
def init(_opts) do
:ets.new(@table, [:named_table, :public, read_concurrency: true])
{:ok, %{}}
end
# ---------------------------------------------------------------------------
# Private helpers
# ---------------------------------------------------------------------------
@spec lookup(String.t()) :: {:ok, String.t()} | :miss
defp lookup(client_id) do
now = System.system_time(:second)
case :ets.lookup(@table, client_id) do
[{^client_id, token, expires_at}] when expires_at - now > @refresh_buffer_s ->
{:ok, token}
_ ->
:miss
end
end
@spec store(String.t(), String.t(), pos_integer()) :: true
defp store(client_id, token, ttl_s) do
expires_at = System.system_time(:second) + ttl_s
:ets.insert(@table, {client_id, token, expires_at})
end
end