Skip to main content

actionqueue_actor/
department.rs

1//! Actor-to-department grouping with reverse index.
2
3use std::collections::{HashMap, HashSet};
4
5use actionqueue_core::ids::{ActorId, DepartmentId};
6
7/// Groups actors by department for department-targeted task routing.
8///
9/// The dispatch loop uses this to route tasks that target a specific
10/// department (e.g., `"engineering"`) to only the actors in that group.
11/// First-come-first-serve is enforced via lease atomicity at the WAL level.
12#[derive(Default)]
13pub struct DepartmentRegistry {
14    departments: HashMap<DepartmentId, HashSet<ActorId>>,
15    /// Reverse index: actor_id → their department.
16    actor_departments: HashMap<ActorId, DepartmentId>,
17}
18
19impl DepartmentRegistry {
20    /// Creates an empty registry.
21    pub fn new() -> Self {
22        Self::default()
23    }
24
25    /// Assigns an actor to a department. If the actor was in another
26    /// department, they are removed from the previous one.
27    pub fn assign(&mut self, actor_id: ActorId, dept: DepartmentId) {
28        // Remove from previous department.
29        if let Some(prev_dept) = self.actor_departments.remove(&actor_id) {
30            if let Some(members) = self.departments.get_mut(&prev_dept) {
31                members.remove(&actor_id);
32            }
33        }
34        self.departments.entry(dept.clone()).or_default().insert(actor_id);
35        self.actor_departments.insert(actor_id, dept);
36    }
37
38    /// Removes an actor from their department.
39    pub fn remove(&mut self, actor_id: ActorId) {
40        if let Some(dept) = self.actor_departments.remove(&actor_id) {
41            if let Some(members) = self.departments.get_mut(&dept) {
42                members.remove(&actor_id);
43            }
44        }
45    }
46
47    /// Returns the set of actor IDs in the given department.
48    pub fn actors_in_department(&self, dept: &DepartmentId) -> &HashSet<ActorId> {
49        self.departments.get(dept).map_or(&EMPTY_SET, |s| s)
50    }
51
52    /// Returns the department of the given actor, if assigned.
53    pub fn department_of(&self, actor_id: ActorId) -> Option<&DepartmentId> {
54        self.actor_departments.get(&actor_id)
55    }
56}
57
58static EMPTY_SET: std::sync::LazyLock<HashSet<ActorId>> = std::sync::LazyLock::new(HashSet::new);
59
60#[cfg(test)]
61mod tests {
62    use actionqueue_core::ids::{ActorId, DepartmentId};
63
64    use super::DepartmentRegistry;
65
66    fn dept(name: &str) -> DepartmentId {
67        DepartmentId::new(name).unwrap()
68    }
69
70    #[test]
71    fn assign_and_lookup() {
72        let mut reg = DepartmentRegistry::new();
73        let actor = ActorId::new();
74        let engineering = dept("engineering");
75        reg.assign(actor, engineering.clone());
76        assert!(reg.actors_in_department(&engineering).contains(&actor));
77        assert_eq!(reg.department_of(actor), Some(&engineering));
78    }
79
80    #[test]
81    fn reassign_moves_actor() {
82        let mut reg = DepartmentRegistry::new();
83        let actor = ActorId::new();
84        let eng = dept("engineering");
85        let ops = dept("ops");
86        reg.assign(actor, eng.clone());
87        reg.assign(actor, ops.clone());
88        assert!(!reg.actors_in_department(&eng).contains(&actor));
89        assert!(reg.actors_in_department(&ops).contains(&actor));
90        assert_eq!(reg.department_of(actor), Some(&ops));
91    }
92
93    #[test]
94    fn remove_clears_actor() {
95        let mut reg = DepartmentRegistry::new();
96        let actor = ActorId::new();
97        let eng = dept("engineering");
98        reg.assign(actor, eng.clone());
99        reg.remove(actor);
100        assert!(!reg.actors_in_department(&eng).contains(&actor));
101        assert!(reg.department_of(actor).is_none());
102    }
103
104    #[test]
105    fn empty_department_returns_empty_set() {
106        let reg = DepartmentRegistry::new();
107        let eng = dept("engineering");
108        assert!(reg.actors_in_department(&eng).is_empty());
109    }
110}