Skip to main content

temporalio_client/
workflow_handle.rs

1use crate::{
2    CancelWorkflowInput, DescribeWorkflowInput, DescribeWorkflowOutput,
3    FetchWorkflowHistoryPageInput, FetchWorkflowHistoryPageOutput, NamespacedClient, Next,
4    PollWorkflowUpdateInput, PollWorkflowUpdateOutput, QueryWorkflowInput, QueryWorkflowOutput,
5    RpcOptions, SignalWorkflowInput, StartWorkflowUpdateInput, StartWorkflowUpdateOutput,
6    TerminateWorkflowInput, WorkflowCancelOptions, WorkflowDescribeOptions,
7    WorkflowExecuteUpdateOptions, WorkflowExecutionStatus, WorkflowFetchHistoryOptions,
8    WorkflowGetResultOptions, WorkflowQueryOptions, WorkflowSignalOptions,
9    WorkflowStartUpdateOptions, WorkflowTerminateOptions,
10    errors::{
11        WorkflowGetResultError, WorkflowInteractionError, WorkflowQueryError, WorkflowUpdateError,
12    },
13    grpc::WorkflowService,
14    interceptors,
15};
16use futures_util::{TryStreamExt, future::BoxFuture, stream, stream::Stream};
17use std::{
18    collections::VecDeque,
19    fmt::Debug,
20    marker::PhantomData,
21    pin::Pin,
22    task::{Context, Poll},
23};
24pub use temporalio_common::UntypedWorkflow;
25use temporalio_common::{
26    HasWorkflowDefinition, QueryDefinition, SignalDefinition, UpdateDefinition, WorkflowDefinition,
27    data_converters::{
28        DataConverter, DecodablePayloads, GenericPayloadConverter, PayloadConversionError,
29        PayloadConverter, RawValue, SerializationContext, SerializationContextData,
30        WorkflowSerializationContext,
31    },
32    error::IncomingError,
33    payload_visitor::decode_payloads,
34    protos::{
35        coresdk::FromPayloadsExt,
36        proto_ts_to_system_time,
37        temporal::api::{
38            common::v1::{Header, Payload, Payloads, WorkflowExecution as ProtoWorkflowExecution},
39            enums::v1::{HistoryEventFilterType, UpdateWorkflowExecutionLifecycleStage},
40            history::{
41                self,
42                v1::{History, HistoryEvent, history_event::Attributes},
43            },
44            query::v1::WorkflowQuery,
45            sdk::v1::UserMetadata,
46            update::{self, v1::WaitPolicy},
47            workflow::v1 as workflow,
48            workflowservice::v1::{
49                DescribeWorkflowExecutionRequest, DescribeWorkflowExecutionResponse,
50                GetWorkflowExecutionHistoryRequest, PollWorkflowExecutionUpdateRequest,
51                QueryWorkflowRequest, RequestCancelWorkflowExecutionRequest,
52                SignalWorkflowExecutionRequest, TerminateWorkflowExecutionRequest,
53                UpdateWorkflowExecutionRequest,
54            },
55        },
56    },
57    search_attributes::SearchAttributes,
58};
59use tonic::IntoRequest;
60use uuid::Uuid;
61
62#[derive(Debug, Clone, Default, PartialEq, Eq)]
63struct DecodedUserMetadata {
64    summary: Option<String>,
65    details: Option<String>,
66}
67
68fn decode_user_metadata(
69    context: &SerializationContextData,
70    user_metadata: Option<UserMetadata>,
71) -> Result<DecodedUserMetadata, PayloadConversionError> {
72    let payload_converter = PayloadConverter::default();
73    let context = SerializationContext::new(context, &payload_converter);
74    let (summary, details) = user_metadata
75        .map(|metadata| (metadata.summary, metadata.details))
76        .unwrap_or_default();
77    Ok(DecodedUserMetadata {
78        summary: match summary {
79            Some(payload) => Some(payload_converter.from_payload(&context, payload)?),
80            None => None,
81        },
82        details: match details {
83            Some(payload) => Some(payload_converter.from_payload(&context, payload)?),
84            None => None,
85        },
86    })
87}
88
89/// Details attached to a cancelled or terminated workflow result.
90#[derive(Clone, Debug)]
91#[non_exhaustive]
92pub struct WorkflowResultDetails {
93    payloads: DecodablePayloads,
94}
95
96impl WorkflowResultDetails {
97    async fn new(
98        payloads: Vec<Payload>,
99        data_converter: &DataConverter,
100    ) -> Result<Self, PayloadConversionError> {
101        let payloads = data_converter
102            .codec()
103            .decode(
104                &SerializationContextData::Workflow(WorkflowSerializationContext::new()),
105                payloads,
106            )
107            .await?;
108        Ok(Self {
109            payloads: DecodablePayloads::new(
110                payloads,
111                data_converter.payload_converter().clone(),
112                SerializationContextData::Workflow(WorkflowSerializationContext::new()),
113            ),
114        })
115    }
116
117    /// Deserialize the details into a typed value using the client's payload converter.
118    pub fn deserialize<T: temporalio_common::data_converters::TemporalDeserializable + 'static>(
119        &self,
120    ) -> Result<T, PayloadConversionError> {
121        self.payloads.deserialize()
122    }
123
124    /// Returns the codec-decoded payloads.
125    pub fn raw(&self) -> &[Payload] {
126        self.payloads.raw()
127    }
128
129    /// Consume these details and return their codec-decoded payloads.
130    pub fn into_raw(self) -> RawValue {
131        self.payloads.into_raw()
132    }
133}
134
135/// Enumerates terminal states for a particular workflow execution
136#[derive(Debug)]
137#[allow(clippy::large_enum_variant)]
138pub enum WorkflowExecutionResult<T> {
139    /// The workflow finished successfully
140    Succeeded(T),
141    /// The workflow finished in failure
142    Failed(IncomingError),
143    /// The workflow was cancelled
144    Cancelled {
145        /// Details provided at cancellation time
146        details: WorkflowResultDetails,
147    },
148    /// The workflow was terminated
149    Terminated {
150        /// Details provided at termination time
151        details: WorkflowResultDetails,
152    },
153    /// The workflow timed out
154    TimedOut,
155    /// The workflow continued as new
156    ContinuedAsNew,
157}
158
159/// Description of a workflow execution returned by `WorkflowHandle::describe`.
160///
161/// Access to the underlying Protobuf message is provided by [`raw`](Self::raw).
162#[derive(Debug, Clone)]
163pub struct WorkflowExecutionDescription {
164    /// The raw proto response from the server.
165    pub raw_description: DescribeWorkflowExecutionResponse,
166    history_length: usize,
167    static_summary: Option<String>,
168    static_details: Option<String>,
169    data_converter: DataConverter,
170}
171
172impl WorkflowExecutionDescription {
173    async fn new(
174        mut raw_description: DescribeWorkflowExecutionResponse,
175        data_converter: &DataConverter,
176    ) -> Result<Self, PayloadConversionError> {
177        let raw_user_metadata = raw_description
178            .execution_config
179            .as_ref()
180            .and_then(|cfg| cfg.user_metadata.clone());
181        decode_payloads(
182            &mut raw_description,
183            data_converter.codec(),
184            &SerializationContextData::Workflow(WorkflowSerializationContext::new()),
185        )
186        .await?;
187        let decoded_metadata = decode_user_metadata(
188            &SerializationContextData::Workflow(WorkflowSerializationContext::new()),
189            raw_user_metadata,
190        )?;
191        let history_length_raw = raw_description
192            .workflow_execution_info
193            .as_ref()
194            .map(|info| info.history_length)
195            .unwrap_or(0);
196        let history_length = history_length_raw.try_into().map_err(|_| {
197            PayloadConversionError::EncodingError(
198                format!("workflow history_length must be non-negative, got {history_length_raw}")
199                    .into(),
200            )
201        })?;
202        Ok(Self {
203            raw_description,
204            history_length,
205            static_summary: decoded_metadata.summary,
206            static_details: decoded_metadata.details,
207            data_converter: data_converter.clone(),
208        })
209    }
210
211    /// The workflow ID.
212    pub fn id(&self) -> &str {
213        self.execution().workflow_id.as_str()
214    }
215
216    /// The run ID.
217    pub fn run_id(&self) -> &str {
218        self.execution().run_id.as_str()
219    }
220
221    /// The workflow type name.
222    pub fn workflow_type(&self) -> &str {
223        self.workflow_type_info().name.as_str()
224    }
225
226    /// The current status of the workflow execution.
227    pub fn status(&self) -> WorkflowExecutionStatus {
228        WorkflowExecutionStatus::from_raw(self.workflow_info().status)
229    }
230
231    /// When the workflow was created.
232    pub fn start_time(&self) -> Option<std::time::SystemTime> {
233        self.workflow_info()
234            .start_time
235            .as_ref()
236            .and_then(proto_ts_to_system_time)
237    }
238
239    /// When the workflow run started or should start.
240    pub fn execution_time(&self) -> Option<std::time::SystemTime> {
241        self.workflow_info()
242            .execution_time
243            .as_ref()
244            .and_then(proto_ts_to_system_time)
245    }
246
247    /// When the workflow was closed, if closed.
248    pub fn close_time(&self) -> Option<std::time::SystemTime> {
249        self.workflow_info()
250            .close_time
251            .as_ref()
252            .and_then(proto_ts_to_system_time)
253    }
254
255    /// The task queue the workflow runs on.
256    pub fn task_queue(&self) -> &str {
257        self.workflow_info().task_queue.as_str()
258    }
259
260    /// Number of events in history.
261    pub fn history_length(&self) -> usize {
262        self.history_length
263    }
264
265    /// Workflow memo decoded with the client's payload converter.
266    pub fn memo(&self) -> crate::Memo {
267        crate::Memo::from_raw(
268            self.workflow_info().memo.clone(),
269            self.data_converter.payload_converter().clone(),
270            SerializationContextData::Workflow(WorkflowSerializationContext::new()),
271        )
272    }
273
274    /// Parent workflow ID, if this is a child workflow.
275    pub fn parent_id(&self) -> Option<&str> {
276        self.workflow_info()
277            .parent_execution
278            .as_ref()
279            .map(|e| e.workflow_id.as_str())
280    }
281
282    /// Parent run ID, if this is a child workflow.
283    pub fn parent_run_id(&self) -> Option<&str> {
284        self.workflow_info()
285            .parent_execution
286            .as_ref()
287            .map(|e| e.run_id.as_str())
288    }
289
290    /// Search attributes on the workflow.
291    pub fn search_attributes(&self) -> SearchAttributes {
292        self.workflow_info()
293            .search_attributes
294            .as_ref()
295            .map(SearchAttributes::from_proto)
296            .unwrap_or_default()
297    }
298
299    /// Static summary configured on the workflow, if present.
300    pub fn static_summary(&self) -> Option<&str> {
301        self.static_summary.as_deref()
302    }
303
304    /// Static details configured on the workflow, if present.
305    pub fn static_details(&self) -> Option<&str> {
306        self.static_details.as_deref()
307    }
308
309    /// Access the raw proto for additional fields not exposed via accessors.
310    pub fn raw(&self) -> &DescribeWorkflowExecutionResponse {
311        &self.raw_description
312    }
313
314    /// Consume the wrapper and return the raw proto.
315    pub fn into_raw(self) -> DescribeWorkflowExecutionResponse {
316        self.raw_description
317    }
318
319    fn workflow_info(&self) -> &workflow::WorkflowExecutionInfo {
320        self.raw_description
321            .workflow_execution_info
322            .as_ref()
323            .expect("describe response missing workflow_execution_info")
324    }
325
326    fn execution(&self) -> &ProtoWorkflowExecution {
327        self.workflow_info()
328            .execution
329            .as_ref()
330            .expect("describe response missing workflow_execution_info.execution")
331    }
332
333    fn workflow_type_info(
334        &self,
335    ) -> &temporalio_common::protos::temporal::api::common::v1::WorkflowType {
336        self.workflow_info()
337            .r#type
338            .as_ref()
339            .expect("describe response missing workflow_execution_info.type")
340    }
341}
342
343/// Workflow execution history returned by [`WorkflowHandle::fetch_history`].
344///
345/// Events and their containing pages are fetched lazily as this stream is polled. Use
346/// [`into_events`](Self::into_events) to fetch and collect all events at once.
347#[derive(derive_more::Debug)]
348pub struct WorkflowHistory {
349    #[debug(skip)]
350    inner: Pin<Box<dyn Stream<Item = Result<HistoryEvent, WorkflowInteractionError>> + Send>>,
351    workflow_id: Option<String>,
352}
353
354impl From<history::v1::History> for WorkflowHistory {
355    fn from(history: history::v1::History) -> Self {
356        let workflow_id =
357            history
358                .events
359                .first()
360                .and_then(|event| match event.attributes.as_ref() {
361                    Some(Attributes::WorkflowExecutionStartedEventAttributes(attributes))
362                        if !attributes.workflow_id.is_empty() =>
363                    {
364                        Some(attributes.workflow_id.clone())
365                    }
366                    _ => None,
367                });
368        Self {
369            inner: Box::pin(stream::iter(history.events.into_iter().map(Ok))),
370            workflow_id,
371        }
372    }
373}
374
375impl Stream for WorkflowHistory {
376    type Item = Result<HistoryEvent, WorkflowInteractionError>;
377
378    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
379        self.inner.as_mut().poll_next(cx)
380    }
381}
382
383/// Error fetching or converting a workflow history.
384#[derive(Debug, thiserror::Error)]
385#[non_exhaustive]
386pub enum WorkflowHistoryError {
387    /// Fetching the workflow history failed.
388    #[error("failed to fetch workflow history: {0}")]
389    Fetch(#[from] WorkflowInteractionError),
390    /// Converting the workflow history JSON failed.
391    #[error("failed to convert workflow history JSON: {0}")]
392    Json(#[from] serde_json::Error),
393}
394
395impl WorkflowHistory {
396    /// Decode a workflow history from JSON bytes.
397    pub fn from_json(bytes: &[u8]) -> Result<Self, WorkflowHistoryError> {
398        let history: History = serde_json::from_slice(bytes)?;
399        Ok(history.into())
400    }
401
402    /// Fetch all remaining events and encode this workflow history as JSON bytes.
403    pub async fn to_json(self) -> Result<Vec<u8>, WorkflowHistoryError> {
404        Ok(serde_json::to_vec(&History {
405            events: self.into_events().await?,
406        })?)
407    }
408
409    /// Return the workflow ID when it is known.
410    pub fn workflow_id(&self) -> Option<&str> {
411        self.workflow_id.as_deref()
412    }
413
414    /// Fetch all remaining history pages and collect their events.
415    pub async fn into_events(self) -> Result<Vec<HistoryEvent>, WorkflowInteractionError> {
416        self.inner.try_collect().await
417    }
418}
419
420/// A workflow handle which can refer to a specific workflow run, or a chain of workflow runs with
421/// the same workflow id.
422#[derive(Clone)]
423pub struct WorkflowHandle<ClientT, W> {
424    client: ClientT,
425    info: WorkflowExecutionInfo,
426
427    _wf_type: PhantomData<W>,
428}
429
430impl<CT, W> WorkflowHandle<CT, W> {
431    /// Return the run id of the Workflow Execution pointed at by this handle, if there is one.
432    pub fn run_id(&self) -> Option<&str> {
433        self.info.run_id.as_deref()
434    }
435}
436
437/// Holds needed information to refer to a specific workflow run, or workflow execution chain
438#[derive(Debug, Clone, bon::Builder)]
439#[builder(on(String, into), state_mod(vis = "pub"))]
440#[non_exhaustive]
441pub struct WorkflowExecutionInfo {
442    /// Namespace the workflow lives in.
443    pub namespace: String,
444    /// The workflow's id.
445    pub workflow_id: String,
446    /// If set, target this specific run of the workflow.
447    pub run_id: Option<String>,
448    /// Run ID used for cancellation and termination to ensure they happen on a workflow starting
449    /// with this run ID. This can be set when getting a workflow handle. When starting a workflow,
450    /// this is set as the resulting run ID if no start signal was provided.
451    pub first_execution_run_id: Option<String>,
452}
453
454impl WorkflowExecutionInfo {
455    /// Bind the workflow info to a specific client, turning it into a workflow handle
456    pub fn bind_untyped<CT>(self, client: CT) -> UntypedWorkflowHandle<CT>
457    where
458        CT: WorkflowService + Clone,
459    {
460        UntypedWorkflowHandle::new(client, self)
461    }
462}
463
464/// A workflow handle to a workflow with unknown types. Uses single argument raw payloads for input
465/// and output.
466pub type UntypedWorkflowHandle<CT> = WorkflowHandle<CT, UntypedWorkflow>;
467
468/// Marker type for sending untyped signals. Stores the signal name for runtime lookup.
469///
470/// Use with `handle.signal(UntypedSignal::new("signal_name"), raw_payload)`.
471pub struct UntypedSignal<W> {
472    name: String,
473    _wf: PhantomData<W>,
474}
475
476impl<W> UntypedSignal<W> {
477    /// Create a new `UntypedSignal` with the given signal name.
478    pub fn new(name: impl Into<String>) -> Self {
479        Self {
480            name: name.into(),
481            _wf: PhantomData,
482        }
483    }
484}
485
486impl<W: WorkflowDefinition> SignalDefinition for UntypedSignal<W> {
487    type Workflow = W;
488    type Input = RawValue;
489
490    fn name(&self) -> &str {
491        &self.name
492    }
493}
494
495/// Marker type for sending untyped queries. Stores the query name for runtime lookup.
496///
497/// Use with `handle.query(UntypedQuery::new("query_name"), raw_payload)`.
498pub struct UntypedQuery<W> {
499    name: String,
500    _wf: PhantomData<W>,
501}
502
503impl<W> UntypedQuery<W> {
504    /// Create a new `UntypedQuery` with the given query name.
505    pub fn new(name: impl Into<String>) -> Self {
506        Self {
507            name: name.into(),
508            _wf: PhantomData,
509        }
510    }
511}
512
513impl<W: WorkflowDefinition> QueryDefinition for UntypedQuery<W> {
514    type Workflow = W;
515    type Input = RawValue;
516    type Output = RawValue;
517
518    fn name(&self) -> &str {
519        &self.name
520    }
521}
522
523/// Marker type for sending untyped updates. Stores the update name for runtime lookup.
524///
525/// Use with `handle.update(UntypedUpdate::new("update_name"), raw_payload)`.
526pub struct UntypedUpdate<W> {
527    name: String,
528    _wf: PhantomData<W>,
529}
530
531impl<W> UntypedUpdate<W> {
532    /// Create a new `UntypedUpdate` with the given update name.
533    pub fn new(name: impl Into<String>) -> Self {
534        Self {
535            name: name.into(),
536            _wf: PhantomData,
537        }
538    }
539}
540
541impl<W: WorkflowDefinition> UpdateDefinition for UntypedUpdate<W> {
542    type Workflow = W;
543    type Input = RawValue;
544    type Output = RawValue;
545
546    fn name(&self) -> &str {
547        &self.name
548    }
549}
550
551/// Shared by [WorkflowHandle::start_update] and the client's update-with-start, which sends the
552/// same update request as one of its operations. Update starts always wait for the update to be
553/// accepted; results are waited on separately via the update handle.
554#[allow(clippy::too_many_arguments)]
555pub(crate) fn build_update_workflow_request(
556    namespace: String,
557    identity: String,
558    workflow_id: String,
559    run_id: String,
560    update_id: String,
561    update_name: String,
562    header: Option<Header>,
563    payloads: Vec<Payload>,
564) -> UpdateWorkflowExecutionRequest {
565    UpdateWorkflowExecutionRequest {
566        namespace,
567        workflow_execution: Some(ProtoWorkflowExecution {
568            workflow_id,
569            run_id,
570        }),
571        wait_policy: Some(WaitPolicy {
572            lifecycle_stage: UpdateWorkflowExecutionLifecycleStage::Accepted.into(),
573        }),
574        request: Some(update::v1::Request {
575            meta: Some(update::v1::Meta {
576                update_id,
577                identity,
578            }),
579            input: Some(update::v1::Input {
580                header,
581                name: update_name,
582                args: Some(Payloads { payloads }),
583            }),
584            ..Default::default()
585        }),
586        ..Default::default()
587    }
588}
589
590impl<CT, W> WorkflowHandle<CT, W>
591where
592    CT: WorkflowService + Clone,
593    W: HasWorkflowDefinition,
594{
595    /// Create a workflow handle from a client and identifying information.
596    pub fn new(client: CT, info: WorkflowExecutionInfo) -> Self {
597        Self {
598            client,
599            info,
600            _wf_type: PhantomData::<W>,
601        }
602    }
603
604    /// Get the workflow execution info
605    pub fn info(&self) -> &WorkflowExecutionInfo {
606        &self.info
607    }
608
609    /// Get the client attached to this handle
610    pub fn client(&self) -> &CT {
611        &self.client
612    }
613
614    /// Await the result of the workflow execution
615    pub async fn get_result(
616        &self,
617        opts: WorkflowGetResultOptions,
618    ) -> Result<W::Output, WorkflowGetResultError>
619    where
620        CT: WorkflowService + NamespacedClient + Clone + 'static,
621    {
622        let raw = self.get_result_raw(opts).await?;
623        match raw {
624            WorkflowExecutionResult::Succeeded(v) => Ok(v),
625            WorkflowExecutionResult::Failed(f) => Err(WorkflowGetResultError::Failed(Box::new(f))),
626            WorkflowExecutionResult::Cancelled { details } => {
627                Err(WorkflowGetResultError::Cancelled { details })
628            }
629            WorkflowExecutionResult::Terminated { details } => {
630                Err(WorkflowGetResultError::Terminated { details })
631            }
632            WorkflowExecutionResult::TimedOut => Err(WorkflowGetResultError::TimedOut),
633            WorkflowExecutionResult::ContinuedAsNew => Err(WorkflowGetResultError::ContinuedAsNew),
634        }
635    }
636
637    /// Await the result of the workflow execution, returning the full
638    /// [`WorkflowExecutionResult`] enum for callers that need to inspect non-success outcomes
639    /// directly.
640    async fn get_result_raw(
641        &self,
642        opts: WorkflowGetResultOptions,
643    ) -> Result<WorkflowExecutionResult<W::Output>, WorkflowInteractionError>
644    where
645        CT: WorkflowService + NamespacedClient + Clone + 'static,
646    {
647        let mut run_id = self.info.run_id.clone().unwrap_or_default();
648        let fetch_opts = WorkflowFetchHistoryOptions::builder()
649            .skip_archival(true)
650            .wait_new_event(true)
651            .event_filter_type(HistoryEventFilterType::CloseEvent)
652            .rpc_options(opts.rpc_options.clone())
653            .build();
654
655        loop {
656            let history = self.fetch_history_for_run(&run_id, fetch_opts.clone());
657            let mut events = history.into_events().await?;
658
659            if events.is_empty() {
660                continue;
661            }
662
663            let event_attrs = events.pop().and_then(|ev| ev.attributes);
664
665            macro_rules! follow {
666                ($attrs:ident) => {
667                    if opts.follow_runs && $attrs.new_execution_run_id != "" {
668                        run_id = $attrs.new_execution_run_id;
669                        continue;
670                    }
671                };
672            }
673
674            let dc = self.client.data_converter();
675
676            break match event_attrs {
677                Some(Attributes::WorkflowExecutionCompletedEventAttributes(attrs)) => {
678                    follow!(attrs);
679                    let payload = attrs
680                        .result
681                        .and_then(|p| p.payloads.into_iter().next())
682                        .unwrap_or_default();
683                    let result: W::Output = dc
684                        .from_payload(&SerializationContextData::Workflow(WorkflowSerializationContext::new()), payload)
685                        .await?;
686                    Ok(WorkflowExecutionResult::Succeeded(result))
687                }
688                Some(Attributes::WorkflowExecutionFailedEventAttributes(attrs)) => {
689                    follow!(attrs);
690                    let mut failure = attrs.failure.unwrap_or_default();
691                    decode_payloads(
692                        &mut failure,
693                        dc.codec(),
694                        &SerializationContextData::Workflow(WorkflowSerializationContext::new()),
695                    )
696                    .await?;
697                    let error = dc.failure_converter().to_error(
698                        failure,
699                        dc.payload_converter(),
700                        &SerializationContextData::Workflow(WorkflowSerializationContext::new()),
701                    )?;
702                    Ok(WorkflowExecutionResult::Failed(error))
703                }
704                Some(Attributes::WorkflowExecutionCanceledEventAttributes(attrs)) => {
705                    Ok(WorkflowExecutionResult::Cancelled {
706                        details: WorkflowResultDetails::new(Vec::from_payloads(attrs.details), dc)
707                            .await?,
708                    })
709                }
710                Some(Attributes::WorkflowExecutionTimedOutEventAttributes(attrs)) => {
711                    follow!(attrs);
712                    Ok(WorkflowExecutionResult::TimedOut)
713                }
714                Some(Attributes::WorkflowExecutionTerminatedEventAttributes(attrs)) => {
715                    Ok(WorkflowExecutionResult::Terminated {
716                        details: WorkflowResultDetails::new(Vec::from_payloads(attrs.details), dc)
717                            .await?,
718                    })
719                }
720                Some(Attributes::WorkflowExecutionContinuedAsNewEventAttributes(attrs)) => {
721                    if opts.follow_runs {
722                        if !attrs.new_execution_run_id.is_empty() {
723                            run_id = attrs.new_execution_run_id;
724                            continue;
725                        } else {
726                            return Err(WorkflowInteractionError::Other(
727                                "New execution run id was empty in continue as new event!".into(),
728                            ));
729                        }
730                    } else {
731                        Ok(WorkflowExecutionResult::ContinuedAsNew)
732                    }
733                }
734                o => Err(WorkflowInteractionError::Other(
735                    format!(
736                        "Server returned an event that didn't match the CloseEvent filter. \
737                         This is either a server bug or a new event the SDK does not understand. \
738                         Event details: {o:?}"
739                    )
740                    .into(),
741                )),
742            };
743        }
744    }
745
746    /// Send a signal to the workflow
747    pub async fn signal<S>(
748        &self,
749        signal: S,
750        input: S::Input,
751        opts: WorkflowSignalOptions,
752    ) -> Result<(), WorkflowInteractionError>
753    where
754        CT: WorkflowService + NamespacedClient + Clone,
755        S: SignalDefinition<Workflow = W::Run>,
756        S::Input: Send,
757    {
758        interceptors::call_signal_workflow(
759            self.client.client_interceptors(),
760            SignalWorkflowInput::new(
761                self.info.workflow_id.clone(),
762                self.info.run_id.clone().unwrap_or_default(),
763                signal.name().to_string(),
764                input,
765                opts,
766            ),
767            Next::new({
768                let mut client = self.client.clone();
769                move |input: SignalWorkflowInput| -> BoxFuture<
770                    '_,
771                    Result<(), WorkflowInteractionError>,
772                > {
773                    Box::pin(async move {
774                        let (workflow_id, run_id, signal_name, args, options) =
775                            input.into_parts();
776                        let data_converter = client.data_converter().clone();
777                        let unencoded_payloads = {
778                            let payload_converter = data_converter.payload_converter();
779                            let context_data = SerializationContextData::Workflow(
780                                WorkflowSerializationContext::new(),
781                            );
782                            let context =
783                                SerializationContext::new(&context_data, payload_converter);
784                            args.serialize_payloads(&context)
785                        };
786                        drop(args);
787                        let payloads = data_converter
788                            .codec()
789                            .encode(&SerializationContextData::Workflow(WorkflowSerializationContext::new()), unencoded_payloads?)
790                            .await?;
791                        let mut request = SignalWorkflowExecutionRequest {
792                            namespace: client.namespace(),
793                            workflow_execution: Some(ProtoWorkflowExecution {
794                                workflow_id,
795                                run_id,
796                            }),
797                            signal_name,
798                            input: Some(Payloads { payloads }),
799                            identity: client.identity(),
800                            request_id: options
801                                .request_id
802                                .unwrap_or_else(|| Uuid::new_v4().to_string()),
803                            header: options.header,
804                            ..Default::default()
805                        }
806                        .into_request();
807                        options.rpc_options.apply_to(&mut request);
808                        WorkflowService::signal_workflow_execution(&mut client, request)
809                            .await
810                            .map_err(WorkflowInteractionError::from_status)?;
811                        Ok(())
812                    })
813                }
814            }),
815        )
816        .await
817    }
818
819    /// Query the workflow
820    pub async fn query<Q>(
821        &self,
822        query: Q,
823        input: Q::Input,
824        opts: WorkflowQueryOptions,
825    ) -> Result<Q::Output, WorkflowQueryError>
826    where
827        CT: WorkflowService + NamespacedClient + Clone,
828        Q: QueryDefinition<Workflow = W::Run>,
829        Q::Input: Send,
830    {
831        let output = interceptors::call_query_workflow(
832            self.client.client_interceptors(),
833            QueryWorkflowInput::new(
834                self.info.workflow_id.clone(),
835                self.info.run_id.clone().unwrap_or_default(),
836                query.name().to_string(),
837                input,
838                opts,
839            ),
840            Next::new({
841                let mut client = self.client.clone();
842                move |input: QueryWorkflowInput| -> BoxFuture<
843                    '_,
844                    Result<QueryWorkflowOutput, WorkflowQueryError>,
845                > {
846                    Box::pin(async move {
847                        let (workflow_id, run_id, query_name, args, options) = input.into_parts();
848                        let data_converter = client.data_converter().clone();
849                        let unencoded_payloads = {
850                            let payload_converter = data_converter.payload_converter();
851                            let context_data = SerializationContextData::Workflow(
852                                WorkflowSerializationContext::new(),
853                            );
854                            let context =
855                                SerializationContext::new(&context_data, payload_converter);
856                            args.serialize_payloads(&context)
857                        };
858                        drop(args);
859                        let payloads = data_converter
860                            .codec()
861                            .encode(&SerializationContextData::Workflow(WorkflowSerializationContext::new()), unencoded_payloads?)
862                            .await?;
863                        let mut request = QueryWorkflowRequest {
864                            namespace: client.namespace(),
865                            execution: Some(ProtoWorkflowExecution {
866                                workflow_id,
867                                run_id,
868                            }),
869                            query: Some(WorkflowQuery {
870                                query_type: query_name,
871                                query_args: Some(Payloads { payloads }),
872                                header: options.header,
873                            }),
874                            query_reject_condition: options
875                                .reject_condition
876                                .map(|condition| condition as i32)
877                                .unwrap_or(1),
878                        }
879                        .into_request();
880                        options.rpc_options.apply_to(&mut request);
881                        let response = client
882                            .query_workflow(request)
883                            .await
884                            .map_err(WorkflowQueryError::from_status)?
885                            .into_inner();
886                        Ok(QueryWorkflowOutput::new(response))
887                    })
888                }
889            }),
890        )
891        .await?;
892        let response = output.response;
893
894        if let Some(rejected) = response.query_rejected {
895            return Err(WorkflowQueryError::Rejected {
896                status: (rejected.status != 0)
897                    .then(|| WorkflowExecutionStatus::from_raw(rejected.status)),
898            });
899        }
900
901        let result_payloads = response
902            .query_result
903            .map(|p| p.payloads)
904            .unwrap_or_default();
905
906        self.client
907            .data_converter()
908            .from_payloads(
909                &SerializationContextData::Workflow(WorkflowSerializationContext::new()),
910                result_payloads,
911            )
912            .await
913            .map_err(WorkflowQueryError::from)
914    }
915
916    /// Send an update to the workflow and wait for it to complete, returning the result.
917    pub async fn execute_update<U>(
918        &self,
919        update: U,
920        input: U::Input,
921        options: WorkflowExecuteUpdateOptions,
922    ) -> Result<U::Output, WorkflowUpdateError>
923    where
924        CT: WorkflowService + NamespacedClient + Clone,
925        U: UpdateDefinition<Workflow = W::Run>,
926        U::Input: Send,
927        U::Output: 'static,
928    {
929        let rpc_options = options.rpc_options.clone();
930        let handle = self.start_update(update, input, options.into()).await?;
931        handle.get_result(rpc_options).await
932    }
933
934    /// Start an update and return a handle without waiting for completion.
935    /// Use `execute_update()` if you want to wait for the result immediately.
936    pub async fn start_update<U>(
937        &self,
938        update: U,
939        input: U::Input,
940        options: WorkflowStartUpdateOptions,
941    ) -> Result<WorkflowUpdateHandle<CT, U::Output>, WorkflowUpdateError>
942    where
943        CT: WorkflowService + NamespacedClient + Clone,
944        U: UpdateDefinition<Workflow = W::Run>,
945        U::Input: Send,
946    {
947        let output = interceptors::call_start_workflow_update(
948            self.client.client_interceptors(),
949            StartWorkflowUpdateInput::new(
950                self.info().workflow_id.clone(),
951                self.info().run_id.clone().unwrap_or_default(),
952                update.name().to_string(),
953                input,
954                options,
955            ),
956            Next::new({
957                let mut client = self.client.clone();
958                move |input: StartWorkflowUpdateInput| -> BoxFuture<
959                        '_,
960                        Result<StartWorkflowUpdateOutput, WorkflowUpdateError>,
961                    > {
962                        Box::pin(async move {
963                            let (workflow_id, run_id, update_name, args, options) =
964                                input.into_parts();
965                            let data_converter = client.data_converter().clone();
966                            let unencoded_payloads = {
967                                let payload_converter = data_converter.payload_converter();
968                                let context_data = SerializationContextData::Workflow(
969                                    WorkflowSerializationContext::new(),
970                                );
971                                let context =
972                                    SerializationContext::new(&context_data, payload_converter);
973                                args.serialize_payloads(&context)
974                            };
975                            drop(args);
976                            let payloads = data_converter
977                                .codec()
978                                .encode(
979                                    &SerializationContextData::Workflow(
980                                        WorkflowSerializationContext::new(),
981                                    ),
982                                    unencoded_payloads?,
983                                )
984                                .await?;
985                            let update_id = options
986                                .update_id
987                                .unwrap_or_else(|| Uuid::new_v4().to_string());
988                            let mut request = build_update_workflow_request(
989                                client.namespace(),
990                                client.identity(),
991                                workflow_id.clone(),
992                                run_id,
993                                update_id.clone(),
994                                update_name,
995                                options.header,
996                                payloads,
997                            )
998                            .into_request();
999                            options.rpc_options.apply_to(&mut request);
1000                            let response =
1001                                WorkflowService::update_workflow_execution(&mut client, request)
1002                                    .await
1003                                    .map_err(WorkflowUpdateError::from_status)?
1004                                    .into_inner();
1005                            let run_id = response
1006                                .update_ref
1007                                .as_ref()
1008                                .and_then(|reference| reference.workflow_execution.as_ref())
1009                                .map(|execution| execution.run_id.clone())
1010                                .filter(|run_id| !run_id.is_empty());
1011                            Ok(StartWorkflowUpdateOutput::new(
1012                                update_id,
1013                                workflow_id,
1014                                run_id,
1015                                response.outcome,
1016                            ))
1017                        })
1018                    }
1019            }),
1020        )
1021        .await?;
1022
1023        Ok(WorkflowUpdateHandle::new(
1024            self.client.clone(),
1025            output.update_id,
1026            output.workflow_id,
1027            output.run_id.or_else(|| self.info().run_id.clone()),
1028            output.known_outcome,
1029        ))
1030    }
1031
1032    /// Get a handle to an existing update.
1033    ///
1034    /// The update definition determines the result type. The returned handle uses this workflow
1035    /// handle's workflow and run IDs and does not validate the update ID until
1036    /// [`get_result`](WorkflowUpdateHandle::get_result) is called.
1037    pub fn get_update_handle<U>(
1038        &self,
1039        update: U,
1040        update_id: impl Into<String>,
1041    ) -> WorkflowUpdateHandle<CT, U::Output>
1042    where
1043        U: UpdateDefinition<Workflow = W::Run>,
1044    {
1045        let _ = update;
1046        WorkflowUpdateHandle::new(
1047            self.client.clone(),
1048            update_id.into(),
1049            self.info.workflow_id.clone(),
1050            self.info.run_id.clone(),
1051            None,
1052        )
1053    }
1054
1055    /// Request cancellation of this workflow.
1056    pub async fn cancel(&self, opts: WorkflowCancelOptions) -> Result<(), WorkflowInteractionError>
1057    where
1058        CT: NamespacedClient,
1059    {
1060        interceptors::call_cancel_workflow(
1061            self.client.client_interceptors(),
1062            CancelWorkflowInput {
1063                workflow_id: self.info.workflow_id.clone(),
1064                run_id: self.info.run_id.clone().unwrap_or_default(),
1065                first_execution_run_id: self
1066                    .info
1067                    .first_execution_run_id
1068                    .clone()
1069                    .unwrap_or_default(),
1070                options: opts,
1071            },
1072            Next::new({
1073                let mut client = self.client.clone();
1074                move |input: CancelWorkflowInput| -> BoxFuture<
1075                    '_,
1076                    Result<(), WorkflowInteractionError>,
1077                > {
1078                    Box::pin(async move {
1079                        let mut request = RequestCancelWorkflowExecutionRequest {
1080                            namespace: client.namespace(),
1081                            workflow_execution: Some(ProtoWorkflowExecution {
1082                                workflow_id: input.workflow_id,
1083                                run_id: input.run_id,
1084                            }),
1085                            identity: client.identity(),
1086                            request_id: input
1087                                .options
1088                                .request_id
1089                                .clone()
1090                                .unwrap_or_else(|| Uuid::new_v4().to_string()),
1091                            first_execution_run_id: input.first_execution_run_id,
1092                            reason: input.options.reason.clone(),
1093                            links: vec![],
1094                        }
1095                        .into_request();
1096                        input.options.rpc_options.apply_to(&mut request);
1097                        WorkflowService::request_cancel_workflow_execution(&mut client, request)
1098                            .await
1099                            .map_err(WorkflowInteractionError::from_status)?;
1100                        Ok(())
1101                    })
1102                }
1103            }),
1104        )
1105        .await
1106    }
1107
1108    /// Terminate this workflow.
1109    pub async fn terminate(
1110        &self,
1111        opts: WorkflowTerminateOptions,
1112    ) -> Result<(), WorkflowInteractionError>
1113    where
1114        CT: NamespacedClient,
1115    {
1116        interceptors::call_terminate_workflow(
1117            self.client.client_interceptors(),
1118            TerminateWorkflowInput {
1119                workflow_id: self.info.workflow_id.clone(),
1120                run_id: self.info.run_id.clone().unwrap_or_default(),
1121                first_execution_run_id: self
1122                    .info
1123                    .first_execution_run_id
1124                    .clone()
1125                    .unwrap_or_default(),
1126                options: opts,
1127            },
1128            Next::new({
1129                let mut client = self.client.clone();
1130                move |input: TerminateWorkflowInput| -> BoxFuture<
1131                    '_,
1132                    Result<(), WorkflowInteractionError>,
1133                > {
1134                    Box::pin(async move {
1135                        let mut request = TerminateWorkflowExecutionRequest {
1136                            namespace: client.namespace(),
1137                            workflow_execution: Some(ProtoWorkflowExecution {
1138                                workflow_id: input.workflow_id,
1139                                run_id: input.run_id,
1140                            }),
1141                            reason: input.options.reason.clone(),
1142                            details: input.options.details.clone(),
1143                            identity: client.identity(),
1144                            first_execution_run_id: input.first_execution_run_id,
1145                            links: vec![],
1146                        }
1147                        .into_request();
1148                        input.options.rpc_options.apply_to(&mut request);
1149                        WorkflowService::terminate_workflow_execution(&mut client, request)
1150                            .await
1151                            .map_err(WorkflowInteractionError::from_status)?;
1152                        Ok(())
1153                    })
1154                }
1155            }),
1156        )
1157        .await
1158    }
1159
1160    /// Get workflow execution description/metadata.
1161    pub async fn describe(
1162        &self,
1163        opts: WorkflowDescribeOptions,
1164    ) -> Result<WorkflowExecutionDescription, WorkflowInteractionError>
1165    where
1166        CT: NamespacedClient,
1167    {
1168        let output = interceptors::call_describe_workflow(
1169            self.client.client_interceptors(),
1170            DescribeWorkflowInput {
1171                workflow_id: self.info.workflow_id.clone(),
1172                run_id: self.info.run_id.clone().unwrap_or_default(),
1173                options: opts,
1174            },
1175            Next::new({
1176                let mut client = self.client.clone();
1177                move |input: DescribeWorkflowInput| -> BoxFuture<
1178                        '_,
1179                        Result<DescribeWorkflowOutput, WorkflowInteractionError>,
1180                    > {
1181                        Box::pin(async move {
1182                            let mut request = DescribeWorkflowExecutionRequest {
1183                                namespace: client.namespace(),
1184                                execution: Some(ProtoWorkflowExecution {
1185                                    workflow_id: input.workflow_id,
1186                                    run_id: input.run_id,
1187                                }),
1188                            }
1189                            .into_request();
1190                            input.options.rpc_options.apply_to(&mut request);
1191                            let response =
1192                                WorkflowService::describe_workflow_execution(&mut client, request)
1193                                    .await
1194                                    .map_err(WorkflowInteractionError::from_status)?
1195                                    .into_inner();
1196                            Ok(DescribeWorkflowOutput::new(response))
1197                        })
1198                    }
1199            }),
1200        )
1201        .await?;
1202        WorkflowExecutionDescription::new(output.response, self.client.data_converter())
1203            .await
1204            .map_err(WorkflowInteractionError::from)
1205    }
1206    /// Fetch workflow execution history as a lazy stream.
1207    ///
1208    /// No request is sent until the returned stream is polled.
1209    pub fn fetch_history(&self, opts: WorkflowFetchHistoryOptions) -> WorkflowHistory
1210    where
1211        CT: NamespacedClient + 'static,
1212    {
1213        let run_id = self.info.run_id.clone().unwrap_or_default();
1214        self.fetch_history_for_run(&run_id, opts)
1215    }
1216
1217    fn fetch_history_for_run(
1218        &self,
1219        run_id: &str,
1220        opts: WorkflowFetchHistoryOptions,
1221    ) -> WorkflowHistory
1222    where
1223        CT: NamespacedClient + 'static,
1224    {
1225        let client = self.client.clone();
1226        let workflow_id = self.info.workflow_id.clone();
1227        let history_workflow_id = workflow_id.clone();
1228        let run_id = run_id.to_string();
1229
1230        let stream = stream::unfold(
1231            (Vec::new(), VecDeque::new(), false),
1232            move |(mut next_page_token, mut buffer, mut exhausted)| {
1233                let client = client.clone();
1234                let workflow_id = workflow_id.clone();
1235                let run_id = run_id.clone();
1236                let opts = opts.clone();
1237
1238                async move {
1239                    loop {
1240                        if let Some(event) = buffer.pop_front() {
1241                            return Some((Ok(event), (next_page_token, buffer, exhausted)));
1242                        }
1243
1244                        if exhausted {
1245                            return None;
1246                        }
1247
1248                        let output = interceptors::call_fetch_workflow_history_page(
1249                            client.client_interceptors(),
1250                            FetchWorkflowHistoryPageInput {
1251                                workflow_id: workflow_id.clone(),
1252                                run_id: run_id.clone(),
1253                                next_page_token: next_page_token.clone(),
1254                                options: opts.clone(),
1255                            },
1256                            Next::new({
1257                                let mut rpc_client = client.clone();
1258                                move |input: FetchWorkflowHistoryPageInput| -> BoxFuture<
1259                                    '_,
1260                                    Result<
1261                                        FetchWorkflowHistoryPageOutput,
1262                                        WorkflowInteractionError,
1263                                    >,
1264                                > {
1265                                    Box::pin(async move {
1266                                        let mut request = GetWorkflowExecutionHistoryRequest {
1267                                            namespace: rpc_client.namespace(),
1268                                            execution: Some(ProtoWorkflowExecution {
1269                                                workflow_id: input.workflow_id,
1270                                                run_id: input.run_id,
1271                                            }),
1272                                            next_page_token: input.next_page_token,
1273                                            skip_archival: input.options.skip_archival,
1274                                            wait_new_event: input.options.wait_new_event,
1275                                            history_event_filter_type: input
1276                                                .options
1277                                                .event_filter_type
1278                                                as i32,
1279                                            ..Default::default()
1280                                        }
1281                                        .into_request();
1282                                        input.options.rpc_options.apply_to(&mut request);
1283                                        let response =
1284                                            WorkflowService::get_workflow_execution_history(
1285                                                &mut rpc_client,
1286                                                request,
1287                                            )
1288                                            .await
1289                                            .map_err(WorkflowInteractionError::from_status)?
1290                                            .into_inner();
1291                                        Ok(FetchWorkflowHistoryPageOutput::new(
1292                                            response
1293                                                .history
1294                                                .map(|history| history.events)
1295                                                .unwrap_or_default(),
1296                                            response.next_page_token,
1297                                        ))
1298                                    })
1299                                }
1300                            }),
1301                        )
1302                        .await;
1303
1304                        match output {
1305                            Ok(output) => {
1306                                exhausted = output.next_page_token.is_empty();
1307                                next_page_token = output.next_page_token;
1308                                buffer = output.events.into();
1309                            }
1310                            Err(error) => {
1311                                return Some((Err(error), (next_page_token, buffer, true)));
1312                            }
1313                        }
1314                    }
1315                }
1316            },
1317        );
1318
1319        WorkflowHistory {
1320            inner: Box::pin(stream),
1321            workflow_id: Some(history_workflow_id),
1322        }
1323    }
1324}
1325
1326/// Handle to a workflow update that has been started but may not be complete.
1327///
1328/// Use [`get_result`](Self::get_result) to wait for the update to complete and retrieve its result.
1329pub struct WorkflowUpdateHandle<CT, T> {
1330    client: CT,
1331    update_id: String,
1332    workflow_id: String,
1333    run_id: Option<String>,
1334    /// If the update was started with `Completed` wait stage, the outcome is already available.
1335    known_outcome: Option<update::v1::Outcome>,
1336    _output: PhantomData<T>,
1337}
1338
1339impl<CT, T> WorkflowUpdateHandle<CT, T> {
1340    pub(crate) fn new(
1341        client: CT,
1342        update_id: String,
1343        workflow_id: String,
1344        run_id: Option<String>,
1345        known_outcome: Option<update::v1::Outcome>,
1346    ) -> Self {
1347        Self {
1348            client,
1349            update_id,
1350            workflow_id,
1351            run_id,
1352            known_outcome,
1353            _output: PhantomData,
1354        }
1355    }
1356
1357    /// Get the update ID.
1358    pub fn id(&self) -> &str {
1359        &self.update_id
1360    }
1361
1362    /// Get the workflow ID.
1363    pub fn workflow_id(&self) -> &str {
1364        &self.workflow_id
1365    }
1366
1367    /// Get the workflow run ID, if available.
1368    pub fn workflow_run_id(&self) -> Option<&str> {
1369        self.run_id.as_deref()
1370    }
1371}
1372
1373impl<CT, T: 'static> WorkflowUpdateHandle<CT, T>
1374where
1375    CT: WorkflowService + NamespacedClient + Clone,
1376{
1377    /// Wait for the update to complete and return the result using the provided RPC controls.
1378    pub async fn get_result(&self, rpc_options: RpcOptions) -> Result<T, WorkflowUpdateError>
1379    where
1380        T: temporalio_common::data_converters::TemporalDeserializable,
1381    {
1382        let output = interceptors::call_poll_workflow_update(
1383            self.client.client_interceptors(),
1384            PollWorkflowUpdateInput {
1385                update_id: self.update_id.clone(),
1386                workflow_id: self.workflow_id.clone(),
1387                run_id: self.run_id.clone().unwrap_or_default(),
1388                rpc_options,
1389            },
1390            Next::new({
1391                let mut client = self.client.clone();
1392                let known_outcome = self.known_outcome.clone();
1393                move |input: PollWorkflowUpdateInput| -> BoxFuture<
1394                    '_,
1395                    Result<PollWorkflowUpdateOutput, WorkflowUpdateError>,
1396                > {
1397                    Box::pin(async move {
1398                        if let Some(outcome) = known_outcome {
1399                            return Ok(PollWorkflowUpdateOutput::new(outcome));
1400                        }
1401                        // The server's internal long-poll timeout (~60s) may expire before the update
1402                        // completes, returning a response with outcome: None. Keep polling until we
1403                        // get an actual outcome.
1404                        loop {
1405                            let mut request = PollWorkflowExecutionUpdateRequest {
1406                                namespace: client.namespace(),
1407                                update_ref: Some(update::v1::UpdateRef {
1408                                    workflow_execution: Some(ProtoWorkflowExecution {
1409                                        workflow_id: input.workflow_id.clone(),
1410                                        run_id: input.run_id.clone(),
1411                                    }),
1412                                    update_id: input.update_id.clone(),
1413                                }),
1414                                identity: client.identity(),
1415                                wait_policy: Some(WaitPolicy {
1416                                    lifecycle_stage:
1417                                        UpdateWorkflowExecutionLifecycleStage::Completed.into(),
1418                                }),
1419                            }
1420                            .into_request();
1421                            input.rpc_options.apply_to(&mut request);
1422                            let response = WorkflowService::poll_workflow_execution_update(
1423                                &mut client,
1424                                request,
1425                            )
1426                            .await
1427                            .map_err(WorkflowUpdateError::from_status)?
1428                            .into_inner();
1429                            if let Some(outcome) = response.outcome {
1430                                return Ok(PollWorkflowUpdateOutput::new(outcome));
1431                            }
1432                        }
1433                    })
1434                }
1435            }),
1436        )
1437        .await?;
1438        let outcome = output.outcome;
1439
1440        match outcome.value {
1441            Some(update::v1::outcome::Value::Success(success)) => self
1442                .client
1443                .data_converter()
1444                .from_payloads(
1445                    &SerializationContextData::Workflow(WorkflowSerializationContext::new()),
1446                    success.payloads,
1447                )
1448                .await
1449                .map_err(WorkflowUpdateError::from),
1450            Some(update::v1::outcome::Value::Failure(failure)) => {
1451                Err(WorkflowUpdateError::Failed(Box::new(failure)))
1452            }
1453            None => Err(WorkflowUpdateError::Other(
1454                "Update returned no outcome value".into(),
1455            )),
1456        }
1457    }
1458}
1459
1460#[cfg(test)]
1461mod tests {
1462    use super::*;
1463    use crate::{ClientInterceptor, test_helpers::XorCodec};
1464    use futures_util::{FutureExt, StreamExt};
1465    use std::{
1466        collections::{HashMap, VecDeque},
1467        sync::{
1468            Arc, Mutex,
1469            atomic::{AtomicUsize, Ordering},
1470        },
1471    };
1472    use temporalio_common::{
1473        data_converters::DefaultFailureConverter,
1474        protos::temporal::api::{
1475            common::v1::{Memo, SearchAttributes},
1476            enums::v1::WorkflowExecutionStatus as ProtoWorkflowExecutionStatus,
1477            history::v1::WorkflowExecutionStartedEventAttributes,
1478            sdk::v1::UserMetadata,
1479            workflow::v1::WorkflowExecutionConfig,
1480            workflowservice::v1::GetWorkflowExecutionHistoryResponse,
1481        },
1482    };
1483    use tonic::{Request, Response};
1484
1485    #[tokio::test]
1486    async fn workflow_history_workflow_id_roundtrips() {
1487        let event = HistoryEvent {
1488            event_id: 1,
1489            attributes: Some(Attributes::WorkflowExecutionStartedEventAttributes(
1490                WorkflowExecutionStartedEventAttributes {
1491                    workflow_id: "workflow-id".to_owned(),
1492                    original_execution_run_id: "run-id".to_owned(),
1493                    ..Default::default()
1494                },
1495            )),
1496            ..Default::default()
1497        };
1498        let history = WorkflowHistory {
1499            inner: Box::pin(stream::iter(std::iter::once(Ok(event)))),
1500            workflow_id: None,
1501        };
1502
1503        let bytes = history.to_json().await.unwrap();
1504
1505        let decoded = WorkflowHistory::from_json(&bytes).unwrap();
1506        assert_eq!(decoded.workflow_id(), Some("workflow-id"));
1507    }
1508
1509    #[derive(Clone)]
1510    struct MockHistoryClient {
1511        responses: Arc<Mutex<VecDeque<Result<GetWorkflowExecutionHistoryResponse, tonic::Status>>>>,
1512        calls: Arc<AtomicUsize>,
1513        interceptors: Vec<Arc<dyn ClientInterceptor>>,
1514    }
1515
1516    impl NamespacedClient for MockHistoryClient {
1517        fn namespace(&self) -> String {
1518            "test-namespace".to_owned()
1519        }
1520
1521        fn identity(&self) -> String {
1522            "test-identity".to_owned()
1523        }
1524
1525        fn client_interceptors(&self) -> &[Arc<dyn ClientInterceptor>] {
1526            &self.interceptors
1527        }
1528    }
1529
1530    impl WorkflowService for MockHistoryClient {
1531        fn get_workflow_execution_history(
1532            &mut self,
1533            _request: Request<GetWorkflowExecutionHistoryRequest>,
1534        ) -> BoxFuture<'_, Result<Response<GetWorkflowExecutionHistoryResponse>, tonic::Status>>
1535        {
1536            self.calls.fetch_add(1, Ordering::SeqCst);
1537            let response = self.responses.lock().unwrap().pop_front().unwrap();
1538            async move { response.map(Response::new) }.boxed()
1539        }
1540    }
1541
1542    struct CountingHistoryInterceptor(Arc<AtomicUsize>);
1543
1544    impl ClientInterceptor for CountingHistoryInterceptor {
1545        fn fetch_workflow_history_page<'a>(
1546            &'a self,
1547            input: FetchWorkflowHistoryPageInput,
1548            next: Next<
1549                'a,
1550                FetchWorkflowHistoryPageInput,
1551                BoxFuture<'a, Result<FetchWorkflowHistoryPageOutput, WorkflowInteractionError>>,
1552            >,
1553        ) -> BoxFuture<'a, Result<FetchWorkflowHistoryPageOutput, WorkflowInteractionError>>
1554        {
1555            self.0.fetch_add(1, Ordering::SeqCst);
1556            next.run(input)
1557        }
1558    }
1559
1560    fn history_response(
1561        event_ids: impl IntoIterator<Item = i64>,
1562        next_page_token: &[u8],
1563    ) -> GetWorkflowExecutionHistoryResponse {
1564        GetWorkflowExecutionHistoryResponse {
1565            history: Some(History {
1566                events: event_ids
1567                    .into_iter()
1568                    .map(|event_id| HistoryEvent {
1569                        event_id,
1570                        ..Default::default()
1571                    })
1572                    .collect(),
1573            }),
1574            next_page_token: next_page_token.to_vec(),
1575            ..Default::default()
1576        }
1577    }
1578
1579    fn history_handle(
1580        responses: impl IntoIterator<Item = Result<GetWorkflowExecutionHistoryResponse, tonic::Status>>,
1581        calls: Arc<AtomicUsize>,
1582        interceptors: Vec<Arc<dyn ClientInterceptor>>,
1583    ) -> WorkflowHandle<MockHistoryClient, UntypedWorkflow> {
1584        WorkflowHandle::new(
1585            MockHistoryClient {
1586                responses: Arc::new(Mutex::new(responses.into_iter().collect())),
1587                calls,
1588                interceptors,
1589            },
1590            WorkflowExecutionInfo {
1591                namespace: "test-namespace".to_owned(),
1592                workflow_id: "workflow-id".to_owned(),
1593                run_id: Some("run-id".to_owned()),
1594                first_execution_run_id: None,
1595            },
1596        )
1597    }
1598
1599    #[tokio::test]
1600    async fn workflow_history_fetches_pages_lazily() {
1601        let calls = Arc::new(AtomicUsize::new(0));
1602        let interceptor_calls = Arc::new(AtomicUsize::new(0));
1603        let handle = history_handle(
1604            [
1605                Ok(history_response([], b"second-page")),
1606                Ok(history_response([1, 2], b"third-page")),
1607                Ok(history_response([3], b"")),
1608            ],
1609            calls.clone(),
1610            vec![Arc::new(CountingHistoryInterceptor(
1611                interceptor_calls.clone(),
1612            ))],
1613        );
1614
1615        let mut history = handle.fetch_history(WorkflowFetchHistoryOptions::default());
1616        assert_eq!(calls.load(Ordering::SeqCst), 0);
1617
1618        assert_eq!(history.next().await.unwrap().unwrap().event_id, 1);
1619        assert_eq!(calls.load(Ordering::SeqCst), 2);
1620        assert_eq!(history.next().await.unwrap().unwrap().event_id, 2);
1621        assert_eq!(calls.load(Ordering::SeqCst), 2);
1622        assert_eq!(history.next().await.unwrap().unwrap().event_id, 3);
1623        assert_eq!(calls.load(Ordering::SeqCst), 3);
1624        assert!(history.next().await.is_none());
1625        assert_eq!(interceptor_calls.load(Ordering::SeqCst), 3);
1626    }
1627
1628    #[tokio::test]
1629    async fn workflow_history_yields_page_error_then_ends() {
1630        let calls = Arc::new(AtomicUsize::new(0));
1631        let handle = history_handle(
1632            [
1633                Ok(history_response([1], b"second-page")),
1634                Err(tonic::Status::unavailable("history unavailable")),
1635            ],
1636            calls.clone(),
1637            Vec::new(),
1638        );
1639        let mut history = handle.fetch_history(WorkflowFetchHistoryOptions::default());
1640
1641        assert_eq!(history.next().await.unwrap().unwrap().event_id, 1);
1642        assert!(matches!(
1643            history.next().await.unwrap(),
1644            Err(WorkflowInteractionError::Rpc(status)) if status.code() == tonic::Code::Unavailable
1645        ));
1646        assert!(history.next().await.is_none());
1647        assert_eq!(calls.load(Ordering::SeqCst), 2);
1648    }
1649
1650    #[tokio::test]
1651    async fn workflow_result_details_support_typed_decoding() {
1652        let converter = DataConverter::new(
1653            PayloadConverter::default(),
1654            DefaultFailureConverter::default(),
1655            XorCodec,
1656        );
1657        let payloads = converter
1658            .to_payloads(
1659                &SerializationContextData::Workflow(WorkflowSerializationContext::new()),
1660                &"workflow-result-details".to_owned(),
1661            )
1662            .await
1663            .unwrap();
1664        let details = WorkflowResultDetails::new(payloads.clone(), &converter)
1665            .await
1666            .unwrap();
1667
1668        assert_ne!(details.raw(), payloads);
1669        let decoded_payloads = details.raw().to_vec();
1670        assert_eq!(
1671            details.deserialize::<String>().unwrap(),
1672            "workflow-result-details"
1673        );
1674        assert_eq!(details.into_raw().payloads, decoded_payloads);
1675    }
1676
1677    #[tokio::test]
1678    async fn workflow_result_detail_conversion_errors_are_reported() {
1679        let details =
1680            WorkflowResultDetails::new(vec![Payload::default()], &DataConverter::default())
1681                .await
1682                .unwrap();
1683
1684        assert_eq!(details.raw(), &[Payload::default()]);
1685        assert!(details.deserialize::<String>().is_err());
1686    }
1687
1688    #[tokio::test]
1689    async fn workflow_description_memo_uses_saved_converter() {
1690        let converter = DataConverter::new(
1691            PayloadConverter::default(),
1692            DefaultFailureConverter::default(),
1693            XorCodec,
1694        );
1695        let encoded = converter
1696            .to_payload(
1697                &SerializationContextData::Workflow(WorkflowSerializationContext::new()),
1698                &"memo-value".to_owned(),
1699            )
1700            .await
1701            .unwrap();
1702        let description = WorkflowExecutionDescription::new(
1703            DescribeWorkflowExecutionResponse {
1704                workflow_execution_info: Some(workflow::WorkflowExecutionInfo {
1705                    memo: Some(Memo {
1706                        fields: HashMap::from([("memo-key".to_owned(), encoded)]),
1707                    }),
1708                    ..Default::default()
1709                }),
1710                ..Default::default()
1711            },
1712            &converter,
1713        )
1714        .await
1715        .unwrap();
1716        let memo = description.memo();
1717
1718        assert_eq!(
1719            memo.get::<String>("memo-key").unwrap(),
1720            Some("memo-value".to_owned())
1721        );
1722    }
1723
1724    #[tokio::test]
1725    async fn workflow_description_accessors_expose_decoded_fields() {
1726        let converter = DataConverter::default();
1727        let memo_payload = converter
1728            .to_payload(
1729                &SerializationContextData::Workflow(WorkflowSerializationContext::new()),
1730                &"memo-value",
1731            )
1732            .await
1733            .unwrap();
1734        let search_attr_payload = converter
1735            .to_payload(
1736                &SerializationContextData::Workflow(WorkflowSerializationContext::new()),
1737                &"search-value",
1738            )
1739            .await
1740            .unwrap();
1741        let summary_payload = converter
1742            .to_payload(
1743                &SerializationContextData::Workflow(WorkflowSerializationContext::new()),
1744                &"workflow summary",
1745            )
1746            .await
1747            .unwrap();
1748        let details_payload = converter
1749            .to_payload(
1750                &SerializationContextData::Workflow(WorkflowSerializationContext::new()),
1751                &"workflow details",
1752            )
1753            .await
1754            .unwrap();
1755        let description = WorkflowExecutionDescription::new(
1756            DescribeWorkflowExecutionResponse {
1757                workflow_execution_info: Some(workflow::WorkflowExecutionInfo {
1758                    execution: Some(ProtoWorkflowExecution {
1759                        workflow_id: "wf-id".to_string(),
1760                        run_id: "run-id".to_string(),
1761                    }),
1762                    r#type: Some(
1763                        temporalio_common::protos::temporal::api::common::v1::WorkflowType {
1764                            name: "wf-type".to_string(),
1765                        },
1766                    ),
1767                    status: ProtoWorkflowExecutionStatus::Completed as i32,
1768                    task_queue: "task-queue".to_string(),
1769                    history_length: 42,
1770                    memo: Some(Memo {
1771                        fields: HashMap::from([("memo-key".to_string(), memo_payload.clone())]),
1772                    }),
1773                    parent_execution: Some(ProtoWorkflowExecution {
1774                        workflow_id: "parent-id".to_string(),
1775                        run_id: "parent-run-id".to_string(),
1776                    }),
1777                    search_attributes: Some(SearchAttributes {
1778                        indexed_fields: HashMap::from([(
1779                            "CustomKeywordField".to_string(),
1780                            search_attr_payload.clone(),
1781                        )]),
1782                    }),
1783                    ..Default::default()
1784                }),
1785                execution_config: Some(WorkflowExecutionConfig {
1786                    user_metadata: Some(UserMetadata {
1787                        summary: Some(summary_payload),
1788                        details: Some(details_payload),
1789                    }),
1790                    ..Default::default()
1791                }),
1792                ..Default::default()
1793            },
1794            &converter,
1795        )
1796        .await
1797        .unwrap();
1798
1799        assert_eq!(description.id(), "wf-id");
1800        assert_eq!(description.run_id(), "run-id");
1801        assert_eq!(description.workflow_type(), "wf-type");
1802        assert_eq!(description.status(), WorkflowExecutionStatus::Completed);
1803        let mut unknown_status_description = description.clone();
1804        unknown_status_description
1805            .raw_description
1806            .workflow_execution_info
1807            .as_mut()
1808            .unwrap()
1809            .status = 123_456;
1810        assert_eq!(
1811            unknown_status_description.status(),
1812            WorkflowExecutionStatus::Unknown
1813        );
1814        assert_eq!(description.task_queue(), "task-queue");
1815        assert_eq!(description.history_length(), 42);
1816        assert_eq!(description.parent_id(), Some("parent-id"));
1817        assert_eq!(description.parent_run_id(), Some("parent-run-id"));
1818        let memo = description.memo();
1819        assert_eq!(memo.raw_value("memo-key"), Some(&memo_payload));
1820        assert_eq!(
1821            memo.get::<String>("memo-key").unwrap(),
1822            Some("memo-value".to_owned())
1823        );
1824        let search_attributes = description.search_attributes();
1825        assert_eq!(
1826            search_attributes.raw_payload("CustomKeywordField"),
1827            Some(&search_attr_payload)
1828        );
1829        assert_eq!(description.static_summary(), Some("workflow summary"));
1830        assert_eq!(description.static_details(), Some("workflow details"));
1831    }
1832
1833    #[tokio::test]
1834    async fn workflow_description_rejects_negative_history_length() {
1835        let err = WorkflowExecutionDescription::new(
1836            DescribeWorkflowExecutionResponse {
1837                workflow_execution_info: Some(workflow::WorkflowExecutionInfo {
1838                    history_length: -1,
1839                    ..Default::default()
1840                }),
1841                ..Default::default()
1842            },
1843            &DataConverter::default(),
1844        )
1845        .await
1846        .unwrap_err();
1847
1848        assert_eq!(
1849            err.to_string(),
1850            "Encoding error: workflow history_length must be non-negative, got -1"
1851        );
1852    }
1853}