use super::wire::Request;
use crate::service::protocol::ServiceErrorCode;
use serde_json::{Value, json};
use sha2::{Digest, Sha256};
use std::{
collections::HashMap,
time::{Duration, Instant},
};
const RETENTION: Duration = Duration::from_secs(600);
const CAPACITY: usize = 256;
struct Entry {
digest: [u8; 32],
outcome: Value,
settled: Option<Instant>,
}
pub(super) struct Operations {
key: [u8; 32],
entries: HashMap<String, Entry>,
}
impl Operations {
pub(super) fn new() -> Self {
let mut key = [0; 32];
key[..16].copy_from_slice(uuid::Uuid::new_v4().as_bytes());
key[16..].copy_from_slice(uuid::Uuid::new_v4().as_bytes());
Self {
key,
entries: HashMap::new(),
}
}
pub(super) fn expire(&mut self, now: Instant) {
self.entries.retain(|_, entry| {
let Some(settled) = entry.settled else { return true };
let age = now.saturating_duration_since(settled);
if age >= RETENTION * 2 { return false; }
if age >= RETENTION {
entry.outcome = json!({"state":"expired", "method":null,"session_id":null,"result":null,"error":null});
entry.digest = [0; 32];
}
true
});
}
fn digest(&self, request: &Request) -> [u8; 32] {
let intent = canonical_json(&json!([
request.method,
request.session_id,
request.payload
]));
let mut inner_pad = [0x36; 64];
let mut outer_pad = [0x5c; 64];
for (index, byte) in self.key.iter().enumerate() {
inner_pad[index] ^= byte;
outer_pad[index] ^= byte;
}
let mut inner = Sha256::new();
inner.update(inner_pad);
inner.update(intent.to_string().as_bytes());
let mut outer = Sha256::new();
outer.update(outer_pad);
outer.update(inner.finalize());
outer.finalize().into()
}
pub(super) fn reserve(
&mut self,
request: &Request,
now: Instant,
) -> Result<(), ServiceErrorCode> {
self.expire(now);
let id = request
.operation_id
.as_ref()
.ok_or(ServiceErrorCode::InvalidPayload)?;
let digest = self.digest(request);
if let Some(entry) = self.entries.get(id) {
return Err(if entry.outcome["state"] == "expired" {
ServiceErrorCode::OperationExpired
} else if entry.digest == digest {
ServiceErrorCode::OperationAlreadyKnown
} else {
ServiceErrorCode::OperationIdentityMismatch
});
}
if self.entries.len() >= CAPACITY {
return Err(ServiceErrorCode::OperationCapacity);
}
self.entries.insert(id.clone(), Entry {
digest,
outcome: json!({"state":"in_progress","method":request.method,"session_id":request.session_id,"result":null,"error":null}),
settled: None,
});
Ok(())
}
pub(super) fn update(
&mut self,
id: &str,
state: &str,
result: Value,
error: Value,
now: Instant,
) {
if let Some(entry) = self.entries.get_mut(id) {
entry.outcome["state"] = json!(state);
entry.outcome["result"] = result;
entry.outcome["error"] = error;
if state == "terminal" || state == "rejected" {
entry.settled = Some(now);
}
}
}
pub(super) fn lookup(&self, instance: &str, target: &str, id: &str) -> Value {
let mut outcome = if target == instance {
self.entries.get(id).map(|entry| entry.outcome.clone())
} else { None }.unwrap_or_else(|| json!({"state":"unknown","method":null,"session_id":null,"result":null,"error":null}));
outcome["target_instance_id"] = json!(target);
outcome["operation_id"] = json!(id);
outcome
}
pub(super) fn retained(&self, id: &str) -> bool {
self.entries
.get(id)
.is_some_and(|entry| entry.outcome["state"] != "expired")
}
pub(super) fn admitted(&self) -> usize {
self.entries
.values()
.filter(|entry| entry.settled.is_none())
.count()
}
}
fn canonical_json(value: &Value) -> Value {
match value {
Value::Object(fields) => {
let mut ordered: Vec<_> = fields.iter().collect();
ordered.sort_unstable_by_key(|(key, _)| *key);
Value::Object(
ordered
.into_iter()
.map(|(key, value)| (key.clone(), canonical_json(value)))
.collect(),
)
}
Value::Array(values) => Value::Array(values.iter().map(canonical_json).collect()),
_ => value.clone(),
}
}