Skip to main content

temporalio_workflow/
workflow_context.rs

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