use crate::domain::{
errors::{AgentError, AgentResult, ErrorCode},
pi_rpc::PiRpcOutput,
};
pub(crate) struct PiRpcDecoder {
buffer: Vec<u8>,
max_record_bytes: usize,
}
pub(crate) struct SizedPiRpcOutput {
pub(crate) output: PiRpcOutput,
pub(crate) json_bytes: usize,
}
impl PiRpcDecoder {
pub(crate) fn new(max_record_bytes: usize) -> Self {
Self {
buffer: Vec::new(),
max_record_bytes,
}
}
#[cfg(test)]
pub(crate) fn push(&mut self, chunk: &[u8]) -> AgentResult<Vec<PiRpcOutput>> {
Ok(self
.push_sized(chunk)?
.into_iter()
.map(|record| record.output)
.collect())
}
pub(crate) fn push_sized(&mut self, mut chunk: &[u8]) -> AgentResult<Vec<SizedPiRpcOutput>> {
let mut records = Vec::new();
while let Some(newline) = chunk.iter().position(|byte| *byte == b'\n') {
self.append(&chunk[..newline])?;
records.push(self.decode_buffered_record()?);
chunk = &chunk[newline + 1..];
}
self.append(chunk)?;
Ok(records)
}
pub(crate) fn finish(&mut self) -> AgentResult<Vec<PiRpcOutput>> {
if self.buffer.iter().all(u8::is_ascii_whitespace) {
self.buffer.clear();
return Ok(Vec::new());
}
Err(invalid_message(
"Pi RPC stream ended with an incomplete JSONL record",
))
}
fn append(&mut self, bytes: &[u8]) -> AgentResult<()> {
let new_len = self.buffer.len().saturating_add(bytes.len());
let is_optional_cr = new_len == self.max_record_bytes.saturating_add(1)
&& bytes.last().or_else(|| self.buffer.last()) == Some(&b'\r');
if new_len > self.max_record_bytes && !is_optional_cr {
return Err(invalid_message(format!(
"Pi RPC record exceeds {} byte limit",
self.max_record_bytes
)));
}
self.buffer.extend_from_slice(bytes);
Ok(())
}
fn decode_buffered_record(&mut self) -> AgentResult<SizedPiRpcOutput> {
if self.buffer.last() == Some(&b'\r') {
self.buffer.pop();
}
if self.buffer.is_empty() {
return Err(invalid_message("Pi RPC stream contained an empty record"));
}
let record = std::str::from_utf8(&self.buffer).map_err(|error| {
invalid_message(format!("Pi RPC record is not valid UTF-8: {error}"))
})?;
let output = serde_json::from_str(record)
.map_err(|error| invalid_message(format!("invalid Pi RPC JSON record: {error}")))?;
let json_bytes = self.buffer.len();
self.buffer.clear();
Ok(SizedPiRpcOutput { output, json_bytes })
}
}
fn invalid_message(message: impl Into<String>) -> AgentError {
AgentError::new(ErrorCode::InvalidMessage, message)
}