1use std::collections::HashSet;
22
23use crate::plan::{ElementId, OverlayOps, PlanOp, PlannedOp};
24use crate::scope::ScopeSet;
25
26#[must_use]
38pub fn filter<'a>(ops: impl IntoIterator<Item = &'a PlannedOp>, see: ScopeSet) -> Vec<PlanOp> {
39 ops.into_iter()
40 .filter(|planned| see.sees(planned.scope))
41 .map(|planned| planned.op.clone())
42 .collect()
43}
44
45#[must_use]
62pub fn splice(shared: Vec<PlanOp>, private: OverlayOps) -> Vec<PlanOp> {
63 let boundary = shared
64 .iter()
65 .position(PlanOp::is_removal)
66 .unwrap_or(shared.len());
67
68 let mut composed =
69 Vec::with_capacity(shared.len() + private.additions.len() + private.removals.len());
70 let mut shared = shared;
71 let tail = shared.split_off(boundary);
72 composed.append(&mut shared);
73 composed.extend(private.removals);
74 composed.extend(private.additions);
75 composed.extend(tail);
76 composed
77}
78
79pub fn collapse(ops: &mut Vec<PlanOp>) {
98 let added: HashSet<ElementId> = ops.iter().filter_map(PlanOp::added).collect();
99 ops.retain(|op| !matches!(op.removed(), Some(id) if added.contains(&id)));
100}
101
102#[cfg(test)]
103mod tests {
104 #![allow(clippy::expect_used)]
105
106 use std::collections::{BTreeMap, BTreeSet};
107
108 use super::*;
109 use crate::ids::{ChannelId, ChannelKey, ConnectionId, Occupant, SessionId};
110 use crate::plan::{plan, plan_elements};
111 use crate::scope::Scope;
112 use crate::view::{Channel, Overlay, ShardView, User, UserFlags};
113
114 fn scope(segments: &[u32]) -> Scope {
115 let mut scope = Scope::ROOT;
116 for segment in segments {
117 scope = scope.child(*segment).expect("within MAX_DEPTH");
118 }
119 scope
120 }
121
122 fn channel(id: u32, parent: u32, scope: Scope) -> Channel {
123 Channel {
124 key: ChannelKey(u64::from(id)),
125 id: ChannelId(id),
126 parent: ChannelId(parent),
127 scope,
128 name: format!("channel-{id}"),
129 position: 0,
130 can_enter: true,
131 can_text: true,
132 links: BTreeSet::new(),
133 }
134 }
135
136 fn user(session: u32, channel: u32, scope: Scope) -> User {
137 User {
138 occupant: Occupant::Connection(ConnectionId(u64::from(session))),
139 session: SessionId(session),
140 channel: ChannelId(channel),
141 scope,
142 name: format!("user-{session}"),
143 flags: UserFlags::default(),
144 }
145 }
146
147 fn two_teams() -> ShardView {
149 let mut view = ShardView::empty();
150 view.channels
151 .insert(ChannelId::ROOT, channel(0, 0, Scope::ROOT));
152 view.channels
153 .insert(ChannelId(1), channel(1, 0, scope(&[7])));
154 view.channels
155 .insert(ChannelId(2), channel(2, 1, scope(&[7, 2])));
156 view.channels
157 .insert(ChannelId(3), channel(3, 1, scope(&[7, 3])));
158 view.users
159 .insert(SessionId(10), user(10, 2, scope(&[7, 2])));
160 view.users
161 .insert(SessionId(11), user(11, 3, scope(&[7, 3])));
162 view
163 }
164
165 fn sessions_touched(ops: &[PlanOp]) -> Vec<(&'static str, u32)> {
166 ops.iter()
167 .filter_map(|op| match op {
168 PlanOp::AddUser(user) => Some(("add", user.session.0)),
169 PlanOp::RemoveUser(session) => Some(("remove", session.0)),
170 PlanOp::MoveUser { session, .. } => Some(("move", session.0)),
171 _ => None,
172 })
173 .collect()
174 }
175
176 #[test]
177 fn a_scope_change_reaches_each_observer_as_the_half_that_concerns_it() {
178 let before = two_teams();
180 let mut after = before.clone();
181 after
182 .users
183 .insert(SessionId(11), user(11, 2, scope(&[7, 2])));
184 let ops = plan(&before, &after);
185
186 let team_three = ScopeSet::new(&[scope(&[7, 3])]).expect("one scope");
187 let team_two = ScopeSet::new(&[scope(&[7, 2])]).expect("one scope");
188 let spectator = ScopeSet::new(&[scope(&[7])]).expect("one scope");
189 let elsewhere = ScopeSet::new(&[scope(&[8])]).expect("one scope");
190
191 assert_eq!(
193 sessions_touched(&filter(&ops, team_three)),
194 vec![("remove", 11)]
195 );
196 assert_eq!(sessions_touched(&filter(&ops, team_two)), vec![("add", 11)]);
198 assert!(filter(&ops, elsewhere).is_empty());
200
201 let mut both = filter(&ops, spectator);
203 assert_eq!(
204 sessions_touched(&both),
205 vec![("add", 11), ("remove", 11)],
206 "the raw filter yields both halves"
207 );
208 collapse(&mut both);
209 assert_eq!(
210 sessions_touched(&both),
211 vec![("add", 11)],
212 "a full AddUser merges client-side, which is the move"
213 );
214 }
215
216 #[test]
217 fn a_move_within_one_scope_stays_a_move() {
218 let before = two_teams();
219 let mut after = before.clone();
220 after
222 .users
223 .insert(SessionId(10), user(10, 1, scope(&[7, 2])));
224
225 let ops = plan(&before, &after);
226 let team_two = ScopeSet::new(&[scope(&[7, 2])]).expect("one scope");
227 assert_eq!(
228 sessions_touched(&filter(&ops, team_two)),
229 vec![("move", 10)]
230 );
231 }
232
233 #[test]
234 fn filtering_preserves_order() {
235 let before = two_teams();
236 let mut after = before.clone();
237 after
238 .channels
239 .insert(ChannelId(4), channel(4, 2, scope(&[7, 2])));
240 after.users.remove(&SessionId(10));
241
242 let ops = plan(&before, &after);
243 let everything = ScopeSet::new(&[Scope::ROOT]).expect("one scope");
244 let filtered = filter(&ops, everything);
245
246 let unfiltered: Vec<&PlanOp> = ops.iter().map(|planned| &planned.op).collect();
247 let kept: Vec<&PlanOp> = filtered.iter().collect();
248 assert_eq!(unfiltered, kept, "an unrestricted filter is the identity");
249
250 let team_two = ScopeSet::new(&[scope(&[7, 2])]).expect("one scope");
252 let restricted = filter(&ops, team_two);
253 let mut remaining = restricted.iter();
254 for op in &ops {
255 if remaining.clone().next() == Some(&op.op) {
256 let _ = remaining.next();
257 }
258 }
259 assert_eq!(remaining.count(), 0, "filtering must not reorder");
260 }
261
262 #[test]
263 fn the_overlay_is_spliced_before_the_shared_removals() {
264 let shared = vec![
266 PlanOp::CreateChannel(channel(5, 0, Scope::ROOT)),
267 PlanOp::RemoveChannel(ChannelId(2)),
268 ];
269 let private = OverlayOps {
270 additions: vec![PlanOp::AddUser(user(99, 5, Scope::ROOT))],
271 removals: vec![PlanOp::RemoveUser(SessionId(98))],
272 };
273
274 let composed = splice(shared, private);
275 let shapes: Vec<&str> = composed
276 .iter()
277 .map(|op| match op {
278 PlanOp::CreateChannel(_) => "create-channel",
279 PlanOp::AddUser(_) => "add-user",
280 PlanOp::RemoveUser(_) => "remove-user",
281 PlanOp::RemoveChannel(_) => "remove-channel",
282 _ => "other",
283 })
284 .collect();
285
286 assert_eq!(
287 shapes,
288 vec![
289 "create-channel",
290 "remove-user",
291 "add-user",
292 "remove-channel"
293 ],
294 "the overlay's withdrawal must precede the shared channel removal"
295 );
296 }
297
298 #[test]
299 fn an_overlay_addition_may_target_a_channel_created_in_the_same_turn() {
300 let shared = vec![PlanOp::CreateChannel(channel(5, 0, Scope::ROOT))];
301 let private = OverlayOps {
302 additions: vec![PlanOp::AddUser(user(99, 5, Scope::ROOT))],
303 removals: Vec::new(),
304 };
305
306 let composed = splice(shared, private);
307 let create = composed
308 .iter()
309 .position(|op| matches!(op, PlanOp::CreateChannel(_)))
310 .expect("the creation is present");
311 let add = composed
312 .iter()
313 .position(|op| matches!(op, PlanOp::AddUser(_)))
314 .expect("the addition is present");
315 assert!(create < add);
316 }
317
318 #[test]
319 fn vanishing_and_unvanishing_both_collapse_to_a_single_addition() {
320 let admin = Occupant::Connection(ConnectionId(99));
321 let placed = User {
322 occupant: admin,
323 session: SessionId(99),
324 channel: ChannelId(2),
325 scope: scope(&[7, 2]),
326 name: "admin".to_owned(),
327 flags: UserFlags::default(),
328 };
329
330 let mut overlay_before = Overlay::default();
332 overlay_before.users.insert(SessionId(99), placed.clone());
333 let private = plan_elements(&overlay_before, &Overlay::default());
334 let mut ops = splice(vec![PlanOp::AddUser(placed.clone())], private);
335 collapse(&mut ops);
336 assert_eq!(sessions_touched(&ops), vec![("add", 99)]);
337
338 let mut overlay_after = Overlay::default();
340 overlay_after.users.insert(SessionId(99), placed);
341 let private = plan_elements(&Overlay::default(), &overlay_after);
342 let mut ops = splice(vec![PlanOp::RemoveUser(SessionId(99))], private);
343 collapse(&mut ops);
344 assert_eq!(sessions_touched(&ops), vec![("add", 99)]);
345 }
346
347 #[test]
348 fn a_removal_with_no_matching_addition_survives_collapse() {
349 let mut ops = vec![
350 PlanOp::RemoveUser(SessionId(10)),
351 PlanOp::RemoveChannel(ChannelId(2)),
352 ];
353 collapse(&mut ops);
354 assert_eq!(ops.len(), 2, "an element that is gone must stay gone");
355 }
356
357 #[test]
358 fn splicing_into_an_all_removal_plan_still_lands_before_them() {
359 let shared = vec![PlanOp::RemoveUser(SessionId(10))];
360 let private = OverlayOps {
361 additions: vec![PlanOp::AddUser(user(99, 2, Scope::ROOT))],
362 removals: Vec::new(),
363 };
364 let composed = splice(shared, private);
365 assert!(matches!(composed[0], PlanOp::AddUser(_)));
366 }
367
368 #[test]
369 fn an_empty_overlay_leaves_the_shared_plan_untouched() {
370 let shared = vec![
371 PlanOp::CreateChannel(channel(5, 0, Scope::ROOT)),
372 PlanOp::RemoveUser(SessionId(10)),
373 ];
374 let composed = splice(shared.clone(), OverlayOps::default());
375 assert_eq!(composed, shared);
376 }
377
378 #[test]
379 fn an_unobserved_delta_filters_to_nothing() {
380 let before = two_teams();
381 let mut after = before.clone();
382 after.users.remove(&SessionId(10));
383 let ops = plan(&before, &after);
384
385 assert!(filter(&ops, ScopeSet::NONE).is_empty());
386 assert!(BTreeMap::<u32, u32>::new().is_empty());
387 }
388}