Daraja.TokenCache (daraja v0.1.0)

Copy Markdown View Source

ETS-backed GenServer that caches Daraja access tokens to eliminate the network round-trip on every API call.

Add Daraja.Supervisor to your application's supervision tree to opt in:

children = [
  {Daraja.Supervisor, []}
]

The library is process-free by default; caching is opt-in.

Cache key

Tokens are keyed by {consumer_key, environment, secret_hash} where secret_hash is a SHA-256 digest of consumer_secret. Rotating the secret while keeping the same consumer key produces a cache miss and fetches a fresh token. Call invalidate/1 after rotation to evict any orphaned entry keyed under the previous secret.

TTL and refresh

Token lifetime comes from Safaricom's OAuth expires_in field. :ttl (default 3600 s) is used only when expires_in is absent or unparseable. :refresh_before (default 120 s) controls how many seconds before expiry the cache proactively fetches a fresh token in the background.

refresh_before should be less than the configured :ttl fallback and the effective token lifetime. When expires_in is shorter than refresh_before, refresh is scheduled immediately. init/1 raises when refresh_before >= ttl.

Concurrent fetches

OAuth network I/O runs in Task.Supervisor workers — not inside the GenServer mailbox — so cache misses for different credentials fetch in parallel. Concurrent misses for the same key share one in-flight task.

Crash and restart

ETS tables are owned by the GenServer process. On crash and supervisor restart, the old table is gone and a fresh one is created — all cached tokens are lost and the next request per client pays a cold network fetch.

ETS access

The cache table is :protected: any process may read (lock-free happy path in get_token/2), but only the owning GenServer may insert or delete. This prevents local cache poisoning by unrelated processes while preserving read concurrency.

Credential retention

Client credentials are held in the GenServer's clients map (keyed like the token cache) so proactive refresh timers carry only the cache key—not a full %Daraja.Client{}—in delayed messages.

Summary

Functions

Returns a cached access token for client, fetching from the network on a miss.

Evicts the cached token for client, canceling any scheduled refresh.

Functions

get_token(client, server \\ __MODULE__)

@spec get_token(Daraja.Client.t(), atom()) ::
  {:ok, String.t()}
  | {:error, :auth_failed, term()}
  | {:error, :http_error, term()}

Returns a cached access token for client, fetching from the network on a miss.

Reads directly from ETS on the happy path (lock-free). Falls back to a GenServer.call on a miss or when the ETS table doesn't exist yet (brief startup race between supervisor start and init/1 completing).

server defaults to Daraja.TokenCache. Pass a custom atom when using a non-default cache name configured via cache_name: in Daraja.Supervisor.

invalidate(client, server \\ __MODULE__)

@spec invalidate(Daraja.Client.t(), atom()) :: :ok

Evicts the cached token for client, canceling any scheduled refresh.

Use after credential rotation to drop a stale entry obtained with the previous secret. With the updated cache key, a rotated secret already misses the cache; invalidate/1 cleans up the orphaned entry and timer.

server defaults to Daraja.TokenCache.

start_link(opts)