Skip to main content

kevy_store/
value_enum.rs

1//! [`Value`], the stored-value enum, and the constants that pick its
2//! string encodings. Split from `value.rs` for the 500-LOC house rule.
3
4use crate::value::{ColdRef, HashData, ListData, SetData, SmallBytes, ZSetData};
5use alloc::boxed::Box;
6use alloc::sync::Arc;
7
8/// A stored value. One variant per Redis type.
9///
10/// The collection variants live behind a **shared pointer** (`Arc`) so the
11/// enum is only as big as `Str` (24 B) + tag = 32 B, not the 56 B `ZSetData`
12/// — every `Entry` (incl. the common string case) is then ~48 B instead of
13/// ~80 B, so the bucket array is ~40% denser/smaller (fewer cache misses on
14/// a large keyspace, less RSS). The extra pointer-chase lands only on
15/// collection ops, not the hot string GET path.
16///
17/// `Arc` (same 8 B as the previous `Box`) is what makes O(short-pause)
18/// persistence possible: [`crate::Store::collect_snapshot`] bumps each
19/// collection's refcount instead of serializing it, and a background thread
20/// walks the frozen payloads at leisure. Mutations go through
21/// [`std::sync::Arc::make_mut`] — a single uniqueness check (the steady
22/// state, no snapshot in flight) or a copy-on-write deep clone when a
23/// snapshot still holds the payload.
24///
25/// `Str` holds a [`SmallBytes`] (24 B, same size as `Vec<u8>`) so byte strings
26/// up to 22 bytes live **inline inside the bucket**, killing the second cache
27/// miss the value pointer-chase used to cost on large-keyspace GETs.
28/// `Clone` is the snapshot-collect primitive: `Str` copies its bytes
29/// (inline = 24 B memcpy; heap = one allocation), collections bump a
30/// refcount. See [`crate::Store::collect_snapshot`].
31///
32/// ```
33/// use kevy_store::{SetCondition, Store, Value};
34/// let mut s = Store::new();
35/// s.set(b"k", b"v".to_vec(), None, SetCondition::Always);
36/// s.rpush(b"l", &[b"a".as_slice()])?;
37/// let mut types = Vec::new();
38/// s.snapshot_each(|_, v: &Value, _| types.push(v.type_name()));
39/// types.sort();
40/// assert_eq!(types, ["list", "string"]);
41/// # Ok::<(), kevy_store::StoreError>(())
42/// ```
43#[derive(Debug, Clone)]
44pub enum Value {
45    /// A byte string, inline up to 22 bytes — see the type doc above for
46    /// why that boundary is where it is.
47    ///
48    /// ```
49    /// use kevy_store::{SetCondition, Store, Value};
50    /// let mut s = Store::new();
51    /// s.set(b"k", b"hello".to_vec(), None, SetCondition::Always);
52    /// let mut hit = false;
53    /// s.snapshot_each(|_, v, _| hit |= matches!(v, Value::Str(b) if b.as_ref() == b"hello"));
54    /// assert!(hit);
55    /// # Ok::<(), kevy_store::StoreError>(())
56    /// ```
57    Str(SmallBytes),
58    /// Following valkey's OBJ_ENCODING_INT: when a SET
59    /// stores a clean canonical i64 ASCII string (parses round-trip), we
60    /// keep the integer **as i64** rather than as 22 B of inline bytes.
61    /// Wins on INCR (in-place `+= delta`, no parse / no format / no
62    /// SmallBytes wrap) and on memory (8 B vs 24 B). GET formats it via
63    /// a per-`Store` scratch buffer.
64    ///
65    /// ```
66    /// use kevy_store::{SetCondition, Store, Value};
67    /// let mut s = Store::new();
68    /// s.set(b"n", b"41".to_vec(), None, SetCondition::Always);
69    /// assert_eq!(s.incr_by(b"n", 1)?, 42);
70    /// let mut hit = false;
71    /// s.snapshot_each(|_, v, _| hit |= matches!(v, Value::Int(42)));
72    /// assert!(hit);
73    /// # Ok::<(), kevy_store::StoreError>(())
74    /// ```
75    Int(i64),
76    /// Values larger
77    /// than [`BULK_THRESHOLD`] bytes get stored behind an
78    /// `Arc<Box<[u8]>>` instead of a heap-backed `SmallBytes`. The Arc
79    /// lets the io_uring reactor's reply path borrow the bytes across
80    /// the SQE→CQE window safely (Arc clone keeps them alive even if
81    /// the keyspace mutates) — the prerequisite for the writev
82    /// zero-copy bulk reply path, which skips the per-GET memcpy from
83    /// value storage into the per-conn output buffer.
84    ///
85    /// **Why `Arc<Box<[u8]>>` and not `Arc<[u8]>`**: `Arc<[u8]>` is a
86    /// DST-backed `ArcInner<[u8]> = { strong, weak, [u8; N] }` whose
87    /// data slot sits past the refcount words. `Arc::from(Vec<u8>)`
88    /// allocates a fresh `ArcInner` and `copy_from_slice`s the bytes
89    /// — a hard mandatory 64 KiB memcpy on every big SET. With
90    /// `Arc<Box<[u8]>>`, the `Box<[u8]>` wrapper occupies the Arc's
91    /// data slot (16 B), pointing AT an unchanged heap buffer; so
92    /// `Arc::new(vec.into_boxed_slice())` is **truly zero-copy**
93    /// (the boxed slice's allocation stays put — only the 32-byte
94    /// `ArcInner` is freshly malloced). Per-GET cost: one extra
95    /// pointer dereference (`&**arc` to get `&[u8]`), measured to be
96    /// negligible vs the per-SET memcpy savings. The `Arc<[u8]>`
97    /// mandatory copy was confirmed with perf-record before switching
98    /// to `Arc<Box<[u8]>>`.
99    ///
100    /// Small values stay on `Str(SmallBytes)` because the inline
101    /// cache-line storage beats an Arc indirection for the common case.
102    ///
103    /// ```
104    /// use kevy_store::{SetCondition, Store, Value};
105    /// let mut s = Store::new();
106    /// s.set(b"k", vec![b'x'; 1024], None, SetCondition::Always);
107    /// let mut hit = false;
108    /// s.snapshot_each(|_, v, _| hit |= matches!(v, Value::ArcBulk(b) if b.len() == 1024));
109    /// assert!(hit);
110    /// # Ok::<(), kevy_store::StoreError>(())
111    /// ```
112    ArcBulk(Arc<Box<[u8]>>),
113    /// A hash below [`HS_PROMOTE`](crate::seg_map::HS_PROMOTE) elements: one map behind one `Arc`, so
114    /// a snapshot pins it whole and the first write during that window
115    /// deep-clones it. Past that size it becomes `SegHash`.
116    ///
117    /// ```
118    /// use kevy_store::{SetCondition, Store, Value};
119    /// let mut s = Store::new();
120    /// let items: Vec<Vec<u8>> = (0..10).map(|i| format!("field-{i}").into_bytes()).collect();
121    /// let refs: Vec<&[u8]> = items.iter().map(|m| m.as_slice()).collect();
122    /// let pairs: Vec<(&[u8], &[u8])> = refs.iter().map(|f| (*f, &b"v"[..])).collect();
123    /// s.hset(b"h", &pairs)?;
124    /// let mut hit = false;
125    /// s.snapshot_each(|_, v, _| hit |= matches!(v, Value::Hash(h) if h.len() == 10));
126    /// assert!(hit);
127    /// # Ok::<(), kevy_store::StoreError>(())
128    /// ```
129    Hash(Arc<HashData>),
130    /// A hash past `seg_map::HS_PROMOTE` fields: an extendible-hash
131    /// directory of `Arc`-shared buckets — a COW write under a live
132    /// snapshot view clones one bucket, not the whole value.
133    ///
134    /// ```
135    /// use kevy_store::{SetCondition, Store, Value};
136    /// let mut s = Store::new();
137    /// let many: Vec<Vec<u8>> = (0..16 * 1024 + 1).map(|i| format!("m{i}").into_bytes()).collect();
138    /// let refs: Vec<&[u8]> = many.iter().map(|m| m.as_slice()).collect();
139    /// let pairs: Vec<(&[u8], &[u8])> = refs.iter().map(|m| (*m, *m)).collect();
140    /// s.hset(b"h", &pairs)?;
141    /// let mut hit = false;
142    /// s.snapshot_each(|_, v, _| hit |= matches!(v, Value::SegHash(_)));
143    /// assert!(hit);
144    /// # Ok::<(), kevy_store::StoreError>(())
145    /// ```
146    SegHash(Arc<crate::seg_map::SegMap<SmallBytes>>),
147    /// A list below [`SEG_PROMOTE`](crate::list_seg::SEG_PROMOTE) elements: one deque behind one `Arc`,
148    /// with the same whole-value copy-on-write. Past that size it becomes
149    /// `SegList`.
150    ///
151    /// ```
152    /// use kevy_store::{SetCondition, Store, Value};
153    /// let mut s = Store::new();
154    /// let items: Vec<Vec<u8>> = (0..10).map(|i| format!("item-{i}").into_bytes()).collect();
155    /// let refs: Vec<&[u8]> = items.iter().map(|m| m.as_slice()).collect();
156    /// s.rpush(b"l", &refs)?;
157    /// let mut hit = false;
158    /// s.snapshot_each(|_, v, _| hit |= matches!(v, Value::List(l) if l.len() == 10));
159    /// assert!(hit);
160    /// # Ok::<(), kevy_store::StoreError>(())
161    /// ```
162    List(Arc<ListData>),
163    /// A list past [`crate::list_seg::SEG_PROMOTE`] elements: a deque of
164    /// `Arc`-shared segments so a COW write under a live snapshot view
165    /// clones one segment, not the whole (possibly multi-GB) value. See
166    /// `list_seg.rs` for the promotion contract.
167    ///
168    /// ```
169    /// use kevy_store::{SetCondition, Store, Value};
170    /// let mut s = Store::new();
171    /// let many: Vec<Vec<u8>> = (0..16 * 1024 + 1).map(|i| format!("m{i}").into_bytes()).collect();
172    /// let refs: Vec<&[u8]> = many.iter().map(|m| m.as_slice()).collect();
173    /// s.rpush(b"l", &refs)?;
174    /// let mut hit = false;
175    /// s.snapshot_each(|_, v, _| hit |= matches!(v, Value::SegList(_)));
176    /// assert!(hit);
177    /// # Ok::<(), kevy_store::StoreError>(())
178    /// ```
179    SegList(Arc<crate::list_seg::SegListData>),
180    /// A set below [`HS_PROMOTE`](crate::seg_map::HS_PROMOTE) elements, on the same terms as `Hash`.
181    ///
182    /// ```
183    /// use kevy_store::{SetCondition, Store, Value};
184    /// let mut s = Store::new();
185    /// let items: Vec<Vec<u8>> = (0..10).map(|i| format!("member-{i}").into_bytes()).collect();
186    /// let refs: Vec<&[u8]> = items.iter().map(|m| m.as_slice()).collect();
187    /// s.sadd(b"s", &refs)?;
188    /// let mut hit = false;
189    /// s.snapshot_each(|_, v, _| hit |= matches!(v, Value::Set(m) if m.len() == 10));
190    /// assert!(hit);
191    /// # Ok::<(), kevy_store::StoreError>(())
192    /// ```
193    Set(Arc<SetData>),
194    /// A set past `seg_map::HS_PROMOTE` members — the set door of the
195    /// same bucket-sharded COW as [`Value::SegHash`].
196    ///
197    /// ```
198    /// use kevy_store::{SetCondition, Store, Value};
199    /// let mut s = Store::new();
200    /// let many: Vec<Vec<u8>> = (0..16 * 1024 + 1).map(|i| format!("m{i}").into_bytes()).collect();
201    /// let refs: Vec<&[u8]> = many.iter().map(|m| m.as_slice()).collect();
202    /// s.sadd(b"s", &refs)?;
203    /// let mut hit = false;
204    /// s.snapshot_each(|_, v, _| hit |= matches!(v, Value::SegSet(_)));
205    /// assert!(hit);
206    /// # Ok::<(), kevy_store::StoreError>(())
207    /// ```
208    SegSet(Arc<crate::seg_map::SegMap<()>>),
209    /// A sorted set: members with scores, plus the order-statistic tree
210    /// that makes rank queries a lookup rather than a scan.
211    ///
212    /// ```
213    /// use kevy_store::{SetCondition, Store, Value};
214    /// let mut s = Store::new();
215    /// s.zadd(b"z", &[(1.0, b"a".as_slice()), (2.0, b"b"), (3.0, b"c"), (4.0, b"d"), (5.0, b"e")])?;
216    /// let mut hit = false;
217    /// s.snapshot_each(|_, v, _| hit |= matches!(v, Value::ZSet(_)));
218    /// assert!(hit);
219    /// # Ok::<(), kevy_store::StoreError>(())
220    /// ```
221    ZSet(Arc<ZSetData>),
222    /// A zset past `zset_seg::Z_PROMOTE` members — sharded member map
223    /// + ordered segments; COW writes clone one bucket + one segment.
224    ///
225    /// ```
226    /// use kevy_store::{SetCondition, Store, Value};
227    /// let mut s = Store::new();
228    /// let many: Vec<Vec<u8>> = (0..16 * 1024 + 1).map(|i| format!("m{i}").into_bytes()).collect();
229    /// let refs: Vec<&[u8]> = many.iter().map(|m| m.as_slice()).collect();
230    /// let pairs: Vec<(f64, &[u8])> = refs.iter().enumerate().map(|(i, m)| (i as f64, *m)).collect();
231    /// s.zadd(b"z", &pairs)?;
232    /// let mut hit = false;
233    /// s.snapshot_each(|_, v, _| hit |= matches!(v, Value::SegZSet(_)));
234    /// assert!(hit);
235    /// # Ok::<(), kevy_store::StoreError>(())
236    /// ```
237    SegZSet(Arc<crate::zset_seg::SegZSetData>),
238    /// A stream: entries, consumer groups and their pending lists. Never
239    /// segmented — a stream trims from the front instead of growing
240    /// without bound.
241    ///
242    /// ```
243    /// use kevy_store::{SetCondition, Store, Value};
244    /// let mut s = Store::new();
245    /// use kevy_store::{MissingStream, XAddIdSpec};
246    /// let fields = vec![(b"temp".to_vec(), b"21".to_vec())];
247    /// s.xadd(b"st", XAddIdSpec::AutoAll, fields, MissingStream::Create, 1_000)?;
248    /// let mut hit = false;
249    /// s.snapshot_each(|_, v, _| hit |= matches!(v, Value::Stream(_)));
250    /// assert!(hit);
251    /// # Ok::<(), kevy_store::StoreError>(())
252    /// ```
253    Stream(Arc<crate::stream::StreamData>),
254    /// Valkey-orthodox encoding switch: tiny sets (1-N
255    /// short members) live inline in 24 bytes instead of behind
256    /// `Arc<SetData>` — matches valkey's `OBJ_ENCODING_LISTPACK` for
257    /// sets, which is what `redis-benchmark -t sadd` default `-r 0`
258    /// (cardinality stays at 1 forever, single 20-byte literal member)
259    /// measures. On overflow (the inline form has no room for another
260    /// member) the set is promoted to `Value::Set(Arc<SetData>)`
261    /// — the Swiss-table path that wins for larger cardinalities.
262    ///
263    /// ```
264    /// use kevy_store::{SetCondition, Store, Value};
265    /// let mut s = Store::new();
266    /// s.sadd(b"s", &[b"only".as_slice()])?;
267    /// let mut hit = false;
268    /// s.snapshot_each(|_, v, _| hit |= matches!(v, Value::SmallSetInline(_)));
269    /// assert!(hit);
270    /// # Ok::<(), kevy_store::StoreError>(())
271    /// ```
272    SmallSetInline(crate::small_set::SmallSetData),
273    /// Tiny hashes
274    /// (1-2 short field-value pairs) live inline in 24 bytes; promoted
275    /// to `Value::Hash(Arc<HashData>)` on overflow. Mirrors valkey's
276    /// `OBJ_ENCODING_LISTPACK` for hashes.
277    ///
278    /// ```
279    /// use kevy_store::{SetCondition, Store, Value};
280    /// let mut s = Store::new();
281    /// s.hset(b"h", &[(b"f".as_slice(), b"v".as_slice())])?;
282    /// let mut hit = false;
283    /// s.snapshot_each(|_, v, _| hit |= matches!(v, Value::SmallHashInline(_)));
284    /// assert!(hit);
285    /// # Ok::<(), kevy_store::StoreError>(())
286    /// ```
287    SmallHashInline(crate::small_hash::SmallHashData),
288    /// A declared table's row: the columns in declared order, in one payload
289    /// buffer, with no field names and no per-row table.
290    ///
291    /// Reachable only for a key under a declared prefix — an undeclared hash
292    /// keeps [`Value::Hash`].
293    ///
294    /// ```
295    /// use kevy_store::{SetCondition, Store, Value};
296    /// let mut s = Store::new();
297    /// s.hset(b"user:1", &[(b"id".as_slice(), b"7".as_slice())])?;
298    /// s.pack_row(b"user:1", &vec![b"id".to_vec(), b"name".to_vec()].into());
299    /// let mut hit = false;
300    /// s.snapshot_each(|_, v, _| hit |= matches!(v, Value::PackedRow(r) if r.get_named(b"id") == Some(&b"7"[..])));
301    /// assert!(hit);
302    /// # Ok::<(), kevy_store::StoreError>(())
303    /// ```
304    PackedRow(crate::packed_row::PackedRow),
305    /// Tiny lists inline encoding; promoted to
306    /// `Value::List(Arc<ListData>)` on overflow.
307    ///
308    /// ```
309    /// use kevy_store::{SetCondition, Store, Value};
310    /// let mut s = Store::new();
311    /// s.rpush(b"l", &[b"a".as_slice(), b"b"])?;
312    /// let mut hit = false;
313    /// s.snapshot_each(|_, v, _| hit |= matches!(v, Value::SmallListInline(_)));
314    /// assert!(hit);
315    /// # Ok::<(), kevy_store::StoreError>(())
316    /// ```
317    SmallListInline(crate::small_list::SmallListData),
318    /// Tiny sorted sets inline encoding; promoted to
319    /// `Value::ZSet(Arc<ZSetData>)` on overflow.
320    ///
321    /// ```
322    /// use kevy_store::{SetCondition, Store, Value};
323    /// let mut s = Store::new();
324    /// s.zadd(b"z", &[(1.0, b"a".as_slice())])?;
325    /// let mut hit = false;
326    /// s.snapshot_each(|_, v, _| hit |= matches!(v, Value::SmallZSetInline(_)));
327    /// assert!(hit);
328    /// # Ok::<(), kevy_store::StoreError>(())
329    /// ```
330    SmallZSetInline(crate::small_zset::SmallZSetData),
331    /// A demoted (tiered-to-disk) value's in-map stub. The two-stage
332    /// funnel (`tier` module) resolves this before any typed match sees
333    /// it: stage 1 answers existence/TYPE/TTL from the stub with zero
334    /// IO; stage 2 materializes (serve or promote) only on a type
335    /// match. Cloning a `Cold` clones the STUB, not the record — paths
336    /// that duplicate values (COPY, cross-shard ship) materialize
337    /// first so two stubs never alias one vlog record.
338    ///
339    /// ```
340    /// use kevy_store::{SetCondition, Store, Value};
341    /// # let dir = std::env::temp_dir().join(format!("kevy-doc-value-cold-{}", std::process::id()));
342    /// let mut s = Store::new();
343    /// s.enable_tiering(&dir, 1 << 20)?;
344    /// s.set(b"k", vec![b'x'; 4096], None, SetCondition::Always);
345    /// s.set_tier_budget(1);
346    /// s.demote_to_watermark();
347    /// let mut cold = false;
348    /// s.snapshot_each(|_, v, _| cold |= matches!(v, Value::Cold(_)));
349    /// assert!(cold);
350    /// // the stub still answers TYPE without touching disk
351    /// assert_eq!(s.type_of(b"k"), "string");
352    /// # std::fs::remove_dir_all(&dir)?;
353    /// # Ok::<(), Box<dyn std::error::Error>>(())
354    /// ```
355    Cold(ColdRef),
356}
357
358/// Threshold (bytes) above which a SET stores its value as
359/// [`Value::ArcBulk`] (writev-eligible on GET) instead of [`Value::Str`]
360/// (inline `SmallBytes`). 64 B ≈ one cache line — below that the
361/// inline-SmallBytes storage wins on L1 locality; above it the
362/// writev-borrow win dominates.
363///
364/// ```
365/// use kevy_store::{BULK_THRESHOLD, SetCondition, Store, Value};
366/// let mut s = Store::new();
367/// s.set(b"at", vec![b'x'; BULK_THRESHOLD], None, SetCondition::Always);
368/// s.set(b"over", vec![b'x'; BULK_THRESHOLD + 1], None, SetCondition::Always);
369/// let mut kinds = Vec::new();
370/// s.snapshot_each(|k, v, _| kinds.push((k.to_vec(), matches!(v, Value::ArcBulk(_)))));
371/// kinds.sort();
372/// assert_eq!(kinds, [(b"at".to_vec(), false), (b"over".to_vec(), true)]);
373/// ```
374pub const BULK_THRESHOLD: usize = 64;
375
376const _: () = {
377    // Don't let future variants undo box-collection's Entry-48B win.
378    assert!(core::mem::size_of::<Value>() <= 32);
379};
380
381/// Heap-size threshold above which an overwritten `Value` is sent to the
382/// runtime's bio thread for off-reactor drop instead of being freed inline
383/// (lazy-drop).
384///
385/// **Why not lower**: a 256 B threshold regressed c=50 -d 10240 SET
386/// p999 from 0.487 → 1.583 ms (worse by 3.25×). The cause: `std::sync::mpsc::Sender::send`
387/// is a few hundred ns of atomic + Box clone, which EXCEEDS the inline
388/// `Box::<[u8]>::drop` cost when the allocator serves the free from a
389/// hot large-class slab (~ 1-3 µs for 10 KB; the bench's steady state).
390/// Off-loading only wins when the inline drop's tail risk (cold-slab
391/// `munmap`/`madvise` consolidation stall, observed at 50-150 µs and
392/// occasionally millisecond-range) exceeds the per-send channel cost
393/// PLUS the cross-thread cache-line bouncing.
394///
395/// With per-shard batch accumulation flushing at the end of every
396/// reactor iteration, the per-mpsc-send cost is amortised across N
397/// drops. That makes the channel hop profitable at smaller sizes than
398/// a lone-send model could justify (lone-send had to lift the
399/// threshold to 16 KB because per-`mpsc::send` cost was a few hundred
400/// ns — at 256 B the inline drop was cheaper).
401///
402/// **Sweet-spot surprise**: intuition suggested dropping the threshold
403/// to 256 B – 1 KB once batching amortises the send. A sweep across
404/// thresholds {512, 1024, 4096, 16384} × c=50 SET -d {1K, 4K, 10K, 64K}
405/// disproved that floor: at ≤ 1 KB threshold, p999 / max on small
406/// values (-d 1024, -d 4096) was variance-bounded equal or
407/// occasionally WORSE than a 16 KB threshold, while the larger
408/// sizes (10 KB / 64 KB) won either way. Cause: the Vec::push +
409/// occasional `MAX_PENDING_DROPS` force-flush stall costs more for
410/// small Arcs (allocator small-class free is sub-µs even at tail)
411/// than the inline drop it avoids.
412///
413/// Picked **4 KB** as the lowest threshold where the bio-off-reactor
414/// win consistently dominates the batch-buffer overhead on tail
415/// metrics. The biggest batching wins (vs lone-send at 16 KB) land on
416/// `-d 64K` SET p50 (-44 %) and `-d 10K` SET max (-35 %), where each
417/// iter's batch already contains several heavy values per shard.
418///
419/// ```
420/// use kevy_store::{HEAP_HEAVY_BYTES, SetCondition, Store};
421/// let (tx, rx) = std::sync::mpsc::channel();
422/// let mut s = Store::new();
423/// s.set_bio_drop_sender(tx);
424/// s.set(b"light", vec![b'x'; HEAP_HEAVY_BYTES - 1], None, SetCondition::Always);
425/// s.set(b"heavy", vec![b'x'; HEAP_HEAVY_BYTES], None, SetCondition::Always);
426/// // overwrite both; only the heavy value is shipped off for dropping
427/// s.set(b"light", b"1".to_vec(), None, SetCondition::Always);
428/// s.set(b"heavy", b"1".to_vec(), None, SetCondition::Always);
429/// s.flush_pending_drops();
430/// assert_eq!(rx.try_recv()?.len(), 1);
431/// # Ok::<(), Box<dyn std::error::Error>>(())
432/// ```
433pub const HEAP_HEAVY_BYTES: usize = 4 * 1024;
434
435/// Sender half of the runtime's bio-drop channel. Wired from
436/// `kevy-rt`'s `bio.rs` via [`crate::Store::set_bio_drop_sender`]; the
437/// concrete payload is `Vec<Value>` — a **batch** of values
438/// produced by one shard since its last flush.
439/// The bio thread (`kevy-rt::bio::spawn`) iterates the batch and
440/// drops each item. One mpsc message per shard-flush amortises the
441/// channel cost (atomic + cross-thread cacheline traffic) across
442/// however many values landed in the batch.
443///
444/// ```
445/// use kevy_store::{BioDropSender, SetCondition, Store};
446/// let (tx, rx) = std::sync::mpsc::channel();
447/// let sender: BioDropSender = tx;
448/// let mut s = Store::new();
449/// s.set_bio_drop_sender(sender);
450/// s.set(b"k", vec![b'x'; 64 * 1024], None, SetCondition::Always);
451/// s.set(b"k", b"small".to_vec(), None, SetCondition::Always);
452/// s.flush_pending_drops();
453/// // the displaced 64 KiB value arrives in one batch for the drop thread
454/// let batch = rx.try_recv()?;
455/// assert_eq!(batch[0].type_name(), "string");
456/// # Ok::<(), Box<dyn std::error::Error>>(())
457/// ```
458#[cfg(feature = "std")]
459pub type BioDropSender = std::sync::mpsc::Sender<Vec<Value>>;
460
461impl Value {
462    /// The Redis type name (`TYPE` command).
463    pub fn type_name(&self) -> &'static str {
464        match self {
465            Value::Str(_) | Value::Int(_) | Value::ArcBulk(_) => "string",
466            Value::Hash(_)
467            | Value::SegHash(_)
468            | Value::SmallHashInline(_)
469            | Value::PackedRow(_) => "hash",
470            Value::List(_) | Value::SegList(_) | Value::SmallListInline(_) => "list",
471            Value::Set(_) | Value::SegSet(_) | Value::SmallSetInline(_) => "set",
472            Value::ZSet(_) | Value::SegZSet(_) | Value::SmallZSetInline(_) => "zset",
473            Value::Stream(_) => "stream",
474            // Stage-1 funnel: TYPE (and SCAN's TYPE filter) answer from
475            // the tag — a cold key never pays a pread for its type.
476            Value::Cold(c) => c.type_name(),
477        }
478    }
479}
480
481// `BioDropSender = mpsc::Sender<Box<Value>>` requires `Value: Send`. Static
482// assert: if a future variant inadvertently makes Value `!Send` (e.g. an
483// `Rc<...>` payload) this fails at compile time, BEFORE the runtime tries
484// to hand a value to the bio thread.
485const _: fn() = || {
486    fn assert_send<T: Send>() {}
487    assert_send::<Value>();
488};