Skip to main content

lace_sans_io/core/
listing.rs

1//! Complete advertisement listings and their changes between rounds.
2
3use std::collections::BTreeMap;
4use std::sync::Arc;
5
6use super::{AdvertisementRecord, LogicError};
7use crate::records::RecordHash;
8
9/// Hash-ordered listing entries. Entries are shared between the listings and
10/// requests that contain them.
11pub type ListingEntries = BTreeMap<RecordHash, Arc<AdvertisementRecord>>;
12
13/// One complete listing: the frozen local listing at its sender and the
14/// complete peer listing at its receiver, with the canonical byte size `B` of
15/// the listing rendered as an `::offers` item: its label line, every
16/// entry's lines, and the terminating LF.
17///
18/// An endpoint keeps one local listing for its logical session and changes it
19/// in place each round; a direct receiver shares its map until it freezes its
20/// request, so the next change finds the map unshared.
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct AdvertisementListing {
23    pub entries: Arc<ListingEntries>,
24    pub(super) canonical_bytes: u64,
25}
26
27impl Default for AdvertisementListing {
28    fn default() -> Self {
29        Self {
30            entries: Arc::default(),
31            canonical_bytes: crate::iltp::EMPTY_LISTING_BYTES,
32        }
33    }
34}
35
36impl AdvertisementListing {
37    /// Size a complete listing from its entries' measured bytes. Fails with
38    /// `ResourceLimit` when the block would exceed the ILTP item cap.
39    #[cfg(test)]
40    pub(super) fn new(entries: ListingEntries) -> Result<Self, LogicError> {
41        let listing = Self {
42            canonical_bytes: crate::iltp::EMPTY_LISTING_BYTES
43                + entries.values().map(|e| e.canonical_bytes()).sum::<u64>(),
44            entries: Arc::new(entries),
45        };
46        listing.check_item_cap()?;
47        Ok(listing)
48    }
49
50    pub fn canonical_bytes(&self) -> u64 {
51        self.canonical_bytes
52    }
53
54    /// Add an entry the listing does not hold.
55    pub(super) fn insert(&mut self, entry: Arc<AdvertisementRecord>) -> Result<(), LogicError> {
56        let hash = entry.record_hash();
57        let bytes = entry.canonical_bytes();
58        if Arc::make_mut(&mut self.entries)
59            .insert(hash, entry)
60            .is_some()
61        {
62            return Err(LogicError::Integration(format!(
63                "listing change adds {hash}, which the listing holds"
64            )));
65        }
66        self.canonical_bytes += bytes;
67        Ok(())
68    }
69
70    /// Remove the entry for `hash`, which the listing holds.
71    pub(super) fn withdraw(
72        &mut self,
73        hash: &RecordHash,
74    ) -> Result<Arc<AdvertisementRecord>, LogicError> {
75        let entry = Arc::make_mut(&mut self.entries)
76            .remove(hash)
77            .ok_or_else(|| {
78                LogicError::Integration(format!(
79                    "listing change withdraws {hash}, which the listing lacks"
80                ))
81            })?;
82        self.canonical_bytes -= entry.canonical_bytes();
83        Ok(entry)
84    }
85
86    /// The sender checks its complete listing against the item cap every
87    /// round, whatever encoding carries it.
88    pub(super) fn check_item_cap(&self) -> Result<(), LogicError> {
89        if self.canonical_bytes > crate::iltp::ITEM_MAX_BYTES as u64 {
90            return Err(LogicError::ResourceLimit(
91                "advertisement listing exceeds ILTP item limit".into(),
92            ));
93        }
94        Ok(())
95    }
96}
97
98#[cfg(test)]
99impl FromIterator<AdvertisementRecord> for AdvertisementListing {
100    fn from_iter<I: IntoIterator<Item = AdvertisementRecord>>(records: I) -> Self {
101        Self::new(
102            records
103                .into_iter()
104                .map(|record| (record.record_hash(), Arc::new(record)))
105                .collect(),
106        )
107        .expect("test listing fits the item cap")
108    }
109}
110
111/// What a complete listing added and withdrew since the previous complete
112/// listing of its logical session.
113#[derive(Debug, Clone, PartialEq, Eq)]
114pub enum ListingChange {
115    /// Every entry is added: the previous listing was empty, as before a
116    /// session's first listing.
117    All,
118    Delta {
119        added: Vec<Arc<AdvertisementRecord>>,
120        withdrawn: Vec<Arc<AdvertisementRecord>>,
121    },
122}
123
124impl Default for ListingChange {
125    /// No change.
126    fn default() -> Self {
127        Self::Delta {
128            added: Vec::new(),
129            withdrawn: Vec::new(),
130        }
131    }
132}
133
134impl ListingChange {
135    /// The change of a listing whose previous listing was `previous`, before
136    /// any entry is added or withdrawn.
137    pub(super) fn starting_from(previous: &ListingEntries) -> Self {
138        if previous.is_empty() {
139            Self::All
140        } else {
141            Self::default()
142        }
143    }
144
145    pub(super) fn note_added(&mut self, entry: &Arc<AdvertisementRecord>) {
146        if let Self::Delta { added, .. } = self {
147            added.push(entry.clone());
148        }
149    }
150
151    pub(super) fn note_withdrawn(&mut self, entry: Arc<AdvertisementRecord>) {
152        if let Self::Delta { withdrawn, .. } = self {
153            withdrawn.push(entry);
154        }
155    }
156
157    /// The added entries of `listing`, the complete listing after the change.
158    pub(super) fn added<'a>(
159        &'a self,
160        listing: &'a ListingEntries,
161    ) -> impl Iterator<Item = &'a Arc<AdvertisementRecord>> + 'a {
162        let (all, delta) = match self {
163            Self::All => (Some(listing.values()), None),
164            Self::Delta { added, .. } => (None, Some(added.iter())),
165        };
166        all.into_iter().flatten().chain(delta.into_iter().flatten())
167    }
168
169    pub(super) fn withdrawn(&self) -> &[Arc<AdvertisementRecord>] {
170        match self {
171            Self::All => &[],
172            Self::Delta { withdrawn, .. } => withdrawn,
173        }
174    }
175
176    /// The change from `previous` to `current`, by comparing them.
177    #[cfg(test)]
178    pub(super) fn between(previous: &ListingEntries, current: &ListingEntries) -> Self {
179        Self::Delta {
180            added: current
181                .iter()
182                .filter(|(hash, _)| !previous.contains_key(hash))
183                .map(|(_, entry)| entry.clone())
184                .collect(),
185            withdrawn: previous
186                .iter()
187                .filter(|(hash, _)| !current.contains_key(hash))
188                .map(|(_, entry)| entry.clone())
189                .collect(),
190        }
191    }
192}