1use super::{ClientParticipantAggregate, ClientResponseCorrelation};
2use crate::wire::{
3 AttachBound, DetachCommitted, DetachEnvelope, DetachInProgress, LeaveCommitted, ServerValue,
4 TerminalizedDetachCell,
5};
6
7#[derive(Clone, Debug, PartialEq, Eq)]
9pub enum DetachReplayStatus {
10 Parked,
12 InFlight,
14 Superseded,
16 LeaveSuperseded,
18 Terminal(DetachReplayTerminal),
20}
21
22#[derive(Clone, Debug, PartialEq, Eq)]
24pub enum DetachReplayTerminal {
25 DetachCommitted(DetachCommitted),
27 DetachInProgress(DetachInProgress),
29 TerminalizedDetachCell(TerminalizedDetachCell),
31 AuthorityRefused(DetachAuthorityRefused),
33}
34
35#[derive(Clone, Debug, PartialEq, Eq)]
55pub struct DetachAuthorityRefused {
56 pub(super) value: alloc::boxed::Box<ServerValue>,
57}
58
59impl DetachAuthorityRefused {
60 #[must_use]
62 pub const fn value(&self) -> &ServerValue {
63 &self.value
64 }
65}
66
67#[derive(Clone, Debug, PartialEq, Eq)]
68pub(super) enum DetachReplayState {
69 Empty,
70 Recorded {
71 request: DetachEnvelope,
72 status: DetachReplayStatus,
73 },
74}
75
76#[derive(Debug, PartialEq, Eq)]
78pub struct SdkDetachReplayAggregate {
79 pub(super) state: DetachReplayState,
80}
81
82impl SdkDetachReplayAggregate {
83 pub(super) const fn new() -> Self {
84 Self {
85 state: DetachReplayState::Empty,
86 }
87 }
88
89 #[must_use]
91 pub const fn request(&self) -> Option<&DetachEnvelope> {
92 match &self.state {
93 DetachReplayState::Empty => None,
94 DetachReplayState::Recorded { request, .. } => Some(request),
95 }
96 }
97
98 #[must_use]
100 pub const fn status(&self) -> Option<&DetachReplayStatus> {
101 match &self.state {
102 DetachReplayState::Empty => None,
103 DetachReplayState::Recorded { status, .. } => Some(status),
104 }
105 }
106
107 pub(super) fn mark_initial_attempt_started(&mut self) {
108 if let DetachReplayState::Recorded { status, .. } = &mut self.state {
109 if matches!(status, DetachReplayStatus::Parked) {
110 *status = DetachReplayStatus::InFlight;
111 }
112 }
113 }
114
115 pub(super) fn can_replace_with(&self, request: &DetachEnvelope) -> bool {
116 match &self.state {
117 DetachReplayState::Recorded {
118 request: retained,
119 status,
120 } => {
121 retained.conversation_id == request.conversation_id
122 && retained.participant_id == request.participant_id
123 && request.capability_generation > retained.capability_generation
124 && matches!(
125 status,
126 DetachReplayStatus::Superseded | DetachReplayStatus::Terminal(_)
127 )
128 }
129 DetachReplayState::Empty => false,
130 }
131 }
132
133 pub(super) fn apply_attach(&mut self, attach: &AttachBound) -> bool {
134 let DetachReplayState::Recorded { request, status } = &mut self.state else {
135 return false;
136 };
137 if attach.conversation_id() == request.conversation_id
143 && attach.participant_id() == request.participant_id
144 && attach.capability_generation() > request.capability_generation
145 {
146 *status = DetachReplayStatus::Superseded;
147 true
148 } else {
149 false
150 }
151 }
152
153 pub(super) fn apply_leave(&mut self, leave: &LeaveCommitted) -> bool {
154 let DetachReplayState::Recorded { request, status } = &mut self.state else {
155 return false;
156 };
157 if leave.conversation_id() == request.conversation_id
158 && leave.participant_id() == request.participant_id
159 && leave.presented_generation() == request.capability_generation
160 {
161 *status = DetachReplayStatus::LeaveSuperseded;
162 true
163 } else {
164 false
165 }
166 }
167
168 pub(super) fn apply_retired(
169 &mut self,
170 conversation_id: u64,
171 participant_id: u64,
172 retired_generation: crate::wire::Generation,
173 ) -> bool {
174 let DetachReplayState::Recorded { request, status } = &mut self.state else {
175 return false;
176 };
177 if request.conversation_id == conversation_id
178 && request.participant_id == participant_id
179 && retired_generation >= request.capability_generation
180 {
181 *status = DetachReplayStatus::LeaveSuperseded;
182 true
183 } else {
184 false
185 }
186 }
187
188 pub(super) fn settle_refused_authority(
200 &mut self,
201 expected: &DetachEnvelope,
202 value: &ServerValue,
203 ) -> bool {
204 let DetachReplayState::Recorded { request, status } = &mut self.state else {
205 return false;
206 };
207 if !matches!(
208 status,
209 DetachReplayStatus::Parked | DetachReplayStatus::InFlight
210 ) {
211 return false;
212 }
213 if request.conversation_id != expected.conversation_id
214 || request.participant_id != expected.participant_id
215 || request.capability_generation != expected.capability_generation
216 || request.detach_attempt_token != expected.detach_attempt_token
217 {
218 return false;
219 }
220 *status = DetachReplayStatus::Terminal(DetachReplayTerminal::AuthorityRefused(
221 DetachAuthorityRefused {
222 value: alloc::boxed::Box::new(value.clone()),
223 },
224 ));
225 true
226 }
227
228 pub(super) const fn is_active(&self) -> bool {
232 matches!(
233 self.state,
234 DetachReplayState::Recorded {
235 status: DetachReplayStatus::Parked | DetachReplayStatus::InFlight,
236 ..
237 }
238 )
239 }
240
241 pub(super) fn apply_detach_committed(&mut self, value: &DetachCommitted) -> bool {
242 let DetachReplayState::Recorded { request, status } = &mut self.state else {
243 return false;
244 };
245 if detach_committed_matches(request, value) {
246 *status =
247 DetachReplayStatus::Terminal(DetachReplayTerminal::DetachCommitted(value.clone()));
248 true
249 } else {
250 false
251 }
252 }
253
254 pub(super) fn apply_detach_in_progress(&mut self, value: &DetachInProgress) -> bool {
255 let DetachReplayState::Recorded { request, status } = &mut self.state else {
256 return false;
257 };
258 if detach_in_progress_matches(request, value) {
259 *status =
260 DetachReplayStatus::Terminal(DetachReplayTerminal::DetachInProgress(value.clone()));
261 true
262 } else {
263 false
264 }
265 }
266
267 pub(super) fn apply_terminalized_detach_cell(
268 &mut self,
269 value: &TerminalizedDetachCell,
270 ) -> bool {
271 let DetachReplayState::Recorded { request, status } = &mut self.state else {
272 return false;
273 };
274 if terminalized_matches(request, value) {
275 *status = DetachReplayStatus::Terminal(DetachReplayTerminal::TerminalizedDetachCell(
276 value.clone(),
277 ));
278 true
279 } else {
280 false
281 }
282 }
283}
284
285#[derive(Clone, Copy, Debug, PartialEq, Eq)]
287pub enum DetachReplayRefusalReason {
288 AlreadyRecorded,
290 InvalidStatus,
292 ForeignInput,
294 LostAuthorityPending,
297}
298
299#[derive(Debug, PartialEq, Eq)]
301pub struct DetachReplayApplied {
302 aggregate: ClientParticipantAggregate,
303}
304
305impl DetachReplayApplied {
306 #[must_use]
308 pub fn into_aggregate(self) -> ClientParticipantAggregate {
309 self.aggregate
310 }
311}
312
313#[derive(Debug, PartialEq, Eq)]
315pub struct DetachReplayRefusal<T> {
316 aggregate: ClientParticipantAggregate,
317 input: T,
318 reason: DetachReplayRefusalReason,
319}
320
321impl<T> DetachReplayRefusal<T> {
322 #[must_use]
324 pub const fn reason(&self) -> DetachReplayRefusalReason {
325 self.reason
326 }
327
328 #[must_use]
330 pub fn into_parts(self) -> (ClientParticipantAggregate, T) {
331 (self.aggregate, self.input)
332 }
333}
334
335#[derive(Debug, PartialEq, Eq)]
337pub struct DetachTransportAttempt {
338 request: DetachEnvelope,
339 authorization: u64,
340}
341
342impl DetachTransportAttempt {
343 #[must_use]
345 pub const fn request(&self) -> &DetachEnvelope {
346 &self.request
347 }
348
349 #[must_use]
353 pub const fn into_request(self) -> (DetachEnvelope, ClientResponseCorrelation) {
354 (
355 self.request,
356 ClientResponseCorrelation {
357 authorization: self.authorization,
358 },
359 )
360 }
361}
362
363#[derive(Debug, PartialEq, Eq)]
365pub enum DetachTransportAttemptDecision {
366 Started {
368 aggregate: ClientParticipantAggregate,
370 attempt: DetachTransportAttempt,
372 },
373 Refused(DetachReplayRefusal<()>),
375}
376
377#[must_use]
384pub fn transport_attempt_started(
385 mut aggregate: ClientParticipantAggregate,
386) -> DetachTransportAttemptDecision {
387 let request = match &aggregate.detach_replay.state {
388 DetachReplayState::Recorded {
389 request,
390 status: DetachReplayStatus::Parked,
391 } => request.clone(),
392 DetachReplayState::Empty | DetachReplayState::Recorded { .. } => {
393 return DetachTransportAttemptDecision::Refused(DetachReplayRefusal {
394 aggregate,
395 input: (),
396 reason: DetachReplayRefusalReason::InvalidStatus,
397 });
398 }
399 };
400 let expected_matches = aggregate.expected.as_ref().is_some_and(|expected| {
401 expected.authorization != 0
402 && matches!(&expected.request, crate::wire::ClientRequest::Detach(value)
403 if value.conversation_id == request.conversation_id
404 && value.participant_id == request.participant_id
405 && value.capability_generation == request.capability_generation
406 && value.detach_attempt_token == request.detach_attempt_token)
407 });
408 if !expected_matches {
409 return DetachTransportAttemptDecision::Refused(DetachReplayRefusal {
410 aggregate,
411 input: (),
412 reason: DetachReplayRefusalReason::InvalidStatus,
413 });
414 }
415 let authorization = aggregate
416 .expected
417 .as_ref()
418 .map_or(0, |expected| expected.authorization);
419 if let Some(expected) = aggregate.expected.as_mut() {
420 expected.issued = true;
421 }
422 if let DetachReplayState::Recorded { status, .. } = &mut aggregate.detach_replay.state {
423 *status = DetachReplayStatus::InFlight;
424 }
425 DetachTransportAttemptDecision::Started {
426 aggregate,
427 attempt: DetachTransportAttempt {
428 request,
429 authorization,
430 },
431 }
432}
433
434#[derive(Clone, Copy, Debug, PartialEq, Eq)]
436pub enum DetachTransportFate {
437 ResponseUnavailable,
439}
440
441#[derive(Debug, PartialEq, Eq)]
443pub enum DetachTransportFateDecision {
444 Parked(DetachReplayApplied),
446 Refused(DetachReplayRefusal<(ClientResponseCorrelation, DetachTransportFate)>),
448}
449
450#[must_use]
456pub fn transport_fate(
457 mut aggregate: ClientParticipantAggregate,
458 correlation: ClientResponseCorrelation,
459 fate: DetachTransportFate,
460) -> DetachTransportFateDecision {
461 if aggregate.operation_loss_pending() {
462 return DetachTransportFateDecision::Refused(DetachReplayRefusal {
463 aggregate,
464 input: (correlation, fate),
465 reason: DetachReplayRefusalReason::LostAuthorityPending,
466 });
467 }
468 if detach_authority_matches(&aggregate, &correlation) {
469 if let DetachReplayState::Recorded { status, .. } = &mut aggregate.detach_replay.state {
470 *status = DetachReplayStatus::Parked;
471 }
472 if let Some(expected) = aggregate.expected.as_mut() {
473 expected.issued = false;
474 }
475 return DetachTransportFateDecision::Parked(DetachReplayApplied { aggregate });
476 }
477 DetachTransportFateDecision::Refused(DetachReplayRefusal {
478 aggregate,
479 input: (correlation, fate),
480 reason: DetachReplayRefusalReason::InvalidStatus,
481 })
482}
483
484#[derive(Debug, PartialEq, Eq)]
486pub enum ApplyAttachDecision {
487 Superseded(DetachReplayApplied),
489 Refused(DetachReplayRefusal<(AttachBound, ClientResponseCorrelation)>),
491}
492
493#[must_use]
495pub fn apply_attach(
496 mut aggregate: ClientParticipantAggregate,
497 attach: AttachBound,
498 correlation: ClientResponseCorrelation,
499) -> ApplyAttachDecision {
500 if aggregate.operation_loss_pending() {
501 return ApplyAttachDecision::Refused(DetachReplayRefusal {
502 aggregate,
503 input: (attach, correlation),
504 reason: DetachReplayRefusalReason::LostAuthorityPending,
505 });
506 }
507 if detach_authority_matches(&aggregate, &correlation)
508 && aggregate.detach_replay.apply_attach(&attach)
509 {
510 aggregate.expected = None;
511 ApplyAttachDecision::Superseded(DetachReplayApplied { aggregate })
512 } else {
513 ApplyAttachDecision::Refused(DetachReplayRefusal {
514 aggregate,
515 input: (attach, correlation),
516 reason: DetachReplayRefusalReason::ForeignInput,
517 })
518 }
519}
520
521#[derive(Debug, PartialEq, Eq)]
523pub enum ApplyLeaveDecision {
524 Superseded(DetachReplayApplied),
526 Refused(DetachReplayRefusal<(LeaveCommitted, ClientResponseCorrelation)>),
528}
529
530#[must_use]
532pub fn apply_leave_durable(
533 mut aggregate: ClientParticipantAggregate,
534 leave: LeaveCommitted,
535 correlation: ClientResponseCorrelation,
536) -> ApplyLeaveDecision {
537 if aggregate.operation_loss_pending() {
538 return ApplyLeaveDecision::Refused(DetachReplayRefusal {
539 aggregate,
540 input: (leave, correlation),
541 reason: DetachReplayRefusalReason::LostAuthorityPending,
542 });
543 }
544 if detach_authority_matches(&aggregate, &correlation)
545 && aggregate.detach_replay.apply_leave(&leave)
546 {
547 aggregate.expected = None;
548 ApplyLeaveDecision::Superseded(DetachReplayApplied { aggregate })
549 } else {
550 ApplyLeaveDecision::Refused(DetachReplayRefusal {
551 aggregate,
552 input: (leave, correlation),
553 reason: DetachReplayRefusalReason::ForeignInput,
554 })
555 }
556}
557
558#[derive(Debug, PartialEq, Eq)]
560pub enum DetachReplayOutcome {
561 DetachCommitted(DetachCommitted),
563 DetachInProgress(DetachInProgress),
565 TerminalizedDetachCell(TerminalizedDetachCell),
567}
568
569#[derive(Debug, PartialEq, Eq)]
571pub enum ApplyDetachOutcomeDecision {
572 Terminal(DetachReplayApplied),
574 Refused(DetachReplayRefusal<(DetachReplayOutcome, ClientResponseCorrelation)>),
576}
577
578#[must_use]
580pub fn apply_detach_outcome(
581 mut aggregate: ClientParticipantAggregate,
582 outcome: DetachReplayOutcome,
583 correlation: ClientResponseCorrelation,
584) -> ApplyDetachOutcomeDecision {
585 if aggregate.operation_loss_pending() {
586 return ApplyDetachOutcomeDecision::Refused(DetachReplayRefusal {
587 aggregate,
588 input: (outcome, correlation),
589 reason: DetachReplayRefusalReason::LostAuthorityPending,
590 });
591 }
592 if !detach_authority_matches(&aggregate, &correlation) {
593 return ApplyDetachOutcomeDecision::Refused(DetachReplayRefusal {
594 aggregate,
595 input: (outcome, correlation),
596 reason: DetachReplayRefusalReason::InvalidStatus,
597 });
598 }
599 let applied = match &outcome {
600 DetachReplayOutcome::DetachCommitted(value) => {
601 aggregate.detach_replay.apply_detach_committed(value)
602 }
603 DetachReplayOutcome::DetachInProgress(value) => {
604 aggregate.detach_replay.apply_detach_in_progress(value)
605 }
606 DetachReplayOutcome::TerminalizedDetachCell(value) => aggregate
607 .detach_replay
608 .apply_terminalized_detach_cell(value),
609 };
610 if applied {
611 aggregate.expected = None;
612 ApplyDetachOutcomeDecision::Terminal(DetachReplayApplied { aggregate })
613 } else {
614 ApplyDetachOutcomeDecision::Refused(DetachReplayRefusal {
615 aggregate,
616 input: (outcome, correlation),
617 reason: DetachReplayRefusalReason::ForeignInput,
618 })
619 }
620}
621
622fn detach_authority_matches(
623 aggregate: &ClientParticipantAggregate,
624 correlation: &ClientResponseCorrelation,
625) -> bool {
626 let Some(expected) = aggregate.expected.as_ref() else {
627 return false;
628 };
629 if !expected.issued || expected.authorization != correlation.authorization {
630 return false;
631 }
632 let DetachReplayState::Recorded {
633 request,
634 status: DetachReplayStatus::InFlight,
635 } = &aggregate.detach_replay.state
636 else {
637 return false;
638 };
639 matches!(&expected.request, crate::wire::ClientRequest::Detach(value)
640 if value.conversation_id == request.conversation_id
641 && value.participant_id == request.participant_id
642 && value.capability_generation == request.capability_generation
643 && value.detach_attempt_token == request.detach_attempt_token)
644}
645
646fn detach_committed_matches(request: &DetachEnvelope, value: &DetachCommitted) -> bool {
647 value.conversation_id() == request.conversation_id
648 && value.participant_id() == request.participant_id
649 && value.capability_generation() == request.capability_generation
650 && value.detach_attempt_token() == request.detach_attempt_token
651}
652
653fn detach_in_progress_matches(request: &DetachEnvelope, value: &DetachInProgress) -> bool {
654 let expected_generation = request.capability_generation;
655 let presented_generation = value.presented_generation;
656 let expected_token = request.detach_attempt_token;
657 let presented_token = value.presented_token;
658 value.conversation_id == request.conversation_id
659 && value.participant_id == request.participant_id
660 && presented_generation == expected_generation
661 && presented_token == expected_token
662}
663
664fn terminalized_matches(request: &DetachEnvelope, value: &TerminalizedDetachCell) -> bool {
665 value.conversation_id() == request.conversation_id
666 && value.participant_id() == request.participant_id
667 && value.capability_generation() == request.capability_generation
668 && value.detach_attempt_token() == request.detach_attempt_token
669}