Recommended durable replay store over AshOnetime.Transaction.nonce/2.
Requires optional ash_onetime 1.5 and Elixir 1.20. The caller owns the existing Ecto repo, PostgreSQL database, and ash_onetime installation migrations. Namespace and commitment bytes are base64url encoded into scope and key; they never become SQL identifiers or logical partitions.
Each claim owns a READ COMMITTED transaction under the remaining deadline.
This transaction commits independently of the verifier's application effect.
To spend a nonce together with an effect, verify with replay explicitly
:not_required, then use ash_onetime in the application's transaction.
No timeout or failure permits automatic retry: the claim may have committed.
Facts use PostgreSQL's transaction clock, read in the same transaction as
admission. Max age is derived from retain_until; one second of rounding
headroom and ash_onetime's cleanup margin keep retention beyond that horizon
without relying on application/database clock synchronization. ash_onetime's
freshness check can still reject clock skew; rejection never spends a nonce.
Past or current retention horizons still block an immediate duplicate.
Historical verification clocks do not move the database's cleanup clock.
Duplicate claims do not
extend retention; use one freshness bound per namespace.
sweep/3 never deletes anything. Cleanup belongs to
mix ash_onetime.prune or AshOnetime.Oban.CleanupWorker; this adapter never
calls ash_onetime's internal store APIs or starts any process.
Summary
Functions
Atomically claim an envelope in an independently committed transaction.
Reference an existing repo with an explicit logical partition and optional schema prefix.
Refuse to sweep: cleanup is externally managed by ash_onetime's prune task or Oban worker.
Types
Functions
@spec claim(ref(), RequestSeal.Replay.Claim.t(), RequestSeal.Custody.Context.t()) :: :claimed | :already_claimed | {:error, :unavailable | :timeout | :failure}
Atomically claim an envelope in an independently committed transaction.
Implements RequestSeal.Replay.claim/3; most callers use
RequestSeal.Replay.claim/3 with the store from store/2. The transaction and
its queries use the remaining time of context as their timeout.
Parameters
ref-{repo, partition, prefix}, the reference built bystore/2.claim- aRequestSeal.Replay.Claim.context- aRequestSeal.Custody.Context.
Returns
:claimed- ash_onetime admitted the nonce.:already_claimed- ash_onetime reported:nonce_already_used.{:error, :timeout}- the deadline had expired before the transaction; a lock or cancellation; and, from 0.5.0, a pool queue timeout (DBConnection:queue_timeout) or any failure observed after the deadline, such as a transaction rolled back when the pool closed the connection at the client timeout. The claim may have committed.{:error, :unavailable}- the repo is not running, a connection could not be checked out, or the database is shutting down or refusing connections.{:error, :failure}- an invalid claim or reference, any other ash_onetime error code, or any other failure.
Examples
iex> context = %RequestSeal.Custody.Context{
...> owner: self(),
...> deadline: System.monotonic_time(:millisecond) + 1_000
...> }
iex> claim = %RequestSeal.Replay.Claim{namespace: "orders", key: "", retain_until: 100}
iex> RequestSeal.Replay.AshOnetime.claim({Example.Repo, "webhooks", nil}, claim, context)
{:error, :failure}
@spec store(module(), keyword()) :: RequestSeal.Replay.Store.t() | {:error, RequestSeal.Replay.Error.t()}
Reference an existing repo with an explicit logical partition and optional schema prefix.
Building the reference does not contact the database; the repo module must be
loaded and export transaction/2.
Parameters
repo- the caller's Ecto repo module.opts- keyword list without duplicate keys; see Options.
Options
:partition- required: the ash_onetime logical partition, a valid UTF-8 string of 1–255 bytes without NUL bytes.:prefix- optional schema prefix, a valid UTF-8 string of 1–63 bytes without NUL bytes. Defaultnil(the repo's default schema).
Returns
RequestSeal.Replay.Store- adapterRequestSeal.Replay.AshOnetimewith{repo, partition, prefix}as its reference.{:error, %RequestSeal.Replay.Error{}}- reason:invalid_storefor an unknown, duplicate or malformed option, or a repo module that is not loaded or lackstransaction/2. Meanings:RequestSeal.Replay.Error.
Examples
iex> RequestSeal.Replay.AshOnetime.store(Example.Repo, partition: "")
{:error, %RequestSeal.Replay.Error{reason: :invalid_store, retryable: false}}Fragment (requires a running Ecto repo with the ash_onetime migrations; not executed here):
store = RequestSeal.Replay.AshOnetime.store(Example.Repo, partition: "webhooks")
claim = %RequestSeal.Replay.Claim{namespace: "orders", key: "nonce-7f3a", retain_until: 1_900_000_000}
:claimed = RequestSeal.Replay.claim(store, claim, timeout: 2_000)
:already_claimed = RequestSeal.Replay.claim(store, claim, timeout: 2_000)
@spec sweep(ref(), integer(), RequestSeal.Custody.Context.t()) :: {:error, :externally_managed}
Refuse to sweep: cleanup is externally managed by ash_onetime's prune task or Oban worker.
Implements RequestSeal.Replay.sweep/3 without touching the database.
Parameters
ref- ignored.now- ignored.context- ignored.
Returns
{:error, :externally_managed}- always.
Examples
iex> context = %RequestSeal.Custody.Context{
...> owner: self(),
...> deadline: System.monotonic_time(:millisecond) + 1_000
...> }
iex> RequestSeal.Replay.AshOnetime.sweep({Example.Repo, "webhooks", nil}, 1_900_000_000, context)
{:error, :externally_managed}