armdb 0.1.12

sharded bitcask key-value storage optimized for NVMe
Documentation
use crate::Key;

/// Trait for receiving write notifications from tree/map operations.
///
/// Used for maintaining secondary indexes, audit logs, or any side-effect
/// that must observe every mutation synchronously.
///
/// # Zero-overhead default
///
/// The default implementation [`NoHook`] has `NEEDS_OLD_VALUE = false` and
/// an empty `on_write`. The compiler eliminates all hook-related branches
/// and calls when `H = NoHook`.
pub trait WriteHook<K: Key>: Send + Sync {
    /// Only affects VarTree / VarMap: when `false`, skips disk I/O for the old
    /// value — `old` is `None` in `on_write`. Const/Typed/Zero collections
    /// always provide the old value (it's in memory, zero cost).
    const NEEDS_OLD_VALUE: bool = true;

    /// When `true`, [`Self::on_init`] is called for every live entry during
    /// collection open (after recovery and migration). When `false` (default),
    /// the init iteration is compiled out entirely.
    const NEEDS_INIT: bool = false;

    /// Called after a successful write operation.
    ///
    /// - `key` — the affected key
    /// - `old` — previous value (`Some` on update/delete when `NEEDS_OLD_VALUE`, else `None`)
    /// - `new` — new value (`Some` on put/insert/update, `None` on delete)
    fn on_write(&self, key: &K, old: Option<&[u8]>, new: Option<&[u8]>);

    /// Called once per live entry during collection open.
    /// Fires after recovery and migration are complete, with the final value.
    fn on_init(&self, _key: &K, _value: &[u8]) {}
}

/// Default no-op hook. All branches are eliminated at compile time.
pub struct NoHook;

impl<K: Key> WriteHook<K> for NoHook {
    const NEEDS_OLD_VALUE: bool = false;
    const NEEDS_INIT: bool = false;

    #[inline(always)]
    fn on_write(&self, _key: &K, _old: Option<&[u8]>, _new: Option<&[u8]>) {}

    #[inline(always)]
    fn on_init(&self, _key: &K, _value: &[u8]) {}
}

/// Typed write hook for [`TypedTree`](crate::TypedTree).
///
/// Receives `&T` directly instead of raw bytes — no encode/decode needed.
/// Called before the atomic swap so both old and new values are accessible.
#[cfg(feature = "typed-tree")]
pub trait TypedWriteHook<K: Key, T>: Send + Sync {
    /// Unused for TypedTree/TypedMap/ZeroTree/ZeroMap — old value is always
    /// provided (it lives in memory). Kept for trait uniformity with `WriteHook`.
    const NEEDS_OLD_VALUE: bool = true;
    const NEEDS_INIT: bool = false;

    fn on_write(&self, key: &K, old: Option<&T>, new: Option<&T>);
    fn on_init(&self, _key: &K, _value: &T) {}
}

#[cfg(feature = "typed-tree")]
impl<K: Key, T> TypedWriteHook<K, T> for NoHook {
    const NEEDS_OLD_VALUE: bool = false;
    const NEEDS_INIT: bool = false;

    #[inline(always)]
    fn on_write(&self, _key: &K, _old: Option<&T>, _new: Option<&T>) {}

    #[inline(always)]
    fn on_init(&self, _key: &K, _value: &T) {}
}

/// Adapter: wraps a [`TypedWriteHook<K, T>`] and implements [`WriteHook<K>`]
/// by converting raw bytes to `&T` via zerocopy. Used by ZeroTree/ZeroMap
/// so their users get typed hook callbacks.
pub struct ZeroHookAdapter<K, T, H> {
    pub(crate) inner: H,
    pub(crate) _marker: std::marker::PhantomData<fn() -> (K, T)>,
}

// SAFETY: The adapter delegates to H which is Send+Sync (via TypedWriteHook bound).
// PhantomData<fn() -> (K, T)> is always Send+Sync.
unsafe impl<K, T, H: Send> Send for ZeroHookAdapter<K, T, H> {}
unsafe impl<K, T, H: Sync> Sync for ZeroHookAdapter<K, T, H> {}

impl<K: Key, T: Copy, H: TypedWriteHook<K, T>> WriteHook<K> for ZeroHookAdapter<K, T, H> {
    const NEEDS_OLD_VALUE: bool = H::NEEDS_OLD_VALUE;
    const NEEDS_INIT: bool = H::NEEDS_INIT;

    #[inline]
    fn on_write(&self, key: &K, old: Option<&[u8]>, new: Option<&[u8]>) {
        let read = |b: &[u8]| -> T {
            debug_assert_eq!(b.len(), size_of::<T>());
            unsafe { std::ptr::read(b.as_ptr().cast()) }
        };
        let old_val = old.map(read);
        let new_val = new.map(read);
        self.inner.on_write(key, old_val.as_ref(), new_val.as_ref());
    }

    #[inline]
    fn on_init(&self, key: &K, value: &[u8]) {
        debug_assert_eq!(value.len(), size_of::<T>());
        let val: T = unsafe { std::ptr::read(value.as_ptr().cast()) };
        self.inner.on_init(key, &val);
    }
}