Skip to main content

temporalio_client/
lib.rs

1#![warn(missing_docs)] // error if there are missing docs
2
3//! This crate contains client implementations that can be used to contact the Temporal service.
4//!
5//! It implements auto-retry behavior and metrics collection.
6
7#[macro_use]
8extern crate tracing;
9
10mod async_activity_handle;
11pub mod callback_based;
12mod dns;
13/// Configuration loading from environment variables and TOML files.
14#[cfg(feature = "envconfig")]
15pub mod envconfig;
16pub mod errors;
17pub mod grpc;
18/// Interceptors for high-level client operations.
19pub mod interceptors;
20mod metrics;
21mod options_structs;
22/// Visible only for tests
23#[doc(hidden)]
24pub mod proxy;
25mod replaceable;
26pub mod request_extensions;
27mod retry;
28mod rpc_options;
29/// Schedule operations: create, describe, update, pause, trigger, backfill, list, and delete.
30pub mod schedules;
31#[cfg(test)]
32mod test_helpers;
33pub mod worker;
34mod workflow_handle;
35mod workflow_status;
36
37pub use crate::{
38    proxy::HttpConnectProxyOptions,
39    request_extensions::PayloadErrorLimits,
40    retry::{CallType, RETRYABLE_ERROR_CODES},
41};
42pub use async_activity_handle::{
43    ActivityHeartbeatResponse, ActivityIdentifier, AsyncActivityHandle,
44};
45#[doc(hidden)]
46pub use retry::jittered;
47
48pub use interceptors::{
49    BackfillScheduleInput, CancelWorkflowInput, ClientInterceptor, CompleteAsyncActivityInput,
50    CountWorkflowsInput, CountWorkflowsOutput, CreateScheduleInput, CreateScheduleOutput,
51    DeleteScheduleInput, DescribeScheduleInput, DescribeScheduleOutput, DescribeWorkflowInput,
52    DescribeWorkflowOutput, FailAsyncActivityInput, FetchWorkflowHistoryPageInput,
53    FetchWorkflowHistoryPageOutput, HasArgs, HeartbeatAsyncActivityInput, ListSchedulesPageInput,
54    ListSchedulesPageOutput, ListWorkflowsPageInput, ListWorkflowsPageOutput, Next,
55    PauseScheduleInput, PollWorkflowUpdateInput, PollWorkflowUpdateOutput, QueryWorkflowInput,
56    QueryWorkflowOutput, ReportAsyncActivityCancellationInput, SendScheduleUpdateInput,
57    SignalWorkflowInput, StartWorkflowInput, StartWorkflowOutput, StartWorkflowUpdateInput,
58    StartWorkflowUpdateOutput, TemporalClientValue, TerminateWorkflowInput, TriggerScheduleInput,
59    UnpauseScheduleInput, UpdateScheduleInput,
60};
61pub use metrics::{LONG_REQUEST_LATENCY_HISTOGRAM_NAME, REQUEST_LATENCY_HISTOGRAM_NAME};
62pub use options_structs::*;
63pub use replaceable::SharedReplaceableClient;
64pub use retry::RetryOptions;
65pub use rpc_options::{RpcMetadata, RpcMetadataError, RpcOptions};
66pub use temporalio_common::{Memo, RetryPolicy};
67pub use url::Url;
68/// Potentially dangerous TLS related functionality.
69pub mod danger {
70    /// Re-export the `ServerCertVerifier` trait so that users can implement custom TLS
71    /// server certificate verification without depending on `tokio-rustls` directly,
72    /// while explicitly acknowledging the danger in the import path.
73    pub use tokio_rustls::rustls::client::danger::ServerCertVerifier;
74}
75pub use tonic;
76pub use workflow_handle::{
77    UntypedQuery, UntypedSignal, UntypedUpdate, UntypedWorkflow, UntypedWorkflowHandle,
78    WorkflowExecutionDescription, WorkflowExecutionInfo, WorkflowExecutionResult, WorkflowHandle,
79    WorkflowHistory, WorkflowResultDetails, WorkflowUpdateHandle,
80};
81pub use workflow_status::WorkflowExecutionStatus;
82
83use crate::{
84    grpc::{
85        AttachMetricLabels, CloudService, HealthService, OperatorService, TestService,
86        WorkflowService,
87    },
88    metrics::{ChannelOrGrpcOverride, GrpcMetricSvc, MetricsContext},
89    request_extensions::RequestExt,
90    worker::ClientWorkerSet,
91};
92use errors::*;
93use futures_util::{future::BoxFuture, stream, stream::Stream};
94use http::Uri;
95use parking_lot::RwLock;
96use std::{
97    collections::{HashMap, VecDeque},
98    fmt::Debug,
99    pin::Pin,
100    str::FromStr,
101    sync::{Arc, OnceLock},
102    task::{Context, Poll},
103    time::{Duration, SystemTime},
104};
105use temporalio_common::{
106    HasWorkflowDefinition,
107    data_converters::{
108        DataConverter, GenericPayloadConverter, PayloadConverter, SerializationContext,
109        SerializationContextData,
110    },
111    payload_visitor::decode_payloads,
112    protos::{
113        coresdk::IntoPayloadsExt,
114        grpc::health::v1::health_client::HealthClient,
115        proto_ts_to_system_time,
116        temporal::api::{
117            cloud::cloudservice::v1::cloud_service_client::CloudServiceClient,
118            common::v1::WorkflowType,
119            enums::v1::TaskQueueKind,
120            errordetails::v1::WorkflowExecutionAlreadyStartedFailure,
121            operatorservice::v1::operator_service_client::OperatorServiceClient,
122            sdk::v1::UserMetadata,
123            taskqueue::v1::TaskQueue,
124            testservice::v1::test_service_client::TestServiceClient,
125            workflow::v1 as workflow,
126            workflowservice::v1::{
127                count_workflow_executions_response, workflow_service_client::WorkflowServiceClient,
128                *,
129            },
130        },
131        utilities::decode_status_detail,
132    },
133    search_attributes::{SearchAttributeError, SearchAttributeValue, SearchAttributes},
134};
135use tonic::{
136    Code, IntoRequest,
137    body::Body,
138    client::GrpcService,
139    codec::CompressionEncoding,
140    codegen::InterceptedService,
141    metadata::{
142        AsciiMetadataKey, AsciiMetadataValue, BinaryMetadataKey, BinaryMetadataValue, MetadataMap,
143        MetadataValue,
144    },
145    service::Interceptor,
146    transport::{Certificate, Endpoint, Identity},
147};
148use tower::ServiceBuilder;
149use uuid::Uuid;
150
151static CLIENT_NAME_HEADER_KEY: &str = "client-name";
152static CLIENT_VERSION_HEADER_KEY: &str = "client-version";
153static TEMPORAL_NAMESPACE_HEADER_KEY: &str = "temporal-namespace";
154
155#[doc(hidden)]
156/// Key used to communicate when a GRPC message is too large
157pub static MESSAGE_TOO_LARGE_KEY: &str = "message-too-large";
158#[doc(hidden)]
159/// Returns the violation, if `status` is the client proactively rejecting an outbound request for exceeding a
160/// payload/memo error size limit.
161pub fn payload_limit_violation_from(
162    status: &tonic::Status,
163) -> Option<&temporalio_common::payload_limits::PayloadLimitViolation> {
164    std::error::Error::source(status).and_then(|src| src.downcast_ref())
165}
166#[doc(hidden)]
167/// Key used to indicate a error was returned by the retryer because of the short-circuit predicate
168pub static ERROR_RETURNED_DUE_TO_SHORT_CIRCUIT: &str = "short-circuit";
169
170/// The server times out polls after 60 seconds. Set our timeout to be slightly beyond that.
171const LONG_POLL_TIMEOUT: Duration = Duration::from_secs(70);
172const OTHER_CALL_TIMEOUT: Duration = Duration::from_secs(30);
173const VERSION: &str = env!("CARGO_PKG_VERSION");
174
175/// A connection to the Temporal service.
176///
177/// Cloning a connection is cheap (single Arc increment). The underlying connection is shared
178/// between clones.
179#[derive(Clone)]
180pub struct Connection {
181    inner: Arc<ConnectionInner>,
182}
183
184#[derive(Clone)]
185struct ConnectionInner {
186    service: TemporalServiceClient,
187    retry_options: RetryOptions,
188    identity: String,
189    headers: Arc<RwLock<ClientHeaders>>,
190    client_name: String,
191    client_version: String,
192    /// Capabilities as read from the `get_system_info` RPC call made on client connection
193    capabilities: Option<get_system_info_response::Capabilities>,
194    workers: Arc<ClientWorkerSet>,
195    _dns_task: Option<Arc<dns::DnsReresolutionHandle>>,
196    /// Configured payload/memo size warning thresholds (bytes); `0` disables that warning.
197    payloads_warn_size: usize,
198    memo_warn_size: usize,
199}
200
201/// Resolve a user-configured warning threshold (bytes) into the internal representation. `0`
202/// disables the warning (`None`); so does a value that doesn't fit in `usize` on this platform (a
203/// threshold larger than any addressable payload could never fire anyway), with a warning logged.
204/// `option` names the configured field, for diagnostics.
205fn resolve_warn_threshold(option: &'static str, bytes: u64) -> usize {
206    usize::try_from(bytes).unwrap_or_else(|_| {
207        warn!(
208            option,
209            configured_bytes = bytes,
210            "Configured payload size warning threshold exceeds the maximum addressable size on this \
211             platform; disabling this warning"
212        );
213        0
214    })
215}
216
217impl Connection {
218    /// Connect to a Temporal service.
219    pub async fn connect(mut options: ConnectionOptions) -> Result<Self, ClientConnectError> {
220        if options.service_override.is_some() {
221            options.grpc_compression = GrpcCompression::None;
222        }
223
224        let first_result = Self::connect_once(&options).await;
225        if options.grpc_compression == GrpcCompression::Gzip
226            && let Err(ClientConnectError::SystemInfoCallError(status)) = &first_result
227            && status.code() == Code::Unimplemented
228            && {
229                let msg = status.message().to_lowercase();
230                msg.contains("decompress")
231                    || msg.contains("grpc-encoding")
232                    || msg.contains("compressor")
233            }
234        {
235            options.grpc_compression = GrpcCompression::None;
236            return Self::connect_once(&options).await;
237        }
238        first_result
239    }
240
241    async fn connect_once(options: &ConnectionOptions) -> Result<Self, ClientConnectError> {
242        let dns_lb_opts = dns::validate_and_get_dns_lb(options)?.cloned();
243        let (service, dns_task) = if let Some(service_override) = options.service_override.clone() {
244            (
245                GrpcMetricSvc {
246                    inner: ChannelOrGrpcOverride::GrpcOverride(service_override),
247                    metrics: options.metrics_meter.clone().map(MetricsContext::new),
248                    disable_errcode_label: options.disable_error_code_metric_tags,
249                },
250                None,
251            )
252        } else if let Some(dns_opts) = &dns_lb_opts {
253            let (channel, sender) = dns::create_balanced_channel(options).await?;
254            let handle = dns::spawn_dns_reresolution(
255                sender,
256                options.target.clone(),
257                options.tls_options.clone(),
258                options.keep_alive.clone(),
259                options.override_origin.clone(),
260                dns_opts.resolution_interval,
261                options.connect_timeout,
262            );
263            (
264                ServiceBuilder::new()
265                    .layer_fn(move |channel| GrpcMetricSvc {
266                        inner: ChannelOrGrpcOverride::Channel(channel),
267                        metrics: options.metrics_meter.clone().map(MetricsContext::new),
268                        disable_errcode_label: options.disable_error_code_metric_tags,
269                    })
270                    .service(channel),
271                Some(handle),
272            )
273        } else {
274            let channel = Endpoint::from_shared(options.target.to_string())?;
275            let channel = if let Some(timeout) = options.connect_timeout {
276                channel.connect_timeout(timeout)
277            } else {
278                channel
279            };
280            let channel = add_tls_to_channel(options.tls_options.as_ref(), channel).await?;
281            let channel = if let Some(keep_alive) = options.keep_alive.as_ref() {
282                channel
283                    .keep_alive_while_idle(true)
284                    .http2_keep_alive_interval(keep_alive.interval)
285                    .keep_alive_timeout(keep_alive.timeout)
286            } else {
287                channel
288            };
289            let channel = if let Some(origin) = options.override_origin.clone() {
290                channel.origin(origin)
291            } else {
292                channel
293            };
294            // If there is a proxy, we have to connect that way
295            let channel = if let Some(proxy) = options.http_connect_proxy.as_ref() {
296                proxy.connect_endpoint(&channel).await?
297            } else {
298                channel.connect().await?
299            };
300            (
301                ServiceBuilder::new()
302                    .layer_fn(move |channel| GrpcMetricSvc {
303                        inner: ChannelOrGrpcOverride::Channel(channel),
304                        metrics: options.metrics_meter.clone().map(MetricsContext::new),
305                        disable_errcode_label: options.disable_error_code_metric_tags,
306                    })
307                    .service(channel),
308                None,
309            )
310        };
311
312        let headers = Arc::new(RwLock::new(ClientHeaders {
313            user_headers: parse_ascii_headers(options.headers.clone().unwrap_or_default())?,
314            user_binary_headers: parse_binary_headers(
315                options.binary_headers.clone().unwrap_or_default(),
316            )?,
317            api_key: options.api_key.clone(),
318        }));
319        let interceptor = ServiceCallInterceptor {
320            client_name: options.client_name.clone(),
321            client_version: options.client_version.clone(),
322            headers: headers.clone(),
323        };
324        let svc = InterceptedService::new(service, interceptor);
325        let mut svc_client = TemporalServiceClient::new(svc, options.grpc_compression);
326
327        let capabilities = if !options.skip_get_system_info {
328            match svc_client
329                .get_system_info(GetSystemInfoRequest::default().into_request())
330                .await
331            {
332                Ok(sysinfo) => sysinfo.into_inner().capabilities,
333                Err(status) => match status.code() {
334                    Code::Unimplemented
335                        if {
336                            let msg = status.message().to_lowercase();
337                            msg.contains("unknown method")
338                                || msg.contains("unknown service")
339                                || msg.contains("method not found")
340                                || (msg.contains("getsysteminfo")
341                                    && (msg.contains("is unimplemented")
342                                        || msg.contains("not implement")))
343                        } =>
344                    {
345                        None
346                    }
347                    _ => return Err(ClientConnectError::SystemInfoCallError(status)),
348                },
349            }
350        } else {
351            None
352        };
353        Ok(Self {
354            inner: Arc::new(ConnectionInner {
355                service: svc_client,
356                retry_options: options.retry_options.clone(),
357                identity: options.identity.clone(),
358                headers,
359                client_name: options.client_name.clone(),
360                client_version: options.client_version.clone(),
361                capabilities,
362                workers: Arc::new(ClientWorkerSet::new()),
363                _dns_task: dns_task,
364                payloads_warn_size: resolve_warn_threshold(
365                    "payloads_warn_size",
366                    options.payload_limits.payloads_warn_size,
367                ),
368                memo_warn_size: resolve_warn_threshold(
369                    "memo_warn_size",
370                    options.payload_limits.memo_warn_size,
371                ),
372            }),
373        })
374    }
375
376    /// Set API key, overwriting any previous one.
377    pub fn set_api_key(&self, api_key: Option<String>) {
378        self.inner.headers.write().api_key = api_key;
379    }
380
381    /// Set HTTP request headers overwriting previous headers.
382    ///
383    /// This will not affect headers set via [ConnectionOptions::binary_headers].
384    ///
385    /// # Errors
386    ///
387    /// Will return an error if any of the provided keys or values are not valid gRPC metadata.
388    /// If an error is returned, the previous headers will remain unchanged.
389    pub fn set_headers(&self, headers: HashMap<String, String>) -> Result<(), InvalidHeaderError> {
390        self.inner.headers.write().user_headers = parse_ascii_headers(headers)?;
391        Ok(())
392    }
393
394    /// Set binary HTTP request headers overwriting previous headers.
395    ///
396    /// This will not affect headers set via [ConnectionOptions::headers].
397    ///
398    /// # Errors
399    ///
400    /// Will return an error if any of the provided keys are not valid gRPC binary metadata keys.
401    /// If an error is returned, the previous headers will remain unchanged.
402    pub fn set_binary_headers(
403        &self,
404        binary_headers: HashMap<String, Vec<u8>>,
405    ) -> Result<(), InvalidHeaderError> {
406        self.inner.headers.write().user_binary_headers = parse_binary_headers(binary_headers)?;
407        Ok(())
408    }
409
410    /// Returns the value used for the `client-name` header by this connection.
411    pub fn client_name(&self) -> &str {
412        &self.inner.client_name
413    }
414
415    /// Returns the value used for the `client-version` header by this connection.
416    pub fn client_version(&self) -> &str {
417        &self.inner.client_version
418    }
419
420    /// Returns the server capabilities we (may have) learned about when establishing an initial
421    /// connection
422    pub fn capabilities(&self) -> Option<&get_system_info_response::Capabilities> {
423        self.inner.capabilities.as_ref()
424    }
425
426    /// Get a mutable reference to the retry options.
427    ///
428    /// Note: If this connection has been cloned, this will copy-on-write to avoid
429    /// affecting other clones.
430    pub fn retry_options_mut(&mut self) -> &mut RetryOptions {
431        &mut Arc::make_mut(&mut self.inner).retry_options
432    }
433
434    /// Get a reference to the connection identity.
435    pub fn identity(&self) -> &str {
436        &self.inner.identity
437    }
438
439    /// Get a mutable reference to the connection identity.
440    ///
441    /// Note: If this connection has been cloned, this will copy-on-write to avoid
442    /// affecting other clones.
443    pub fn identity_mut(&mut self) -> &mut String {
444        &mut Arc::make_mut(&mut self.inner).identity
445    }
446
447    /// Returns a reference to a registry with workers using this client instance.
448    pub fn workers(&self) -> Arc<ClientWorkerSet> {
449        self.inner.workers.clone()
450    }
451
452    /// Returns the client-wide key.
453    pub fn worker_grouping_key(&self) -> Uuid {
454        self.inner.workers.worker_grouping_key()
455    }
456
457    /// Get the underlying workflow service client for making raw gRPC calls.
458    pub fn workflow_service(&self) -> Box<dyn WorkflowService> {
459        self.inner.service.workflow_service()
460    }
461
462    /// Get the underlying operator service client for making raw gRPC calls.
463    pub fn operator_service(&self) -> Box<dyn OperatorService> {
464        self.inner.service.operator_service()
465    }
466
467    /// Get the underlying cloud service client for making raw gRPC calls.
468    pub fn cloud_service(&self) -> Box<dyn CloudService> {
469        self.inner.service.cloud_service()
470    }
471
472    /// Get the underlying test service client for making raw gRPC calls.
473    pub fn test_service(&self) -> Box<dyn TestService> {
474        self.inner.service.test_service()
475    }
476
477    /// Get the underlying health service client for making raw gRPC calls.
478    pub fn health_service(&self) -> Box<dyn HealthService> {
479        self.inner.service.health_service()
480    }
481}
482
483#[derive(Debug)]
484struct ClientHeaders {
485    user_headers: HashMap<AsciiMetadataKey, AsciiMetadataValue>,
486    user_binary_headers: HashMap<BinaryMetadataKey, BinaryMetadataValue>,
487    api_key: Option<String>,
488}
489
490impl ClientHeaders {
491    fn apply_to_metadata(&self, metadata: &mut MetadataMap) {
492        for (key, val) in self.user_headers.iter() {
493            // Only if not already present
494            if !metadata.contains_key(key) {
495                metadata.insert(key, val.clone());
496            }
497        }
498        for (key, val) in self.user_binary_headers.iter() {
499            // Only if not already present
500            if !metadata.contains_key(key) {
501                metadata.insert_bin(key, val.clone());
502            }
503        }
504        if let Some(api_key) = &self.api_key {
505            // Only if not already present
506            if !metadata.contains_key("authorization")
507                && let Ok(val) = format!("Bearer {api_key}").parse()
508            {
509                metadata.insert("authorization", val);
510            }
511        }
512    }
513}
514
515/// If TLS is configured, set the appropriate options on the provided channel and return it.
516/// Passes it through if TLS options not set.
517async fn add_tls_to_channel(
518    tls_options: Option<&TlsOptions>,
519    mut channel: Endpoint,
520) -> Result<Endpoint, ClientConnectError> {
521    if let Some(tls_cfg) = tls_options {
522        if tls_cfg.server_cert_verifier.is_some() && tls_cfg.server_root_ca_cert.is_some() {
523            return Err(ClientConnectError::InvalidConfig(
524                "Cannot set both `server_root_ca_cert` and `server_cert_verifier`".to_owned(),
525            ));
526        }
527
528        let mut tls = tonic::transport::ClientTlsConfig::new();
529
530        if tls_cfg.server_cert_verifier.is_none() {
531            if let Some(root_cert) = &tls_cfg.server_root_ca_cert {
532                let server_root_ca_cert = Certificate::from_pem(root_cert);
533                tls = tls.ca_certificate(server_root_ca_cert);
534            } else {
535                tls = tls.with_native_roots();
536            }
537        }
538
539        if let Some(domain) = &tls_cfg.domain {
540            tls = tls.domain_name(domain);
541
542            // This song and dance ultimately is just to make sure the `:authority` header ends
543            // up correct on requests while we use TLS. Setting the header directly in our
544            // interceptor doesn't work since seemingly it is overridden at some point by
545            // something lower level.
546            let uri: Uri = format!("https://{domain}").parse()?;
547            channel = channel.origin(uri);
548        }
549
550        if let Some(client_opts) = &tls_cfg.client_tls_options {
551            let client_identity =
552                Identity::from_pem(&client_opts.client_cert, &client_opts.client_private_key);
553            tls = tls.identity(client_identity);
554        }
555
556        return if let Some(verifier) = &tls_cfg.server_cert_verifier {
557            channel
558                .tls_config_with_verifier(tls, verifier.clone())
559                .map_err(Into::into)
560        } else {
561            channel.tls_config(tls).map_err(Into::into)
562        };
563    }
564    Ok(channel)
565}
566
567fn parse_ascii_headers(
568    headers: HashMap<String, String>,
569) -> Result<HashMap<AsciiMetadataKey, AsciiMetadataValue>, InvalidHeaderError> {
570    let mut parsed_headers = HashMap::with_capacity(headers.len());
571    for (k, v) in headers.into_iter() {
572        let key = match AsciiMetadataKey::from_str(&k) {
573            Ok(key) => key,
574            Err(err) => {
575                return Err(InvalidHeaderError::InvalidAsciiHeaderKey {
576                    key: k,
577                    source: err,
578                });
579            }
580        };
581        let value = match MetadataValue::from_str(&v) {
582            Ok(value) => value,
583            Err(err) => {
584                return Err(InvalidHeaderError::InvalidAsciiHeaderValue {
585                    key: k,
586                    value: v,
587                    source: err,
588                });
589            }
590        };
591        parsed_headers.insert(key, value);
592    }
593
594    Ok(parsed_headers)
595}
596
597fn parse_binary_headers(
598    headers: HashMap<String, Vec<u8>>,
599) -> Result<HashMap<BinaryMetadataKey, BinaryMetadataValue>, InvalidHeaderError> {
600    let mut parsed_headers = HashMap::with_capacity(headers.len());
601    for (k, v) in headers.into_iter() {
602        let key = match BinaryMetadataKey::from_str(&k) {
603            Ok(key) => key,
604            Err(err) => {
605                return Err(InvalidHeaderError::InvalidBinaryHeaderKey {
606                    key: k,
607                    source: err,
608                });
609            }
610        };
611        let value = BinaryMetadataValue::from_bytes(&v);
612        parsed_headers.insert(key, value);
613    }
614
615    Ok(parsed_headers)
616}
617
618/// Interceptor which attaches common metadata (like "client-name") to every outgoing call
619#[derive(Clone)]
620pub struct ServiceCallInterceptor {
621    client_name: String,
622    client_version: String,
623    /// Only accessed as a reader
624    headers: Arc<RwLock<ClientHeaders>>,
625}
626
627impl Interceptor for ServiceCallInterceptor {
628    /// This function will get called on each outbound request. Returning a `Status` here will
629    /// cancel the request and have that status returned to the caller.
630    fn call(
631        &mut self,
632        mut request: tonic::Request<()>,
633    ) -> Result<tonic::Request<()>, tonic::Status> {
634        let metadata = request.metadata_mut();
635        if !metadata.contains_key(CLIENT_NAME_HEADER_KEY) {
636            metadata.insert(
637                CLIENT_NAME_HEADER_KEY,
638                self.client_name
639                    .parse()
640                    .unwrap_or_else(|_| MetadataValue::from_static("")),
641            );
642        }
643        if !metadata.contains_key(CLIENT_VERSION_HEADER_KEY) {
644            metadata.insert(
645                CLIENT_VERSION_HEADER_KEY,
646                self.client_version
647                    .parse()
648                    .unwrap_or_else(|_| MetadataValue::from_static("")),
649            );
650        }
651        self.headers.read().apply_to_metadata(metadata);
652        request.set_default_timeout(OTHER_CALL_TIMEOUT);
653
654        Ok(request)
655    }
656}
657
658/// Aggregates various services exposed by the Temporal server
659#[derive(Clone)]
660pub struct TemporalServiceClient {
661    workflow_svc_client: Box<dyn WorkflowService>,
662    operator_svc_client: Box<dyn OperatorService>,
663    cloud_svc_client: Box<dyn CloudService>,
664    test_svc_client: Box<dyn TestService>,
665    health_svc_client: Box<dyn HealthService>,
666}
667
668/// We up the limit on incoming messages from server from the 4Mb default to 128Mb. If for
669/// whatever reason this needs to be changed by the user, we support overriding it via env var.
670fn get_decode_max_size() -> usize {
671    static _DECODE_MAX_SIZE: OnceLock<usize> = OnceLock::new();
672    *_DECODE_MAX_SIZE.get_or_init(|| {
673        std::env::var("TEMPORAL_MAX_INCOMING_GRPC_BYTES")
674            .ok()
675            .and_then(|s| s.parse().ok())
676            .unwrap_or(128 * 1024 * 1024)
677    })
678}
679
680impl TemporalServiceClient {
681    fn new<T>(svc: T, compression: GrpcCompression) -> Self
682    where
683        T: GrpcService<Body> + Send + Sync + Clone + 'static,
684        T::ResponseBody: tonic::codegen::Body<Data = tonic::codegen::Bytes> + Send + 'static,
685        T::Error: Into<tonic::codegen::StdError>,
686        <T::ResponseBody as tonic::codegen::Body>::Error: Into<tonic::codegen::StdError> + Send,
687        <T as GrpcService<Body>>::Future: Send,
688    {
689        // The generated service clients don't share a trait exposing the compression setters, so
690        // a macro applies the same configuration to each concrete client type.
691        macro_rules! configure {
692            ($client:expr) => {{
693                let client = $client.max_decoding_message_size(get_decode_max_size());
694                match compression {
695                    GrpcCompression::Gzip => client
696                        .send_compressed(CompressionEncoding::Gzip)
697                        .accept_compressed(CompressionEncoding::Gzip),
698                    GrpcCompression::None => client,
699                }
700            }};
701        }
702
703        let workflow_svc_client = Box::new(configure!(WorkflowServiceClient::new(svc.clone())));
704        let operator_svc_client = Box::new(configure!(OperatorServiceClient::new(svc.clone())));
705        let cloud_svc_client = Box::new(configure!(CloudServiceClient::new(svc.clone())));
706        let test_svc_client = Box::new(configure!(TestServiceClient::new(svc.clone())));
707        let health_svc_client = Box::new(configure!(HealthClient::new(svc.clone())));
708
709        Self {
710            workflow_svc_client,
711            operator_svc_client,
712            cloud_svc_client,
713            test_svc_client,
714            health_svc_client,
715        }
716    }
717
718    /// Create a service client from implementations of the individual underlying services. Useful
719    /// for mocking out service implementations.
720    pub fn from_services(
721        workflow: Box<dyn WorkflowService>,
722        operator: Box<dyn OperatorService>,
723        cloud: Box<dyn CloudService>,
724        test: Box<dyn TestService>,
725        health: Box<dyn HealthService>,
726    ) -> Self {
727        Self {
728            workflow_svc_client: workflow,
729            operator_svc_client: operator,
730            cloud_svc_client: cloud,
731            test_svc_client: test,
732            health_svc_client: health,
733        }
734    }
735
736    /// Get the underlying workflow service client
737    pub fn workflow_service(&self) -> Box<dyn WorkflowService> {
738        self.workflow_svc_client.clone()
739    }
740    /// Get the underlying operator service client
741    pub fn operator_service(&self) -> Box<dyn OperatorService> {
742        self.operator_svc_client.clone()
743    }
744    /// Get the underlying cloud service client
745    pub fn cloud_service(&self) -> Box<dyn CloudService> {
746        self.cloud_svc_client.clone()
747    }
748    /// Get the underlying test service client
749    pub fn test_service(&self) -> Box<dyn TestService> {
750        self.test_svc_client.clone()
751    }
752    /// Get the underlying health service client
753    pub fn health_service(&self) -> Box<dyn HealthService> {
754        self.health_svc_client.clone()
755    }
756}
757
758/// Contains an instance of a namespace-bound client for interacting with the Temporal server.
759/// Cheap to clone.
760#[derive(Clone)]
761pub struct Client {
762    connection: Connection,
763    options: Arc<ClientOptions>,
764}
765
766impl Client {
767    /// Create a new client from a connection and options.
768    ///
769    /// Currently infallible, but returns a `Result` for future extensibility
770    /// (e.g., interceptor or plugin validation).
771    pub fn new(connection: Connection, options: ClientOptions) -> Result<Self, ClientNewError> {
772        Ok(Client {
773            connection,
774            options: Arc::new(options),
775        })
776    }
777
778    /// Return the options this client was initialized with
779    pub fn options(&self) -> &ClientOptions {
780        &self.options
781    }
782
783    /// Return this client's options mutably.
784    ///
785    /// Note: If this client has been cloned, this will copy-on-write to avoid affecting other
786    /// clones.
787    pub fn options_mut(&mut self) -> &mut ClientOptions {
788        Arc::make_mut(&mut self.options)
789    }
790
791    /// Returns a reference to the underlying connection
792    pub fn connection(&self) -> &Connection {
793        &self.connection
794    }
795
796    /// Returns a mutable reference to the underlying connection
797    pub fn connection_mut(&mut self) -> &mut Connection {
798        &mut self.connection
799    }
800}
801
802// High-level workflow operations on Client.
803// These forward to the internal WorkflowClientTrait blanket impl which is
804// available because Client implements WorkflowService + NamespacedClient + Clone.
805impl Client {
806    /// Start a workflow execution.
807    ///
808    /// Returns a [`WorkflowHandle`] that can be used to interact with the workflow
809    /// (e.g., get its result, send signals, query, etc.).
810    pub async fn start_workflow<W>(
811        &self,
812        workflow: W,
813        input: W::Input,
814        options: WorkflowStartOptions,
815    ) -> Result<WorkflowHandle<Self, W>, WorkflowStartError>
816    where
817        W: HasWorkflowDefinition,
818        W::Input: Send,
819    {
820        WorkflowClientTrait::start_workflow(self, workflow, input, options).await
821    }
822
823    /// Get a handle to an existing workflow.
824    ///
825    /// For untyped access, use `get_workflow_handle::<UntypedWorkflow>(...)`.
826    pub fn get_workflow_handle<W: HasWorkflowDefinition>(
827        &self,
828        workflow_id: impl Into<String>,
829    ) -> WorkflowHandle<Self, W> {
830        WorkflowClientTrait::get_workflow_handle(self, workflow_id)
831    }
832
833    /// List workflows matching a query.
834    ///
835    /// Returns a stream that lazily paginates through results.
836    /// Use `limit` in options to cap the number of results returned.
837    pub fn list_workflows(
838        &self,
839        query: impl Into<String>,
840        opts: WorkflowListOptions,
841    ) -> ListWorkflowsStream {
842        WorkflowClientTrait::list_workflows(self, query, opts)
843    }
844
845    /// Count workflows matching a query.
846    pub async fn count_workflows(
847        &self,
848        query: impl Into<String>,
849        opts: WorkflowCountOptions,
850    ) -> Result<WorkflowExecutionCount, ClientError> {
851        WorkflowClientTrait::count_workflows(self, query, opts).await
852    }
853
854    /// Get a handle to complete an activity asynchronously.
855    ///
856    /// An activity returning `ActivityError::WillCompleteAsync` can be completed with this handle.
857    pub fn get_async_activity_handle(
858        &self,
859        identifier: ActivityIdentifier,
860    ) -> AsyncActivityHandle<Self> {
861        WorkflowClientTrait::get_async_activity_handle(self, identifier)
862    }
863}
864
865impl NamespacedClient for Client {
866    fn namespace(&self) -> String {
867        self.options.namespace.clone()
868    }
869
870    fn identity(&self) -> String {
871        self.connection.identity().to_owned()
872    }
873
874    fn data_converter(&self) -> &DataConverter {
875        &self.options.data_converter
876    }
877
878    fn client_interceptors(&self) -> &[Arc<dyn ClientInterceptor>] {
879        &self.options.client_interceptors
880    }
881}
882
883/// Enum to help reference a namespace by either the namespace name or the namespace id
884#[derive(Clone)]
885pub enum Namespace {
886    /// Namespace name
887    Name(String),
888    /// Namespace id
889    Id(String),
890}
891
892/// This trait provides higher-level friendlier interaction with the server.
893/// See the [WorkflowService] trait for a lower-level client.
894pub(crate) trait WorkflowClientTrait: NamespacedClient {
895    /// Start a workflow execution.
896    fn start_workflow<W>(
897        &self,
898        workflow: W,
899        input: W::Input,
900        options: WorkflowStartOptions,
901    ) -> impl Future<Output = Result<WorkflowHandle<Self, W>, WorkflowStartError>>
902    where
903        Self: Sized,
904        W: HasWorkflowDefinition,
905        W::Input: Send;
906
907    /// Get a handle to an existing workflow. `run_id` may be left blank to specify the most recent
908    /// execution having the provided `workflow_id`.
909    ///
910    /// For untyped access, use `get_workflow_handle::<UntypedWorkflow>(...)`.
911    ///
912    /// See also [WorkflowHandle::new], for specifying namespace or first_execution_run_id.
913    fn get_workflow_handle<W: HasWorkflowDefinition>(
914        &self,
915        workflow_id: impl Into<String>,
916    ) -> WorkflowHandle<Self, W>
917    where
918        Self: Sized;
919
920    /// List workflows matching a query.
921    /// Returns a stream that lazily paginates through results.
922    /// Use `limit` in options to cap the number of results returned.
923    fn list_workflows(
924        &self,
925        query: impl Into<String>,
926        opts: WorkflowListOptions,
927    ) -> ListWorkflowsStream;
928
929    /// Count workflows matching a query.
930    fn count_workflows(
931        &self,
932        query: impl Into<String>,
933        opts: WorkflowCountOptions,
934    ) -> impl Future<Output = Result<WorkflowExecutionCount, ClientError>>;
935
936    /// Get a handle to complete an activity asynchronously.
937    ///
938    /// An activity returning `ActivityError::WillCompleteAsync` can be completed with this handle.
939    fn get_async_activity_handle(
940        &self,
941        identifier: ActivityIdentifier,
942    ) -> AsyncActivityHandle<Self>
943    where
944        Self: Sized;
945}
946
947/// A client that is bound to a namespace
948pub trait NamespacedClient {
949    /// Returns the namespace this client is bound to
950    fn namespace(&self) -> String;
951    /// Returns the client identity
952    fn identity(&self) -> String;
953    /// Returns the data converter for serializing/deserializing payloads.
954    /// Default implementation returns a static default converter.
955    fn data_converter(&self) -> &DataConverter {
956        static DEFAULT: OnceLock<DataConverter> = OnceLock::new();
957        DEFAULT.get_or_init(DataConverter::default)
958    }
959    /// Returns the interceptors used for high-level client operations.
960    ///
961    /// # Warning
962    ///
963    /// This provider exists so SDK-owned client handles can carry interceptor configuration
964    /// through the high-level client blanket implementation. Custom client implementations should
965    /// normally retain the default empty chain unless they deliberately provide the same plumbing.
966    fn client_interceptors(&self) -> &[Arc<dyn ClientInterceptor>] {
967        &[]
968    }
969}
970
971/// A workflow execution returned from list operations.
972/// This represents information about a workflow present in visibility.
973#[derive(Debug, Clone)]
974pub struct WorkflowExecution {
975    raw: workflow::WorkflowExecutionInfo,
976    data_converter: DataConverter,
977}
978
979impl WorkflowExecution {
980    fn new_with_data_converter(
981        raw: workflow::WorkflowExecutionInfo,
982        data_converter: DataConverter,
983    ) -> Self {
984        Self {
985            raw,
986            data_converter,
987        }
988    }
989
990    /// The workflow ID.
991    pub fn id(&self) -> &str {
992        self.raw
993            .execution
994            .as_ref()
995            .map(|e| e.workflow_id.as_str())
996            .unwrap_or("")
997    }
998
999    /// The run ID.
1000    pub fn run_id(&self) -> &str {
1001        self.raw
1002            .execution
1003            .as_ref()
1004            .map(|e| e.run_id.as_str())
1005            .unwrap_or("")
1006    }
1007
1008    /// The workflow type name.
1009    pub fn workflow_type(&self) -> &str {
1010        self.raw
1011            .r#type
1012            .as_ref()
1013            .map(|t| t.name.as_str())
1014            .unwrap_or("")
1015    }
1016
1017    /// The current status of the workflow execution.
1018    pub fn status(&self) -> WorkflowExecutionStatus {
1019        WorkflowExecutionStatus::from_raw(self.raw.status)
1020    }
1021
1022    /// When the workflow was created.
1023    pub fn start_time(&self) -> Option<SystemTime> {
1024        self.raw
1025            .start_time
1026            .as_ref()
1027            .and_then(proto_ts_to_system_time)
1028    }
1029
1030    /// When the workflow run started or should start.
1031    pub fn execution_time(&self) -> Option<SystemTime> {
1032        self.raw
1033            .execution_time
1034            .as_ref()
1035            .and_then(proto_ts_to_system_time)
1036    }
1037
1038    /// When the workflow was closed, if closed.
1039    pub fn close_time(&self) -> Option<SystemTime> {
1040        self.raw
1041            .close_time
1042            .as_ref()
1043            .and_then(proto_ts_to_system_time)
1044    }
1045
1046    /// The task queue the workflow runs on.
1047    pub fn task_queue(&self) -> &str {
1048        &self.raw.task_queue
1049    }
1050
1051    /// Number of events in history.
1052    pub fn history_length(&self) -> i64 {
1053        self.raw.history_length
1054    }
1055
1056    /// Workflow memo decoded with the client's payload converter.
1057    pub fn memo(&self) -> Memo {
1058        Memo::from_raw(
1059            self.raw.memo.clone(),
1060            self.data_converter.payload_converter().clone(),
1061            SerializationContextData::Workflow,
1062        )
1063    }
1064
1065    /// Parent workflow ID, if this is a child workflow.
1066    pub fn parent_id(&self) -> Option<&str> {
1067        self.raw
1068            .parent_execution
1069            .as_ref()
1070            .map(|e| e.workflow_id.as_str())
1071    }
1072
1073    /// Parent run ID, if this is a child workflow.
1074    pub fn parent_run_id(&self) -> Option<&str> {
1075        self.raw
1076            .parent_execution
1077            .as_ref()
1078            .map(|e| e.run_id.as_str())
1079    }
1080
1081    /// Search attributes on the workflow.
1082    pub fn search_attributes(&self) -> SearchAttributes {
1083        self.raw
1084            .search_attributes
1085            .as_ref()
1086            .map(SearchAttributes::from_proto)
1087            .unwrap_or_default()
1088    }
1089
1090    /// Access the raw proto for additional fields not exposed via accessors.
1091    pub fn raw(&self) -> &workflow::WorkflowExecutionInfo {
1092        &self.raw
1093    }
1094
1095    /// Consume the wrapper and return the raw proto.
1096    pub fn into_raw(self) -> workflow::WorkflowExecutionInfo {
1097        self.raw
1098    }
1099}
1100
1101/// A stream of workflow executions from a list query.
1102/// Internally paginates through results from the server.
1103pub struct ListWorkflowsStream {
1104    inner: Pin<Box<dyn Stream<Item = Result<WorkflowExecution, ClientError>> + Send>>,
1105}
1106
1107impl ListWorkflowsStream {
1108    fn new(
1109        inner: Pin<Box<dyn Stream<Item = Result<WorkflowExecution, ClientError>> + Send>>,
1110    ) -> Self {
1111        Self { inner }
1112    }
1113}
1114
1115impl Stream for ListWorkflowsStream {
1116    type Item = Result<WorkflowExecution, ClientError>;
1117
1118    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
1119        self.inner.as_mut().poll_next(cx)
1120    }
1121}
1122
1123/// Result of a workflow count operation.
1124///
1125/// If the query includes a group-by clause, `groups` will contain the aggregated
1126/// counts and `count` will be the sum of all group counts.
1127#[derive(Debug, Clone)]
1128pub struct WorkflowExecutionCount {
1129    count: usize,
1130    groups: Vec<WorkflowCountAggregationGroup>,
1131}
1132
1133impl WorkflowExecutionCount {
1134    pub(crate) fn from_response(resp: CountWorkflowExecutionsResponse) -> Self {
1135        Self {
1136            count: resp.count as usize,
1137            groups: resp
1138                .groups
1139                .into_iter()
1140                .map(WorkflowCountAggregationGroup::from_proto)
1141                .collect(),
1142        }
1143    }
1144
1145    /// The approximate number of workflows matching the query.
1146    /// If grouping was applied, this is the sum of all group counts.
1147    pub fn count(&self) -> usize {
1148        self.count
1149    }
1150
1151    /// The groups if the query had a group-by clause, or empty if not.
1152    pub fn groups(&self) -> &[WorkflowCountAggregationGroup] {
1153        &self.groups
1154    }
1155}
1156
1157/// Aggregation group from a workflow count query with a group-by clause.
1158#[derive(Debug, Clone)]
1159pub struct WorkflowCountAggregationGroup {
1160    raw: count_workflow_executions_response::AggregationGroup,
1161}
1162
1163impl WorkflowCountAggregationGroup {
1164    fn from_proto(proto: count_workflow_executions_response::AggregationGroup) -> Self {
1165        Self { raw: proto }
1166    }
1167
1168    /// Retrieve a typed group value at `index`.
1169    ///
1170    ///  Returns `None` if the index is out of bounds or deserialization fails.
1171    ///  Use [`Self::try_get`] for explicit error handling.
1172    pub fn get<T: SearchAttributeValue>(&self, index: usize) -> Option<T> {
1173        self.try_get(index).ok().flatten()
1174    }
1175
1176    /// Retrieve a typed group value at `index`, preserving deserialization
1177    /// errors.
1178    ///
1179    /// Returns `Ok(None)` if the index is out of bounds and `Err` if the
1180    /// payload cannot be deserialized.
1181    pub fn try_get<T: SearchAttributeValue>(
1182        &self,
1183        index: usize,
1184    ) -> Result<Option<T>, SearchAttributeError> {
1185        match self.raw.group_values.get(index) {
1186            Some(payload) => T::from_search_attribute_payload(payload).map(Some),
1187            None => Ok(None),
1188        }
1189    }
1190
1191    /// The approximate number of workflows matching for this group.
1192    pub fn count(&self) -> usize {
1193        self.raw.count as usize
1194    }
1195}
1196
1197impl<T> WorkflowClientTrait for T
1198where
1199    T: WorkflowService + NamespacedClient + Clone + Send + Sync + 'static,
1200{
1201    async fn start_workflow<W>(
1202        &self,
1203        workflow: W,
1204        input: W::Input,
1205        options: WorkflowStartOptions,
1206    ) -> Result<WorkflowHandle<Self, W>, WorkflowStartError>
1207    where
1208        W: HasWorkflowDefinition,
1209        W::Input: Send,
1210    {
1211        let namespace = self.namespace();
1212        let interceptor_output = interceptors::call_start_workflow(
1213            self.client_interceptors(),
1214            StartWorkflowInput::new(workflow.name().to_owned(), input, options),
1215            Next::new({
1216                let client = (*self).clone();
1217                move |input: StartWorkflowInput| -> BoxFuture<
1218                    '_,
1219                    Result<StartWorkflowOutput, WorkflowStartError>,
1220                > {
1221                    let mut client = client;
1222                    Box::pin(async move {
1223                        let (workflow_type, args, options, rpc_options) = input.into_parts();
1224                        let data_converter = client.data_converter().clone();
1225                        let unencoded_payloads = {
1226                            let payload_converter = data_converter.payload_converter();
1227                            let context = SerializationContext {
1228                                data: &SerializationContextData::Workflow,
1229                                converter: payload_converter,
1230                            };
1231                            args.serialize_payloads(&context)
1232                        };
1233                        drop(args);
1234
1235                        let payloads = data_converter
1236                            .codec()
1237                            .encode(&SerializationContextData::Workflow, unencoded_payloads?)
1238                            .await?;
1239                        let namespace = client.namespace();
1240                        let workflow_id = options.workflow_id.clone();
1241                        let task_queue_name = options.task_queue.clone();
1242
1243                        let user_metadata = if options.static_summary.is_some()
1244                            || options.static_details.is_some()
1245                        {
1246                            let payload_converter = PayloadConverter::default();
1247                            let context = SerializationContext {
1248                                data: &SerializationContextData::Workflow,
1249                                converter: &payload_converter,
1250                            };
1251                            Some(UserMetadata {
1252                                summary: options.static_summary.map(|summary| {
1253                                    payload_converter.to_payload(&context, &summary).expect(
1254                                        "String-to-JSON payload serialization is infallible",
1255                                    )
1256                                }),
1257                                details: options.static_details.map(|details| {
1258                                    payload_converter.to_payload(&context, &details).expect(
1259                                        "String-to-JSON payload serialization is infallible",
1260                                    )
1261                                }),
1262                            })
1263                        } else {
1264                            None
1265                        };
1266
1267                        let run_id = if let Some(start_signal) = options.start_signal {
1268                            let mut request = SignalWithStartWorkflowExecutionRequest {
1269                                namespace,
1270                                workflow_id: workflow_id.clone(),
1271                                workflow_type: Some(WorkflowType {
1272                                    name: workflow_type,
1273                                }),
1274                                task_queue: Some(TaskQueue {
1275                                    name: task_queue_name,
1276                                    kind: TaskQueueKind::Normal as i32,
1277                                    normal_name: String::new(),
1278                                }),
1279                                input: payloads.into_payloads(),
1280                                signal_name: start_signal.signal_name,
1281                                signal_input: start_signal.input,
1282                                identity: client.identity(),
1283                                request_id: Uuid::new_v4().to_string(),
1284                                workflow_id_reuse_policy: options.id_reuse_policy as i32,
1285                                workflow_id_conflict_policy: options.id_conflict_policy as i32,
1286                                workflow_execution_timeout: options
1287                                    .execution_timeout
1288                                    .and_then(|duration| duration.try_into().ok()),
1289                                workflow_run_timeout: options
1290                                    .run_timeout
1291                                    .and_then(|duration| duration.try_into().ok()),
1292                                workflow_task_timeout: options
1293                                    .task_timeout
1294                                    .and_then(|duration| duration.try_into().ok()),
1295                                search_attributes: options
1296                                    .search_attributes
1297                                    .map(|attributes| attributes.into_proto()),
1298                                cron_schedule: options.cron_schedule.unwrap_or_default(),
1299                                retry_policy: options.retry_policy.map(Into::into),
1300                                header: options.header.or(start_signal.header),
1301                                user_metadata,
1302                                ..Default::default()
1303                            }
1304                            .into_request();
1305                            rpc_options.apply_to(&mut request);
1306                            WorkflowService::signal_with_start_workflow_execution(
1307                                &mut client,
1308                                request,
1309                            )
1310                            .await?
1311                            .into_inner()
1312                            .run_id
1313                        } else {
1314                            let mut request = StartWorkflowExecutionRequest {
1315                                namespace,
1316                                input: payloads.into_payloads(),
1317                                workflow_id: workflow_id.clone(),
1318                                workflow_type: Some(WorkflowType {
1319                                    name: workflow_type,
1320                                }),
1321                                task_queue: Some(TaskQueue {
1322                                    name: task_queue_name,
1323                                    kind: TaskQueueKind::Unspecified as i32,
1324                                    normal_name: String::new(),
1325                                }),
1326                                request_id: Uuid::new_v4().to_string(),
1327                                workflow_id_reuse_policy: options.id_reuse_policy as i32,
1328                                workflow_id_conflict_policy: options.id_conflict_policy as i32,
1329                                workflow_execution_timeout: options
1330                                    .execution_timeout
1331                                    .and_then(|duration| duration.try_into().ok()),
1332                                workflow_run_timeout: options
1333                                    .run_timeout
1334                                    .and_then(|duration| duration.try_into().ok()),
1335                                workflow_task_timeout: options
1336                                    .task_timeout
1337                                    .and_then(|duration| duration.try_into().ok()),
1338                                search_attributes: options
1339                                    .search_attributes
1340                                    .map(|attributes| attributes.into_proto()),
1341                                cron_schedule: options.cron_schedule.unwrap_or_default(),
1342                                request_eager_execution: options.enable_eager_workflow_start,
1343                                retry_policy: options.retry_policy.map(Into::into),
1344                                links: options.links,
1345                                completion_callbacks: options.completion_callbacks,
1346                                priority: Some(options.priority.into()),
1347                                header: options.header,
1348                                user_metadata,
1349                                ..Default::default()
1350                            }
1351                            .into_request();
1352                            rpc_options.apply_to(&mut request);
1353                            client
1354                                .start_workflow_execution(request)
1355                                .await
1356                                .map_err(|status| {
1357                                    if status.code() == Code::AlreadyExists {
1358                                        let run_id = decode_status_detail::<
1359                                            WorkflowExecutionAlreadyStartedFailure,
1360                                        >(
1361                                            status.details()
1362                                        )
1363                                        .map(|failure| failure.run_id);
1364                                        WorkflowStartError::AlreadyStarted {
1365                                            run_id,
1366                                            source: status,
1367                                        }
1368                                    } else {
1369                                        WorkflowStartError::Rpc(status)
1370                                    }
1371                                })?
1372                                .into_inner()
1373                                .run_id
1374                        };
1375
1376                        Ok(StartWorkflowOutput::new(workflow_id, run_id))
1377                    })
1378                }
1379            }),
1380        )
1381        .await?;
1382        let StartWorkflowOutput {
1383            workflow_id,
1384            run_id,
1385        } = interceptor_output;
1386
1387        Ok(WorkflowHandle::new(
1388            self.clone(),
1389            WorkflowExecutionInfo {
1390                namespace,
1391                workflow_id,
1392                run_id: Some(run_id.clone()),
1393                first_execution_run_id: Some(run_id),
1394            },
1395        ))
1396    }
1397
1398    fn get_workflow_handle<W: HasWorkflowDefinition>(
1399        &self,
1400        workflow_id: impl Into<String>,
1401    ) -> WorkflowHandle<Self, W>
1402    where
1403        Self: Sized,
1404    {
1405        WorkflowHandle::new(
1406            self.clone(),
1407            WorkflowExecutionInfo {
1408                namespace: self.namespace(),
1409                workflow_id: workflow_id.into(),
1410                run_id: None,
1411                first_execution_run_id: None,
1412            },
1413        )
1414    }
1415
1416    fn list_workflows(
1417        &self,
1418        query: impl Into<String>,
1419        opts: WorkflowListOptions,
1420    ) -> ListWorkflowsStream {
1421        let client = self.clone();
1422        let namespace = self.namespace();
1423        let query = query.into();
1424        let limit = opts.limit;
1425        let rpc_options = opts.rpc_options;
1426
1427        // State: (next_page_token, buffer, yielded_count, exhausted)
1428        let initial_state = (Vec::new(), VecDeque::new(), 0, false);
1429
1430        let stream = stream::unfold(
1431            initial_state,
1432            move |(next_page_token, mut buffer, mut yielded, exhausted)| {
1433                let client = client.clone();
1434                let namespace = namespace.clone();
1435                let query = query.clone();
1436                let rpc_options = rpc_options.clone();
1437
1438                async move {
1439                    if let Some(l) = limit
1440                        && yielded >= l
1441                    {
1442                        return None;
1443                    }
1444
1445                    if let Some(exec) = buffer.pop_front() {
1446                        yielded += 1;
1447                        return Some((Ok(exec), (next_page_token, buffer, yielded, exhausted)));
1448                    }
1449
1450                    if exhausted {
1451                        return None;
1452                    }
1453
1454                    let response = interceptors::call_list_workflows_page(
1455                        client.client_interceptors(),
1456                        ListWorkflowsPageInput {
1457                            query,
1458                            next_page_token: next_page_token.clone(),
1459                            rpc_options,
1460                        },
1461                        Next::new({
1462                            let mut rpc_client = client.clone();
1463                            move |input: ListWorkflowsPageInput| -> BoxFuture<
1464                                '_,
1465                                Result<ListWorkflowsPageOutput, ClientError>,
1466                            > {
1467                                Box::pin(async move {
1468                                    let mut request = ListWorkflowExecutionsRequest {
1469                                        namespace,
1470                                        page_size: 0,
1471                                        next_page_token: input.next_page_token,
1472                                        query: input.query,
1473                                    }
1474                                    .into_request();
1475                                    input.rpc_options.apply_to(&mut request);
1476                                    let response = WorkflowService::list_workflow_executions(
1477                                        &mut rpc_client,
1478                                        request,
1479                                    )
1480                                    .await?
1481                                    .into_inner();
1482                                    Ok(ListWorkflowsPageOutput::new(
1483                                        response.executions,
1484                                        response.next_page_token,
1485                                    ))
1486                                })
1487                            }
1488                        }),
1489                    )
1490                    .await;
1491
1492                    match response {
1493                        Ok(mut output) => {
1494                            let new_exhausted = output.next_page_token.is_empty();
1495                            let new_token = output.next_page_token;
1496
1497                            let data_converter = client.data_converter().clone();
1498                            for execution in &mut output.executions {
1499                                if let Some(memo) = execution.memo.as_mut()
1500                                    && let Err(err) = decode_payloads(
1501                                        memo,
1502                                        data_converter.codec(),
1503                                        &SerializationContextData::Workflow,
1504                                    )
1505                                    .await
1506                                {
1507                                    return Some((
1508                                        Err(ClientError::from(err)),
1509                                        (new_token, buffer, yielded, true),
1510                                    ));
1511                                }
1512                            }
1513                            buffer = output
1514                                .executions
1515                                .into_iter()
1516                                .map(|raw| {
1517                                    WorkflowExecution::new_with_data_converter(
1518                                        raw,
1519                                        data_converter.clone(),
1520                                    )
1521                                })
1522                                .collect();
1523
1524                            if let Some(exec) = buffer.pop_front() {
1525                                yielded += 1;
1526                                Some((Ok(exec), (new_token, buffer, yielded, new_exhausted)))
1527                            } else {
1528                                None
1529                            }
1530                        }
1531                        Err(e) => Some((Err(e), (next_page_token, buffer, yielded, true))),
1532                    }
1533                }
1534            },
1535        );
1536
1537        ListWorkflowsStream::new(Box::pin(stream))
1538    }
1539
1540    async fn count_workflows(
1541        &self,
1542        query: impl Into<String>,
1543        opts: WorkflowCountOptions,
1544    ) -> Result<WorkflowExecutionCount, ClientError> {
1545        let output = interceptors::call_count_workflows(
1546            self.client_interceptors(),
1547            CountWorkflowsInput {
1548                query: query.into(),
1549                options: opts,
1550            },
1551            Next::new({
1552                let mut client = (*self).clone();
1553                move |input: CountWorkflowsInput| -> BoxFuture<
1554                    '_,
1555                    Result<CountWorkflowsOutput, ClientError>,
1556                > {
1557                    Box::pin(async move {
1558                        let mut request = CountWorkflowExecutionsRequest {
1559                            namespace: client.namespace(),
1560                            query: input.query,
1561                        }
1562                        .into_request();
1563                        input.options.rpc_options.apply_to(&mut request);
1564                        let response = WorkflowService::count_workflow_executions(
1565                            &mut client,
1566                            request,
1567                        )
1568                        .await?
1569                        .into_inner();
1570                        Ok(CountWorkflowsOutput::new(response))
1571                    })
1572                }
1573            }),
1574        )
1575        .await?;
1576
1577        Ok(WorkflowExecutionCount::from_response(output.response))
1578    }
1579
1580    fn get_async_activity_handle(&self, identifier: ActivityIdentifier) -> AsyncActivityHandle<Self>
1581    where
1582        Self: Sized,
1583    {
1584        AsyncActivityHandle::new(self.clone(), identifier)
1585    }
1586}
1587
1588macro_rules! dbg_panic {
1589  ($($arg:tt)*) => {
1590      use tracing::error;
1591      error!($($arg)*);
1592      debug_assert!(false, $($arg)*);
1593  };
1594}
1595pub(crate) use dbg_panic;
1596
1597#[cfg(test)]
1598mod tests {
1599    use super::*;
1600    use crate::callback_based::CallbackBasedGrpcService;
1601    use std::sync::atomic::{AtomicUsize, Ordering};
1602    use std::time::Instant;
1603    use temporalio_common::search_attributes::SearchAttributeKey;
1604    use tonic::{Status, metadata::Ascii};
1605    use url::Url;
1606
1607    #[test]
1608    fn count_aggregation_group_gets_typed_value() {
1609        let attrs = SearchAttributes::new([SearchAttributeKey::int("group").value_set(42)]);
1610        let group = WorkflowCountAggregationGroup {
1611            raw: count_workflow_executions_response::AggregationGroup {
1612                group_values: vec![attrs.raw_payload("group").unwrap().clone()],
1613                count: 1,
1614            },
1615        };
1616
1617        assert_eq!(group.get::<i64>(0), Some(42));
1618        assert_eq!(group.get::<i64>(1), None);
1619        assert!(group.try_get::<String>(0).is_err());
1620        assert_eq!(group.try_get::<i64>(1).unwrap(), None);
1621    }
1622
1623    fn connection_options_for_system_info_test(
1624        service_override: CallbackBasedGrpcService,
1625    ) -> ConnectionOptions {
1626        ConnectionOptions::new(Url::parse("http://localhost:7233").unwrap())
1627            .service_override(service_override)
1628            .dns_load_balancing(None)
1629            .build()
1630    }
1631
1632    #[test]
1633    fn applies_headers() {
1634        // Initial header set
1635        let headers = Arc::new(RwLock::new(ClientHeaders {
1636            user_headers: HashMap::new(),
1637            user_binary_headers: HashMap::new(),
1638            api_key: Some("my-api-key".to_owned()),
1639        }));
1640        headers.clone().write().user_headers.insert(
1641            "my-meta-key".parse().unwrap(),
1642            "my-meta-val".parse().unwrap(),
1643        );
1644        headers.clone().write().user_binary_headers.insert(
1645            "my-bin-meta-key-bin".parse().unwrap(),
1646            vec![1, 2, 3].try_into().unwrap(),
1647        );
1648        let mut interceptor = ServiceCallInterceptor {
1649            client_name: "cute-kitty".to_string(),
1650            client_version: "0.1.0".to_string(),
1651            headers: headers.clone(),
1652        };
1653
1654        // Confirm on metadata
1655        let req = interceptor.call(tonic::Request::new(())).unwrap();
1656        assert_eq!(req.metadata().get("my-meta-key").unwrap(), "my-meta-val");
1657        assert_eq!(
1658            req.metadata().get("authorization").unwrap(),
1659            "Bearer my-api-key"
1660        );
1661        assert_eq!(
1662            req.metadata().get_bin("my-bin-meta-key-bin").unwrap(),
1663            vec![1, 2, 3].as_slice()
1664        );
1665
1666        // Overwrite at request time
1667        let mut req = tonic::Request::new(());
1668        req.metadata_mut()
1669            .insert("my-meta-key", "my-meta-val2".parse().unwrap());
1670        req.metadata_mut()
1671            .insert("authorization", "my-api-key2".parse().unwrap());
1672        req.metadata_mut()
1673            .insert_bin("my-bin-meta-key-bin", vec![4, 5, 6].try_into().unwrap());
1674        let req = interceptor.call(req).unwrap();
1675        assert_eq!(req.metadata().get("my-meta-key").unwrap(), "my-meta-val2");
1676        assert_eq!(req.metadata().get("authorization").unwrap(), "my-api-key2");
1677        assert_eq!(
1678            req.metadata().get_bin("my-bin-meta-key-bin").unwrap(),
1679            vec![4, 5, 6].as_slice()
1680        );
1681
1682        // Overwrite auth on header
1683        headers.clone().write().user_headers.insert(
1684            "authorization".parse().unwrap(),
1685            "my-api-key3".parse().unwrap(),
1686        );
1687        let req = interceptor.call(tonic::Request::new(())).unwrap();
1688        assert_eq!(req.metadata().get("my-meta-key").unwrap(), "my-meta-val");
1689        assert_eq!(req.metadata().get("authorization").unwrap(), "my-api-key3");
1690
1691        // Remove headers and auth and confirm gone
1692        headers.clone().write().user_headers.clear();
1693        headers.clone().write().user_binary_headers.clear();
1694        headers.clone().write().api_key.take();
1695        let req = interceptor.call(tonic::Request::new(())).unwrap();
1696        assert!(!req.metadata().contains_key("my-meta-key"));
1697        assert!(!req.metadata().contains_key("authorization"));
1698        assert!(!req.metadata().contains_key("my-bin-meta-key-bin"));
1699
1700        // Timeout header not overriden
1701        let mut req = tonic::Request::new(());
1702        req.metadata_mut()
1703            .insert("grpc-timeout", "1S".parse().unwrap());
1704        let req = interceptor.call(req).unwrap();
1705        assert_eq!(
1706            req.metadata().get("grpc-timeout").unwrap(),
1707            "1S".parse::<MetadataValue<Ascii>>().unwrap()
1708        );
1709    }
1710
1711    #[test]
1712    fn invalid_ascii_header_key() {
1713        let invalid_headers = {
1714            let mut h = HashMap::new();
1715            h.insert("x-binary-key-bin".to_owned(), "value".to_owned());
1716            h
1717        };
1718
1719        let result = parse_ascii_headers(invalid_headers);
1720        assert!(result.is_err());
1721        assert_eq!(
1722            result.err().unwrap().to_string(),
1723            "Invalid ASCII header key 'x-binary-key-bin': invalid gRPC metadata key name"
1724        );
1725    }
1726
1727    #[test]
1728    fn invalid_ascii_header_value() {
1729        let invalid_headers = {
1730            let mut h = HashMap::new();
1731            // Nul bytes are valid UTF-8, but not valid ascii gRPC headers:
1732            h.insert("x-ascii-key".to_owned(), "\x00value".to_owned());
1733            h
1734        };
1735
1736        let result = parse_ascii_headers(invalid_headers);
1737        assert!(result.is_err());
1738        assert_eq!(
1739            result.err().unwrap().to_string(),
1740            "Invalid ASCII header value for key 'x-ascii-key': failed to parse metadata value"
1741        );
1742    }
1743
1744    #[test]
1745    fn invalid_binary_header_key() {
1746        let invalid_headers = {
1747            let mut h = HashMap::new();
1748            h.insert("x-ascii-key".to_owned(), vec![1, 2, 3]);
1749            h
1750        };
1751
1752        let result = parse_binary_headers(invalid_headers);
1753        assert!(result.is_err());
1754        assert_eq!(
1755            result.err().unwrap().to_string(),
1756            "Invalid binary header key 'x-ascii-key': invalid gRPC metadata key name"
1757        );
1758    }
1759
1760    #[test]
1761    fn keep_alive_defaults() {
1762        let opts = ConnectionOptions::new(Url::parse("https://smolkitty").unwrap())
1763            .identity("enchicat".to_string())
1764            .client_name("cute-kitty".to_string())
1765            .client_version("0.1.0".to_string())
1766            .build();
1767        assert_eq!(
1768            opts.keep_alive.clone().unwrap().interval,
1769            ClientKeepAliveOptions::default().interval
1770        );
1771        assert_eq!(
1772            opts.keep_alive.clone().unwrap().timeout,
1773            ClientKeepAliveOptions::default().timeout
1774        );
1775
1776        // Can be explicitly set to None
1777        let opts = ConnectionOptions::new(Url::parse("https://smolkitty").unwrap())
1778            .identity("enchicat".to_string())
1779            .client_name("cute-kitty".to_string())
1780            .client_version("0.1.0".to_string())
1781            .keep_alive(None)
1782            .build();
1783        dbg!(&opts.keep_alive);
1784        assert!(opts.keep_alive.is_none());
1785    }
1786
1787    #[rstest::rstest]
1788    #[case(
1789        "unknown method GetSystemInfo for service temporal.api.workflowservice.v1.WorkflowService"
1790    )]
1791    #[case("Method temporal.api.workflowservice.v1.WorkflowService/GetSystemInfo is unimplemented")]
1792    #[case(
1793        "The server does not implement the method /temporal.api.workflowservice.v1.WorkflowService/GetSystemInfo"
1794    )]
1795    #[tokio::test]
1796    async fn get_system_info_missing_method_falls_back_to_empty_capabilities(
1797        #[case] message: &'static str,
1798    ) {
1799        let attempts = Arc::new(AtomicUsize::new(0));
1800        let attempts_clone = attempts.clone();
1801        let service_override = CallbackBasedGrpcService {
1802            callback: Arc::new(move |req| {
1803                let attempts = attempts_clone.clone();
1804                Box::pin(async move {
1805                    assert_eq!(req.rpc, "GetSystemInfo");
1806                    attempts.fetch_add(1, Ordering::SeqCst);
1807                    Err(Status::unimplemented(message))
1808                })
1809            }),
1810        };
1811
1812        let connection =
1813            Connection::connect(connection_options_for_system_info_test(service_override))
1814                .await
1815                .unwrap();
1816
1817        assert!(connection.capabilities().is_none());
1818        assert_eq!(attempts.load(Ordering::SeqCst), 1);
1819    }
1820
1821    #[tokio::test]
1822    async fn get_system_info_non_missing_unimplemented_fails_connect() {
1823        let attempts = Arc::new(AtomicUsize::new(0));
1824        let attempts_clone = attempts.clone();
1825        let service_override = CallbackBasedGrpcService {
1826            callback: Arc::new(move |req| {
1827                let attempts = attempts_clone.clone();
1828                Box::pin(async move {
1829                    assert_eq!(req.rpc, "GetSystemInfo");
1830                    attempts.fetch_add(1, Ordering::SeqCst);
1831                    Err(Status::unimplemented("backend temporarily unimplemented"))
1832                })
1833            }),
1834        };
1835
1836        let err =
1837            match Connection::connect(connection_options_for_system_info_test(service_override))
1838                .await
1839            {
1840                Ok(_) => panic!("connection should fail"),
1841                Err(err) => err,
1842            };
1843
1844        assert!(matches!(
1845            err,
1846            ClientConnectError::SystemInfoCallError(status)
1847                if status.code() == Code::Unimplemented
1848                    && status.message() == "backend temporarily unimplemented"
1849        ));
1850        assert_eq!(attempts.load(Ordering::SeqCst), 1);
1851    }
1852
1853    #[tokio::test]
1854    async fn connect_timeout_bounds_connection_attempt() {
1855        let url = Url::parse("http://10.255.255.1:7233").unwrap();
1856        let opts = ConnectionOptions::new(url)
1857            .connect_timeout(Duration::from_millis(500))
1858            .build();
1859        let start = Instant::now();
1860        let result = Connection::connect(opts).await;
1861        assert!(result.is_err(), "connection should fail");
1862        assert!(start.elapsed() < Duration::from_secs(2));
1863    }
1864
1865    mod tls_custom_verifier_tests {
1866        use super::*;
1867        use tokio_rustls::rustls::{
1868            DigitallySignedStruct, Error as RustlsError, SignatureScheme,
1869            client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier},
1870            pki_types::{CertificateDer, ServerName, UnixTime},
1871        };
1872
1873        /// A minimal mock verifier for testing. In production, users would
1874        /// implement real certificate pinning or custom validation here.
1875        #[derive(Debug)]
1876        struct MockVerifier;
1877
1878        impl ServerCertVerifier for MockVerifier {
1879            fn verify_server_cert(
1880                &self,
1881                _end_entity: &CertificateDer<'_>,
1882                _intermediates: &[CertificateDer<'_>],
1883                _server_name: &ServerName<'_>,
1884                _ocsp_response: &[u8],
1885                _now: UnixTime,
1886            ) -> Result<ServerCertVerified, RustlsError> {
1887                Ok(ServerCertVerified::assertion())
1888            }
1889
1890            fn verify_tls12_signature(
1891                &self,
1892                _message: &[u8],
1893                _cert: &CertificateDer<'_>,
1894                _dss: &DigitallySignedStruct,
1895            ) -> Result<HandshakeSignatureValid, RustlsError> {
1896                Ok(HandshakeSignatureValid::assertion())
1897            }
1898
1899            fn verify_tls13_signature(
1900                &self,
1901                _message: &[u8],
1902                _cert: &CertificateDer<'_>,
1903                _dss: &DigitallySignedStruct,
1904            ) -> Result<HandshakeSignatureValid, RustlsError> {
1905                Ok(HandshakeSignatureValid::assertion())
1906            }
1907
1908            fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
1909                vec![
1910                    SignatureScheme::ECDSA_NISTP256_SHA256,
1911                    SignatureScheme::RSA_PSS_SHA256,
1912                ]
1913            }
1914        }
1915
1916        #[tokio::test]
1917        async fn add_tls_to_channel_with_custom_verifier() {
1918            let tls_opts = TlsOptions {
1919                server_cert_verifier: Some(Arc::new(MockVerifier)),
1920                domain: Some("test.temporal.io".to_string()),
1921                ..Default::default()
1922            };
1923            let endpoint = tonic::transport::Channel::from_static("https://test.temporal.io:7233");
1924            let result = add_tls_to_channel(Some(&tls_opts), endpoint).await;
1925            assert!(
1926                result.is_ok(),
1927                "add_tls_to_channel should succeed with a custom verifier: {:?}",
1928                result.err()
1929            );
1930        }
1931
1932        #[tokio::test]
1933        async fn add_tls_to_channel_with_verifier_and_ca_cert_fails() {
1934            // When both server_cert_verifier and server_root_ca_cert are set,
1935            // add_tls_to_channel should fail with InvalidConfig.
1936            let tls_opts = TlsOptions {
1937                server_root_ca_cert: Some(b"some-ca-cert-bytes".to_vec()),
1938                server_cert_verifier: Some(Arc::new(MockVerifier)),
1939                domain: Some("test.temporal.io".to_string()),
1940                ..Default::default()
1941            };
1942            let endpoint = tonic::transport::Channel::from_static("https://test.temporal.io:7233");
1943            let result = add_tls_to_channel(Some(&tls_opts), endpoint).await;
1944            assert!(
1945                matches!(result, Err(ClientConnectError::InvalidConfig(_))),
1946                "add_tls_to_channel should fail with InvalidConfig when both CA cert and verifier are set: {:?}",
1947                result
1948            );
1949        }
1950
1951        #[tokio::test]
1952        async fn add_tls_to_channel_without_verifier_still_works() {
1953            // Regression test: the original PEM path must still work.
1954            let tls_opts = TlsOptions {
1955                domain: Some("test.temporal.io".to_string()),
1956                ..Default::default()
1957            };
1958            let endpoint = tonic::transport::Channel::from_static("https://test.temporal.io:7233");
1959            let result = add_tls_to_channel(Some(&tls_opts), endpoint).await;
1960            assert!(
1961                result.is_ok(),
1962                "add_tls_to_channel should succeed without a verifier (native roots): {:?}",
1963                result.err()
1964            );
1965        }
1966    }
1967
1968    mod start_workflow_interceptor_tests {
1969        use super::*;
1970        use crate::request_extensions::RetryConfigForCall;
1971        use parking_lot::Mutex;
1972        use std::sync::atomic::{AtomicUsize, Ordering};
1973        use temporalio_common::{
1974            HasWorkflowDefinition, WorkflowDefinition,
1975            data_converters::{
1976                DefaultFailureConverter, PayloadCodec, PayloadConversionError,
1977                SerializationContext, SerializationContextData, TemporalSerializable,
1978            },
1979            protos::temporal::api::common::v1::Payload,
1980        };
1981        use tonic::{Request, Response};
1982
1983        struct TestWorkflow;
1984
1985        impl WorkflowDefinition for TestWorkflow {
1986            type Input = Vec<String>;
1987            type Output = ();
1988
1989            fn name(&self) -> &str {
1990                "test-workflow"
1991            }
1992        }
1993
1994        impl HasWorkflowDefinition for TestWorkflow {
1995            type Run = Self;
1996        }
1997
1998        #[derive(Default)]
1999        struct RecordedStart {
2000            calls: usize,
2001            workflow_type: String,
2002            payloads: Vec<Payload>,
2003            ascii_metadata: Option<String>,
2004            binary_metadata: Option<Vec<u8>>,
2005            grpc_timeout: Option<String>,
2006            retry_options: Option<RetryOptions>,
2007        }
2008
2009        struct CountingCodec {
2010            encode_calls: Arc<AtomicUsize>,
2011        }
2012
2013        impl PayloadCodec for CountingCodec {
2014            fn encode(
2015                &self,
2016                _context: &SerializationContextData,
2017                payloads: Vec<Payload>,
2018            ) -> futures_util::future::BoxFuture<
2019                'static,
2020                Result<Vec<Payload>, PayloadConversionError>,
2021            > {
2022                self.encode_calls.fetch_add(1, Ordering::SeqCst);
2023                Box::pin(async move { Ok(payloads) })
2024            }
2025
2026            fn decode(
2027                &self,
2028                _context: &SerializationContextData,
2029                payloads: Vec<Payload>,
2030            ) -> futures_util::future::BoxFuture<
2031                'static,
2032                Result<Vec<Payload>, PayloadConversionError>,
2033            > {
2034                Box::pin(async move { Ok(payloads) })
2035            }
2036        }
2037
2038        #[derive(Clone)]
2039        struct MockStartWorkflowClient {
2040            recorded: Arc<Mutex<RecordedStart>>,
2041            data_converter: DataConverter,
2042        }
2043
2044        impl NamespacedClient for MockStartWorkflowClient {
2045            fn namespace(&self) -> String {
2046                "test-namespace".to_owned()
2047            }
2048
2049            fn identity(&self) -> String {
2050                "test-identity".to_owned()
2051            }
2052
2053            fn data_converter(&self) -> &DataConverter {
2054                &self.data_converter
2055            }
2056        }
2057
2058        impl WorkflowService for MockStartWorkflowClient {
2059            fn start_workflow_execution(
2060                &mut self,
2061                request: Request<StartWorkflowExecutionRequest>,
2062            ) -> futures_util::future::BoxFuture<
2063                '_,
2064                Result<Response<StartWorkflowExecutionResponse>, tonic::Status>,
2065            > {
2066                let ascii_metadata = request
2067                    .metadata()
2068                    .get("call-meta")
2069                    .map(|value| value.to_str().unwrap().to_owned());
2070                let binary_metadata = request
2071                    .metadata()
2072                    .get_bin("call-meta-bin")
2073                    .map(|value| value.to_bytes().unwrap().to_vec());
2074                let grpc_timeout = request
2075                    .metadata()
2076                    .get("grpc-timeout")
2077                    .map(|value| value.to_str().unwrap().to_owned());
2078                let retry_options = request
2079                    .extensions()
2080                    .get::<RetryConfigForCall>()
2081                    .map(|config| config.0.clone());
2082                let request = request.into_inner();
2083                let mut recorded = self.recorded.lock();
2084                recorded.calls += 1;
2085                recorded.workflow_type = request.workflow_type.unwrap().name;
2086                recorded.payloads = request.input.unwrap_or_default().payloads;
2087                recorded.ascii_metadata = ascii_metadata;
2088                recorded.binary_metadata = binary_metadata;
2089                recorded.grpc_timeout = grpc_timeout;
2090                recorded.retry_options = retry_options;
2091
2092                Box::pin(async {
2093                    Ok(Response::new(StartWorkflowExecutionResponse {
2094                        run_id: "server-run-id".to_owned(),
2095                        ..Default::default()
2096                    }))
2097                })
2098            }
2099
2100            fn signal_with_start_workflow_execution(
2101                &mut self,
2102                request: Request<SignalWithStartWorkflowExecutionRequest>,
2103            ) -> futures_util::future::BoxFuture<
2104                '_,
2105                Result<Response<SignalWithStartWorkflowExecutionResponse>, tonic::Status>,
2106            > {
2107                let ascii_metadata = request
2108                    .metadata()
2109                    .get("call-meta")
2110                    .map(|value| value.to_str().unwrap().to_owned());
2111                let binary_metadata = request
2112                    .metadata()
2113                    .get_bin("call-meta-bin")
2114                    .map(|value| value.to_bytes().unwrap().to_vec());
2115                let grpc_timeout = request
2116                    .metadata()
2117                    .get("grpc-timeout")
2118                    .map(|value| value.to_str().unwrap().to_owned());
2119                let retry_options = request
2120                    .extensions()
2121                    .get::<RetryConfigForCall>()
2122                    .map(|config| config.0.clone());
2123                let request = request.into_inner();
2124                let mut recorded = self.recorded.lock();
2125                recorded.calls += 1;
2126                recorded.workflow_type = request.workflow_type.unwrap().name;
2127                recorded.payloads = request.input.unwrap_or_default().payloads;
2128                recorded.ascii_metadata = ascii_metadata;
2129                recorded.binary_metadata = binary_metadata;
2130                recorded.grpc_timeout = grpc_timeout;
2131                recorded.retry_options = retry_options;
2132
2133                Box::pin(async {
2134                    Ok(Response::new(SignalWithStartWorkflowExecutionResponse {
2135                        run_id: "signal-server-run-id".to_owned(),
2136                        ..Default::default()
2137                    }))
2138                })
2139            }
2140        }
2141
2142        #[derive(Clone)]
2143        struct InterceptedClient {
2144            inner: MockStartWorkflowClient,
2145            interceptors: Vec<Arc<dyn ClientInterceptor>>,
2146        }
2147
2148        impl NamespacedClient for InterceptedClient {
2149            fn namespace(&self) -> String {
2150                self.inner.namespace()
2151            }
2152
2153            fn identity(&self) -> String {
2154                self.inner.identity()
2155            }
2156
2157            fn data_converter(&self) -> &DataConverter {
2158                self.inner.data_converter()
2159            }
2160
2161            fn client_interceptors(&self) -> &[Arc<dyn ClientInterceptor>] {
2162                &self.interceptors
2163            }
2164        }
2165
2166        impl WorkflowService for InterceptedClient {
2167            fn start_workflow_execution(
2168                &mut self,
2169                request: Request<StartWorkflowExecutionRequest>,
2170            ) -> futures_util::future::BoxFuture<
2171                '_,
2172                Result<Response<StartWorkflowExecutionResponse>, tonic::Status>,
2173            > {
2174                self.inner.start_workflow_execution(request)
2175            }
2176
2177            fn signal_with_start_workflow_execution(
2178                &mut self,
2179                request: Request<SignalWithStartWorkflowExecutionRequest>,
2180            ) -> futures_util::future::BoxFuture<
2181                '_,
2182                Result<Response<SignalWithStartWorkflowExecutionResponse>, tonic::Status>,
2183            > {
2184                self.inner.signal_with_start_workflow_execution(request)
2185            }
2186        }
2187
2188        struct OrderedInterceptor {
2189            name: &'static str,
2190            events: Arc<Mutex<Vec<String>>>,
2191            encode_calls: Arc<AtomicUsize>,
2192        }
2193
2194        impl ClientInterceptor for OrderedInterceptor {
2195            fn start_workflow<'a>(
2196                &'a self,
2197                mut input: StartWorkflowInput,
2198                next: Next<
2199                    'a,
2200                    StartWorkflowInput,
2201                    BoxFuture<'a, Result<StartWorkflowOutput, WorkflowStartError>>,
2202                >,
2203            ) -> BoxFuture<'a, Result<StartWorkflowOutput, WorkflowStartError>> {
2204                Box::pin(async move {
2205                    assert_eq!(self.encode_calls.load(Ordering::SeqCst), 0);
2206                    self.events.lock().push(format!("{}-pre", self.name));
2207                    tokio::task::yield_now().await;
2208                    if self.name == "outer" {
2209                        input
2210                            .args_mut::<Vec<String>>()
2211                            .unwrap()
2212                            .push("mutated".to_owned());
2213                    } else {
2214                        assert_eq!(
2215                            input.args_ref::<Vec<String>>().unwrap(),
2216                            &["initial".to_owned(), "mutated".to_owned()]
2217                        );
2218                        input.replace_args("replacement".to_owned());
2219                        input.workflow_type = "replacement-workflow".to_owned();
2220                    }
2221                    let result = next.run(input).await;
2222                    tokio::task::yield_now().await;
2223                    self.events.lock().push(format!("{}-post", self.name));
2224                    result
2225                })
2226            }
2227        }
2228
2229        struct ShortCircuitInterceptor;
2230
2231        impl ClientInterceptor for ShortCircuitInterceptor {
2232            fn start_workflow<'a>(
2233                &'a self,
2234                input: StartWorkflowInput,
2235                _next: Next<
2236                    'a,
2237                    StartWorkflowInput,
2238                    BoxFuture<'a, Result<StartWorkflowOutput, WorkflowStartError>>,
2239                >,
2240            ) -> BoxFuture<'a, Result<StartWorkflowOutput, WorkflowStartError>> {
2241                assert_eq!(
2242                    input.args_ref::<Vec<String>>().unwrap(),
2243                    &["initial".to_owned()]
2244                );
2245                Box::pin(async {
2246                    Ok(StartWorkflowOutput::new(
2247                        "short-circuit-workflow-id",
2248                        "short-circuit-run-id",
2249                    ))
2250                })
2251            }
2252        }
2253
2254        struct CountingInput {
2255            conversion_calls: Arc<AtomicUsize>,
2256        }
2257
2258        impl TemporalSerializable for CountingInput {
2259            fn to_payloads(
2260                &self,
2261                _context: &SerializationContext<'_>,
2262            ) -> Result<Vec<Payload>, PayloadConversionError> {
2263                self.conversion_calls.fetch_add(1, Ordering::SeqCst);
2264                Ok(vec![Payload::default()])
2265            }
2266        }
2267
2268        struct ConversionTimingInterceptor {
2269            conversion_calls: Arc<AtomicUsize>,
2270        }
2271
2272        impl ClientInterceptor for ConversionTimingInterceptor {
2273            fn start_workflow<'a>(
2274                &'a self,
2275                mut input: StartWorkflowInput,
2276                next: Next<
2277                    'a,
2278                    StartWorkflowInput,
2279                    BoxFuture<'a, Result<StartWorkflowOutput, WorkflowStartError>>,
2280                >,
2281            ) -> BoxFuture<'a, Result<StartWorkflowOutput, WorkflowStartError>> {
2282                input.replace_args(CountingInput {
2283                    conversion_calls: self.conversion_calls.clone(),
2284                });
2285                let future = next.run(input);
2286                assert_eq!(self.conversion_calls.load(Ordering::SeqCst), 0);
2287                future
2288            }
2289        }
2290
2291        fn mock_client(
2292            interceptors: Vec<Arc<dyn ClientInterceptor>>,
2293            encode_calls: Arc<AtomicUsize>,
2294        ) -> (InterceptedClient, Arc<Mutex<RecordedStart>>) {
2295            let recorded = Arc::new(Mutex::new(RecordedStart::default()));
2296            let data_converter = DataConverter::new(
2297                PayloadConverter::default(),
2298                DefaultFailureConverter,
2299                CountingCodec {
2300                    encode_calls: encode_calls.clone(),
2301                },
2302            );
2303            (
2304                InterceptedClient {
2305                    inner: MockStartWorkflowClient {
2306                        recorded: recorded.clone(),
2307                        data_converter,
2308                    },
2309                    interceptors,
2310                },
2311                recorded,
2312            )
2313        }
2314
2315        #[tokio::test]
2316        async fn interceptors_order_mutate_replace_and_defer_conversion() {
2317            let events = Arc::new(Mutex::new(Vec::new()));
2318            let encode_calls = Arc::new(AtomicUsize::new(0));
2319            let interceptors: Vec<Arc<dyn ClientInterceptor>> = vec![
2320                Arc::new(OrderedInterceptor {
2321                    name: "outer",
2322                    events: events.clone(),
2323                    encode_calls: encode_calls.clone(),
2324                }),
2325                Arc::new(OrderedInterceptor {
2326                    name: "inner",
2327                    events: events.clone(),
2328                    encode_calls: encode_calls.clone(),
2329                }),
2330            ];
2331            let (client, recorded) = mock_client(interceptors, encode_calls.clone());
2332
2333            let handle = client
2334                .start_workflow(
2335                    TestWorkflow,
2336                    vec!["initial".to_owned()],
2337                    WorkflowStartOptions::new("task-queue", "workflow-id").build(),
2338                )
2339                .await
2340                .unwrap();
2341
2342            assert_eq!(
2343                events.lock().as_slice(),
2344                ["outer-pre", "inner-pre", "inner-post", "outer-post"]
2345            );
2346            assert_eq!(encode_calls.load(Ordering::SeqCst), 1);
2347            assert_eq!(handle.run_id(), Some("server-run-id"));
2348            let payloads = {
2349                let recorded = recorded.lock();
2350                assert_eq!(recorded.calls, 1);
2351                assert_eq!(recorded.workflow_type, "replacement-workflow");
2352                recorded.payloads.clone()
2353            };
2354            let replacement: String = client
2355                .data_converter()
2356                .from_payloads(&SerializationContextData::Workflow, payloads)
2357                .await
2358                .unwrap();
2359            assert_eq!(replacement, "replacement");
2360        }
2361
2362        #[tokio::test]
2363        async fn interceptor_can_short_circuit() {
2364            let encode_calls = Arc::new(AtomicUsize::new(0));
2365            let (client, recorded) = mock_client(
2366                vec![Arc::new(ShortCircuitInterceptor)],
2367                encode_calls.clone(),
2368            );
2369            let handle = client
2370                .start_workflow(
2371                    TestWorkflow,
2372                    vec!["initial".to_owned()],
2373                    WorkflowStartOptions::new("task-queue", "ignored-workflow-id").build(),
2374                )
2375                .await
2376                .unwrap();
2377
2378            assert_eq!(handle.info().workflow_id, "short-circuit-workflow-id");
2379            assert_eq!(handle.run_id(), Some("short-circuit-run-id"));
2380            assert_eq!(recorded.lock().calls, 0);
2381            assert_eq!(encode_calls.load(Ordering::SeqCst), 0);
2382        }
2383
2384        #[tokio::test]
2385        async fn payload_conversion_waits_for_next_future_poll() {
2386            let conversion_calls = Arc::new(AtomicUsize::new(0));
2387            let encode_calls = Arc::new(AtomicUsize::new(0));
2388            let recorded = Arc::new(Mutex::new(RecordedStart::default()));
2389            let data_converter = DataConverter::new(
2390                PayloadConverter::UseWrappers,
2391                DefaultFailureConverter,
2392                CountingCodec {
2393                    encode_calls: encode_calls.clone(),
2394                },
2395            );
2396            let client = InterceptedClient {
2397                inner: MockStartWorkflowClient {
2398                    recorded: recorded.clone(),
2399                    data_converter,
2400                },
2401                interceptors: vec![Arc::new(ConversionTimingInterceptor {
2402                    conversion_calls: conversion_calls.clone(),
2403                })],
2404            };
2405
2406            client
2407                .start_workflow(
2408                    TestWorkflow,
2409                    vec!["initial".to_owned()],
2410                    WorkflowStartOptions::new("task-queue", "workflow-id").build(),
2411                )
2412                .await
2413                .unwrap();
2414
2415            assert_eq!(conversion_calls.load(Ordering::SeqCst), 1);
2416            assert_eq!(encode_calls.load(Ordering::SeqCst), 1);
2417            assert_eq!(recorded.lock().calls, 1);
2418        }
2419
2420        #[tokio::test]
2421        async fn custom_client_defaults_to_empty_chain() {
2422            let recorded = Arc::new(Mutex::new(RecordedStart::default()));
2423            let client = MockStartWorkflowClient {
2424                recorded: recorded.clone(),
2425                data_converter: DataConverter::default(),
2426            };
2427            assert!(client.client_interceptors().is_empty());
2428
2429            client
2430                .start_workflow(
2431                    TestWorkflow,
2432                    vec!["initial".to_owned()],
2433                    WorkflowStartOptions::new("task-queue", "workflow-id").build(),
2434                )
2435                .await
2436                .unwrap();
2437            assert_eq!(recorded.lock().calls, 1);
2438        }
2439
2440        #[tokio::test]
2441        async fn rpc_options_reach_the_request() {
2442            let (client, recorded) = mock_client(Vec::new(), Arc::new(AtomicUsize::new(0)));
2443            let mut rpc_options = RpcOptions {
2444                timeout: Some(Duration::from_millis(250)),
2445                retry_options: Some(RetryOptions::no_retries()),
2446                ..Default::default()
2447            };
2448            rpc_options
2449                .metadata
2450                .insert("call-meta", "call-value")
2451                .unwrap();
2452            rpc_options
2453                .metadata
2454                .insert_binary("call-meta-bin", vec![0, 255])
2455                .unwrap();
2456            let mut options = WorkflowStartOptions::new("task-queue", "workflow-id").build();
2457            options.rpc_options = rpc_options.clone();
2458
2459            client
2460                .start_workflow(TestWorkflow, vec!["initial".to_owned()], options)
2461                .await
2462                .unwrap();
2463
2464            {
2465                let recorded = recorded.lock();
2466                assert_eq!(recorded.ascii_metadata.as_deref(), Some("call-value"));
2467                assert_eq!(recorded.binary_metadata.as_deref(), Some(&[0, 255][..]));
2468                assert_eq!(recorded.grpc_timeout.as_deref(), Some("250000u"));
2469                assert_eq!(recorded.retry_options, Some(RetryOptions::no_retries()));
2470            }
2471
2472            let mut options = WorkflowStartOptions::new("task-queue", "signal-workflow-id").build();
2473            options.start_signal = Some(WorkflowStartSignal::new("signal-name").build());
2474            options.rpc_options = rpc_options;
2475            let handle = client
2476                .start_workflow(TestWorkflow, vec!["initial".to_owned()], options)
2477                .await
2478                .unwrap();
2479
2480            let recorded = recorded.lock();
2481            assert_eq!(recorded.calls, 2);
2482            assert_eq!(recorded.ascii_metadata.as_deref(), Some("call-value"));
2483            assert_eq!(recorded.binary_metadata.as_deref(), Some(&[0, 255][..]));
2484            assert_eq!(recorded.grpc_timeout.as_deref(), Some("250000u"));
2485            assert_eq!(recorded.retry_options, Some(RetryOptions::no_retries()));
2486            assert_eq!(handle.run_id(), Some("signal-server-run-id"));
2487        }
2488
2489        #[test]
2490        fn rpc_metadata_combines_with_and_overrides_connection_defaults() {
2491            let headers = Arc::new(RwLock::new(ClientHeaders {
2492                user_headers: HashMap::from([
2493                    (
2494                        "shared-meta".parse().unwrap(),
2495                        "connection-value".parse().unwrap(),
2496                    ),
2497                    (
2498                        "connection-meta".parse().unwrap(),
2499                        "connection-only".parse().unwrap(),
2500                    ),
2501                ]),
2502                user_binary_headers: HashMap::from([
2503                    (
2504                        "shared-meta-bin".parse().unwrap(),
2505                        BinaryMetadataValue::from_bytes(&[1]),
2506                    ),
2507                    (
2508                        "connection-meta-bin".parse().unwrap(),
2509                        BinaryMetadataValue::from_bytes(&[2]),
2510                    ),
2511                ]),
2512                api_key: None,
2513            }));
2514            let mut service_interceptor = ServiceCallInterceptor {
2515                client_name: "test-client".to_owned(),
2516                client_version: "test-version".to_owned(),
2517                headers,
2518            };
2519            let mut rpc_options = RpcOptions::default();
2520            rpc_options
2521                .metadata
2522                .insert("shared-meta", "call-value")
2523                .unwrap();
2524            rpc_options
2525                .metadata
2526                .insert("call-meta", "call-only")
2527                .unwrap();
2528            rpc_options
2529                .metadata
2530                .insert_binary("shared-meta-bin", vec![3])
2531                .unwrap();
2532            rpc_options
2533                .metadata
2534                .insert_binary("call-meta-bin", vec![4])
2535                .unwrap();
2536            let mut request = Request::new(());
2537            rpc_options.apply_to(&mut request);
2538
2539            let request = service_interceptor.call(request).unwrap();
2540            assert_eq!(request.metadata().get("shared-meta").unwrap(), "call-value");
2541            assert_eq!(request.metadata().get("call-meta").unwrap(), "call-only");
2542            assert_eq!(
2543                request.metadata().get("connection-meta").unwrap(),
2544                "connection-only"
2545            );
2546            assert_eq!(
2547                request.metadata().get_bin("shared-meta-bin").unwrap(),
2548                &[3][..]
2549            );
2550            assert_eq!(
2551                request.metadata().get_bin("call-meta-bin").unwrap(),
2552                &[4][..]
2553            );
2554            assert_eq!(
2555                request.metadata().get_bin("connection-meta-bin").unwrap(),
2556                &[2][..]
2557            );
2558        }
2559    }
2560
2561    mod list_workflows_tests {
2562        use super::*;
2563        use crate::test_helpers::{FailingCodec, XorCodec};
2564        use futures_util::{FutureExt, StreamExt};
2565        use std::sync::atomic::{AtomicUsize, Ordering};
2566        use temporalio_common::{
2567            data_converters::DefaultFailureConverter,
2568            protos::temporal::api::common::v1::{
2569                Memo as ProtoMemo, Payload, WorkflowExecution as ProtoWorkflowExecution,
2570            },
2571        };
2572        use tonic::{Request, Response};
2573
2574        #[derive(Clone)]
2575        struct MockListWorkflowsClient {
2576            call_count: Arc<AtomicUsize>,
2577            // Returns this many workflows per page
2578            page_size: usize,
2579            // Total workflows available
2580            total_workflows: usize,
2581            data_converter: DataConverter,
2582            memo_payload: Option<Payload>,
2583            interceptors: Vec<Arc<dyn ClientInterceptor>>,
2584        }
2585
2586        impl NamespacedClient for MockListWorkflowsClient {
2587            fn namespace(&self) -> String {
2588                "test-namespace".to_string()
2589            }
2590            fn identity(&self) -> String {
2591                "test-identity".to_string()
2592            }
2593            fn data_converter(&self) -> &DataConverter {
2594                &self.data_converter
2595            }
2596            fn client_interceptors(&self) -> &[Arc<dyn ClientInterceptor>] {
2597                &self.interceptors
2598            }
2599        }
2600
2601        struct CountingListInterceptor {
2602            calls: Arc<AtomicUsize>,
2603        }
2604
2605        impl ClientInterceptor for CountingListInterceptor {
2606            fn list_workflows_page<'a>(
2607                &'a self,
2608                input: ListWorkflowsPageInput,
2609                next: Next<
2610                    'a,
2611                    ListWorkflowsPageInput,
2612                    BoxFuture<'a, Result<ListWorkflowsPageOutput, ClientError>>,
2613                >,
2614            ) -> BoxFuture<'a, Result<ListWorkflowsPageOutput, ClientError>> {
2615                self.calls.fetch_add(1, Ordering::SeqCst);
2616                next.run(input)
2617            }
2618        }
2619
2620        impl WorkflowService for MockListWorkflowsClient {
2621            fn list_workflow_executions(
2622                &mut self,
2623                request: Request<ListWorkflowExecutionsRequest>,
2624            ) -> futures_util::future::BoxFuture<
2625                '_,
2626                Result<Response<ListWorkflowExecutionsResponse>, tonic::Status>,
2627            > {
2628                self.call_count.fetch_add(1, Ordering::SeqCst);
2629                let req = request.into_inner();
2630
2631                // Determine offset from page token
2632                let offset: usize = if req.next_page_token.is_empty() {
2633                    0
2634                } else {
2635                    String::from_utf8(req.next_page_token)
2636                        .unwrap()
2637                        .parse()
2638                        .unwrap()
2639                };
2640
2641                let remaining = self.total_workflows.saturating_sub(offset);
2642                let count = remaining.min(self.page_size);
2643                let new_offset = offset + count;
2644
2645                let executions: Vec<_> = (offset..offset + count)
2646                    .map(|i| workflow::WorkflowExecutionInfo {
2647                        execution: Some(ProtoWorkflowExecution {
2648                            workflow_id: format!("wf-{i}"),
2649                            run_id: format!("run-{i}"),
2650                        }),
2651                        r#type: Some(WorkflowType {
2652                            name: "TestWorkflow".to_string(),
2653                        }),
2654                        task_queue: "test-queue".to_string(),
2655                        memo: self.memo_payload.clone().map(|payload| ProtoMemo {
2656                            fields: HashMap::from([("memo-key".to_owned(), payload)]),
2657                        }),
2658                        ..Default::default()
2659                    })
2660                    .collect();
2661
2662                let next_page_token = if new_offset < self.total_workflows {
2663                    new_offset.to_string().into_bytes()
2664                } else {
2665                    vec![]
2666                };
2667
2668                async move {
2669                    Ok(Response::new(ListWorkflowExecutionsResponse {
2670                        executions,
2671                        next_page_token,
2672                    }))
2673                }
2674                .boxed()
2675            }
2676        }
2677
2678        #[tokio::test]
2679        async fn list_workflows_paginates_through_all_results() {
2680            let call_count = Arc::new(AtomicUsize::new(0));
2681            let interceptor_calls = Arc::new(AtomicUsize::new(0));
2682            let client = MockListWorkflowsClient {
2683                call_count: call_count.clone(),
2684                page_size: 3,
2685                total_workflows: 10,
2686                data_converter: DataConverter::default(),
2687                memo_payload: None,
2688                interceptors: vec![Arc::new(CountingListInterceptor {
2689                    calls: interceptor_calls.clone(),
2690                })],
2691            };
2692
2693            let stream = client.list_workflows("", WorkflowListOptions::default());
2694            let results: Vec<_> = stream.collect().await;
2695
2696            assert_eq!(results.len(), 10);
2697            for (i, result) in results.iter().enumerate() {
2698                let wf = result.as_ref().unwrap();
2699                assert_eq!(wf.id(), format!("wf-{i}"));
2700                assert_eq!(wf.run_id(), format!("run-{i}"));
2701            }
2702            // Should have made 4 calls: pages of 3, 3, 3, 1
2703            assert_eq!(call_count.load(Ordering::SeqCst), 4);
2704            assert_eq!(interceptor_calls.load(Ordering::SeqCst), 4);
2705        }
2706
2707        #[tokio::test]
2708        async fn list_workflows_respects_limit() {
2709            let call_count = Arc::new(AtomicUsize::new(0));
2710            let client = MockListWorkflowsClient {
2711                call_count: call_count.clone(),
2712                page_size: 3,
2713                total_workflows: 10,
2714                data_converter: DataConverter::default(),
2715                memo_payload: None,
2716                interceptors: Vec::new(),
2717            };
2718
2719            let opts = WorkflowListOptions::builder().limit(5).build();
2720            let stream = client.list_workflows("", opts);
2721            let results: Vec<_> = stream.collect().await;
2722
2723            assert_eq!(results.len(), 5);
2724            for (i, result) in results.iter().enumerate() {
2725                let wf = result.as_ref().unwrap();
2726                assert_eq!(wf.id(), format!("wf-{i}"));
2727            }
2728            // Should have made 2 calls: 1 page of 3, then 2 more from next page
2729            assert_eq!(call_count.load(Ordering::SeqCst), 2);
2730        }
2731
2732        #[tokio::test]
2733        async fn list_workflows_limit_less_than_page_size() {
2734            let call_count = Arc::new(AtomicUsize::new(0));
2735            let client = MockListWorkflowsClient {
2736                call_count: call_count.clone(),
2737                page_size: 10,
2738                total_workflows: 100,
2739                data_converter: DataConverter::default(),
2740                memo_payload: None,
2741                interceptors: Vec::new(),
2742            };
2743
2744            let opts = WorkflowListOptions::builder().limit(3).build();
2745            let stream = client.list_workflows("", opts);
2746            let results: Vec<_> = stream.collect().await;
2747
2748            assert_eq!(results.len(), 3);
2749            // Only 1 call needed since limit < page_size
2750            assert_eq!(call_count.load(Ordering::SeqCst), 1);
2751        }
2752
2753        #[tokio::test]
2754        async fn list_workflows_empty_results() {
2755            let call_count = Arc::new(AtomicUsize::new(0));
2756            let client = MockListWorkflowsClient {
2757                call_count: call_count.clone(),
2758                page_size: 10,
2759                total_workflows: 0,
2760                data_converter: DataConverter::default(),
2761                memo_payload: None,
2762                interceptors: Vec::new(),
2763            };
2764
2765            let stream = client.list_workflows("", WorkflowListOptions::default());
2766            let results: Vec<_> = stream.collect().await;
2767
2768            assert_eq!(results.len(), 0);
2769            assert_eq!(call_count.load(Ordering::SeqCst), 1);
2770        }
2771
2772        #[tokio::test]
2773        async fn list_workflows_exposes_typed_memo() {
2774            let data_converter = DataConverter::new(
2775                PayloadConverter::default(),
2776                DefaultFailureConverter,
2777                XorCodec,
2778            );
2779            let memo_payload = data_converter
2780                .to_payload(
2781                    &SerializationContextData::Workflow,
2782                    &"memo-value".to_owned(),
2783                )
2784                .await
2785                .unwrap();
2786            let client = MockListWorkflowsClient {
2787                call_count: Arc::new(AtomicUsize::new(0)),
2788                page_size: 1,
2789                total_workflows: 1,
2790                data_converter,
2791                memo_payload: Some(memo_payload),
2792                interceptors: Vec::new(),
2793            };
2794
2795            let workflow = client
2796                .list_workflows("", WorkflowListOptions::default())
2797                .next()
2798                .await
2799                .unwrap()
2800                .unwrap();
2801
2802            assert_eq!(
2803                workflow.memo().get::<String>("memo-key").unwrap(),
2804                Some("memo-value".to_owned())
2805            );
2806        }
2807
2808        #[tokio::test]
2809        async fn list_workflows_yields_codec_error_then_ends() {
2810            let client = MockListWorkflowsClient {
2811                call_count: Arc::new(AtomicUsize::new(0)),
2812                page_size: 1,
2813                total_workflows: 1,
2814                data_converter: DataConverter::new(
2815                    PayloadConverter::default(),
2816                    DefaultFailureConverter,
2817                    FailingCodec,
2818                ),
2819                memo_payload: Some(Payload::default()),
2820                interceptors: Vec::new(),
2821            };
2822            let mut stream = client.list_workflows("", WorkflowListOptions::default());
2823
2824            let err = stream.next().await.unwrap().unwrap_err();
2825
2826            assert!(matches!(err, ClientError::PayloadConversion(_)));
2827            assert!(stream.next().await.is_none());
2828        }
2829    }
2830}