# `RequestSeal.Replay`
[🔗](https://github.com/baselabs/request_seal/blob/v0.5.2/lib/request_seal/replay.ex#L1)

Atomic whole-envelope replay port with caller-owned stores and commitments.

Adapters implement `c:claim/3` and `c: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.

# `claim`

```elixir
@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 from `RequestSeal.Replay.Store`'s `ref`.
  * `claim` - a `RequestSeal.Replay.Claim` with its namespace, key and exclusive
    retention end in Unix seconds.
  * `context` - a `RequestSeal.Custody.Context` carrying 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`.

# `sweep`

```elixir
@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 from `RequestSeal.Replay.Store`'s `ref`.
  * `now` - Unix seconds from 0 through 253,402,300,799, from a clock the caller
    trusts and reconciles with verification clocks.
  * `context` - a `RequestSeal.Custody.Context` carrying the absolute deadline.

## Returns

  * `{:ok, removed}` - the number of entries deleted.
  * `{:error, reason}` - an adapter-specific atom; the bundled adapters return
    `:failure`, `:timeout` or `:unavailable`, and `RequestSeal.Replay.AshOnetime`
    always returns `:externally_managed`.

# `claim`

```elixir
@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` - a `RequestSeal.Replay.Store` whose adapter module is loaded and
    exports `claim/3` and `sweep/3`, for example from
    `RequestSeal.Replay.ETS.store/1`.
  * `claim` - a `RequestSeal.Replay.Claim` accepted by
    `RequestSeal.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`. `retryable` is 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

---

*Consult [api-reference.md](api-reference.md) for complete listing*
