use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
use super::{DispatchContext, Recorder};
use crate::{
effect::{
Answered, Disposition, EffectId, EffectKind, EffectRecord, HandlerDescriptor, HandlerKey,
},
error::ErrorReport,
streaming::{Item, StreamEvent, StreamRecord},
tool::ToolResultContext,
};
#[derive(Clone, Default)]
pub struct RecordBuffer {
slots: Arc<Mutex<Vec<Slot>>>,
keep_events: bool,
timed: bool,
}
#[derive(Clone)]
struct Slot {
id: EffectId,
key: HandlerKey,
kind: EffectKind,
origin: DispatchContext,
outcome: Option<Result<Answered, ErrorReport>>,
events: Option<Vec<Item<StreamEvent>>>,
stream_origin: Option<crate::message::Origin>,
tool_output: Option<ToolResultContext>,
started_ms: Option<u64>,
finished_ms: Option<u64>,
}
impl Slot {
fn into_record(self) -> Option<EffectRecord> {
Some(EffectRecord {
published: self.tool_output,
id: self.id,
key: self.key,
kind: self.kind,
outcome: self.outcome?,
events: self
.events
.and_then(|items| StreamRecord::from_items(items).ok()),
stream_origin: self.stream_origin,
parent: self.origin.parent,
scope: self.origin.scope,
purpose: self.origin.purpose,
turn: None,
started_ms: self.started_ms,
finished_ms: self.finished_ms,
reviewed: None,
disposition: Disposition::Ran,
decided_by: None,
})
}
}
impl RecordBuffer {
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn keeping_stream_events(mut self) -> Self {
self.keep_events = true;
self
}
#[must_use]
pub fn timed(mut self) -> Self {
self.timed = true;
self
}
pub fn take(&self, id: EffectId) -> Option<EffectRecord> {
let mut slots = self.slots();
let position = slots
.iter()
.rposition(|slot| slot.id == id && slot.outcome.is_some())?;
slots.remove(position).into_record()
}
pub fn take_resolved(&self) -> Vec<EffectRecord> {
self.take_resolved_where(|_| true)
}
pub fn take_resolved_where(&self, mut take: impl FnMut(EffectId) -> bool) -> Vec<EffectRecord> {
let mut slots = self.slots();
let (taken, left) = std::mem::take(&mut *slots)
.into_iter()
.partition(|slot| slot.outcome.is_some() && take(slot.id));
*slots = left;
drop(slots);
taken.into_iter().filter_map(Slot::into_record).collect()
}
pub fn records(&self) -> Vec<EffectRecord> {
let slots = self.slots();
let resolved: Vec<Slot> = slots
.iter()
.filter(|slot| slot.outcome.is_some())
.cloned()
.collect();
drop(slots);
resolved.into_iter().filter_map(Slot::into_record).collect()
}
pub fn contains(&self, id: EffectId) -> bool {
self.slots().iter().any(|slot| slot.id == id)
}
pub fn remove(&self, id: EffectId) -> Option<HandlerKey> {
let mut slots = self.slots();
let position = slots.iter().rposition(|slot| slot.id == id)?;
Some(slots.remove(position).key)
}
pub fn kept_events(&self, id: EffectId) -> Option<usize> {
let slots = self.slots();
let slot = slots.iter().rev().find(|slot| slot.id == id)?;
slot.events.as_ref().map(Vec::len)
}
pub fn resolved(&self) -> usize {
let slots = self.slots();
slots.iter().filter(|slot| slot.outcome.is_some()).count()
}
pub fn in_flight(&self) -> usize {
let slots = self.slots();
slots.iter().filter(|slot| slot.outcome.is_none()).count()
}
fn slots(&self) -> MutexGuard<'_, Vec<Slot>> {
self.slots.lock().unwrap_or_else(PoisonError::into_inner)
}
fn with_slot(&self, id: EffectId, keep: impl Fn(&Slot) -> bool, edit: impl FnOnce(&mut Slot)) {
let mut slots = self.slots();
if let Some(slot) = slots
.iter_mut()
.rev()
.find(|slot| slot.id == id && keep(slot))
{
edit(slot);
}
}
fn now(&self) -> Option<u64> {
use web_time::{SystemTime, UNIX_EPOCH};
if !self.timed {
return None;
}
let since = SystemTime::now().duration_since(UNIX_EPOCH).ok()?;
Some(u64::try_from(since.as_millis()).unwrap_or(u64::MAX))
}
}
impl std::fmt::Debug for RecordBuffer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RecordBuffer")
.field("resolved", &self.resolved())
.field("in_flight", &self.in_flight())
.finish()
}
}
impl Recorder for RecordBuffer {
fn handlers(&self, _handlers: Vec<HandlerDescriptor>) {}
fn begin(&self, id: EffectId, key: HandlerKey, kind: EffectKind, origin: DispatchContext) {
let events = (self.keep_events && kind.streams()).then(Vec::new);
let started_ms = self.now();
self.slots().push(Slot {
id,
key,
kind,
origin,
outcome: None,
events,
stream_origin: None,
tool_output: None,
started_ms,
finished_ms: None,
});
}
fn discard(&self, id: EffectId) {
self.remove(id);
}
fn patch(&self, id: EffectId, kind: EffectKind) {
self.with_slot(id, |_| true, |slot| slot.kind = kind);
}
fn keep_events(&self) -> bool {
self.keep_events
}
fn event(&self, id: EffectId, item: &Item<StreamEvent>) {
self.with_slot(
id,
|_| true,
|slot| {
if let Some(events) = &mut slot.events {
events.push(item.clone());
}
},
);
}
fn origin(&self, id: EffectId, origin: &crate::message::Origin) {
let keeps = |slot: &Slot| slot.events.is_some();
self.with_slot(id, keeps, |slot| {
slot.stream_origin = Some(origin.clone());
});
}
fn tool_output(&self, id: EffectId, output: ToolResultContext) {
let open = |slot: &Slot| slot.outcome.is_none();
self.with_slot(id, open, |slot| slot.tool_output = Some(output));
}
fn resolve(&self, id: EffectId, outcome: Result<Answered, ErrorReport>) {
let finished_ms = self.now();
self.with_slot(
id,
|_| true,
|slot| {
slot.outcome = Some(outcome);
slot.finished_ms = finished_ms;
},
);
}
}
#[cfg(test)]
mod tests;