Skip to main content

cobble_data_structure/
list.rs

1use bytes::{Buf, BufMut, Bytes, BytesMut};
2use cobble::{Error, MergeOperator, Result, TimeProvider, ValueType};
3use serde::{Deserialize, Serialize};
4use serde_json::Value as JsonValue;
5use std::collections::VecDeque;
6use std::mem::size_of;
7use std::sync::Arc;
8
9pub(crate) const LIST_OPERATOR_ID: &str = "cobble.list.v1";
10
11#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
12pub struct ListConfig {
13    pub max_elements: Option<usize>,
14    pub retain_mode: ListRetainMode,
15    pub preserve_element_ttl: bool,
16}
17
18impl Default for ListConfig {
19    fn default() -> Self {
20        Self {
21            max_elements: None,
22            retain_mode: ListRetainMode::Last,
23            preserve_element_ttl: false,
24        }
25    }
26}
27
28#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
29#[serde(rename_all = "snake_case")]
30pub enum ListRetainMode {
31    First,
32    #[default]
33    Last,
34}
35
36#[derive(Clone)]
37struct ListMergeOperator {
38    config: ListConfig,
39}
40
41impl ListMergeOperator {
42    fn new(config: ListConfig) -> Self {
43        Self { config }
44    }
45}
46
47impl MergeOperator for ListMergeOperator {
48    fn id(&self) -> String {
49        LIST_OPERATOR_ID.to_string()
50    }
51
52    fn metadata(&self) -> Option<JsonValue> {
53        serde_json::to_value(&self.config).ok()
54    }
55
56    fn merge(
57        &self,
58        existing_value: Bytes,
59        value: Bytes,
60        time_provider: Option<&dyn TimeProvider>,
61    ) -> Result<(Bytes, Option<ValueType>)> {
62        self.merge_batch(existing_value, vec![value], time_provider)
63    }
64
65    fn merge_batch(
66        &self,
67        existing_value: Bytes,
68        operands: Vec<Bytes>,
69        time_provider: Option<&dyn TimeProvider>,
70    ) -> Result<(Bytes, Option<ValueType>)> {
71        if operands.is_empty() {
72            return Ok((existing_value, None));
73        }
74
75        if !self.config.preserve_element_ttl
76            && self.config.max_elements.is_none()
77            && existing_value.is_empty()
78            && operands.len() == 1
79        {
80            let operand = operands.into_iter().next().expect("len checked to be one");
81            let _ = parse_payload_body(&operand)?;
82            return Ok((operand, None));
83        }
84
85        // Fast path: validate payloads, then splice raw encoded bodies without decode/re-encode.
86        if let Some(merged) = try_fast_append_batch(&existing_value, &operands, &self.config)? {
87            return Ok((merged, None));
88        }
89
90        let now_seconds = time_provider
91            .map(|provider| provider.now_seconds())
92            .unwrap_or(0);
93        if let (ListRetainMode::Last, Some(max_elements)) =
94            (self.config.retain_mode, self.config.max_elements)
95        {
96            let (elements, reached_last_cap) = collect_last_from_newest(
97                &existing_value,
98                &operands,
99                &self.config,
100                now_seconds,
101                max_elements,
102            )?;
103            let output = encode_list_payload(&elements, &self.config)?;
104            let value_type = if reached_last_cap && !self.config.preserve_element_ttl {
105                Some(ValueType::Put)
106            } else {
107                None
108            };
109            return Ok((output, value_type));
110        }
111        let mut accumulator = ListAccumulator::new(&self.config);
112        accumulator.ingest_payload(&existing_value, now_seconds)?;
113        if !accumulator.should_stop() {
114            for operand in &operands {
115                accumulator.ingest_payload(operand, now_seconds)?;
116                if accumulator.should_stop() {
117                    break;
118                }
119            }
120        }
121        let (elements, reached_last_cap) = accumulator.into_parts();
122        let output = encode_list_payload(&elements, &self.config)?;
123        let value_type = if reached_last_cap
124            && self.config.retain_mode == ListRetainMode::Last
125            && !self.config.preserve_element_ttl
126        {
127            Some(ValueType::Put)
128        } else {
129            None
130        };
131        Ok((output, value_type))
132    }
133}
134
135pub(crate) fn list_operator(config: ListConfig) -> Arc<dyn MergeOperator> {
136    Arc::new(ListMergeOperator::new(config))
137}
138
139pub(crate) fn list_operator_from_metadata(
140    id: &str,
141    metadata: Option<&JsonValue>,
142) -> Option<Arc<dyn MergeOperator>> {
143    if id != LIST_OPERATOR_ID {
144        return None;
145    }
146    let config = serde_json::from_value::<ListConfig>(metadata?.clone()).ok()?;
147    Some(list_operator(config))
148}
149
150pub(crate) fn encode_list_for_write(
151    elements: Vec<Bytes>,
152    config: &ListConfig,
153    ttl_seconds: Option<u32>,
154    now_seconds: u32,
155) -> Result<Bytes> {
156    let expires_at_secs = if config.preserve_element_ttl {
157        ttl_seconds.map(|ttl| now_seconds.saturating_add(ttl))
158    } else {
159        None
160    };
161    let decoded = elements
162        .into_iter()
163        .map(|value| DecodedListElement {
164            value,
165            expires_at_secs,
166        })
167        .collect::<Vec<_>>();
168    encode_list_payload(&decoded, config)
169}
170
171#[cfg(feature = "ffi")]
172pub(crate) fn encode_borrowed_list_for_write(
173    elements: &[&[u8]],
174    config: &ListConfig,
175    ttl_seconds: Option<u32>,
176    now_seconds: u32,
177) -> Result<Bytes> {
178    let expires_at_secs = if config.preserve_element_ttl {
179        ttl_seconds.map(|ttl| now_seconds.saturating_add(ttl))
180    } else {
181        None
182    };
183    encode_list_parts(
184        elements.iter().map(|value| (*value, expires_at_secs)),
185        config,
186    )
187}
188
189pub(crate) fn decode_list_for_read(
190    raw: &Bytes,
191    config: &ListConfig,
192    now_seconds: u32,
193) -> Result<Vec<Bytes>> {
194    let mut accumulator = ListAccumulator::new(config);
195    accumulator.ingest_payload(raw, now_seconds)?;
196    let (elements, _) = accumulator.into_parts();
197    Ok(elements.into_iter().map(|element| element.value).collect())
198}
199
200/// Rewrite element bytes without applying read-time TTL or retention policies.
201pub(crate) fn transform_list_elements<F>(
202    payload: Bytes,
203    preserve_element_ttl: bool,
204    transform: &F,
205) -> Result<Bytes>
206where
207    F: Fn(Bytes) -> Result<Bytes>,
208{
209    if payload.is_empty() {
210        return Ok(payload);
211    }
212    let mut cursor = ListPayloadCursor::new(&payload, preserve_element_ttl)?;
213    let mut output = BytesMut::with_capacity(payload.len());
214    output.put_u32_le(cursor.remaining_elements as u32);
215    while let Some(element) = cursor.next()? {
216        let value = transform(element.value)?;
217        let len = u32::try_from(value.len()).map_err(|_| {
218            Error::InputError(format!(
219                "list element is too large to encode: {} bytes",
220                value.len()
221            ))
222        })?;
223        if preserve_element_ttl {
224            output.put_u32_le(element.expires_at_secs.unwrap_or(0));
225        }
226        output.put_u32_le(len);
227        output.extend_from_slice(&value);
228    }
229    Ok(output.freeze())
230}
231
232#[derive(Clone)]
233struct DecodedListElement {
234    value: Bytes,
235    expires_at_secs: Option<u32>,
236}
237
238/// Cursor over a single encoded list payload.
239///
240/// `remaining` owns a cheap clone of the original `Bytes`, so each `split_to` is zero-copy.
241struct ListPayloadCursor {
242    remaining: Bytes,
243    preserve_element_ttl: bool,
244    remaining_elements: usize,
245}
246
247impl ListPayloadCursor {
248    fn new(payload: &Bytes, preserve_element_ttl: bool) -> Result<Self> {
249        if payload.is_empty() {
250            return Ok(Self {
251                remaining: Bytes::new(),
252                preserve_element_ttl,
253                remaining_elements: 0,
254            });
255        }
256        let mut remaining = payload.clone();
257        if remaining.remaining() < size_of::<u32>() {
258            return Err(Error::FileFormatError(
259                "invalid list payload: missing element count".to_string(),
260            ));
261        }
262        let remaining_elements = remaining.get_u32_le() as usize;
263        Ok(Self {
264            remaining,
265            preserve_element_ttl,
266            remaining_elements,
267        })
268    }
269
270    fn next(&mut self) -> Result<Option<DecodedListElement>> {
271        if self.remaining_elements == 0 {
272            if self.remaining.has_remaining() {
273                return Err(Error::InvalidState(
274                    "invalid list payload: trailing bytes found".to_string(),
275                ));
276            }
277            return Ok(None);
278        }
279        let expires_at_secs = if self.preserve_element_ttl {
280            if self.remaining.remaining() < size_of::<u32>() {
281                return Err(Error::InvalidState(
282                    "invalid list payload: missing element ttl timestamp".to_string(),
283                ));
284            }
285            let expires_at = self.remaining.get_u32_le();
286            if expires_at == 0 {
287                None
288            } else {
289                Some(expires_at)
290            }
291        } else {
292            None
293        };
294        if self.remaining.remaining() < size_of::<u32>() {
295            return Err(Error::InvalidState(
296                "invalid list payload: missing element length".to_string(),
297            ));
298        }
299        let element_len = self.remaining.get_u32_le() as usize;
300        if self.remaining.remaining() < element_len {
301            return Err(Error::InvalidState(format!(
302                "invalid list payload: element length {} exceeds remaining {}",
303                element_len,
304                self.remaining.remaining()
305            )));
306        }
307        self.remaining_elements -= 1;
308        Ok(Some(DecodedListElement {
309            value: self.remaining.split_to(element_len),
310            expires_at_secs,
311        }))
312    }
313}
314
315/// Streaming accumulator used by merge/read paths.
316///
317/// It filters TTL inline and applies first/last policies while scanning payloads, so merge does
318/// not need to decode everything into a temporary full vector.
319struct ListAccumulator {
320    config: ListConfig,
321    mode: ListAccumulatorMode,
322    reached_last_cap: bool,
323}
324
325enum ListAccumulatorMode {
326    All(Vec<DecodedListElement>),
327    First {
328        max: usize,
329        kept: Vec<DecodedListElement>,
330    },
331    Last {
332        max: usize,
333        kept: VecDeque<DecodedListElement>,
334    },
335}
336
337impl ListAccumulator {
338    fn new(config: &ListConfig) -> Self {
339        let mode = match (config.max_elements, config.retain_mode) {
340            (Some(max), ListRetainMode::First) => ListAccumulatorMode::First {
341                max,
342                kept: Vec::with_capacity(max),
343            },
344            (Some(max), ListRetainMode::Last) => ListAccumulatorMode::Last {
345                max,
346                kept: VecDeque::with_capacity(max),
347            },
348            (None, _) => ListAccumulatorMode::All(Vec::new()),
349        };
350        Self {
351            config: config.clone(),
352            mode,
353            reached_last_cap: false,
354        }
355    }
356
357    fn ingest_payload(&mut self, payload: &Bytes, now_seconds: u32) -> Result<()> {
358        let mut cursor = ListPayloadCursor::new(payload, self.config.preserve_element_ttl)?;
359        while let Some(element) = cursor.next()? {
360            if self.config.preserve_element_ttl
361                && element
362                    .expires_at_secs
363                    .is_some_and(|expires_at| expires_at <= now_seconds)
364            {
365                continue;
366            }
367            match &mut self.mode {
368                ListAccumulatorMode::All(values) => {
369                    values.push(element);
370                }
371                ListAccumulatorMode::First { max, kept } => {
372                    if kept.len() < *max {
373                        kept.push(element);
374                    }
375                }
376                ListAccumulatorMode::Last { max, kept } => {
377                    kept.push_back(element);
378                    if kept.len() > *max {
379                        let _ = kept.pop_front();
380                    }
381                    if kept.len() == *max {
382                        self.reached_last_cap = true;
383                    }
384                }
385            }
386            if self.should_stop() {
387                break;
388            }
389        }
390        Ok(())
391    }
392
393    fn should_stop(&self) -> bool {
394        match &self.mode {
395            ListAccumulatorMode::First { max, kept } => kept.len() >= *max,
396            ListAccumulatorMode::All(_) | ListAccumulatorMode::Last { .. } => false,
397        }
398    }
399
400    fn into_parts(self) -> (Vec<DecodedListElement>, bool) {
401        let elements = match self.mode {
402            ListAccumulatorMode::All(values) => values,
403            ListAccumulatorMode::First { kept, .. } => kept,
404            ListAccumulatorMode::Last { kept, .. } => kept.into_iter().collect(),
405        };
406        (elements, self.reached_last_cap)
407    }
408}
409
410fn encode_list_payload(elements: &[DecodedListElement], config: &ListConfig) -> Result<Bytes> {
411    encode_list_parts(
412        elements
413            .iter()
414            .map(|element| (element.value.as_ref(), element.expires_at_secs)),
415        config,
416    )
417}
418
419fn encode_list_parts<'a, I>(elements: I, config: &ListConfig) -> Result<Bytes>
420where
421    I: Clone + ExactSizeIterator<Item = (&'a [u8], Option<u32>)>,
422{
423    if elements.len() > u32::MAX as usize {
424        return Err(Error::InputError(format!(
425            "too many list elements to encode: {}",
426            elements.len()
427        )));
428    }
429    let ttl_bytes = if config.preserve_element_ttl {
430        size_of::<u32>()
431    } else {
432        0
433    };
434    let body_size = elements.clone().try_fold(0usize, |total, (value, _)| {
435        if value.len() > u32::MAX as usize {
436            return Err(Error::InputError(format!(
437                "list element is too large to encode: {} bytes",
438                value.len()
439            )));
440        }
441        total
442            .checked_add(ttl_bytes)
443            .and_then(|size| size.checked_add(size_of::<u32>()))
444            .and_then(|size| size.checked_add(value.len()))
445            .ok_or_else(|| Error::InputError("encoded list size overflows usize".to_string()))
446    })?;
447    let total_size = size_of::<u32>()
448        .checked_add(body_size)
449        .ok_or_else(|| Error::InputError("encoded list size overflows usize".to_string()))?;
450    let mut out = BytesMut::with_capacity(total_size);
451    out.put_u32_le(elements.len() as u32);
452    for (value, expires_at_secs) in elements {
453        if config.preserve_element_ttl {
454            out.put_u32_le(expires_at_secs.unwrap_or(0));
455        }
456        out.put_u32_le(value.len() as u32);
457        out.extend_from_slice(value);
458    }
459    Ok(out.freeze())
460}
461
462fn try_fast_append_batch(
463    existing: &Bytes,
464    operands: &[Bytes],
465    config: &ListConfig,
466) -> Result<Option<Bytes>> {
467    if config.preserve_element_ttl {
468        return Ok(None);
469    }
470    let (mut total_count, existing_body) = parse_payload_body(existing)?;
471    let mut operand_bodies = Vec::with_capacity(operands.len());
472    for operand in operands {
473        let (count, body) = parse_payload_body(operand)?;
474        total_count = total_count.checked_add(count).ok_or_else(|| {
475            Error::InputError(format!(
476                "list element count overflow during merge: {} + {}",
477                total_count, count
478            ))
479        })?;
480        operand_bodies.push(body);
481    }
482    if let Some(max_elements) = config.max_elements
483        && total_count > max_elements
484    {
485        return Ok(None);
486    }
487    if total_count > u32::MAX as usize {
488        return Err(Error::InputError(format!(
489            "too many list elements to encode: {}",
490            total_count
491        )));
492    }
493    let total_body_size =
494        existing_body.len() + operand_bodies.iter().map(Bytes::len).sum::<usize>();
495    let mut out = BytesMut::with_capacity(size_of::<u32>() + total_body_size);
496    out.put_u32_le(total_count as u32);
497    out.extend_from_slice(existing_body.as_ref());
498    for body in operand_bodies {
499        out.extend_from_slice(body.as_ref());
500    }
501    Ok(Some(out.freeze()))
502}
503
504// Fast-path helper: only decode header count and body slice.
505// We intentionally skip full payload scan here to keep merge append path minimal.
506fn parse_payload_body(payload: &Bytes) -> Result<(usize, Bytes)> {
507    if payload.is_empty() {
508        return Ok((0, Bytes::new()));
509    }
510    if payload.len() < size_of::<u32>() {
511        return Err(Error::FileFormatError(
512            "invalid list payload: missing element count".to_string(),
513        ));
514    }
515    let mut header = payload.slice(..size_of::<u32>());
516    let element_count = header.get_u32_le() as usize;
517    Ok((element_count, payload.slice(size_of::<u32>()..)))
518}
519
520/// Collect `retain last N` result by traversing payloads from newest to oldest.
521///
522/// This allows merge to stop once N items are collected, skipping older payloads entirely.
523fn collect_last_from_newest(
524    existing_value: &Bytes,
525    operands: &[Bytes],
526    config: &ListConfig,
527    now_seconds: u32,
528    max_elements: usize,
529) -> Result<(Vec<DecodedListElement>, bool)> {
530    if max_elements == 0 {
531        return Ok((Vec::new(), true));
532    }
533    let mut newest_to_oldest = Vec::with_capacity(max_elements);
534    for payload in operands.iter().rev().chain(std::iter::once(existing_value)) {
535        if newest_to_oldest.len() >= max_elements {
536            break;
537        }
538        let needed = max_elements - newest_to_oldest.len();
539        collect_last_from_single_payload(
540            payload,
541            config,
542            now_seconds,
543            needed,
544            &mut newest_to_oldest,
545        )?;
546    }
547    let reached_last_cap = newest_to_oldest.len() >= max_elements;
548    newest_to_oldest.reverse();
549    Ok((newest_to_oldest, reached_last_cap))
550}
551
552fn collect_last_from_single_payload(
553    payload: &Bytes,
554    config: &ListConfig,
555    now_seconds: u32,
556    needed: usize,
557    out_newest_to_oldest: &mut Vec<DecodedListElement>,
558) -> Result<()> {
559    if needed == 0 {
560        return Ok(());
561    }
562    let mut cursor = ListPayloadCursor::new(payload, config.preserve_element_ttl)?;
563    let mut tail = VecDeque::with_capacity(needed);
564    while let Some(element) = cursor.next()? {
565        if config.preserve_element_ttl
566            && element
567                .expires_at_secs
568                .is_some_and(|expires_at| expires_at <= now_seconds)
569        {
570            continue;
571        }
572        tail.push_back(element);
573        if tail.len() > needed {
574            let _ = tail.pop_front();
575        }
576    }
577    while let Some(element) = tail.pop_back() {
578        out_newest_to_oldest.push(element);
579    }
580    Ok(())
581}
582
583#[cfg(test)]
584#[path = "../tests/unit/list.rs"]
585mod tests;