Atomic whole-envelope replay port with caller-owned stores and commitments.
Adapters implement claim/3 and sweep/3. Claiming never evicts entries.
Generic and Web Bot Auth verification reject a derived retention end above
253,402,300,799 Unix seconds with RequestSeal.Error reason
:retention_exceeded at :replay before commitment or storage. Such claims
cannot be swept. Direct store callers still receive the existing invalid-claim
failure; RequestSeal.Replay.Claim.valid?/1 rejects them.
Negative retention ends are already expired.
A single monotonic deadline covers the commitment and store operation; caller
death or deadline expiry kills the operation worker. Cancellation cannot undo
an already committed claim. No error permits automatic retry.
Multi-node operators sweep at now - max_internode_skew to preserve claims
while any verifier still accepts the authenticated input.
One namespace should use one freshness bound because duplicates never extend retention.
Summary
Callbacks
Atomically record claim in the store ref, unless its namespace and key are already stored.
Delete stored claims whose exclusive retention end is at or before now.
Functions
Submit one atomic claim to a store under a bounded operation deadline.
Callbacks
@callback claim(term(), RequestSeal.Replay.Claim.t(), RequestSeal.Custody.Context.t()) :: :claimed | :already_claimed | {:error, :unavailable | :timeout | :full | :failure}
Atomically record claim in the store ref, unless its namespace and key are already stored.
RequestSeal.Replay.claim/3 validates the claim and builds context before
calling the adapter. An adapter honors context as the operation deadline
and never evicts or refreshes an existing entry while claiming.
Parameters
ref- the adapter's reference, taken fromRequestSeal.Replay.Store'sref.claim- aRequestSeal.Replay.Claimwith its namespace, key and exclusive retention end in Unix seconds.context- aRequestSeal.Custody.Contextcarrying the absolute deadline.
Returns
:claimed- the entry was stored by this call.:already_claimed- an entry with the same namespace and key is stored.{:error, :unavailable}- the store could not be reached.{:error, :timeout}- the deadline expired; the claim may have been committed.{:error, :full}- the store is at capacity.{:error, :failure}- any other failure, including an invalid claim or reference.
RequestSeal.Replay.claim/3 maps these to RequestSeal.Replay.Error reasons
:store_unavailable, :store_timeout, :store_full and :store_failed;
any other adapter result also becomes :store_failed.
@callback sweep(term(), 0..253_402_300_799, RequestSeal.Custody.Context.t()) :: {:ok, non_neg_integer()} | {:error, atom()}
Delete stored claims whose exclusive retention end is at or before now.
Parameters
ref- the adapter's reference, taken fromRequestSeal.Replay.Store'sref.now- Unix seconds from 0 through 253,402,300,799, from a clock the caller trusts and reconciles with verification clocks.context- aRequestSeal.Custody.Contextcarrying the absolute deadline.
Returns
{:ok, removed}- the number of entries deleted.{:error, reason}- an adapter-specific atom; the bundled adapters return:failure,:timeoutor:unavailable, andRequestSeal.Replay.AshOnetimealways returns:externally_managed.
Functions
@spec claim(RequestSeal.Replay.Store.t(), RequestSeal.Replay.Claim.t(), keyword()) :: :claimed | :already_claimed | {:error, RequestSeal.Replay.Error.t()}
Submit one atomic claim to a store under a bounded operation deadline.
The claim runs in a monitored worker. Caller death or deadline expiry kills the worker; a store that already committed the claim keeps it.
Parameters
store- aRequestSeal.Replay.Storewhose adapter module is loaded and exportsclaim/3andsweep/3, for example fromRequestSeal.Replay.ETS.store/1.claim- aRequestSeal.Replay.Claimaccepted byRequestSeal.Replay.Claim.valid?/1.opts- keyword list; see Options.
Options
:timeout- required, and the only accepted option: operation deadline in milliseconds, 1 through 300,000.
Returns
:claimed- this call stored the claim.:already_claimed- the store already holds the namespace and key.{:error, %RequestSeal.Replay.Error{}}- reasons this function can return::invalid_options,:invalid_store,:invalid_claim,:store_unavailable,:store_timeout,:store_full,:store_failed.retryableis always false. Meanings:RequestSeal.Replay.Error.
Examples
iex> {:ok, pid} = RequestSeal.Replay.ETS.start_link(max_entries: 100)
iex> store = RequestSeal.Replay.ETS.store(pid)
iex> claim = %RequestSeal.Replay.Claim{
...> namespace: "orders",
...> key: "nonce-7f3a",
...> retain_until: 1_900_000_000
...> }
iex> RequestSeal.Replay.claim(store, claim, timeout: 1_000)
:claimed
iex> RequestSeal.Replay.claim(store, claim, timeout: 1_000)
:already_claimed
iex> RequestSeal.Replay.claim(store, %{claim | namespace: "telemetry"}, timeout: 1_000)
:claimed
iex> RequestSeal.Replay.claim(store, claim, timeout: 0)
{:error, %RequestSeal.Replay.Error{reason: :invalid_options, retryable: false}}
iex> RequestSeal.Replay.claim(store, %{claim | key: ""}, timeout: 1_000)
{:error, %RequestSeal.Replay.Error{reason: :invalid_claim, retryable: false}}
iex> GenServer.stop(pid)
:ok