Skip to main content

temporalio_workflow/
workflow_context.rs

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