-module(aws@internal@credentials_cache). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/aws/internal/credentials_cache.gleam"). -export([start/3, start_default/1, get/1, as_provider/1, shutdown/1, shutdown_sync/2]). -export_type([cache/0, message/0, state/0, start_error/0]). -if(?OTP_RELEASE >= 27). -define(MODULEDOC(Str), -moduledoc(Str)). -define(DOC(Str), -doc(Str)). -else. -define(MODULEDOC(Str), -compile([])). -define(DOC(Str), -compile([])). -endif. ?MODULEDOC( " Credentials cache: a `gleam_otp` actor that owns a `Provider` and caches\n" " the last successful `Credentials`. Re-fetches when the cached value is\n" " within `buffer_seconds` of its `expires_at`, or when the cache is empty\n" " (first call) or the previous fetch failed.\n" "\n" " Non-expiring credentials (`expires_at = None`) are cached forever — env\n" " vars don't rotate without a process restart, so re-reading them on every\n" " signed request would be wasteful.\n" "\n" " Concurrency: actor messages are handled sequentially, so two parallel\n" " `get` calls during the first fetch coalesce into a single provider\n" " invocation. No thundering-herd shielding beyond that — adequate for the\n" " rates AWS SDKs see in practice.\n" ). -opaque cache() :: {cache, gleam@erlang@process:subject(message())}. -type message() :: {get, gleam@erlang@process:subject({ok, aws@credentials:credentials()} | {error, aws@credentials:provider_error()})} | stop. -type state() :: {state, aws@credentials:provider(), fun(() -> integer()), integer(), gleam@option:option(aws@credentials:credentials())}. -type start_error() :: {start_failed, gleam@otp@actor:start_error()}. -file("src/aws/internal/credentials_cache.gleam", 192). -spec fresh_enough(state()) -> boolean(). fresh_enough(State) -> case erlang:element(5, State) of none -> false; {some, Creds} -> case erlang:element(5, Creds) of none -> true; {some, Expires_at} -> (Expires_at - (erlang:element(3, State))()) > erlang:element( 4, State ) end end. -file("src/aws/internal/credentials_cache.gleam", 145). -spec handle_message(state(), message()) -> gleam@otp@actor:next(state(), message()). handle_message(State, Message) -> case Message of stop -> gleam@otp@actor:stop(); {get, Reply} -> case fresh_enough(State) of true -> Creds@1 = case erlang:element(5, State) of {some, Creds} -> Creds; _assert_fail -> erlang:error(#{gleam_error => let_assert, message => <<"Pattern match failed, no pattern matched the value."/utf8>>, file => <>, module => <<"aws/internal/credentials_cache"/utf8>>, function => <<"handle_message"/utf8>>, line => 154, value => _assert_fail, start => 5552, 'end' => 5589, pattern_start => 5563, pattern_end => 5574}) end, aws@internal@log:debug( fun() -> <<<<"aws credentials cache: hit (source "/utf8, (erlang:element(6, Creds@1))/binary>>/binary, ")"/utf8>> end ), gleam@erlang@process:send(Reply, {ok, Creds@1}), gleam@otp@actor:continue(State); false -> aws@internal@log:debug( fun() -> <<"aws credentials cache: miss — refreshing"/utf8>> end ), case (erlang:element(3, erlang:element(2, State)))() of {ok, Creds@2} -> aws@internal@log:debug( fun() -> <<<<"aws credentials cache: refreshed (source "/utf8, (erlang:element(6, Creds@2))/binary>>/binary, ")"/utf8>> end ), gleam@erlang@process:send(Reply, {ok, Creds@2}), gleam@otp@actor:continue( {state, erlang:element(2, State), erlang:element(3, State), erlang:element(4, State), {some, Creds@2}} ); {error, Error} -> aws@internal@log:debug( fun() -> <<"aws credentials cache: refresh failed"/utf8>> end ), gleam@erlang@process:send(Reply, {error, Error}), gleam@otp@actor:continue(State) end end end. -file("src/aws/internal/credentials_cache.gleam", 67). ?DOC( " Start the cache actor.\n" "\n" " - `provider`: the upstream provider this cache wraps. Can itself be a\n" " `credentials.chain([...])` — the cache doesn't care.\n" " - `clock`: returns unix seconds. The default production wiring uses\n" " `erlang:system_time(second)`; tests pass a closure over a controlled\n" " counter so they can fast-forward across expiries.\n" " - `buffer_seconds`: trigger a refresh this many seconds before\n" " `expires_at`. See `default_buffer_seconds`.\n" ). -spec start(aws@credentials:provider(), fun(() -> integer()), integer()) -> {ok, cache()} | {error, start_error()}. start(Provider, Clock, Buffer_seconds) -> Initial_state = {state, Provider, Clock, Buffer_seconds, none}, case begin _pipe = gleam@otp@actor:new(Initial_state), _pipe@1 = gleam@otp@actor:on_message(_pipe, fun handle_message/2), gleam@otp@actor:start(_pipe@1) end of {ok, Started} -> {ok, {cache, erlang:element(3, Started)}}; {error, Reason} -> {error, {start_failed, Reason}} end. -file("src/aws/internal/credentials_cache.gleam", 91). ?DOC( " Start a cache using the OS clock and `default_buffer_seconds`. For\n" " production wiring this is almost always what you want.\n" ). -spec start_default(aws@credentials:provider()) -> {ok, cache()} | {error, start_error()}. start_default(Provider) -> start(Provider, fun aws_ffi:unix_seconds/0, 300). -file("src/aws/internal/credentials_cache.gleam", 103). ?DOC( " Fetch the current credentials, refreshing from the wrapped provider if\n" " the cache is empty or the credentials are within the refresh buffer of\n" " expiry. Returns whatever the provider produced — the cache itself never\n" " fabricates errors.\n" ). -spec get(cache()) -> {ok, aws@credentials:credentials()} | {error, aws@credentials:provider_error()}. get(Cache) -> case aws@internal@actor_lifecycle:safe_call( erlang:element(2, Cache), 5000, fun(Field@0) -> {get, Field@0} end ) of {ok, Provider_result} -> Provider_result; {error, nil} -> aws@internal@log:warning( <<"aws credentials cache: actor unavailable (dead or timed out)"/utf8>> ), {error, {fetch_failed, <<"credentials cache actor unavailable"/utf8>>}} end. -file("src/aws/internal/credentials_cache.gleam", 126). ?DOC( " Re-expose the cache as a regular `Provider`. The returned provider's\n" " `fetch` closure proxies to `get(cache)` — so the rest of the SDK can\n" " thread `Provider` values around as before, but now hot-path reads\n" " debounce into the actor and avoid re-running the seven-stage chain\n" " on every signed request.\n" ). -spec as_provider(cache()) -> aws@credentials:provider(). as_provider(Cache) -> {provider, <<"Cached"/utf8>>, fun() -> get(Cache) end}. -file("src/aws/internal/credentials_cache.gleam", 133). ?DOC( " Tell the cache actor to exit. Fire-and-forget. See\n" " `aws/internal/actor_lifecycle.shutdown_via_stop` for the contract;\n" " idempotent because Erlang silently drops sends to a dead Pid.\n" ). -spec shutdown(cache()) -> nil. shutdown(Cache) -> aws@internal@actor_lifecycle:shutdown_via_stop( erlang:element(2, Cache), stop ). -file("src/aws/internal/credentials_cache.gleam", 141). ?DOC( " Synchronous teardown — monitors the actor, sends `Stop`, waits for\n" " `DOWN`. `Ok(Nil)` on clean exit, `Error(Nil)` only on real timeout.\n" " Already-dead actors short-circuit to `Ok(Nil)` via\n" " `subject_owner` returning `Error`.\n" ). -spec shutdown_sync(cache(), integer()) -> {ok, nil} | {error, nil}. shutdown_sync(Cache, Timeout_ms) -> aws@internal@actor_lifecycle:shutdown_via_stop_sync( erlang:element(2, Cache), stop, Timeout_ms ).