idempotent 0.3.0

Type-safe idempotency keys for building reliable, retry-safe APIs
Documentation
[![Build Status][badge-actions]][url-actions]
[![Crates.io][badge-crate]][url-crate]
[![Documentation][badge-docs]][url-docs]
[![MPL-2.0 license][badge-license]][url-license]

# idempotent

Idempotency library with at-most-once execution and response caching.

## Highlights

- **Typestate entries:** `Processing``Completed` is checked at compile time, so you can't forget to complete an entry or complete one twice
- **Pluggable stores:** comes with an in-memory store and a Valkey/Redis store; implement [`IdempotencyStore`][url-docs-store] for your own backend
- **Fencing tokens:** rejects stale completions when a key expires and gets reclaimed while the original request is still running
- **Fingerprint matching:** returns a conflict when a retry carries a different request body than the original; ships with an xxHash-based default, implement `FingerprintStrategy` for your own
- **UUID keys by default:** `IdempotencyKey::default()` generates a random UUID v4

## Usage

Add to your `Cargo.toml`

```toml
[dependencies]
idempotent = { version = "0.3.0", features = ["memory"] }
```

### Quick example

```rust
use idempotent::{
    IdempotencyKey, IdempotencyEntry, IdempotencyStore,
    InsertResult, CachedResponse,
};
use idempotent::fingerprint::{DefaultFingerprintStrategy, FingerprintStrategy};
use idempotent::memory::MemoryStore;

let store = MemoryStore::new();
let key = IdempotencyKey::default();

let fingerprint = DefaultFingerprintStrategy.compute("POST /credentials/issue", b"VerifiableCredential");
let entry = IdempotencyEntry::new(fingerprint);

match store.try_insert(&key, entry).await? {
    InsertResult::Claimed { fencing_token, entry } => {
        // Execute your side effect (e.g. issue a verifiable credential)
        let response = issue_credential().await;

        let completed = entry.complete(CachedResponse::from(response));
        store.complete(&key, completed, fencing_token).await?;
    }
    InsertResult::Exists(existing) => {
        // Return the cached response or signal a conflict
    }
}
```

## Optional features

- **memory:** enables the in-memory store, suitable for development or single-node deployment
- **valkey:** enables the Valkey/Redis store, using Lua scripts for atomic operations
- **tracing:** instruments store operations with [`tracing`][url-tracing] spans and events
- **serde:** derives [`Serialize`][url-serde-serialize] and [`Deserialize`][url-serde-deserialize] on `IdempotencyKey`, `IdempotencyEntry`, `Fingerprint`, and `FencingToken`

## Supported Rust versions

The minimum supported Rust version is **1.94.0**.

## License

Unless otherwise noted, this project is licensed under the [Mozilla Public License Version 2.0][url-license].

[badge-actions]: https://github.com/aklanti/idempotent/workflows/CI/badge.svg
[url-actions]: https://github.com/aklanti/idempotent/actions/workflows/main.yaml
[badge-crate]: https://img.shields.io/crates/v/idempotent
[url-crate]: https://crates.io/crates/idempotent
[badge-docs]: https://img.shields.io/docsrs/idempotent/latest
[url-docs]: https://docs.rs/idempotent/latest/idempotent
[url-docs-store]: https://docs.rs/idempotent/latest/idempotent/trait.IdempotencyStore.html
[badge-license]: https://img.shields.io/badge/License-MPL_2.0-blue.svg
[url-license]: LICENSE
[url-serde-serialize]: https://docs.rs/serde/1/serde/trait.Serialize.html
[url-serde-deserialize]: https://docs.rs/serde/1/serde/trait.Deserialize.html
[url-tracing]: https://docs.rs/tracing/latest/tracing