#### [Abblix\.Jwt](https://www.abblix.com/en/docs/api/abblix-jwt 'index')
### [Abblix\.Jwt\.ReplayPrevention](https://www.abblix.com/en/docs/api/abblix-jwt/Abblix.Jwt.ReplayPrevention 'Abblix\.Jwt\.ReplayPrevention')

## ReplayCacheBase Class

Everything a replay cache does apart from the one operation only its store can perform\. A
derived class supplies that operation \- reserve this key if it is absent, and say whether it
was \- and inherits the rest: the freshness window turned into a lifetime and floored, the key
namespaced, and the store's answer passed back as the verdict\.

```csharp
public abstract class ReplayCacheBase : Abblix.Jwt.ReplayPrevention.IReplayCache
```

Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') → ReplayCacheBase

Derived  
↳ [DistributedReplayCache](https://www.abblix.com/en/docs/api/abblix-jwt/Abblix.Jwt.ReplayPrevention.DistributedReplayCache 'Abblix\.Jwt\.ReplayPrevention\.DistributedReplayCache')

Implements [IReplayCache](https://www.abblix.com/en/docs/api/abblix-jwt/Abblix.Jwt.ReplayPrevention.IReplayCache 'Abblix\.Jwt\.ReplayPrevention\.IReplayCache')

### Remarks

Every store that can decide a first sighting decides it the same way and they differ only in
spelling - Redis `SET key value NX PX ttl`, PostgreSQL `INSERT ... ON CONFLICT DO
             NOTHING`, DynamoDB a conditional put - so the shape belongs here and the spelling belongs to
a subclass, which costs this assembly no dependency on any of them.

A subclass rather than a delegate because the implementation deserves a NAME. It is what a
container registers, what a stack trace prints, and what an operator reads when asking which
replay cache a deployment actually wired - and "the strict one" and "the probabilistic one"
differ in exactly the way a name is needed to tell apart, since both answer the same contract
and only one of them can be relied on to refuse.

What a subclass must NOT do is as fixed as what it must: no read before the write, no release,
no retry. Whether the reservation is indivisible is the store's promise, and it is the whole of
what distinguishes a strict cache from [DistributedReplayCache](https://www.abblix.com/en/docs/api/abblix-jwt/Abblix.Jwt.ReplayPrevention.DistributedReplayCache 'Abblix\.Jwt\.ReplayPrevention\.DistributedReplayCache'); a subclass that
read first would hand back the very race the shape exists to close.

<b>Redis.</b> One command, and the condition is evaluated by the server inside the write that
             performs it, so no caller can be between the two. Redis expires the key itself, which is the
             whole of the retention question here:

```csharp
public sealed class RedisReplayCache(IConnectionMultiplexer connection, TimeProvider clock, string prefix)
    : ReplayCacheBase(clock, prefix)
{
    private readonly IDatabase _database = connection.GetDatabase()

    protected override Task<bool> ReserveIfAbsentAsync(
        string key, TimeSpan timeToLive, CancellationToken cancellationToken)
        => _database.StringSetAsync(key, 1, timeToLive, When.NotExists)
}
```

The stored value is a presence marker and nothing reads it: presence of the key is the whole
fact. `When.NotExists` is what makes the answer meaningful - with `When.Always` the
call still compiles, still returns a bool, and returns true every time, so every replay reads
as fresh.

<b>PostgreSQL.</b> The primary key does the deciding, and the statement affects one row when it
             inserted and none when it conflicted:

```csharp
INSERT INTO replay_reservations (reservation_key, expires_at)
VALUES (@key, @expiresAt)
ON CONFLICT (reservation_key) DO NOTHING
```

A row count of one is a first sighting, zero is a replay - the same answer Redis gives, from the
uniqueness constraint rather than from a flag. What differs is retention: a table does not
expire anything, so the subclass computes `expires_at` from the lifetime it was handed and
something must remove rows past it. Until they are removed the identifier stays reserved, and
the direction of that error is the reassuring one - a replay cache that remembers too long
refuses a request it could have allowed, while one that forgets too early accepts a replay. So
a cleanup that lags is a size problem, never a security one, and it may be a scheduled delete
or a partition drop rather than anything the reservation path waits for.

<b>How to know an implementation is right.</b> Not by reading it: a read-then-write version
             satisfies the signature, passes every sequential test and fails only under load. Drive it with
             many callers reserving ONE identifier at once, over separate connections - a single pooled
             connection can serialize them and hide the defect - and require the count of true answers to be
             exactly one. That assertion holds under every interleaving when the store decides, and fails as
             soon as any two callers overlap when it does not.
### Methods

## ReplayCacheBase\.TryReserveAsync\(string, DateTimeOffset, CancellationToken\) Method {#Abblix.Jwt.ReplayPrevention.ReplayCacheBase.TryReserveAsync(string,System.DateTimeOffset,System.Threading.CancellationToken)}

Reserves an identifier, answering whether this is its first sighting\.

```csharp
public System.Threading.Tasks.Task<bool> TryReserveAsync(string identifier, System.DateTimeOffset expiresAt, System.Threading.CancellationToken cancellationToken=default(System.Threading.CancellationToken));
```
#### Parameters

###### `identifier` [System\.String](https://learn.microsoft.com/en-us/dotnet/api/system.string 'System\.String') {#Abblix.Jwt.ReplayPrevention.ReplayCacheBase.TryReserveAsync(string,System.DateTimeOffset,System.Threading.CancellationToken).identifier}

What identifies the token\. A profile whose identifier is unique only within a scope
composes that scope into the value it passes \- a SET's "jti" is unique per event feed
\(RFC 8417 Section 2\.2\), so its receiver reserves the issuer and the identifier together\.

###### `expiresAt` [System\.DateTimeOffset](https://learn.microsoft.com/en-us/dotnet/api/system.datetimeoffset 'System\.DateTimeOffset') {#Abblix.Jwt.ReplayPrevention.ReplayCacheBase.TryReserveAsync(string,System.DateTimeOffset,System.Threading.CancellationToken).expiresAt}

When the identifier stops being worth remembering, which is the last moment a replay of
this token could still pass the caller's own freshness checks\. Forgetting earlier would
let that token replay; the implementation is free to remember longer\.

###### `cancellationToken` [System\.Threading\.CancellationToken](https://learn.microsoft.com/en-us/dotnet/api/system.threading.cancellationtoken 'System\.Threading\.CancellationToken') {#Abblix.Jwt.ReplayPrevention.ReplayCacheBase.TryReserveAsync(string,System.DateTimeOffset,System.Threading.CancellationToken).cancellationToken}

Cancels the cache round trip\.

Implements [TryReserveAsync\(string, DateTimeOffset, CancellationToken\)](https://www.abblix.com/en/docs/api/abblix-jwt/Abblix.Jwt.ReplayPrevention.IReplayCache#Abblix.Jwt.ReplayPrevention.IReplayCache.TryReserveAsync(string,System.DateTimeOffset,System.Threading.CancellationToken) 'Abblix\.Jwt\.ReplayPrevention\.IReplayCache\.TryReserveAsync\(string, System\.DateTimeOffset, System\.Threading\.CancellationToken\)')

#### Returns
[System\.Threading\.Tasks\.Task&lt;](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task-1 'System\.Threading\.Tasks\.Task\`1')[System\.Boolean](https://learn.microsoft.com/en-us/dotnet/api/system.boolean 'System\.Boolean')[&gt;](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task-1 'System\.Threading\.Tasks\.Task\`1')  
True when the identifier was newly reserved and the token is therefore fresh; false when
it was already there, which is a replay\.
