use std::collections::BTreeMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
#[derive(Clone, Default)]
pub struct ExtensionStatusMailbox {
entries: Arc<Mutex<BTreeMap<String, String>>>,
revision: Arc<AtomicU64>,
}
impl ExtensionStatusMailbox {
pub fn new() -> Self {
Self::default()
}
pub fn set(&self, key: &str, value: &str) -> bool {
let key = key.trim();
if key.is_empty() {
return false;
}
let Ok(mut entries) = self.entries.lock() else {
return false;
};
let changed = if value.trim().is_empty() {
entries.remove(key).is_some()
} else {
entries.insert(key.to_string(), value.to_string()) != Some(value.to_string())
};
if changed {
self.revision.fetch_add(1, Ordering::SeqCst);
}
changed
}
pub fn handle(&self, args: serde_json::Value) -> Result<serde_json::Value, String> {
let key = args
.get("key")
.and_then(serde_json::Value::as_str)
.ok_or("set_status requires a string `key`")?;
let value = args
.get("value")
.and_then(serde_json::Value::as_str)
.unwrap_or("");
let changed = self.set(key, value);
Ok(serde_json::json!({ "ok": true, "changed": changed }))
}
pub fn revision(&self) -> u64 {
self.revision.load(Ordering::SeqCst)
}
pub fn text(&self) -> String {
self.entries
.lock()
.map(|entries| {
entries
.values()
.filter(|value| !value.trim().is_empty())
.cloned()
.collect::<Vec<_>>()
.join(" ")
})
.unwrap_or_default()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn set_and_clear_round_trip() {
let mailbox = ExtensionStatusMailbox::new();
assert_eq!(mailbox.text(), "");
assert!(mailbox.set("langfuse", "langfuse ✓"));
assert_eq!(mailbox.text(), "langfuse ✓");
assert!(!mailbox.set("langfuse", "langfuse ✓"));
assert!(mailbox.set("langfuse", "langfuse ✓ (trace sent)"));
assert_eq!(mailbox.text(), "langfuse ✓ (trace sent)");
let before = mailbox.revision();
assert!(mailbox.set("langfuse", ""));
assert_eq!(mailbox.text(), "");
assert!(mailbox.revision() > before);
assert!(!mailbox.set("langfuse", ""));
assert!(!mailbox.set(" ", "ignored"));
}
#[test]
fn multiple_extensions_render_in_key_order() {
let mailbox = ExtensionStatusMailbox::new();
mailbox.set("zeta", "z");
mailbox.set("alpha", "a");
assert_eq!(mailbox.text(), "a z");
}
#[test]
fn handle_parses_the_action_payload() {
let mailbox = ExtensionStatusMailbox::new();
let out = mailbox
.handle(serde_json::json!({"key": "langfuse", "value": "✓"}))
.expect("ok");
assert_eq!(out["ok"], serde_json::json!(true));
assert_eq!(mailbox.text(), "✓");
assert!(mailbox.handle(serde_json::json!({"value": "x"})).is_err());
}
}