1use super::{ClientParticipantAggregate, ClientResponseCorrelation};
2use crate::wire::{
3 AttachBound, DetachCommitted, DetachEnvelope, DetachInProgress, LeaveCommitted,
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}
32
33#[derive(Clone, Debug, PartialEq, Eq)]
34pub(super) enum DetachReplayState {
35 Empty,
36 Recorded {
37 request: DetachEnvelope,
38 status: DetachReplayStatus,
39 },
40}
41
42#[derive(Debug, PartialEq, Eq)]
44pub struct SdkDetachReplayAggregate {
45 pub(super) state: DetachReplayState,
46}
47
48impl SdkDetachReplayAggregate {
49 pub(super) const fn new() -> Self {
50 Self {
51 state: DetachReplayState::Empty,
52 }
53 }
54
55 #[must_use]
57 pub const fn request(&self) -> Option<&DetachEnvelope> {
58 match &self.state {
59 DetachReplayState::Empty => None,
60 DetachReplayState::Recorded { request, .. } => Some(request),
61 }
62 }
63
64 #[must_use]
66 pub const fn status(&self) -> Option<&DetachReplayStatus> {
67 match &self.state {
68 DetachReplayState::Empty => None,
69 DetachReplayState::Recorded { status, .. } => Some(status),
70 }
71 }
72
73 pub(super) const fn mark_initial_attempt_started(&mut self) {
74 if let DetachReplayState::Recorded { status, .. } = &mut self.state {
75 if matches!(status, DetachReplayStatus::Parked) {
76 *status = DetachReplayStatus::InFlight;
77 }
78 }
79 }
80
81 pub(super) fn can_replace_with(&self, request: &DetachEnvelope) -> bool {
82 match &self.state {
83 DetachReplayState::Recorded {
84 request: retained,
85 status,
86 } => {
87 retained.conversation_id == request.conversation_id
88 && retained.participant_id == request.participant_id
89 && request.capability_generation > retained.capability_generation
90 && matches!(
91 status,
92 DetachReplayStatus::Superseded | DetachReplayStatus::Terminal(_)
93 )
94 }
95 DetachReplayState::Empty => false,
96 }
97 }
98
99 pub(super) fn apply_attach(&mut self, attach: &AttachBound) -> bool {
100 let DetachReplayState::Recorded { request, status } = &mut self.state else {
101 return false;
102 };
103 if attach.conversation_id() == request.conversation_id
104 && attach.participant_id() == request.participant_id
105 && attach.request_generation() == request.capability_generation
106 && attach.capability_generation() > request.capability_generation
107 {
108 *status = DetachReplayStatus::Superseded;
109 true
110 } else {
111 false
112 }
113 }
114
115 pub(super) fn apply_leave(&mut self, leave: &LeaveCommitted) -> bool {
116 let DetachReplayState::Recorded { request, status } = &mut self.state else {
117 return false;
118 };
119 if leave.conversation_id() == request.conversation_id
120 && leave.participant_id() == request.participant_id
121 && leave.presented_generation() == request.capability_generation
122 {
123 *status = DetachReplayStatus::LeaveSuperseded;
124 true
125 } else {
126 false
127 }
128 }
129
130 pub(super) fn apply_retired(
131 &mut self,
132 conversation_id: u64,
133 participant_id: u64,
134 retired_generation: crate::wire::Generation,
135 ) -> bool {
136 let DetachReplayState::Recorded { request, status } = &mut self.state else {
137 return false;
138 };
139 if request.conversation_id == conversation_id
140 && request.participant_id == participant_id
141 && retired_generation >= request.capability_generation
142 {
143 *status = DetachReplayStatus::LeaveSuperseded;
144 true
145 } else {
146 false
147 }
148 }
149
150 pub(super) fn apply_detach_committed(&mut self, value: &DetachCommitted) -> bool {
151 let DetachReplayState::Recorded { request, status } = &mut self.state else {
152 return false;
153 };
154 if detach_committed_matches(request, value) {
155 *status =
156 DetachReplayStatus::Terminal(DetachReplayTerminal::DetachCommitted(value.clone()));
157 true
158 } else {
159 false
160 }
161 }
162
163 pub(super) fn apply_detach_in_progress(&mut self, value: &DetachInProgress) -> bool {
164 let DetachReplayState::Recorded { request, status } = &mut self.state else {
165 return false;
166 };
167 if detach_in_progress_matches(request, value) {
168 *status =
169 DetachReplayStatus::Terminal(DetachReplayTerminal::DetachInProgress(value.clone()));
170 true
171 } else {
172 false
173 }
174 }
175
176 pub(super) fn apply_terminalized_detach_cell(
177 &mut self,
178 value: &TerminalizedDetachCell,
179 ) -> bool {
180 let DetachReplayState::Recorded { request, status } = &mut self.state else {
181 return false;
182 };
183 if terminalized_matches(request, value) {
184 *status = DetachReplayStatus::Terminal(DetachReplayTerminal::TerminalizedDetachCell(
185 value.clone(),
186 ));
187 true
188 } else {
189 false
190 }
191 }
192}
193
194#[derive(Clone, Copy, Debug, PartialEq, Eq)]
196pub enum DetachReplayRefusalReason {
197 AlreadyRecorded,
199 InvalidStatus,
201 ForeignInput,
203 LostAuthorityPending,
206}
207
208#[derive(Debug, PartialEq, Eq)]
210pub struct DetachReplayApplied {
211 aggregate: ClientParticipantAggregate,
212}
213
214impl DetachReplayApplied {
215 #[must_use]
217 pub fn into_aggregate(self) -> ClientParticipantAggregate {
218 self.aggregate
219 }
220}
221
222#[derive(Debug, PartialEq, Eq)]
224pub struct DetachReplayRefusal<T> {
225 aggregate: ClientParticipantAggregate,
226 input: T,
227 reason: DetachReplayRefusalReason,
228}
229
230impl<T> DetachReplayRefusal<T> {
231 #[must_use]
233 pub const fn reason(&self) -> DetachReplayRefusalReason {
234 self.reason
235 }
236
237 #[must_use]
239 pub fn into_parts(self) -> (ClientParticipantAggregate, T) {
240 (self.aggregate, self.input)
241 }
242}
243
244#[derive(Debug, PartialEq, Eq)]
246pub struct DetachTransportAttempt {
247 request: DetachEnvelope,
248 authorization: u64,
249}
250
251impl DetachTransportAttempt {
252 #[must_use]
254 pub const fn request(&self) -> &DetachEnvelope {
255 &self.request
256 }
257
258 #[must_use]
262 pub const fn into_request(self) -> (DetachEnvelope, ClientResponseCorrelation) {
263 (
264 self.request,
265 ClientResponseCorrelation {
266 authorization: self.authorization,
267 },
268 )
269 }
270}
271
272#[derive(Debug, PartialEq, Eq)]
274pub enum DetachTransportAttemptDecision {
275 Started {
277 aggregate: ClientParticipantAggregate,
279 attempt: DetachTransportAttempt,
281 },
282 Refused(DetachReplayRefusal<()>),
284}
285
286#[must_use]
293pub fn transport_attempt_started(
294 mut aggregate: ClientParticipantAggregate,
295) -> DetachTransportAttemptDecision {
296 let request = match &aggregate.detach_replay.state {
297 DetachReplayState::Recorded {
298 request,
299 status: DetachReplayStatus::Parked,
300 } => request.clone(),
301 DetachReplayState::Empty | DetachReplayState::Recorded { .. } => {
302 return DetachTransportAttemptDecision::Refused(DetachReplayRefusal {
303 aggregate,
304 input: (),
305 reason: DetachReplayRefusalReason::InvalidStatus,
306 });
307 }
308 };
309 let expected_matches = aggregate.expected.as_ref().is_some_and(|expected| {
310 expected.authorization != 0
311 && matches!(&expected.request, crate::wire::ClientRequest::Detach(value)
312 if value.conversation_id == request.conversation_id
313 && value.participant_id == request.participant_id
314 && value.capability_generation == request.capability_generation
315 && value.detach_attempt_token == request.detach_attempt_token)
316 });
317 if !expected_matches {
318 return DetachTransportAttemptDecision::Refused(DetachReplayRefusal {
319 aggregate,
320 input: (),
321 reason: DetachReplayRefusalReason::InvalidStatus,
322 });
323 }
324 let authorization = aggregate
325 .expected
326 .as_ref()
327 .map_or(0, |expected| expected.authorization);
328 if let Some(expected) = aggregate.expected.as_mut() {
329 expected.issued = true;
330 }
331 if let DetachReplayState::Recorded { status, .. } = &mut aggregate.detach_replay.state {
332 *status = DetachReplayStatus::InFlight;
333 }
334 DetachTransportAttemptDecision::Started {
335 aggregate,
336 attempt: DetachTransportAttempt {
337 request,
338 authorization,
339 },
340 }
341}
342
343#[derive(Clone, Copy, Debug, PartialEq, Eq)]
345pub enum DetachTransportFate {
346 ResponseUnavailable,
348}
349
350#[derive(Debug, PartialEq, Eq)]
352pub enum DetachTransportFateDecision {
353 Parked(DetachReplayApplied),
355 Refused(DetachReplayRefusal<(ClientResponseCorrelation, DetachTransportFate)>),
357}
358
359#[must_use]
365pub fn transport_fate(
366 mut aggregate: ClientParticipantAggregate,
367 correlation: ClientResponseCorrelation,
368 fate: DetachTransportFate,
369) -> DetachTransportFateDecision {
370 if aggregate.operation_loss_pending() {
371 return DetachTransportFateDecision::Refused(DetachReplayRefusal {
372 aggregate,
373 input: (correlation, fate),
374 reason: DetachReplayRefusalReason::LostAuthorityPending,
375 });
376 }
377 if detach_authority_matches(&aggregate, &correlation) {
378 if let DetachReplayState::Recorded { status, .. } = &mut aggregate.detach_replay.state {
379 *status = DetachReplayStatus::Parked;
380 }
381 if let Some(expected) = aggregate.expected.as_mut() {
382 expected.issued = false;
383 }
384 return DetachTransportFateDecision::Parked(DetachReplayApplied { aggregate });
385 }
386 DetachTransportFateDecision::Refused(DetachReplayRefusal {
387 aggregate,
388 input: (correlation, fate),
389 reason: DetachReplayRefusalReason::InvalidStatus,
390 })
391}
392
393#[derive(Debug, PartialEq, Eq)]
395pub enum ApplyAttachDecision {
396 Superseded(DetachReplayApplied),
398 Refused(DetachReplayRefusal<(AttachBound, ClientResponseCorrelation)>),
400}
401
402#[must_use]
404pub fn apply_attach(
405 mut aggregate: ClientParticipantAggregate,
406 attach: AttachBound,
407 correlation: ClientResponseCorrelation,
408) -> ApplyAttachDecision {
409 if aggregate.operation_loss_pending() {
410 return ApplyAttachDecision::Refused(DetachReplayRefusal {
411 aggregate,
412 input: (attach, correlation),
413 reason: DetachReplayRefusalReason::LostAuthorityPending,
414 });
415 }
416 if detach_authority_matches(&aggregate, &correlation)
417 && aggregate.detach_replay.apply_attach(&attach)
418 {
419 aggregate.expected = None;
420 ApplyAttachDecision::Superseded(DetachReplayApplied { aggregate })
421 } else {
422 ApplyAttachDecision::Refused(DetachReplayRefusal {
423 aggregate,
424 input: (attach, correlation),
425 reason: DetachReplayRefusalReason::ForeignInput,
426 })
427 }
428}
429
430#[derive(Debug, PartialEq, Eq)]
432pub enum ApplyLeaveDecision {
433 Superseded(DetachReplayApplied),
435 Refused(DetachReplayRefusal<(LeaveCommitted, ClientResponseCorrelation)>),
437}
438
439#[must_use]
441pub fn apply_leave_durable(
442 mut aggregate: ClientParticipantAggregate,
443 leave: LeaveCommitted,
444 correlation: ClientResponseCorrelation,
445) -> ApplyLeaveDecision {
446 if aggregate.operation_loss_pending() {
447 return ApplyLeaveDecision::Refused(DetachReplayRefusal {
448 aggregate,
449 input: (leave, correlation),
450 reason: DetachReplayRefusalReason::LostAuthorityPending,
451 });
452 }
453 if detach_authority_matches(&aggregate, &correlation)
454 && aggregate.detach_replay.apply_leave(&leave)
455 {
456 aggregate.expected = None;
457 ApplyLeaveDecision::Superseded(DetachReplayApplied { aggregate })
458 } else {
459 ApplyLeaveDecision::Refused(DetachReplayRefusal {
460 aggregate,
461 input: (leave, correlation),
462 reason: DetachReplayRefusalReason::ForeignInput,
463 })
464 }
465}
466
467#[derive(Debug, PartialEq, Eq)]
469pub enum DetachReplayOutcome {
470 DetachCommitted(DetachCommitted),
472 DetachInProgress(DetachInProgress),
474 TerminalizedDetachCell(TerminalizedDetachCell),
476}
477
478#[derive(Debug, PartialEq, Eq)]
480pub enum ApplyDetachOutcomeDecision {
481 Terminal(DetachReplayApplied),
483 Refused(DetachReplayRefusal<(DetachReplayOutcome, ClientResponseCorrelation)>),
485}
486
487#[must_use]
489pub fn apply_detach_outcome(
490 mut aggregate: ClientParticipantAggregate,
491 outcome: DetachReplayOutcome,
492 correlation: ClientResponseCorrelation,
493) -> ApplyDetachOutcomeDecision {
494 if aggregate.operation_loss_pending() {
495 return ApplyDetachOutcomeDecision::Refused(DetachReplayRefusal {
496 aggregate,
497 input: (outcome, correlation),
498 reason: DetachReplayRefusalReason::LostAuthorityPending,
499 });
500 }
501 if !detach_authority_matches(&aggregate, &correlation) {
502 return ApplyDetachOutcomeDecision::Refused(DetachReplayRefusal {
503 aggregate,
504 input: (outcome, correlation),
505 reason: DetachReplayRefusalReason::InvalidStatus,
506 });
507 }
508 let applied = match &outcome {
509 DetachReplayOutcome::DetachCommitted(value) => {
510 aggregate.detach_replay.apply_detach_committed(value)
511 }
512 DetachReplayOutcome::DetachInProgress(value) => {
513 aggregate.detach_replay.apply_detach_in_progress(value)
514 }
515 DetachReplayOutcome::TerminalizedDetachCell(value) => aggregate
516 .detach_replay
517 .apply_terminalized_detach_cell(value),
518 };
519 if applied {
520 aggregate.expected = None;
521 ApplyDetachOutcomeDecision::Terminal(DetachReplayApplied { aggregate })
522 } else {
523 ApplyDetachOutcomeDecision::Refused(DetachReplayRefusal {
524 aggregate,
525 input: (outcome, correlation),
526 reason: DetachReplayRefusalReason::ForeignInput,
527 })
528 }
529}
530
531fn detach_authority_matches(
532 aggregate: &ClientParticipantAggregate,
533 correlation: &ClientResponseCorrelation,
534) -> bool {
535 let Some(expected) = aggregate.expected.as_ref() else {
536 return false;
537 };
538 if !expected.issued || expected.authorization != correlation.authorization {
539 return false;
540 }
541 let DetachReplayState::Recorded {
542 request,
543 status: DetachReplayStatus::InFlight,
544 } = &aggregate.detach_replay.state
545 else {
546 return false;
547 };
548 matches!(&expected.request, crate::wire::ClientRequest::Detach(value)
549 if value.conversation_id == request.conversation_id
550 && value.participant_id == request.participant_id
551 && value.capability_generation == request.capability_generation
552 && value.detach_attempt_token == request.detach_attempt_token)
553}
554
555fn detach_committed_matches(request: &DetachEnvelope, value: &DetachCommitted) -> bool {
556 value.conversation_id() == request.conversation_id
557 && value.participant_id() == request.participant_id
558 && value.capability_generation() == request.capability_generation
559 && value.detach_attempt_token() == request.detach_attempt_token
560}
561
562fn detach_in_progress_matches(request: &DetachEnvelope, value: &DetachInProgress) -> bool {
563 let expected_generation = request.capability_generation;
564 let presented_generation = value.presented_generation;
565 let expected_token = request.detach_attempt_token;
566 let presented_token = value.presented_token;
567 value.conversation_id == request.conversation_id
568 && value.participant_id == request.participant_id
569 && presented_generation == expected_generation
570 && presented_token == expected_token
571}
572
573fn terminalized_matches(request: &DetachEnvelope, value: &TerminalizedDetachCell) -> bool {
574 value.conversation_id() == request.conversation_id
575 && value.participant_id() == request.participant_id
576 && value.capability_generation() == request.capability_generation
577 && value.detach_attempt_token() == request.detach_attempt_token
578}