lodestone_client/
operation_waiter.rs1use lodestone_wire::{
2 LODESTONE_API_MAX_OPERATION_WAIT_ATTEMPTS, LODESTONE_API_MAX_OPERATION_WAIT_INTERVAL_MILLIS, Locator, Operation,
3 OperationState, U64, WaitOperationRequest,
4};
5
6use crate::{CallOptions, Client, ClientError};
7
8#[derive(Clone, Copy, Debug, Eq, PartialEq)]
9pub struct OperationWaitOptions {
10 pub maximum_attempts: u32,
11 pub interval_millis: u64,
12}
13
14impl Default for OperationWaitOptions {
15 fn default() -> Self {
16 Self {
17 maximum_attempts: 20,
18 interval_millis: 100,
19 }
20 }
21}
22
23#[derive(Clone, Debug, PartialEq)]
24pub struct OperationWaitOutcome {
25 pub operation: Operation,
26 pub attempts: u32,
27 pub terminal: bool,
28}
29
30#[derive(Debug)]
31pub enum OperationWaitError {
32 InvalidOptions,
33 InvalidAuthorityRevision,
34 Client(ClientError),
35}
36
37impl Client {
38 pub async fn wait_for_operation(
41 &self,
42 locator: Locator,
43 after_authority_revision: U64,
44 wait: OperationWaitOptions,
45 options: CallOptions,
46 ) -> Result<OperationWaitOutcome, OperationWaitError> {
47 validate_wait_options(wait)?;
48 if after_authority_revision.validate().is_err() {
49 return Err(OperationWaitError::InvalidAuthorityRevision);
50 }
51 let result = self
52 .wait_operation(
53 WaitOperationRequest {
54 locator,
55 after_authority_revision,
56 maximum_attempts: wait.maximum_attempts,
57 interval_millis: U64 {
58 value: wait.interval_millis.to_string(),
59 },
60 },
61 options,
62 )
63 .await
64 .map_err(OperationWaitError::Client)?;
65 let terminal = operation_is_terminal(result.operation.current_state);
66 Ok(OperationWaitOutcome {
67 operation: result.operation,
68 attempts: result.attempts,
69 terminal,
70 })
71 }
72}
73
74fn validate_wait_options(options: OperationWaitOptions) -> Result<(), OperationWaitError> {
75 if options.maximum_attempts == 0
76 || options.maximum_attempts > LODESTONE_API_MAX_OPERATION_WAIT_ATTEMPTS
77 || options.interval_millis == 0
78 || options.interval_millis > LODESTONE_API_MAX_OPERATION_WAIT_INTERVAL_MILLIS
79 {
80 Err(OperationWaitError::InvalidOptions)
81 } else {
82 Ok(())
83 }
84}
85
86const fn operation_is_terminal(state: OperationState) -> bool {
87 !matches!(state, OperationState::Pending)
88}
89
90impl std::fmt::Display for OperationWaitError {
91 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92 formatter.write_str(match self {
93 Self::InvalidOptions => "operation wait options exceed the native bounded contract",
94 Self::InvalidAuthorityRevision => "operation wait authority revision must be a canonical u64",
95 Self::Client(_) => "operation wait request failed",
96 })
97 }
98}
99
100impl std::error::Error for OperationWaitError {}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105
106 #[test]
107 fn operation_wait_options_are_bounded_by_native_limits() {
108 assert!(validate_wait_options(OperationWaitOptions::default()).is_ok());
109 assert!(matches!(
110 validate_wait_options(OperationWaitOptions {
111 maximum_attempts: LODESTONE_API_MAX_OPERATION_WAIT_ATTEMPTS.saturating_add(1),
112 interval_millis: 1,
113 }),
114 Err(OperationWaitError::InvalidOptions)
115 ));
116 assert!(matches!(
117 validate_wait_options(OperationWaitOptions {
118 maximum_attempts: 1,
119 interval_millis: 0,
120 }),
121 Err(OperationWaitError::InvalidOptions)
122 ));
123 }
124
125 #[test]
126 fn operation_wait_accepts_the_wire_zero_revision_and_classifies_terminal_state() {
127 assert!(U64 { value: "0".to_owned() }.validate().is_ok());
128 assert!(!operation_is_terminal(OperationState::Pending));
129 assert!(operation_is_terminal(OperationState::Succeeded));
130 }
131}