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
109 && attach.participant_id() == request.participant_id
110 && attach.capability_generation() > request.capability_generation
111 {
112 *status = DetachReplayStatus::Superseded;
113 true
114 } else {
115 false
116 }
117 }
118
119 pub(super) fn apply_leave(&mut self, leave: &LeaveCommitted) -> bool {
120 let DetachReplayState::Recorded { request, status } = &mut self.state else {
121 return false;
122 };
123 if leave.conversation_id() == request.conversation_id
124 && leave.participant_id() == request.participant_id
125 && leave.presented_generation() == request.capability_generation
126 {
127 *status = DetachReplayStatus::LeaveSuperseded;
128 true
129 } else {
130 false
131 }
132 }
133
134 pub(super) fn apply_retired(
135 &mut self,
136 conversation_id: u64,
137 participant_id: u64,
138 retired_generation: crate::wire::Generation,
139 ) -> bool {
140 let DetachReplayState::Recorded { request, status } = &mut self.state else {
141 return false;
142 };
143 if request.conversation_id == conversation_id
144 && request.participant_id == participant_id
145 && retired_generation >= request.capability_generation
146 {
147 *status = DetachReplayStatus::LeaveSuperseded;
148 true
149 } else {
150 false
151 }
152 }
153
154 pub(super) fn apply_detach_committed(&mut self, value: &DetachCommitted) -> bool {
155 let DetachReplayState::Recorded { request, status } = &mut self.state else {
156 return false;
157 };
158 if detach_committed_matches(request, value) {
159 *status =
160 DetachReplayStatus::Terminal(DetachReplayTerminal::DetachCommitted(value.clone()));
161 true
162 } else {
163 false
164 }
165 }
166
167 pub(super) fn apply_detach_in_progress(&mut self, value: &DetachInProgress) -> bool {
168 let DetachReplayState::Recorded { request, status } = &mut self.state else {
169 return false;
170 };
171 if detach_in_progress_matches(request, value) {
172 *status =
173 DetachReplayStatus::Terminal(DetachReplayTerminal::DetachInProgress(value.clone()));
174 true
175 } else {
176 false
177 }
178 }
179
180 pub(super) fn apply_terminalized_detach_cell(
181 &mut self,
182 value: &TerminalizedDetachCell,
183 ) -> bool {
184 let DetachReplayState::Recorded { request, status } = &mut self.state else {
185 return false;
186 };
187 if terminalized_matches(request, value) {
188 *status = DetachReplayStatus::Terminal(DetachReplayTerminal::TerminalizedDetachCell(
189 value.clone(),
190 ));
191 true
192 } else {
193 false
194 }
195 }
196}
197
198#[derive(Clone, Copy, Debug, PartialEq, Eq)]
200pub enum DetachReplayRefusalReason {
201 AlreadyRecorded,
203 InvalidStatus,
205 ForeignInput,
207 LostAuthorityPending,
210}
211
212#[derive(Debug, PartialEq, Eq)]
214pub struct DetachReplayApplied {
215 aggregate: ClientParticipantAggregate,
216}
217
218impl DetachReplayApplied {
219 #[must_use]
221 pub fn into_aggregate(self) -> ClientParticipantAggregate {
222 self.aggregate
223 }
224}
225
226#[derive(Debug, PartialEq, Eq)]
228pub struct DetachReplayRefusal<T> {
229 aggregate: ClientParticipantAggregate,
230 input: T,
231 reason: DetachReplayRefusalReason,
232}
233
234impl<T> DetachReplayRefusal<T> {
235 #[must_use]
237 pub const fn reason(&self) -> DetachReplayRefusalReason {
238 self.reason
239 }
240
241 #[must_use]
243 pub fn into_parts(self) -> (ClientParticipantAggregate, T) {
244 (self.aggregate, self.input)
245 }
246}
247
248#[derive(Debug, PartialEq, Eq)]
250pub struct DetachTransportAttempt {
251 request: DetachEnvelope,
252 authorization: u64,
253}
254
255impl DetachTransportAttempt {
256 #[must_use]
258 pub const fn request(&self) -> &DetachEnvelope {
259 &self.request
260 }
261
262 #[must_use]
266 pub const fn into_request(self) -> (DetachEnvelope, ClientResponseCorrelation) {
267 (
268 self.request,
269 ClientResponseCorrelation {
270 authorization: self.authorization,
271 },
272 )
273 }
274}
275
276#[derive(Debug, PartialEq, Eq)]
278pub enum DetachTransportAttemptDecision {
279 Started {
281 aggregate: ClientParticipantAggregate,
283 attempt: DetachTransportAttempt,
285 },
286 Refused(DetachReplayRefusal<()>),
288}
289
290#[must_use]
297pub fn transport_attempt_started(
298 mut aggregate: ClientParticipantAggregate,
299) -> DetachTransportAttemptDecision {
300 let request = match &aggregate.detach_replay.state {
301 DetachReplayState::Recorded {
302 request,
303 status: DetachReplayStatus::Parked,
304 } => request.clone(),
305 DetachReplayState::Empty | DetachReplayState::Recorded { .. } => {
306 return DetachTransportAttemptDecision::Refused(DetachReplayRefusal {
307 aggregate,
308 input: (),
309 reason: DetachReplayRefusalReason::InvalidStatus,
310 });
311 }
312 };
313 let expected_matches = aggregate.expected.as_ref().is_some_and(|expected| {
314 expected.authorization != 0
315 && matches!(&expected.request, crate::wire::ClientRequest::Detach(value)
316 if value.conversation_id == request.conversation_id
317 && value.participant_id == request.participant_id
318 && value.capability_generation == request.capability_generation
319 && value.detach_attempt_token == request.detach_attempt_token)
320 });
321 if !expected_matches {
322 return DetachTransportAttemptDecision::Refused(DetachReplayRefusal {
323 aggregate,
324 input: (),
325 reason: DetachReplayRefusalReason::InvalidStatus,
326 });
327 }
328 let authorization = aggregate
329 .expected
330 .as_ref()
331 .map_or(0, |expected| expected.authorization);
332 if let Some(expected) = aggregate.expected.as_mut() {
333 expected.issued = true;
334 }
335 if let DetachReplayState::Recorded { status, .. } = &mut aggregate.detach_replay.state {
336 *status = DetachReplayStatus::InFlight;
337 }
338 DetachTransportAttemptDecision::Started {
339 aggregate,
340 attempt: DetachTransportAttempt {
341 request,
342 authorization,
343 },
344 }
345}
346
347#[derive(Clone, Copy, Debug, PartialEq, Eq)]
349pub enum DetachTransportFate {
350 ResponseUnavailable,
352}
353
354#[derive(Debug, PartialEq, Eq)]
356pub enum DetachTransportFateDecision {
357 Parked(DetachReplayApplied),
359 Refused(DetachReplayRefusal<(ClientResponseCorrelation, DetachTransportFate)>),
361}
362
363#[must_use]
369pub fn transport_fate(
370 mut aggregate: ClientParticipantAggregate,
371 correlation: ClientResponseCorrelation,
372 fate: DetachTransportFate,
373) -> DetachTransportFateDecision {
374 if aggregate.operation_loss_pending() {
375 return DetachTransportFateDecision::Refused(DetachReplayRefusal {
376 aggregate,
377 input: (correlation, fate),
378 reason: DetachReplayRefusalReason::LostAuthorityPending,
379 });
380 }
381 if detach_authority_matches(&aggregate, &correlation) {
382 if let DetachReplayState::Recorded { status, .. } = &mut aggregate.detach_replay.state {
383 *status = DetachReplayStatus::Parked;
384 }
385 if let Some(expected) = aggregate.expected.as_mut() {
386 expected.issued = false;
387 }
388 return DetachTransportFateDecision::Parked(DetachReplayApplied { aggregate });
389 }
390 DetachTransportFateDecision::Refused(DetachReplayRefusal {
391 aggregate,
392 input: (correlation, fate),
393 reason: DetachReplayRefusalReason::InvalidStatus,
394 })
395}
396
397#[derive(Debug, PartialEq, Eq)]
399pub enum ApplyAttachDecision {
400 Superseded(DetachReplayApplied),
402 Refused(DetachReplayRefusal<(AttachBound, ClientResponseCorrelation)>),
404}
405
406#[must_use]
408pub fn apply_attach(
409 mut aggregate: ClientParticipantAggregate,
410 attach: AttachBound,
411 correlation: ClientResponseCorrelation,
412) -> ApplyAttachDecision {
413 if aggregate.operation_loss_pending() {
414 return ApplyAttachDecision::Refused(DetachReplayRefusal {
415 aggregate,
416 input: (attach, correlation),
417 reason: DetachReplayRefusalReason::LostAuthorityPending,
418 });
419 }
420 if detach_authority_matches(&aggregate, &correlation)
421 && aggregate.detach_replay.apply_attach(&attach)
422 {
423 aggregate.expected = None;
424 ApplyAttachDecision::Superseded(DetachReplayApplied { aggregate })
425 } else {
426 ApplyAttachDecision::Refused(DetachReplayRefusal {
427 aggregate,
428 input: (attach, correlation),
429 reason: DetachReplayRefusalReason::ForeignInput,
430 })
431 }
432}
433
434#[derive(Debug, PartialEq, Eq)]
436pub enum ApplyLeaveDecision {
437 Superseded(DetachReplayApplied),
439 Refused(DetachReplayRefusal<(LeaveCommitted, ClientResponseCorrelation)>),
441}
442
443#[must_use]
445pub fn apply_leave_durable(
446 mut aggregate: ClientParticipantAggregate,
447 leave: LeaveCommitted,
448 correlation: ClientResponseCorrelation,
449) -> ApplyLeaveDecision {
450 if aggregate.operation_loss_pending() {
451 return ApplyLeaveDecision::Refused(DetachReplayRefusal {
452 aggregate,
453 input: (leave, correlation),
454 reason: DetachReplayRefusalReason::LostAuthorityPending,
455 });
456 }
457 if detach_authority_matches(&aggregate, &correlation)
458 && aggregate.detach_replay.apply_leave(&leave)
459 {
460 aggregate.expected = None;
461 ApplyLeaveDecision::Superseded(DetachReplayApplied { aggregate })
462 } else {
463 ApplyLeaveDecision::Refused(DetachReplayRefusal {
464 aggregate,
465 input: (leave, correlation),
466 reason: DetachReplayRefusalReason::ForeignInput,
467 })
468 }
469}
470
471#[derive(Debug, PartialEq, Eq)]
473pub enum DetachReplayOutcome {
474 DetachCommitted(DetachCommitted),
476 DetachInProgress(DetachInProgress),
478 TerminalizedDetachCell(TerminalizedDetachCell),
480}
481
482#[derive(Debug, PartialEq, Eq)]
484pub enum ApplyDetachOutcomeDecision {
485 Terminal(DetachReplayApplied),
487 Refused(DetachReplayRefusal<(DetachReplayOutcome, ClientResponseCorrelation)>),
489}
490
491#[must_use]
493pub fn apply_detach_outcome(
494 mut aggregate: ClientParticipantAggregate,
495 outcome: DetachReplayOutcome,
496 correlation: ClientResponseCorrelation,
497) -> ApplyDetachOutcomeDecision {
498 if aggregate.operation_loss_pending() {
499 return ApplyDetachOutcomeDecision::Refused(DetachReplayRefusal {
500 aggregate,
501 input: (outcome, correlation),
502 reason: DetachReplayRefusalReason::LostAuthorityPending,
503 });
504 }
505 if !detach_authority_matches(&aggregate, &correlation) {
506 return ApplyDetachOutcomeDecision::Refused(DetachReplayRefusal {
507 aggregate,
508 input: (outcome, correlation),
509 reason: DetachReplayRefusalReason::InvalidStatus,
510 });
511 }
512 let applied = match &outcome {
513 DetachReplayOutcome::DetachCommitted(value) => {
514 aggregate.detach_replay.apply_detach_committed(value)
515 }
516 DetachReplayOutcome::DetachInProgress(value) => {
517 aggregate.detach_replay.apply_detach_in_progress(value)
518 }
519 DetachReplayOutcome::TerminalizedDetachCell(value) => aggregate
520 .detach_replay
521 .apply_terminalized_detach_cell(value),
522 };
523 if applied {
524 aggregate.expected = None;
525 ApplyDetachOutcomeDecision::Terminal(DetachReplayApplied { aggregate })
526 } else {
527 ApplyDetachOutcomeDecision::Refused(DetachReplayRefusal {
528 aggregate,
529 input: (outcome, correlation),
530 reason: DetachReplayRefusalReason::ForeignInput,
531 })
532 }
533}
534
535fn detach_authority_matches(
536 aggregate: &ClientParticipantAggregate,
537 correlation: &ClientResponseCorrelation,
538) -> bool {
539 let Some(expected) = aggregate.expected.as_ref() else {
540 return false;
541 };
542 if !expected.issued || expected.authorization != correlation.authorization {
543 return false;
544 }
545 let DetachReplayState::Recorded {
546 request,
547 status: DetachReplayStatus::InFlight,
548 } = &aggregate.detach_replay.state
549 else {
550 return false;
551 };
552 matches!(&expected.request, crate::wire::ClientRequest::Detach(value)
553 if value.conversation_id == request.conversation_id
554 && value.participant_id == request.participant_id
555 && value.capability_generation == request.capability_generation
556 && value.detach_attempt_token == request.detach_attempt_token)
557}
558
559fn detach_committed_matches(request: &DetachEnvelope, value: &DetachCommitted) -> bool {
560 value.conversation_id() == request.conversation_id
561 && value.participant_id() == request.participant_id
562 && value.capability_generation() == request.capability_generation
563 && value.detach_attempt_token() == request.detach_attempt_token
564}
565
566fn detach_in_progress_matches(request: &DetachEnvelope, value: &DetachInProgress) -> bool {
567 let expected_generation = request.capability_generation;
568 let presented_generation = value.presented_generation;
569 let expected_token = request.detach_attempt_token;
570 let presented_token = value.presented_token;
571 value.conversation_id == request.conversation_id
572 && value.participant_id == request.participant_id
573 && presented_generation == expected_generation
574 && presented_token == expected_token
575}
576
577fn terminalized_matches(request: &DetachEnvelope, value: &TerminalizedDetachCell) -> bool {
578 value.conversation_id() == request.conversation_id
579 && value.participant_id() == request.participant_id
580 && value.capability_generation() == request.capability_generation
581 && value.detach_attempt_token() == request.detach_attempt_token
582}