Skip to main content

nodedb_lite/engine/array/
manifest.rs

1//! Per-array segment manifest persisted in the `Array` namespace.
2//!
3//! Key layout: `manifest:{name}` → zerompk-encoded `ArrayManifest`.
4//! Each segment reference carries a numeric ID used to form the segment
5//! storage key (`segment:{name}:{id}`).
6
7use std::sync::Arc;
8
9use nodedb_types::Namespace;
10use serde::{Deserialize, Serialize};
11
12use crate::error::LiteError;
13use crate::storage::engine::{StorageEngine, WriteOp};
14
15const MANIFEST_PREFIX: &str = "manifest:";
16
17/// Reference to one flushed segment.
18#[derive(
19    Debug,
20    Clone,
21    PartialEq,
22    Eq,
23    Serialize,
24    Deserialize,
25    zerompk::ToMessagePack,
26    zerompk::FromMessagePack,
27)]
28pub struct SegmentRef {
29    /// Monotonically increasing segment ID within this array.
30    pub id: u64,
31    /// Byte length of the segment payload stored in the KV store.
32    pub byte_len: u64,
33}
34
35/// Full manifest for one array.
36#[derive(
37    Debug, Clone, Default, Serialize, Deserialize, zerompk::ToMessagePack, zerompk::FromMessagePack,
38)]
39pub struct ArrayManifest {
40    pub segments: Vec<SegmentRef>,
41    /// Next segment ID to assign.
42    pub next_id: u64,
43}
44
45impl ArrayManifest {
46    pub fn new() -> Self {
47        Self::default()
48    }
49
50    /// Allocate the next segment ID and push a new `SegmentRef`.
51    pub fn push_segment(&mut self, byte_len: u64) -> u64 {
52        let id = self.next_id;
53        self.next_id += 1;
54        self.segments.push(SegmentRef { id, byte_len });
55        id
56    }
57
58    /// Remove segments whose IDs appear in `drop_ids` (retention compaction).
59    pub fn drop_segments(&mut self, drop_ids: &[u64]) {
60        let set: std::collections::HashSet<u64> = drop_ids.iter().copied().collect();
61        self.segments.retain(|s| !set.contains(&s.id));
62    }
63}
64
65fn manifest_key(name: &str) -> Vec<u8> {
66    let mut k = MANIFEST_PREFIX.as_bytes().to_vec();
67    k.extend_from_slice(name.as_bytes());
68    k
69}
70
71pub fn segment_key(name: &str, id: u64) -> Vec<u8> {
72    format!("segment:{name}:{id}").into_bytes()
73}
74
75/// Persist the manifest for `name` to storage.
76pub async fn save_manifest<S: StorageEngine>(
77    storage: &Arc<S>,
78    name: &str,
79    manifest: &ArrayManifest,
80) -> Result<(), LiteError> {
81    let bytes = zerompk::to_msgpack_vec(manifest).map_err(|e| LiteError::Serialization {
82        detail: format!("encode ArrayManifest: {e}"),
83    })?;
84    storage
85        .put(Namespace::Array, &manifest_key(name), &bytes)
86        .await
87}
88
89/// Load the manifest for `name` from storage. Returns an empty manifest
90/// when the key is absent (first open of a freshly created array).
91pub async fn load_manifest<S: StorageEngine>(
92    storage: &Arc<S>,
93    name: &str,
94) -> Result<ArrayManifest, LiteError> {
95    match storage.get(Namespace::Array, &manifest_key(name)).await? {
96        Some(bytes) => zerompk::from_msgpack(&bytes).map_err(|e| LiteError::Serialization {
97            detail: format!("decode ArrayManifest: {e}"),
98        }),
99        None => Ok(ArrayManifest::new()),
100    }
101}
102
103/// Remove the manifest and all segments of `name` from storage. Used by
104/// `delete_array`. `extra_ops` join the same batch as the manifest delete.
105///
106/// On a pagedb backend each segment lives in a pagedb segment file. Those
107/// files are tombstoned first, then the KV batch removes any KV blob, the
108/// manifest, and `extra_ops` together.
109pub async fn drop_manifest<S: StorageEngine>(
110    storage: &Arc<S>,
111    name: &str,
112    manifest: &ArrayManifest,
113    extra_ops: Vec<WriteOp>,
114) -> Result<(), LiteError> {
115    #[cfg(not(target_arch = "wasm32"))]
116    if let Some(ext) = storage.as_array_segment_ext() {
117        for s in &manifest.segments {
118            ext.delete_array_segment(name, s.id).await?;
119        }
120    }
121    let mut ops: Vec<WriteOp> = manifest
122        .segments
123        .iter()
124        .map(|s| WriteOp::Delete {
125            ns: Namespace::Array,
126            key: segment_key(name, s.id),
127        })
128        .collect();
129    ops.push(WriteOp::Delete {
130        ns: Namespace::Array,
131        key: manifest_key(name),
132    });
133    ops.extend(extra_ops);
134    storage.batch_write(&ops).await
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140    use crate::storage::pagedb_storage::PagedbStorageMem;
141    use std::sync::Arc;
142
143    use crate::storage::engine::StorageEngine;
144
145    #[tokio::test]
146    async fn manifest_push_and_persist() {
147        let storage = Arc::new(PagedbStorageMem::open_in_memory().await.unwrap());
148        let mut m = ArrayManifest::new();
149        m.push_segment(128);
150        m.push_segment(256);
151        save_manifest(&storage, "a", &m).await.unwrap();
152
153        let m2 = load_manifest(&storage, "a").await.unwrap();
154        assert_eq!(m2.segments.len(), 2);
155        assert_eq!(m2.segments[0].id, 0);
156        assert_eq!(m2.segments[1].id, 1);
157        assert_eq!(m2.next_id, 2);
158    }
159
160    #[tokio::test]
161    async fn load_missing_returns_empty() {
162        let storage = Arc::new(PagedbStorageMem::open_in_memory().await.unwrap());
163        let m = load_manifest(&storage, "no_such").await.unwrap();
164        assert!(m.segments.is_empty());
165        assert_eq!(m.next_id, 0);
166    }
167
168    #[tokio::test]
169    async fn drop_removes_manifest_and_segments() {
170        let storage = Arc::new(PagedbStorageMem::open_in_memory().await.unwrap());
171        let mut m = ArrayManifest::new();
172        let id = m.push_segment(64);
173        let seg_bytes = b"fake_segment_bytes";
174        storage
175            .put(Namespace::Array, &segment_key("b", id), seg_bytes)
176            .await
177            .unwrap();
178        save_manifest(&storage, "b", &m).await.unwrap();
179
180        drop_manifest(&storage, "b", &m, Vec::new()).await.unwrap();
181
182        assert!(
183            load_manifest(&storage, "b")
184                .await
185                .unwrap()
186                .segments
187                .is_empty()
188        );
189        assert!(
190            storage
191                .get(Namespace::Array, &segment_key("b", id))
192                .await
193                .unwrap()
194                .is_none()
195        );
196    }
197
198    #[test]
199    fn drop_segments_filter() {
200        let mut m = ArrayManifest::new();
201        m.push_segment(10);
202        m.push_segment(20);
203        m.push_segment(30);
204        m.drop_segments(&[0, 2]);
205        assert_eq!(m.segments.len(), 1);
206        assert_eq!(m.segments[0].id, 1);
207    }
208}