use std::collections::BTreeSet;
use std::sync::Arc;
use liminal::durability::DurableStore;
use liminal::protocol::Frame;
use liminal_protocol::lifecycle::ConnectionConversationTracking;
use liminal_protocol::wire::{
BindingEpoch, ClientRequest, CodecError, ConnectionIncarnation, ConversationId,
ObserverRecoveryHandshake, ParticipantId, ServerValue, ValidatedFrameLimit,
};
use super::transport::{
ParticipantIngress, ParticipantSession, encode_server_value, gate_generic_frame,
normalize_configured_frame_limit,
};
use super::{
ObserverPublicationTarget, ParticipantOfferedProgress, ParticipantPublication,
ParticipantPublicationInbox, ParticipantPublicationRegistry,
};
#[derive(Debug, Default)]
pub struct ParticipantConnectionConversations {
tracked: BTreeSet<ConversationId>,
}
impl ParticipantConnectionConversations {
#[must_use]
pub fn tracking(&self, conversation_id: ConversationId) -> ConnectionConversationTracking {
if self.tracked.contains(&conversation_id) {
ConnectionConversationTracking::AlreadyTracked
} else {
ConnectionConversationTracking::Untracked
}
}
#[must_use]
pub fn occupied(&self) -> u64 {
u64::try_from(self.tracked.len()).unwrap_or(u64::MAX)
}
pub fn track(&mut self, conversation_id: ConversationId) {
self.tracked.insert(conversation_id);
}
#[must_use]
pub fn tracked_conversations(&self) -> Vec<ConversationId> {
self.tracked.iter().copied().collect()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ParticipantConnectionContext {
connection_incarnation: ConnectionIncarnation,
}
impl ParticipantConnectionContext {
#[must_use]
pub const fn new(connection_incarnation: ConnectionIncarnation) -> Self {
Self {
connection_incarnation,
}
}
#[must_use]
pub const fn connection_incarnation(self) -> ConnectionIncarnation {
self.connection_incarnation
}
}
#[derive(Clone, Debug, thiserror::Error, PartialEq, Eq)]
pub enum ParticipantSemanticError {
#[error("participant semantic service is unavailable")]
Unavailable,
#[error("participant semantic service failed: {message}")]
Internal {
message: String,
},
}
pub trait ParticipantSemanticHandler: core::fmt::Debug + Send + Sync {
fn publication_conversation_limit(&self) -> u64 {
0
}
fn ready_connection_incarnations(
&self,
_conversation_id: ConversationId,
) -> Result<Vec<ConnectionIncarnation>, ParticipantSemanticError> {
Ok(Vec::new())
}
fn next_publication(
&self,
_connection_incarnation: ConnectionIncarnation,
_conversation_id: ConversationId,
_offered: Option<ParticipantOfferedProgress>,
) -> Result<Option<ParticipantPublication>, ParticipantSemanticError> {
Ok(None)
}
fn publication_binding_is_current(
&self,
_conversation_id: ConversationId,
_participant_id: ParticipantId,
_binding_epoch: BindingEpoch,
) -> Result<bool, ParticipantSemanticError> {
Ok(false)
}
fn record_publication_offer(
&self,
_publication: &ParticipantPublication,
) -> Result<(), ParticipantSemanticError> {
Ok(())
}
fn handle_observer_recovery(
&self,
context: ParticipantConnectionContext,
conversations: &mut ParticipantConnectionConversations,
request: ObserverRecoveryHandshake,
target: Option<ObserverPublicationTarget>,
) -> Result<ServerValue, ParticipantSemanticError> {
drop(target);
self.handle(
context,
conversations,
ClientRequest::ObserverRecovery(request),
)
}
fn handle(
&self,
context: ParticipantConnectionContext,
conversations: &mut ParticipantConnectionConversations,
request: ClientRequest,
) -> Result<ServerValue, ParticipantSemanticError>;
}
#[derive(Clone, Debug)]
pub struct InstalledParticipantService {
handler: Arc<dyn ParticipantSemanticHandler>,
durable_store: Arc<dyn DurableStore>,
frame_limit: ValidatedFrameLimit,
publication_registry: Arc<ParticipantPublicationRegistry>,
}
impl InstalledParticipantService {
pub(crate) fn new(
handler: Arc<dyn ParticipantSemanticHandler>,
durable_store: Arc<dyn DurableStore>,
configured_wf: u64,
) -> Result<Self, CodecError> {
Ok(Self {
handler,
durable_store,
frame_limit: normalize_configured_frame_limit(configured_wf)?,
publication_registry: Arc::new(ParticipantPublicationRegistry::default()),
})
}
#[must_use]
pub(crate) fn durable_store(&self) -> Arc<dyn DurableStore> {
Arc::clone(&self.durable_store)
}
#[must_use]
pub(crate) const fn frame_limit(&self) -> ValidatedFrameLimit {
self.frame_limit
}
#[must_use]
pub(crate) fn publication_conversation_limit(&self) -> u64 {
self.handler.publication_conversation_limit()
}
#[must_use]
pub(crate) fn new_publication_inbox(&self) -> ParticipantPublicationInbox {
ParticipantPublicationInbox::new(self.handler.publication_conversation_limit())
}
#[must_use]
pub(crate) fn publication_registry(&self) -> &ParticipantPublicationRegistry {
&self.publication_registry
}
pub(crate) fn next_publication(
&self,
connection_incarnation: ConnectionIncarnation,
conversation_id: ConversationId,
offered: Option<ParticipantOfferedProgress>,
) -> Result<Option<ParticipantPublication>, ParticipantSemanticError> {
self.handler
.next_publication(connection_incarnation, conversation_id, offered)
}
pub(crate) fn publication_binding_is_current(
&self,
conversation_id: ConversationId,
participant_id: ParticipantId,
binding_epoch: BindingEpoch,
) -> Result<bool, ParticipantSemanticError> {
self.handler
.publication_binding_is_current(conversation_id, participant_id, binding_epoch)
}
pub(crate) fn record_publication_offer(
&self,
publication: &ParticipantPublication,
) -> Result<(), ParticipantSemanticError> {
self.handler.record_publication_offer(publication)
}
fn notify_ready(
&self,
conversation_id: ConversationId,
) -> Result<(), ParticipantSemanticError> {
for incarnation in self
.handler
.ready_connection_incarnations(conversation_id)?
{
self.publication_registry
.notify(incarnation, conversation_id)
.map_err(|error| ParticipantSemanticError::Internal {
message: format!("participant publication wake failed: {error}"),
})?;
}
Ok(())
}
}
impl ParticipantSemanticHandler for InstalledParticipantService {
fn publication_conversation_limit(&self) -> u64 {
self.handler.publication_conversation_limit()
}
fn handle(
&self,
context: ParticipantConnectionContext,
conversations: &mut ParticipantConnectionConversations,
request: ClientRequest,
) -> Result<ServerValue, ParticipantSemanticError> {
if let ClientRequest::ObserverRecovery(request) = request {
let target = self
.publication_registry
.observer_target(context.connection_incarnation())
.map_err(|error| ParticipantSemanticError::Internal {
message: format!("observer publication target failed: {error}"),
})?;
return self
.handler
.handle_observer_recovery(context, conversations, request, target);
}
let conversation_id = request_conversation_id(&request);
let value = self.handler.handle(context, conversations, request)?;
if let Some(conversation_id) = conversation_id {
self.notify_ready(conversation_id)?;
}
Ok(value)
}
}
const fn request_conversation_id(request: &ClientRequest) -> Option<ConversationId> {
match request {
ClientRequest::Enrollment(request) => Some(request.conversation_id),
ClientRequest::CredentialAttach(request) => Some(request.conversation_id),
ClientRequest::Detach(request) => Some(request.conversation_id),
ClientRequest::ParticipantAck(request) => Some(request.conversation_id),
ClientRequest::Leave(request) => Some(request.conversation_id),
ClientRequest::MarkerAck(request) => Some(request.conversation_id),
ClientRequest::RecordAdmission(request) => Some(request.conversation_id),
ClientRequest::ObserverRecovery(_) => None,
}
}
#[derive(Debug)]
pub enum ParticipantDispatch {
NotParticipant,
Respond(Frame),
RespondThenClose(Frame),
Fatal(ParticipantDispatchError),
}
#[derive(Debug, thiserror::Error)]
pub enum ParticipantDispatchError {
#[error("invalid generic participant frame")]
InvalidGenericFrame,
#[error(transparent)]
Semantic(#[from] ParticipantSemanticError),
#[error("failed to encode participant response: {0:?}")]
Encode(CodecError),
}
#[must_use]
pub fn dispatch_generic_frame(
frame: &Frame,
authenticated: bool,
session: ParticipantSession,
context: ParticipantConnectionContext,
conversations: &mut ParticipantConnectionConversations,
handler: &dyn ParticipantSemanticHandler,
) -> ParticipantDispatch {
let (value, close_after_response) = match gate_generic_frame(frame, authenticated, session) {
ParticipantIngress::NotParticipant => return ParticipantDispatch::NotParticipant,
ParticipantIngress::Rejected(rejection) => {
(ServerValue::ParticipantTransportRejected(rejection), true)
}
ParticipantIngress::InvalidGenericFrame => {
return ParticipantDispatch::Fatal(ParticipantDispatchError::InvalidGenericFrame);
}
ParticipantIngress::Request(request) => {
match handler.handle(context, conversations, request) {
Ok(value) => (value, false),
Err(error) => {
return ParticipantDispatch::Fatal(ParticipantDispatchError::Semantic(error));
}
}
}
};
match encode_server_value(value) {
Ok(frame) if close_after_response => ParticipantDispatch::RespondThenClose(frame),
Ok(frame) => ParticipantDispatch::Respond(frame),
Err(error) => ParticipantDispatch::Fatal(ParticipantDispatchError::Encode(error)),
}
}