use liminal_protocol::client::ClientOperationRecordRefusalReason;
use liminal_protocol::wire::{
AttachSecret, ClientRequest, Generation, LeaveAttemptToken, LeaveRequest, ServerValue,
};
use liminal_sdk::remote::RemoteParticipantHandle;
use crate::config::BusAttachment;
use crate::error::{AttachError, LeaveError};
use crate::id::{ConversationId, ConversationSeq, ParticipantRef};
use crate::outcome::{JoinGrant, LeaveOutcome};
use crate::seam::{StoreAdapter, classify_refusal};
use crate::store::ResumeStore;
use super::attach::{connect, map_restore_error};
use super::pump::SubmitFailure;
use super::state::ConversationHandle;
fn leave_token(conversation: ConversationId, participant: ParticipantRef) -> LeaveAttemptToken {
let mut bytes = [0_u8; 16];
bytes[..8].copy_from_slice(&conversation.to_wire().to_le_bytes());
bytes[8..].copy_from_slice(&participant.to_wire().to_le_bytes());
LeaveAttemptToken::new(bytes)
}
fn map_attach_error(error: AttachError) -> LeaveError {
match error {
AttachError::Endpoint { detail } => LeaveError::Endpoint { detail },
AttachError::ResumeRejected { detail } => LeaveError::ResumeRejected { detail },
AttachError::Store { detail } => LeaveError::Store { detail },
other => LeaveError::Protocol {
detail: format!("{other}"),
},
}
}
impl<S: ResumeStore> ConversationHandle<S> {
pub fn leave(&mut self) -> Result<LeaveOutcome, LeaveError> {
let token = leave_token(self.conversation, self.me);
let request = ClientRequest::Leave(LeaveRequest {
conversation_id: self.conversation.to_wire(),
participant_id: self.me.to_wire(),
capability_generation: self.generation,
attach_secret: AttachSecret::new(self.grant.credential.to_bytes()),
leave_attempt_token: token,
});
let answer = match self.submit(request) {
Ok(answer) => answer,
Err(SubmitFailure::RecordRefused {
reason: ClientOperationRecordRefusalReason::AlreadyDead,
..
}) => return Ok(LeaveOutcome::AlreadyLeft),
Err(other) => return Err(submit_to_leave(other)),
};
let outcome = classify_commit(&answer, token)?;
self.attached = false;
Ok(outcome)
}
pub fn leave_resumed(
attachment: &BusAttachment,
grant: &JoinGrant,
canonical_state: &[u8],
store: S,
) -> Result<LeaveOutcome, LeaveError> {
let generation =
Generation::new(grant.generation).ok_or_else(|| LeaveError::ResumeRejected {
detail: "grant carries the forbidden zero generation".to_owned(),
})?;
let config = connect(attachment).map_err(map_attach_error)?;
let sdk =
RemoteParticipantHandle::restore(&config, StoreAdapter::new(store), canonical_state)
.map_err(|error| map_attach_error(map_restore_error(error)))?;
let mut handle = Self::bare(sdk, grant.conversation, attachment);
handle.me = grant.participant;
handle.generation = generation;
handle.grant = grant.clone();
let token = leave_token(grant.conversation, grant.participant);
let request = ClientRequest::Leave(LeaveRequest {
conversation_id: grant.conversation.to_wire(),
participant_id: grant.participant.to_wire(),
capability_generation: generation,
attach_secret: AttachSecret::new(grant.credential.to_bytes()),
leave_attempt_token: token,
});
let answer = match handle.submit(request) {
Ok(answer) => answer,
Err(SubmitFailure::RecordRefused {
reason: ClientOperationRecordRefusalReason::AlreadyDead,
..
}) => return Ok(LeaveOutcome::AlreadyLeft),
Err(other) => return Err(submit_to_leave(other)),
};
classify_commit(&answer, token)
}
}
fn classify_commit(
answer: &ServerValue,
token: LeaveAttemptToken,
) -> Result<LeaveOutcome, LeaveError> {
match answer {
ServerValue::LeaveCommitted(receipt) => {
if receipt.leave_attempt_token() != token {
return Err(LeaveError::Protocol {
detail: format!("leave commit echoed a foreign attempt token: {receipt:?}"),
});
}
Ok(LeaveOutcome::Left {
seq: ConversationSeq::new(receipt.left_delivery_seq()),
})
}
other => {
let (class, detail) = classify_refusal(other);
Err(LeaveError::Refused { class, detail })
}
}
}
fn submit_to_leave(failure: SubmitFailure) -> LeaveError {
match failure {
SubmitFailure::RecordRefused { detail, .. }
| SubmitFailure::State { detail }
| SubmitFailure::InboundRefused { detail } => LeaveError::Protocol { detail },
SubmitFailure::TransportLost { detail } | SubmitFailure::ConnectionFate { detail } => {
LeaveError::ConnectionLost { detail }
}
SubmitFailure::AnswerTimeout { waited } => LeaveError::AnswerTimeout { waited },
}
}