use std::collections::{BTreeMap, HashMap};
use super::record::{ManagedSessionId, SessionRecord};
#[derive(Debug, Clone)]
struct SlotMeta {
id: ManagedSessionId,
source_id: Option<String>,
}
#[derive(Debug)]
pub struct SlotRegistry {
id_to_slot: HashMap<ManagedSessionId, u32>,
slots: BTreeMap<u32, SlotMeta>,
}
impl SlotRegistry {
pub fn new() -> Self {
Self {
id_to_slot: HashMap::new(),
slots: BTreeMap::new(),
}
}
fn lowest_free(&self) -> u32 {
let mut candidate = 1;
for &slot in self.slots.keys() {
if slot > candidate {
break;
}
candidate = slot + 1;
}
candidate
}
pub fn observe(&mut self, id: ManagedSessionId, source_id: Option<&str>) -> u32 {
if let Some(&slot) = self.id_to_slot.get(&id) {
if let Some(meta) = self.slots.get_mut(&slot) {
meta.source_id = source_id.map(str::to_owned);
}
return slot;
}
let slot = self.lowest_free();
self.id_to_slot.insert(id, slot);
self.slots.insert(
slot,
SlotMeta {
id,
source_id: source_id.map(str::to_owned),
},
);
slot
}
pub fn release(&mut self, id: &ManagedSessionId) -> Option<u32> {
let slot = self.id_to_slot.remove(id)?;
self.slots.remove(&slot);
Some(slot)
}
pub fn assigned_slots(&self) -> Vec<u32> {
self.slots.keys().copied().collect()
}
pub fn max_slot(&self) -> u32 {
self.slots.keys().next_back().copied().unwrap_or(0)
}
pub fn id_at(&self, slot: u32) -> Option<ManagedSessionId> {
self.slots.get(&slot).map(|m| m.id)
}
pub fn source_id_at(&self, slot: u32) -> Option<&str> {
self.slots.get(&slot).and_then(|m| m.source_id.as_deref())
}
}
impl Default for SlotRegistry {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct NumberedSlot {
pub slot: u32,
pub record: Option<SessionRecord>,
pub source_id: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
use uuid::Uuid;
fn id() -> ManagedSessionId {
ManagedSessionId::from(Uuid::new_v4())
}
#[test]
fn slot_registry_assigns_stable_increasing_numbers() {
let mut reg = SlotRegistry::new();
let a = id();
let b = id();
let slot_a = reg.observe(a, Some("owner/repo"));
let slot_b = reg.observe(b, None);
assert_eq!(slot_a, 1);
assert_eq!(slot_b, 2);
assert_eq!(reg.observe(a, Some("owner/repo")), 1);
assert_eq!(reg.max_slot(), 2);
}
#[test]
fn slot_registry_never_reuses_a_number() {
let mut reg = SlotRegistry::new();
let a = id();
let b = id();
let c = id();
assert_eq!(reg.observe(a, None), 1);
assert_eq!(reg.observe(b, None), 2);
assert_eq!(reg.observe(c, None), 3);
assert_eq!(reg.id_at(1), Some(a));
}
#[test]
fn slot_registry_refreshes_source_id_while_live() {
let mut reg = SlotRegistry::new();
let a = id();
reg.observe(a, None);
assert_eq!(reg.source_id_at(1), None);
reg.observe(a, Some("owner/repo"));
assert_eq!(reg.source_id_at(1), Some("owner/repo"));
}
#[test]
fn slot_registry_release_frees_the_slot_for_reuse() {
let mut reg = SlotRegistry::new();
let (a, b, c) = (id(), id(), id());
assert_eq!(reg.observe(a, None), 1);
assert_eq!(reg.observe(b, None), 2);
assert_eq!(reg.observe(c, None), 3);
assert_eq!(reg.release(&b), Some(2));
assert_eq!(reg.assigned_slots(), vec![1, 3]);
assert_eq!(reg.id_at(2), None, "released slot points at nothing");
assert_eq!(reg.max_slot(), 3);
let d = id();
assert_eq!(reg.observe(d, None), 2);
assert_eq!(reg.observe(a, None), 1);
assert_eq!(reg.observe(c, None), 3);
let mut held = vec![
reg.observe(a, None),
reg.observe(c, None),
reg.observe(d, None),
];
held.sort_unstable();
held.dedup();
assert_eq!(held.len(), 3, "no two live sessions share a NUM");
}
#[test]
fn slot_registry_release_of_unknown_id_is_a_noop() {
let mut reg = SlotRegistry::new();
let a = id();
reg.observe(a, None);
assert_eq!(reg.release(&id()), None);
assert_eq!(reg.assigned_slots(), vec![1], "held set untouched");
}
#[test]
fn slot_registry_unassigned_slot_and_max_are_empty() {
let reg = SlotRegistry::new();
assert_eq!(reg.max_slot(), 0);
assert_eq!(reg.id_at(1), None);
assert_eq!(reg.source_id_at(1), None);
}
}