use std::collections::{HashMap, HashSet};
use std::sync::{Arc, RwLock};
use crate::ids::{ContentId, ContentIdPrefix, GovernanceLogId, PostId};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Shown {
pub read: String,
pub bytes: u64,
pub raw: u64,
pub post: Option<PostId>,
pub turn: u64,
}
#[derive(Debug, Default)]
struct Inner {
shown: HashMap<ContentId, Shown>,
pending: Vec<(ContentId, Shown)>,
read: String,
legends: HashSet<&'static str>,
pending_legends: Vec<&'static str>,
summaries: HashMap<(GovernanceLogId, u64), Shown>,
pending_summaries: Vec<((GovernanceLogId, u64), Shown)>,
summaries_at: HashMap<(GovernanceLogId, &'static str), Shown>,
pending_summaries_at: Vec<((GovernanceLogId, &'static str), Shown)>,
parts: HashMap<String, Shown>,
pending_parts: Vec<(String, Shown)>,
constitution: Option<u64>,
cues: Cues,
turn: u64,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct Cues {
pub links: bool,
pub full_reads_left: Option<u64>,
}
pub fn text_key(text: &str) -> u64 {
use std::hash::{Hash, Hasher};
let mut h = std::collections::hash_map::DefaultHasher::new();
text.trim().hash(&mut h);
h.finish()
}
pub fn summary_paragraphs(text: &str) -> Vec<String> {
text.split("\n\n")
.map(str::trim)
.filter(|p| !p.is_empty())
.map(str::to_string)
.collect()
}
impl Shown {
pub fn joined<'a>(shown: impl IntoIterator<Item = &'a Shown>) -> Shown {
let mut reads: Vec<&str> = Vec::new();
let (mut bytes, mut turn) = (0, 0);
for s in shown {
if !reads.contains(&s.read.as_str()) {
reads.push(&s.read);
}
bytes += s.bytes;
turn = turn.max(s.turn);
}
Shown {
read: reads.join(" and "),
bytes,
raw: bytes,
post: None,
turn,
}
}
}
pub fn summary_text(text: &str) -> String {
const OPEN: &str = "Read this first: attachment=\"";
let Some(at) = text.find(OPEN) else {
return text.trim().to_string();
};
let Some(end) = text[at + OPEN.len()..].find("\".") else {
return text.trim().to_string();
};
let end = at + OPEN.len() + end + 2;
format!("{}{}", text[..at].trim_end(), &text[end..])
.trim()
.to_string()
}
#[derive(Debug, Clone, Default)]
pub struct ShownOnce(Arc<RwLock<Inner>>);
impl ShownOnce {
pub fn begin(&self, read: impl Into<String>) {
let mut inner = self.0.write().expect("shown-once lock");
inner.pending.clear();
inner.pending_legends.clear();
inner.pending_summaries.clear();
inner.pending_summaries_at.clear();
inner.pending_parts.clear();
inner.read = read.into();
}
pub fn document_key(text: &str) -> String {
format!("document {}", text_key(text))
}
pub fn hold_constitution(&self, text: &str) {
let mut inner = self.0.write().expect("shown-once lock");
let key = text_key(text);
inner.constitution = Some(key);
inner.parts.insert(
Self::document_key(text),
Shown {
read: "your system prompt".into(),
bytes: text.trim().len() as u64,
raw: text.trim().len() as u64,
post: None,
turn: 0,
},
);
}
pub fn holds_constitution(&self, text: &str) -> Option<bool> {
let inner = self.0.read().expect("shown-once lock");
inner.constitution.map(|k| k == text_key(text))
}
pub fn earlier_part(&self, link: &str) -> Option<Shown> {
let inner = self.0.read().expect("shown-once lock");
inner.parts.get(link).cloned().or_else(|| {
inner
.pending_parts
.iter()
.find(|(p, _)| p == link)
.map(|(_, s)| s.clone())
})
}
pub fn mark_part(&self, link: String, bytes: usize) {
let mut inner = self.0.write().expect("shown-once lock");
let read = inner.read.clone();
let turn = inner.turn;
inner.pending_parts.push((
link,
Shown {
read,
bytes: bytes as u64,
raw: bytes as u64,
post: None,
turn,
},
));
}
pub fn next_turn(&self) {
self.0.write().expect("shown-once lock").turn += 1;
}
pub fn turn(&self) -> u64 {
self.0.read().expect("shown-once lock").turn
}
pub fn set_cues(&self, cues: Cues) {
self.0.write().expect("shown-once lock").cues = cues;
}
pub fn cues(&self) -> Cues {
self.0.read().expect("shown-once lock").cues
}
fn paragraph_shown(
&self,
id: &GovernanceLogId,
para: &str,
) -> Option<Shown> {
let key = (id.clone(), text_key(&summary_text(para)));
let inner = self.0.read().expect("shown-once lock");
inner
.pending_summaries
.iter()
.rev()
.find(|(k, _)| *k == key)
.map(|(_, s)| s.clone())
.or_else(|| inner.summaries.get(&key).cloned())
}
pub fn summary_seen(
&self,
id: &GovernanceLogId,
text: &str,
) -> Vec<(String, Option<Shown>)> {
summary_paragraphs(text)
.into_iter()
.map(|p| {
let shown = self.paragraph_shown(id, &p);
(p, shown)
})
.collect()
}
pub fn earlier_summary(
&self,
id: &GovernanceLogId,
text: Option<&str>,
) -> Option<Shown> {
match text {
Some(text) => {
let seen = self.summary_seen(id, text);
if seen.is_empty() || seen.iter().any(|(_, s)| s.is_none()) {
return None;
}
Some(Shown::joined(seen.iter().filter_map(|(_, s)| s.as_ref())))
}
None => {
let inner = self.0.read().expect("shown-once lock");
inner
.pending_summaries
.iter()
.find(|((k, _), _)| k == id)
.map(|(_, s)| s.clone())
.or_else(|| {
inner
.summaries
.iter()
.find(|((k, _), _)| k == id)
.map(|(_, s)| s.clone())
})
}
}
}
pub fn earlier_summary_at(
&self,
id: &GovernanceLogId,
version: &'static str,
) -> Option<Shown> {
let key = (id.clone(), version);
let inner = self.0.read().expect("shown-once lock");
inner
.pending_summaries_at
.iter()
.find(|(k, _)| *k == key)
.map(|(_, s)| s.clone())
.or_else(|| inner.summaries_at.get(&key).cloned())
}
pub fn mark_summary(
&self,
id: GovernanceLogId,
version: Option<&'static str>,
text: &str,
) {
let paras = summary_paragraphs(text);
self.mark_paragraphs(id, version, paras.iter().map(String::as_str));
}
pub fn mark_paragraphs<'a>(
&self,
id: GovernanceLogId,
version: Option<&'static str>,
paras: impl IntoIterator<Item = &'a str>,
) {
let mut inner = self.0.write().expect("shown-once lock");
let read = inner.read.clone();
let turn = inner.turn;
let shown = |bytes: usize| Shown {
read: read.clone(),
bytes: bytes as u64,
raw: bytes as u64,
post: None,
turn,
};
let mut total = 0;
for p in paras {
let p = summary_text(p);
total += p.len();
inner
.pending_summaries
.push(((id.clone(), text_key(&p)), shown(p.len())));
}
if total > 0 {
inner
.pending_summaries_at
.push(((id, version.unwrap_or("latest")), shown(total)));
}
}
pub fn legend_shown(&self, legend: &'static str) -> bool {
let inner = self.0.read().expect("shown-once lock");
inner.legends.contains(legend)
|| inner.pending_legends.contains(&legend)
}
pub fn legend(&self, legend: &'static str) -> bool {
let mut inner = self.0.write().expect("shown-once lock");
if inner.legends.contains(legend)
|| inner.pending_legends.contains(&legend)
{
return false;
}
inner.pending_legends.push(legend);
true
}
pub fn read_of(id: impl std::fmt::Display) -> String {
format!("your read of {id}")
}
pub fn read_of_entry(
entry: &crate::responses::GovernanceEntryResponse,
) -> String {
let mut read = Self::read_of(&entry.id);
if let Some(n) = entry.round {
read.push_str(&format!(" round {n}"));
} else if let Some(a) = &entry.attachment {
read.push_str(&format!(" {a:?}"));
}
if let Some(version) = entry.version_label() {
read.push_str(&format!(" ({version})"));
}
read
}
pub fn read_of_content(id: ContentId) -> String {
Self::read_of(ContentIdPrefix::from(id))
}
pub fn earlier(&self, id: impl Into<ContentId>) -> Option<Shown> {
let id = id.into();
let inner = self.0.read().expect("shown-once lock");
inner.shown.get(&id).cloned().or_else(|| {
inner
.pending
.iter()
.find(|(p, _)| *p == id)
.map(|(_, s)| s.clone())
})
}
pub fn contains(&self, id: impl Into<ContentId>) -> bool {
self.earlier(id).is_some()
}
pub fn mark(
&self,
id: impl Into<ContentId>,
bytes: usize,
raw: usize,
post: Option<PostId>,
) {
let mut inner = self.0.write().expect("shown-once lock");
let read = inner.read.clone();
let turn = inner.turn;
inner.pending.push((
id.into(),
Shown {
read,
bytes: bytes as u64,
raw: raw as u64,
post,
turn,
},
));
}
pub fn commit(&self) {
let mut inner = self.0.write().expect("shown-once lock");
let pending = std::mem::take(&mut inner.pending);
for (id, shown) in pending {
inner.shown.entry(id).or_insert(shown);
}
let legends = std::mem::take(&mut inner.pending_legends);
inner.legends.extend(legends);
let summaries = std::mem::take(&mut inner.pending_summaries);
for (id, shown) in summaries {
inner.summaries.insert(id, shown);
}
let summaries = std::mem::take(&mut inner.pending_summaries_at);
for (id, shown) in summaries {
inner.summaries_at.insert(id, shown);
}
let parts = std::mem::take(&mut inner.pending_parts);
for (link, shown) in parts {
inner.parts.entry(link).or_insert(shown);
}
}
pub fn discard(&self) {
let mut inner = self.0.write().expect("shown-once lock");
inner.pending.clear();
inner.pending_legends.clear();
inner.pending_summaries.clear();
inner.pending_summaries_at.clear();
inner.pending_parts.clear();
}
pub fn comments_of(&self, post: PostId) -> (u64, u64) {
let inner = self.0.read().expect("shown-once lock");
inner
.shown
.values()
.filter(|s| s.post == Some(post))
.fold((0, 0), |(n, b), s| (n + 1, b + s.raw))
}
}
#[cfg(test)]
mod tests {
use super::*;
use uuid::Uuid;
fn id(n: u128) -> ContentId {
ContentId::from(Uuid::from_u128(n))
}
#[test]
fn marked_items_are_shown_once_committed() {
let once = ShownOnce::default();
once.begin("your read of 00000000");
once.mark(id(1), 100, 100, None);
assert!(once.contains(id(1)));
once.commit();
let shown = once.earlier(id(1)).unwrap();
assert_eq!(shown.read, "your read of 00000000");
assert_eq!(shown.bytes, 100);
}
#[test]
fn a_discarded_result_marks_nothing() {
let once = ShownOnce::default();
once.begin("a");
once.mark(id(1), 100, 100, None);
once.discard();
assert!(!once.contains(id(1)));
once.begin("b");
once.mark(id(2), 1, 1, None);
once.begin("c");
once.commit();
assert!(!once.contains(id(2)));
}
#[test]
fn a_legend_is_shown_once() {
let once = ShownOnce::default();
once.begin("a");
assert!(once.legend("x"));
assert!(!once.legend("x"), "not twice in one result");
once.discard();
once.begin("b");
assert!(once.legend("x"));
once.commit();
once.begin("c");
assert!(!once.legend("x"));
}
#[test]
fn the_first_showing_is_kept() {
let once = ShownOnce::default();
once.begin("first");
once.mark(id(1), 10, 10, None);
once.commit();
once.begin("second");
once.mark(id(1), 20, 20, None);
once.commit();
assert_eq!(once.earlier(id(1)).unwrap().read, "first");
}
#[test]
fn a_posts_comments_are_counted() {
let once = ShownOnce::default();
let post = PostId::from(Uuid::from_u128(9));
once.begin("r");
once.mark(id(1), 10, 10, Some(post));
once.mark(id(2), 30, 30, Some(post));
once.mark(id(3), 50, 50, None);
once.commit();
assert_eq!(once.comments_of(post), (2, 40));
}
}