1use super::{
2 ClientBindingState, ClientParticipantAggregate, ClientResponseCorrelation, correlation,
3};
4use crate::wire::{AttachBound, ReceiptReplay, ServerValue};
5
6#[derive(Clone, Copy, Debug, PartialEq, Eq)]
8pub enum ClientInboundRefusalReason {
9 AlreadyDead,
11 ForeignResponse,
13 DelayedResponse,
15 AmbiguousResponse,
17 MissingResponseAuthority,
19 LostAuthorityPending,
22}
23
24#[derive(Debug, PartialEq, Eq)]
26pub struct ClientInboundApplied {
27 aggregate: ClientParticipantAggregate,
28 value: ServerValue,
29}
30
31impl ClientInboundApplied {
32 #[must_use]
34 pub fn into_parts(self) -> (ClientParticipantAggregate, ServerValue) {
35 (self.aggregate, self.value)
36 }
37}
38
39#[derive(Debug, PartialEq, Eq)]
41pub struct ClientInboundRefusal {
42 aggregate: ClientParticipantAggregate,
43 value: ServerValue,
44 reason: ClientInboundRefusalReason,
45}
46
47impl ClientInboundRefusal {
48 #[must_use]
50 pub const fn reason(&self) -> ClientInboundRefusalReason {
51 self.reason
52 }
53
54 #[must_use]
56 pub fn into_parts(self) -> (ClientParticipantAggregate, ServerValue) {
57 (self.aggregate, self.value)
58 }
59}
60
61#[derive(Debug, PartialEq, Eq)]
63pub enum ClientInboundDecision {
64 Applied(ClientInboundApplied),
66 Refused(ClientInboundRefusal),
68}
69
70#[derive(Debug, PartialEq, Eq)]
72pub struct ClientCorrelatedInboundRefusal {
73 aggregate: ClientParticipantAggregate,
74 value: ServerValue,
75 correlation: ClientResponseCorrelation,
76 reason: ClientInboundRefusalReason,
77}
78
79impl ClientCorrelatedInboundRefusal {
80 #[must_use]
82 pub const fn reason(&self) -> ClientInboundRefusalReason {
83 self.reason
84 }
85
86 #[must_use]
88 pub fn into_parts(
89 self,
90 ) -> (
91 ClientParticipantAggregate,
92 ServerValue,
93 ClientResponseCorrelation,
94 ) {
95 (self.aggregate, self.value, self.correlation)
96 }
97}
98
99#[derive(Debug, PartialEq, Eq)]
101pub enum ClientCorrelatedInboundDecision {
102 Applied(ClientInboundApplied),
104 Refused(ClientCorrelatedInboundRefusal),
106}
107
108#[must_use]
110pub fn decide_inbound(
111 aggregate: ClientParticipantAggregate,
112 value: ServerValue,
113) -> ClientInboundDecision {
114 decide_inbound_inner(aggregate, value, false)
115}
116
117#[must_use]
119pub fn decide_correlated_inbound(
120 aggregate: ClientParticipantAggregate,
121 value: ServerValue,
122 correlation: ClientResponseCorrelation,
123) -> ClientCorrelatedInboundDecision {
124 let current_authority = aggregate.expected.as_ref().is_some_and(|expected| {
125 expected.issued && expected.authorization == correlation.authorization
126 });
127 if !current_authority {
128 return ClientCorrelatedInboundDecision::Refused(ClientCorrelatedInboundRefusal {
129 aggregate,
130 value,
131 correlation,
132 reason: ClientInboundRefusalReason::DelayedResponse,
133 });
134 }
135 match decide_inbound_inner(aggregate, value, true) {
136 ClientInboundDecision::Applied(applied) => {
137 ClientCorrelatedInboundDecision::Applied(applied)
138 }
139 ClientInboundDecision::Refused(refusal) => {
140 let reason = refusal.reason();
141 let (aggregate, value) = refusal.into_parts();
142 ClientCorrelatedInboundDecision::Refused(ClientCorrelatedInboundRefusal {
143 aggregate,
144 value,
145 correlation,
146 reason,
147 })
148 }
149 }
150}
151
152fn decide_inbound_inner(
153 mut aggregate: ClientParticipantAggregate,
154 value: ServerValue,
155 has_response_authority: bool,
156) -> ClientInboundDecision {
157 if aggregate.binding.is_left() {
158 return inbound_refusal(aggregate, value, ClientInboundRefusalReason::AlreadyDead);
159 }
160
161 if let Some(request) = correlation::participant_ack_request(&value) {
162 if aggregate.binding.matches_ack(request) {
163 return ClientInboundDecision::Applied(ClientInboundApplied { aggregate, value });
164 }
165 return inbound_refusal(
166 aggregate,
167 value,
168 ClientInboundRefusalReason::ForeignResponse,
169 );
170 }
171
172 if matches!(value, ServerValue::ParticipantTransportRejected(_)) {
173 return ClientInboundDecision::Applied(ClientInboundApplied { aggregate, value });
174 }
175
176 let Some(expected) = aggregate.expected.as_ref() else {
177 return inbound_refusal(
178 aggregate,
179 value,
180 ClientInboundRefusalReason::DelayedResponse,
181 );
182 };
183
184 if expected.lost.is_some() {
185 return inbound_refusal(
186 aggregate,
187 value,
188 ClientInboundRefusalReason::LostAuthorityPending,
189 );
190 }
191
192 if !has_response_authority {
193 return inbound_refusal(
194 aggregate,
195 value,
196 ClientInboundRefusalReason::MissingResponseAuthority,
197 );
198 }
199
200 if !aggregate.binding.accepts_request(&expected.request) {
201 return inbound_refusal(
202 aggregate,
203 value,
204 ClientInboundRefusalReason::ForeignResponse,
205 );
206 }
207
208 if !correlation::matches_request(&value, &expected.request) {
209 let same_request_class = value.originating_request()
210 == Some(expected.request.discriminant())
211 || matches!(
212 (&value, &expected.request),
213 (
214 ServerValue::ObserverRecoveryAccepted(_)
215 | ServerValue::InvalidObserverEpoch(_)
216 | ServerValue::InvalidObserverEpochList(_),
217 crate::wire::ClientRequest::ObserverRecovery(_)
218 )
219 );
220 let same_identity = correlation::same_identity(&value, &expected.request);
221 let reason = if same_request_class && same_identity {
222 if matches!(
223 expected.request,
224 crate::wire::ClientRequest::RecordAdmission(_)
225 ) {
226 ClientInboundRefusalReason::AmbiguousResponse
227 } else {
228 ClientInboundRefusalReason::DelayedResponse
229 }
230 } else {
231 ClientInboundRefusalReason::ForeignResponse
232 };
233 return inbound_refusal(aggregate, value, reason);
234 }
235
236 aggregate.expected = None;
243 apply_correlated_value(&mut aggregate, &value);
244 ClientInboundDecision::Applied(ClientInboundApplied { aggregate, value })
245}
246
247const fn inbound_refusal(
248 aggregate: ClientParticipantAggregate,
249 value: ServerValue,
250 reason: ClientInboundRefusalReason,
251) -> ClientInboundDecision {
252 ClientInboundDecision::Refused(ClientInboundRefusal {
253 aggregate,
254 value,
255 reason,
256 })
257}
258
259fn apply_correlated_value(aggregate: &mut ClientParticipantAggregate, value: &ServerValue) {
260 match value {
261 ServerValue::EnrollBound(value) => apply_enroll_bound(aggregate, value),
262 ServerValue::Bound(ReceiptReplay::Enrollment(value)) => {
263 apply_enroll_bound(aggregate, value);
264 }
265 ServerValue::AttachBound(value)
266 | ServerValue::Bound(ReceiptReplay::CredentialAttach(value)) => {
267 apply_attach_bound(aggregate, value);
268 aggregate.detach_replay.apply_attach(value);
269 }
270 ServerValue::DetachCommitted(value) => {
271 let attach_secret = match aggregate.binding {
272 ClientBindingState::Bound { attach_secret, .. }
273 | ClientBindingState::Detached { attach_secret, .. } => attach_secret,
274 ClientBindingState::Unbound | ClientBindingState::Left { .. } => return,
275 };
276 aggregate.binding = ClientBindingState::Detached {
277 conversation_id: value.conversation_id(),
278 participant_id: value.participant_id(),
279 generation: value.capability_generation(),
280 attach_secret,
281 };
282 aggregate.detach_replay.apply_detach_committed(value);
283 }
284 ServerValue::DetachInProgress(value) => {
285 aggregate.detach_replay.apply_detach_in_progress(value);
286 }
287 ServerValue::StaleAuthority(crate::wire::StaleAuthority::Detach(
288 crate::wire::DetachStaleAuthority::TerminalizedDetachCell(value),
289 )) => {
290 aggregate
291 .detach_replay
292 .apply_terminalized_detach_cell(value);
293 }
294 ServerValue::LeaveCommitted(value) => {
295 aggregate.binding = ClientBindingState::Left {
296 conversation_id: value.conversation_id(),
297 participant_id: value.participant_id(),
298 generation: value.retired_generation(),
299 };
300 aggregate.detach_replay.apply_leave(value);
301 }
302 ServerValue::Retired(value) => {
303 apply_retired(aggregate, value);
304 }
305 ServerValue::ParticipantTransportRejected(_)
306 | ServerValue::AttemptTokenBodyConflict(_)
307 | ServerValue::ConnectionConversationCapacityExceeded(_)
308 | ServerValue::ConnectionConversationBindingOccupied(_)
309 | ServerValue::ConversationOrderExhausted(_)
310 | ServerValue::ParticipantUnknown(_)
311 | ServerValue::NoBinding(_)
312 | ServerValue::StaleAuthority(_)
313 | ServerValue::MarkerClosureCapacityExceeded(_)
314 | ServerValue::EnrollmentKnown(_)
315 | ServerValue::ReceiptExpired(_)
316 | ServerValue::ReceiptCapacityExceeded(_)
317 | ServerValue::IdentityCapacityExceeded(_)
318 | ServerValue::ObserverBackpressure(_)
319 | ServerValue::ConversationSequenceExhausted(_)
320 | ServerValue::StaleOrUnknownReceipt(_)
321 | ServerValue::MarkerNotDelivered(_)
322 | ServerValue::MarkerMismatch(_)
323 | ServerValue::UnboundReceipt(_)
324 | ServerValue::AckCommitted(_)
325 | ServerValue::AckNoOp(_)
326 | ServerValue::AckGap(_)
327 | ServerValue::AckRegression(_)
328 | ServerValue::MarkerAckCommitted(_)
329 | ServerValue::RecordCommitted(_)
330 | ServerValue::RecordTooLarge(_)
331 | ServerValue::ObserverRecoveryAccepted(_)
332 | ServerValue::InvalidObserverEpoch(_)
333 | ServerValue::InvalidObserverEpochList(_) => {}
334 }
335}
336
337const fn apply_enroll_bound(
338 aggregate: &mut ClientParticipantAggregate,
339 value: &crate::wire::EnrollBound,
340) {
341 aggregate.binding = ClientBindingState::Bound {
342 conversation_id: value.conversation_id(),
343 participant_id: value.participant_id(),
344 generation: value.capability_generation(),
345 attach_secret: value.attach_secret(),
346 binding_epoch: value.origin_binding_epoch(),
347 };
348}
349
350const fn apply_attach_bound(aggregate: &mut ClientParticipantAggregate, value: &AttachBound) {
351 aggregate.binding = ClientBindingState::Bound {
352 conversation_id: value.conversation_id(),
353 participant_id: value.participant_id(),
354 generation: value.capability_generation(),
355 attach_secret: value.attach_secret(),
356 binding_epoch: value.origin_binding_epoch(),
357 };
358}
359
360fn apply_retired(aggregate: &mut ClientParticipantAggregate, value: &crate::wire::Retired) {
361 let (conversation_id, participant_id, generation) = match value {
362 crate::wire::Retired::Enrollment {
363 request,
364 participant_id,
365 retired_generation,
366 } => (
367 request.conversation_id,
368 *participant_id,
369 *retired_generation,
370 ),
371 crate::wire::Retired::Participant {
372 request,
373 retired_generation,
374 } => {
375 let (conversation_id, participant_id) = participant_reference_identity(request);
376 (conversation_id, participant_id, *retired_generation)
377 }
378 };
379 aggregate.binding = ClientBindingState::Left {
380 conversation_id,
381 participant_id,
382 generation,
383 };
384 aggregate
385 .detach_replay
386 .apply_retired(conversation_id, participant_id, generation);
387}
388
389const fn participant_reference_identity(
390 request: &crate::wire::ParticipantReferenceEnvelope,
391) -> (u64, u64) {
392 match request {
393 crate::wire::ParticipantReferenceEnvelope::CredentialAttach(value) => {
394 (value.conversation_id, value.participant_id)
395 }
396 crate::wire::ParticipantReferenceEnvelope::Detach(value) => {
397 (value.conversation_id, value.participant_id)
398 }
399 crate::wire::ParticipantReferenceEnvelope::ParticipantAck(value) => {
400 (value.conversation_id, value.participant_id)
401 }
402 crate::wire::ParticipantReferenceEnvelope::Leave(value) => {
403 (value.conversation_id, value.participant_id)
404 }
405 crate::wire::ParticipantReferenceEnvelope::MarkerAck(value) => {
406 (value.conversation_id, value.participant_id)
407 }
408 crate::wire::ParticipantReferenceEnvelope::RecordAdmission(value) => {
409 (value.conversation_id, value.participant_id)
410 }
411 }
412}