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 self.ensure_runtime_build_available(run_id, &snapshot.spec)?;
38 match self
39 .record_event_at(
40 run_id,
41 snapshot.last_sequence,
42 FlowEvent::HookReceived {
43 hook_id: hook_id.to_string(),
44 payload: payload.clone(),
45 },
46 )
47 .await
48 {
49 Ok(_) => {}
50 Err(error) if is_event_conflict(&error) => continue,
51 Err(error) => return Err(error),
52 }
53 }
54 HookStatus::Received => {
55 if recorded_payload.as_ref() != Some(&payload) {
56 return Err(hook_conflict(
57 run_id,
58 hook_id,
59 "was already resumed with a different payload",
60 ));
61 }
62 if snapshot.status.is_terminal() {
63 return Ok(());
64 }
65 self.ensure_runtime_build_available(run_id, &snapshot.spec)?;
66 }
67 HookStatus::Disposed => {
68 return Err(hook_conflict(run_id, hook_id, "was already disposed"));
69 }
70 HookStatus::Cancelled => {
71 return Err(hook_conflict(run_id, hook_id, "was cancelled"));
72 }
73 }
74
75 match self.drive(run_id).await {
76 Ok(_) => return Ok(()),
77 Err(error) if is_event_conflict(&error) => continue,
78 Err(error) => return Err(error),
79 }
80 }
81
82 Err(FlowError::ReplayLimitExceeded(self.max_replay_iterations))
83 }
84
85 pub async fn dispose_hook(&self, run_id: &str, hook_id: &str) -> Result<()> {
92 for _ in 0..self.max_replay_iterations {
93 let snapshot = self.snapshot(run_id).await?;
94 let Some(hook) = snapshot.hooks.get(hook_id) else {
95 if snapshot.status.is_terminal() {
96 return Err(FlowError::RunTerminal(run_id.to_string()));
97 }
98 return Err(FlowError::InvalidTransition(format!(
99 "hook {hook_id} does not exist for run {run_id}"
100 )));
101 };
102
103 match hook.status {
104 HookStatus::Active => {
105 if snapshot.status.is_terminal() {
106 return Err(FlowError::RunTerminal(run_id.to_string()));
107 }
108 self.ensure_runtime_build_available(run_id, &snapshot.spec)?;
109 match self
110 .record_event_at(
111 run_id,
112 snapshot.last_sequence,
113 FlowEvent::HookDisposed {
114 hook_id: hook_id.to_string(),
115 },
116 )
117 .await
118 {
119 Ok(_) => {}
120 Err(error) if is_event_conflict(&error) => continue,
121 Err(error) => return Err(error),
122 }
123 }
124 HookStatus::Disposed => {
125 if snapshot.status.is_terminal() {
126 return Ok(());
127 }
128 self.ensure_runtime_build_available(run_id, &snapshot.spec)?;
129 }
130 HookStatus::Received => {
131 return Err(hook_conflict(run_id, hook_id, "was already resumed"));
132 }
133 HookStatus::Cancelled => {
134 return Err(hook_conflict(run_id, hook_id, "was cancelled"));
135 }
136 }
137
138 match self.drive(run_id).await {
139 Ok(_) => return Ok(()),
140 Err(error) if is_event_conflict(&error) => continue,
141 Err(error) => return Err(error),
142 }
143 }
144
145 Err(FlowError::ReplayLimitExceeded(self.max_replay_iterations))
146 }
147
148 pub async fn resume_hook_by_token(
154 &self,
155 token: &str,
156 payload: serde_json::Value,
157 ) -> Result<(String, String)> {
158 let mut matches = self
159 .store
160 .find_active_hooks_by_token(token)
161 .await?
162 .into_iter()
163 .map(|active| (active.run_id, active.hook.hook_id))
164 .collect::<Vec<_>>();
165
166 match matches.len() {
167 0 => Err(FlowError::HookTokenNotFound(token.to_string())),
168 1 => {
169 let (run_id, hook_id) = matches.remove(0);
170 self.resume_hook(&run_id, &hook_id, payload).await?;
171 Ok((run_id, hook_id))
172 }
173 _ => Err(FlowError::InvalidTransition(
174 "hook token is active in multiple runs (value redacted)".to_string(),
175 )),
176 }
177 }
178
179 pub async fn dispose_hook_by_token(&self, token: &str) -> Result<(String, String)> {
184 let mut matches = self
185 .store
186 .find_active_hooks_by_token(token)
187 .await?
188 .into_iter()
189 .map(|active| (active.run_id, active.hook.hook_id))
190 .collect::<Vec<_>>();
191
192 match matches.len() {
193 0 => Err(FlowError::HookTokenNotFound(token.to_string())),
194 1 => {
195 let (run_id, hook_id) = matches.remove(0);
196 self.dispose_hook(&run_id, &hook_id).await?;
197 Ok((run_id, hook_id))
198 }
199 _ => Err(FlowError::InvalidTransition(
200 "hook token is active in multiple runs (value redacted)".to_string(),
201 )),
202 }
203 }
204}
205
206fn hook_conflict(run_id: &str, hook_id: &str, reason: &str) -> FlowError {
207 FlowError::HookConflict {
208 run_id: run_id.to_string(),
209 hook_id: hook_id.to_string(),
210 reason: reason.to_string(),
211 }
212}