ferroid 0.2.0

A flexible ID generator for producing unique, monotonic, and lexicographically sortable Snowflake-style IDs.
Documentation
# ferroid

[`ferroid`](https://github.com/s0l0ist/ferroid) is a Rust crate for generating
and parsing **Snowflake-style unique IDs**.

It supports pre-built layouts for platforms like Twitter, Discord, Instagram,
and Mastodon. These IDs are 64-bit integers that encode timestamps,
machine/shard IDs, and sequence numbers - making them **lexicographically
sortable**, **scalable**, and ideal for **distributed systems**.

Features:

- ๐Ÿ“Œ Bit-level layout compatibility with major Snowflake formats
- ๐Ÿงฉ Pluggable time sources via the `TimeSource` trait
- ๐Ÿงต Lock-based and lock-free thread-safe ID generation
- ๐Ÿ“ Customizable layouts via the `Snowflake` trait
- ๐Ÿ”ข Lexicographically sortable string encoding

## ๐Ÿ“ฆ Supported Layouts

| Platform  | Timestamp Bits | Machine ID Bits | Sequence Bits | Epoch                   |
| --------- | -------------- | --------------- | ------------- | ----------------------- |
| Twitter   | 41             | 10              | 12            | 2010-11-04 01:42:54.657 |
| Discord   | 42             | 10              | 12            | 2015-01-01 00:00:00.000 |
| Instagram | 41             | 13              | 10            | 2011-01-01 00:00:00.000 |
| Mastodon  | 48             | 0               | 16            | 1970-01-01 00:00:00.000 |

## ๐Ÿ”ง Generator Comparison

| Generator                  | Thread-Safe | Lock-Free | Throughput | Use Case                                                                       |
| -------------------------- | ----------- | --------- | ---------- | ------------------------------------------------------------------------------ |
| `BasicSnowflakeGenerator`  | โŒ          | โŒ        | Highest    | Single-threaded, zero contention; ideal for sharded/core-local generators      |
| `LockSnowflakeGenerator`   | โœ…          | โŒ        | Medium     | Multi-threaded workloads where fair access across threads is important         |
| `AtomicSnowflakeGenerator` | โœ…          | โœ…        | High       | Multi-threaded workloads where fair access is sacrificed for higher throughput |

All generators produce **monotonically increasing**, **time-ordered**, and
**unique** IDs.

## ๐Ÿš€ Usage

### Generate an ID

#### Synchronous

Calling `next_id()` may yield `Pending` if the current sequence is exhausted. In
that case, you can spin, yield, or sleep depending on your environment:

```rust
use ferroid::{MonotonicClock, TWITTER_EPOCH, BasicSnowflakeGenerator, SnowflakeTwitterId, IdGenStatus};

let clock = MonotonicClock::with_epoch(TWITTER_EPOCH);
let generator = BasicSnowflakeGenerator::new(0, clock);

let id: SnowflakeTwitterId = loop {
    match generator.next_id() {
        IdGenStatus::Ready { id } => break id,
        IdGenStatus::Pending { yield_for } => {
            println!("Exhausted; wait for: {}ms", yield_for);
            std::hint::spin_loop();
            // Use `std::hint::spin_loop()` for single-threaded or per-thread generators.
            // Use `std::thread::yield_now()` when sharing a generator across multiple threads.
            // Use `std::thread::sleep(Duration::from_millis(yield_for.to_u64().unwrap())` to sleep.
        }
    }
};

println!("Generated ID: {}", id);
```

#### Asynchronous

If you're in an async context (e.g., using [Tokio](https://tokio.rs/)), you can
use the `async-tokio` feature and import the `SnowflakeGeneratorAsyncExt` trait
to await a new ID:

```rust
use ferroid::{
    MonotonicClock, Result, MASTODON_EPOCH, AtomicSnowflakeGenerator, SnowflakeMastodonId,
    SnowflakeGeneratorAsyncExt, TokioSleep,
};

#[tokio::main]
async fn main() -> Result<()> {
    let clock = MonotonicClock::with_epoch(MASTODON_EPOCH);
    let generator =  AtomicSnowflakeGenerator::<SnowflakeMastodonId, _>::new(0, clock);

    // Generate a non-blocking ID that sleeps if the generator isn't ready.
    let id = generator.try_next_id_async::<TokioSleep>().await?;
    println!("Generated ID: {}", id);

    Ok(())
}
```

### Custom Layouts

To define a custom Snowflake layout, implement `Snowflake`:

```rust
use core::fmt;
use ferroid::Snowflake;

#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
struct MyCustomId {
    id: u64,
}

impl Snowflake for MyCustomId {
    // ...
}

impl fmt::Display for MyCustomId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.id)
    }
}
```

### Behavior

- If the clock **advances**: reset sequence to 0 โ†’ `IdGenStatus::Ready`
- If the clock is **unchanged**: increment sequence โ†’ `IdGenStatus::Ready`
- If the clock **goes backward**: return `IdGenStatus::Pending`
- If the sequence **overflows**: return `IdGenStatus::Pending`

### Serialize as padded string

Use `.to_padded_string()` or `.encode()` (enabled with `base32` feature) for
sortable representations:

```rust
use ferroid::{SnowflakeTwitterId, SnowflakeBase32Ext};

let id = SnowflakeTwitterId::from(123456, 1, 42);
println!("default: {id}");
// > default: 517811998762

println!("padded: {}", id.to_padded_string());
// > padded: 00000000517811998762

let encoded = id.encode();
println!("base32: {encoded}");
// > base32: 00000Y4G0082M

let decoded = SnowflakeTwitterId::decode(&encoded).expect("decode should succeed");
assert_eq!(id, decoded);
```

## ๐Ÿ“ˆ Benchmarks

`ferroid` ships with Criterion benchmarks to measure ID generation performance:

- `BasicSnowflakeGenerator`: single-threaded generator
- `LockSnowflakeGenerator`: mutex-based, thread-safe generator
- `AtomicSnowflakeGenerator`: lock-free, thread-safe generator

Benchmark scenarios include:

- Single-threaded with/without a real clock
- Multi-threaded with/without a real clock

**NOTE**: Shared generators (like `LockSnowflakeGenerator` and
`AtomicSnowflakeGenerator`) can slow down under high thread contention. This
happens because threads must coordinate access - either through mutex locks or
atomic compare-and-swap (CAS) loops - which introduces overhead.

For maximum throughput, **avoid sharing**. Instead, give each thread its own
generator instance. This eliminates contention and allows every thread to issue
IDs independently at full speed.

The thread-safe generators are primarily for convenience, or for use cases where
ID generation is not expected to be the performance bottleneck.

To run:

```sh
cargo criterion --all-features
```

## ๐Ÿงช Testing

Run all tests with:

```sh
cargo test --all-features
```

## ๐Ÿ“„ License

Licensed under either of:

- [Apache License, Version 2.0]https://www.apache.org/licenses/LICENSE-2.0
  ([LICENSE-APACHE]LICENSE-APACHE)
- [MIT License]https://opensource.org/licenses/MIT
  ([LICENSE-MIT]LICENSE-MIT)

at your option.

Unless you explicitly state otherwise, any contribution intentionally submitted
for inclusion in the work by you, as defined in the Apache-2.0 license, shall be
dual licensed as above, without any additional terms or conditions.