frame_conv/handle/
leave.rs1use liminal_protocol::client::ClientOperationRecordRefusalReason;
23use liminal_protocol::wire::{
24 AttachSecret, ClientRequest, Generation, LeaveAttemptToken, LeaveRequest, ServerValue,
25};
26use liminal_sdk::remote::RemoteParticipantHandle;
27
28use crate::config::BusAttachment;
29use crate::error::{AttachError, LeaveError};
30use crate::id::{ConversationId, ConversationSeq, ParticipantRef};
31use crate::outcome::{JoinGrant, LeaveOutcome};
32use crate::seam::{StoreAdapter, classify_refusal};
33use crate::store::ResumeStore;
34
35use super::attach::{connect, map_restore_error};
36use super::pump::SubmitFailure;
37use super::state::ConversationHandle;
38
39fn leave_token(conversation: ConversationId, participant: ParticipantRef) -> LeaveAttemptToken {
44 let mut bytes = [0_u8; 16];
45 bytes[..8].copy_from_slice(&conversation.to_wire().to_le_bytes());
46 bytes[8..].copy_from_slice(&participant.to_wire().to_le_bytes());
47 LeaveAttemptToken::new(bytes)
48}
49
50fn map_attach_error(error: AttachError) -> LeaveError {
51 match error {
52 AttachError::Endpoint { detail } => LeaveError::Endpoint { detail },
53 AttachError::ResumeRejected { detail } => LeaveError::ResumeRejected { detail },
54 AttachError::Store { detail } => LeaveError::Store { detail },
55 other => LeaveError::Protocol {
56 detail: format!("{other}"),
57 },
58 }
59}
60
61impl<S: ResumeStore> ConversationHandle<S> {
62 pub fn leave(&mut self) -> Result<LeaveOutcome, LeaveError> {
77 let token = leave_token(self.conversation, self.me);
78 let request = ClientRequest::Leave(LeaveRequest {
79 conversation_id: self.conversation.to_wire(),
80 participant_id: self.me.to_wire(),
81 capability_generation: self.generation,
82 attach_secret: AttachSecret::new(self.grant.credential.to_bytes()),
83 leave_attempt_token: token,
84 });
85 let answer = match self.submit(request) {
86 Ok(answer) => answer,
87 Err(SubmitFailure::RecordRefused {
88 reason: ClientOperationRecordRefusalReason::AlreadyDead,
89 ..
90 }) => return Ok(LeaveOutcome::AlreadyLeft),
91 Err(other) => return Err(submit_to_leave(other)),
92 };
93 let outcome = classify_commit(&answer, token)?;
94 self.attached = false;
97 Ok(outcome)
98 }
99
100 pub fn leave_resumed(
115 attachment: &BusAttachment,
116 grant: &JoinGrant,
117 canonical_state: &[u8],
118 store: S,
119 ) -> Result<LeaveOutcome, LeaveError> {
120 let generation =
121 Generation::new(grant.generation).ok_or_else(|| LeaveError::ResumeRejected {
122 detail: "grant carries the forbidden zero generation".to_owned(),
123 })?;
124 let config = connect(attachment).map_err(map_attach_error)?;
125 let sdk =
126 RemoteParticipantHandle::restore(&config, StoreAdapter::new(store), canonical_state)
127 .map_err(|error| map_attach_error(map_restore_error(error)))?;
128 let mut handle = Self::bare(sdk, grant.conversation, attachment);
129 handle.me = grant.participant;
130 handle.generation = generation;
131 handle.grant = grant.clone();
132 let token = leave_token(grant.conversation, grant.participant);
133 let request = ClientRequest::Leave(LeaveRequest {
134 conversation_id: grant.conversation.to_wire(),
135 participant_id: grant.participant.to_wire(),
136 capability_generation: generation,
137 attach_secret: AttachSecret::new(grant.credential.to_bytes()),
138 leave_attempt_token: token,
139 });
140 let answer = match handle.submit(request) {
141 Ok(answer) => answer,
142 Err(SubmitFailure::RecordRefused {
143 reason: ClientOperationRecordRefusalReason::AlreadyDead,
144 ..
145 }) => return Ok(LeaveOutcome::AlreadyLeft),
146 Err(other) => return Err(submit_to_leave(other)),
147 };
148 classify_commit(&answer, token)
149 }
150}
151
152fn classify_commit(
156 answer: &ServerValue,
157 token: LeaveAttemptToken,
158) -> Result<LeaveOutcome, LeaveError> {
159 match answer {
160 ServerValue::LeaveCommitted(receipt) => {
161 if receipt.leave_attempt_token() != token {
162 return Err(LeaveError::Protocol {
163 detail: format!("leave commit echoed a foreign attempt token: {receipt:?}"),
164 });
165 }
166 Ok(LeaveOutcome::Left {
167 seq: ConversationSeq::new(receipt.left_delivery_seq()),
168 })
169 }
170 other => {
171 let (class, detail) = classify_refusal(other);
172 Err(LeaveError::Refused { class, detail })
173 }
174 }
175}
176
177fn submit_to_leave(failure: SubmitFailure) -> LeaveError {
180 match failure {
181 SubmitFailure::RecordRefused { detail, .. }
182 | SubmitFailure::State { detail }
183 | SubmitFailure::InboundRefused { detail } => LeaveError::Protocol { detail },
184 SubmitFailure::TransportLost { detail } | SubmitFailure::ConnectionFate { detail } => {
185 LeaveError::ConnectionLost { detail }
186 }
187 SubmitFailure::AnswerTimeout { waited } => LeaveError::AnswerTimeout { waited },
188 }
189}