PicoBot/src/delivery/service.rs

122 lines
3.6 KiB
Rust

use std::sync::Arc;
use tokio::sync::{oneshot, watch};
use crate::bus::CommittedTurnDelta;
use crate::channels::{ChannelManager, TurnTarget};
use crate::delivery::{DeliveryCoordinator, DeliveryError};
use crate::session::TurnSnapshot;
use crate::task_supervisor::TaskSupervisor;
use super::coordinator::SinkRoute;
/// Gateway-owned facade that resolves a target Channel and starts exactly one
/// TurnSink lifecycle. Session workers depend on this abstraction rather than
/// on Channel implementations or WebSocket/Feishu protocols.
#[derive(Clone)]
pub struct TurnDeliveryService {
coordinator: DeliveryCoordinator,
channels: ChannelManager,
supervisor: TaskSupervisor,
}
/// Completion handle for one TurnSink lifecycle.
///
/// Creating a sink only proves that delivery started. Callers consume this
/// handle after publishing a terminal snapshot to learn whether the terminal
/// write actually reached the Channel.
pub struct TurnDeliveryHandle {
pub(crate) completion: oneshot::Receiver<Result<(), DeliveryError>>,
}
impl TurnDeliveryHandle {
pub async fn wait(self) -> Result<(), DeliveryError> {
self.completion
.await
.unwrap_or(Err(DeliveryError::CompletionLost))
}
}
impl TurnDeliveryService {
pub fn new(
coordinator: DeliveryCoordinator,
channels: ChannelManager,
supervisor: TaskSupervisor,
) -> Self {
Self {
coordinator,
channels,
supervisor,
}
}
pub async fn start(
&self,
target: TurnTarget,
snapshots: watch::Receiver<Arc<TurnSnapshot>>,
) -> Result<TurnDeliveryHandle, DeliveryError> {
let channel = self
.channels
.get_channel(&target.channel)
.await
.ok_or_else(|| DeliveryError::ChannelNotFound(target.channel.clone()))?;
let live_policy = channel.live_policy();
let presentation = channel.presentation_policy();
let sink = channel
.open_turn(target.clone())
.await
.map_err(DeliveryError::OpenFailed)?;
let completion = self.coordinator.spawn_sink(
&self.supervisor,
SinkRoute {
channel: target.channel,
chat_id: target.chat_id,
live_policy,
presentation,
},
snapshots,
sink,
)?;
Ok(TurnDeliveryHandle { completion })
}
pub async fn commit(
&self,
target: &TurnTarget,
delta: CommittedTurnDelta,
) -> Result<(), DeliveryError> {
let channel = self
.channels
.get_channel(&target.channel)
.await
.ok_or_else(|| DeliveryError::ChannelNotFound(target.channel.clone()))?;
channel
.commit_turn(target, delta)
.await
.map_err(DeliveryError::FinalFailed)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn handle_reports_terminal_delivery_failure() {
let (sender, completion) = oneshot::channel();
sender.send(Err(DeliveryError::FinalTimedOut)).unwrap();
let result = TurnDeliveryHandle { completion }.wait().await;
assert!(matches!(result, Err(DeliveryError::FinalTimedOut)));
}
#[tokio::test]
async fn handle_reports_lost_delivery_task() {
let (sender, completion) = oneshot::channel();
drop(sender);
let result = TurnDeliveryHandle { completion }.wait().await;
assert!(matches!(result, Err(DeliveryError::CompletionLost)));
}
}