hashiverse_lib/client/timeline/
recent_posts_pen.rs1use bytes::Bytes;
16use std::collections::HashSet;
17
18use crate::tools::buckets::{BucketLocation, BucketType};
19use crate::tools::time::{DurationMillis, MILLIS_IN_MINUTE, TimeMillis};
20use crate::tools::types::Id;
21
22const RECENT_POSTS_PEN_TTL: DurationMillis = MILLIS_IN_MINUTE.const_mul(10);
23
24pub struct RecentPostsPenEntry {
25 pub bucket_location: BucketLocation,
26 pub post_id: Id,
27 pub encoded_post_bytes: Bytes,
28 pub time_millis: TimeMillis,
29}
30
31pub struct RecentPostsPen {
44 entries: Vec<RecentPostsPenEntry>,
45}
46
47impl Default for RecentPostsPen {
48 fn default() -> Self {
49 Self::new()
50 }
51}
52
53impl RecentPostsPen {
54 pub fn new() -> Self {
55 Self { entries: Vec::new() }
56 }
57
58 pub fn add_all(&mut self, bucket_locations_and_post_ids: &[(BucketLocation, Id)], encoded_post_bytes: Bytes, time_millis: TimeMillis) {
61 for (bucket_location, post_id) in bucket_locations_and_post_ids {
62 self.entries.push(RecentPostsPenEntry {
63 bucket_location: bucket_location.clone(),
64 post_id: *post_id,
65 encoded_post_bytes: encoded_post_bytes.clone(),
66 time_millis,
67 });
68 }
69 }
70
71 pub fn get_matching_posts(
75 &mut self,
76 bucket_type: BucketType,
77 base_id: &Id,
78 already_seen_ids: &HashSet<Id>,
79 current_time: TimeMillis,
80 ) -> Vec<(BucketLocation, Bytes, Id)> {
81 let cutoff = current_time - RECENT_POSTS_PEN_TTL;
83 self.entries.retain(|entry| entry.time_millis >= cutoff);
84
85 let mut matching_posts: Vec<(BucketLocation, Bytes, Id)> = Vec::new();
86
87 for entry in &self.entries {
88 if entry.bucket_location.bucket_type != bucket_type || entry.bucket_location.base_id != *base_id {
89 continue;
90 }
91 if already_seen_ids.contains(&entry.post_id) {
92 continue;
93 }
94
95 matching_posts.push((entry.bucket_location.clone(), entry.encoded_post_bytes.clone(), entry.post_id));
96 }
97
98 matching_posts
99 }
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105 use crate::tools::time::MILLIS_IN_MINUTE;
106
107 fn make_entry(bucket_type: BucketType, base_id: Id, post_id: Id, time: TimeMillis) -> (BucketLocation, Id) {
108 let bucket_location = BucketLocation::new(bucket_type, base_id, MILLIS_IN_MINUTE, time).unwrap();
109 (bucket_location, post_id)
110 }
111
112 #[test]
113 fn test_matching_by_bucket_type_and_base_id() {
114 let mut pen = RecentPostsPen::new();
115 let base_id = Id::random();
116 let other_base_id = Id::random();
117 let post_id = Id::random();
118 let time = TimeMillis::from_epoch_offset_str("1M").unwrap();
119
120 let entries = vec![
121 make_entry(BucketType::User, base_id, post_id, time),
122 make_entry(BucketType::Hashtag, other_base_id, post_id, time),
123 ];
124 pen.add_all(&entries, Bytes::from_static(b"test post"), time);
125
126 let already_seen = HashSet::new();
127
128 let result = pen.get_matching_posts(BucketType::User, &base_id, &already_seen, time);
130 assert_eq!(result.len(), 1);
131
132 let result = pen.get_matching_posts(BucketType::Hashtag, &other_base_id, &already_seen, time);
134 assert_eq!(result.len(), 1);
135
136 let result = pen.get_matching_posts(BucketType::User, &other_base_id, &already_seen, time);
138 assert_eq!(result.len(), 0);
139
140 let result = pen.get_matching_posts(BucketType::Hashtag, &base_id, &already_seen, time);
142 assert_eq!(result.len(), 0);
143 }
144
145 #[test]
146 fn test_ttl_expiration() {
147 let mut pen = RecentPostsPen::new();
148 let base_id = Id::random();
149 let post_id = Id::random();
150 let time = TimeMillis::from_epoch_offset_str("1M").unwrap();
151
152 pen.add_all(&[make_entry(BucketType::User, base_id, post_id, time)], Bytes::from_static(b"post"), time);
153
154 let already_seen = HashSet::new();
155
156 let within_ttl = time + MILLIS_IN_MINUTE.const_mul(9);
158 let result = pen.get_matching_posts(BucketType::User, &base_id, &already_seen, within_ttl);
159 assert_eq!(result.len(), 1);
160
161 let past_ttl = time + MILLIS_IN_MINUTE.const_mul(11);
163 let result = pen.get_matching_posts(BucketType::User, &base_id, &already_seen, past_ttl);
164 assert_eq!(result.len(), 0);
165 }
166
167 #[test]
168 fn test_multiple_tokens_same_post_returns_all() {
169 let mut pen = RecentPostsPen::new();
170 let base_id = Id::random();
171 let post_id = Id::random();
172 let time = TimeMillis::from_epoch_offset_str("1M").unwrap();
173
174 let entries = vec![
177 make_entry(BucketType::User, base_id, post_id, time),
178 make_entry(BucketType::User, base_id, post_id, time),
179 make_entry(BucketType::User, base_id, post_id, time),
180 ];
181 pen.add_all(&entries, Bytes::from_static(b"post"), time);
182
183 let already_seen = HashSet::new();
184 let result = pen.get_matching_posts(BucketType::User, &base_id, &already_seen, time);
185 assert_eq!(result.len(), 3);
186 }
187
188 #[test]
189 fn test_already_seen_filtering() {
190 let mut pen = RecentPostsPen::new();
191 let base_id = Id::random();
192 let post_id = Id::random();
193 let time = TimeMillis::from_epoch_offset_str("1M").unwrap();
194
195 pen.add_all(&[make_entry(BucketType::User, base_id, post_id, time)], Bytes::from_static(b"post"), time);
196
197 let mut already_seen = HashSet::new();
198 already_seen.insert(post_id);
199
200 let result = pen.get_matching_posts(BucketType::User, &base_id, &already_seen, time);
201 assert_eq!(result.len(), 0);
202 }
203
204 #[test]
205 fn test_single_post_multiple_timelines() {
206 let mut pen = RecentPostsPen::new();
207 let user_id = Id::random();
208 let hashtag_id = Id::random();
209 let mention_id = Id::random();
210 let post_id = Id::random();
211 let time = TimeMillis::from_epoch_offset_str("1M").unwrap();
212
213 let entries = vec![
214 make_entry(BucketType::User, user_id, post_id, time),
215 make_entry(BucketType::Hashtag, hashtag_id, post_id, time),
216 make_entry(BucketType::Mention, mention_id, post_id, time),
217 ];
218 pen.add_all(&entries, Bytes::from_static(b"post"), time);
219
220 let already_seen = HashSet::new();
221
222 assert_eq!(pen.get_matching_posts(BucketType::User, &user_id, &already_seen, time).len(), 1);
224 assert_eq!(pen.get_matching_posts(BucketType::Hashtag, &hashtag_id, &already_seen, time).len(), 1);
225 assert_eq!(pen.get_matching_posts(BucketType::Mention, &mention_id, &already_seen, time).len(), 1);
226 }
227}