Skip to main content

armdb/
hook.rs

1use crate::Key;
2
3/// Trait for receiving write notifications from tree/map operations.
4///
5/// Used for maintaining secondary indexes, audit logs, or any side-effect
6/// that must observe every mutation synchronously.
7///
8/// # Zero-overhead default
9///
10/// The default implementation [`NoHook`] has `NEEDS_OLD_VALUE = false` and
11/// an empty `on_write`. The compiler eliminates all hook-related branches
12/// and calls when `H = NoHook`.
13pub trait WriteHook<K: Key>: Send + Sync {
14    /// Only affects VarTree / VarMap: when `false`, skips disk I/O for the old
15    /// value — `old` is `None` in `on_write`. Const/Typed/Zero collections
16    /// always provide the old value (it's in memory, zero cost).
17    const NEEDS_OLD_VALUE: bool = true;
18
19    /// When `true`, [`Self::on_init`] is called for every live entry during
20    /// collection open (after recovery and migration). When `false` (default),
21    /// the init iteration is compiled out entirely.
22    const NEEDS_INIT: bool = false;
23
24    /// Called after a successful write operation.
25    ///
26    /// - `key` — the affected key
27    /// - `old` — previous value (`Some` on update/delete when `NEEDS_OLD_VALUE`, else `None`)
28    /// - `new` — new value (`Some` on put/insert/update, `None` on delete)
29    fn on_write(&self, key: &K, old: Option<&[u8]>, new: Option<&[u8]>);
30
31    /// Called once per live entry during collection open.
32    /// Fires after recovery and migration are complete, with the final value.
33    fn on_init(&self, _key: &K, _value: &[u8]) {}
34}
35
36/// Default no-op hook. All branches are eliminated at compile time.
37pub struct NoHook;
38
39impl<K: Key> WriteHook<K> for NoHook {
40    const NEEDS_OLD_VALUE: bool = false;
41    const NEEDS_INIT: bool = false;
42
43    #[inline(always)]
44    fn on_write(&self, _key: &K, _old: Option<&[u8]>, _new: Option<&[u8]>) {}
45
46    #[inline(always)]
47    fn on_init(&self, _key: &K, _value: &[u8]) {}
48}
49
50/// Typed write hook for [`TypedTree`](crate::TypedTree).
51///
52/// Receives `&T` directly instead of raw bytes — no encode/decode needed.
53/// Called before the atomic swap so both old and new values are accessible.
54#[cfg(feature = "typed-tree")]
55pub trait TypedWriteHook<K: Key, T>: Send + Sync {
56    /// Unused for TypedTree/TypedMap/ZeroTree/ZeroMap — old value is always
57    /// provided (it lives in memory). Kept for trait uniformity with `WriteHook`.
58    const NEEDS_OLD_VALUE: bool = true;
59    const NEEDS_INIT: bool = false;
60
61    fn on_write(&self, key: &K, old: Option<&T>, new: Option<&T>);
62    fn on_init(&self, _key: &K, _value: &T) {}
63}
64
65#[cfg(feature = "typed-tree")]
66impl<K: Key, T> TypedWriteHook<K, T> for NoHook {
67    const NEEDS_OLD_VALUE: bool = false;
68    const NEEDS_INIT: bool = false;
69
70    #[inline(always)]
71    fn on_write(&self, _key: &K, _old: Option<&T>, _new: Option<&T>) {}
72
73    #[inline(always)]
74    fn on_init(&self, _key: &K, _value: &T) {}
75}
76
77/// Adapter: wraps a [`TypedWriteHook<K, T>`] and implements [`WriteHook<K>`]
78/// by converting raw bytes to `&T` via zerocopy. Used by ZeroTree/ZeroMap
79/// so their users get typed hook callbacks.
80pub struct ZeroHookAdapter<K, T, H> {
81    pub(crate) inner: H,
82    pub(crate) _marker: std::marker::PhantomData<fn() -> (K, T)>,
83}
84
85// SAFETY: The adapter delegates to H which is Send+Sync (via TypedWriteHook bound).
86// PhantomData<fn() -> (K, T)> is always Send+Sync.
87unsafe impl<K, T, H: Send> Send for ZeroHookAdapter<K, T, H> {}
88unsafe impl<K, T, H: Sync> Sync for ZeroHookAdapter<K, T, H> {}
89
90impl<K: Key, T: Copy, H: TypedWriteHook<K, T>> WriteHook<K> for ZeroHookAdapter<K, T, H> {
91    const NEEDS_OLD_VALUE: bool = H::NEEDS_OLD_VALUE;
92    const NEEDS_INIT: bool = H::NEEDS_INIT;
93
94    #[inline]
95    fn on_write(&self, key: &K, old: Option<&[u8]>, new: Option<&[u8]>) {
96        let read = |b: &[u8]| -> T {
97            debug_assert_eq!(b.len(), size_of::<T>());
98            unsafe { std::ptr::read(b.as_ptr().cast()) }
99        };
100        let old_val = old.map(read);
101        let new_val = new.map(read);
102        self.inner.on_write(key, old_val.as_ref(), new_val.as_ref());
103    }
104
105    #[inline]
106    fn on_init(&self, key: &K, value: &[u8]) {
107        debug_assert_eq!(value.len(), size_of::<T>());
108        let val: T = unsafe { std::ptr::read(value.as_ptr().cast()) };
109        self.inner.on_init(key, &val);
110    }
111}
112
113/// Adapter: wraps a [`TypedWriteHook<K, T>`] and implements [`WriteHook<K>`]
114/// by decoding raw bytes to `T` via a [`Codec`](crate::codec::Codec).
115/// Used by `VarTypedTree` / `VarTypedMap` so their users get typed hook callbacks.
116///
117/// Decode errors are silently dropped — `on_write` / `on_init` pass `None` for
118/// the failed side, matching the permissive error handling of the rest of the
119/// `Var*` API.
120#[cfg(all(feature = "typed-tree", feature = "var-collections"))]
121pub struct VarTypedHookAdapter<K, T, C, H> {
122    pub(crate) inner: H,
123    pub(crate) codec: C,
124    pub(crate) _marker: std::marker::PhantomData<fn() -> (K, T)>,
125}
126
127// SAFETY: delegates to H (Send+Sync via TypedWriteHook) and C (Send+Sync via Codec).
128// PhantomData<fn() -> (K, T)> is always Send+Sync.
129#[cfg(all(feature = "typed-tree", feature = "var-collections"))]
130unsafe impl<K, T, C: Send, H: Send> Send for VarTypedHookAdapter<K, T, C, H> {}
131#[cfg(all(feature = "typed-tree", feature = "var-collections"))]
132unsafe impl<K, T, C: Sync, H: Sync> Sync for VarTypedHookAdapter<K, T, C, H> {}
133
134#[cfg(all(feature = "typed-tree", feature = "var-collections"))]
135impl<K, T, C, H> WriteHook<K> for VarTypedHookAdapter<K, T, C, H>
136where
137    K: Key,
138    T: Send + Sync,
139    C: crate::codec::Codec<T>,
140    H: TypedWriteHook<K, T>,
141{
142    const NEEDS_OLD_VALUE: bool = H::NEEDS_OLD_VALUE;
143    const NEEDS_INIT: bool = H::NEEDS_INIT;
144
145    #[inline]
146    fn on_write(&self, key: &K, old: Option<&[u8]>, new: Option<&[u8]>) {
147        let old_t = old.and_then(|b| self.codec.decode_from(b).ok());
148        let new_t = new.and_then(|b| self.codec.decode_from(b).ok());
149        self.inner.on_write(key, old_t.as_ref(), new_t.as_ref());
150    }
151
152    #[inline]
153    fn on_init(&self, key: &K, value: &[u8]) {
154        if let Ok(val) = self.codec.decode_from(value) {
155            self.inner.on_init(key, &val);
156        }
157    }
158}