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 if matches!(value, ServerValue::WriteOutcomeUnknown(_)) {
289 return ClientInboundDecision::Applied(ClientInboundApplied { aggregate, value });
290 }
291
292 retire_expected_operation(&mut aggregate, &value);
293 ClientInboundDecision::Applied(ClientInboundApplied { aggregate, value })
294}
295
296fn retire_expected_operation(aggregate: &mut ClientParticipantAggregate, value: &ServerValue) {
323 let expected_detach = match aggregate
324 .expected
325 .as_ref()
326 .map(|expected| &expected.request)
327 {
328 Some(crate::wire::ClientRequest::Detach(request)) => Some(request.clone()),
329 _ => None,
330 };
331 aggregate.expected = None;
332 apply_correlated_value(aggregate, value);
333 if let Some(request) = expected_detach {
334 let envelope = crate::wire::DetachEnvelope {
335 conversation_id: request.conversation_id,
336 participant_id: request.participant_id,
337 capability_generation: request.capability_generation,
338 detach_attempt_token: request.detach_attempt_token,
339 };
340 aggregate
341 .detach_replay
342 .settle_refused_authority(&envelope, value);
343 debug_assert!(
344 !aggregate.detach_replay.is_active()
345 || aggregate.detach_replay.request() != Some(&envelope),
346 "retiring an expected detach must leave its own replay settled"
347 );
348 }
349 debug_assert!(
350 aggregate.expected.is_none(),
351 "the expected slot must be retired by this statement"
352 );
353}
354
355const fn inbound_refusal(
356 aggregate: ClientParticipantAggregate,
357 value: ServerValue,
358 reason: ClientInboundRefusalReason,
359) -> ClientInboundDecision {
360 ClientInboundDecision::Refused(ClientInboundRefusal {
361 aggregate,
362 value,
363 reason,
364 })
365}
366
367fn apply_correlated_value(aggregate: &mut ClientParticipantAggregate, value: &ServerValue) {
368 match value {
369 ServerValue::EnrollBound(value) => apply_enroll_bound(aggregate, value),
370 ServerValue::Bound(ReceiptReplay::Enrollment(value)) => {
371 apply_enroll_bound(aggregate, value);
372 }
373 ServerValue::AttachBound(value)
374 | ServerValue::Bound(ReceiptReplay::CredentialAttach(value)) => {
375 apply_attach_bound(aggregate, value);
376 aggregate.detach_replay.apply_attach(value);
377 }
378 ServerValue::UnboundReceipt(ReceiptReplay::CredentialAttach(value)) => {
379 apply_unbound_attach_receipt(aggregate, value);
380 aggregate.detach_replay.apply_attach(value);
381 }
382 ServerValue::DetachCommitted(value) => {
383 let attach_secret = match aggregate.binding {
384 ClientBindingState::Bound { attach_secret, .. }
385 | ClientBindingState::Detached { attach_secret, .. } => attach_secret,
386 ClientBindingState::Unbound | ClientBindingState::Left { .. } => return,
387 };
388 aggregate.binding = ClientBindingState::Detached {
389 conversation_id: value.conversation_id(),
390 participant_id: value.participant_id(),
391 generation: value.capability_generation(),
392 attach_secret,
393 };
394 aggregate.detach_replay.apply_detach_committed(value);
395 }
396 ServerValue::DetachInProgress(value) => {
397 aggregate.detach_replay.apply_detach_in_progress(value);
398 }
399 ServerValue::StaleAuthority(crate::wire::StaleAuthority::Detach(
400 crate::wire::DetachStaleAuthority::TerminalizedDetachCell(value),
401 )) => {
402 aggregate
403 .detach_replay
404 .apply_terminalized_detach_cell(value);
405 }
406 ServerValue::LeaveCommitted(value) => {
407 aggregate.binding = ClientBindingState::Left {
408 conversation_id: value.conversation_id(),
409 participant_id: value.participant_id(),
410 generation: value.retired_generation(),
411 };
412 aggregate.detach_replay.apply_leave(value);
413 }
414 ServerValue::Retired(value) => {
415 apply_retired(aggregate, value);
416 }
417 ServerValue::ParticipantTransportRejected(_)
418 | ServerValue::AttemptTokenBodyConflict(_)
419 | ServerValue::ConnectionConversationCapacityExceeded(_)
420 | ServerValue::ConnectionConversationBindingOccupied(_)
421 | ServerValue::ConversationOrderExhausted(_)
422 | ServerValue::ParticipantUnknown(_)
423 | ServerValue::NoBinding(_)
424 | ServerValue::StaleAuthority(_)
425 | ServerValue::MarkerClosureCapacityExceeded(_)
426 | ServerValue::EnrollmentKnown(_)
427 | ServerValue::ReceiptExpired(_)
428 | ServerValue::ReceiptCapacityExceeded(_)
429 | ServerValue::IdentityCapacityExceeded(_)
430 | ServerValue::ObserverBackpressure(_)
431 | ServerValue::ConversationSequenceExhausted(_)
432 | ServerValue::StaleOrUnknownReceipt(_)
433 | ServerValue::MarkerNotDelivered(_)
434 | ServerValue::MarkerMismatch(_)
435 | ServerValue::UnboundReceipt(ReceiptReplay::Enrollment(_))
440 | ServerValue::AckCommitted(_)
441 | ServerValue::AckNoOp(_)
442 | ServerValue::AckGap(_)
443 | ServerValue::AckRegression(_)
444 | ServerValue::MarkerAckCommitted(_)
445 | ServerValue::RecordCommitted(_)
446 | ServerValue::RecordTooLarge(_)
447 | ServerValue::RecordAdmissionProtocolFault(_)
451 | ServerValue::ObserverRecoveryAccepted(_)
452 | ServerValue::InvalidObserverEpoch(_)
453 | ServerValue::InvalidObserverEpochList(_)
454 | ServerValue::MarkerSettlementBackpressure(_)
459 | ServerValue::EnrollmentSettlementBackpressure(_)
460 | ServerValue::WriteOutcomeUnknown(_) => {}
462 }
463}
464
465const fn apply_enroll_bound(
466 aggregate: &mut ClientParticipantAggregate,
467 value: &crate::wire::EnrollBound,
468) {
469 aggregate.binding = ClientBindingState::Bound {
470 conversation_id: value.conversation_id(),
471 participant_id: value.participant_id(),
472 generation: value.capability_generation(),
473 attach_secret: value.attach_secret(),
474 binding_epoch: value.origin_binding_epoch(),
475 };
476}
477
478const fn apply_unbound_attach_receipt(
506 aggregate: &mut ClientParticipantAggregate,
507 value: &AttachBound,
508) {
509 aggregate.binding = ClientBindingState::Detached {
510 conversation_id: value.conversation_id(),
511 participant_id: value.participant_id(),
512 generation: value.capability_generation(),
513 attach_secret: value.attach_secret(),
514 };
515}
516
517const fn apply_attach_bound(aggregate: &mut ClientParticipantAggregate, value: &AttachBound) {
518 aggregate.binding = ClientBindingState::Bound {
519 conversation_id: value.conversation_id(),
520 participant_id: value.participant_id(),
521 generation: value.capability_generation(),
522 attach_secret: value.attach_secret(),
523 binding_epoch: value.origin_binding_epoch(),
524 };
525}
526
527fn apply_retired(aggregate: &mut ClientParticipantAggregate, value: &crate::wire::Retired) {
528 let (conversation_id, participant_id, generation) = match value {
529 crate::wire::Retired::Enrollment {
530 request,
531 participant_id,
532 retired_generation,
533 } => (
534 request.conversation_id,
535 *participant_id,
536 *retired_generation,
537 ),
538 crate::wire::Retired::Participant {
539 request,
540 retired_generation,
541 } => {
542 let (conversation_id, participant_id) = participant_reference_identity(request);
543 (conversation_id, participant_id, *retired_generation)
544 }
545 };
546 aggregate.binding = ClientBindingState::Left {
547 conversation_id,
548 participant_id,
549 generation,
550 };
551 aggregate
552 .detach_replay
553 .apply_retired(conversation_id, participant_id, generation);
554}
555
556const fn participant_reference_identity(
557 request: &crate::wire::ParticipantReferenceEnvelope,
558) -> (u64, u64) {
559 match request {
560 crate::wire::ParticipantReferenceEnvelope::CredentialAttach(value) => {
561 (value.conversation_id, value.participant_id)
562 }
563 crate::wire::ParticipantReferenceEnvelope::Detach(value) => {
564 (value.conversation_id, value.participant_id)
565 }
566 crate::wire::ParticipantReferenceEnvelope::ParticipantAck(value) => {
567 (value.conversation_id, value.participant_id)
568 }
569 crate::wire::ParticipantReferenceEnvelope::Leave(value) => {
570 (value.conversation_id, value.participant_id)
571 }
572 crate::wire::ParticipantReferenceEnvelope::MarkerAck(value) => {
573 (value.conversation_id, value.participant_id)
574 }
575 crate::wire::ParticipantReferenceEnvelope::RecordAdmission(value) => {
576 (value.conversation_id, value.participant_id)
577 }
578 }
579}