ferroid 2.0.0

High-performance ULID and Snowflake-style IDs. Unique, monotonic, and lexicographically sortable IDs optimized for low-latency services and async workloads.
Documentation
use core::{convert::Infallible, future::Future};

use crate::{
    futures::SmolSleep,
    generator::{Result, UlidGenerator},
    id::UlidId,
    rand::RandSource,
    time::TimeSource,
};

/// Extension trait for asynchronously generating ULIDs using the
/// [`smol`](https://docs.rs/smol) async runtime.
///
/// This trait provides convenience methods that use [`SmolSleep`] as the sleep
/// provider, allowing you to call async methods without manually specifying the
/// sleep strategy.
///
/// [`SmolSleep`]: crate::futures::SmolSleep
pub trait UlidGeneratorAsyncSmolExt<ID, T, R>
where
    ID: UlidId,
    T: TimeSource<ID::Ty>,
    R: RandSource<ID::Ty>,
{
    type Err;

    /// Returns a future that resolves to the next available ULID.
    ///
    /// This infallible method uses [`SmolSleep`] and is only available for
    /// generators with infallible error types.
    ///
    /// [`SmolSleep`]: crate::futures::SmolSleep
    fn next_id_async(&self) -> impl Future<Output = ID>
    where
        Self::Err: Into<Infallible>;

    /// Returns a future that resolves to the next available ULID using
    /// [`SmolSleep`].
    ///
    /// # Errors
    ///
    /// Returns an error if the underlying generator fails.
    ///
    /// [`SmolSleep`]: crate::futures::SmolSleep
    fn try_next_id_async(&self) -> impl Future<Output = Result<ID, Self::Err>>;
}

impl<G, ID, T, R> UlidGeneratorAsyncSmolExt<ID, T, R> for G
where
    G: UlidGenerator<ID, T, R> + Sync,
    ID: UlidId + Send,
    T: TimeSource<ID::Ty> + Send,
    R: RandSource<ID::Ty> + Send,
{
    type Err = G::Err;

    fn next_id_async(&self) -> impl Future<Output = ID>
    where
        Self::Err: Into<Infallible>,
    {
        <Self as crate::futures::UlidGeneratorAsyncExt<ID, T, R>>::next_id_async::<SmolSleep>(self)
    }

    fn try_next_id_async(&self) -> impl Future<Output = Result<ID, Self::Err>> {
        <Self as crate::futures::UlidGeneratorAsyncExt<ID, T, R>>::try_next_id_async::<SmolSleep>(
            self,
        )
    }
}

#[cfg(test)]
mod tests {
    use std::{collections::HashSet, vec::Vec};

    use futures::future::try_join_all;
    use smol::Task;

    use super::*;
    use crate::{
        futures::{SleepProvider, SmolYield},
        generator::{BasicUlidGenerator, LockMonoUlidGenerator, Result},
        id::ULID,
        rand::{RandSource, ThreadRandom},
        time::{MonotonicClock, TimeSource},
    };

    const TOTAL_IDS: usize = 4096;
    const NUM_GENERATORS: u64 = 8;
    const IDS_PER_GENERATOR: usize = TOTAL_IDS * 8;

    #[test]
    fn basic_can_call_next_id_async() {
        smol::block_on(async {
            let generator =
                BasicUlidGenerator::new(MonotonicClock::default(), ThreadRandom::default());
            let id = generator.next_id_async().await;
            assert!(matches!(id, ULID { .. }));

            let id = UlidGeneratorAsyncSmolExt::next_id_async(&generator).await;
            assert!(matches!(id, ULID { .. }));
        })
    }

    #[cfg(target_has_atomic = "128")]
    #[test]
    fn atomic_can_call_next_id_async() {
        use crate::generator::AtomicMonoUlidGenerator;

        smol::block_on(async {
            let generator =
                AtomicMonoUlidGenerator::new(MonotonicClock::default(), ThreadRandom::default());
            let id = generator.next_id_async().await;
            assert!(matches!(id, ULID { .. }));

            let id = UlidGeneratorAsyncSmolExt::next_id_async(&generator).await;
            assert!(matches!(id, ULID { .. }));
        })
    }

    #[cfg(feature = "parking-lot")]
    #[test]
    fn lock_can_call_next_id_async() {
        smol::block_on(async {
            let generator =
                LockMonoUlidGenerator::new(MonotonicClock::default(), ThreadRandom::default());
            let id = generator.next_id_async().await;
            assert!(matches!(id, ULID { .. }));

            let id = UlidGeneratorAsyncSmolExt::next_id_async(&generator).await;
            assert!(matches!(id, ULID { .. }));
        })
    }

