use std::{
collections::BTreeMap,
path::{Path, PathBuf},
sync::Arc,
};
use crate::utils::FastConcurrentMap;
pub(crate) const BUNDLE_EXT: &str = "bundle";
pub(crate) fn is_bundleable(filepath: &Path) -> bool {
filepath.extension().and_then(|ext| ext.to_str()) != Some("del")
}
pub(crate) fn object(filepath: &Path) -> Option<PathBuf> {
let segment = filepath.file_stem()?.to_str()?;
Some(PathBuf::from(format!("{segment}.{BUNDLE_EXT}")))
}
#[derive(Clone, Debug, Default)]
pub(crate) struct Bundler {
pending: Arc<FastConcurrentMap<PathBuf, Vec<u8>>>,
}
impl Bundler {
pub async fn buffer(&self, path: PathBuf, bytes: Vec<u8>) {
let _ = self.pending.insert_async(path, bytes).await;
}
pub async fn get(&self, path: &Path) -> Option<Vec<u8>> {
self.pending
.read_async(path, |_, bytes| bytes.clone())
.await
}
pub async fn drain(&self) -> Vec<Bundle> {
let mut files = Vec::new();
self.pending
.retain_async(|path, bytes| {
files.push((path.clone(), std::mem::take(bytes)));
false
})
.await;
let mut by_segment: BTreeMap<String, Vec<(PathBuf, Vec<u8>)>> = BTreeMap::new();
for (path, bytes) in files {
let Some(segment) = path.file_stem().and_then(|stem| stem.to_str()) else {
continue;
};
by_segment
.entry(segment.to_string())
.or_default()
.push((path, bytes));
}
by_segment
.into_iter()
.map(|(segment, mut files)| {
files.sort_by(|(a, _), (b, _)| a.cmp(b));
let mut bytes = Vec::new();
let mut entries = Vec::with_capacity(files.len());
for (path, file) in files {
let offset = bytes.len() as u64;
bytes.extend_from_slice(&file);
entries.push(BundleEntry {
path,
offset,
length: file.len() as u64,
});
}
let path = PathBuf::from(format!("{segment}.{BUNDLE_EXT}"));
Bundle {
path,
bytes,
entries,
}
})
.collect()
}
}
#[derive(Clone, Debug)]
pub(crate) struct Bundle {
pub path: PathBuf,
pub bytes: Vec<u8>,
pub entries: Vec<BundleEntry>,
}
#[derive(Clone, Debug)]
pub(crate) struct BundleEntry {
pub path: PathBuf,
pub offset: u64,
pub length: u64,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn is_bundleable_excludes_deletes() {
assert!(is_bundleable(Path::new("seg.idx")));
assert!(is_bundleable(Path::new("seg.store")));
assert!(!is_bundleable(Path::new("seg.123.del")));
}
#[tokio::test]
async fn drain_groups_by_segment_and_lays_out_offsets() {
let bundler = Bundler::default();
bundler.buffer(PathBuf::from("a.idx"), vec![1, 2, 3]).await;
bundler.buffer(PathBuf::from("a.term"), vec![4, 5]).await;
bundler.buffer(PathBuf::from("b.idx"), vec![9]).await;
let mut bundles = bundler.drain().await;
bundles.sort_by(|x, y| x.path.cmp(&y.path));
assert_eq!(bundles.len(), 2);
let a = &bundles[0];
assert_eq!(a.path, PathBuf::from("a.bundle"));
assert_eq!(a.bytes, vec![1, 2, 3, 4, 5]);
assert_eq!(a.entries.len(), 2);
assert_eq!(a.entries[0].path, PathBuf::from("a.idx"));
assert_eq!((a.entries[0].offset, a.entries[0].length), (0, 3));
assert_eq!(a.entries[1].path, PathBuf::from("a.term"));
assert_eq!((a.entries[1].offset, a.entries[1].length), (3, 2));
let b = &bundles[1];
assert_eq!(b.path, PathBuf::from("b.bundle"));
assert_eq!(b.bytes, vec![9]);
assert!(bundler.drain().await.is_empty());
}
}