Skip to main content

temporalio_workflow/
workflow_context.rs

1mod options;
2mod view;
3
4pub use options::{
5    ActivityCancellationType, ActivityOptions, ChildWorkflowCancellationType, ChildWorkflowOptions,
6    ContinueAsNewOptions, ContinueAsNewVersioningBehavior, LocalActivityOptions,
7    NexusOperationCancellationType, NexusOperationOptions, ParentClosePolicy,
8    SignalWorkflowOptions, TimerOptions, VersioningIntent, WaitConditionOptions,
9    WorkflowIdReusePolicy,
10};
11pub use temporalio_common_wasm::protos::coresdk::child_workflow::StartChildWorkflowExecutionFailedCause;
12pub use view::{NamespacedWorkflowInfo, WorkflowContextView};
13
14use crate::{
15    MemoValue, WorkflowCancellationError, WorkflowCancellationToken,
16    runtime::{
17        SdkGuardedFuture, SdkWakeGuard,
18        entry::WorkflowImplementation,
19        host::WorkflowHost,
20        mark_intercepted_future_activation,
21        model::{
22            CancelExternalWfResult, CancellableID, NexusStartResult, SignalExternalWfResult,
23            TimerResult, UnblockEvent, Unblockable, WorkflowTermination,
24        },
25        types::WorkflowInit,
26    },
27    workflow_interceptors::{
28        CancelExternalWorkflowInput, CancellableWorkflowOutboundFuture,
29        ChildWorkflowOutboundResult, ContinueAsNewInput, ScheduleActivityInput,
30        ScheduleLocalActivityInput, SignalWorkflowInput, SignalWorkflowResult,
31        SignalWorkflowTarget, StartChildWorkflowInput, StartChildWorkflowResult,
32        StartNexusOperationInput, StartTimerInput, WorkflowCancellationHandle, WorkflowInterceptor,
33        WorkflowInterceptorConstructor, WorkflowInterceptorContext, WorkflowNext,
34        WorkflowOutboundFuture, WorkflowOutboundValue, call_cancel_external_workflow,
35        call_continue_as_new, call_schedule_activity, call_schedule_local_activity,
36        call_signal_workflow, call_start_child_workflow, call_start_nexus_operation,
37        call_start_timer,
38    },
39};
40use futures_channel::oneshot;
41use futures_util::{
42    FutureExt,
43    future::{FusedFuture, Shared},
44    task::Context,
45};
46use rand::SeedableRng;
47use rand_pcg::Pcg64Mcg;
48use std::{
49    cell::{Cell, RefCell},
50    collections::{HashMap, HashSet},
51    future::{self, Future},
52    marker::PhantomData,
53    pin::Pin,
54    rc::Rc,
55    sync::{
56        Arc,
57        atomic::{AtomicBool, Ordering},
58    },
59    task::{Poll, Waker},
60    time::SystemTime,
61};
62use temporalio_common_wasm::{
63    ActivityDefinition, Memo, SignalDefinition, WorkflowDefinition,
64    data_converters::{
65        ActivityExecutionDecodeHint, ChildWorkflowExecutionDecodeHint,
66        ChildWorkflowStartDecodeHint, DataConverter, GenericPayloadConverter,
67        PayloadConversionError, PayloadConverter, SerializationContext, SerializationContextData,
68        TemporalDeserializable, WorkflowSignalDecodeHint,
69    },
70    error::{
71        ActivityExecutionError, ChildWorkflowExecutionError, ChildWorkflowStartError,
72        WorkflowSignalError,
73    },
74    protos::{
75        coresdk::{
76            activity_result::{ActivityResolution, Cancellation, activity_resolution},
77            child_workflow::{ChildWorkflowResult, child_workflow_result},
78            common::NamespacedWorkflowExecution,
79            nexus::NexusOperationResult,
80            workflow_activation::{
81                InitializeWorkflow, WorkflowActivation as CoreWorkflowActivation,
82                resolve_child_workflow_execution_start::Status as ChildWorkflowStartStatus,
83                workflow_activation_job::Variant as ActivationVariant,
84            },
85            workflow_commands::{
86                CancelChildWorkflowExecution, CancelSignalWorkflow, CancelTimer,
87                ModifyWorkflowProperties, RequestCancelActivity,
88                RequestCancelExternalWorkflowExecution, RequestCancelLocalActivity,
89                RequestCancelNexusOperation, SetPatchMarker, UpsertWorkflowSearchAttributes,
90                signal_external_workflow_execution, workflow_command,
91            },
92        },
93        temporal::api::{
94            common::v1::{Memo as ProtoMemo, Payload, SearchAttributes as ProtoSearchAttributes},
95            failure::v1::{CanceledFailureInfo, Failure, failure::FailureInfo},
96        },
97        utilities::TryIntoOrNone,
98    },
99    search_attributes::{SearchAttributeUpdate, SearchAttributes},
100    worker::WorkerDeploymentVersion,
101};
102use uuid::Builder;
103
104mod private {
105    use rand::distr::{Distribution, StandardUniform};
106    use rand_pcg::Pcg64Mcg;
107
108    pub trait Sealed: Sized {
109        fn sample(rng: &mut Pcg64Mcg) -> Self;
110    }
111
112    pub(super) fn sample<T>(rng: &mut Pcg64Mcg) -> T
113    where
114        StandardUniform: Distribution<T>,
115    {
116        StandardUniform.sample(rng)
117    }
118}
119
120/// A numeric value that can be generated by [`WorkflowContext::random`].
121///
122/// This trait is sealed and implemented for `u8`, `u16`, `u32`, `u64`, `u128`, `i8`, `i16`,
123/// `i32`, `i64`, `i128`, `f32`, and `f64`.
124pub trait WorkflowRandomValue: private::Sealed + Sized {}
125
126macro_rules! impl_random_value {
127    ($($ty:ty),* $(,)?) => {
128        $(
129            impl private::Sealed for $ty {
130                fn sample(rng: &mut Pcg64Mcg) -> Self {
131                    private::sample(rng)
132                }
133            }
134
135            impl WorkflowRandomValue for $ty {}
136        )*
137    };
138}
139
140impl_random_value!(u8, u16, u32, u64, u128, i8, i16, i32, i64, i128, f32, f64);
141
142/// Non-generic base context containing all workflow execution infrastructure.
143///
144/// This is used internally by futures and commands that don't need typed workflow state.
145#[derive(Clone)]
146pub struct BaseWorkflowContext {
147    inner: Rc<WorkflowContextInner>,
148}
149
150/// Input provided to a worker's patch activation callback.
151#[derive(Clone, Debug)]
152#[non_exhaustive]
153pub struct PatchActivationInput {
154    /// Information about the workflow execution calling [`SyncWorkflowContext::patched`].
155    pub workflow_info: WorkflowContextView,
156    /// Patch ID passed to [`SyncWorkflowContext::patched`].
157    pub patch_id: String,
158}
159
160/// Callback that decides whether a newly encountered patch should be activated.
161pub type PatchActivationCallback =
162    Arc<dyn Fn(PatchActivationInput) -> bool + Send + Sync + 'static>;
163
164/// Invokes a patch activation callback with a workflow information snapshot.
165#[doc(hidden)]
166pub struct PatchActivationCaller {
167    callback: PatchActivationCallback,
168    workflow_info: WorkflowContextView,
169}
170
171impl PatchActivationCaller {
172    /// Creates a caller from workflow initialization data.
173    pub fn new(
174        callback: PatchActivationCallback,
175        namespace: String,
176        task_queue: String,
177        run_id: String,
178        init: InitializeWorkflow,
179        payload_converter: PayloadConverter,
180    ) -> Self {
181        Self {
182            callback,
183            workflow_info: WorkflowContextView::new(
184                namespace,
185                task_queue,
186                run_id,
187                init,
188                payload_converter,
189            ),
190        }
191    }
192
193    /// Invokes the callback for a patch ID.
194    pub fn call(&self, patch_id: String) -> bool {
195        (self.callback)(PatchActivationInput {
196            workflow_info: self.workflow_info.clone(),
197            patch_id,
198        })
199    }
200}
201
202pub(crate) struct WorkflowPollWakerGuard<'a> {
203    current_waker: &'a RefCell<Option<Waker>>,
204    previous: Option<Waker>,
205}
206
207impl Drop for WorkflowPollWakerGuard<'_> {
208    fn drop(&mut self) {
209        self.current_waker.replace(self.previous.take());
210    }
211}
212
213fn outbound_type_error(
214    value: &str,
215) -> temporalio_common_wasm::data_converters::PayloadConversionError {
216    temporalio_common_wasm::data_converters::PayloadConversionError::EncodingError(Box::new(
217        std::io::Error::new(
218            std::io::ErrorKind::InvalidData,
219            format!("workflow interceptor returned the wrong concrete {value} type"),
220        ),
221    ))
222}
223
224impl BaseWorkflowContext {
225    pub(crate) fn apply_activation_context(
226        &self,
227        activation: &CoreWorkflowActivation,
228        is_replaying_history_events: bool,
229    ) {
230        let mut shared = self.inner.shared.borrow_mut();
231        shared.activation = activation.clone();
232        shared.is_replaying_history_events = is_replaying_history_events;
233        if let Some(seed) = activation.jobs.iter().find_map(|job| match &job.variant {
234            Some(ActivationVariant::UpdateRandomSeed(attrs)) => Some(attrs.randomness_seed),
235            _ => None,
236        }) {
237            shared.random = Pcg64Mcg::seed_from_u64(seed);
238        }
239    }
240
241    fn random<T>(&self) -> T
242    where
243        T: WorkflowRandomValue,
244    {
245        let random = &mut self.inner.shared.borrow_mut().random;
246        <T as private::Sealed>::sample(random)
247    }
248
249    fn uuid4(&self) -> String {
250        Builder::from_random_bytes(self.random::<u128>().to_be_bytes())
251            .into_uuid()
252            .hyphenated()
253            .to_string()
254    }
255
256    /// Returns the [`DataConverter`] associated with this workflow's worker.
257    pub fn data_converter(&self) -> &DataConverter {
258        &self.inner.data_converter
259    }
260
261    /// Return the workflow's unique identifier.
262    pub fn workflow_id(&self) -> &str {
263        &self.inner.initial_information.workflow_id
264    }
265
266    /// Return the run id of this workflow execution.
267    pub fn run_id(&self) -> &str {
268        &self.inner.run_id
269    }
270
271    /// Return the namespace the workflow is executing in.
272    pub fn namespace(&self) -> &str {
273        &self.inner.namespace
274    }
275
276    /// Return the task queue the workflow is executing in.
277    pub fn task_queue(&self) -> &str {
278        &self.inner.task_queue
279    }
280
281    /// Return the workflow type name.
282    pub fn workflow_type(&self) -> &str {
283        &self.inner.initial_information.workflow_type
284    }
285
286    pub(crate) fn initial_headers(&self) -> HashMap<String, Payload> {
287        self.inner.initial_information.headers.clone()
288    }
289
290    /// Return the current time according to the workflow.
291    pub fn workflow_time(&self) -> Option<SystemTime> {
292        self.inner
293            .shared
294            .borrow()
295            .activation
296            .timestamp
297            .try_into_or_none()
298    }
299
300    /// Return the length of history so far at this point in the workflow.
301    pub fn history_length(&self) -> u32 {
302        self.inner.shared.borrow().activation.history_length
303    }
304
305    /// Return current values for workflow search attributes.
306    pub fn search_attributes(&self) -> SearchAttributes {
307        SearchAttributes::from_proto(&self.inner.shared.borrow().search_attributes)
308    }
309
310    /// Returns true if the workflow is replaying (including during queries and update validators), false otherwise.
311    pub fn is_replaying(&self) -> bool {
312        self.inner.shared.borrow().activation.is_replaying
313    }
314
315    /// Return true if the workflow is replaying history events (excluding queries and update validators), false otherwise.
316    pub fn is_replaying_history_events(&self) -> bool {
317        self.inner.shared.borrow().is_replaying_history_events
318    }
319
320    /// Returns the payload converter used by the worker running this workflow.
321    pub fn payload_converter(&self) -> &PayloadConverter {
322        self.inner.data_converter.payload_converter()
323    }
324
325    pub(crate) fn construction_waker(&self) -> Waker {
326        self.inner
327            .current_waker
328            .borrow()
329            .clone()
330            .unwrap_or_else(|| Waker::noop().clone())
331    }
332
333    pub(crate) fn enter_runtime_poll<'a>(&'a self, waker: &Waker) -> WorkflowPollWakerGuard<'a> {
334        WorkflowPollWakerGuard {
335            previous: self.inner.current_waker.replace(Some(waker.clone())),
336            current_waker: &self.inner.current_waker,
337        }
338    }
339
340    pub(crate) fn notify_patch(&self, patch_id: String) {
341        self.inner
342            .shared
343            .borrow_mut()
344            .notified_patches
345            .insert(patch_id);
346    }
347
348    fn prepare_outbound_future<T>(
349        &self,
350        mut future: WorkflowOutboundFuture<T>,
351    ) -> WorkflowOutboundFuture<T> {
352        let waker = self.construction_waker();
353        let mut cx = Context::from_waker(&waker);
354        future.poll_for_construction(&mut cx);
355        future
356    }
357
358    fn prepare_cancellable_outbound_future<T>(
359        &self,
360        mut future: CancellableWorkflowOutboundFuture<T>,
361    ) -> CancellableWorkflowOutboundFuture<T> {
362        let waker = self.construction_waker();
363        let mut cx = Context::from_waker(&waker);
364        future.poll_for_construction(&mut cx);
365        future
366    }
367
368    /// Create a read-only view of this context.
369    pub(crate) fn view(&self) -> WorkflowContextView {
370        let shared = self.inner.shared.borrow();
371        let mut initial_information = self.inner.initial_information.clone();
372        if initial_information.memo.is_some() || !shared.memo.fields.is_empty() {
373            initial_information.memo = Some(shared.memo.clone());
374        }
375        WorkflowContextView::new(
376            self.inner.namespace.clone(),
377            self.inner.task_queue.clone(),
378            self.inner.run_id.clone(),
379            initial_information,
380            self.inner.data_converter.payload_converter().clone(),
381        )
382    }
383}
384
385#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
386enum PendingCommandId {
387    Timer(u32),
388    Activity(u32),
389    ChildWorkflowStart(u32),
390    ChildWorkflowComplete(u32),
391    SignalExternal(u32),
392    CancelExternal(u32),
393    NexusOpStart(u32),
394    NexusOpComplete(u32),
395}
396
397impl PendingCommandId {
398    fn from_unblock_event(event: &UnblockEvent) -> Self {
399        match event {
400            UnblockEvent::Timer(seq, _) => Self::Timer(*seq),
401            UnblockEvent::Activity(seq, _) => Self::Activity(*seq),
402            UnblockEvent::WorkflowStart(seq, _) => Self::ChildWorkflowStart(*seq),
403            UnblockEvent::WorkflowComplete(seq, _) => Self::ChildWorkflowComplete(*seq),
404            UnblockEvent::SignalExternal(seq, _) => Self::SignalExternal(*seq),
405            UnblockEvent::CancelExternal(seq, _) => Self::CancelExternal(*seq),
406            UnblockEvent::NexusOperationStart(seq, _) => Self::NexusOpStart(*seq),
407            UnblockEvent::NexusOperationComplete(seq, _) => Self::NexusOpComplete(*seq),
408        }
409    }
410}
411
412struct WorkflowRuntimeState {
413    host: Rc<dyn WorkflowHost>,
414    pending_unblocks: RefCell<HashMap<PendingCommandId, oneshot::Sender<UnblockEvent>>>,
415    forced_wft_failure: RefCell<Option<Box<dyn std::error::Error + Send + Sync>>>,
416    progress_made: Cell<bool>,
417}
418
419impl WorkflowRuntimeState {
420    fn new(host: Rc<dyn WorkflowHost>) -> Self {
421        Self {
422            host,
423            pending_unblocks: RefCell::new(HashMap::new()),
424            forced_wft_failure: RefCell::new(None),
425            progress_made: Cell::new(false),
426        }
427    }
428
429    fn register_unblocker(&self, id: PendingCommandId, unblocker: oneshot::Sender<UnblockEvent>) {
430        self.pending_unblocks.borrow_mut().insert(id, unblocker);
431    }
432
433    fn unblock(&self, event: UnblockEvent) -> Result<(), anyhow::Error> {
434        let id = PendingCommandId::from_unblock_event(&event);
435        let unblocker = self
436            .pending_unblocks
437            .borrow_mut()
438            .remove(&id)
439            .ok_or_else(|| anyhow::anyhow!("Command {id:?} not found to unblock"))?;
440        self.progress_made.set(true);
441        let _guard = SdkWakeGuard::new();
442        let _ = unblocker.send(event);
443        Ok(())
444    }
445
446    fn maybe_unblock(&self, event: UnblockEvent) -> bool {
447        let id = PendingCommandId::from_unblock_event(&event);
448        let Some(unblocker) = self.pending_unblocks.borrow_mut().remove(&id) else {
449            return false;
450        };
451        self.progress_made.set(true);
452        let _guard = SdkWakeGuard::new();
453        let _ = unblocker.send(event);
454        true
455    }
456
457    fn set_forced_wft_failure(&self, err: Box<dyn std::error::Error + Send + Sync>) {
458        *self.forced_wft_failure.borrow_mut() = Some(err);
459        self.progress_made.set(true);
460    }
461
462    fn take_forced_wft_failure(&self) -> Option<Box<dyn std::error::Error + Send + Sync>> {
463        self.forced_wft_failure.borrow_mut().take()
464    }
465
466    fn mark_progress(&self) {
467        self.progress_made.set(true);
468    }
469
470    fn take_progress(&self) -> bool {
471        self.progress_made.replace(false)
472    }
473}
474
475struct WorkflowContextInner {
476    namespace: String,
477    task_queue: String,
478    run_id: String,
479    initial_information: InitializeWorkflow,
480    runtime: WorkflowRuntimeState,
481    cancellation_token: WorkflowCancellationToken,
482    cancelled_operations: RefCell<HashSet<CancellableSeqNum>>,
483    shared: RefCell<WorkflowContextSharedData>,
484    seq_nums: RefCell<WfCtxProtectedDat>,
485    data_converter: DataConverter,
486    patch_activation_callback: Option<PatchActivationCallback>,
487    state_mutated: Cell<bool>,
488    current_waker: RefCell<Option<Waker>>,
489    workflow_interceptors: Rc<[Arc<dyn WorkflowInterceptor>]>,
490}
491
492/// Identical to [`CancellableID`], but only containing command type and seq number, omitting any reason.
493#[derive(Eq, Hash, PartialEq)]
494enum CancellableSeqNum {
495    Timer(u32),
496    Activity(u32),
497    LocalActivity(u32),
498    ChildWorkflow(u32),
499    SignalExternalWorkflow(u32),
500    NexusOp(u32),
501}
502
503impl From<&CancellableID> for CancellableSeqNum {
504    fn from(value: &CancellableID) -> Self {
505        match value {
506            CancellableID::Timer(seq) => Self::Timer(*seq),
507            CancellableID::Activity(seq) => Self::Activity(*seq),
508            CancellableID::LocalActivity(seq) => Self::LocalActivity(*seq),
509            CancellableID::ChildWorkflow { seqnum, .. } => Self::ChildWorkflow(*seqnum),
510            CancellableID::SignalExternalWorkflow(seq) => Self::SignalExternalWorkflow(*seq),
511            CancellableID::NexusOp(seq) => Self::NexusOp(*seq),
512        }
513    }
514}
515
516/// Context provided to synchronous signal and update handlers.
517///
518/// This type provides all workflow context capabilities except `state()`, `state_mut()`,
519/// and `wait_condition()`. Those methods are not applicable in sync handler contexts.
520///
521/// Sync handlers receive `&mut self` directly, so they can reference and mutate workflow state without
522/// needing `state()`/`state_mut()`.
523pub struct SyncWorkflowContext<W> {
524    base: BaseWorkflowContext,
525    /// Headers from the current handler invocation (signal, update, etc.)
526    headers: Rc<HashMap<String, Payload>>,
527    _phantom: PhantomData<W>,
528}
529
530impl<W> Clone for SyncWorkflowContext<W> {
531    fn clone(&self) -> Self {
532        Self {
533            base: self.base.clone(),
534            headers: self.headers.clone(),
535            _phantom: PhantomData,
536        }
537    }
538}
539
540/// Used within workflows to issue commands, get info, etc.
541///
542/// The type parameter `W` represents the workflow type. This enables type-safe
543/// access to workflow state via `state_mut()` for mutations.
544pub struct WorkflowContext<W> {
545    sync: SyncWorkflowContext<W>,
546    /// The workflow instance
547    workflow_state: Rc<RefCell<W>>,
548    /// Wakers registered by `wait_condition` futures. Drained and woken on
549    /// every `state_mut` call so that waker-based combinators (e.g.
550    /// `FuturesOrdered`) re-poll the condition after state changes.
551    condition_wakers: Rc<RefCell<Vec<Waker>>>,
552}
553
554impl<W> Clone for WorkflowContext<W> {
555    fn clone(&self) -> Self {
556        Self {
557            sync: self.sync.clone(),
558            workflow_state: self.workflow_state.clone(),
559            condition_wakers: self.condition_wakers.clone(),
560        }
561    }
562}
563
564impl BaseWorkflowContext {
565    /// Construct a base context and its interceptors from initial workflow information.
566    #[doc(hidden)]
567    pub fn from_raw(
568        init: WorkflowInit,
569        data_converter: DataConverter,
570        host: Rc<dyn WorkflowHost>,
571        patch_activation_callback: Option<PatchActivationCallback>,
572        workflow_interceptor_constructors: Vec<WorkflowInterceptorConstructor>,
573    ) -> Self {
574        let WorkflowInit {
575            namespace,
576            task_queue,
577            run_id,
578            initialize_workflow,
579        } = init;
580        let view = WorkflowContextView::new(
581            namespace,
582            task_queue,
583            run_id,
584            initialize_workflow,
585            data_converter.payload_converter().clone(),
586        );
587        let workflow_interceptors = workflow_interceptor_constructors
588            .into_iter()
589            .map(|constructor| constructor.construct(&view))
590            .collect::<Vec<_>>()
591            .into();
592        let (namespace, task_queue, run_id, init_workflow_job) = view.into_parts();
593        Self {
594            inner: Rc::new(WorkflowContextInner {
595                namespace,
596                task_queue,
597                run_id,
598                shared: RefCell::new(WorkflowContextSharedData {
599                    random: Pcg64Mcg::seed_from_u64(init_workflow_job.randomness_seed),
600                    memo: init_workflow_job.memo.clone().unwrap_or_default(),
601                    search_attributes: init_workflow_job
602                        .search_attributes
603                        .clone()
604                        .unwrap_or_default(),
605                    is_replaying_history_events: false,
606                    changes: Default::default(),
607                    activation: Default::default(),
608                    current_details: Default::default(),
609                    notified_patches: Default::default(),
610                }),
611                initial_information: init_workflow_job,
612                runtime: WorkflowRuntimeState::new(host),
613                cancellation_token: WorkflowCancellationToken::new(),
614                cancelled_operations: Default::default(),
615                seq_nums: RefCell::new(WfCtxProtectedDat {
616                    next_timer_sequence_number: 1,
617                    next_activity_sequence_number: 1,
618                    next_child_workflow_sequence_number: 1,
619                    next_cancel_external_wf_sequence_number: 1,
620                    next_signal_external_wf_sequence_number: 1,
621                    next_nexus_op_sequence_number: 1,
622                }),
623                data_converter,
624                patch_activation_callback,
625                state_mutated: Cell::new(false),
626                current_waker: RefCell::new(None),
627                workflow_interceptors,
628            }),
629        }
630    }
631
632    pub(crate) fn workflow_interceptors(&self) -> Rc<[Arc<dyn WorkflowInterceptor>]> {
633        self.inner.workflow_interceptors.clone()
634    }
635
636    /// Check and clear the state_mutated flag. Returns `true` if `state_mut`
637    /// was called since the last time this method was invoked.
638    pub(crate) fn take_state_mutated(&self) -> bool {
639        self.inner.state_mutated.replace(false)
640    }
641
642    /// Mark that workflow state has been mutated.
643    pub(crate) fn set_state_mutated(&self) {
644        self.inner.state_mutated.set(true);
645    }
646
647    pub(crate) fn take_runtime_progress(&self) -> bool {
648        self.inner.runtime.take_progress()
649    }
650
651    pub(crate) fn take_forced_wft_failure(
652        &self,
653    ) -> Option<Box<dyn std::error::Error + Send + Sync>> {
654        self.inner.runtime.take_forced_wft_failure()
655    }
656
657    pub(crate) fn notify_cancel(&self, reason: String) {
658        if reason.is_empty() {
659            self.inner.cancellation_token.cancel();
660        } else {
661            self.inner.cancellation_token.cancel_with_reason(reason);
662        }
663        self.inner.runtime.mark_progress();
664    }
665
666    /// Return the workflow's root cancellation token.
667    pub fn cancellation_token(&self) -> WorkflowCancellationToken {
668        self.inner.cancellation_token.clone()
669    }
670
671    pub(crate) fn unblock(&self, event: UnblockEvent) -> Result<(), anyhow::Error> {
672        self.inner.runtime.unblock(event)
673    }
674
675    /// Cancel any cancellable operation by ID
676    fn cancel(&self, cancellable_id: CancellableID) {
677        if !self
678            .inner
679            .cancelled_operations
680            .borrow_mut()
681            .insert((&cancellable_id).into())
682        {
683            return;
684        }
685        match cancellable_id {
686            CancellableID::Timer(seq) => {
687                if self
688                    .inner
689                    .runtime
690                    .maybe_unblock(UnblockEvent::Timer(seq, TimerResult::Cancelled))
691                {
692                    self.inner.runtime.host.push_command(
693                        workflow_command::Variant::CancelTimer(CancelTimer { seq }).into(),
694                    );
695                }
696            }
697            CancellableID::Activity(seq) => {
698                self.inner.runtime.host.push_command(
699                    workflow_command::Variant::RequestCancelActivity(RequestCancelActivity { seq })
700                        .into(),
701                );
702            }
703            CancellableID::LocalActivity(seq) => {
704                self.inner.runtime.host.push_command(
705                    workflow_command::Variant::RequestCancelLocalActivity(
706                        RequestCancelLocalActivity { seq },
707                    )
708                    .into(),
709                );
710            }
711            CancellableID::ChildWorkflow { seqnum, reason } => {
712                self.inner.runtime.host.push_command(
713                    workflow_command::Variant::CancelChildWorkflowExecution(
714                        CancelChildWorkflowExecution {
715                            child_workflow_seq: seqnum,
716                            reason,
717                        },
718                    )
719                    .into(),
720                );
721            }
722            CancellableID::SignalExternalWorkflow(seq) => {
723                self.inner.runtime.host.push_command(
724                    workflow_command::Variant::CancelSignalWorkflow(CancelSignalWorkflow { seq })
725                        .into(),
726                );
727            }
728            CancellableID::NexusOp(seq) => {
729                self.inner.runtime.host.push_command(
730                    workflow_command::Variant::RequestCancelNexusOperation(
731                        RequestCancelNexusOperation { seq },
732                    )
733                    .into(),
734                );
735            }
736        }
737    }
738
739    fn cancellation_handle(&self, cancellable_id: CancellableID) -> WorkflowCancellationHandle {
740        let base_ctx = self.clone();
741        WorkflowCancellationHandle::new(move |reason| {
742            let id = reason.map_or_else(
743                || cancellable_id.clone(),
744                |reason| cancellable_id.clone().with_reason(reason),
745            );
746            base_ctx.cancel(id);
747        })
748    }
749
750    /// Return the current value of current_details.
751    pub fn current_details(&self) -> String {
752        self.inner.shared.borrow().current_details.clone()
753    }
754
755    /// Request to create a timer
756    pub fn timer<T: Into<TimerOptions>>(
757        &self,
758        opts: T,
759    ) -> impl CancellableFuture<Output = TimerResult> + use<T> {
760        let input = StartTimerInput::new(opts.into());
761        let base_ctx = self.clone();
762        let next = WorkflowNext::new(move |input: StartTimerInput| {
763            let mut opts = input.into_options();
764            let cancellation_token = opts
765                .cancellation_token
766                .take()
767                .unwrap_or_else(|| base_ctx.cancellation_token());
768            let seq = base_ctx.inner.seq_nums.borrow_mut().next_timer_seq();
769            let (cmd, unblocker) =
770                CancellableWFCommandFut::new(CancellableID::Timer(seq), base_ctx.clone());
771            base_ctx
772                .inner
773                .runtime
774                .register_unblocker(PendingCommandId::Timer(seq), unblocker);
775            base_ctx
776                .inner
777                .runtime
778                .host
779                .push_command(opts.into_command(seq));
780            CancellableWorkflowOutboundFuture::new(
781                cmd,
782                base_ctx.cancellation_handle(CancellableID::Timer(seq)),
783            )
784            .with_cancellation_token(cancellation_token)
785        });
786        let interceptors = self.inner.workflow_interceptors.clone();
787        let future = call_start_timer(
788            interceptors,
789            WorkflowInterceptorContext::new(self.clone()),
790            input,
791            next,
792        );
793        self.prepare_cancellable_outbound_future(future)
794    }
795
796    /// Request to run an activity
797    #[allow(clippy::result_large_err)]
798    pub fn execute_activity<AD: ActivityDefinition>(
799        &self,
800        activity: AD,
801        input: impl Into<AD::Input>,
802        opts: ActivityOptions,
803    ) -> impl CancellableFuture<Output = Result<AD::Output, ActivityExecutionError>>
804    where
805        AD::Output: TemporalDeserializable,
806    {
807        let input =
808            ScheduleActivityInput::new(activity.name().to_string(), Box::new(input.into()), opts);
809        let base_ctx = self.clone();
810        let next = WorkflowNext::new(move |input: ScheduleActivityInput| {
811            let (activity_type, input, headers, mut opts) = input.into_parts();
812            let input = match input.downcast::<AD::Input>() {
813                Ok(input) => *input,
814                Err(_) => {
815                    return CancellableWorkflowOutboundFuture::new(
816                        async {
817                            Err(ActivityExecutionError::Serialization(outbound_type_error(
818                                "activity input",
819                            )))
820                        },
821                        WorkflowCancellationHandle::noop(),
822                    );
823                }
824            };
825            let payload_converter = base_ctx.inner.data_converter.payload_converter();
826            let ctx = SerializationContext {
827                data: &SerializationContextData::Workflow,
828                converter: payload_converter,
829            };
830            match payload_converter.to_payloads(&ctx, &input) {
831                Ok(payloads) => {
832                    let cancellation_token = opts
833                        .cancellation_token
834                        .take()
835                        .unwrap_or_else(|| base_ctx.cancellation_token());
836                    let seq = base_ctx.inner.seq_nums.borrow_mut().next_activity_seq();
837                    let (cmd, unblocker) = CancellableWFCommandFut::new(
838                        CancellableID::Activity(seq),
839                        base_ctx.clone(),
840                    );
841                    base_ctx
842                        .inner
843                        .runtime
844                        .register_unblocker(PendingCommandId::Activity(seq), unblocker);
845                    if opts.task_queue.is_none() {
846                        opts.task_queue = Some(base_ctx.inner.task_queue.clone());
847                    }
848                    base_ctx.inner.runtime.host.push_command(opts.into_command(
849                        seq,
850                        activity_type,
851                        payloads,
852                        headers,
853                    ));
854                    CancellableWorkflowOutboundFuture::new(
855                        ActivityFut::running(cmd, base_ctx.inner.data_converter.clone()),
856                        base_ctx.cancellation_handle(CancellableID::Activity(seq)),
857                    )
858                    .with_cancellation_token(cancellation_token)
859                }
860                Err(err) => CancellableWorkflowOutboundFuture::new(
861                    ActivityFut::<future::Ready<ActivityResolution>, AD::Output>::eager(err.into()),
862                    WorkflowCancellationHandle::noop(),
863                ),
864            }
865            .map(|result| result.map(|output| Box::new(output) as Box<dyn WorkflowOutboundValue>))
866        });
867        let interceptors = self.inner.workflow_interceptors.clone();
868        let future = call_schedule_activity(
869            interceptors,
870            WorkflowInterceptorContext::new(self.clone()),
871            input,
872            next,
873        )
874        .map(|result| {
875            result.and_then(|output| {
876                output
877                    .downcast::<AD::Output>()
878                    .map(|output| *output)
879                    .map_err(|_| {
880                        ActivityExecutionError::Serialization(outbound_type_error(
881                            "activity output",
882                        ))
883                    })
884            })
885        });
886        self.prepare_cancellable_outbound_future(future)
887    }
888
889    /// Request to run a local activity
890    #[allow(clippy::result_large_err)]
891    pub fn execute_local_activity<AD: ActivityDefinition>(
892        &self,
893        activity: AD,
894        input: impl Into<AD::Input>,
895        opts: LocalActivityOptions,
896    ) -> impl CancellableFuture<Output = Result<AD::Output, ActivityExecutionError>>
897    where
898        AD::Output: TemporalDeserializable,
899    {
900        let input = ScheduleLocalActivityInput::new(
901            activity.name().to_string(),
902            Box::new(input.into()),
903            opts,
904        );
905        let base_ctx = self.clone();
906        let next = WorkflowNext::new(move |input: ScheduleLocalActivityInput| {
907            let (activity_type, input, headers, mut opts) = input.into_parts();
908            let input = match input.downcast::<AD::Input>() {
909                Ok(input) => *input,
910                Err(_) => {
911                    return CancellableWorkflowOutboundFuture::new(
912                        async {
913                            Err(ActivityExecutionError::Serialization(outbound_type_error(
914                                "local activity input",
915                            )))
916                        },
917                        WorkflowCancellationHandle::noop(),
918                    );
919                }
920            };
921            let payload_converter = base_ctx.inner.data_converter.payload_converter();
922            let ctx = SerializationContext {
923                data: &SerializationContextData::Workflow,
924                converter: payload_converter,
925            };
926            match payload_converter.to_payloads(&ctx, &input) {
927                Ok(payloads) => {
928                    let cancellation_token = opts
929                        .cancellation_token
930                        .take()
931                        .unwrap_or_else(|| base_ctx.cancellation_token());
932                    let future = LATimerBackoffFut::new(
933                        activity_type,
934                        payloads,
935                        headers,
936                        opts,
937                        cancellation_token.clone(),
938                        base_ctx.clone(),
939                    );
940                    cancellable_outbound(ActivityFut::running(
941                        future,
942                        base_ctx.inner.data_converter.clone(),
943                    ))
944                    .with_cancellation_token(cancellation_token)
945                }
946                Err(err) => CancellableWorkflowOutboundFuture::new(
947                    ActivityFut::<future::Ready<ActivityResolution>, AD::Output>::eager(err.into()),
948                    WorkflowCancellationHandle::noop(),
949                ),
950            }
951            .map(|result| result.map(|output| Box::new(output) as Box<dyn WorkflowOutboundValue>))
952        });
953        let interceptors = self.inner.workflow_interceptors.clone();
954        let future = call_schedule_local_activity(
955            interceptors,
956            WorkflowInterceptorContext::new(self.clone()),
957            input,
958            next,
959        )
960        .map(|result| {
961            result.and_then(|output| {
962                output
963                    .downcast::<AD::Output>()
964                    .map(|output| *output)
965                    .map_err(|_| {
966                        ActivityExecutionError::Serialization(outbound_type_error(
967                            "local activity output",
968                        ))
969                    })
970            })
971        });
972        self.prepare_cancellable_outbound_future(future)
973    }
974
975    /// Start a child workflow with typed input/output.
976    pub(crate) fn start_child_workflow<WD: WorkflowDefinition + 'static>(
977        &self,
978        workflow: WD,
979        input: impl Into<WD::Input>,
980        opts: ChildWorkflowOptions,
981    ) -> impl CancellableFutureWithReason<
982        Output = Result<StartedChildWorkflow<WD>, ChildWorkflowStartError>,
983    >
984    where
985        WD::Output: TemporalDeserializable,
986    {
987        let input =
988            StartChildWorkflowInput::new(workflow.name().to_string(), Box::new(input.into()), opts);
989        let base_ctx = self.clone();
990        let next = WorkflowNext::new(move |input: StartChildWorkflowInput| {
991            let (workflow_type, input, headers, mut opts) = input.into_parts();
992            let input = match input.downcast::<WD::Input>() {
993                Ok(input) => *input,
994                Err(_) => {
995                    return CancellableWorkflowOutboundFuture::new(
996                        async {
997                            Err(ChildWorkflowStartError::Serialization(outbound_type_error(
998                                "child workflow input",
999                            )))
1000                        },
1001                        WorkflowCancellationHandle::noop(),
1002                    );
1003                }
1004            };
1005            let payload_converter = base_ctx.inner.data_converter.payload_converter();
1006            let ctx = SerializationContext {
1007                data: &SerializationContextData::Workflow,
1008                converter: payload_converter,
1009            };
1010            let payloads = match payload_converter.to_payloads(&ctx, &input) {
1011                Ok(payloads) => payloads,
1012                Err(err) => {
1013                    return CancellableWorkflowOutboundFuture::new(
1014                        ChildWorkflowStartFut::<future::Ready<PendingChildWorkflow<WD>>, WD>::eager(
1015                            err.into(),
1016                        ),
1017                        WorkflowCancellationHandle::noop(),
1018                    );
1019                }
1020            };
1021            let workflow_id = opts
1022                .workflow_id
1023                .take()
1024                .filter(|id| !id.is_empty())
1025                .unwrap_or_else(|| base_ctx.uuid4());
1026            let cancellation_token = opts
1027                .cancellation_token
1028                .take()
1029                .unwrap_or_else(|| base_ctx.cancellation_token());
1030
1031            let child_seq = base_ctx
1032                .inner
1033                .seq_nums
1034                .borrow_mut()
1035                .next_child_workflow_seq();
1036            // Immediately create the command/future for the result, otherwise if the user does
1037            // not await the result until *after* we receive an activation for it, there will be nothing
1038            // to match when unblocking.
1039            let (result_cmd, unblocker) = CancellableWFCommandFut::new(
1040                CancellableID::ChildWorkflow {
1041                    seqnum: child_seq,
1042                    reason: String::new(),
1043                },
1044                base_ctx.clone(),
1045            );
1046            base_ctx.inner.runtime.register_unblocker(
1047                PendingCommandId::ChildWorkflowComplete(child_seq),
1048                unblocker,
1049            );
1050            base_ctx.inner.runtime.host.push_command(opts.into_command(
1051                child_seq,
1052                workflow_type,
1053                payloads,
1054                headers,
1055                workflow_id.clone(),
1056            ));
1057
1058            let result_future =
1059                cancellable_outbound_with_reason(ChildWorkflowFut::<_, WD::Output>::Running {
1060                    inner: result_cmd,
1061                    data_converter: base_ctx.inner.data_converter.clone(),
1062                    _phantom: PhantomData,
1063                })
1064                .map(|result| {
1065                    result.map(|output| Box::new(output) as Box<dyn WorkflowOutboundValue>)
1066                })
1067                .with_cancellation_token(cancellation_token);
1068
1069            let common = ChildWfCommon {
1070                workflow_id: workflow_id.clone(),
1071                child_seq,
1072                result_future,
1073                base_ctx: base_ctx.clone(),
1074            };
1075
1076            let (cmd, unblocker) =
1077                CancellableWFCommandFut::<PendingChildWorkflow<WD>, ChildWfCommon>::new_with_dat(
1078                    CancellableID::ChildWorkflow {
1079                        seqnum: child_seq,
1080                        reason: String::new(),
1081                    },
1082                    common,
1083                    base_ctx.clone(),
1084                );
1085            base_ctx
1086                .inner
1087                .runtime
1088                .register_unblocker(PendingCommandId::ChildWorkflowStart(child_seq), unblocker);
1089
1090            cancellable_outbound_with_reason(ChildWorkflowStartFut::Running(cmd))
1091        });
1092        let interceptors = self.inner.workflow_interceptors.clone();
1093        let future = call_start_child_workflow(
1094            interceptors,
1095            WorkflowInterceptorContext::new(self.clone()),
1096            input,
1097            next,
1098        )
1099        .map(|result| result.map(StartChildWorkflowOutput::into_started));
1100        self.prepare_cancellable_outbound_future(future)
1101    }
1102
1103    /// Request to run a local activity with no implementation of timer-backoff based retrying.
1104    fn local_activity_no_timer_retry(
1105        self,
1106        activity_type: String,
1107        arguments: Vec<Payload>,
1108        headers: HashMap<String, Payload>,
1109        opts: LocalActivityOptions,
1110    ) -> impl CancellableFuture<Output = ActivityResolution> {
1111        let seq = self.inner.seq_nums.borrow_mut().next_activity_seq();
1112        let (cmd, unblocker) =
1113            CancellableWFCommandFut::new(CancellableID::LocalActivity(seq), self.clone());
1114        self.inner
1115            .runtime
1116            .register_unblocker(PendingCommandId::Activity(seq), unblocker);
1117        self.inner.runtime.host.push_command(opts.into_command(
1118            seq,
1119            activity_type,
1120            arguments,
1121            headers,
1122        ));
1123        cmd
1124    }
1125
1126    fn signal_workflow<S: SignalDefinition + 'static>(
1127        &self,
1128        target: SignalWorkflowTarget,
1129        signal: S,
1130        input: S::Input,
1131        options: SignalWorkflowOptions,
1132    ) -> CancellableWorkflowOutboundFuture<SignalWorkflowResult> {
1133        let input = SignalWorkflowInput::new(
1134            S::name(&signal).to_string(),
1135            target,
1136            Box::new(input),
1137            options,
1138        );
1139        let base_ctx = self.clone();
1140        let next = WorkflowNext::new(move |input: SignalWorkflowInput| {
1141            let (signal_name, target, input, headers, mut options) = input.into_parts();
1142            let cancellation_token = options
1143                .cancellation_token
1144                .take()
1145                .unwrap_or_else(|| base_ctx.cancellation_token());
1146            let input = match input.downcast::<S::Input>() {
1147                Ok(input) => *input,
1148                Err(_) => {
1149                    return CancellableWorkflowOutboundFuture::new(
1150                        async {
1151                            Err(WorkflowSignalError::Serialization(outbound_type_error(
1152                                "signal input",
1153                            )))
1154                        },
1155                        WorkflowCancellationHandle::noop(),
1156                    );
1157                }
1158            };
1159            let payload_converter = base_ctx.data_converter().payload_converter();
1160            let ctx = SerializationContext {
1161                data: &SerializationContextData::Workflow,
1162                converter: payload_converter,
1163            };
1164            let payloads = match payload_converter.to_payloads(&ctx, &input) {
1165                Ok(payloads) => payloads,
1166                Err(err) => {
1167                    return CancellableWorkflowOutboundFuture::new(
1168                        async move { Err(err.into()) },
1169                        WorkflowCancellationHandle::noop(),
1170                    );
1171                }
1172            };
1173            let target = match target {
1174                SignalWorkflowTarget::Child { workflow_id } => {
1175                    signal_external_workflow_execution::Target::ChildWorkflowId(workflow_id)
1176                }
1177                SignalWorkflowTarget::External {
1178                    namespace,
1179                    workflow_id,
1180                    run_id,
1181                } => signal_external_workflow_execution::Target::WorkflowExecution(
1182                    NamespacedWorkflowExecution {
1183                        namespace,
1184                        workflow_id,
1185                        run_id: run_id.unwrap_or_default(),
1186                    },
1187                ),
1188            };
1189            let seq = base_ctx
1190                .inner
1191                .seq_nums
1192                .borrow_mut()
1193                .next_signal_external_wf_seq();
1194            let (cmd, unblocker) = CancellableWFCommandFut::new(
1195                CancellableID::SignalExternalWorkflow(seq),
1196                base_ctx.clone(),
1197            );
1198            base_ctx
1199                .inner
1200                .runtime
1201                .register_unblocker(PendingCommandId::SignalExternal(seq), unblocker);
1202            base_ctx
1203                .inner
1204                .runtime
1205                .host
1206                .push_command(options.into_command(seq, signal_name, payloads, headers, target));
1207            cancellable_outbound(SignalChildFut::Running {
1208                inner: cmd,
1209                data_converter: base_ctx.data_converter().clone(),
1210            })
1211            .with_cancellation_token(cancellation_token)
1212        });
1213        let interceptors = self.inner.workflow_interceptors.clone();
1214        let future = call_signal_workflow(
1215            interceptors,
1216            WorkflowInterceptorContext::new(self.clone()),
1217            input,
1218            next,
1219        );
1220        self.prepare_cancellable_outbound_future(future)
1221    }
1222
1223    pub(crate) fn external_workflow(
1224        &self,
1225        workflow_id: impl Into<String>,
1226        run_id: Option<String>,
1227    ) -> ExternalWorkflowHandle {
1228        ExternalWorkflowHandle {
1229            workflow_id: workflow_id.into(),
1230            run_id,
1231            namespace: self.inner.namespace.clone(),
1232            base_ctx: self.clone(),
1233        }
1234    }
1235
1236    fn cancel_external_workflow(
1237        &self,
1238        input: CancelExternalWorkflowInput,
1239    ) -> WorkflowOutboundFuture<CancelExternalWfResult> {
1240        let base_ctx = self.clone();
1241        let next = WorkflowNext::new(move |input: CancelExternalWorkflowInput| {
1242            let seq = base_ctx
1243                .inner
1244                .seq_nums
1245                .borrow_mut()
1246                .next_cancel_external_wf_seq();
1247            let (cmd, unblocker) = WFCommandFut::new();
1248            base_ctx
1249                .inner
1250                .runtime
1251                .register_unblocker(PendingCommandId::CancelExternal(seq), unblocker);
1252            base_ctx.inner.runtime.host.push_command(
1253                workflow_command::Variant::RequestCancelExternalWorkflowExecution(
1254                    RequestCancelExternalWorkflowExecution {
1255                        seq,
1256                        workflow_execution: Some(NamespacedWorkflowExecution {
1257                            namespace: base_ctx.inner.namespace.clone(),
1258                            workflow_id: input.workflow_id,
1259                            run_id: input.run_id.unwrap_or_default(),
1260                        }),
1261                        reason: input.reason.unwrap_or_default(),
1262                    },
1263                )
1264                .into(),
1265            );
1266            WorkflowOutboundFuture::new(cmd)
1267        });
1268        let interceptors = self.inner.workflow_interceptors.clone();
1269        let future = call_cancel_external_workflow(
1270            interceptors,
1271            WorkflowInterceptorContext::new(self.clone()),
1272            input,
1273            next,
1274        );
1275        self.prepare_outbound_future(future)
1276    }
1277
1278    pub(crate) fn start_nexus_operation(
1279        &self,
1280        opts: NexusOperationOptions,
1281    ) -> impl CancellableFuture<Output = NexusStartResult> {
1282        let input = StartNexusOperationInput::new(opts);
1283        let base_ctx = self.clone();
1284        let next = WorkflowNext::new(move |input: StartNexusOperationInput| {
1285            let mut opts = input.into_options();
1286            let cancellation_token = opts
1287                .cancellation_token
1288                .take()
1289                .unwrap_or_else(|| base_ctx.cancellation_token());
1290            let seq = base_ctx.inner.seq_nums.borrow_mut().next_nexus_op_seq();
1291            let (result_future, unblocker) =
1292                CancellableWFCommandFut::new(CancellableID::NexusOp(seq), base_ctx.clone());
1293            base_ctx
1294                .inner
1295                .runtime
1296                .register_unblocker(PendingCommandId::NexusOpComplete(seq), unblocker);
1297            base_ctx
1298                .inner
1299                .runtime
1300                .host
1301                .push_command(opts.into_command(seq));
1302            let result_future = CancellableWorkflowOutboundFuture::new(
1303                result_future,
1304                base_ctx.cancellation_handle(CancellableID::NexusOp(seq)),
1305            )
1306            .with_cancellation_token(cancellation_token)
1307            .shared();
1308            let (cmd, unblocker) = CancellableWFCommandFut::new_with_dat(
1309                CancellableID::NexusOp(seq),
1310                NexusUnblockData {
1311                    result_future: result_future.clone(),
1312                    schedule_seq: seq,
1313                    base_ctx: base_ctx.clone(),
1314                },
1315                base_ctx.clone(),
1316            );
1317            base_ctx
1318                .inner
1319                .runtime
1320                .register_unblocker(PendingCommandId::NexusOpStart(seq), unblocker);
1321            cancellable_outbound(cmd)
1322        });
1323        let interceptors = self.inner.workflow_interceptors.clone();
1324        let future = call_start_nexus_operation(
1325            interceptors,
1326            WorkflowInterceptorContext::new(self.clone()),
1327            input,
1328            next,
1329        );
1330        self.prepare_cancellable_outbound_future(future)
1331    }
1332}
1333
1334impl<W> SyncWorkflowContext<W> {
1335    /// Return the workflow's unique identifier
1336    pub fn workflow_id(&self) -> &str {
1337        &self.base.inner.initial_information.workflow_id
1338    }
1339
1340    /// Return the run id of this workflow execution
1341    pub fn run_id(&self) -> &str {
1342        &self.base.inner.run_id
1343    }
1344
1345    /// Return the namespace the workflow is executing in
1346    pub fn namespace(&self) -> &str {
1347        &self.base.inner.namespace
1348    }
1349
1350    /// Return the task queue the workflow is executing in
1351    pub fn task_queue(&self) -> &str {
1352        &self.base.inner.task_queue
1353    }
1354
1355    /// Return the current time according to the workflow (which is not wall-clock time).
1356    pub fn workflow_time(&self) -> Option<SystemTime> {
1357        self.base
1358            .inner
1359            .shared
1360            .borrow()
1361            .activation
1362            .timestamp
1363            .try_into_or_none()
1364    }
1365
1366    /// Return the length of history so far at this point in the workflow
1367    pub fn history_length(&self) -> u32 {
1368        self.base.inner.shared.borrow().activation.history_length
1369    }
1370
1371    /// Return the deployment version, if any,  as it was when this point in the workflow was first
1372    /// reached. If this code is being executed for the first time, return this Worker's deployment
1373    /// version if it has one.
1374    pub fn current_deployment_version(&self) -> Option<WorkerDeploymentVersion> {
1375        self.base
1376            .inner
1377            .shared
1378            .borrow()
1379            .activation
1380            .clone()
1381            .deployment_version_for_current_task
1382            .map(Into::into)
1383    }
1384
1385    /// Return current values for workflow search attributes.
1386    pub fn search_attributes(&self) -> SearchAttributes {
1387        SearchAttributes::from_proto(&self.base.inner.shared.borrow().search_attributes)
1388    }
1389
1390    /// Return the current workflow memo values.
1391    pub fn memo(&self) -> Memo {
1392        Memo::from_raw(
1393            Some(self.base.inner.shared.borrow().memo.clone()),
1394            self.payload_converter().clone(),
1395            SerializationContextData::Workflow,
1396        )
1397    }
1398
1399    /// Generates a deterministic pseudo-random value of type `T`.
1400    ///
1401    /// The value is derived from Temporal's workflow randomness seed, making it safe during
1402    /// replay. This generator is not cryptographically secure.
1403    pub fn random<T>(&self) -> T
1404    where
1405        T: WorkflowRandomValue,
1406    {
1407        self.base.random()
1408    }
1409
1410    /// Generates a deterministic lowercase, hyphenated version 4 UUID string.
1411    ///
1412    /// This uses [`Self::random`] and is not cryptographically secure.
1413    pub fn uuid4(&self) -> String {
1414        self.base.uuid4()
1415    }
1416
1417    /// Returns true if the current workflow task is happening under replay
1418    pub fn is_replaying(&self) -> bool {
1419        self.base.inner.shared.borrow().activation.is_replaying
1420    }
1421
1422    /// Returns true if the current work is replaying history events
1423    pub fn is_replaying_history_events(&self) -> bool {
1424        self.base.inner.shared.borrow().is_replaying_history_events
1425    }
1426
1427    /// Returns true if the server suggests this workflow should continue-as-new
1428    pub fn continue_as_new_suggested(&self) -> bool {
1429        self.base
1430            .inner
1431            .shared
1432            .borrow()
1433            .activation
1434            .continue_as_new_suggested
1435    }
1436
1437    /// Returns true if the workflow's target worker deployment version changed.
1438    ///
1439    /// This experimental signal is intended for workers using worker deployment versioning.
1440    pub fn target_worker_deployment_version_changed(&self) -> bool {
1441        self.base
1442            .inner
1443            .shared
1444            .borrow()
1445            .activation
1446            .target_worker_deployment_version_changed
1447    }
1448
1449    /// Returns the headers for the current handler invocation (signal, update, query, etc.).
1450    ///
1451    /// When called from within a signal handler, returns the headers that were sent with that
1452    /// signal. When called from the main workflow run method, returns an empty map.
1453    pub fn headers(&self) -> &HashMap<String, Payload> {
1454        &self.headers
1455    }
1456
1457    /// Returns the [PayloadConverter] currently used by the worker running this workflow.
1458    pub fn payload_converter(&self) -> &PayloadConverter {
1459        self.base.inner.data_converter.payload_converter()
1460    }
1461
1462    /// Return Rust-native information about this workflow execution.
1463    pub fn info(&self) -> WorkflowContextView {
1464        self.view()
1465    }
1466
1467    /// Return the workflow's root cancellation token.
1468    pub fn cancellation_token(&self) -> WorkflowCancellationToken {
1469        self.base.cancellation_token()
1470    }
1471
1472    /// A future that resolves if/when the workflow is cancelled, with an optional user-provided reason.
1473    pub fn cancelled(&self) -> impl FusedFuture<Output = Option<String>> + '_ {
1474        let token = self.cancellation_token();
1475        async move {
1476            token.cancelled().await;
1477            token.reason()
1478        }
1479        .fuse()
1480    }
1481
1482    /// Signal that this workflow should continue as a new workflow execution with the given input and
1483    /// options.
1484    ///
1485    /// This always returns an `Err` which should be propigated.
1486    pub fn continue_as_new(
1487        &self,
1488        input: <W::Run as WorkflowDefinition>::Input,
1489        opts: ContinueAsNewOptions,
1490    ) -> Result<std::convert::Infallible, WorkflowTermination>
1491    where
1492        W: WorkflowImplementation,
1493    {
1494        let input = ContinueAsNewInput::new(Box::new(input), opts);
1495        let base_ctx = self.base.clone();
1496        let workflow_type = base_ctx.workflow_type().to_string();
1497        let next = WorkflowNext::new(move |input: ContinueAsNewInput| {
1498            let (input, headers, opts) = input.into_parts();
1499            let input = match input.downcast::<<W::Run as WorkflowDefinition>::Input>() {
1500                Ok(input) => input,
1501                Err(_) => return Err(outbound_type_error("continue-as-new input").into()),
1502            };
1503            let pc = base_ctx.data_converter().payload_converter();
1504            let ctx = SerializationContext {
1505                data: &SerializationContextData::Workflow,
1506                converter: pc,
1507            };
1508            let arguments = pc
1509                .to_payloads(&ctx, &*input)
1510                .map_err(WorkflowTermination::from)?;
1511            let request = opts.into_request(workflow_type, arguments, headers, pc)?;
1512            Err(WorkflowTermination::continue_as_new(request))
1513        });
1514        let interceptors = self.base.inner.workflow_interceptors.clone();
1515        call_continue_as_new(
1516            interceptors,
1517            crate::workflow_interceptors::SyncWorkflowInterceptorContext::new(self.base.clone()),
1518            input,
1519            next,
1520        )
1521    }
1522
1523    /// Request to create a timer
1524    pub fn timer<T: Into<TimerOptions>>(
1525        &self,
1526        opts: T,
1527    ) -> impl CancellableFuture<Output = TimerResult> {
1528        self.base.timer(opts)
1529    }
1530
1531    /// Request to run an activity
1532    pub fn execute_activity<AD: ActivityDefinition>(
1533        &self,
1534        activity: AD,
1535        input: impl Into<AD::Input>,
1536        opts: ActivityOptions,
1537    ) -> impl CancellableFuture<Output = Result<AD::Output, ActivityExecutionError>>
1538    where
1539        AD::Output: TemporalDeserializable,
1540    {
1541        self.base.execute_activity(activity, input, opts)
1542    }
1543
1544    /// Request to run an activity
1545    ///
1546    /// Deprecated alias for [`SyncWorkflowContext::execute_activity`].
1547    #[deprecated(note = "use `execute_activity` instead")]
1548    pub fn start_activity<AD: ActivityDefinition>(
1549        &self,
1550        activity: AD,
1551        input: impl Into<AD::Input>,
1552        opts: ActivityOptions,
1553    ) -> impl CancellableFuture<Output = Result<AD::Output, ActivityExecutionError>>
1554    where
1555        AD::Output: TemporalDeserializable,
1556    {
1557        self.execute_activity(activity, input, opts)
1558    }
1559
1560    /// Request to run a local activity
1561    pub fn execute_local_activity<AD: ActivityDefinition>(
1562        &self,
1563        activity: AD,
1564        input: impl Into<AD::Input>,
1565        opts: LocalActivityOptions,
1566    ) -> impl CancellableFuture<Output = Result<AD::Output, ActivityExecutionError>>
1567    where
1568        AD::Output: TemporalDeserializable,
1569    {
1570        self.base.execute_local_activity(activity, input, opts)
1571    }
1572
1573    /// Request to run a local activity
1574    ///
1575    /// Deprecated alias for [`SyncWorkflowContext::execute_local_activity`].
1576    #[deprecated(note = "use `execute_local_activity` instead")]
1577    pub fn start_local_activity<AD: ActivityDefinition>(
1578        &self,
1579        activity: AD,
1580        input: impl Into<AD::Input>,
1581        opts: LocalActivityOptions,
1582    ) -> impl CancellableFuture<Output = Result<AD::Output, ActivityExecutionError>>
1583    where
1584        AD::Output: TemporalDeserializable,
1585    {
1586        self.execute_local_activity(activity, input, opts)
1587    }
1588
1589    /// Start a child workflow. Returns a future that resolves to a [StartedChildWorkflow]
1590    /// which can be used to await the result, send signals, or cancel the child.
1591    pub fn start_child_workflow<WD: WorkflowDefinition + 'static>(
1592        &self,
1593        workflow: WD,
1594        input: impl Into<WD::Input>,
1595        opts: ChildWorkflowOptions,
1596    ) -> impl CancellableFutureWithReason<
1597        Output = Result<StartedChildWorkflow<WD>, ChildWorkflowStartError>,
1598    >
1599    where
1600        WD::Output: TemporalDeserializable,
1601    {
1602        self.base.start_child_workflow(workflow, input, opts)
1603    }
1604
1605    /// Deprecated alias for [`SyncWorkflowContext::start_child_workflow`].
1606    #[deprecated(note = "use `start_child_workflow` instead")]
1607    pub fn child_workflow<WD: WorkflowDefinition + 'static>(
1608        &self,
1609        workflow: WD,
1610        input: impl Into<WD::Input>,
1611        opts: ChildWorkflowOptions,
1612    ) -> impl CancellableFutureWithReason<
1613        Output = Result<StartedChildWorkflow<WD>, ChildWorkflowStartError>,
1614    >
1615    where
1616        WD::Output: TemporalDeserializable,
1617    {
1618        self.start_child_workflow(workflow, input, opts)
1619    }
1620
1621    /// Check (or record) that this workflow history was created with the provided patch.
1622    ///
1623    /// Workers can use their experimental patch activation callback to delay activating a newly
1624    /// introduced patch during a rolling deployment. The callback is only consulted when the
1625    /// marker would otherwise be created for the first time.
1626    pub fn patched(&self, patch_id: &str) -> bool {
1627        self.patch_impl(patch_id, false)
1628    }
1629
1630    /// Record that this workflow history was created with the provided patch, and it is being
1631    /// phased out.
1632    pub fn deprecate_patch(&self, patch_id: &str) -> bool {
1633        self.patch_impl(patch_id, true)
1634    }
1635
1636    fn patch_impl(&self, patch_id: &str, deprecated: bool) -> bool {
1637        if let Some(present) = self.base.inner.shared.borrow().changes.get(patch_id) {
1638            return *present;
1639        }
1640
1641        let shared = self.base.inner.shared.borrow();
1642        let replaying = shared.activation.is_replaying;
1643        let notified = shared.notified_patches.contains(patch_id);
1644        drop(shared);
1645
1646        // Replay and deprecation must follow history; only a fresh patch consults rollout policy.
1647        let res = if deprecated || replaying || notified {
1648            !replaying || notified
1649        } else if let Some(callback) = &self.base.inner.patch_activation_callback {
1650            callback(PatchActivationInput {
1651                workflow_info: self.base.view(),
1652                patch_id: patch_id.to_string(),
1653            })
1654        } else {
1655            true
1656        };
1657
1658        if res {
1659            self.base.inner.runtime.host.push_command(
1660                workflow_command::Variant::SetPatchMarker(SetPatchMarker {
1661                    patch_id: patch_id.to_string(),
1662                    deprecated,
1663                })
1664                .into(),
1665            );
1666        }
1667
1668        self.base
1669            .inner
1670            .shared
1671            .borrow_mut()
1672            .changes
1673            .insert(patch_id.to_string(), res);
1674
1675        res
1676    }
1677
1678    /// Get a handle to an external workflow for sending signals or requesting cancellation.
1679    pub fn external_workflow(
1680        &self,
1681        workflow_id: impl Into<String>,
1682        run_id: Option<String>,
1683    ) -> ExternalWorkflowHandle {
1684        self.base.external_workflow(workflow_id, run_id)
1685    }
1686
1687    /// Add, update, or remove search attributes using typed keys.
1688    ///
1689    /// Updates are applied to the local in-memory view immediately so that
1690    /// subsequent calls to [`search_attributes()`](Self::search_attributes)
1691    /// reflect the changes. The command is also sent to the server.
1692    pub fn upsert_search_attributes(
1693        &self,
1694        updates: impl IntoIterator<Item = SearchAttributeUpdate>,
1695    ) {
1696        // Collect so we can iterate twice: once for local state, once for the
1697        // wire proto (which uses a different encoding for "unset").
1698        let updates: Vec<SearchAttributeUpdate> = updates.into_iter().collect();
1699
1700        // Update local state using the typed API, which correctly removes keys
1701        // on unset (rather than inserting empty payloads like the wire format).
1702        {
1703            let mut shared = self.base.inner.shared.borrow_mut();
1704            let mut attrs = SearchAttributes::from_proto(&shared.search_attributes);
1705            for update in updates.iter().cloned() {
1706                attrs.apply(update);
1707            }
1708            shared.search_attributes = attrs.into_proto();
1709        }
1710
1711        let proto = SearchAttributes::updates_to_proto(updates);
1712        self.base.inner.runtime.host.push_command(
1713            workflow_command::Variant::UpsertWorkflowSearchAttributes(
1714                UpsertWorkflowSearchAttributes {
1715                    search_attributes: Some(proto),
1716                },
1717            )
1718            .into(),
1719        );
1720    }
1721
1722    /// Add or replace memo values with `Some`; remove memo keys with `None`.
1723    pub fn upsert_memo<K>(
1724        &self,
1725        updates: impl IntoIterator<Item = (K, Option<MemoValue>)>,
1726    ) -> Result<(), PayloadConversionError>
1727    where
1728        K: Into<String>,
1729    {
1730        let mut fields = HashMap::new();
1731        let mut local_updates = Vec::new();
1732        for (key, value) in updates {
1733            let key = key.into();
1734            let (command_payload, local_payload) = match value {
1735                Some(value) => {
1736                    let payload = value.to_payload(self.payload_converter())?;
1737                    (payload.clone(), Some(payload))
1738                }
1739                None => (
1740                    MemoValue::new(()).to_payload(self.payload_converter())?,
1741                    None,
1742                ),
1743            };
1744            fields.insert(key.clone(), command_payload);
1745            local_updates.push((key, local_payload));
1746        }
1747        {
1748            let mut shared = self.base.inner.shared.borrow_mut();
1749            for (key, payload) in local_updates {
1750                match payload {
1751                    Some(payload) => {
1752                        shared.memo.fields.insert(key, payload);
1753                    }
1754                    None => {
1755                        shared.memo.fields.remove(&key);
1756                    }
1757                }
1758            }
1759        }
1760        self.base.inner.runtime.host.push_command(
1761            workflow_command::Variant::ModifyWorkflowProperties(ModifyWorkflowProperties {
1762                upserted_memo: Some(ProtoMemo { fields }),
1763            })
1764            .into(),
1765        );
1766        Ok(())
1767    }
1768
1769    /// Set the current details string for this workflow execution.
1770    ///
1771    /// The value is surfaced to the Temporal server UI in real time via the
1772    /// the workflow metadata query.
1773    pub fn set_current_details(&self, details: impl Into<String>) {
1774        let details = details.into();
1775        self.base.inner.shared.borrow_mut().current_details = details.clone();
1776        self.base.inner.runtime.host.set_current_details(details);
1777    }
1778
1779    /// Force a workflow task failure (EX: in order to retry on non-sticky queue)
1780    pub fn force_task_fail(&self, with: impl Into<Box<dyn std::error::Error + Send + Sync>>) {
1781        self.base.inner.runtime.set_forced_wft_failure(with.into());
1782    }
1783
1784    /// Start a nexus operation
1785    pub fn start_nexus_operation(
1786        &self,
1787        opts: NexusOperationOptions,
1788    ) -> impl CancellableFuture<Output = NexusStartResult> {
1789        self.base.start_nexus_operation(opts)
1790    }
1791
1792    /// Create a read-only view of this context.
1793    pub(crate) fn view(&self) -> WorkflowContextView {
1794        self.base.view()
1795    }
1796}
1797
1798impl<W> WorkflowContext<W> {
1799    /// Create a new wf context from a base context and workflow state.
1800    pub(crate) fn from_base(base: BaseWorkflowContext, workflow_state: Rc<RefCell<W>>) -> Self {
1801        Self {
1802            sync: SyncWorkflowContext {
1803                base,
1804                headers: Rc::new(HashMap::new()),
1805                _phantom: PhantomData,
1806            },
1807            workflow_state,
1808            condition_wakers: Rc::new(RefCell::new(Vec::new())),
1809        }
1810    }
1811
1812    /// Returns a new context with the specified headers set.
1813    pub(crate) fn with_headers(&self, headers: HashMap<String, Payload>) -> Self {
1814        Self {
1815            sync: SyncWorkflowContext {
1816                base: self.sync.base.clone(),
1817                headers: Rc::new(headers),
1818                _phantom: PhantomData,
1819            },
1820            workflow_state: self.workflow_state.clone(),
1821            condition_wakers: self.condition_wakers.clone(),
1822        }
1823    }
1824
1825    /// Returns a [`SyncWorkflowContext`] extracted from this context.
1826    pub(crate) fn sync_context(&self) -> SyncWorkflowContext<W> {
1827        self.sync.clone()
1828    }
1829
1830    /// Create a read-only view of this context.
1831    pub(crate) fn view(&self) -> WorkflowContextView {
1832        self.sync.view()
1833    }
1834
1835    // --- Delegated methods from SyncWorkflowContext ---
1836
1837    /// Return the workflow's unique identifier
1838    pub fn workflow_id(&self) -> &str {
1839        self.sync.workflow_id()
1840    }
1841
1842    /// Return the run id of this workflow execution
1843    pub fn run_id(&self) -> &str {
1844        self.sync.run_id()
1845    }
1846
1847    /// Return the namespace the workflow is executing in
1848    pub fn namespace(&self) -> &str {
1849        self.sync.namespace()
1850    }
1851
1852    /// Return the task queue the workflow is executing in
1853    pub fn task_queue(&self) -> &str {
1854        self.sync.task_queue()
1855    }
1856
1857    /// Return the current time according to the workflow (which is not wall-clock time).
1858    pub fn workflow_time(&self) -> Option<SystemTime> {
1859        self.sync.workflow_time()
1860    }
1861
1862    /// Return the length of history so far at this point in the workflow
1863    pub fn history_length(&self) -> u32 {
1864        self.sync.history_length()
1865    }
1866
1867    /// Return the deployment version, if any, as it was when this point in the workflow was first
1868    /// reached. If this code is being executed for the first time, return this Worker's deployment
1869    /// version if it has one.
1870    pub fn current_deployment_version(&self) -> Option<WorkerDeploymentVersion> {
1871        self.sync.current_deployment_version()
1872    }
1873
1874    /// Return current values for workflow search attributes.
1875    pub fn search_attributes(&self) -> SearchAttributes {
1876        self.sync.search_attributes()
1877    }
1878
1879    /// Return the current workflow memo values.
1880    pub fn memo(&self) -> Memo {
1881        self.sync.memo()
1882    }
1883
1884    /// Generates a deterministic pseudo-random value of type `T`.
1885    ///
1886    /// See [`SyncWorkflowContext::random`].
1887    pub fn random<T>(&self) -> T
1888    where
1889        T: WorkflowRandomValue,
1890    {
1891        self.sync.random()
1892    }
1893
1894    /// Generates a deterministic lowercase, hyphenated version 4 UUID string.
1895    ///
1896    /// See [`SyncWorkflowContext::uuid4`].
1897    pub fn uuid4(&self) -> String {
1898        self.sync.uuid4()
1899    }
1900
1901    /// Returns true if the current workflow task is happening under replay
1902    pub fn is_replaying(&self) -> bool {
1903        self.sync.is_replaying()
1904    }
1905
1906    /// Returns true if the current work is replaying history events
1907    pub fn is_replaying_history_events(&self) -> bool {
1908        self.sync.is_replaying_history_events()
1909    }
1910
1911    /// Returns true if the server suggests this workflow should continue-as-new
1912    pub fn continue_as_new_suggested(&self) -> bool {
1913        self.sync.continue_as_new_suggested()
1914    }
1915
1916    /// Returns true if the workflow's target worker deployment version changed.
1917    ///
1918    /// This experimental signal is intended for workers using worker deployment versioning.
1919    pub fn target_worker_deployment_version_changed(&self) -> bool {
1920        self.sync.target_worker_deployment_version_changed()
1921    }
1922
1923    /// Returns the headers for the current handler invocation (signal, update, query, etc.).
1924    pub fn headers(&self) -> &HashMap<String, Payload> {
1925        self.sync.headers()
1926    }
1927
1928    /// Returns the [PayloadConverter] currently used by the worker running this workflow.
1929    pub fn payload_converter(&self) -> &PayloadConverter {
1930        self.sync.payload_converter()
1931    }
1932
1933    /// Return Rust-native information about this workflow execution.
1934    pub fn info(&self) -> WorkflowContextView {
1935        self.sync.info()
1936    }
1937
1938    /// Return the workflow's root cancellation token.
1939    pub fn cancellation_token(&self) -> WorkflowCancellationToken {
1940        self.sync.cancellation_token()
1941    }
1942
1943    /// A future that resolves if/when the workflow is cancelled, with an optional user-provided reason.
1944    pub fn cancelled(&self) -> impl FusedFuture<Output = Option<String>> + '_ {
1945        self.sync.cancelled()
1946    }
1947
1948    /// Request to create a timer
1949    pub fn timer<T: Into<TimerOptions>>(
1950        &self,
1951        opts: T,
1952    ) -> impl CancellableFuture<Output = TimerResult> {
1953        self.sync.timer(opts)
1954    }
1955
1956    /// Request to run an activity
1957    pub fn execute_activity<AD: ActivityDefinition>(
1958        &self,
1959        activity: AD,
1960        input: impl Into<AD::Input>,
1961        opts: ActivityOptions,
1962    ) -> impl CancellableFuture<Output = Result<AD::Output, ActivityExecutionError>>
1963    where
1964        AD::Output: TemporalDeserializable,
1965    {
1966        self.sync.execute_activity(activity, input, opts)
1967    }
1968
1969    /// Request to run an activity
1970    ///
1971    /// Deprecated alias for [`WorkflowContext::execute_activity`].
1972    #[deprecated(note = "use `execute_activity` instead")]
1973    pub fn start_activity<AD: ActivityDefinition>(
1974        &self,
1975        activity: AD,
1976        input: impl Into<AD::Input>,
1977        opts: ActivityOptions,
1978    ) -> impl CancellableFuture<Output = Result<AD::Output, ActivityExecutionError>>
1979    where
1980        AD::Output: TemporalDeserializable,
1981    {
1982        self.execute_activity(activity, input, opts)
1983    }
1984
1985    /// Request to run a local activity
1986    pub fn execute_local_activity<AD: ActivityDefinition>(
1987        &self,
1988        activity: AD,
1989        input: impl Into<AD::Input>,
1990        opts: LocalActivityOptions,
1991    ) -> impl CancellableFuture<Output = Result<AD::Output, ActivityExecutionError>>
1992    where
1993        AD::Output: TemporalDeserializable,
1994    {
1995        self.sync.execute_local_activity(activity, input, opts)
1996    }
1997
1998    /// Request to run a local activity
1999    ///
2000    /// Deprecated alias for [`WorkflowContext::execute_local_activity`].
2001    #[deprecated(note = "use `execute_local_activity` instead")]
2002    pub fn start_local_activity<AD: ActivityDefinition>(
2003        &self,
2004        activity: AD,
2005        input: impl Into<AD::Input>,
2006        opts: LocalActivityOptions,
2007    ) -> impl CancellableFuture<Output = Result<AD::Output, ActivityExecutionError>>
2008    where
2009        AD::Output: TemporalDeserializable,
2010    {
2011        self.execute_local_activity(activity, input, opts)
2012    }
2013
2014    /// Start a child workflow. See [SyncWorkflowContext::start_child_workflow] for details.
2015    pub fn start_child_workflow<WD: WorkflowDefinition + 'static>(
2016        &self,
2017        workflow: WD,
2018        input: impl Into<WD::Input>,
2019        opts: ChildWorkflowOptions,
2020    ) -> impl CancellableFutureWithReason<
2021        Output = Result<StartedChildWorkflow<WD>, ChildWorkflowStartError>,
2022    >
2023    where
2024        WD::Output: TemporalDeserializable,
2025    {
2026        self.sync.start_child_workflow(workflow, input, opts)
2027    }
2028
2029    /// Deprecated alias for [`WorkflowContext::start_child_workflow`].
2030    #[deprecated(note = "use `start_child_workflow` instead")]
2031    pub fn child_workflow<WD: WorkflowDefinition + 'static>(
2032        &self,
2033        workflow: WD,
2034        input: impl Into<WD::Input>,
2035        opts: ChildWorkflowOptions,
2036    ) -> impl CancellableFutureWithReason<
2037        Output = Result<StartedChildWorkflow<WD>, ChildWorkflowStartError>,
2038    >
2039    where
2040        WD::Output: TemporalDeserializable,
2041    {
2042        self.start_child_workflow(workflow, input, opts)
2043    }
2044
2045    /// Check (or record) that this workflow history was created with the provided patch
2046    pub fn patched(&self, patch_id: &str) -> bool {
2047        self.sync.patched(patch_id)
2048    }
2049
2050    /// Record that this workflow history was created with the provided patch, and it is being
2051    /// phased out.
2052    pub fn deprecate_patch(&self, patch_id: &str) -> bool {
2053        self.sync.deprecate_patch(patch_id)
2054    }
2055
2056    /// Get a handle to an external workflow. See [SyncWorkflowContext::external_workflow].
2057    pub fn external_workflow(
2058        &self,
2059        workflow_id: impl Into<String>,
2060        run_id: Option<String>,
2061    ) -> ExternalWorkflowHandle {
2062        self.sync.external_workflow(workflow_id, run_id)
2063    }
2064
2065    /// Add, update, or remove search attributes using typed keys.
2066    pub fn upsert_search_attributes(
2067        &self,
2068        updates: impl IntoIterator<Item = SearchAttributeUpdate>,
2069    ) {
2070        self.sync.upsert_search_attributes(updates)
2071    }
2072
2073    /// Add or replace memo values with `Some`; remove memo keys with `None`.
2074    pub fn upsert_memo<K>(
2075        &self,
2076        updates: impl IntoIterator<Item = (K, Option<MemoValue>)>,
2077    ) -> Result<(), PayloadConversionError>
2078    where
2079        K: Into<String>,
2080    {
2081        self.sync.upsert_memo(updates)
2082    }
2083
2084    /// Set the current details string for this workflow execution.
2085    ///
2086    /// See [`SyncWorkflowContext::set_current_details`].
2087    pub fn set_current_details(&self, details: impl Into<String>) {
2088        self.sync.set_current_details(details)
2089    }
2090
2091    /// Force a workflow task failure (EX: in order to retry on non-sticky queue)
2092    pub fn force_task_fail(&self, with: impl Into<Box<dyn std::error::Error + Send + Sync>>) {
2093        self.sync.force_task_fail(with)
2094    }
2095
2096    /// Start a nexus operation
2097    pub fn start_nexus_operation(
2098        &self,
2099        opts: NexusOperationOptions,
2100    ) -> impl CancellableFuture<Output = NexusStartResult> {
2101        self.sync.start_nexus_operation(opts)
2102    }
2103
2104    /// Access workflow state immutably via closure.
2105    ///
2106    /// The borrow is scoped to the closure and cannot escape, preventing
2107    /// borrows from being held across await points.
2108    pub fn state<R>(&self, f: impl FnOnce(&W) -> R) -> R {
2109        f(&*self.workflow_state.borrow())
2110    }
2111
2112    /// Access workflow state mutably via closure.
2113    ///
2114    /// The borrow is scoped to the closure and cannot escape, preventing
2115    /// borrows from being held across await points.
2116    ///
2117    /// After the mutation, all wakers registered by pending `wait_condition`
2118    /// futures are woken so that waker-based combinators (e.g.
2119    /// `FuturesOrdered`) re-poll them on the next pass.
2120    pub fn state_mut<R>(&self, f: impl FnOnce(&mut W) -> R) -> R {
2121        let result = f(&mut *self.workflow_state.borrow_mut());
2122        let _guard = SdkWakeGuard::new();
2123        for waker in self.condition_wakers.borrow_mut().drain(..) {
2124            waker.wake();
2125        }
2126        self.sync.base.set_state_mutated();
2127        result
2128    }
2129
2130    /// Signal that this workflow should continue as a new workflow execution with the given input and
2131    /// options.
2132    ///
2133    /// This always returns an `Err` which should be propigated
2134    pub fn continue_as_new(
2135        &self,
2136        input: <W::Run as WorkflowDefinition>::Input,
2137        opts: ContinueAsNewOptions,
2138    ) -> Result<std::convert::Infallible, WorkflowTermination>
2139    where
2140        W: WorkflowImplementation,
2141    {
2142        self.sync.continue_as_new(input, opts)
2143    }
2144
2145    /// Wait for some condition on workflow state to become true, yielding the workflow if not.
2146    ///
2147    /// The condition closure receives an immutable reference to the workflow state,
2148    /// which is borrowed only for the duration of each poll (not across await points).
2149    /// By default, the wait inherits workflow cancellation.
2150    pub fn wait_condition<'a>(
2151        &'a self,
2152        condition: impl FnMut(&W) -> bool + 'a,
2153    ) -> impl FusedFuture<Output = Result<(), WorkflowCancellationError>> + 'a {
2154        self.wait_condition_with_options(condition, Default::default())
2155    }
2156
2157    /// Wait for some condition on workflow state to become true with the provided options.
2158    pub fn wait_condition_with_options<'a>(
2159        &'a self,
2160        mut condition: impl FnMut(&W) -> bool + 'a,
2161        options: WaitConditionOptions,
2162    ) -> impl FusedFuture<Output = Result<(), WorkflowCancellationError>> + 'a {
2163        let token = options
2164            .cancellation_token
2165            .unwrap_or_else(|| self.cancellation_token());
2166        let wait_token = token.clone();
2167        let mut cancelled = Box::pin(async move {
2168            wait_token.cancelled().await;
2169        });
2170        future::poll_fn(move |cx: &mut Context<'_>| {
2171            if condition(&*self.workflow_state.borrow()) {
2172                Poll::Ready(Ok(()))
2173            } else if cancelled.as_mut().poll(cx).is_ready() {
2174                Poll::Ready(Err(WorkflowCancellationError::new(token.reason())))
2175            } else {
2176                self.condition_wakers.borrow_mut().push(cx.waker().clone());
2177                Poll::Pending
2178            }
2179        })
2180        .fuse()
2181    }
2182}
2183
2184struct WfCtxProtectedDat {
2185    next_timer_sequence_number: u32,
2186    next_activity_sequence_number: u32,
2187    next_child_workflow_sequence_number: u32,
2188    next_cancel_external_wf_sequence_number: u32,
2189    next_signal_external_wf_sequence_number: u32,
2190    next_nexus_op_sequence_number: u32,
2191}
2192
2193impl WfCtxProtectedDat {
2194    fn next_timer_seq(&mut self) -> u32 {
2195        let seq = self.next_timer_sequence_number;
2196        self.next_timer_sequence_number += 1;
2197        seq
2198    }
2199    fn next_activity_seq(&mut self) -> u32 {
2200        let seq = self.next_activity_sequence_number;
2201        self.next_activity_sequence_number += 1;
2202        seq
2203    }
2204    fn next_child_workflow_seq(&mut self) -> u32 {
2205        let seq = self.next_child_workflow_sequence_number;
2206        self.next_child_workflow_sequence_number += 1;
2207        seq
2208    }
2209    fn next_cancel_external_wf_seq(&mut self) -> u32 {
2210        let seq = self.next_cancel_external_wf_sequence_number;
2211        self.next_cancel_external_wf_sequence_number += 1;
2212        seq
2213    }
2214    fn next_signal_external_wf_seq(&mut self) -> u32 {
2215        let seq = self.next_signal_external_wf_sequence_number;
2216        self.next_signal_external_wf_sequence_number += 1;
2217        seq
2218    }
2219    fn next_nexus_op_seq(&mut self) -> u32 {
2220        let seq = self.next_nexus_op_sequence_number;
2221        self.next_nexus_op_sequence_number += 1;
2222        seq
2223    }
2224}
2225
2226#[derive(Clone, Debug)]
2227struct WorkflowContextSharedData {
2228    /// Maps change ids -> resolved status
2229    changes: HashMap<String, bool>,
2230    /// Kept separate from memoized decisions so replay still emits the matching patch command.
2231    notified_patches: HashSet<String>,
2232    activation: CoreWorkflowActivation,
2233    memo: ProtoMemo,
2234    is_replaying_history_events: bool,
2235    search_attributes: ProtoSearchAttributes,
2236    random: Pcg64Mcg,
2237    /// Current details string, surfaced via the workflow metadata query.
2238    current_details: String,
2239}
2240
2241/// A Future that can be cancelled.
2242/// Used in the prototype SDK for cancelling operations like timers and activities.
2243pub trait CancellableFuture: FusedFuture {
2244    /// Cancel this Future
2245    fn cancel(&self);
2246}
2247
2248/// A Future that can be cancelled with a reason
2249pub trait CancellableFutureWithReason: CancellableFuture {
2250    /// Cancel this Future with a reason
2251    fn cancel_with_reason(&self, reason: String);
2252}
2253
2254fn cancellable_outbound<T: 'static>(
2255    future: impl CancellableFuture<Output = T> + 'static,
2256) -> CancellableWorkflowOutboundFuture<T> {
2257    let future = Rc::new(RefCell::new(Box::pin(future)));
2258    let polled = future.clone();
2259    let cancellation = WorkflowCancellationHandle::new(move |_| {
2260        future.borrow().as_ref().get_ref().cancel();
2261    });
2262    CancellableWorkflowOutboundFuture::new(
2263        future::poll_fn(move |cx| polled.borrow_mut().as_mut().poll(cx)),
2264        cancellation,
2265    )
2266}
2267
2268fn cancellable_outbound_with_reason<T: 'static>(
2269    future: impl CancellableFutureWithReason<Output = T> + 'static,
2270) -> CancellableWorkflowOutboundFuture<T> {
2271    let future = Rc::new(RefCell::new(Box::pin(future)));
2272    let polled = future.clone();
2273    let cancellation = WorkflowCancellationHandle::new(move |reason| {
2274        let future = future.borrow();
2275        let future = future.as_ref().get_ref();
2276        if let Some(reason) = reason {
2277            future.cancel_with_reason(reason);
2278        } else {
2279            future.cancel();
2280        }
2281    });
2282    CancellableWorkflowOutboundFuture::new(
2283        future::poll_fn(move |cx| polled.borrow_mut().as_mut().poll(cx)),
2284        cancellation,
2285    )
2286}
2287
2288pub(crate) struct WFCommandFut<T, D> {
2289    _unused: PhantomData<T>,
2290    result_rx: oneshot::Receiver<UnblockEvent>,
2291    other_dat: Option<D>,
2292}
2293impl<T> WFCommandFut<T, ()> {
2294    fn new() -> (Self, oneshot::Sender<UnblockEvent>) {
2295        Self::new_with_dat(())
2296    }
2297}
2298
2299impl<T, D> WFCommandFut<T, D> {
2300    fn new_with_dat(other_dat: D) -> (Self, oneshot::Sender<UnblockEvent>) {
2301        let (tx, rx) = oneshot::channel();
2302        (
2303            Self {
2304                _unused: PhantomData,
2305                result_rx: rx,
2306                other_dat: Some(other_dat),
2307            },
2308            tx,
2309        )
2310    }
2311}
2312
2313impl<T, D> Unpin for WFCommandFut<T, D> where T: Unblockable<OtherDat = D> {}
2314impl<T, D> Future for WFCommandFut<T, D>
2315where
2316    T: Unblockable<OtherDat = D>,
2317{
2318    type Output = T;
2319
2320    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
2321        let poll = self.result_rx.poll_unpin(cx).map(|x| {
2322            let od = self
2323                .other_dat
2324                .take()
2325                .expect("Other data must exist when resolving command future");
2326            Unblockable::unblock(x.unwrap(), od)
2327        });
2328        if poll.is_pending() {
2329            mark_intercepted_future_activation();
2330        }
2331        poll
2332    }
2333}
2334impl<T, D> FusedFuture for WFCommandFut<T, D>
2335where
2336    T: Unblockable<OtherDat = D>,
2337{
2338    fn is_terminated(&self) -> bool {
2339        self.other_dat.is_none()
2340    }
2341}
2342
2343struct CancellableWFCommandFut<T, D> {
2344    cmd_fut: WFCommandFut<T, D>,
2345    cancellable_id: CancellableID,
2346    base_ctx: BaseWorkflowContext,
2347}
2348impl<T> CancellableWFCommandFut<T, ()> {
2349    fn new(
2350        cancellable_id: CancellableID,
2351        base_ctx: BaseWorkflowContext,
2352    ) -> (Self, oneshot::Sender<UnblockEvent>) {
2353        Self::new_with_dat(cancellable_id, (), base_ctx)
2354    }
2355}
2356impl<T, D> CancellableWFCommandFut<T, D> {
2357    fn new_with_dat(
2358        cancellable_id: CancellableID,
2359        other_dat: D,
2360        base_ctx: BaseWorkflowContext,
2361    ) -> (Self, oneshot::Sender<UnblockEvent>) {
2362        let (cmd_fut, sender) = WFCommandFut::new_with_dat(other_dat);
2363        (
2364            Self {
2365                cmd_fut,
2366                cancellable_id,
2367                base_ctx,
2368            },
2369            sender,
2370        )
2371    }
2372}
2373impl<T, D> Unpin for CancellableWFCommandFut<T, D> where T: Unblockable<OtherDat = D> {}
2374impl<T, D> Future for CancellableWFCommandFut<T, D>
2375where
2376    T: Unblockable<OtherDat = D>,
2377{
2378    type Output = T;
2379
2380    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
2381        self.cmd_fut.poll_unpin(cx)
2382    }
2383}
2384impl<T, D> FusedFuture for CancellableWFCommandFut<T, D>
2385where
2386    T: Unblockable<OtherDat = D>,
2387{
2388    fn is_terminated(&self) -> bool {
2389        self.cmd_fut.is_terminated()
2390    }
2391}
2392
2393impl<T, D> CancellableFuture for CancellableWFCommandFut<T, D>
2394where
2395    T: Unblockable<OtherDat = D>,
2396{
2397    fn cancel(&self) {
2398        self.base_ctx.cancel(self.cancellable_id.clone());
2399    }
2400}
2401impl<T, D> CancellableFutureWithReason for CancellableWFCommandFut<T, D>
2402where
2403    T: Unblockable<OtherDat = D>,
2404{
2405    fn cancel_with_reason(&self, reason: String) {
2406        self.base_ctx
2407            .cancel(self.cancellable_id.clone().with_reason(reason));
2408    }
2409}
2410
2411struct LATimerBackoffFut {
2412    la_opts: LocalActivityOptions,
2413    activity_type: String,
2414    arguments: Vec<Payload>,
2415    headers: HashMap<String, Payload>,
2416    current_fut: Pin<Box<dyn CancellableFuture<Output = ActivityResolution> + Unpin>>,
2417    timer_fut: Option<Pin<Box<dyn CancellableFuture<Output = TimerResult> + Unpin>>>,
2418    cancellation_token: WorkflowCancellationToken,
2419    base_ctx: BaseWorkflowContext,
2420    next_attempt: u32,
2421    next_sched_time: Option<prost_types::Timestamp>,
2422    did_cancel: AtomicBool,
2423    terminated: bool,
2424}
2425impl LATimerBackoffFut {
2426    fn new(
2427        activity_type: String,
2428        arguments: Vec<Payload>,
2429        headers: HashMap<String, Payload>,
2430        opts: LocalActivityOptions,
2431        cancellation_token: WorkflowCancellationToken,
2432        base_ctx: BaseWorkflowContext,
2433    ) -> Self {
2434        let current_fut = Box::pin(base_ctx.clone().local_activity_no_timer_retry(
2435            activity_type.clone(),
2436            arguments.clone(),
2437            headers.clone(),
2438            opts.clone(),
2439        ));
2440        Self {
2441            la_opts: opts,
2442            activity_type,
2443            arguments,
2444            headers,
2445            current_fut,
2446            timer_fut: None,
2447            cancellation_token,
2448            base_ctx,
2449            next_attempt: 1,
2450            next_sched_time: None,
2451            did_cancel: AtomicBool::new(false),
2452            terminated: false,
2453        }
2454    }
2455}
2456impl Unpin for LATimerBackoffFut {}
2457impl Future for LATimerBackoffFut {
2458    type Output = ActivityResolution;
2459
2460    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
2461        // If the timer exists, wait for it first
2462        if let Some(tf) = self.timer_fut.as_mut() {
2463            return match tf.poll_unpin(cx) {
2464                Poll::Ready(tr) => {
2465                    self.timer_fut = None;
2466                    // Schedule next LA if this timer wasn't cancelled
2467                    if let TimerResult::Fired = tr {
2468                        let mut opts = self.la_opts.clone();
2469                        opts.attempt = Some(self.next_attempt);
2470                        opts.original_schedule_time
2471                            .clone_from(&self.next_sched_time);
2472                        self.current_fut =
2473                            Box::pin(self.base_ctx.clone().local_activity_no_timer_retry(
2474                                self.activity_type.clone(),
2475                                self.arguments.clone(),
2476                                self.headers.clone(),
2477                                opts,
2478                            ));
2479                        Poll::Pending
2480                    } else {
2481                        self.terminated = true;
2482                        Poll::Ready(ActivityResolution {
2483                            status: Some(activity_resolution::Status::Cancelled(Cancellation {
2484                                failure: Some(Failure {
2485                                    message: "Activity cancelled".to_owned(),
2486                                    failure_info: Some(FailureInfo::CanceledFailureInfo(
2487                                        CanceledFailureInfo::default(),
2488                                    )),
2489                                    ..Default::default()
2490                                }),
2491                            })),
2492                        })
2493                    }
2494                }
2495                Poll::Pending => Poll::Pending,
2496            };
2497        }
2498        let poll_res = self.current_fut.poll_unpin(cx);
2499        if let Poll::Ready(ref r) = poll_res
2500            && let Some(activity_resolution::Status::Backoff(b)) = r.status.as_ref()
2501        {
2502            // If we've already said we want to cancel, don't schedule the backoff timer. Just
2503            // return cancel status. This can happen if cancel comes after the LA says it wants
2504            // to back off but before we have scheduled the timer.
2505            if self.did_cancel.load(Ordering::Acquire) {
2506                self.terminated = true;
2507                return Poll::Ready(ActivityResolution {
2508                    status: Some(activity_resolution::Status::Cancelled(Cancellation {
2509                        failure: Some(Failure {
2510                            message: "Activity cancelled".to_owned(),
2511                            failure_info: Some(FailureInfo::CanceledFailureInfo(
2512                                CanceledFailureInfo::default(),
2513                            )),
2514                            ..Default::default()
2515                        }),
2516                    })),
2517                });
2518            }
2519
2520            let timer_f = self.base_ctx.timer(TimerOptions {
2521                duration: b
2522                    .backoff_duration
2523                    .expect("Duration is set")
2524                    .try_into()
2525                    .expect("duration converts ok"),
2526                cancellation_token: Some(self.cancellation_token.clone()),
2527                summary: None,
2528                event_group_markers: vec![],
2529            });
2530            self.timer_fut = Some(Box::pin(timer_f));
2531            self.next_attempt = b.attempt;
2532            self.next_sched_time.clone_from(&b.original_schedule_time);
2533            return Poll::Pending;
2534        }
2535        if poll_res.is_ready() {
2536            self.terminated = true;
2537        }
2538        poll_res
2539    }
2540}
2541impl FusedFuture for LATimerBackoffFut {
2542    fn is_terminated(&self) -> bool {
2543        self.terminated
2544    }
2545}
2546impl CancellableFuture for LATimerBackoffFut {
2547    fn cancel(&self) {
2548        self.did_cancel.store(true, Ordering::Release);
2549        if let Some(tf) = self.timer_fut.as_ref() {
2550            tf.cancel();
2551        }
2552        self.current_fut.cancel();
2553    }
2554}
2555
2556/// Future for activity results. Either an immediate error or a running activity.
2557enum ActivityFut<F, Output> {
2558    /// Immediate error (e.g., input serialization failure). Resolves on first poll.
2559    Errored {
2560        error: Option<Box<ActivityExecutionError>>,
2561        _phantom: PhantomData<Output>,
2562    },
2563    /// Running activity that will deserialize output on completion.
2564    Running {
2565        inner: F,
2566        data_converter: DataConverter,
2567        _phantom: PhantomData<Output>,
2568    },
2569    Terminated,
2570}
2571
2572impl<F, Output> ActivityFut<F, Output> {
2573    fn eager(err: ActivityExecutionError) -> Self {
2574        Self::Errored {
2575            error: Some(Box::new(err)),
2576            _phantom: PhantomData,
2577        }
2578    }
2579
2580    fn running(inner: F, data_converter: DataConverter) -> Self {
2581        Self::Running {
2582            inner,
2583            data_converter,
2584            _phantom: PhantomData,
2585        }
2586    }
2587}
2588
2589impl<F, Output> Unpin for ActivityFut<F, Output> where F: Unpin {}
2590
2591impl<F, Output> Future for ActivityFut<F, Output>
2592where
2593    F: Future<Output = ActivityResolution> + Unpin,
2594    Output: TemporalDeserializable + 'static,
2595{
2596    type Output = Result<Output, ActivityExecutionError>;
2597
2598    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
2599        let this = self.get_mut();
2600        let poll = match this {
2601            ActivityFut::Errored { error, .. } => {
2602                Poll::Ready(Err(*error.take().expect("polled after completion")))
2603            }
2604            ActivityFut::Running {
2605                inner,
2606                data_converter,
2607                ..
2608            } => match Pin::new(inner).poll(cx) {
2609                Poll::Pending => Poll::Pending,
2610                Poll::Ready(resolution) => Poll::Ready({
2611                    let status = resolution.status.ok_or_else(|| {
2612                        data_converter
2613                            .to_error(
2614                                &SerializationContextData::Workflow,
2615                                Failure {
2616                                    message: "Activity completed without a status".to_string(),
2617                                    ..Default::default()
2618                                },
2619                                ActivityExecutionDecodeHint { cancelled: false },
2620                            )
2621                            .expect("synthetic activity failure should decode")
2622                    })?;
2623
2624                    match status {
2625                        activity_resolution::Status::Completed(success) => {
2626                            let payload = success.result.unwrap_or_default();
2627                            let ctx = SerializationContext {
2628                                data: &SerializationContextData::Workflow,
2629                                converter: data_converter.payload_converter(),
2630                            };
2631                            data_converter
2632                                .payload_converter()
2633                                .from_payload::<Output>(&ctx, payload)
2634                                .map_err(ActivityExecutionError::Serialization)
2635                        }
2636                        activity_resolution::Status::Failed(f) => Err(data_converter.to_error(
2637                            &SerializationContextData::Workflow,
2638                            f.failure.unwrap_or_default(),
2639                            ActivityExecutionDecodeHint { cancelled: false },
2640                        )?),
2641                        activity_resolution::Status::Cancelled(c) => Err(data_converter.to_error(
2642                            &SerializationContextData::Workflow,
2643                            c.failure.unwrap_or_default(),
2644                            ActivityExecutionDecodeHint { cancelled: true },
2645                        )?),
2646                        activity_resolution::Status::Backoff(_) => {
2647                            panic!("DoBackoff should be handled by LATimerBackoffFut")
2648                        }
2649                    }
2650                }),
2651            },
2652            ActivityFut::Terminated => panic!("polled after termination"),
2653        };
2654        if poll.is_ready() {
2655            *this = ActivityFut::Terminated;
2656        }
2657        poll
2658    }
2659}
2660
2661impl<F, Output> FusedFuture for ActivityFut<F, Output>
2662where
2663    F: Future<Output = ActivityResolution> + Unpin,
2664    Output: TemporalDeserializable + 'static,
2665{
2666    fn is_terminated(&self) -> bool {
2667        matches!(self, ActivityFut::Terminated)
2668    }
2669}
2670
2671impl<F, Output> CancellableFuture for ActivityFut<F, Output>
2672where
2673    F: CancellableFuture<Output = ActivityResolution> + Unpin,
2674    Output: TemporalDeserializable + 'static,
2675{
2676    fn cancel(&self) {
2677        if let ActivityFut::Running { inner, .. } = self {
2678            inner.cancel()
2679        }
2680    }
2681}
2682
2683pub(crate) struct ChildWfCommon {
2684    workflow_id: String,
2685    child_seq: u32,
2686    result_future: CancellableWorkflowOutboundFuture<ChildWorkflowOutboundResult>,
2687    base_ctx: BaseWorkflowContext,
2688}
2689
2690/// Child workflow in pending state. Internal type used during the start handshake;
2691/// `ChildWorkflowStartFut` converts this into `Result<StartedChildWorkflow, _>` before
2692/// the caller sees it.
2693#[derive(derive_more::Debug)]
2694pub(crate) struct PendingChildWorkflow<WD: WorkflowDefinition> {
2695    pub(crate) status: ChildWorkflowStartStatus,
2696    #[debug(skip)]
2697    pub(crate) common: ChildWfCommon,
2698    pub(crate) _phantom: PhantomData<WD>,
2699}
2700
2701/// Output produced when an intercepted child workflow successfully starts.
2702#[derive(derive_more::Debug)]
2703pub struct StartChildWorkflowOutput {
2704    /// Run ID of the child workflow
2705    pub run_id: String,
2706    #[debug(skip)]
2707    result_future: CancellableWorkflowOutboundFuture<ChildWorkflowOutboundResult>,
2708    workflow_id: String,
2709    child_seq: u32,
2710    #[debug(skip)]
2711    base_ctx: BaseWorkflowContext,
2712}
2713
2714impl StartChildWorkflowOutput {
2715    /// Replace the intercepted child completion future.
2716    pub fn map_result(
2717        mut self,
2718        map: impl FnOnce(
2719            CancellableWorkflowOutboundFuture<ChildWorkflowOutboundResult>,
2720        ) -> CancellableWorkflowOutboundFuture<ChildWorkflowOutboundResult>,
2721    ) -> Self {
2722        self.result_future = map(self.result_future);
2723        self
2724    }
2725
2726    fn into_started<WD: WorkflowDefinition>(self) -> StartedChildWorkflow<WD> {
2727        StartedChildWorkflow {
2728            run_id: self.run_id,
2729            result_future: self.result_future,
2730            workflow_id: self.workflow_id,
2731            child_seq: self.child_seq,
2732            base_ctx: self.base_ctx,
2733            _phantom: PhantomData,
2734        }
2735    }
2736}
2737
2738/// Child workflow in started state.
2739#[derive(derive_more::Debug)]
2740pub struct StartedChildWorkflow<WD: WorkflowDefinition> {
2741    /// Run ID of the child workflow
2742    pub run_id: String,
2743    #[debug(skip)]
2744    result_future: CancellableWorkflowOutboundFuture<ChildWorkflowOutboundResult>,
2745    workflow_id: String,
2746    child_seq: u32,
2747    #[debug(skip)]
2748    base_ctx: BaseWorkflowContext,
2749    _phantom: PhantomData<WD>,
2750}
2751
2752/// Future for child workflow results. Wraps the raw result future and deserializes
2753/// the output on completion.
2754enum ChildWorkflowFut<F, Output> {
2755    Running {
2756        inner: F,
2757        data_converter: DataConverter,
2758        _phantom: PhantomData<Output>,
2759    },
2760    Terminated,
2761}
2762
2763impl<F, Output> Unpin for ChildWorkflowFut<F, Output> where F: Unpin {}
2764
2765impl<F, Output> Future for ChildWorkflowFut<F, Output>
2766where
2767    F: Future<Output = ChildWorkflowResult> + Unpin,
2768    Output: TemporalDeserializable + 'static,
2769{
2770    type Output = Result<Output, ChildWorkflowExecutionError>;
2771
2772    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
2773        let this = self.get_mut();
2774        let poll = match this {
2775            ChildWorkflowFut::Running {
2776                inner,
2777                data_converter,
2778                ..
2779            } => match Pin::new(inner).poll(cx) {
2780                Poll::Pending => Poll::Pending,
2781                Poll::Ready(result) => Poll::Ready({
2782                    let status = result.status.ok_or_else(|| {
2783                        data_converter
2784                            .to_error(
2785                                &SerializationContextData::Workflow,
2786                                Failure {
2787                                    message: "Child workflow completed without a status"
2788                                        .to_string(),
2789                                    ..Default::default()
2790                                },
2791                                ChildWorkflowExecutionDecodeHint,
2792                            )
2793                            .expect("synthetic child workflow failure should decode")
2794                    })?;
2795                    match status {
2796                        child_workflow_result::Status::Completed(success) => {
2797                            let payloads = success.result.into_iter().collect();
2798                            let ctx = SerializationContext {
2799                                data: &SerializationContextData::Workflow,
2800                                converter: data_converter.payload_converter(),
2801                            };
2802                            data_converter
2803                                .payload_converter()
2804                                .from_payloads::<Output>(&ctx, payloads)
2805                                .map_err(ChildWorkflowExecutionError::Serialization)
2806                        }
2807                        child_workflow_result::Status::Failed(f) => Err(data_converter.to_error(
2808                            &SerializationContextData::Workflow,
2809                            f.failure.unwrap_or_default(),
2810                            ChildWorkflowExecutionDecodeHint,
2811                        )?),
2812                        child_workflow_result::Status::Cancelled(c) => Err(data_converter
2813                            .to_error(
2814                                &SerializationContextData::Workflow,
2815                                c.failure.unwrap_or_default(),
2816                                ChildWorkflowExecutionDecodeHint,
2817                            )?),
2818                    }
2819                }),
2820            },
2821            ChildWorkflowFut::Terminated => panic!("polled after termination"),
2822        };
2823        if poll.is_ready() {
2824            *this = ChildWorkflowFut::Terminated;
2825        }
2826        poll
2827    }
2828}
2829
2830impl<F, Output> FusedFuture for ChildWorkflowFut<F, Output>
2831where
2832    F: Future<Output = ChildWorkflowResult> + Unpin,
2833    Output: TemporalDeserializable + 'static,
2834{
2835    fn is_terminated(&self) -> bool {
2836        matches!(self, ChildWorkflowFut::Terminated)
2837    }
2838}
2839
2840impl<F, Output> CancellableFutureWithReason for ChildWorkflowFut<F, Output>
2841where
2842    F: CancellableFutureWithReason<Output = ChildWorkflowResult> + Unpin,
2843    Output: TemporalDeserializable + 'static,
2844{
2845    fn cancel_with_reason(&self, reason: String) {
2846        if let ChildWorkflowFut::Running { inner, .. } = self {
2847            inner.cancel_with_reason(reason)
2848        }
2849    }
2850}
2851
2852impl<F, Output> CancellableFuture for ChildWorkflowFut<F, Output>
2853where
2854    F: CancellableFutureWithReason<Output = ChildWorkflowResult> + Unpin,
2855    Output: TemporalDeserializable + 'static,
2856{
2857    fn cancel(&self) {
2858        if let ChildWorkflowFut::Running { inner, .. } = self {
2859            inner.cancel()
2860        }
2861    }
2862}
2863
2864/// Wrapper future for starting a child workflow. Mirrors `ActivityFut` to allow returning
2865/// serialization errors eagerly.
2866enum ChildWorkflowStartFut<F, WD: WorkflowDefinition> {
2867    /// Immediate error (e.g., input serialization failure). Resolves on first poll.
2868    Errored {
2869        error: Option<Box<ChildWorkflowStartError>>,
2870        _phantom: PhantomData<WD>,
2871    },
2872    Running(F),
2873    Terminated,
2874}
2875
2876impl<F, WD: WorkflowDefinition> ChildWorkflowStartFut<F, WD> {
2877    fn eager(err: ChildWorkflowStartError) -> Self {
2878        Self::Errored {
2879            error: Some(Box::new(err)),
2880            _phantom: PhantomData,
2881        }
2882    }
2883}
2884
2885impl<F, WD: WorkflowDefinition> Unpin for ChildWorkflowStartFut<F, WD> where F: Unpin {}
2886
2887impl<F, WD> Future for ChildWorkflowStartFut<F, WD>
2888where
2889    F: Future<Output = PendingChildWorkflow<WD>> + Unpin,
2890    WD: WorkflowDefinition,
2891{
2892    type Output = StartChildWorkflowResult;
2893
2894    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
2895        let this = self.get_mut();
2896        let poll = match this {
2897            ChildWorkflowStartFut::Errored { error, .. } => {
2898                Poll::Ready(Err(*error.take().expect("polled after completion")))
2899            }
2900            ChildWorkflowStartFut::Running(inner) => match Pin::new(inner).poll(cx) {
2901                Poll::Pending => Poll::Pending,
2902                Poll::Ready(pending) => Poll::Ready(match pending.status {
2903                    ChildWorkflowStartStatus::Succeeded(s) => {
2904                        let ChildWfCommon {
2905                            workflow_id,
2906                            child_seq,
2907                            result_future,
2908                            base_ctx,
2909                        } = pending.common;
2910                        Ok(StartChildWorkflowOutput {
2911                            run_id: s.run_id,
2912                            result_future,
2913                            workflow_id,
2914                            child_seq,
2915                            base_ctx,
2916                        })
2917                    }
2918                    ChildWorkflowStartStatus::Failed(f) => {
2919                        let mut result_future = pending.common.result_future;
2920                        result_future.unregister_cancellation();
2921                        Err(ChildWorkflowStartError::StartFailed {
2922                            workflow_id: f.workflow_id,
2923                            workflow_type: f.workflow_type,
2924                            cause: StartChildWorkflowExecutionFailedCause::try_from(f.cause)
2925                                .unwrap_or(StartChildWorkflowExecutionFailedCause::Unspecified),
2926                        })
2927                    }
2928                    ChildWorkflowStartStatus::Cancelled(c) => {
2929                        let ChildWfCommon {
2930                            mut result_future,
2931                            base_ctx,
2932                            ..
2933                        } = pending.common;
2934                        result_future.unregister_cancellation();
2935                        Err(base_ctx.data_converter().to_error(
2936                            &SerializationContextData::Workflow,
2937                            c.failure.unwrap_or_default(),
2938                            ChildWorkflowStartDecodeHint,
2939                        )?)
2940                    }
2941                }),
2942            },
2943            ChildWorkflowStartFut::Terminated => panic!("polled after termination"),
2944        };
2945        if poll.is_ready() {
2946            *this = ChildWorkflowStartFut::Terminated;
2947        }
2948        poll
2949    }
2950}
2951
2952impl<F, WD> FusedFuture for ChildWorkflowStartFut<F, WD>
2953where
2954    F: Future<Output = PendingChildWorkflow<WD>> + Unpin,
2955    WD: WorkflowDefinition,
2956{
2957    fn is_terminated(&self) -> bool {
2958        matches!(self, ChildWorkflowStartFut::Terminated)
2959    }
2960}
2961
2962impl<F, WD> CancellableFuture for ChildWorkflowStartFut<F, WD>
2963where
2964    F: CancellableFutureWithReason<Output = PendingChildWorkflow<WD>> + Unpin,
2965    WD: WorkflowDefinition,
2966{
2967    fn cancel(&self) {
2968        if let ChildWorkflowStartFut::Running(inner) = self {
2969            inner.cancel()
2970        }
2971    }
2972}
2973
2974impl<F, WD> CancellableFutureWithReason for ChildWorkflowStartFut<F, WD>
2975where
2976    F: CancellableFutureWithReason<Output = PendingChildWorkflow<WD>> + Unpin,
2977    WD: WorkflowDefinition,
2978{
2979    fn cancel_with_reason(&self, reason: String) {
2980        if let ChildWorkflowStartFut::Running(inner) = self {
2981            inner.cancel_with_reason(reason)
2982        }
2983    }
2984}
2985
2986/// Wrapper future for signaling a child workflow.
2987enum SignalChildFut<F> {
2988    Running {
2989        inner: F,
2990        data_converter: DataConverter,
2991    },
2992    Terminated,
2993}
2994
2995impl<F> Unpin for SignalChildFut<F> where F: Unpin {}
2996
2997impl<F> Future for SignalChildFut<F>
2998where
2999    F: Future<Output = SignalExternalWfResult> + Unpin,
3000{
3001    type Output = Result<(), WorkflowSignalError>;
3002
3003    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
3004        let this = self.get_mut();
3005        let poll = match this {
3006            SignalChildFut::Running {
3007                inner,
3008                data_converter,
3009            } => match Pin::new(inner).poll(cx) {
3010                Poll::Pending => Poll::Pending,
3011                Poll::Ready(Ok(_)) => Poll::Ready(Ok(())),
3012                Poll::Ready(Err(failure)) => Poll::Ready(Err(data_converter.to_error(
3013                    &SerializationContextData::Workflow,
3014                    failure,
3015                    WorkflowSignalDecodeHint,
3016                )?)),
3017            },
3018            SignalChildFut::Terminated => panic!("polled after termination"),
3019        };
3020        if poll.is_ready() {
3021            *this = SignalChildFut::Terminated;
3022        }
3023        poll
3024    }
3025}
3026
3027impl<F> FusedFuture for SignalChildFut<F>
3028where
3029    F: Future<Output = SignalExternalWfResult> + Unpin,
3030{
3031    fn is_terminated(&self) -> bool {
3032        matches!(self, SignalChildFut::Terminated)
3033    }
3034}
3035
3036impl<F> CancellableFuture for SignalChildFut<F>
3037where
3038    F: CancellableFuture<Output = SignalExternalWfResult> + Unpin,
3039{
3040    fn cancel(&self) {
3041        if let SignalChildFut::Running { inner, .. } = self {
3042            inner.cancel()
3043        }
3044    }
3045}
3046
3047impl<WD: WorkflowDefinition> StartedChildWorkflow<WD>
3048where
3049    WD::Output: TemporalDeserializable + 'static,
3050{
3051    /// Consumes self and returns a future that deserializes the child workflow result
3052    /// into `WD::Output`.
3053    pub fn result(
3054        self,
3055    ) -> impl CancellableFutureWithReason<Output = Result<WD::Output, ChildWorkflowExecutionError>>
3056    {
3057        self.result_future.map(|result| {
3058            result.and_then(|output| {
3059                output
3060                    .downcast::<WD::Output>()
3061                    .map(|output| *output)
3062                    .map_err(|_| {
3063                        ChildWorkflowExecutionError::Serialization(outbound_type_error(
3064                            "child workflow output",
3065                        ))
3066                    })
3067            })
3068        })
3069    }
3070
3071    /// Cancel the child workflow
3072    pub fn cancel(&self, reason: String) {
3073        self.base_ctx.cancel(CancellableID::ChildWorkflow {
3074            seqnum: self.child_seq,
3075            reason,
3076        });
3077    }
3078
3079    /// Send a typed signal to the child workflow.
3080    ///
3081    /// By default, the signal inherits workflow cancellation.
3082    pub fn signal<S: SignalDefinition<Workflow = WD> + 'static>(
3083        &self,
3084        signal: S,
3085        input: S::Input,
3086        options: SignalWorkflowOptions,
3087    ) -> impl CancellableFuture<Output = Result<(), WorkflowSignalError>> + 'static {
3088        self.base_ctx.signal_workflow(
3089            SignalWorkflowTarget::Child {
3090                workflow_id: self.workflow_id.clone(),
3091            },
3092            signal,
3093            input,
3094            options,
3095        )
3096    }
3097}
3098
3099/// Handle to an external workflow for sending signals or requesting cancellation.
3100///
3101/// Obtained via [`SyncWorkflowContext::external_workflow`],
3102/// [`WorkflowContext::external_workflow`], or
3103/// [`WorkflowInterceptorContext::external_workflow`].
3104#[derive(derive_more::Debug)]
3105pub struct ExternalWorkflowHandle {
3106    workflow_id: String,
3107    run_id: Option<String>,
3108    namespace: String,
3109    #[debug(skip)]
3110    base_ctx: BaseWorkflowContext,
3111}
3112
3113impl ExternalWorkflowHandle {
3114    /// The workflow ID of the external workflow.
3115    pub fn workflow_id(&self) -> &str {
3116        &self.workflow_id
3117    }
3118
3119    /// The run ID of the external workflow, or `None` if targeting the latest run.
3120    pub fn run_id(&self) -> Option<&str> {
3121        self.run_id.as_deref()
3122    }
3123
3124    /// Send a signal to the external workflow.
3125    ///
3126    /// By default, the signal inherits workflow cancellation.
3127    pub fn signal<S: SignalDefinition + 'static>(
3128        &self,
3129        signal: S,
3130        input: S::Input,
3131        options: SignalWorkflowOptions,
3132    ) -> impl CancellableFuture<Output = Result<(), WorkflowSignalError>> + 'static {
3133        self.base_ctx.signal_workflow(
3134            SignalWorkflowTarget::External {
3135                namespace: self.namespace.clone(),
3136                workflow_id: self.workflow_id.clone(),
3137                run_id: self.run_id.clone(),
3138            },
3139            signal,
3140            input,
3141            options,
3142        )
3143    }
3144
3145    /// Request cancellation of the external workflow.
3146    pub fn cancel(
3147        &self,
3148        reason: Option<String>,
3149    ) -> impl FusedFuture<Output = CancelExternalWfResult> {
3150        self.base_ctx
3151            .cancel_external_workflow(CancelExternalWorkflowInput {
3152                workflow_id: self.workflow_id.clone(),
3153                run_id: self.run_id.clone(),
3154                reason,
3155            })
3156    }
3157}
3158
3159#[derive(derive_more::Debug)]
3160#[debug("StartedNexusOperation{{ operation_token: {operation_token:?} }}")]
3161/// Handle to a started Nexus operation.
3162pub struct StartedNexusOperation {
3163    /// The operation token, if the operation started asynchronously
3164    pub operation_token: Option<String>,
3165    #[debug(skip)]
3166    pub(crate) result_future: Shared<CancellableWorkflowOutboundFuture<NexusOperationResult>>,
3167    pub(crate) schedule_seq: u32,
3168    #[debug(skip)]
3169    pub(crate) base_ctx: BaseWorkflowContext,
3170}
3171
3172pub(crate) struct NexusUnblockData {
3173    pub(crate) result_future: Shared<CancellableWorkflowOutboundFuture<NexusOperationResult>>,
3174    pub(crate) schedule_seq: u32,
3175    pub(crate) base_ctx: BaseWorkflowContext,
3176}
3177
3178impl StartedNexusOperation {
3179    /// Wait for the operation result.
3180    pub async fn result(&self) -> NexusOperationResult {
3181        // The result future is a `Shared`; poll it inside an `SdkWakeGuard` (via
3182        // `SdkGuardedFuture`) so its internal waker machinery isn't mistaken for a non-SDK wake on
3183        // replay (which would fail the workflow task with TMPRL1100).
3184        SdkGuardedFuture(self.result_future.clone()).await
3185    }
3186
3187    /// Request cancellation of the operation.
3188    pub fn cancel(&self) {
3189        self.base_ctx
3190            .cancel(CancellableID::NexusOp(self.schedule_seq));
3191    }
3192}
3193
3194#[cfg(test)]
3195mod tests {
3196    use super::*;
3197    use crate::MemoValues;
3198    use std::{
3199        collections::HashMap,
3200        sync::{
3201            Mutex,
3202            atomic::{AtomicUsize, Ordering as AtomicOrdering},
3203        },
3204        task::Wake,
3205        time::Duration,
3206    };
3207    use temporalio_common_wasm::{
3208        RetryPolicy,
3209        data_converters::{TemporalDeserializable, TemporalSerializable},
3210        error::OutgoingWorkflowError,
3211        protos::{
3212            coresdk::{
3213                AsJsonPayloadExt, FromJsonPayloadExt,
3214                common::VersioningIntent as ProtoVersioningIntent,
3215                workflow_activation::{
3216                    ResolveChildWorkflowExecutionStartSuccess, UpdateRandomSeed,
3217                    WorkflowActivationJob, resolve_nexus_operation_start,
3218                },
3219                workflow_commands::WorkflowCommand,
3220            },
3221            temporal::api::{
3222                common::v1::{Payload, RetryPolicy as ProtoRetryPolicy},
3223                enums::v1::ContinueAsNewVersioningBehavior as ProtoContinueAsNewVersioningBehavior,
3224            },
3225        },
3226    };
3227    use temporalio_macros::{workflow, workflow_methods};
3228
3229    #[derive(Default)]
3230    struct NoopHost;
3231
3232    struct CountingWake(Arc<AtomicUsize>);
3233
3234    impl Wake for CountingWake {
3235        fn wake(self: Arc<Self>) {
3236            self.wake_by_ref();
3237        }
3238
3239        fn wake_by_ref(self: &Arc<Self>) {
3240            self.0.fetch_add(1, AtomicOrdering::Relaxed);
3241        }
3242    }
3243
3244    impl WorkflowHost for NoopHost {
3245        fn set_current_details(&self, _details: String) {}
3246        fn push_command(&self, _command: WorkflowCommand) {}
3247    }
3248
3249    #[derive(Default)]
3250    struct RecordingHost {
3251        commands: Rc<RefCell<Vec<WorkflowCommand>>>,
3252    }
3253
3254    impl WorkflowHost for RecordingHost {
3255        fn set_current_details(&self, _details: String) {}
3256
3257        fn push_command(&self, command: WorkflowCommand) {
3258            self.commands.borrow_mut().push(command);
3259        }
3260    }
3261
3262    #[derive(Debug)]
3263    struct FailingMemoValue;
3264
3265    impl TemporalSerializable for FailingMemoValue {
3266        fn to_payload(
3267            &self,
3268            _ctx: &temporalio_common_wasm::data_converters::SerializationContext<'_>,
3269        ) -> Result<Payload, temporalio_common_wasm::data_converters::PayloadConversionError>
3270        {
3271            Err(
3272                temporalio_common_wasm::data_converters::PayloadConversionError::EncodingError(
3273                    std::io::Error::other("memo serialization failure").into(),
3274                ),
3275            )
3276        }
3277    }
3278
3279    #[workflow]
3280    #[derive(Default)]
3281    struct TestWorkflow;
3282
3283    #[workflow_methods]
3284    impl TestWorkflow {
3285        #[run]
3286        async fn run(_ctx: &mut WorkflowContext<Self>, _input: u8) -> crate::WorkflowResult<()> {
3287            unreachable!("test workflow run should not be polled")
3288        }
3289
3290        #[signal]
3291        fn test_signal(&mut self, _ctx: &mut SyncWorkflowContext<Self>, _input: String) {
3292            unreachable!("test workflow signal should not be dispatched")
3293        }
3294    }
3295
3296    struct TestActivity;
3297
3298    impl ActivityDefinition for TestActivity {
3299        type Input = ();
3300        type Output = ();
3301
3302        fn name(&self) -> &str {
3303            "test_activity"
3304        }
3305    }
3306
3307    fn test_context() -> WorkflowContext<TestWorkflow> {
3308        test_context_with_seed(0)
3309    }
3310
3311    fn test_context_with_seed(randomness_seed: u64) -> WorkflowContext<TestWorkflow> {
3312        let init = InitializeWorkflow {
3313            workflow_type: TestWorkflow.name().to_string(),
3314            randomness_seed,
3315            ..Default::default()
3316        };
3317        let init = WorkflowInit {
3318            namespace: "default".to_string(),
3319            task_queue: "orig-task-queue".to_string(),
3320            run_id: "run-id".to_string(),
3321            initialize_workflow: init,
3322        };
3323        let base = BaseWorkflowContext::from_raw(
3324            init,
3325            DataConverter::default(),
3326            Rc::new(NoopHost),
3327            None,
3328            Vec::new(),
3329        );
3330        WorkflowContext::from_base(base, Rc::new(RefCell::new(TestWorkflow)))
3331    }
3332
3333    fn patch_test_context(
3334        callback: Option<PatchActivationCallback>,
3335    ) -> (
3336        BaseWorkflowContext,
3337        WorkflowContext<TestWorkflow>,
3338        Rc<RefCell<Vec<WorkflowCommand>>>,
3339    ) {
3340        let init = InitializeWorkflow {
3341            workflow_id: "workflow-id".to_string(),
3342            workflow_type: TestWorkflow.name().to_string(),
3343            ..Default::default()
3344        };
3345        let host = Rc::new(RecordingHost::default());
3346        let commands = host.commands.clone();
3347        let init = WorkflowInit {
3348            namespace: "default".to_string(),
3349            task_queue: "task-queue".to_string(),
3350            run_id: "run-id".to_string(),
3351            initialize_workflow: init,
3352        };
3353        let base = BaseWorkflowContext::from_raw(
3354            init,
3355            DataConverter::default(),
3356            host,
3357            callback,
3358            Vec::new(),
3359        );
3360
3361        let ctx = WorkflowContext::from_base(base.clone(), Rc::new(RefCell::new(TestWorkflow)));
3362        (base, ctx, commands)
3363    }
3364
3365    struct ShortCircuitFirstTimer {
3366        calls: AtomicUsize,
3367    }
3368
3369    impl WorkflowInterceptor for ShortCircuitFirstTimer {
3370        fn start_timer(
3371            &self,
3372            _ctx: WorkflowInterceptorContext,
3373            input: StartTimerInput,
3374            next: WorkflowNext<
3375                'static,
3376                StartTimerInput,
3377                CancellableWorkflowOutboundFuture<TimerResult>,
3378            >,
3379        ) -> CancellableWorkflowOutboundFuture<TimerResult> {
3380            if self.calls.fetch_add(1, Ordering::Relaxed) == 0 {
3381                CancellableWorkflowOutboundFuture::new(
3382                    async { TimerResult::Cancelled },
3383                    WorkflowCancellationHandle::new(|_| {}),
3384                )
3385            } else {
3386                next.run(input)
3387            }
3388        }
3389    }
3390
3391    #[test]
3392    fn short_circuited_outbound_call_does_not_consume_sequence_number() {
3393        let host = Rc::new(RecordingHost::default());
3394        let init = InitializeWorkflow {
3395            workflow_type: TestWorkflow.name().to_string(),
3396            ..Default::default()
3397        };
3398        let init = WorkflowInit {
3399            namespace: "default".to_string(),
3400            task_queue: "task-queue".to_string(),
3401            run_id: "run-id".to_string(),
3402            initialize_workflow: init,
3403        };
3404        let base = BaseWorkflowContext::from_raw(
3405            init,
3406            DataConverter::default(),
3407            host.clone(),
3408            None,
3409            vec![WorkflowInterceptorConstructor::new(|_| {
3410                ShortCircuitFirstTimer {
3411                    calls: AtomicUsize::new(0),
3412                }
3413            })],
3414        );
3415
3416        let first = base.timer(Duration::from_secs(1));
3417        assert_eq!(first.now_or_never(), Some(TimerResult::Cancelled));
3418        let _second = base.timer(Duration::from_secs(1));
3419
3420        let commands = host.commands.borrow();
3421        assert_eq!(commands.len(), 1);
3422        let Some(workflow_command::Variant::StartTimer(timer)) = &commands[0].variant else {
3423            panic!("expected start timer command");
3424        };
3425        assert_eq!(timer.seq, 1);
3426    }
3427
3428    #[test]
3429    fn custom_token_cancels_command_backed_operations() {
3430        let host = Rc::new(RecordingHost::default());
3431        let init = WorkflowInit {
3432            namespace: "default".to_string(),
3433            task_queue: "task-queue".to_string(),
3434            run_id: "run-id".to_string(),
3435            initialize_workflow: InitializeWorkflow {
3436                workflow_type: TestWorkflow.name().to_string(),
3437                ..Default::default()
3438            },
3439        };
3440        let base = BaseWorkflowContext::from_raw(
3441            init,
3442            DataConverter::default(),
3443            host.clone(),
3444            None,
3445            Vec::new(),
3446        );
3447        let token = WorkflowCancellationToken::new();
3448
3449        let timer = base.timer(TimerOptions {
3450            duration: Duration::from_secs(1),
3451            cancellation_token: Some(token.clone()),
3452            summary: None,
3453            event_group_markers: vec![],
3454        });
3455
3456        let mut activity_options = ActivityOptions::start_to_close_timeout(Duration::from_secs(1));
3457        activity_options.cancellation_token = Some(token.clone());
3458        let activity = base.execute_activity(TestActivity, (), activity_options);
3459
3460        let mut local_activity_options = LocalActivityOptions {
3461            schedule_to_close_timeout: Some(Duration::from_secs(1)),
3462            ..Default::default()
3463        };
3464        local_activity_options.cancellation_token = Some(token.clone());
3465        let local_activity = base.execute_local_activity(TestActivity, (), local_activity_options);
3466
3467        let child_options = ChildWorkflowOptions {
3468            cancellation_token: Some(token.clone()),
3469            ..Default::default()
3470        };
3471        let child = base.start_child_workflow(TestWorkflow::run, 1, child_options);
3472
3473        let signal = base.external_workflow("external", None).signal(
3474            TestWorkflow::test_signal,
3475            "input".to_string(),
3476            SignalWorkflowOptions::builder()
3477                .cancellation_token(token.clone())
3478                .build(),
3479        );
3480
3481        let nexus_options = NexusOperationOptions::builder()
3482            .endpoint("endpoint")
3483            .service("service")
3484            .operation("operation")
3485            .cancellation_token(token.clone())
3486            .build();
3487        let nexus = base.start_nexus_operation(nexus_options);
3488
3489        token.cancel_with_reason("group cancelled");
3490        timer.cancel();
3491        activity.cancel();
3492        local_activity.cancel();
3493        child.cancel_with_reason("explicit cancellation".to_string());
3494        signal.cancel();
3495        nexus.cancel();
3496
3497        let commands = host.commands.borrow();
3498        assert_eq!(
3499            commands
3500                .iter()
3501                .filter(|command| matches!(
3502                    &command.variant,
3503                    Some(workflow_command::Variant::CancelTimer(_))
3504                ))
3505                .count(),
3506            1
3507        );
3508        assert_eq!(
3509            commands
3510                .iter()
3511                .filter(|command| matches!(
3512                    &command.variant,
3513                    Some(workflow_command::Variant::RequestCancelActivity(_))
3514                ))
3515                .count(),
3516            1
3517        );
3518        assert_eq!(
3519            commands
3520                .iter()
3521                .filter(|command| matches!(
3522                    &command.variant,
3523                    Some(workflow_command::Variant::RequestCancelLocalActivity(_))
3524                ))
3525                .count(),
3526            1
3527        );
3528        let child_cancellations = commands
3529            .iter()
3530            .filter_map(|command| match &command.variant {
3531                Some(workflow_command::Variant::CancelChildWorkflowExecution(cancel)) => {
3532                    Some(cancel)
3533                }
3534                _ => None,
3535            })
3536            .collect::<Vec<_>>();
3537        assert_eq!(child_cancellations.len(), 1);
3538        assert_eq!(child_cancellations[0].reason, "group cancelled");
3539        assert_eq!(
3540            commands
3541                .iter()
3542                .filter(|command| matches!(
3543                    &command.variant,
3544                    Some(workflow_command::Variant::CancelSignalWorkflow(_))
3545                ))
3546                .count(),
3547            1
3548        );
3549        assert_eq!(
3550            commands
3551                .iter()
3552                .filter(|command| matches!(
3553                    &command.variant,
3554                    Some(workflow_command::Variant::RequestCancelNexusOperation(_))
3555                ))
3556                .count(),
3557            1
3558        );
3559    }
3560
3561    #[test]
3562    fn child_and_nexus_tokens_remain_active_after_start() {
3563        let host = Rc::new(RecordingHost::default());
3564        let init = WorkflowInit {
3565            namespace: "default".to_string(),
3566            task_queue: "task-queue".to_string(),
3567            run_id: "run-id".to_string(),
3568            initialize_workflow: InitializeWorkflow {
3569                workflow_type: TestWorkflow.name().to_string(),
3570                ..Default::default()
3571            },
3572        };
3573        let base = BaseWorkflowContext::from_raw(
3574            init,
3575            DataConverter::default(),
3576            host.clone(),
3577            None,
3578            Vec::new(),
3579        );
3580
3581        let child_token = WorkflowCancellationToken::new();
3582        let child_options = ChildWorkflowOptions {
3583            cancellation_token: Some(child_token.clone()),
3584            ..Default::default()
3585        };
3586        let child = base.start_child_workflow(TestWorkflow::run, 1, child_options);
3587        base.unblock(UnblockEvent::WorkflowStart(
3588            1,
3589            Box::new(ChildWorkflowStartStatus::Succeeded(
3590                ResolveChildWorkflowExecutionStartSuccess {
3591                    run_id: "child-run".to_string(),
3592                },
3593            )),
3594        ))
3595        .unwrap();
3596        let started_child = child
3597            .now_or_never()
3598            .expect("child start should resolve")
3599            .unwrap();
3600        child_token.cancel();
3601        started_child.cancel("explicit cancellation".to_string());
3602
3603        let nexus_token = WorkflowCancellationToken::new();
3604        let nexus_options = NexusOperationOptions::builder()
3605            .endpoint("endpoint")
3606            .service("service")
3607            .operation("operation")
3608            .cancellation_token(nexus_token.clone())
3609            .build();
3610        let nexus = base.start_nexus_operation(nexus_options);
3611        base.unblock(UnblockEvent::NexusOperationStart(
3612            1,
3613            Box::new(resolve_nexus_operation_start::Status::OperationToken(
3614                "operation-token".to_string(),
3615            )),
3616        ))
3617        .unwrap();
3618        let started_nexus = nexus
3619            .now_or_never()
3620            .expect("Nexus start should resolve")
3621            .unwrap();
3622        nexus_token.cancel();
3623        started_nexus.cancel();
3624
3625        let commands = host.commands.borrow();
3626        assert_eq!(
3627            commands
3628                .iter()
3629                .filter(|command| matches!(
3630                    &command.variant,
3631                    Some(workflow_command::Variant::CancelChildWorkflowExecution(_))
3632                ))
3633                .count(),
3634            1
3635        );
3636        assert_eq!(
3637            commands
3638                .iter()
3639                .filter(|command| matches!(
3640                    &command.variant,
3641                    Some(workflow_command::Variant::RequestCancelNexusOperation(_))
3642                ))
3643                .count(),
3644            1
3645        );
3646    }
3647
3648    #[test]
3649    fn local_activity_token_cancels_retry_backoff_timer() {
3650        let host = Rc::new(RecordingHost::default());
3651        let init = WorkflowInit {
3652            namespace: "default".to_string(),
3653            task_queue: "task-queue".to_string(),
3654            run_id: "run-id".to_string(),
3655            initialize_workflow: InitializeWorkflow {
3656                workflow_type: TestWorkflow.name().to_string(),
3657                ..Default::default()
3658            },
3659        };
3660        let base = BaseWorkflowContext::from_raw(
3661            init,
3662            DataConverter::default(),
3663            host.clone(),
3664            None,
3665            Vec::new(),
3666        );
3667        let token = WorkflowCancellationToken::new();
3668        let mut options = LocalActivityOptions {
3669            schedule_to_close_timeout: Some(Duration::from_secs(10)),
3670            ..Default::default()
3671        };
3672        options.cancellation_token = Some(token.clone());
3673        let activity = base.execute_local_activity(TestActivity, (), options);
3674        futures_util::pin_mut!(activity);
3675        base.unblock(UnblockEvent::Activity(
3676            1,
3677            Box::new(ActivityResolution {
3678                status: Some(activity_resolution::Status::Backoff(
3679                    temporalio_common_wasm::protos::coresdk::activity_result::DoBackoff {
3680                        attempt: 2,
3681                        backoff_duration: Some(Duration::from_secs(5).try_into().unwrap()),
3682                        original_schedule_time: None,
3683                    },
3684                )),
3685            }),
3686        ))
3687        .unwrap();
3688
3689        assert!(activity.as_mut().now_or_never().is_none());
3690        token.cancel();
3691
3692        let commands = host.commands.borrow();
3693        assert!(commands.iter().any(|command| matches!(
3694            &command.variant,
3695            Some(workflow_command::Variant::StartTimer(_))
3696        )));
3697        assert!(commands.iter().any(|command| matches!(
3698            &command.variant,
3699            Some(workflow_command::Variant::CancelTimer(_))
3700        )));
3701    }
3702
3703    #[test]
3704    fn patch_activation_callback_activates_and_memoizes() {
3705        let calls = Arc::new(AtomicUsize::new(0));
3706        let input = Arc::new(Mutex::new(None));
3707        let callback_calls = calls.clone();
3708        let callback_input = input.clone();
3709        let callback: PatchActivationCallback = Arc::new(move |value| {
3710            callback_calls.fetch_add(1, AtomicOrdering::Relaxed);
3711            *callback_input.lock().unwrap() = Some(value);
3712            true
3713        });
3714        let (_, ctx, commands) = patch_test_context(Some(callback));
3715
3716        assert!(ctx.patched("my-patch"));
3717        assert!(ctx.patched("my-patch"));
3718        assert_eq!(calls.load(AtomicOrdering::Relaxed), 1);
3719        assert_eq!(commands.borrow().len(), 1);
3720        let input = input.lock().unwrap();
3721        let input = input.as_ref().unwrap();
3722        assert_eq!(input.workflow_info.workflow_id(), "workflow-id");
3723        assert_eq!(input.workflow_info.run_id(), "run-id");
3724        assert_eq!(input.patch_id, "my-patch");
3725    }
3726
3727    #[test]
3728    fn patch_activation_callback_can_decline_and_memoizes() {
3729        let calls = Arc::new(AtomicUsize::new(0));
3730        let callback_calls = calls.clone();
3731        let callback: PatchActivationCallback = Arc::new(move |_| {
3732            callback_calls.fetch_add(1, AtomicOrdering::Relaxed);
3733            false
3734        });
3735        let (_, ctx, commands) = patch_test_context(Some(callback));
3736
3737        assert!(!ctx.patched("my-patch"));
3738        assert!(!ctx.patched("my-patch"));
3739        assert_eq!(calls.load(AtomicOrdering::Relaxed), 1);
3740        assert!(commands.borrow().is_empty());
3741    }
3742
3743    #[test]
3744    fn patch_activation_callback_bypasses_history_and_deprecation() {
3745        let callback: PatchActivationCallback = Arc::new(|_| panic!("callback must not run"));
3746
3747        let (base, ctx, commands) = patch_test_context(Some(callback.clone()));
3748        base.apply_activation_context(
3749            &CoreWorkflowActivation {
3750                is_replaying: true,
3751                ..Default::default()
3752            },
3753            true,
3754        );
3755        assert!(!ctx.patched("replay-patch"));
3756        assert!(commands.borrow().is_empty());
3757
3758        let (base, ctx, commands) = patch_test_context(Some(callback.clone()));
3759        base.apply_activation_context(
3760            &CoreWorkflowActivation {
3761                is_replaying: true,
3762                ..Default::default()
3763            },
3764            true,
3765        );
3766        base.notify_patch("existing-patch".to_string());
3767        assert!(ctx.patched("existing-patch"));
3768        assert_eq!(commands.borrow().len(), 1);
3769
3770        let (_, ctx, commands) = patch_test_context(Some(callback));
3771        assert!(ctx.deprecate_patch("deprecated-patch"));
3772        assert_eq!(commands.borrow().len(), 1);
3773    }
3774
3775    #[test]
3776    fn patch_activation_defaults_to_active() {
3777        let (_, ctx, commands) = patch_test_context(None);
3778
3779        assert!(ctx.patched("my-patch"));
3780        assert_eq!(commands.borrow().len(), 1);
3781    }
3782
3783    #[test]
3784    fn random_is_deterministic_for_supported_numeric_types() {
3785        let first = test_context_with_seed(42);
3786        let second = test_context_with_seed(42);
3787
3788        assert_eq!(first.random::<u8>(), second.random::<u8>());
3789        assert_eq!(first.random::<i64>(), second.random::<i64>());
3790        assert_eq!(first.random::<u128>(), second.random::<u128>());
3791        assert_eq!(first.random::<f32>(), second.random::<f32>());
3792        assert_eq!(first.random::<f64>(), second.random::<f64>());
3793        assert_eq!(first.uuid4(), second.uuid4());
3794    }
3795
3796    #[test]
3797    fn random_is_reseeded_by_activation() {
3798        let ctx = test_context_with_seed(123);
3799        let expected = ctx.random::<u64>();
3800        let activation = CoreWorkflowActivation {
3801            jobs: vec![WorkflowActivationJob {
3802                variant: Some(ActivationVariant::UpdateRandomSeed(UpdateRandomSeed {
3803                    randomness_seed: 123,
3804                })),
3805            }],
3806            ..Default::default()
3807        };
3808
3809        ctx.sync.base.apply_activation_context(&activation, false);
3810
3811        assert_eq!(ctx.random::<u64>(), expected);
3812    }
3813
3814    struct MutatingRemainingOutboundInterceptor;
3815
3816    impl WorkflowInterceptor for MutatingRemainingOutboundInterceptor {
3817        fn signal_workflow(
3818            &self,
3819            _ctx: WorkflowInterceptorContext,
3820            mut input: SignalWorkflowInput,
3821            next: WorkflowNext<
3822                'static,
3823                SignalWorkflowInput,
3824                CancellableWorkflowOutboundFuture<SignalWorkflowResult>,
3825            >,
3826        ) -> CancellableWorkflowOutboundFuture<SignalWorkflowResult> {
3827            *input.signal_name_mut() = "mutated-signal".to_string();
3828            *input.input_mut::<String>().unwrap() = "mutated-input".to_string();
3829            *input.target_mut() = SignalWorkflowTarget::External {
3830                namespace: "mutated-namespace".to_string(),
3831                workflow_id: "mutated-workflow".to_string(),
3832                run_id: Some("mutated-run".to_string()),
3833            };
3834            input
3835                .headers_mut()
3836                .insert("signal-header".to_string(), Payload::default());
3837            next.run(input)
3838        }
3839
3840        fn cancel_external_workflow(
3841            &self,
3842            _ctx: WorkflowInterceptorContext,
3843            mut input: CancelExternalWorkflowInput,
3844            next: WorkflowNext<
3845                'static,
3846                CancelExternalWorkflowInput,
3847                WorkflowOutboundFuture<CancelExternalWfResult>,
3848            >,
3849        ) -> WorkflowOutboundFuture<CancelExternalWfResult> {
3850            input.workflow_id = "mutated-cancel-workflow".to_string();
3851            input.run_id = Some("mutated-cancel-run".to_string());
3852            input.reason = Some("mutated-reason".to_string());
3853            next.run(input)
3854        }
3855
3856        fn continue_as_new(
3857            &self,
3858            _ctx: crate::workflow_interceptors::SyncWorkflowInterceptorContext,
3859            mut input: ContinueAsNewInput,
3860            next: WorkflowNext<
3861                'static,
3862                ContinueAsNewInput,
3863                crate::workflow_interceptors::ContinueAsNewResult,
3864            >,
3865        ) -> crate::workflow_interceptors::ContinueAsNewResult {
3866            *input.input_mut::<u8>().unwrap() = 42;
3867            input.options_mut().workflow_type = Some("mutated-workflow-type".to_string());
3868            input.headers_mut().insert(
3869                "continue-header".to_string(),
3870                Payload::from(b"continue-header-value".as_slice()),
3871            );
3872            next.run(input)
3873        }
3874
3875        fn start_nexus_operation(
3876            &self,
3877            _ctx: WorkflowInterceptorContext,
3878            mut input: StartNexusOperationInput,
3879            next: WorkflowNext<
3880                'static,
3881                StartNexusOperationInput,
3882                CancellableWorkflowOutboundFuture<
3883                    crate::workflow_interceptors::StartNexusOperationResult,
3884                >,
3885            >,
3886        ) -> CancellableWorkflowOutboundFuture<
3887            crate::workflow_interceptors::StartNexusOperationResult,
3888        > {
3889            input.options_mut().endpoint = "mutated-endpoint".to_string();
3890            input.options_mut().service = "mutated-service".to_string();
3891            input.options_mut().operation = "mutated-operation".to_string();
3892            next.run(input)
3893        }
3894    }
3895
3896    #[test]
3897    fn outbound_interceptors_mutate_signal_cancel_continue_as_new_and_nexus() {
3898        let host = Rc::new(RecordingHost::default());
3899        let init = InitializeWorkflow {
3900            workflow_type: TestWorkflow.name().to_string(),
3901            ..Default::default()
3902        };
3903        let init = WorkflowInit {
3904            namespace: "default".to_string(),
3905            task_queue: "task-queue".to_string(),
3906            run_id: "run-id".to_string(),
3907            initialize_workflow: init,
3908        };
3909        let base = BaseWorkflowContext::from_raw(
3910            init,
3911            DataConverter::default(),
3912            host.clone(),
3913            None,
3914            vec![WorkflowInterceptorConstructor::new(|_| {
3915                MutatingRemainingOutboundInterceptor
3916            })],
3917        );
3918        let ctx = WorkflowContext::from_base(base, Rc::new(RefCell::new(TestWorkflow)));
3919
3920        let signal = ctx
3921            .external_workflow("original-workflow", Some("original-run".to_string()))
3922            .signal(
3923                TestWorkflow::test_signal,
3924                "original-input".to_string(),
3925                Default::default(),
3926            );
3927        let cancel_target =
3928            ctx.external_workflow("cancel-workflow", Some("cancel-run".to_string()));
3929        let cancel = cancel_target.cancel(Some("original-reason".to_string()));
3930        let termination = ctx
3931            .continue_as_new(7, ContinueAsNewOptions::default())
3932            .expect_err("continue_as_new should terminate the workflow");
3933        let sync_ctx = ctx.sync_context();
3934        let nexus = sync_ctx.start_nexus_operation(
3935            NexusOperationOptions::builder()
3936                .endpoint("original-endpoint")
3937                .service("original-service")
3938                .operation("original-operation")
3939                .build(),
3940        );
3941        drop((signal, cancel, nexus));
3942
3943        let WorkflowTermination::ContinueAsNew(continue_as_new) = termination else {
3944            panic!("expected continue-as-new termination")
3945        };
3946        assert_eq!(continue_as_new.workflow_type, "mutated-workflow-type");
3947        assert_eq!(
3948            continue_as_new.arguments,
3949            vec![42u8.as_json_payload().unwrap()]
3950        );
3951        assert!(continue_as_new.headers.contains_key("continue-header"));
3952
3953        let commands = host.commands.borrow();
3954        assert_eq!(commands.len(), 3);
3955        let Some(workflow_command::Variant::SignalExternalWorkflowExecution(signal)) =
3956            &commands[0].variant
3957        else {
3958            panic!("expected signal command")
3959        };
3960        assert_eq!(signal.signal_name, "mutated-signal");
3961        assert_eq!(
3962            signal.args,
3963            vec!["mutated-input".to_string().as_json_payload().unwrap()]
3964        );
3965        assert!(signal.headers.contains_key("signal-header"));
3966        let Some(signal_external_workflow_execution::Target::WorkflowExecution(target)) =
3967            &signal.target
3968        else {
3969            panic!("expected external workflow signal target")
3970        };
3971        assert_eq!(target.namespace, "mutated-namespace");
3972        assert_eq!(target.workflow_id, "mutated-workflow");
3973        assert_eq!(target.run_id, "mutated-run");
3974
3975        let Some(workflow_command::Variant::RequestCancelExternalWorkflowExecution(cancel)) =
3976            &commands[1].variant
3977        else {
3978            panic!("expected external cancellation command")
3979        };
3980        let target = cancel.workflow_execution.as_ref().unwrap();
3981        assert_eq!(target.workflow_id, "mutated-cancel-workflow");
3982        assert_eq!(target.run_id, "mutated-cancel-run");
3983        assert_eq!(cancel.reason, "mutated-reason");
3984
3985        let Some(workflow_command::Variant::ScheduleNexusOperation(nexus)) = &commands[2].variant
3986        else {
3987            panic!("expected Nexus operation command")
3988        };
3989        assert_eq!(nexus.endpoint, "mutated-endpoint");
3990        assert_eq!(nexus.service, "mutated-service");
3991        assert_eq!(nexus.operation, "mutated-operation");
3992    }
3993
3994    #[test]
3995    fn continue_as_new_interceptor_header_reaches_proto_command() {
3996        let init = InitializeWorkflow {
3997            workflow_type: TestWorkflow.name().to_string(),
3998            ..Default::default()
3999        };
4000        let init = WorkflowInit {
4001            namespace: "default".to_string(),
4002            task_queue: "task-queue".to_string(),
4003            run_id: "run-id".to_string(),
4004            initialize_workflow: init,
4005        };
4006        let base = BaseWorkflowContext::from_raw(
4007            init,
4008            DataConverter::default(),
4009            Rc::new(NoopHost),
4010            None,
4011            vec![WorkflowInterceptorConstructor::new(|_| {
4012                MutatingRemainingOutboundInterceptor
4013            })],
4014        );
4015        let ctx = WorkflowContext::from_base(base, Rc::new(RefCell::new(TestWorkflow)));
4016
4017        let termination = ctx
4018            .continue_as_new(7, ContinueAsNewOptions::default())
4019            .expect_err("continue_as_new should terminate the workflow");
4020        let WorkflowTermination::ContinueAsNew(proto_command) = termination else {
4021            panic!("expected continue-as-new termination")
4022        };
4023
4024        assert_eq!(
4025            proto_command.headers,
4026            HashMap::from([(
4027                "continue-header".to_string(),
4028                Payload::from(b"continue-header-value".as_slice()),
4029            )])
4030        );
4031    }
4032
4033    #[test]
4034    fn construction_waker_uses_runtime_poll_waker() {
4035        let base = test_context().sync.base;
4036        let wakes = Arc::new(AtomicUsize::new(0));
4037        let waker = Waker::from(Arc::new(CountingWake(wakes.clone())));
4038        let _guard = base.enter_runtime_poll(&waker);
4039        base.construction_waker().wake_by_ref();
4040        assert_eq!(wakes.load(AtomicOrdering::Relaxed), 1);
4041    }
4042
4043    #[test]
4044    fn workflow_context_continue_as_new_serializes_input_and_defaults() {
4045        let ctx = test_context();
4046
4047        let termination = ctx
4048            .continue_as_new(7, ContinueAsNewOptions::default())
4049            .expect_err("continue_as_new should terminate the workflow");
4050        assert!(
4051            matches!(termination, WorkflowTermination::ContinueAsNew(_)),
4052            "expected continue-as-new termination, got {termination:?}"
4053        );
4054        let WorkflowTermination::ContinueAsNew(cmd) = termination else {
4055            unreachable!()
4056        };
4057
4058        assert_eq!(
4059            *cmd,
4060            crate::runtime::types::ContinueAsNewRequest {
4061                workflow_type: TestWorkflow.name().to_string(),
4062                task_queue: String::new(),
4063                arguments: vec![7u8.as_json_payload().unwrap()],
4064                workflow_run_timeout: None,
4065                workflow_task_timeout: None,
4066                backoff_start_interval: None,
4067                memo: HashMap::new(),
4068                headers: HashMap::new(),
4069                search_attributes: None,
4070                retry_policy: None,
4071                versioning_intent: ProtoVersioningIntent::Unspecified.into(),
4072                initial_versioning_behavior: ProtoContinueAsNewVersioningBehavior::Unspecified
4073                    .into(),
4074            }
4075        );
4076    }
4077
4078    #[test]
4079    fn sync_workflow_context_continue_as_new_applies_options() {
4080        let ctx = test_context();
4081        let sync = ctx.sync_context();
4082        let mut memo = MemoValues::new();
4083        memo.insert("memo-key", "memo-value".to_string());
4084        let mut proto_search_attributes = ProtoSearchAttributes::default();
4085        proto_search_attributes.indexed_fields.insert(
4086            "CustomKeywordField".to_string(),
4087            Payload::from(b"value".as_slice()),
4088        );
4089        let search_attributes = SearchAttributes::from_proto(&proto_search_attributes);
4090
4091        let termination = sync
4092            .continue_as_new(
4093                11,
4094                ContinueAsNewOptions {
4095                    workflow_type: Some("next-workflow".to_string()),
4096                    task_queue: Some("next-task-queue".to_string()),
4097                    run_timeout: Some(Duration::from_secs(10)),
4098                    task_timeout: Some(Duration::from_secs(3)),
4099                    backoff_start_interval: Some(Duration::from_secs(4)),
4100                    memo: Some(memo.clone()),
4101                    search_attributes: Some(search_attributes.clone()),
4102                    retry_policy: Some(RetryPolicy::builder().maximum_attempts(5).build()),
4103                    versioning_intent: Some(ProtoVersioningIntent::Compatible.into()),
4104                    initial_versioning_behavior: Some(
4105                        ContinueAsNewVersioningBehavior::UseRampingVersion,
4106                    ),
4107                },
4108            )
4109            .expect_err("continue_as_new should terminate the workflow");
4110        assert!(
4111            matches!(termination, WorkflowTermination::ContinueAsNew(_)),
4112            "expected continue-as-new termination, got {termination:?}"
4113        );
4114        let WorkflowTermination::ContinueAsNew(cmd) = termination else {
4115            unreachable!()
4116        };
4117
4118        assert_eq!(
4119            *cmd,
4120            crate::runtime::types::ContinueAsNewRequest {
4121                workflow_type: "next-workflow".to_string(),
4122                task_queue: "next-task-queue".to_string(),
4123                arguments: vec![11u8.as_json_payload().unwrap()],
4124                workflow_run_timeout: Some(Duration::from_secs(10).try_into().unwrap()),
4125                workflow_task_timeout: Some(Duration::from_secs(3).try_into().unwrap()),
4126                backoff_start_interval: Some(Duration::from_secs(4).try_into().unwrap()),
4127                memo: HashMap::from([(
4128                    "memo-key".to_string(),
4129                    "memo-value".as_json_payload().unwrap(),
4130                )]),
4131                headers: HashMap::new(),
4132                search_attributes: Some(proto_search_attributes),
4133                retry_policy: Some(ProtoRetryPolicy {
4134                    initial_interval: Some(Duration::from_secs(1).try_into().unwrap()),
4135                    backoff_coefficient: 2.0,
4136                    maximum_attempts: 5,
4137                    ..Default::default()
4138                }),
4139                versioning_intent: ProtoVersioningIntent::Compatible.into(),
4140                initial_versioning_behavior: ProtoContinueAsNewVersioningBehavior::UseRampingVersion
4141                    as i32,
4142            }
4143        );
4144    }
4145
4146    #[test]
4147    fn continue_as_new_preserves_explicit_empty_search_attributes() {
4148        let ctx = test_context();
4149        let sync = ctx.sync_context();
4150
4151        let termination = sync
4152            .continue_as_new(
4153                11,
4154                ContinueAsNewOptions {
4155                    search_attributes: Some(SearchAttributes::default()),
4156                    ..Default::default()
4157                },
4158            )
4159            .expect_err("continue_as_new should terminate the workflow");
4160        let WorkflowTermination::ContinueAsNew(cmd) = termination else {
4161            unreachable!()
4162        };
4163
4164        assert_eq!(
4165            cmd.search_attributes,
4166            Some(ProtoSearchAttributes::default())
4167        );
4168    }
4169
4170    #[test]
4171    fn workflow_context_continue_as_new_applies_auto_upgrade_versioning_behavior() {
4172        let ctx = test_context();
4173
4174        let termination = ctx
4175            .continue_as_new(
4176                13,
4177                ContinueAsNewOptions {
4178                    initial_versioning_behavior: Some(ContinueAsNewVersioningBehavior::AutoUpgrade),
4179                    ..Default::default()
4180                },
4181            )
4182            .expect_err("continue_as_new should terminate the workflow");
4183        let WorkflowTermination::ContinueAsNew(cmd) = termination else {
4184            unreachable!()
4185        };
4186
4187        assert_eq!(
4188            cmd.initial_versioning_behavior,
4189            ProtoContinueAsNewVersioningBehavior::AutoUpgrade as i32
4190        );
4191    }
4192
4193    #[test]
4194    fn continue_as_new_preserves_input_serialization_errors() {
4195        #[derive(Debug)]
4196        struct FailingInput;
4197
4198        impl TemporalSerializable for FailingInput {
4199            fn to_payload(
4200                &self,
4201                _ctx: &temporalio_common_wasm::data_converters::SerializationContext<'_>,
4202            ) -> Result<Payload, temporalio_common_wasm::data_converters::PayloadConversionError>
4203            {
4204                Err(
4205                    temporalio_common_wasm::data_converters::PayloadConversionError::EncodingError(
4206                        std::io::Error::other("serialization failure").into(),
4207                    ),
4208                )
4209            }
4210        }
4211
4212        impl TemporalDeserializable for FailingInput {
4213            fn from_payload(
4214                _ctx: &temporalio_common_wasm::data_converters::SerializationContext<'_>,
4215                _payload: Payload,
4216            ) -> Result<Self, temporalio_common_wasm::data_converters::PayloadConversionError>
4217            {
4218                unreachable!("test input is only serialized")
4219            }
4220        }
4221
4222        #[workflow]
4223        #[derive(Default)]
4224        struct FailingWorkflow;
4225
4226        #[workflow_methods]
4227        impl FailingWorkflow {
4228            #[run]
4229            async fn run(
4230                _ctx: &mut WorkflowContext<Self>,
4231                _input: FailingInput,
4232            ) -> crate::WorkflowResult<()> {
4233                unreachable!("test workflow run should not be polled")
4234            }
4235        }
4236
4237        let init = InitializeWorkflow {
4238            workflow_type: "failing-workflow".to_string(),
4239            ..Default::default()
4240        };
4241        let init = WorkflowInit {
4242            namespace: "default".to_string(),
4243            task_queue: "orig-task-queue".to_string(),
4244            run_id: "run-id".to_string(),
4245            initialize_workflow: init,
4246        };
4247        let base = BaseWorkflowContext::from_raw(
4248            init,
4249            DataConverter::default(),
4250            Rc::new(NoopHost),
4251            None,
4252            Vec::new(),
4253        );
4254        let ctx = WorkflowContext::from_base(base, Rc::new(RefCell::new(FailingWorkflow)));
4255
4256        let termination = ctx
4257            .continue_as_new(FailingInput, ContinueAsNewOptions::default())
4258            .expect_err("input serialization should fail");
4259        let WorkflowTermination::Failed(OutgoingWorkflowError::PayloadConversion(err)) =
4260            termination
4261        else {
4262            panic!("expected a payload conversion failure");
4263        };
4264        assert_eq!(err.to_string(), "Encoding error: serialization failure");
4265    }
4266
4267    #[test]
4268    fn continue_as_new_preserves_memo_serialization_errors() {
4269        let ctx = test_context();
4270        let mut memo = MemoValues::new();
4271        memo.insert("invalid", FailingMemoValue);
4272
4273        let termination = ctx
4274            .continue_as_new(
4275                7,
4276                ContinueAsNewOptions {
4277                    memo: Some(memo),
4278                    ..Default::default()
4279                },
4280            )
4281            .expect_err("memo serialization should fail");
4282        let WorkflowTermination::Failed(OutgoingWorkflowError::PayloadConversion(err)) =
4283            termination
4284        else {
4285            panic!("expected a payload conversion failure");
4286        };
4287        assert_eq!(
4288            err.to_string(),
4289            "Encoding error: memo serialization failure"
4290        );
4291    }
4292
4293    #[test]
4294    fn upsert_search_attributes_updates_local_state() {
4295        use temporalio_common_wasm::search_attributes::SearchAttributeKey;
4296
4297        const K: SearchAttributeKey<i64> = SearchAttributeKey::int("my_int");
4298
4299        let ctx = test_context();
4300        assert!(ctx.search_attributes().is_empty());
4301
4302        ctx.upsert_search_attributes([K.value_set(42)]);
4303        let attrs = ctx.search_attributes();
4304        assert_eq!(attrs.get(&K), Some(42));
4305    }
4306
4307    #[test]
4308    fn upsert_memo_updates_local_state_and_encodes_removals() {
4309        let init = InitializeWorkflow {
4310            workflow_type: TestWorkflow.name().to_string(),
4311            memo: Some(ProtoMemo {
4312                fields: HashMap::from([("old".to_string(), "before".as_json_payload().unwrap())]),
4313            }),
4314            ..Default::default()
4315        };
4316        let host = Rc::new(RecordingHost::default());
4317        let init = WorkflowInit {
4318            namespace: "default".to_string(),
4319            task_queue: "orig-task-queue".to_string(),
4320            run_id: "run-id".to_string(),
4321            initialize_workflow: init,
4322        };
4323        let base = BaseWorkflowContext::from_raw(
4324            init,
4325            DataConverter::default(),
4326            host.clone(),
4327            None,
4328            Vec::new(),
4329        );
4330        let ctx = WorkflowContext::from_base(base, Rc::new(RefCell::new(TestWorkflow)));
4331
4332        assert_eq!(
4333            ctx.memo().get::<String>("old").unwrap(),
4334            Some("before".to_string())
4335        );
4336        ctx.upsert_memo([("new", Some(MemoValue::new(42_u32))), ("old", None)])
4337            .unwrap();
4338
4339        let current = ctx.memo();
4340        assert_eq!(current.get::<u32>("new").unwrap(), Some(42));
4341        assert_eq!(current.get::<String>("old").unwrap(), None);
4342        let view = ctx.view();
4343        assert_eq!(view.memo().get::<u32>("new").unwrap(), Some(42));
4344        assert_eq!(
4345            view.memo().raw(),
4346            view.raw()
4347                .memo
4348                .as_ref()
4349                .expect("view memo should be present")
4350        );
4351
4352        let commands = host.commands.borrow();
4353        let [command] = commands.as_slice() else {
4354            panic!("expected one modify-properties command");
4355        };
4356        let Some(workflow_command::Variant::ModifyWorkflowProperties(command)) = &command.variant
4357        else {
4358            panic!("expected a modify-properties command");
4359        };
4360        let fields = &command.upserted_memo.as_ref().unwrap().fields;
4361        let payload_converter = PayloadConverter::default();
4362        let removal_payload = MemoValue::new(()).to_payload(&payload_converter).unwrap();
4363        assert_eq!(fields.get("old"), Some(&removal_payload));
4364        assert_eq!(
4365            u32::from_json_payload(fields.get("new").unwrap()).unwrap(),
4366            42
4367        );
4368    }
4369
4370    #[test]
4371    fn upsert_memo_conversion_failure_does_not_mutate_or_emit_command() {
4372        let host = Rc::new(RecordingHost::default());
4373        let init = InitializeWorkflow {
4374            workflow_type: TestWorkflow.name().to_string(),
4375            ..Default::default()
4376        };
4377        let init = WorkflowInit {
4378            namespace: "default".to_string(),
4379            task_queue: "orig-task-queue".to_string(),
4380            run_id: "run-id".to_string(),
4381            initialize_workflow: init,
4382        };
4383        let base = BaseWorkflowContext::from_raw(
4384            init,
4385            DataConverter::default(),
4386            host.clone(),
4387            None,
4388            Vec::new(),
4389        );
4390        let ctx = WorkflowContext::from_base(base, Rc::new(RefCell::new(TestWorkflow)));
4391        let err = ctx
4392            .upsert_memo([
4393                ("valid", Some(MemoValue::new("value".to_string()))),
4394                ("invalid", Some(MemoValue::new(FailingMemoValue))),
4395            ])
4396            .unwrap_err();
4397
4398        assert_eq!(
4399            err.to_string(),
4400            "Encoding error: memo serialization failure"
4401        );
4402        assert_eq!(ctx.memo().get::<String>("valid").unwrap(), None);
4403        assert!(host.commands.borrow().is_empty());
4404    }
4405
4406    #[test]
4407    fn upsert_search_attributes_unset_removes_from_local_state() {
4408        use temporalio_common_wasm::search_attributes::SearchAttributeKey;
4409
4410        const K: SearchAttributeKey<String> = SearchAttributeKey::keyword("my_kw");
4411
4412        let ctx = test_context();
4413        // Set, then unset.
4414        ctx.upsert_search_attributes([K.value_set("hello".into())]);
4415        assert_eq!(ctx.search_attributes().get(&K), Some("hello".into()));
4416
4417        ctx.upsert_search_attributes([K.value_unset()]);
4418        assert!(!ctx.search_attributes().contains_key(&K));
4419        assert!(ctx.search_attributes().is_empty());
4420    }
4421
4422    #[test]
4423    fn upsert_search_attributes_multiple_updates_last_wins() {
4424        use temporalio_common_wasm::search_attributes::SearchAttributeKey;
4425
4426        const K: SearchAttributeKey<i64> = SearchAttributeKey::int("counter");
4427
4428        let ctx = test_context();
4429        ctx.upsert_search_attributes([K.value_set(1), K.value_set(2)]);
4430        assert_eq!(ctx.search_attributes().get(&K), Some(2));
4431    }
4432
4433    #[test]
4434    fn upsert_search_attributes_merges_with_initial() {
4435        use temporalio_common_wasm::search_attributes::SearchAttributeKey;
4436
4437        const A: SearchAttributeKey<i64> = SearchAttributeKey::int("attr_a");
4438        const B: SearchAttributeKey<String> = SearchAttributeKey::keyword("attr_b");
4439
4440        // Start with initial search attribute A.
4441        let init_sa = SearchAttributes::new([A.value_set(1)]).into_proto();
4442        let init = InitializeWorkflow {
4443            workflow_type: TestWorkflow.name().to_string(),
4444            search_attributes: Some(init_sa),
4445            ..Default::default()
4446        };
4447        let init = WorkflowInit {
4448            namespace: "default".to_string(),
4449            task_queue: "tq".to_string(),
4450            run_id: "run-id".to_string(),
4451            initialize_workflow: init,
4452        };
4453        let base = BaseWorkflowContext::from_raw(
4454            init,
4455            DataConverter::default(),
4456            Rc::new(NoopHost),
4457            None,
4458            Vec::new(),
4459        );
4460        let ctx = WorkflowContext::from_base(base, Rc::new(RefCell::new(TestWorkflow)));
4461
4462        assert_eq!(ctx.search_attributes().get(&A), Some(1));
4463
4464        // Upsert B — A should still be present.
4465        ctx.upsert_search_attributes([B.value_set("hello".into())]);
4466        assert_eq!(ctx.search_attributes().get(&A), Some(1));
4467        assert_eq!(ctx.search_attributes().get(&B), Some("hello".into()));
4468    }
4469
4470    #[test]
4471    fn view_search_attributes_returns_typed() {
4472        use temporalio_common_wasm::search_attributes::SearchAttributeKey;
4473
4474        const K: SearchAttributeKey<bool> = SearchAttributeKey::bool("active");
4475
4476        let init_sa = SearchAttributes::new([K.value_set(true)]).into_proto();
4477        let init = InitializeWorkflow {
4478            workflow_type: TestWorkflow.name().to_string(),
4479            search_attributes: Some(init_sa),
4480            ..Default::default()
4481        };
4482        let init = WorkflowInit {
4483            namespace: "default".to_string(),
4484            task_queue: "tq".to_string(),
4485            run_id: "run-id".to_string(),
4486            initialize_workflow: init,
4487        };
4488        let base = BaseWorkflowContext::from_raw(
4489            init,
4490            DataConverter::default(),
4491            Rc::new(NoopHost),
4492            None,
4493            Vec::new(),
4494        );
4495        let ctx = WorkflowContext::from_base(base, Rc::new(RefCell::new(TestWorkflow)));
4496
4497        let view = ctx.view();
4498        let sa = view
4499            .search_attributes()
4500            .expect("should have search attributes");
4501        assert_eq!(sa.get(&K), Some(true));
4502    }
4503
4504    #[test]
4505    fn workflow_info_retains_raw_initialization() {
4506        let init = InitializeWorkflow {
4507            workflow_type: TestWorkflow.name().to_string(),
4508            identity: "raw-only-identity".to_owned(),
4509            ..Default::default()
4510        };
4511        let expected = init.clone();
4512        let init = WorkflowInit {
4513            namespace: "default".to_string(),
4514            task_queue: "tq".to_string(),
4515            run_id: "run-id".to_string(),
4516            initialize_workflow: init,
4517        };
4518        let base = BaseWorkflowContext::from_raw(
4519            init,
4520            DataConverter::default(),
4521            Rc::new(NoopHost),
4522            None,
4523            Vec::new(),
4524        );
4525        let ctx = WorkflowContext::from_base(base, Rc::new(RefCell::new(TestWorkflow)));
4526        let info = ctx.info();
4527
4528        assert_eq!(info.raw().identity, "raw-only-identity");
4529        assert_eq!(info.raw(), &expected);
4530        assert_eq!(info.into_raw(), expected);
4531    }
4532}