use crate::{ContentIdentity, FlowRef, PeerId};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::SystemTime;
use std::time::UNIX_EPOCH;
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Eq, PartialEq)]
pub enum AcquisitionPolicy {
LocalOnly,
PreferLocal,
RemoteAllowed,
Required,
}
impl Default for AcquisitionPolicy {
fn default() -> Self {
AcquisitionPolicy::PreferLocal
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
pub enum ContentTransferError {
NotFound,
Unauthorized,
Unavailable,
HashMismatch { expected: String, computed: String },
VersionUnavailable,
AcquisitionError(String),
}
impl std::fmt::Display for ContentTransferError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ContentTransferError::NotFound => write!(f, "Content not found"),
ContentTransferError::Unauthorized => write!(f, "Access denied"),
ContentTransferError::Unavailable => write!(f, "Provider unavailable"),
ContentTransferError::HashMismatch { expected, computed } => {
write!(f, "Hash mismatch: expected {}, got {}", expected, computed)
}
ContentTransferError::VersionUnavailable => write!(f, "Version unavailable"),
ContentTransferError::AcquisitionError(msg) => write!(f, "Acquisition error: {}", msg),
}
}
}
impl std::error::Error for ContentTransferError {}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContentRequest {
pub request_id: String,
pub flow_ref: FlowRef,
pub content_identity: Option<ContentIdentity>,
}
impl ContentRequest {
pub fn new(
request_id: String,
flow_ref: FlowRef,
content_identity: Option<ContentIdentity>,
) -> Self {
Self {
request_id,
flow_ref,
content_identity,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContentResponse {
pub request_id: String,
pub content_identity: ContentIdentity,
pub payload: Vec<u8>,
pub provider: PeerId,
pub acquired_at_ms: u128,
}
impl ContentResponse {
pub fn new(
request_id: String,
content_identity: ContentIdentity,
payload: Vec<u8>,
provider: PeerId,
) -> Self {
let now_ms = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis())
.unwrap_or(0);
Self {
request_id,
content_identity,
payload,
provider,
acquired_at_ms: now_ms,
}
}
}
#[derive(Debug, Clone)]
pub struct AcquisitionResult {
pub payload: Vec<u8>,
pub source_peer: PeerId,
pub acquired_at_ms: u128,
}
impl AcquisitionResult {
pub fn new(payload: Vec<u8>, source_peer: PeerId) -> Self {
let now_ms = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis())
.unwrap_or(0);
Self {
payload,
source_peer,
acquired_at_ms: now_ms,
}
}
}
#[derive(Clone)]
pub struct AcquisitionDeduplicator {
in_flight: Arc<
Mutex<
HashMap<
String,
Vec<
tokio::sync::oneshot::Receiver<Result<AcquisitionResult, ContentTransferError>>,
>,
>,
>,
>,
}
impl AcquisitionDeduplicator {
pub fn new() -> Self {
Self {
in_flight: Arc::new(Mutex::new(HashMap::new())),
}
}
pub fn is_in_flight(&self, key: &str) -> Result<bool, String> {
let in_flight = self.in_flight.lock().map_err(|e| e.to_string())?;
Ok(in_flight.contains_key(key) && !in_flight[key].is_empty())
}
pub fn register_acquisition(
&self,
key: String,
receiver: tokio::sync::oneshot::Receiver<Result<AcquisitionResult, ContentTransferError>>,
) -> Result<bool, String> {
let mut in_flight = self.in_flight.lock().map_err(|e| e.to_string())?;
let is_first = !in_flight.contains_key(&key) || in_flight[&key].is_empty();
in_flight.entry(key).or_insert_with(Vec::new).push(receiver);
Ok(is_first)
}
pub fn mark_complete(
&self,
key: &str,
_result: Result<AcquisitionResult, ContentTransferError>,
) -> Result<(), String> {
let mut in_flight = self.in_flight.lock().map_err(|e| e.to_string())?;
in_flight.remove(key);
Ok(())
}
}
impl Default for AcquisitionDeduplicator {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_acquisition_policy_default() {
assert_eq!(AcquisitionPolicy::default(), AcquisitionPolicy::PreferLocal);
}
#[test]
fn test_content_request_creation() {
let flow_ref = FlowRef::new("docs", "123");
let content_id = ContentIdentity::new("abc123");
let request = ContentRequest::new("req-1".to_string(), flow_ref.clone(), Some(content_id));
assert_eq!(request.request_id, "req-1");
assert_eq!(request.flow_ref.object_id, "123");
}
#[test]
fn test_content_response_creation() {
let content_id = ContentIdentity::new("abc123");
let peer = PeerId::new("peer-a");
let payload = vec![1, 2, 3];
let response = ContentResponse::new(
"req-1".to_string(),
content_id,
payload.clone(),
peer.clone(),
);
assert_eq!(response.request_id, "req-1");
assert_eq!(response.payload, payload);
assert_eq!(response.provider, peer);
}
#[test]
fn test_acquisition_result_creation() {
let peer = PeerId::new("peer-a");
let payload = vec![1, 2, 3];
let result = AcquisitionResult::new(payload.clone(), peer.clone());
assert_eq!(result.payload, payload);
assert_eq!(result.source_peer, peer);
assert!(result.acquired_at_ms > 0);
}
#[test]
fn test_acquisition_deduplicator_creation() {
let dedup = AcquisitionDeduplicator::new();
assert!(!dedup.is_in_flight("key-1").unwrap());
}
#[test]
fn test_content_transfer_error_display() {
let err = ContentTransferError::NotFound;
assert_eq!(err.to_string(), "Content not found");
let err = ContentTransferError::HashMismatch {
expected: "abc".to_string(),
computed: "def".to_string(),
};
assert!(err.to_string().contains("abc"));
}
}