1use crate::error::{FlowError, Result};
2use crate::model::{FlowEvent, HookStatus};
3
4use super::{validation::is_event_conflict, FlowEngine};
5
6impl FlowEngine {
7 pub async fn resume_hook(
14 &self,
15 run_id: &str,
16 hook_id: &str,
17 payload: serde_json::Value,
18 ) -> Result<()> {
19 for _ in 0..self.max_replay_iterations {
20 let snapshot = self.snapshot(run_id).await?;
21 let Some(hook) = snapshot.hooks.get(hook_id) else {
22 if snapshot.status.is_terminal() {
23 return Err(FlowError::RunTerminal(run_id.to_string()));
24 }
25 return Err(FlowError::InvalidTransition(format!(
26 "hook {hook_id} does not exist for run {run_id}"
27 )));
28 };
29 let hook_status = hook.status;
30 let recorded_payload = hook.payload.clone();
31
32 match hook_status {
33 HookStatus::Active => {
34 if snapshot.status.is_terminal() {
35 return Err(FlowError::RunTerminal(run_id.to_string()));
36 }
37 match self
38 .record_event_at(
39 run_id,
40 snapshot.last_sequence,
41 FlowEvent::HookReceived {
42 hook_id: hook_id.to_string(),
43 payload: payload.clone(),
44 },
45 )
46 .await
47 {
48 Ok(_) => {}
49 Err(error) if is_event_conflict(&error) => continue,
50 Err(error) => return Err(error),
51 }
52 }
53 HookStatus::Received => {
54 if recorded_payload.as_ref() != Some(&payload) {
55 return Err(hook_conflict(
56 run_id,
57 hook_id,
58 "was already resumed with a different payload",
59 ));
60 }
61 if snapshot.status.is_terminal() {
62 return Ok(());
63 }
64 }
65 HookStatus::Disposed => {
66 return Err(hook_conflict(run_id, hook_id, "was already disposed"));
67 }
68 HookStatus::Cancelled => {
69 return Err(hook_conflict(run_id, hook_id, "was cancelled"));
70 }
71 }
72
73 match self.drive(run_id).await {
74 Ok(_) => return Ok(()),
75 Err(error) if is_event_conflict(&error) => continue,
76 Err(error) => return Err(error),
77 }
78 }
79
80 Err(FlowError::ReplayLimitExceeded(self.max_replay_iterations))
81 }
82
83 pub async fn dispose_hook(&self, run_id: &str, hook_id: &str) -> Result<()> {
90 for _ in 0..self.max_replay_iterations {
91 let snapshot = self.snapshot(run_id).await?;
92 let Some(hook) = snapshot.hooks.get(hook_id) else {
93 if snapshot.status.is_terminal() {
94 return Err(FlowError::RunTerminal(run_id.to_string()));
95 }
96 return Err(FlowError::InvalidTransition(format!(
97 "hook {hook_id} does not exist for run {run_id}"
98 )));
99 };
100
101 match hook.status {
102 HookStatus::Active => {
103 if snapshot.status.is_terminal() {
104 return Err(FlowError::RunTerminal(run_id.to_string()));
105 }
106 match self
107 .record_event_at(
108 run_id,
109 snapshot.last_sequence,
110 FlowEvent::HookDisposed {
111 hook_id: hook_id.to_string(),
112 },
113 )
114 .await
115 {
116 Ok(_) => {}
117 Err(error) if is_event_conflict(&error) => continue,
118 Err(error) => return Err(error),
119 }
120 }
121 HookStatus::Disposed => {
122 if snapshot.status.is_terminal() {
123 return Ok(());
124 }
125 }
126 HookStatus::Received => {
127 return Err(hook_conflict(run_id, hook_id, "was already resumed"));
128 }
129 HookStatus::Cancelled => {
130 return Err(hook_conflict(run_id, hook_id, "was cancelled"));
131 }
132 }
133
134 match self.drive(run_id).await {
135 Ok(_) => return Ok(()),
136 Err(error) if is_event_conflict(&error) => continue,
137 Err(error) => return Err(error),
138 }
139 }
140
141 Err(FlowError::ReplayLimitExceeded(self.max_replay_iterations))
142 }
143
144 pub async fn resume_hook_by_token(
150 &self,
151 token: &str,
152 payload: serde_json::Value,
153 ) -> Result<(String, String)> {
154 let mut matches = self
155 .store
156 .find_active_hooks_by_token(token)
157 .await?
158 .into_iter()
159 .map(|active| (active.run_id, active.hook.hook_id))
160 .collect::<Vec<_>>();
161
162 match matches.len() {
163 0 => Err(FlowError::HookTokenNotFound(token.to_string())),
164 1 => {
165 let (run_id, hook_id) = matches.remove(0);
166 self.resume_hook(&run_id, &hook_id, payload).await?;
167 Ok((run_id, hook_id))
168 }
169 _ => Err(FlowError::InvalidTransition(
170 "hook token is active in multiple runs (value redacted)".to_string(),
171 )),
172 }
173 }
174
175 pub async fn dispose_hook_by_token(&self, token: &str) -> Result<(String, String)> {
180 let mut matches = self
181 .store
182 .find_active_hooks_by_token(token)
183 .await?
184 .into_iter()
185 .map(|active| (active.run_id, active.hook.hook_id))
186 .collect::<Vec<_>>();
187
188 match matches.len() {
189 0 => Err(FlowError::HookTokenNotFound(token.to_string())),
190 1 => {
191 let (run_id, hook_id) = matches.remove(0);
192 self.dispose_hook(&run_id, &hook_id).await?;
193 Ok((run_id, hook_id))
194 }
195 _ => Err(FlowError::InvalidTransition(
196 "hook token is active in multiple runs (value redacted)".to_string(),
197 )),
198 }
199 }
200}
201
202fn hook_conflict(run_id: &str, hook_id: &str, reason: &str) -> FlowError {
203 FlowError::HookConflict {
204 run_id: run_id.to_string(),
205 hook_id: hook_id.to_string(),
206 reason: reason.to_string(),
207 }
208}