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