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};