use concurrent_queue::ConcurrentQueue;
pub use foundation_core::synca::mpp::Receiver;
use foundation_core::synca::mpp::TrackedBroadcaster;
use foundation_core::valtron::Stream;
use foundation_db::traits::DocumentStore;
use foundation_db::StorageResult;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use crate::types::{SessionId, SessionRecord};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum MessageEvent {
Appended {
id: String,
variant: &'static str,
},
Flushed { count: usize },
Error { msg: String },
}
impl MessageEvent {
fn variant_name(record: &SessionRecord) -> &'static str {
match record {
SessionRecord::Conversation { .. } => "conversation",
SessionRecord::WorkingMemory { .. } => "working_memory",
SessionRecord::Observation { .. } => "observation",
SessionRecord::Reflection { .. } => "reflection",
SessionRecord::FailedAction { .. } => "failed_action",
SessionRecord::Summary { .. } => "summary",
SessionRecord::Retracted { .. } => "retracted",
}
}
}
struct MessageInner<D> {
session_id: SessionId,
doc_store: D,
write_buffer: ConcurrentQueue<SessionRecord>,
broadcaster: TrackedBroadcaster<MessageEvent>,
flush_threshold: usize,
flush_pending: std::sync::atomic::AtomicBool,
}
impl<D: DocumentStore> MessageInner<D> {
fn flush(&self) -> StorageResult<usize> {
if self
.flush_pending
.swap(true, std::sync::atomic::Ordering::SeqCst)
{
return Ok(0); }
let mut count = 0;
while let Ok(record) = self.write_buffer.pop() {
let id = match &record {
SessionRecord::Conversation { message } => message.id().to_string(),
_ => foundation_compact::ids::new_scru128_string(),
};
match &record {
SessionRecord::Conversation { .. } => {
self.doc_store.append_with_id(
&self.session_id.to_string(),
&id,
record.clone(),
)?;
}
SessionRecord::WorkingMemory { .. }
| SessionRecord::Observation { .. }
| SessionRecord::Reflection { .. } => {
self.doc_store
.append_promotable(&self.session_id.to_string(), record.clone())?;
}
_ => {
self.doc_store
.append(&self.session_id.to_string(), record.clone())?;
}
}
count += 1;
}
self.flush_pending
.store(false, std::sync::atomic::Ordering::SeqCst);
if count > 0 {
self.broadcaster.broadcast(MessageEvent::Flushed { count });
}
Ok(count)
}
fn should_flush(&self) -> bool {
self.write_buffer.len() >= self.flush_threshold
}
}
pub struct MessageApi<D> {
inner: Arc<MessageInner<D>>,
}
impl<D> Clone for MessageApi<D> {
fn clone(&self) -> Self {
Self {
inner: self.inner.clone(),
}
}
}
impl<D> MessageApi<D> {
pub fn new(session_id: SessionId, doc_store: D) -> Self {
Self::with_config(session_id, doc_store, 50, 256)
}
pub fn with_config(
session_id: SessionId,
doc_store: D,
flush_threshold: usize,
subscriber_capacity: usize,
) -> Self {
Self {
inner: Arc::new(MessageInner {
session_id,
doc_store,
write_buffer: ConcurrentQueue::unbounded(),
broadcaster: TrackedBroadcaster::new(subscriber_capacity),
flush_threshold,
flush_pending: std::sync::atomic::AtomicBool::new(false),
}),
}
}
#[must_use]
pub fn subscribe(&self) -> Receiver<MessageEvent> {
self.inner.broadcaster.subscribe()
}
}
impl<D: DocumentStore> MessageApi<D> {
#[must_use]
pub fn append(&self, record: SessionRecord) -> String {
let id = match &record {
SessionRecord::Conversation { message } => message.id().to_string(),
_ => foundation_compact::ids::new_scru128_string(),
};
let variant = MessageEvent::variant_name(&record);
self.inner
.write_buffer
.push(record)
.expect("unbounded queue never fails");
self.inner.broadcaster.broadcast(MessageEvent::Appended {
id: id.clone(),
variant,
});
if self.inner.should_flush() {
let _ = self.inner.flush();
}
id
}
pub fn flush(&self) -> StorageResult<usize> {
self.inner.flush()
}
pub fn recent(&self, n: usize) -> StorageResult<Vec<SessionRecord>> {
let _ = self.inner.flush();
let docs = self
.inner
.doc_store
.scan_documents(&self.inner.session_id.to_string(), n)?;
docs.into_iter()
.map(|d| serde_json::from_str::<SessionRecord>(&d.content))
.collect::<Result<Vec<_>, _>>()
.map_err(|e| foundation_db::StorageError::Deserialization(e.to_string()))
}
pub fn all(&self) -> StorageResult<Vec<SessionRecord>> {
let _ = self.inner.flush();
let stream = self
.inner
.doc_store
.scan_all::<SessionRecord>(&self.inner.session_id.to_string())?;
let mut records = Vec::new();
for item in stream {
if let Stream::Next(result) = item {
records.push(result?);
}
}
Ok(records)
}
pub fn scan_from(&self, from_id: &str, n: usize) -> StorageResult<Vec<SessionRecord>> {
let _ = self.inner.flush();
let docs = self.inner.doc_store.scan_documents_from(
&self.inner.session_id.to_string(),
from_id,
n,
)?;
docs.into_iter()
.map(|d| serde_json::from_str::<SessionRecord>(&d.content))
.collect::<Result<Vec<_>, _>>()
.map_err(|e| foundation_db::StorageError::Deserialization(e.to_string()))
}
pub fn clear(&self) -> StorageResult<u64> {
let _ = self.inner.flush();
self.inner
.doc_store
.delete_all(&self.inner.session_id.to_string())
}
}