Skip to main content

a3s_flow/
context.rs

1use chrono::{DateTime, Utc};
2use serde::de::DeserializeOwned;
3
4use crate::error::{FlowError, Result};
5use crate::model::{
6    CancellationRequest, ChildOperationReference, ChildWorkflowCancellationPolicy,
7    ChildWorkflowCommand, FlowEvent, FlowEventEnvelope, HookMetadata, JsonValue, RetryPolicy,
8    RuntimeCommand, StepCommand, WorkflowProgress, WorkflowSignal, WorkflowSpec,
9    WorkflowTerminalOutcome,
10};
11use crate::runtime::WorkflowInvocation;
12
13/// Replay helper for Rust workflow runtimes.
14///
15/// `WorkflowContext` is a read-only view over a workflow invocation. It provides
16/// deterministic helpers for inspecting persisted history and returning the
17/// next command to the engine.
18pub struct WorkflowContext<'a> {
19    invocation: &'a WorkflowInvocation,
20}
21
22impl<'a> WorkflowContext<'a> {
23    /// Creates a replay context over one immutable runtime invocation.
24    pub fn new(invocation: &'a WorkflowInvocation) -> Self {
25        Self { invocation }
26    }
27
28    /// Returns the stable run identifier.
29    pub fn run_id(&self) -> &str {
30        &self.invocation.run_id
31    }
32
33    /// Returns the workflow's initial JSON input.
34    pub fn input(&self) -> &JsonValue {
35        &self.invocation.input
36    }
37
38    /// Return the immutable workflow definition pinned by `run_created`.
39    pub fn spec(&self) -> &WorkflowSpec {
40        &self.invocation.spec
41    }
42
43    /// Return whether this run was created with a replay-safe patch marker.
44    ///
45    /// Marker presence never changes for an existing run. A compatible runtime
46    /// can therefore keep both code paths and deterministically replay old
47    /// unmarked histories alongside new marked histories.
48    pub fn has_patch_marker(&self, patch_id: &str) -> bool {
49        self.spec().has_patch_marker(patch_id)
50    }
51
52    /// Decode the workflow input into a host-defined serde type.
53    pub fn input_as<T>(&self) -> Result<T>
54    where
55        T: DeserializeOwned,
56    {
57        self.invocation.input_as()
58    }
59
60    /// Returns committed history in ascending event-sequence order.
61    pub fn history(&self) -> &[FlowEventEnvelope] {
62        &self.invocation.history
63    }
64
65    /// Return the durable cleanup-aware cancellation request, when present.
66    pub fn cancellation_request(&self) -> Option<&CancellationRequest> {
67        self.history()
68            .iter()
69            .find_map(|envelope| match &envelope.event {
70                FlowEvent::RunCancellationRequested { request } => Some(request),
71                _ => None,
72            })
73    }
74
75    /// Return a durable progress update by its idempotency identity.
76    pub fn progress(&self, progress_id: &str) -> Option<&WorkflowProgress> {
77        self.history()
78            .iter()
79            .find_map(|envelope| match &envelope.event {
80                FlowEvent::RunProgressRecorded { progress }
81                    if progress.progress_id == progress_id =>
82                {
83                    Some(progress)
84                }
85                _ => None,
86            })
87    }
88
89    /// Return a durable child-operation reference by its parent-local id.
90    pub fn child_operation(&self, reference_id: &str) -> Option<&ChildOperationReference> {
91        self.history()
92            .iter()
93            .find_map(|envelope| match &envelope.event {
94                FlowEvent::ChildOperationLinked { child } if child.reference_id == reference_id => {
95                    Some(child)
96                }
97                _ => None,
98            })
99    }
100
101    /// Return the engine-generated root run ID for a durable child request.
102    pub fn child_workflow_run_id(&self, child_id: &str) -> Option<&str> {
103        self.history()
104            .iter()
105            .find_map(|envelope| match &envelope.event {
106                FlowEvent::ChildWorkflowRequested {
107                    child_id: id,
108                    child_run_id,
109                    ..
110                } if id == child_id => Some(child_run_id.as_str()),
111                _ => None,
112            })
113    }
114
115    /// Return the terminal outcome durably observed for a child workflow.
116    pub fn child_workflow_outcome(&self, child_id: &str) -> Option<&WorkflowTerminalOutcome> {
117        self.history()
118            .iter()
119            .rev()
120            .find_map(|envelope| match &envelope.event {
121                FlowEvent::ChildWorkflowResolved {
122                    child_id: id,
123                    outcome,
124                } if id == child_id => Some(outcome),
125                _ => None,
126            })
127    }
128
129    /// Return a received signal by its caller-owned delivery identity.
130    pub fn signal(&self, signal_id: &str) -> Option<&WorkflowSignal> {
131        self.history()
132            .iter()
133            .find_map(|envelope| match &envelope.event {
134                FlowEvent::SignalReceived { signal } if signal.signal_id == signal_id => {
135                    Some(signal)
136                }
137                _ => None,
138            })
139    }
140
141    /// Return the payload paired with a completed deterministic signal wait.
142    pub fn signal_payload(&self, wait_id: &str) -> Option<&JsonValue> {
143        let signal_id = self
144            .history()
145            .iter()
146            .find_map(|envelope| match &envelope.event {
147                FlowEvent::SignalWaitCompleted {
148                    wait_id: completed_wait_id,
149                    signal_id,
150                } if completed_wait_id == wait_id => Some(signal_id.as_str()),
151                _ => None,
152            })?;
153        self.signal(signal_id).map(|signal| &signal.payload)
154    }
155
156    /// Decode the payload paired with a completed deterministic signal wait.
157    pub fn signal_payload_as<T>(&self, wait_id: &str) -> Result<Option<T>>
158    where
159        T: DeserializeOwned,
160    {
161        self.signal_payload(wait_id)
162            .cloned()
163            .map(serde_json::from_value)
164            .transpose()
165            .map_err(FlowError::from)
166    }
167
168    /// Returns the durable JSON output of a completed step.
169    pub fn step_output(&self, step_id: &str) -> Option<&JsonValue> {
170        self.history()
171            .iter()
172            .find_map(|envelope| match &envelope.event {
173                FlowEvent::StepCompleted {
174                    step_id: id,
175                    output,
176                } if id == step_id => Some(output),
177                _ => None,
178            })
179    }
180
181    /// Decodes a completed step output into a host-defined serde type.
182    pub fn step_output_as<T>(&self, step_id: &str) -> Result<Option<T>>
183    where
184        T: DeserializeOwned,
185    {
186        self.step_output(step_id)
187            .cloned()
188            .map(serde_json::from_value)
189            .transpose()
190            .map_err(FlowError::from)
191    }
192
193    /// Returns whether the step has a durable successful output.
194    pub fn step_completed(&self, step_id: &str) -> bool {
195        self.step_output(step_id).is_some()
196    }
197
198    /// Returns the terminal error of a step that exhausted its retries.
199    pub fn step_failed(&self, step_id: &str) -> Option<&str> {
200        self.history()
201            .iter()
202            .rev()
203            .find_map(|envelope| match &envelope.event {
204                FlowEvent::StepFailed {
205                    step_id: id, error, ..
206                } if id == step_id => Some(error.as_str()),
207                _ => None,
208            })
209    }
210
211    /// Returns whether a durable timer wait has completed.
212    pub fn wait_completed(&self, wait_id: &str) -> bool {
213        self.history().iter().any(|envelope| {
214            matches!(
215                &envelope.event,
216                FlowEvent::WaitCompleted { wait_id: id } if id == wait_id
217            )
218        })
219    }
220
221    /// Returns the durable JSON payload received by a hook.
222    pub fn hook_payload(&self, hook_id: &str) -> Option<&JsonValue> {
223        self.history()
224            .iter()
225            .find_map(|envelope| match &envelope.event {
226                FlowEvent::HookReceived {
227                    hook_id: id,
228                    payload,
229                } if id == hook_id => Some(payload),
230                _ => None,
231            })
232    }
233
234    /// Decodes a received hook payload into a host-defined serde type.
235    pub fn hook_payload_as<T>(&self, hook_id: &str) -> Result<Option<T>>
236    where
237        T: DeserializeOwned,
238    {
239        self.hook_payload(hook_id)
240            .cloned()
241            .map(serde_json::from_value)
242            .transpose()
243            .map_err(FlowError::from)
244    }
245
246    /// Returns whether a hook was explicitly closed without a payload.
247    pub fn hook_disposed(&self, hook_id: &str) -> bool {
248        self.history().iter().any(|envelope| {
249            matches!(
250                &envelope.event,
251                FlowEvent::HookDisposed { hook_id: id } if id == hook_id
252            )
253        })
254    }
255
256    /// Returns a command that completes the workflow successfully.
257    pub fn complete(&self, output: JsonValue) -> RuntimeCommand {
258        RuntimeCommand::Complete { output }
259    }
260
261    /// Returns a command that fails the workflow.
262    pub fn fail(&self, error: impl Into<String>) -> RuntimeCommand {
263        RuntimeCommand::Fail {
264            error: error.into(),
265        }
266    }
267
268    /// Finish a previously requested cancellation after cleanup is durable.
269    pub fn cancel(&self) -> RuntimeCommand {
270        RuntimeCommand::Cancel
271    }
272
273    /// Finish a run with a typed timeout outcome.
274    pub fn timeout(&self, deadline: DateTime<Utc>, reason: Option<String>) -> RuntimeCommand {
275        RuntimeCommand::Timeout { deadline, reason }
276    }
277
278    /// Close this history segment and continue with fresh history and `input`.
279    ///
280    /// The engine persists the successor identity before creating it and
281    /// carries the exact current [`WorkflowSpec`] into the new run.
282    pub fn continue_as_new(&self, input: JsonValue) -> RuntimeCommand {
283        RuntimeCommand::ContinueAsNew { input }
284    }
285
286    /// Persist an idempotently identified progress update and replay.
287    pub fn record_progress(&self, progress: WorkflowProgress) -> RuntimeCommand {
288        RuntimeCommand::RecordProgress { progress }
289    }
290
291    /// Persist a child-operation reference and replay.
292    pub fn link_child_operation(&self, child: ChildOperationReference) -> RuntimeCommand {
293        RuntimeCommand::LinkChildOperation { child }
294    }
295
296    /// Start or await a first-class child workflow.
297    ///
298    /// The child ID is stable within this parent history. By default, a parent
299    /// cancellation request is propagated to an open child and the parent
300    /// waits for the child's terminal outcome.
301    pub fn start_child_workflow(
302        &self,
303        child_id: impl Into<String>,
304        spec: WorkflowSpec,
305        input: JsonValue,
306    ) -> RuntimeCommand {
307        self.start_child_workflow_with_policy(
308            child_id,
309            spec,
310            input,
311            ChildWorkflowCancellationPolicy::default(),
312        )
313    }
314
315    /// Start or await a child with an explicit cancellation policy.
316    pub fn start_child_workflow_with_policy(
317        &self,
318        child_id: impl Into<String>,
319        spec: WorkflowSpec,
320        input: JsonValue,
321        cancellation_policy: ChildWorkflowCancellationPolicy,
322    ) -> RuntimeCommand {
323        RuntimeCommand::StartChildWorkflow {
324            child_id: child_id.into(),
325            spec,
326            input,
327            cancellation_policy,
328        }
329    }
330
331    /// Create a child definition for a bounded durable batch.
332    pub fn child_workflow(
333        &self,
334        child_id: impl Into<String>,
335        spec: WorkflowSpec,
336        input: JsonValue,
337    ) -> ChildWorkflowCommand {
338        ChildWorkflowCommand::new(child_id, spec, input)
339    }
340
341    /// Create a batch child definition with an explicit cancellation policy.
342    pub fn child_workflow_with_policy(
343        &self,
344        child_id: impl Into<String>,
345        spec: WorkflowSpec,
346        input: JsonValue,
347        cancellation_policy: ChildWorkflowCancellationPolicy,
348    ) -> ChildWorkflowCommand {
349        self.child_workflow(child_id, spec, input)
350            .with_cancellation_policy(cancellation_policy)
351    }
352
353    /// Durably request a deterministic batch before any child starts.
354    pub fn start_child_workflows(&self, children: Vec<ChildWorkflowCommand>) -> RuntimeCommand {
355        RuntimeCommand::start_child_workflows(children)
356    }
357
358    /// Schedules one durable step with the default retry policy.
359    pub fn schedule_step(
360        &self,
361        step_id: impl Into<String>,
362        step_name: impl Into<String>,
363        input: JsonValue,
364    ) -> RuntimeCommand {
365        RuntimeCommand::schedule_step(step_id, step_name, input)
366    }
367
368    /// Schedules one durable step with an explicit retry policy.
369    pub fn schedule_step_with_retry(
370        &self,
371        step_id: impl Into<String>,
372        step_name: impl Into<String>,
373        input: JsonValue,
374        retry: RetryPolicy,
375    ) -> RuntimeCommand {
376        RuntimeCommand::ScheduleStep {
377            step_id: step_id.into(),
378            step_name: step_name.into(),
379            input,
380            retry,
381        }
382    }
383
384    /// Creates a step definition with the default retry policy.
385    pub fn step(
386        &self,
387        step_id: impl Into<String>,
388        step_name: impl Into<String>,
389        input: JsonValue,
390    ) -> StepCommand {
391        StepCommand::new(step_id, step_name, input)
392    }
393
394    /// Creates a step definition with an explicit retry policy.
395    pub fn step_with_retry(
396        &self,
397        step_id: impl Into<String>,
398        step_name: impl Into<String>,
399        input: JsonValue,
400        retry: RetryPolicy,
401    ) -> StepCommand {
402        StepCommand::new(step_id, step_name, input).with_retry(retry)
403    }
404
405    /// Atomically schedules a deterministic batch of durable steps.
406    pub fn schedule_steps(&self, steps: Vec<StepCommand>) -> RuntimeCommand {
407        RuntimeCommand::schedule_steps(steps)
408    }
409
410    /// Suspends replay until the given UTC deadline becomes ready.
411    pub fn wait_until(
412        &self,
413        wait_id: impl Into<String>,
414        resume_at: DateTime<Utc>,
415    ) -> RuntimeCommand {
416        RuntimeCommand::WaitUntil {
417            wait_id: wait_id.into(),
418            resume_at,
419        }
420    }
421
422    /// Creates an externally completable hook with JSON metadata.
423    pub fn create_hook(
424        &self,
425        hook_id: impl Into<String>,
426        token: impl Into<String>,
427        metadata: JsonValue,
428    ) -> RuntimeCommand {
429        RuntimeCommand::CreateHook {
430            hook_id: hook_id.into(),
431            token: token.into(),
432            metadata,
433        }
434    }
435
436    /// Creates an externally completable hook with typed metadata.
437    pub fn create_hook_with_metadata(
438        &self,
439        hook_id: impl Into<String>,
440        token: impl Into<String>,
441        metadata: HookMetadata,
442    ) -> Result<RuntimeCommand> {
443        Ok(self.create_hook(hook_id, token, metadata.into_json()?))
444    }
445
446    /// Suspend until the next queued signal with `signal_name` is paired with
447    /// the stable `wait_id`.
448    pub fn wait_for_signal(
449        &self,
450        wait_id: impl Into<String>,
451        signal_name: impl Into<String>,
452    ) -> RuntimeCommand {
453        RuntimeCommand::WaitForSignal {
454            wait_id: wait_id.into(),
455            signal_name: signal_name.into(),
456        }
457    }
458}