use std::collections::BTreeMap;
use std::sync::{Arc, Mutex, PoisonError};
use liminal_protocol::wire::ConversationId;
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
pub struct UnloadableConversation {
pub conversation_id: ConversationId,
pub class: &'static str,
pub reason: String,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
pub struct UnloadableConversationsStatus {
pub participant_installed: bool,
pub count: usize,
pub conversations: Vec<UnloadableConversation>,
}
#[derive(Clone, Debug, Default)]
pub struct UnloadableConversationRecord {
entries: Arc<Mutex<BTreeMap<ConversationId, UnloadableConversation>>>,
}
impl UnloadableConversationRecord {
#[must_use]
pub fn record(&self, entry: UnloadableConversation) -> bool {
self.entries.lock().is_ok_and(|mut entries| {
entries.insert(entry.conversation_id, entry);
true
})
}
#[must_use]
pub fn retire(&self, conversation_id: ConversationId) -> bool {
self.entries.lock().is_ok_and(|mut entries| {
entries.remove(&conversation_id);
true
})
}
#[must_use]
pub fn snapshot(&self) -> Vec<UnloadableConversation> {
self.entries
.lock()
.unwrap_or_else(PoisonError::into_inner)
.values()
.cloned()
.collect()
}
}
#[derive(Clone, Debug, Default)]
pub struct SharedUnloadableConversations {
record: Arc<Mutex<Option<UnloadableConversationRecord>>>,
}
impl SharedUnloadableConversations {
pub fn install(&self, record: UnloadableConversationRecord) {
*self.record.lock().unwrap_or_else(PoisonError::into_inner) = Some(record);
}
#[must_use]
pub fn status(&self) -> UnloadableConversationsStatus {
let record = self
.record
.lock()
.unwrap_or_else(PoisonError::into_inner)
.clone();
record.map_or_else(
|| UnloadableConversationsStatus {
participant_installed: false,
count: 0,
conversations: Vec::new(),
},
|record| {
let conversations = record.snapshot();
UnloadableConversationsStatus {
participant_installed: true,
count: conversations.len(),
conversations,
}
},
)
}
}
#[cfg(test)]
mod tests {
use super::{
SharedUnloadableConversations, UnloadableConversation, UnloadableConversationRecord,
};
fn refusal(conversation_id: u64) -> UnloadableConversation {
UnloadableConversation {
conversation_id,
class: "internal",
reason: "expected value at line 1 column 1".to_owned(),
}
}
#[test]
fn an_uninstalled_surface_reports_that_it_is_looking_at_nothing() {
let status = SharedUnloadableConversations::default().status();
assert!(!status.participant_installed);
assert_eq!(status.count, 0);
assert!(status.conversations.is_empty());
}
#[test]
fn an_installed_but_empty_record_is_a_different_zero() {
let surface = SharedUnloadableConversations::default();
surface.install(UnloadableConversationRecord::default());
let status = surface.status();
assert!(status.participant_installed);
assert_eq!(status.count, 0);
}
#[test]
fn the_surface_follows_the_record_it_was_given() {
let record = UnloadableConversationRecord::default();
let surface = SharedUnloadableConversations::default();
surface.install(record.clone());
assert!(record.record(refusal(7_201)));
assert!(record.record(refusal(7_100)));
let status = surface.status();
assert!(status.participant_installed);
assert_eq!(status.count, 2);
assert_eq!(
status
.conversations
.iter()
.map(|entry| entry.conversation_id)
.collect::<Vec<_>>(),
vec![7_100, 7_201]
);
assert_eq!(status.conversations[1].class, "internal");
assert!(record.retire(7_100));
let status = surface.status();
assert_eq!(status.count, 1);
assert_eq!(status.conversations[0].conversation_id, 7_201);
}
}