1use super::{ClientParticipantAggregate, DetachReplayStatus, ExpectedOperationState, replay};
4use crate::wire::ClientRequest;
5
6#[derive(Debug, PartialEq, Eq)]
8pub struct ExpectedParticipantOperation {
9 request: ClientRequest,
10 authorization: u64,
11}
12
13impl ExpectedParticipantOperation {
14 #[must_use]
16 pub const fn request(&self) -> &ClientRequest {
17 &self.request
18 }
19
20 #[must_use]
27 pub fn into_request(self) -> (ClientRequest, ClientResponseCorrelation) {
28 (
29 self.request,
30 ClientResponseCorrelation {
31 authorization: self.authorization,
32 },
33 )
34 }
35}
36
37#[derive(Debug, PartialEq, Eq)]
39pub struct ClientResponseCorrelation {
40 pub(super) authorization: u64,
41}
42
43#[derive(Clone, Copy, Debug, PartialEq, Eq)]
45pub enum ClientOperationRecordRefusalReason {
46 OutstandingOperation,
48 DetachReplayOutstanding,
50 DetachReplayIncompatible,
54 AbandonmentPending,
57 BindingMismatch,
59 AlreadyDead,
61 AmbiguousCorrelation,
63 AuthorizationExhausted,
65}
66
67#[derive(Debug, PartialEq, Eq)]
69pub struct ClientOperationRecordRefusal {
70 aggregate: ClientParticipantAggregate,
71 request: ClientRequest,
72 reason: ClientOperationRecordRefusalReason,
73}
74
75impl ClientOperationRecordRefusal {
76 #[must_use]
78 pub const fn reason(&self) -> ClientOperationRecordRefusalReason {
79 self.reason
80 }
81
82 #[must_use]
84 pub fn into_parts(self) -> (ClientParticipantAggregate, ClientRequest) {
85 (self.aggregate, self.request)
86 }
87}
88
89#[derive(Debug, PartialEq, Eq)]
91pub struct ClientContinuousOperation {
92 aggregate: ClientParticipantAggregate,
93 operation: ExpectedParticipantOperation,
94}
95
96impl ClientContinuousOperation {
97 #[must_use]
99 pub fn into_parts(self) -> (ClientParticipantAggregate, ExpectedParticipantOperation) {
100 (self.aggregate, self.operation)
101 }
102}
103
104#[derive(Debug, PartialEq, Eq)]
117pub struct ClientPendingOperationRecord {
118 pub(super) successor: ClientParticipantAggregate,
119 operation: ExpectedParticipantOperation,
120 prior_replay: Option<replay::DetachReplayState>,
121}
122
123impl ClientPendingOperationRecord {
124 #[must_use]
127 pub fn commit(self) -> ClientOperationCommit {
128 ClientOperationCommit {
129 aggregate: self.successor,
130 operation: self.operation,
131 }
132 }
133
134 #[must_use]
137 pub fn abort(mut self) -> (ClientParticipantAggregate, ClientRequest) {
138 self.successor.expected = None;
139 self.successor.next_operation_authorization =
140 self.operation.authorization.saturating_sub(1);
141 if let Some(prior_replay) = self.prior_replay {
142 self.successor.detach_replay.state = prior_replay;
143 }
144 (self.successor, self.operation.request)
145 }
146}
147
148#[derive(Debug, PartialEq, Eq)]
154pub struct ClientOperationCommit {
155 pub(super) aggregate: ClientParticipantAggregate,
156 operation: ExpectedParticipantOperation,
157}
158
159impl ClientOperationCommit {
160 #[must_use]
162 pub fn into_parts(mut self) -> (ClientParticipantAggregate, ExpectedParticipantOperation) {
163 if let Some(expected) = self.aggregate.expected.as_mut() {
164 expected.issued = true;
165 }
166 if matches!(self.operation.request, ClientRequest::Detach(_)) {
167 self.aggregate.detach_replay.mark_initial_attempt_started();
168 }
169 (self.aggregate, self.operation)
170 }
171}
172
173#[derive(Debug, PartialEq, Eq)]
175pub enum RecoveredExpectedOperationDecision {
176 Recovered {
178 aggregate: ClientParticipantAggregate,
180 operation: ExpectedParticipantOperation,
182 },
183 NotAvailable {
185 aggregate: ClientParticipantAggregate,
187 already_issued: bool,
189 },
190}
191
192#[must_use]
197pub fn recover_expected_operation(
198 mut aggregate: ClientParticipantAggregate,
199) -> RecoveredExpectedOperationDecision {
200 let Some(expected) = aggregate.expected.as_mut() else {
201 return RecoveredExpectedOperationDecision::NotAvailable {
202 aggregate,
203 already_issued: false,
204 };
205 };
206 if expected.issued {
207 return RecoveredExpectedOperationDecision::NotAvailable {
208 aggregate,
209 already_issued: true,
210 };
211 }
212 expected.issued = true;
213 let request = expected.request.clone();
214 let authorization = expected.authorization;
215 if matches!(request, ClientRequest::Detach(_)) {
216 aggregate.detach_replay.mark_initial_attempt_started();
217 }
218 RecoveredExpectedOperationDecision::Recovered {
219 aggregate,
220 operation: ExpectedParticipantOperation {
221 request,
222 authorization,
223 },
224 }
225}
226
227#[derive(Clone, Copy, Debug, PartialEq, Eq)]
229pub enum LostAuthorityResolutionRefusalReason {
230 NoPendingTestimony,
232}
233
234#[derive(Debug, PartialEq, Eq)]
242pub enum LostOperationAuthorityDecision {
243 Recorded {
245 aggregate: ClientParticipantAggregate,
247 request: ClientRequest,
249 testimony: super::LostAuthorityTestimony,
251 },
252 DetachParked {
254 aggregate: ClientParticipantAggregate,
256 request: ClientRequest,
258 testimony: super::LostAuthorityTestimony,
260 },
261 Refused {
263 aggregate: ClientParticipantAggregate,
265 reason: LostAuthorityResolutionRefusalReason,
267 },
268}
269
270#[must_use]
275pub fn resolve_lost_operation_authority(
276 mut aggregate: ClientParticipantAggregate,
277) -> LostOperationAuthorityDecision {
278 let Some(expected) = aggregate.expected.as_mut() else {
279 return LostOperationAuthorityDecision::Refused {
280 aggregate,
281 reason: LostAuthorityResolutionRefusalReason::NoPendingTestimony,
282 };
283 };
284 let Some(testimony) = expected.lost.take() else {
285 return LostOperationAuthorityDecision::Refused {
286 aggregate,
287 reason: LostAuthorityResolutionRefusalReason::NoPendingTestimony,
288 };
289 };
290 if matches!(expected.request, ClientRequest::Detach(_)) {
291 expected.issued = false;
292 let request = expected.request.clone();
293 if let replay::DetachReplayState::Recorded { status, .. } =
294 &mut aggregate.detach_replay.state
295 {
296 *status = DetachReplayStatus::Parked;
297 }
298 return LostOperationAuthorityDecision::DetachParked {
299 aggregate,
300 request,
301 testimony,
302 };
303 }
304 let request = expected.request.clone();
305 aggregate.expected = None;
306 LostOperationAuthorityDecision::Recorded {
307 aggregate,
308 request,
309 testimony,
310 }
311}
312
313#[derive(Clone, Copy, Debug, PartialEq, Eq)]
315pub enum ExpectedOperationTransportFate {
316 ResponseUnavailable,
318}
319
320#[derive(Clone, Copy, Debug, PartialEq, Eq)]
322pub enum ExpectedOperationFateRefusalReason {
323 NoIssuedOperation,
325 StaleCorrelation,
327 DetachUsesReplayFate,
329 LostAuthorityPending,
332}
333
334#[derive(Debug, PartialEq, Eq)]
336pub enum ExpectedOperationFateDecision {
337 Recorded {
339 aggregate: ClientParticipantAggregate,
341 request: ClientRequest,
343 fate: ExpectedOperationTransportFate,
345 },
346 Refused {
348 aggregate: ClientParticipantAggregate,
350 correlation: ClientResponseCorrelation,
352 fate: ExpectedOperationTransportFate,
354 reason: ExpectedOperationFateRefusalReason,
356 },
357}
358
359#[must_use]
365pub fn record_expected_operation_fate(
366 mut aggregate: ClientParticipantAggregate,
367 correlation: ClientResponseCorrelation,
368 fate: ExpectedOperationTransportFate,
369) -> ExpectedOperationFateDecision {
370 let Some(expected) = aggregate.expected.as_ref() else {
371 return ExpectedOperationFateDecision::Refused {
372 aggregate,
373 correlation,
374 fate,
375 reason: ExpectedOperationFateRefusalReason::NoIssuedOperation,
376 };
377 };
378 if !expected.issued {
379 return ExpectedOperationFateDecision::Refused {
380 aggregate,
381 correlation,
382 fate,
383 reason: ExpectedOperationFateRefusalReason::NoIssuedOperation,
384 };
385 }
386 if expected.lost.is_some() {
387 return ExpectedOperationFateDecision::Refused {
388 aggregate,
389 correlation,
390 fate,
391 reason: ExpectedOperationFateRefusalReason::LostAuthorityPending,
392 };
393 }
394 if expected.authorization != correlation.authorization {
395 return ExpectedOperationFateDecision::Refused {
396 aggregate,
397 correlation,
398 fate,
399 reason: ExpectedOperationFateRefusalReason::StaleCorrelation,
400 };
401 }
402 if matches!(expected.request, ClientRequest::Detach(_)) {
403 return ExpectedOperationFateDecision::Refused {
404 aggregate,
405 correlation,
406 fate,
407 reason: ExpectedOperationFateRefusalReason::DetachUsesReplayFate,
408 };
409 }
410 let expected = aggregate.expected.take();
411 match expected {
412 Some(expected) => ExpectedOperationFateDecision::Recorded {
413 aggregate,
414 request: expected.request,
415 fate,
416 },
417 None => ExpectedOperationFateDecision::Refused {
418 aggregate,
419 correlation,
420 fate,
421 reason: ExpectedOperationFateRefusalReason::NoIssuedOperation,
422 },
423 }
424}
425
426#[derive(Debug, PartialEq, Eq)]
428pub enum ClientOperationRecordDecision {
429 Pending(ClientPendingOperationRecord),
431 Continuous(ClientContinuousOperation),
433 Refused(ClientOperationRecordRefusal),
435}
436
437#[must_use]
443pub fn record_operation(
444 mut aggregate: ClientParticipantAggregate,
445 request: ClientRequest,
446) -> ClientOperationRecordDecision {
447 if !aggregate.binding.accepts_request(&request) {
448 let reason = if aggregate.binding.is_left() {
449 ClientOperationRecordRefusalReason::AlreadyDead
450 } else {
451 ClientOperationRecordRefusalReason::BindingMismatch
452 };
453 return ClientOperationRecordDecision::Refused(ClientOperationRecordRefusal {
454 aggregate,
455 request,
456 reason,
457 });
458 }
459 if matches!(request, ClientRequest::ParticipantAck(_)) {
460 return ClientOperationRecordDecision::Continuous(ClientContinuousOperation {
461 aggregate,
462 operation: ExpectedParticipantOperation {
463 request,
464 authorization: 0,
465 },
466 });
467 }
468 if aggregate.expected.is_some() {
469 return ClientOperationRecordDecision::Refused(ClientOperationRecordRefusal {
470 aggregate,
471 request,
472 reason: ClientOperationRecordRefusalReason::OutstandingOperation,
473 });
474 }
475 if aggregate.restored_abandonment.is_some()
476 && matches!(request, ClientRequest::ObserverRecovery(_))
477 {
478 return ClientOperationRecordDecision::Refused(ClientOperationRecordRefusal {
479 aggregate,
480 request,
481 reason: ClientOperationRecordRefusalReason::AbandonmentPending,
482 });
483 }
484 let Some(authorization) = aggregate.next_operation_authorization.checked_add(1) else {
485 return ClientOperationRecordDecision::Refused(ClientOperationRecordRefusal {
486 aggregate,
487 request,
488 reason: ClientOperationRecordRefusalReason::AuthorizationExhausted,
489 });
490 };
491 let prior_replay = match admit_detach_replay(&mut aggregate, &request) {
492 Ok(prior_replay) => prior_replay,
493 Err(reason) => {
494 return ClientOperationRecordDecision::Refused(ClientOperationRecordRefusal {
495 aggregate,
496 request,
497 reason,
498 });
499 }
500 };
501 aggregate.next_operation_authorization = authorization;
502 aggregate.expected = Some(ExpectedOperationState {
503 request: request.clone(),
504 issued: false,
505 authorization,
506 lost: None,
507 });
508 ClientOperationRecordDecision::Pending(ClientPendingOperationRecord {
509 successor: aggregate,
510 operation: ExpectedParticipantOperation {
511 request,
512 authorization,
513 },
514 prior_replay,
515 })
516}
517
518fn admit_detach_replay(
526 aggregate: &mut ClientParticipantAggregate,
527 request: &ClientRequest,
528) -> Result<Option<replay::DetachReplayState>, ClientOperationRecordRefusalReason> {
529 let ClientRequest::Detach(value) = request else {
530 return Ok(None);
531 };
532 let envelope = crate::wire::DetachEnvelope {
533 conversation_id: value.conversation_id,
534 participant_id: value.participant_id,
535 capability_generation: value.capability_generation,
536 detach_attempt_token: value.detach_attempt_token,
537 };
538 match &aggregate.detach_replay.state {
539 replay::DetachReplayState::Empty => {
540 let prior = aggregate.detach_replay.state.clone();
541 aggregate.detach_replay.state = replay::DetachReplayState::Recorded {
542 request: envelope,
543 status: DetachReplayStatus::Parked,
544 };
545 Ok(Some(prior))
546 }
547 replay::DetachReplayState::Recorded {
548 request: retained,
549 status,
550 } if retained == &envelope => {
551 if matches!(status, DetachReplayStatus::Parked) {
552 Ok(Some(aggregate.detach_replay.state.clone()))
553 } else {
554 Err(ClientOperationRecordRefusalReason::DetachReplayIncompatible)
555 }
556 }
557 replay::DetachReplayState::Recorded { .. }
558 if aggregate.detach_replay.can_replace_with(&envelope) =>
559 {
560 let prior = aggregate.detach_replay.state.clone();
561 aggregate.detach_replay.state = replay::DetachReplayState::Recorded {
562 request: envelope,
563 status: DetachReplayStatus::Parked,
564 };
565 Ok(Some(prior))
566 }
567 replay::DetachReplayState::Recorded { .. } => {
568 Err(ClientOperationRecordRefusalReason::DetachReplayOutstanding)
569 }
570 }
571}