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)]
107pub struct ClientCorrelatedInboundRetained {
108 aggregate: ClientParticipantAggregate,
109 value: ServerValue,
110 correlation: ClientResponseCorrelation,
111}
112
113impl ClientCorrelatedInboundRetained {
114 #[must_use]
116 pub fn into_parts(
117 self,
118 ) -> (
119 ClientParticipantAggregate,
120 ServerValue,
121 ClientResponseCorrelation,
122 ) {
123 (self.aggregate, self.value, self.correlation)
124 }
125}
126
127#[derive(Debug, PartialEq, Eq)]
129pub enum ClientCorrelatedInboundDecision {
130 Applied(ClientInboundApplied),
133 AppliedRetaining(ClientCorrelatedInboundRetained),
136 Refused(ClientCorrelatedInboundRefusal),
138}
139
140#[must_use]
142pub fn decide_inbound(
143 aggregate: ClientParticipantAggregate,
144 value: ServerValue,
145) -> ClientInboundDecision {
146 decide_inbound_inner(aggregate, value, false)
147}
148
149#[must_use]
151pub fn decide_correlated_inbound(
152 aggregate: ClientParticipantAggregate,
153 value: ServerValue,
154 correlation: ClientResponseCorrelation,
155) -> ClientCorrelatedInboundDecision {
156 let current_authority = aggregate.expected.as_ref().is_some_and(|expected| {
157 expected.issued && expected.authorization == correlation.authorization
158 });
159 if !current_authority {
160 return ClientCorrelatedInboundDecision::Refused(ClientCorrelatedInboundRefusal {
161 aggregate,
162 value,
163 correlation,
164 reason: ClientInboundRefusalReason::DelayedResponse,
165 });
166 }
167 match decide_inbound_inner(aggregate, value, true) {
168 ClientInboundDecision::Applied(applied) => {
169 let still_owed = applied.aggregate.expected.as_ref().is_some_and(|expected| {
174 expected.issued && expected.authorization == correlation.authorization
175 });
176 if still_owed {
177 ClientCorrelatedInboundDecision::AppliedRetaining(ClientCorrelatedInboundRetained {
178 aggregate: applied.aggregate,
179 value: applied.value,
180 correlation,
181 })
182 } else {
183 ClientCorrelatedInboundDecision::Applied(applied)
184 }
185 }
186 ClientInboundDecision::Refused(refusal) => {
187 let reason = refusal.reason();
188 let (aggregate, value) = refusal.into_parts();
189 ClientCorrelatedInboundDecision::Refused(ClientCorrelatedInboundRefusal {
190 aggregate,
191 value,
192 correlation,
193 reason,
194 })
195 }
196 }
197}
198
199fn decide_inbound_inner(
200 mut aggregate: ClientParticipantAggregate,
201 value: ServerValue,
202 has_response_authority: bool,
203) -> ClientInboundDecision {
204 if aggregate.binding.is_left() {
205 return inbound_refusal(aggregate, value, ClientInboundRefusalReason::AlreadyDead);
206 }
207
208 if let Some(request) = correlation::participant_ack_request(&value) {
209 if aggregate.binding.matches_ack(request) {
210 return ClientInboundDecision::Applied(ClientInboundApplied { aggregate, value });
211 }
212 return inbound_refusal(
213 aggregate,
214 value,
215 ClientInboundRefusalReason::ForeignResponse,
216 );
217 }
218
219 if matches!(value, ServerValue::ParticipantTransportRejected(_)) {
220 return ClientInboundDecision::Applied(ClientInboundApplied { aggregate, value });
221 }
222
223 let Some(expected) = aggregate.expected.as_ref() else {
224 return inbound_refusal(
225 aggregate,
226 value,
227 ClientInboundRefusalReason::DelayedResponse,
228 );
229 };
230
231 if expected.lost.is_some() {
232 return inbound_refusal(
233 aggregate,
234 value,
235 ClientInboundRefusalReason::LostAuthorityPending,
236 );
237 }
238
239 if !has_response_authority {
240 return inbound_refusal(
241 aggregate,
242 value,
243 ClientInboundRefusalReason::MissingResponseAuthority,
244 );
245 }
246
247 if !aggregate.binding.accepts_request(&expected.request) {
248 return inbound_refusal(
249 aggregate,
250 value,
251 ClientInboundRefusalReason::ForeignResponse,
252 );
253 }
254
255 if !correlation::matches_request(&value, &expected.request) {
256 let same_request_class = value.originating_request()
257 == Some(expected.request.discriminant())
258 || matches!(
259 (&value, &expected.request),
260 (
261 ServerValue::ObserverRecoveryAccepted(_)
262 | ServerValue::InvalidObserverEpoch(_)
263 | ServerValue::InvalidObserverEpochList(_),
264 crate::wire::ClientRequest::ObserverRecovery(_)
265 )
266 );
267 let same_identity = correlation::same_identity(&value, &expected.request);
268 let reason = if same_request_class && same_identity {
269 if matches!(
270 expected.request,
271 crate::wire::ClientRequest::RecordAdmission(_)
272 ) {
273 ClientInboundRefusalReason::AmbiguousResponse
274 } else {
275 ClientInboundRefusalReason::DelayedResponse
276 }
277 } else {
278 ClientInboundRefusalReason::ForeignResponse
279 };
280 return inbound_refusal(aggregate, value, reason);
281 }
282
283 retire_expected_operation(&mut aggregate, &value);
284 ClientInboundDecision::Applied(ClientInboundApplied { aggregate, value })
285}
286
287fn retire_expected_operation(aggregate: &mut ClientParticipantAggregate, value: &ServerValue) {
314 let expected_detach = match aggregate
315 .expected
316 .as_ref()
317 .map(|expected| &expected.request)
318 {
319 Some(crate::wire::ClientRequest::Detach(request)) => Some(request.clone()),
320 _ => None,
321 };
322 aggregate.expected = None;
323 apply_correlated_value(aggregate, value);
324 if let Some(request) = expected_detach {
325 let envelope = crate::wire::DetachEnvelope {
326 conversation_id: request.conversation_id,
327 participant_id: request.participant_id,
328 capability_generation: request.capability_generation,
329 detach_attempt_token: request.detach_attempt_token,
330 };
331 aggregate
332 .detach_replay
333 .settle_refused_authority(&envelope, value);
334 debug_assert!(
335 !aggregate.detach_replay.is_active()
336 || aggregate.detach_replay.request() != Some(&envelope),
337 "retiring an expected detach must leave its own replay settled"
338 );
339 }
340 debug_assert!(
341 aggregate.expected.is_none(),
342 "the expected slot must be retired by this statement"
343 );
344}
345
346const fn inbound_refusal(
347 aggregate: ClientParticipantAggregate,
348 value: ServerValue,
349 reason: ClientInboundRefusalReason,
350) -> ClientInboundDecision {
351 ClientInboundDecision::Refused(ClientInboundRefusal {
352 aggregate,
353 value,
354 reason,
355 })
356}
357
358fn apply_correlated_value(aggregate: &mut ClientParticipantAggregate, value: &ServerValue) {
359 match value {
360 ServerValue::EnrollBound(value) => apply_enroll_bound(aggregate, value),
361 ServerValue::Bound(ReceiptReplay::Enrollment(value)) => {
362 apply_enroll_bound(aggregate, value);
363 }
364 ServerValue::AttachBound(value)
365 | ServerValue::Bound(ReceiptReplay::CredentialAttach(value)) => {
366 apply_attach_bound(aggregate, value);
367 aggregate.detach_replay.apply_attach(value);
368 }
369 ServerValue::UnboundReceipt(ReceiptReplay::CredentialAttach(value)) => {
370 apply_unbound_attach_receipt(aggregate, value);
371 aggregate.detach_replay.apply_attach(value);
372 }
373 ServerValue::DetachCommitted(value) => {
374 let attach_secret = match aggregate.binding {
375 ClientBindingState::Bound { attach_secret, .. }
376 | ClientBindingState::Detached { attach_secret, .. } => attach_secret,
377 ClientBindingState::Unbound | ClientBindingState::Left { .. } => return,
378 };
379 aggregate.binding = ClientBindingState::Detached {
380 conversation_id: value.conversation_id(),
381 participant_id: value.participant_id(),
382 generation: value.capability_generation(),
383 attach_secret,
384 };
385 aggregate.detach_replay.apply_detach_committed(value);
386 }
387 ServerValue::DetachInProgress(value) => {
388 aggregate.detach_replay.apply_detach_in_progress(value);
389 }
390 ServerValue::StaleAuthority(crate::wire::StaleAuthority::Detach(
391 crate::wire::DetachStaleAuthority::TerminalizedDetachCell(value),
392 )) => {
393 aggregate
394 .detach_replay
395 .apply_terminalized_detach_cell(value);
396 }
397 ServerValue::LeaveCommitted(value) => {
398 aggregate.binding = ClientBindingState::Left {
399 conversation_id: value.conversation_id(),
400 participant_id: value.participant_id(),
401 generation: value.retired_generation(),
402 };
403 aggregate.detach_replay.apply_leave(value);
404 }
405 ServerValue::Retired(value) => {
406 apply_retired(aggregate, value);
407 }
408 ServerValue::ParticipantTransportRejected(_)
409 | ServerValue::AttemptTokenBodyConflict(_)
410 | ServerValue::ConnectionConversationCapacityExceeded(_)
411 | ServerValue::ConnectionConversationBindingOccupied(_)
412 | ServerValue::ConversationOrderExhausted(_)
413 | ServerValue::ParticipantUnknown(_)
414 | ServerValue::NoBinding(_)
415 | ServerValue::StaleAuthority(_)
416 | ServerValue::MarkerClosureCapacityExceeded(_)
417 | ServerValue::EnrollmentKnown(_)
418 | ServerValue::ReceiptExpired(_)
419 | ServerValue::ReceiptCapacityExceeded(_)
420 | ServerValue::IdentityCapacityExceeded(_)
421 | ServerValue::ObserverBackpressure(_)
422 | ServerValue::ConversationSequenceExhausted(_)
423 | ServerValue::StaleOrUnknownReceipt(_)
424 | ServerValue::MarkerNotDelivered(_)
425 | ServerValue::MarkerMismatch(_)
426 | ServerValue::UnboundReceipt(ReceiptReplay::Enrollment(_))
431 | ServerValue::AckCommitted(_)
432 | ServerValue::AckNoOp(_)
433 | ServerValue::AckGap(_)
434 | ServerValue::AckRegression(_)
435 | ServerValue::MarkerAckCommitted(_)
436 | ServerValue::RecordCommitted(_)
437 | ServerValue::RecordTooLarge(_)
438 | ServerValue::RecordAdmissionProtocolFault(_)
442 | ServerValue::ObserverRecoveryAccepted(_)
443 | ServerValue::InvalidObserverEpoch(_)
444 | ServerValue::InvalidObserverEpochList(_)
445 | ServerValue::MarkerSettlementBackpressure(_)
450 | ServerValue::EnrollmentSettlementBackpressure(_) => {}
451 }
452}
453
454const fn apply_enroll_bound(
455 aggregate: &mut ClientParticipantAggregate,
456 value: &crate::wire::EnrollBound,
457) {
458 aggregate.binding = ClientBindingState::Bound {
459 conversation_id: value.conversation_id(),
460 participant_id: value.participant_id(),
461 generation: value.capability_generation(),
462 attach_secret: value.attach_secret(),
463 binding_epoch: value.origin_binding_epoch(),
464 };
465}
466
467const fn apply_unbound_attach_receipt(
495 aggregate: &mut ClientParticipantAggregate,
496 value: &AttachBound,
497) {
498 aggregate.binding = ClientBindingState::Detached {
499 conversation_id: value.conversation_id(),
500 participant_id: value.participant_id(),
501 generation: value.capability_generation(),
502 attach_secret: value.attach_secret(),
503 };
504}
505
506const fn apply_attach_bound(aggregate: &mut ClientParticipantAggregate, value: &AttachBound) {
507 aggregate.binding = ClientBindingState::Bound {
508 conversation_id: value.conversation_id(),
509 participant_id: value.participant_id(),
510 generation: value.capability_generation(),
511 attach_secret: value.attach_secret(),
512 binding_epoch: value.origin_binding_epoch(),
513 };
514}
515
516fn apply_retired(aggregate: &mut ClientParticipantAggregate, value: &crate::wire::Retired) {
517 let (conversation_id, participant_id, generation) = match value {
518 crate::wire::Retired::Enrollment {
519 request,
520 participant_id,
521 retired_generation,
522 } => (
523 request.conversation_id,
524 *participant_id,
525 *retired_generation,
526 ),
527 crate::wire::Retired::Participant {
528 request,
529 retired_generation,
530 } => {
531 let (conversation_id, participant_id) = participant_reference_identity(request);
532 (conversation_id, participant_id, *retired_generation)
533 }
534 };
535 aggregate.binding = ClientBindingState::Left {
536 conversation_id,
537 participant_id,
538 generation,
539 };
540 aggregate
541 .detach_replay
542 .apply_retired(conversation_id, participant_id, generation);
543}
544
545const fn participant_reference_identity(
546 request: &crate::wire::ParticipantReferenceEnvelope,
547) -> (u64, u64) {
548 match request {
549 crate::wire::ParticipantReferenceEnvelope::CredentialAttach(value) => {
550 (value.conversation_id, value.participant_id)
551 }
552 crate::wire::ParticipantReferenceEnvelope::Detach(value) => {
553 (value.conversation_id, value.participant_id)
554 }
555 crate::wire::ParticipantReferenceEnvelope::ParticipantAck(value) => {
556 (value.conversation_id, value.participant_id)
557 }
558 crate::wire::ParticipantReferenceEnvelope::Leave(value) => {
559 (value.conversation_id, value.participant_id)
560 }
561 crate::wire::ParticipantReferenceEnvelope::MarkerAck(value) => {
562 (value.conversation_id, value.participant_id)
563 }
564 crate::wire::ParticipantReferenceEnvelope::RecordAdmission(value) => {
565 (value.conversation_id, value.participant_id)
566 }
567 }
568}