    // Test the explicit SleepProvider approach
    #[test]
    fn generates_many_unique_ids_basic_smol_sleep() {
        smol::block_on(async {
            test_many_ulid_unique_ids_explicit::<ULID, _, _, _, SmolSleep>(
                LockMonoUlidGenerator::new,
                MonotonicClock::default,
                ThreadRandom::default,
            )
            .await
            .unwrap();
        });
    }
    #[test]
    fn generates_many_unique_ids_basic_smol_yield() {
        smol::block_on(async {
            test_many_ulid_unique_ids_explicit::<ULID, _, _, _, SmolYield>(
                LockMonoUlidGenerator::new,
                MonotonicClock::default,
                ThreadRandom::default,
            )
            .await
            .unwrap();
        });
    }
    #[test]
    fn generates_many_unique_ids_basic_smol_convenience() {
        smol::block_on(async {
            test_many_ulid_unique_ids_convenience::<ULID, _, _, _>(
                LockMonoUlidGenerator::new,
                MonotonicClock::default,
                ThreadRandom::default,
            )
            .await
            .unwrap();
        });
    }

    // Helper function for explicit SleepProvider testing
    async fn test_many_ulid_unique_ids_explicit<ID, G, T, R, S>(
        generator_fn: impl Fn(T, R) -> G,
        clock_fn: impl Fn() -> T,
        rand_fn: impl Fn() -> R,
    ) -> Result<()>
    where
        G: UlidGenerator<ID, T, R> + Send + Sync + 'static,
        ID: UlidId + Send + 'static,
        T: TimeSource<ID::Ty> + Clone + Send,
        R: RandSource<ID::Ty> + Clone + Send,
        S: SleepProvider,
    {
        let clock = clock_fn();
        let rand = rand_fn();
        let generators: Vec<_> = (0..NUM_GENERATORS)
            .map(|_| generator_fn(clock.clone(), rand.clone()))
            .collect();

        // Test explicit SleepProvider syntax
        let tasks: Vec<Task<Result<Vec<ID>>>> = generators
            .into_iter()
            .map(|g| {
                smol::spawn(async move {
                    let mut ids = Vec::with_capacity(IDS_PER_GENERATOR);
                    for _ in 0..IDS_PER_GENERATOR {
                        let id = crate::futures::UlidGeneratorAsyncExt::try_next_id_async::<S>(&g)
                            .await
                            .unwrap();
                        ids.push(id);
                    }
                    Ok(ids)
                })
            })
            .collect();

        validate_unique_ulid_ids(tasks).await
    }

    // Helper function for convenience extension trait testing
    async fn test_many_ulid_unique_ids_convenience<ID, G, T, R>(
        generator_fn: impl Fn(T, R) -> G,
        clock_fn: impl Fn() -> T,
        rand_fn: impl Fn() -> R,
    ) -> Result<()>
    where
        G: UlidGenerator<ID, T, R> + Send + Sync + 'static,
        G::Err: Send + Sync + 'static,
        ID: UlidId + Send + 'static,
        T: TimeSource<ID::Ty> + Clone + Send,
        R: RandSource<ID::Ty> + Clone + Send,
    {
        let clock = clock_fn();
        let rand = rand_fn();
        let generators: Vec<_> = (0..NUM_GENERATORS)
            .map(|_| generator_fn(clock.clone(), rand.clone()))
            .collect();

        // Test convenience extension trait syntax (uses SmolSleep by default)
        let tasks: Vec<Task<Result<Vec<ID>>>> = generators
            .into_iter()
            .map(|g| {
                smol::spawn(async move {
                    let mut ids = Vec::with_capacity(IDS_PER_GENERATOR);
                    for _ in 0..IDS_PER_GENERATOR {
                        // This uses the convenience method - no explicit
                        // SleepProvider type!
                        let id = g.try_next_id_async().await.unwrap();
                        ids.push(id);
                    }
                    Ok(ids)
                })
            })
            .collect();

        validate_unique_ulid_ids(tasks).await
    }

    // Helper to validate uniqueness - shared between test approaches
    async fn validate_unique_ulid_ids(tasks: Vec<Task<Result<Vec<impl UlidId>>>>) -> Result<()> {
        let all_ids: Vec<_> = try_join_all(tasks).await?.into_iter().flatten().collect();

        #[allow(clippy::cast_possible_truncation)]
        let expected_total = NUM_GENERATORS as usize * IDS_PER_GENERATOR;
        assert_eq!(
            all_ids.len(),
            expected_total,
            "Expected {} IDs but got {}",
            expected_total,
            all_ids.len()
        );

        let mut seen = HashSet::with_capacity(all_ids.len());
        for id in &all_ids {
            assert!(seen.insert(id), "Duplicate ID found: {id:?}");
        }

        Ok(())
    }
}