Skip to main content

dex_sdk/
client.rs

1// Copyright (c) 2026 Super Durable, Inc.
2//
3// Licensed under the Sustainable Use License 1.0.
4// You may not use this file except in compliance with the License.
5// See the LICENSE file in the repository root.
6//
7// SPDX-License-Identifier: LicenseRef-Sustainable-Use-1.0
8
9use std::any::TypeId;
10use std::collections::{BTreeMap, BTreeSet};
11use std::sync::Arc;
12use std::time::{Duration, SystemTime};
13
14use dex_protocol::dex::flow_service_client::FlowServiceClient;
15use dex_protocol::dex::{
16    ActiveStepSearchMode as ProtoSearchMode, AttributeStoreNames, FlowAlreadyStartedOptions,
17    FlowConfig as ProtoFlowConfig, FlowResetStepMethod, FlowResetType, FlowRetryPolicy,
18    FlowStartOptions, FlowStatus as ProtoFlowStatus, FlowTimeoutPolicy as ProtoFlowTimeoutPolicy,
19    GetFlowSummaryRequest, IdReusePolicy as ProtoIdReusePolicy, InvokeRpcRequest,
20    ListStreamMessagesRequest, ReadStreamRequest, ResetFlowRequest, SearchFlowsRequest,
21    SkipTimerRequest, StartFlowRequest, StepDurability as ProtoStepDurability, StopFlowRequest,
22    StopType as ProtoStopType, TriggerContinueAsNewRequest, UpdateFlowConfigRequest,
23    WaitForAttributeRequest, WaitForFlowRequest, WaitForStepCompletionRequest,
24    WorkerTarget as ProtoWorkerTarget, WriteStreamRequest,
25};
26use tokio::runtime::Runtime;
27use tonic::{Code, Request};
28use uuid::Uuid;
29
30mod attribute_reads;
31pub(crate) mod flow_service_tls;
32
33pub use flow_service_tls::FlowServiceTlsOptions;
34use flow_service_tls::flow_service_endpoint;
35
36pub use attribute_reads::{AttributeRead, AttributeReads};
37
38use crate::credential::{CredentialChannel, CredentialInterceptor};
39use crate::sdk_error::{FlowTargetRequirement, ServiceError};
40use crate::server_protocol::{NegotiatedServerProtocolVersion, negotiate_server_protocol};
41use crate::stop_flow_options::StopType;
42use crate::time_travel_options::{TimeTravelPoint, TimeTravelStepMethod};
43use crate::value_hydrator::ValueHydrator;
44use crate::value_mapper;
45use crate::wait_options::ClientRequestBudget;
46use crate::worker_dispatcher::{map_flow_timeout_handler_options, map_step_options};
47use crate::{
48    ActiveStepSearchMode, Attribute, AttributeMap, AttributeMatch, BlobCache, ClientOptions, Flow,
49    FlowConfig, FlowErrorType, FlowInfo, FlowResult, FlowStatus, FlowTimeoutPolicy, IdReusePolicy,
50    Registry, RetryPolicy, Rpc, RpcInvokeOptions, SdkError, SdkResult, SearchFlowEntry,
51    SearchFlowsOptions, SearchFlowsPage, StartFlowOptions, StepCompletion, StepDurability,
52    StepExecutionId, StopFlowOptions, Stream, StreamMessage, StreamMessagesPage, TimeTravelOptions,
53    TimerId, Value, WaitForAttributeOptions, WaitForStepCompletionOptions,
54};
55
56/// Provides blocking, typed control of registered Dex Flows.
57///
58/// Client methods block the calling thread while an internal Tokio runtime performs gRPC and blob
59/// hydration. The Client owns its transport resources; dropping it releases them. Flow, Step, RPC,
60/// Attribute, and Channel arguments are checked against the [`Registry`].
61/// Construction performs no network I/O. Before its first service request, every Client negotiates
62/// protocol version 4. Successful negotiation and incompatibility errors are cached for its lifetime;
63/// transient negotiation failures are retried on the next call. Incompatibility errors name the
64/// Server version and required server-zd release. Concurrent first calls share one negotiation.
65/// An unreachable Server during negotiation returns the usual service error with its gRPC code.
66/// A positive request timeout on a wait method also bounds negotiation and the wait for another
67/// caller's negotiation; its expiry returns [`SdkError::RequestTimeout`] and is not cached.
68///
69/// Changed in 1.35.0: protocol 4 is required (server-zd newer than v0.1.16).
70/// Upgrade the Server first.
71///
72/// Changed in 1.26.0: only server-zd is supported as the Dex Server.
73///
74/// A Client call never retries a failed application RPC automatically.
75/// Inspect [`SdkError`] variants and [`crate::ServiceError`] status details before retrying an
76/// operation whose external effects may already have happened.
77///
78/// # Examples
79///
80/// ```no_run
81/// use dex_sdk::{BlobCache, BlobCacheConfig, Client, ClientOptions, Registry};
82/// use std::sync::Arc;
83///
84/// let cache = Arc::new(BlobCache::open(BlobCacheConfig::new(
85///     "/tmp/dex-blobs", 64 * 1024 * 1024, 0,
86/// )?)?);
87/// let client = Client::try_new(Registry::new(), cache, ClientOptions::new())?;
88/// client.health_check()?;
89/// # Ok::<(), Box<dyn std::error::Error>>(())
90/// ```
91pub struct Client {
92    runtime: Runtime,
93    registry: Registry,
94    service: FlowServiceClient<CredentialChannel>,
95    hydrator: ValueHydrator,
96    negotiated_protocol_version: NegotiatedServerProtocolVersion,
97    protocol_negotiation: tokio::sync::Mutex<Option<Result<u32, String>>>,
98}
99
100impl Client {
101    /// Creates a Client or panics when runtime or endpoint initialization fails.
102    ///
103    /// Prefer [`Self::try_new`] when the application can recover from startup configuration errors.
104    ///
105    /// # Panics
106    ///
107    /// Panics if a Tokio runtime cannot be created or the server address or TLS settings are invalid.
108    pub fn new(registry: Registry, blob_cache: Arc<BlobCache>, options: ClientOptions) -> Self {
109        Self::try_new(registry, blob_cache, options)
110            .unwrap_or_else(|error| panic!("cannot create Dex Client: {error}"))
111    }
112
113    /// Creates a Client from validated definitions, a shared BlobCache, and connection options.
114    ///
115    /// # Errors
116    ///
117    /// Returns [`SdkError::Service`] if runtime or endpoint initialization fails. The transport connects lazily,
118    /// so server availability is checked by the first operation. Invalid TLS options return
119    /// [`SdkError::InvalidArgument`]. Certificate and handshake failures occur on the first call.
120    pub fn try_new(
121        registry: Registry,
122        blob_cache: Arc<BlobCache>,
123        options: ClientOptions,
124    ) -> SdkResult<Self> {
125        let runtime = Runtime::new().map_err(service_error)?;
126        let endpoint = flow_service_endpoint(
127            options.server_address_value(),
128            options.flow_service_tls_value(),
129            "client",
130        )?;
131        let service = {
132            let _runtime_guard = runtime.enter();
133            FlowServiceClient::new(CredentialChannel::new(
134                endpoint.connect_lazy(),
135                CredentialInterceptor::new(options.credential_value()),
136            ))
137        };
138        let hydrator = ValueHydrator::new(service.clone(), blob_cache);
139        Ok(Self {
140            runtime,
141            registry,
142            service,
143            hydrator,
144            negotiated_protocol_version: NegotiatedServerProtocolVersion::default(),
145            protocol_negotiation: tokio::sync::Mutex::new(None),
146        })
147    }
148
149    /// Starts a registered Flow on pull Workers with other options using server defaults.
150    ///
151    /// Returns the server-assigned run ID. `input` is encoded for the registered starting Step.
152    /// Changed in 1.25.0: starts always route to pull Workers.
153    /// Success acknowledges the start; it does not wait for a Worker or Flow completion.
154    ///
155    /// ```no_run
156    /// use dex_sdk::{Client, Flow, FlowStatus, SdkResult};
157    /// use std::time::Duration;
158    ///
159    /// fn start_and_wait<F: Flow>(client: &Client, flow: &F, input: F::StartInput)
160    ///     -> SdkResult<(String, FlowStatus)> {
161    ///     let run_id = client.start_flow(flow, "order-1", input)?;
162    ///     let result = client.wait_for_flow_with_timeout("order-1", Duration::from_secs(60))?;
163    ///     Ok((run_id, result.status()))
164    /// }
165    /// ```
166    ///
167    /// Use [`Self::wait_for_flow`] to observe the terminal result.
168    ///
169    /// # Errors
170    ///
171    /// Returns a definition or mapping error locally, [`SdkError::FlowAlreadyStarted`] for a reuse
172    /// conflict, or another service-backed [`SdkError`] when Dex rejects the request.
173    pub fn start_flow<SomeFlow: Flow>(
174        &self,
175        flow: &SomeFlow,
176        flow_id: &str,
177        input: SomeFlow::StartInput,
178    ) -> SdkResult<String> {
179        self.start_flow_with_options(flow, flow_id, input, StartFlowOptions::new())
180    }
181
182    /// Starts a registered Flow with explicit lifecycle, retry, and initial persistence options.
183    ///
184    /// Always selects pull Workers, overriding any server-configured push target.
185    /// Returns the server-assigned run ID. A missing request ID is replaced with a generated UUID.
186    /// When the options contain an initial value for a hard-delete Attribute, the Client first
187    /// uses the Server protocol version negotiated before the first service request.
188    ///
189    /// # Errors
190    ///
191    /// Returns [`SdkError`] for invalid IDs, unregistered or mismatched Flow types, encoding
192    /// failures, invalid option durations, reuse conflicts, and service failures. Returns
193    /// [`SdkError::InvalidArgument`] for an invalid initial hard-delete Attribute value or a
194    /// definition that does not match the registered one. Returns [`SdkError::Service`] when the
195    /// Server does not support the protocol version this SDK requires.
196    pub fn start_flow_with_options<SomeFlow: Flow>(
197        &self,
198        flow: &SomeFlow,
199        flow_id: &str,
200        input: SomeFlow::StartInput,
201        options: StartFlowOptions,
202    ) -> SdkResult<String> {
203        require_name(flow_id, "Flow ID")?;
204        let registered = self.registry.flow(flow.flow_type())?;
205        if registered.type_id != TypeId::of::<SomeFlow>() {
206            return Err(SdkError::FlowDefinition {
207                message: format!(
208                    "Flow {} does not match the registered Rust type",
209                    flow.flow_type()
210                ),
211            });
212        }
213        let (start_step_type, step_input, step_options) = match &registered.start_step {
214            Some(step) => (
215                step.name.to_string(),
216                Some(value_mapper::encode(&input)?),
217                Some(
218                    map_step_options(
219                        registered,
220                        registered
221                            .handler
222                            .step_options(step.name)
223                            .map_err(sdk_handler_error)?,
224                    )
225                    .map_err(sdk_handler_error)?,
226                ),
227            ),
228            None => (String::new(), None, None),
229        };
230        let flow_timeout_seconds = optional_seconds(options.timeout)?;
231        let flow_timeout_policy =
232            map_flow_timeout_policy(registered, flow_timeout_seconds, options.timeout_policy)?;
233        let request = StartFlowRequest {
234            flow_id: flow_id.to_string(),
235            flow_type: registered.name.to_string(),
236            flow_timeout_seconds,
237            flow_timeout_policy,
238            start_step_type,
239            step_input,
240            step_options,
241            flow_start_options: Some(self.map_start_options(
242                registered,
243                &options,
244                flow_timeout_seconds,
245                flow_timeout_policy,
246            )?),
247            request_id: options
248                .request_id
249                .clone()
250                .unwrap_or_else(|| Uuid::new_v4().to_string()),
251        };
252        self.require_server_protocol("start_flow")?;
253        let mut service = self.service.clone();
254        self.runtime.block_on(async {
255            service
256                .start_flow(request)
257                .await
258                .map(|response| response.into_inner().run_id)
259                .map_err(|status| {
260                    SdkError::from_status(
261                        status,
262                        "start_flow",
263                        Some(flow_id),
264                        FlowTargetRequirement::None,
265                    )
266                })
267        })
268    }
269
270    /// Invokes a registered RPC with typed input and decodes its typed output.
271    ///
272    /// ```no_run
273    /// use dex_sdk::{Client, Rpc, SdkResult};
274    /// fn describe_order(client: &Client, rpc: Rpc<(), String>) -> SdkResult<String> {
275    ///     client.invoke_rpc("order-1", rpc, ())
276    /// }
277    /// ```
278    ///
279    /// The call blocks until the handler result is decoded or a request error occurs.
280    /// RPCs do not enter the durable Step queue. When no Worker takes the RPC within the dispatch
281    /// wait, Dex returns an Unavailable service error. The dispatch wait is the Server's configurable
282    /// `matching.dispatchWait` setting, which defaults to ten seconds and is bounded by the caller's
283    /// deadline.
284    /// A timeout or lost response does not prove the handler performed no effects; make retries safe.
285    ///
286    /// A non-transactional RPC without Attribute locks starts from a backend query. A retained
287    /// terminal execution can serve it when the handler returns no durable effects. Locks,
288    /// transactional execution, returned effects, or server policy can require an active
289    /// execution.
290    ///
291    /// # Errors
292    ///
293    /// Returns [`SdkError::RpcLockConflict`] when locks cannot be acquired, WorkerInvocation for a
294    /// handler failure, FlowNotActiveOrNotFound when the target is missing or the selected path cannot
295    /// use its closed execution, or a
296    /// mapping/service error.
297    pub fn invoke_rpc<Input: Value, Output: Value>(
298        &self,
299        flow_id: &str,
300        rpc: Rpc<Input, Output>,
301        input: Input,
302    ) -> SdkResult<Output> {
303        self.do_invoke_rpc(flow_id, rpc.name(), &input, &RpcInvokeOptions::new())
304    }
305
306    /// Invokes a registered RPC with typed input and additive runtime map-instance selections.
307    ///
308    /// # Errors
309    ///
310    /// Returns a local definition error for an unregistered or wrong-kind selection. Invalid map
311    /// instance names and all errors documented by [`Self::invoke_rpc`] are also returned.
312    pub fn invoke_rpc_with_options<Input: Value, Output: Value>(
313        &self,
314        flow_id: &str,
315        rpc: Rpc<Input, Output>,
316        input: Input,
317        options: RpcInvokeOptions,
318    ) -> SdkResult<Output> {
319        self.do_invoke_rpc(flow_id, rpc.name(), &input, &options)
320    }
321
322    /// Invokes a registered no-input RPC and decodes its typed output.
323    ///
324    /// Returns the same errors as [`Self::invoke_rpc`].
325    pub fn invoke_rpc_without_input<Output: Value>(
326        &self,
327        flow_id: &str,
328        rpc: Rpc<(), Output>,
329    ) -> SdkResult<Output> {
330        self.do_invoke_rpc(flow_id, rpc.name(), &(), &RpcInvokeOptions::new())
331    }
332
333    /// Invokes a registered no-input RPC with additive runtime map-instance selections.
334    ///
335    /// # Errors
336    ///
337    /// Returns the same errors as [`Self::invoke_rpc_with_options`].
338    pub fn invoke_rpc_without_input_with_options<Output: Value>(
339        &self,
340        flow_id: &str,
341        rpc: Rpc<(), Output>,
342        options: RpcInvokeOptions,
343    ) -> SdkResult<Output> {
344        self.do_invoke_rpc(flow_id, rpc.name(), &(), &options)
345    }
346
347    /// Appends one typed best-effort Stream message with source metadata.
348    ///
349    /// The Flow instance need not exist or be active. Every call appends a message, including calls
350    /// that repeat the same source. A source may contain `#`.
351    ///
352    /// # Errors
353    ///
354    /// Returns a definition error for an unregistered Stream, InvalidArgument for an empty Flow ID
355    /// or source, a mapping error, or a FlowService failure.
356    pub fn write_stream<T: Value>(
357        &self,
358        flow_id: &str,
359        stream: &Stream<T>,
360        source: &str,
361        value: T,
362    ) -> SdkResult<()> {
363        require_name(flow_id, "Flow ID")?;
364        require_name(source, "Stream source")?;
365        let flow_type = self.registry.flow_for_stream(stream)?.name.to_string();
366        let request = WriteStreamRequest {
367            flow_id: flow_id.to_string(),
368            flow_type,
369            stream_name: stream.name().to_string(),
370            stream_capacity_bytes: stream.stream_capacity_bytes(),
371            value: Some(value_mapper::encode(&value)?),
372            source: source.to_string(),
373        };
374        self.call_empty(
375            "write_stream",
376            Some(flow_id),
377            FlowTargetRequirement::None,
378            |mut service| async move { service.write_stream(request).await },
379        )
380    }
381
382    /// Blocks for the next retained Stream message using the server default wait.
383    ///
384    /// An empty token starts at the retained head. Pass the returned token unchanged to resume.
385    pub fn read_stream<T: Value>(
386        &self,
387        flow_id: &str,
388        stream: &Stream<T>,
389        resume_token: &str,
390    ) -> SdkResult<StreamMessage<T>> {
391        self.read_stream_result(flow_id, stream, resume_token, None)
392    }
393
394    /// Blocks for the next retained Stream message with an explicit server wait.
395    ///
396    /// # Errors
397    ///
398    /// Returns [`SdkError::LongPollTimeout`] when no message arrives, plus the same definition,
399    /// duration, mapping, and service errors as [`Self::read_stream`].
400    pub fn read_stream_with_timeout<T: Value>(
401        &self,
402        flow_id: &str,
403        stream: &Stream<T>,
404        resume_token: &str,
405        timeout: Duration,
406    ) -> SdkResult<StreamMessage<T>> {
407        self.read_stream_result(flow_id, stream, resume_token, Some(timeout))
408    }
409
410    /// Returns one newest-first page of retained Stream messages without waiting for writes.
411    ///
412    /// An empty `before_page_token` starts at the retained tail. Pass
413    /// [`StreamMessagesPage::next_page_token`] unchanged to read the next, older page. Stream
414    /// trimming may remove messages between pages.
415    /// Tokens are exclusive and scoped to the Flow ID, the selected Flow type, and the Stream. A
416    /// token from another Flow ID or Stream is rejected. If trimming passes the page anchor, the
417    /// server returns an empty page rather than restarting at the tail. New writes after the first
418    /// page do not enter that older-page chain.
419    ///
420    /// `page_size` must be between 1 and the Server's `streamStore.maxReadMessages` setting, which
421    /// defaults to 1000. The Server rejects a larger value; it does not reduce it.
422    ///
423    /// # Errors
424    ///
425    /// Returns InvalidArgument when `flow_id` is empty, `page_size` is not positive, or
426    /// `page_size` exceeds `streamStore.maxReadMessages`. Returns a
427    /// definition error for an unregistered Stream, a mapping error for an incompatible retained
428    /// value, or a FlowService error when the request fails.
429    pub fn list_stream_messages<T: Value>(
430        &self,
431        flow_id: &str,
432        stream: &Stream<T>,
433        page_size: i32,
434        before_page_token: &str,
435    ) -> SdkResult<StreamMessagesPage<T>> {
436        require_name(flow_id, "Flow ID")?;
437        if page_size < 1 {
438            return Err(invalid("Stream page size must be positive"));
439        }
440        let flow_type = self.registry.flow_for_stream(stream)?.name.to_string();
441        let request = ListStreamMessagesRequest {
442            flow_id: flow_id.to_string(),
443            flow_type,
444            stream_name: stream.name().to_string(),
445            page_size,
446            before_page_token: before_page_token.to_string(),
447        };
448        self.require_server_protocol("list_stream_messages")?;
449        let mut service = self.service.clone();
450        let response = self.runtime.block_on(async {
451            service
452                .list_stream_messages(request)
453                .await
454                .map(|response| response.into_inner())
455                .map_err(|status| {
456                    SdkError::from_status(
457                        status,
458                        "list_stream_messages",
459                        Some(flow_id),
460                        FlowTargetRequirement::None,
461                    )
462                })
463        })?;
464        let messages = response
465            .messages
466            .into_iter()
467            .map(|message| {
468                if message.resume_token.is_empty() {
469                    return Err(invalid("ListStreamMessages returned an empty resume token"));
470                }
471                let value = message
472                    .value
473                    .ok_or_else(|| invalid("ListStreamMessages omitted a Stream message Value"))?;
474                let created_time = message.created_time.map(timestamp).ok_or_else(|| {
475                    invalid("ListStreamMessages omitted a Stream message creation time")
476                })?;
477                Ok(StreamMessage {
478                    value: value_mapper::decode(&value)?,
479                    resume_token: message.resume_token,
480                    created_time,
481                    source: message.source,
482                })
483            })
484            .collect::<SdkResult<Vec<_>>>()?;
485        Ok(StreamMessagesPage {
486            messages,
487            next_page_token: response.next_page_token,
488        })
489    }
490
491    /// Blocks until a Flow closes and returns its terminal result.
492    ///
493    /// # Errors
494    ///
495    /// Returns FlowNotFound for an unknown ID or a mapping/service error. Every terminal status is
496    /// represented by [`FlowResult`]. This overload has no client-side timeout.
497    pub fn wait_for_flow(&self, flow_id: &str) -> SdkResult<FlowResult> {
498        self.wait_for_flow_result(flow_id, None)
499    }
500
501    /// Blocks up to `timeout` and returns the terminal result with all output-bearing completions.
502    ///
503    /// Like [`Self::wait_for_flow`], terminal failures are returned as [`FlowResult`], not as
504    /// request errors. A wait timeout does not stop the Flow. Durations use whole seconds.
505    /// See the [`Self::start_flow`] example for a start followed by a bounded wait.
506    ///
507    /// # Errors
508    ///
509    /// Returns [`SdkError::LongPollTimeout`] when the timeout elapses while the Flow remains active;
510    /// other errors match [`Self::wait_for_flow`].
511    pub fn wait_for_flow_with_timeout(
512        &self,
513        flow_id: &str,
514        timeout: Duration,
515    ) -> SdkResult<FlowResult> {
516        self.wait_for_flow_result(flow_id, Some(timeout))
517    }
518
519    /// Returns current identity, status, type, and start time for the latest run.
520    ///
521    /// # Errors
522    ///
523    /// Returns [`SdkError::FlowNotFound`] for an unknown ID or a service/protocol error.
524    pub fn describe_flow(&self, flow_id: &str) -> SdkResult<FlowInfo> {
525        self.require_server_protocol("describe_flow")?;
526        let mut service = self.service.clone();
527        let response = self.runtime.block_on(async {
528            service
529                .get_flow_summary(GetFlowSummaryRequest {
530                    flow_id: flow_id.to_string(),
531                    run_id: String::new(),
532                })
533                .await
534                .map(|response| response.into_inner())
535                .map_err(|status| {
536                    SdkError::from_status(
537                        status,
538                        "describe_flow",
539                        Some(flow_id),
540                        FlowTargetRequirement::Existing,
541                    )
542                })
543        })?;
544        let execution = response
545            .flow_execution_id
546            .ok_or_else(|| service_error("GetFlowSummary omitted FlowExecutionID"))?;
547        Ok(FlowInfo {
548            flow_id: execution.flow_id,
549            run_id: execution.run_id,
550            flow_type: response.flow_type,
551            status: map_flow_status(response.flow_status)?,
552            started_at: response
553                .start_time
554                .map_or(SystemTime::UNIX_EPOCH, timestamp),
555        })
556    }
557
558    /// Executes a server search query and returns its first page.
559    ///
560    /// `page_size` must be non-negative; zero uses the server default. Indexed values are hydrated
561    /// and decoded as [`serde_json::Value`].
562    pub fn search_flows(&self, query: &str, page_size: i32) -> SdkResult<SearchFlowsPage> {
563        self.search_flows_page(query, page_size, "")
564    }
565
566    /// Continues a search with the opaque token returned by a previous page.
567    ///
568    /// Keep `query` and `page_size` consistent across pages. An empty returned token marks the last
569    /// page.
570    pub fn search_flows_page(
571        &self,
572        query: &str,
573        page_size: i32,
574        next_page_token: &str,
575    ) -> SdkResult<SearchFlowsPage> {
576        self.search_flows_with_options(query, page_size, next_page_token, SearchFlowsOptions::new())
577    }
578
579    /// Searches with explicit inclusion options; retain query and options across pages. An empty token starts pagination.
580    pub fn search_flows_with_options(
581        &self,
582        query: &str,
583        page_size: i32,
584        next_page_token: &str,
585        options: SearchFlowsOptions,
586    ) -> SdkResult<SearchFlowsPage> {
587        if page_size < 0 {
588            return Err(invalid("search page size must not be negative"));
589        }
590        self.require_server_protocol("search_flows")?;
591        let mut service = self.service.clone();
592        self.runtime.block_on(async {
593            let response = service
594                .search_flows(SearchFlowsRequest {
595                    query: query.to_string(),
596                    page_size,
597                    next_page_token: next_page_token.to_string(),
598                    include_continued_as_new: options.include_continued_as_new,
599                })
600                .await
601                .map(|response| response.into_inner())
602                .map_err(|status| {
603                    SdkError::from_status(status, "search_flows", None, FlowTargetRequirement::None)
604                })?;
605            let values = response
606                .flow_runs
607                .iter()
608                .flat_map(|entry| {
609                    entry.indexed_attributes.iter().map(|attribute| {
610                        attribute
611                            .value
612                            .clone()
613                            .map(|value| (entry.flow_id.clone(), value))
614                            .ok_or_else(|| invalid("Indexed Attribute has no Value"))
615                    })
616                })
617                .collect::<SdkResult<Vec<_>>>()?;
618            let mut values = self.hydrator.hydrate_for_flows(values).await?.into_iter();
619            let mut flows = Vec::with_capacity(response.flow_runs.len());
620            for entry in response.flow_runs {
621                let mut indexed_attributes = BTreeMap::new();
622                for attribute in entry.indexed_attributes {
623                    let value = values
624                        .next()
625                        .ok_or_else(|| invalid("Indexed Attribute hydration count mismatch"))?;
626                    indexed_attributes.insert(attribute.key, value_mapper::decode_untyped(&value)?);
627                }
628                flows.push(SearchFlowEntry {
629                    flow_id: entry.flow_id,
630                    run_id: entry.run_id,
631                    flow_type: entry.flow_type,
632                    status: map_flow_status(entry.flow_status)?,
633                    started_at: entry.start_time.map(timestamp),
634                    closed_at: entry.close_time.map(timestamp),
635                    indexed_attributes,
636                });
637            }
638            Ok(SearchFlowsPage {
639                flows,
640                next_page_token: response.next_page_token,
641            })
642        })
643    }
644
645    /// Cancels, terminates, or fails an active Flow according to `options`.
646    ///
647    /// Success acknowledges the stop request. Use [`Self::wait_for_flow`] to observe its terminal
648    /// status. This does not stop the Worker or undo effects in an external service.
649    ///
650    /// # Errors
651    ///
652    /// Returns [`SdkError::FlowNotActiveOrNotFound`] when no active run exists or another service error.
653    pub fn stop_flow(&self, flow_id: &str, options: StopFlowOptions) -> SdkResult<()> {
654        let stop_type = match options.stop_type {
655            StopType::Cancel => ProtoStopType::Cancel,
656            StopType::Terminate => ProtoStopType::Terminate,
657            StopType::Fail => ProtoStopType::Fail,
658        };
659        self.call_empty(
660            "stop_flow",
661            Some(flow_id),
662            FlowTargetRequirement::Active,
663            |mut service| async move {
664                service
665                    .stop_flow(StopFlowRequest {
666                        flow_id: flow_id.to_string(),
667                        run_id: String::new(),
668                        reason: options.reason.unwrap_or_default(),
669                        stop_type: stop_type as i32,
670                    })
671                    .await
672            },
673        )
674    }
675
676    /// Re-runs an existing Flow from the selected historical point.
677    ///
678    /// Time travel keeps the Flow's run ID and starts the next run attempt of that run. Re-run Steps
679    /// reuse the Flow ID, run ID, and Step execution IDs of the original execution. An external
680    /// idempotency key built only from those IDs therefore matches the effect from before time
681    /// travel. Reconcile such effects, or build keys that the application changes for each re-run.
682    ///
683    /// Returns the run ID, which is the same run ID the Flow had before time travel.
684    ///
685    /// # Errors
686    ///
687    /// Returns InvalidArgument for an unrepresentable time, FlowNotFound for an unknown ID, or
688    /// another service error.
689    pub fn time_travel(&self, flow_id: &str, options: TimeTravelOptions) -> SdkResult<String> {
690        let mut request = ResetFlowRequest {
691            flow_id: flow_id.to_string(),
692            reset_type: FlowResetType::Unspecified as i32,
693            reason: options.reason.unwrap_or_default(),
694            skip_writes_reapply: options.skip_writes_reapply,
695            ..Default::default()
696        };
697        match options.point {
698            TimeTravelPoint::Beginning => request.reset_type = FlowResetType::Beginning as i32,
699            TimeTravelPoint::HistoryEventTime(time) => {
700                request.reset_type = FlowResetType::HistoryEventTime as i32;
701                request.history_event_time = rfc3339(time)?;
702            }
703            TimeTravelPoint::StepType(step_type) => {
704                request.reset_type = FlowResetType::StepType as i32;
705                request.step_type = step_type.to_string();
706            }
707            TimeTravelPoint::StepExecution(execution, method) => {
708                request.reset_type = FlowResetType::StepExecutionId as i32;
709                request.step_execution_id =
710                    format!("{}-{}", execution.step_type, execution.execution_number);
711                request.step_method = match method {
712                    TimeTravelStepMethod::WaitFor => FlowResetStepMethod::WaitFor as i32,
713                    TimeTravelStepMethod::Execute => FlowResetStepMethod::Execute as i32,
714                };
715            }
716        }
717        self.require_server_protocol("time_travel")?;
718        let mut service = self.service.clone();
719        self.runtime.block_on(async {
720            service
721                .reset_flow(request)
722                .await
723                .map(|response| response.into_inner().run_id)
724                .map_err(|status| {
725                    SdkError::from_status(
726                        status,
727                        "time_travel",
728                        Some(flow_id),
729                        FlowTargetRequirement::Existing,
730                    )
731                })
732        })
733    }
734
735    /// Marks one timer in an active Step execution as satisfied immediately.
736    ///
737    /// # Errors
738    ///
739    /// Returns InvalidArgument for an oversized index, FlowNotActiveOrNotFound for a terminal Flow, or a
740    /// service error when the Step execution or timer cannot be targeted.
741    pub fn skip_timer(
742        &self,
743        flow_id: &str,
744        step_execution: StepExecutionId,
745        timer: TimerId,
746    ) -> SdkResult<()> {
747        let (timer_condition_id, timer_condition_index) = match timer {
748            TimerId::ConditionId(id) => (id, None),
749            TimerId::ConditionIndex(index) => (
750                String::new(),
751                Some(i32::try_from(index).map_err(|_| invalid("timer index exceeds int32"))?),
752            ),
753        };
754        self.call_empty(
755            "skip_timer",
756            Some(flow_id),
757            FlowTargetRequirement::Active,
758            |mut service| async move {
759                service
760                    .skip_timer(SkipTimerRequest {
761                        flow_id: flow_id.to_string(),
762                        run_id: String::new(),
763                        step_execution_id: format!(
764                            "{}-{}",
765                            step_execution.step_type, step_execution.execution_number
766                        ),
767                        timer_condition_id,
768                        timer_condition_index,
769                    })
770                    .await
771            },
772        )
773    }
774
775    /// Blocks until one Step execution completes or its request budget expires.
776    ///
777    /// The server derives a stable Request ID from the Step execution when none is supplied.
778    ///
779    /// # Errors
780    ///
781    /// Returns [`SdkError::RequestTimeout`] when a positive request timeout expires,
782    /// FlowNotActiveOrNotFound when appropriate, or another service error. Successful completion returns
783    /// `()` and does not decode Step output.
784    pub fn wait_for_step_completion(
785        &self,
786        flow_id: &str,
787        step_execution: StepExecutionId,
788        options: WaitForStepCompletionOptions,
789    ) -> SdkResult<()> {
790        let request_budget = ClientRequestBudget::new(options.request_timeout)?;
791        let internal_handler_timeout_seconds = seconds32(options.internal_handler_timeout)?;
792        self.require_server_protocol_within_budget(
793            "wait_for_step_completion",
794            flow_id,
795            &request_budget,
796        )?;
797        loop {
798            let request_attempt =
799                request_budget.next_attempt("wait_for_step_completion", flow_id)?;
800            let request_flow_id = flow_id.to_string();
801            let request_step_type = step_execution.step_type.to_string();
802            let request_step_execution_number = step_execution.execution_number.to_string();
803            let request_id = options.request_id.clone();
804            let result = self.call_empty(
805                "wait_for_step_completion",
806                Some(flow_id),
807                FlowTargetRequirement::Active,
808                |mut service| async move {
809                    let mut request = Request::new(WaitForStepCompletionRequest {
810                        flow_id: request_flow_id,
811                        step_type: request_step_type,
812                        step_execution_number: request_step_execution_number,
813                        request_timeout_seconds: request_attempt.request_timeout_seconds,
814                        internal_handler_timeout_seconds,
815                        request_id,
816                    });
817                    if let Some(transport_timeout) = request_attempt.transport_timeout {
818                        request.set_timeout(transport_timeout);
819                    }
820                    service.wait_for_step_completion(request).await
821                },
822            );
823            match result {
824                Err(SdkError::LongPollTimeout { .. }) => continue,
825                Err(error)
826                    if error.service_error().is_some_and(|service| {
827                        matches!(service.code(), Code::DeadlineExceeded | Code::Cancelled)
828                    }) && request_budget.has_deadline() =>
829                {
830                    return Err(SdkError::request_timeout(
831                        "wait_for_step_completion",
832                        flow_id,
833                    ));
834                }
835                result => return result,
836            }
837        }
838    }
839
840    /// Blocks until a singleton Attribute in the current run satisfies `attribute_match`.
841    ///
842    /// Returns the current value observed by the successful wait. The server derives a stable
843    /// Request ID from the condition when none is supplied. String and Boolean
844    /// Attributes support equality matches. Integer and floating-point Attributes support every
845    /// match. A positive request-timeout expiry returns [`SdkError::RequestTimeout`]. A
846    /// hard-delete Attribute, defined with [`Attribute::hard_delete_on_null`], returns
847    /// [`SdkError::InvalidArgument`] before the Client contacts Dex.
848    pub fn wait_for_attribute_match<T: Value>(
849        &self,
850        flow_id: &str,
851        attribute: &Attribute<T>,
852        attribute_match: AttributeMatch<T>,
853        options: WaitForAttributeOptions,
854    ) -> SdkResult<T> {
855        require_waitable_attribute(attribute.name(), attribute.is_hard_delete_on_null())?;
856        self.wait_for_attribute_value(flow_id, attribute.name(), &attribute_match, options)
857    }
858
859    /// Blocks until one AttributeMap instance satisfies `attribute_match`.
860    ///
861    /// This targets the current run and otherwise has the same match, request-timeout and handler-generation,
862    /// request-ID, return-value, and error behavior as [`Self::wait_for_attribute_match`]. A
863    /// hard-delete AttributeMap, defined with [`AttributeMap::hard_delete_on_null`], returns
864    /// [`SdkError::InvalidArgument`] before the Client contacts Dex.
865    pub fn wait_for_attribute_map_instance_match<T: Value>(
866        &self,
867        flow_id: &str,
868        attribute: &AttributeMap<T>,
869        instance: &str,
870        attribute_match: AttributeMatch<T>,
871        options: WaitForAttributeOptions,
872    ) -> SdkResult<T> {
873        require_waitable_attribute(attribute.name(), attribute.is_hard_delete_on_null())?;
874        self.wait_for_attribute_value(
875            flow_id,
876            &map_physical_name(attribute.name(), instance)?,
877            &attribute_match,
878            options,
879        )
880    }
881
882    fn wait_for_attribute_value<T: Value>(
883        &self,
884        flow_id: &str,
885        key: &str,
886        attribute_match: &AttributeMatch<T>,
887        options: WaitForAttributeOptions,
888    ) -> SdkResult<T> {
889        let mut encoded_match = attribute_match.encode()?;
890        encoded_match.key = key.to_string();
891        let request_budget = ClientRequestBudget::new(options.request_timeout)?;
892        let internal_handler_timeout_seconds = seconds32(options.internal_handler_timeout)?;
893        self.require_server_protocol_within_budget(
894            "wait_for_attribute_match",
895            flow_id,
896            &request_budget,
897        )?;
898        let response = loop {
899            let request_attempt =
900                request_budget.next_attempt("wait_for_attribute_match", flow_id)?;
901            let mut service = self.service.clone();
902            let mut request = Request::new(WaitForAttributeRequest {
903                flow_id: flow_id.to_string(),
904                r#match: Some(encoded_match.clone()),
905                request_timeout_seconds: request_attempt.request_timeout_seconds,
906                internal_handler_timeout_seconds,
907                request_id: options.request_id.clone(),
908            });
909            if let Some(transport_timeout) = request_attempt.transport_timeout {
910                request.set_timeout(transport_timeout);
911            }
912            let result = self
913                .runtime
914                .block_on(service.wait_for_attribute(request))
915                .map_err(|status| {
916                    SdkError::from_status(
917                        status,
918                        "wait_for_attribute_match",
919                        Some(flow_id),
920                        FlowTargetRequirement::Active,
921                    )
922                });
923            match result {
924                Ok(response) => break response.into_inner(),
925                Err(SdkError::LongPollTimeout { .. }) => continue,
926                Err(error)
927                    if error.service_error().is_some_and(|service| {
928                        matches!(service.code(), Code::DeadlineExceeded | Code::Cancelled)
929                    }) && request_budget.has_deadline() =>
930                {
931                    return Err(SdkError::request_timeout(
932                        "wait_for_attribute_match",
933                        flow_id,
934                    ));
935                }
936                Err(error) => return Err(error),
937            }
938        };
939        let matched_value = response.matched_value.ok_or_else(|| SdkError::Service {
940            service: ServiceError::local(
941                "wait_for_attribute_match",
942                "WaitForAttribute response is incomplete",
943            ),
944        })?;
945        value_mapper::decode(&matched_value)
946    }
947
948    /// Replaces mutable runtime configuration fields on an active Flow.
949    ///
950    /// Leaves the Worker target unset, preserving the active Flow target.
951    /// Other unset fields also retain the active Flow setting.
952    pub fn update_flow_config(&self, flow_id: &str, config: FlowConfig) -> SdkResult<()> {
953        let flow_config = self.map_flow_config(Some(&config))?;
954        self.call_empty(
955            "update_flow_config",
956            Some(flow_id),
957            FlowTargetRequirement::Active,
958            |mut service| async move {
959                service
960                    .update_flow_config(UpdateFlowConfigRequest {
961                        flow_id: flow_id.to_string(),
962                        run_id: String::new(),
963                        flow_config: Some(flow_config),
964                    })
965                    .await
966            },
967        )
968    }
969
970    /// Requests that an active Flow roll its history into a successor run.
971    pub fn trigger_continue_as_new(&self, flow_id: &str) -> SdkResult<()> {
972        self.call_empty(
973            "trigger_continue_as_new",
974            Some(flow_id),
975            FlowTargetRequirement::Active,
976            |mut service| async move {
977                service
978                    .trigger_continue_as_new(TriggerContinueAsNewRequest {
979                        flow_id: flow_id.to_string(),
980                        run_id: String::new(),
981                    })
982                    .await
983            },
984        )
985    }
986
987    /// Verifies that Dex FlowService responds successfully.
988    ///
989    /// Returns `Ok(())` for a healthy service and a service-backed [`SdkError`] otherwise.
990    pub fn health_check(&self) -> SdkResult<()> {
991        self.call_empty(
992            "health_check",
993            None,
994            FlowTargetRequirement::None,
995            |mut service| async move { service.health_check(()).await },
996        )
997    }
998
999    fn do_invoke_rpc<Input: Value, Output: Value>(
1000        &self,
1001        flow_id: &str,
1002        rpc_name: &str,
1003        input: &Input,
1004        invoke_options: &RpcInvokeOptions,
1005    ) -> SdkResult<Output> {
1006        let (flow, rpc) = self.registry.rpc_with_flow(rpc_name)?;
1007        let mut lock_attribute_keys = rpc
1008            .locks
1009            .iter()
1010            .map(|lock| lock.physical_name())
1011            .collect::<BTreeSet<_>>();
1012        for lock in &invoke_options.lock_attribute_map_instances {
1013            let Some(instance) = lock.instance() else {
1014                return Err(SdkError::FlowDefinition {
1015                    message: "RpcInvokeOptions lock must target an AttributeMap instance".into(),
1016                });
1017            };
1018            require_rpc_invoke_definition(
1019                flow,
1020                lock.attribute_name(),
1021                crate::persistence::PersistenceKind::AttributeMap,
1022            )?;
1023            crate::registry::validate_map_instance(instance).map_err(invalid)?;
1024            lock_attribute_keys.insert(lock.physical_name());
1025        }
1026        let mut load_attribute_map_instances = rpc
1027            .load_attribute_maps
1028            .iter()
1029            .map(|load| map_load_name(&load.name, load.instance.as_deref()))
1030            .collect::<BTreeSet<_>>();
1031        for load in &invoke_options.load_attribute_map_instances {
1032            let instance = require_rpc_invoke_map_load(
1033                flow,
1034                &load.name,
1035                load.instance.as_deref(),
1036                crate::persistence::PersistenceKind::AttributeMap,
1037            )?;
1038            load_attribute_map_instances
1039                .insert(crate::registry::physical_name(&load.name, instance));
1040        }
1041        let mut load_channel_names = rpc
1042            .load_channels
1043            .iter()
1044            .map(|load| load.name.clone())
1045            .collect::<Vec<_>>();
1046        load_channel_names.sort();
1047        let mut load_channel_map_instances = rpc
1048            .load_channel_maps
1049            .iter()
1050            .map(|load| map_load_name(&load.name, load.instance.as_deref()))
1051            .collect::<BTreeSet<_>>();
1052        for load in &invoke_options.load_channel_map_instances {
1053            let instance = require_rpc_invoke_map_load(
1054                flow,
1055                &load.name,
1056                load.instance.as_deref(),
1057                crate::persistence::PersistenceKind::ChannelMap,
1058            )?;
1059            load_channel_map_instances.insert(crate::registry::physical_name(&load.name, instance));
1060        }
1061        let request = InvokeRpcRequest {
1062            flow_id: flow_id.to_string(),
1063            run_id: String::new(),
1064            rpc_name: rpc_name.to_string(),
1065            input: Some(value_mapper::encode(input)?),
1066            timeout_seconds: optional_seconds(rpc.timeout)?,
1067            lock_attribute_keys: lock_attribute_keys.into_iter().collect(),
1068            request_id: Uuid::new_v4().to_string(),
1069            is_transactional: rpc.is_transactional,
1070            load_attribute_map_instances: load_attribute_map_instances.into_iter().collect(),
1071            load_channel_names,
1072            load_channel_map_instances: load_channel_map_instances.into_iter().collect(),
1073        };
1074        self.require_server_protocol("invoke_rpc")?;
1075        let mut service = self.service.clone();
1076        let output = self.runtime.block_on(async {
1077            service
1078                .invoke_rpc(request)
1079                .await
1080                .map(|response| response.into_inner().output)
1081                .map_err(|status| {
1082                    SdkError::from_status(
1083                        status,
1084                        "invoke_rpc",
1085                        Some(flow_id),
1086                        FlowTargetRequirement::Active,
1087                    )
1088                })
1089        })?;
1090        let output = output.ok_or_else(|| invalid("InvokeRPC omitted output"))?;
1091        let output = self
1092            .runtime
1093            .block_on(self.hydrator.hydrate(flow_id, output))?;
1094        value_mapper::decode(&output)
1095    }
1096
1097    fn read_stream_result<T: Value>(
1098        &self,
1099        flow_id: &str,
1100        stream: &Stream<T>,
1101        resume_token: &str,
1102        timeout: Option<Duration>,
1103    ) -> SdkResult<StreamMessage<T>> {
1104        require_name(flow_id, "Flow ID")?;
1105        let flow_type = self.registry.flow_for_stream(stream)?.name.to_string();
1106        let wait_time_seconds = optional_seconds(timeout)?;
1107        let request = ReadStreamRequest {
1108            flow_id: flow_id.to_string(),
1109            flow_type,
1110            stream_name: stream.name().to_string(),
1111            resume_token: resume_token.to_string(),
1112            wait_time_seconds,
1113        };
1114        self.require_server_protocol("read_stream")?;
1115        let mut service = self.service.clone();
1116        let response = self.runtime.block_on(async {
1117            service
1118                .read_stream(request)
1119                .await
1120                .map(|response| response.into_inner())
1121                .map_err(|status| {
1122                    SdkError::from_status(
1123                        status,
1124                        "read_stream",
1125                        Some(flow_id),
1126                        FlowTargetRequirement::None,
1127                    )
1128                })
1129        })?;
1130        let message = response
1131            .message
1132            .ok_or_else(|| invalid("ReadStream omitted its message"))?;
1133        if message.resume_token.is_empty() {
1134            return Err(invalid("ReadStream returned an empty resume token"));
1135        }
1136        let value = message
1137            .value
1138            .ok_or_else(|| invalid("ReadStream omitted its Value"))?;
1139        let created_time = message
1140            .created_time
1141            .map(timestamp)
1142            .ok_or_else(|| invalid("ReadStream omitted its creation time"))?;
1143        Ok(StreamMessage {
1144            value: value_mapper::decode(&value)?,
1145            resume_token: message.resume_token,
1146            created_time,
1147            source: message.source,
1148        })
1149    }
1150
1151    fn wait_for_flow_result(
1152        &self,
1153        flow_id: &str,
1154        timeout: Option<Duration>,
1155    ) -> SdkResult<FlowResult> {
1156        self.require_server_protocol("wait_for_flow")?;
1157        let mut service = self.service.clone();
1158        let response = self.runtime.block_on(async {
1159            service
1160                .wait_for_flow(WaitForFlowRequest {
1161                    flow_id: flow_id.to_string(),
1162                    run_id: String::new(),
1163                    needs_results: true,
1164                    wait_time_seconds: optional_seconds(timeout)?,
1165                })
1166                .await
1167                .map(|response| response.into_inner())
1168                .map_err(|status| {
1169                    SdkError::from_status(
1170                        status,
1171                        "wait_for_flow",
1172                        Some(flow_id),
1173                        FlowTargetRequirement::Existing,
1174                    )
1175                })
1176        });
1177        let response = response?;
1178        let mut outputs = Vec::with_capacity(response.results.len());
1179        for result in &response.results {
1180            outputs.push(result.completed_step_output.clone().ok_or_else(|| {
1181                SdkError::ValueMapping {
1182                    message: "Step completion output is required".to_string(),
1183                }
1184            })?);
1185        }
1186        let outputs = self
1187            .runtime
1188            .block_on(self.hydrator.hydrate_all(flow_id, outputs))?;
1189        let completions = response
1190            .results
1191            .into_iter()
1192            .zip(outputs)
1193            .map(|(result, output)| {
1194                StepCompletion::new(
1195                    result.completed_step_type,
1196                    result.completed_step_execution_id,
1197                    output,
1198                )
1199            })
1200            .collect();
1201        Ok(FlowResult::new(
1202            map_flow_status(response.flow_status)?,
1203            map_flow_error_type(response.error_type),
1204            (!response.error_message.is_empty()).then_some(response.error_message),
1205            completions,
1206        ))
1207    }
1208
1209    fn map_start_options(
1210        &self,
1211        flow: &crate::registry::RegisteredFlow,
1212        options: &StartFlowOptions,
1213        flow_timeout_seconds: i32,
1214        flow_timeout_policy: i32,
1215    ) -> SdkResult<FlowStartOptions> {
1216        let attributes = options
1217            .attributes
1218            .iter()
1219            .map(|attribute| {
1220                let definition =
1221                    flow.persistence
1222                        .get(&attribute.attribute_name)
1223                        .ok_or_else(|| {
1224                            invalid(format!(
1225                                "initial Attribute is not registered: {}",
1226                                attribute.key
1227                            ))
1228                        })?;
1229                attribute.encode_attribute_write(definition)
1230            })
1231            .collect::<SdkResult<Vec<_>>>()?;
1232        if let Some(write) = attributes.iter().find(|write| write.is_hard_delete_on_null) {
1233            self.require_hard_delete_attribute_support("start_flow", &write.key)?;
1234        }
1235        let config = options.config_override.as_ref();
1236        let flow_config_override = Some(map_pull_flow_config(config)?);
1237        #[expect(
1238            clippy::needless_update,
1239            reason = "The test preserves forward-compatible construction of SDK options."
1240        )]
1241        let flow_start_options = FlowStartOptions {
1242            id_reuse_policy: match options.id_reuse_policy {
1243                IdReusePolicy::Default => ProtoIdReusePolicy::Unspecified,
1244                IdReusePolicy::AllowIfPreviousFailed => {
1245                    ProtoIdReusePolicy::AllowIfPreviousExistsAbnormally
1246                }
1247                IdReusePolicy::AllowIfNotRunning => ProtoIdReusePolicy::AllowIfNoRunning,
1248                IdReusePolicy::Disallow => ProtoIdReusePolicy::DisallowReuse,
1249                IdReusePolicy::TerminateIfRunning => ProtoIdReusePolicy::AllowTerminateIfRunning,
1250            } as i32,
1251            flow_start_delay_seconds: optional_seconds(options.start_delay)?,
1252            retry_policy: options
1253                .retry_policy
1254                .clone()
1255                .map(map_flow_retry)
1256                .transpose()?,
1257            attributes,
1258            flow_config_override,
1259            flow_already_started_options: Some(FlowAlreadyStartedOptions {
1260                ignore_already_started_error: options.ignore_already_started,
1261            }),
1262            timeout_handler_options: map_flow_timeout_handler_options(
1263                flow,
1264                flow_timeout_seconds,
1265                flow_timeout_policy,
1266                options.timeout_handler_options.as_ref(),
1267            )
1268            .map_err(sdk_handler_error)?,
1269            ..Default::default()
1270        };
1271        Ok(flow_start_options)
1272    }
1273
1274    fn require_server_protocol(&self, operation: &'static str) -> SdkResult<()> {
1275        self.runtime
1276            .block_on(self.negotiate_server_protocol_once(operation))
1277    }
1278
1279    /// Negotiates within the caller's remaining request budget and returns
1280    /// [`SdkError::RequestTimeout`] when it expires first; a timeout is never cached.
1281    fn require_server_protocol_within_budget(
1282        &self,
1283        operation: &'static str,
1284        flow_id: &str,
1285        request_budget: &ClientRequestBudget,
1286    ) -> SdkResult<()> {
1287        let Some(deadline) = request_budget.deadline() else {
1288            return self.require_server_protocol(operation);
1289        };
1290        self.runtime
1291            .block_on(async {
1292                tokio::time::timeout_at(
1293                    tokio::time::Instant::from_std(deadline),
1294                    self.negotiate_server_protocol_once(operation),
1295                )
1296                .await
1297            })
1298            .unwrap_or_else(|_| Err(SdkError::request_timeout(operation, flow_id)))
1299    }
1300
1301    async fn negotiate_server_protocol_once(&self, operation: &'static str) -> SdkResult<()> {
1302        let mut cached = self.protocol_negotiation.lock().await;
1303        if cached.is_none() {
1304            let mut service = self.service.clone();
1305            let server_info = service
1306                .get_server_info(())
1307                .await
1308                .map_err(|status| {
1309                    SdkError::from_status(status, operation, None, FlowTargetRequirement::None)
1310                })?
1311                .into_inner();
1312            *cached = Some(negotiate_server_protocol(&server_info));
1313        }
1314        match cached.as_ref().expect("Server negotiation was cached") {
1315            Ok(version) => {
1316                self.negotiated_protocol_version.store(*version);
1317                Ok(())
1318            }
1319            Err(message) => Err(SdkError::Service {
1320                service: ServiceError::local("server_protocol", message.clone()),
1321            }),
1322        }
1323    }
1324
1325    fn require_hard_delete_attribute_support(
1326        &self,
1327        operation: &'static str,
1328        attribute_key: &str,
1329    ) -> SdkResult<()> {
1330        self.require_server_protocol(operation)?;
1331        self.negotiated_protocol_version
1332            .require_hard_delete_attribute_support(attribute_key)
1333            .map_err(|error| SdkError::Service {
1334                service: ServiceError::local(operation, error),
1335            })
1336    }
1337
1338    fn map_flow_config(&self, config: Option<&FlowConfig>) -> SdkResult<ProtoFlowConfig> {
1339        map_flow_config(config)
1340    }
1341
1342    fn call_empty<Response, Future, Call>(
1343        &self,
1344        operation: &'static str,
1345        flow_id: Option<&str>,
1346        requirement: FlowTargetRequirement,
1347        call: Call,
1348    ) -> SdkResult<()>
1349    where
1350        Call: FnOnce(FlowServiceClient<CredentialChannel>) -> Future,
1351        Future: std::future::Future<Output = Result<tonic::Response<Response>, tonic::Status>>,
1352    {
1353        self.require_server_protocol(operation)?;
1354        let service = self.service.clone();
1355        self.runtime
1356            .block_on(call(service))
1357            .map(|_| ())
1358            .map_err(|status| SdkError::from_status(status, operation, flow_id, requirement))
1359    }
1360}
1361
1362pub(crate) fn map_pull_flow_config(config: Option<&FlowConfig>) -> SdkResult<ProtoFlowConfig> {
1363    Ok(ProtoFlowConfig {
1364        worker_target: Some(ProtoWorkerTarget { is_pull_mode: true }),
1365        ..map_flow_config(config)?
1366    })
1367}
1368
1369pub(crate) fn map_flow_config(config: Option<&FlowConfig>) -> SdkResult<ProtoFlowConfig> {
1370    Ok(ProtoFlowConfig {
1371        active_step_search_mode: config
1372            .and_then(|config| config.active_step_search_mode)
1373            .map(|mode| match mode {
1374                ActiveStepSearchMode::All => ProtoSearchMode::EnabledForAll as i32,
1375                ActiveStepSearchMode::WithWaitFor => {
1376                    ProtoSearchMode::EnabledForStepsWithWaitFor as i32
1377                }
1378                ActiveStepSearchMode::Disabled => ProtoSearchMode::Disabled as i32,
1379            }),
1380        continue_as_new_threshold: config
1381            .and_then(|config| config.continue_as_new_threshold)
1382            .map(i32::try_from)
1383            .transpose()
1384            .map_err(|_| invalid("continue-as-new threshold exceeds int32"))?,
1385        continue_as_new_page_size_in_bytes: config
1386            .and_then(|config| config.continue_as_new_page_size_bytes)
1387            .map(i32::try_from)
1388            .transpose()
1389            .map_err(|_| invalid("continue-as-new page size exceeds int32"))?,
1390        step_durability: config
1391            .and_then(|config| config.step_durability)
1392            .map(|durability| {
1393                (match durability {
1394                    StepDurability::Default => ProtoStepDurability::Unspecified,
1395                    StepDurability::Sync => ProtoStepDurability::Sync,
1396                    StepDurability::Async => ProtoStepDurability::Async,
1397                }) as i32
1398            }),
1399        worker_target: None,
1400        attribute_store_names: config.and_then(|config| {
1401            config
1402                .attribute_store_names
1403                .as_ref()
1404                .map(|names| AttributeStoreNames {
1405                    names: names.clone(),
1406                })
1407        }),
1408    })
1409}
1410
1411pub(crate) fn map_flow_retry(retry: RetryPolicy) -> SdkResult<FlowRetryPolicy> {
1412    Ok(FlowRetryPolicy {
1413        initial_interval_seconds: optional_seconds(retry.initial_interval)?,
1414        backoff_coefficient: retry.backoff_coefficient.unwrap_or_default() as f32,
1415        maximum_interval_seconds: optional_seconds(retry.maximum_interval)?,
1416        maximum_attempts: i32::try_from(retry.maximum_attempts.unwrap_or_default())
1417            .map_err(|_| invalid("maximum attempts exceed int32"))?,
1418    })
1419}
1420
1421pub(crate) fn map_flow_status(status: i32) -> SdkResult<FlowStatus> {
1422    match ProtoFlowStatus::try_from(status).ok() {
1423        Some(ProtoFlowStatus::Running) => Ok(FlowStatus::Running),
1424        Some(ProtoFlowStatus::Completed) => Ok(FlowStatus::Completed),
1425        Some(ProtoFlowStatus::Failed) => Ok(FlowStatus::Failed),
1426        Some(ProtoFlowStatus::ServerSideTimeoutInternalOnly) => {
1427            Ok(FlowStatus::ServerSideTimeoutInternalOnly)
1428        }
1429        Some(ProtoFlowStatus::Terminated) => Ok(FlowStatus::Terminated),
1430        Some(ProtoFlowStatus::Canceled) => Ok(FlowStatus::Canceled),
1431        Some(ProtoFlowStatus::ContinuedAsNew) => Ok(FlowStatus::ContinuedAsNew),
1432        _ => Err(invalid(format!("unknown Flow status {status}"))),
1433    }
1434}
1435
1436pub(crate) fn map_flow_error_type(error_type: i32) -> Option<FlowErrorType> {
1437    use dex_protocol::dex::FlowErrorType as ProtoFlowErrorType;
1438
1439    match ProtoFlowErrorType::try_from(error_type).ok() {
1440        Some(ProtoFlowErrorType::StepDecisionFailingFlow) => {
1441            Some(FlowErrorType::StepDecisionFailed)
1442        }
1443        Some(ProtoFlowErrorType::ClientApiFailingFlow) => Some(FlowErrorType::ClientApiFailed),
1444        Some(ProtoFlowErrorType::WorkerApiFail) => Some(FlowErrorType::WorkerApiFailed),
1445        Some(ProtoFlowErrorType::InvalidUserFlowCode) => Some(FlowErrorType::InvalidUserFlowCode),
1446        Some(ProtoFlowErrorType::Internal) => Some(FlowErrorType::Internal),
1447        Some(ProtoFlowErrorType::FlowTimeout) => Some(FlowErrorType::FlowTimeout),
1448        _ => None,
1449    }
1450}
1451
1452pub(crate) fn map_flow_timeout_policy(
1453    flow: &crate::registry::RegisteredFlow,
1454    timeout_seconds: i32,
1455    policy: FlowTimeoutPolicy,
1456) -> SdkResult<i32> {
1457    if timeout_seconds == 0 {
1458        if policy != FlowTimeoutPolicy::Default {
1459            return Err(invalid("Flow timeout policy requires a positive timeout"));
1460        }
1461        return Ok(ProtoFlowTimeoutPolicy::Unspecified as i32);
1462    }
1463    let resolved = match policy {
1464        FlowTimeoutPolicy::Default if flow.handler.has_timeout_handler() => {
1465            FlowTimeoutPolicy::Handler
1466        }
1467        FlowTimeoutPolicy::Default => FlowTimeoutPolicy::Fail,
1468        configured => configured,
1469    };
1470    if resolved == FlowTimeoutPolicy::Handler && !flow.handler.has_timeout_handler() {
1471        return Err(invalid(format!(
1472            "Flow {} has no timeout handler",
1473            flow.name
1474        )));
1475    }
1476    Ok(match resolved {
1477        FlowTimeoutPolicy::Default => ProtoFlowTimeoutPolicy::Unspecified,
1478        FlowTimeoutPolicy::Fail => ProtoFlowTimeoutPolicy::Fail,
1479        FlowTimeoutPolicy::Cancel => ProtoFlowTimeoutPolicy::Cancel,
1480        FlowTimeoutPolicy::Handler => ProtoFlowTimeoutPolicy::Handler,
1481    } as i32)
1482}
1483
1484fn timestamp(timestamp: prost_types::Timestamp) -> SystemTime {
1485    if timestamp.seconds >= 0 {
1486        SystemTime::UNIX_EPOCH
1487            + Duration::from_secs(timestamp.seconds as u64)
1488            + Duration::from_nanos(timestamp.nanos.max(0) as u64)
1489    } else {
1490        SystemTime::UNIX_EPOCH - Duration::from_secs(timestamp.seconds.unsigned_abs())
1491    }
1492}
1493
1494fn rfc3339(time: SystemTime) -> SdkResult<String> {
1495    let duration = time
1496        .duration_since(SystemTime::UNIX_EPOCH)
1497        .map_err(service_error)?;
1498    let days = (duration.as_secs() / 86_400) as i64;
1499    let seconds = duration.as_secs() % 86_400;
1500    let (year, month, day) = civil_from_days(days);
1501    let hour = seconds / 3_600;
1502    let minute = seconds % 3_600 / 60;
1503    let second = seconds % 60;
1504    Ok(format!(
1505        "{year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}Z"
1506    ))
1507}
1508
1509fn civil_from_days(days: i64) -> (i64, i64, i64) {
1510    let days = days + 719_468;
1511    let era = if days >= 0 { days } else { days - 146_096 } / 146_097;
1512    let day_of_era = days - era * 146_097;
1513    let year_of_era =
1514        (day_of_era - day_of_era / 1_460 + day_of_era / 36_524 - day_of_era / 146_096) / 365;
1515    let mut year = year_of_era + era * 400;
1516    let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
1517    let month_prime = (5 * day_of_year + 2) / 153;
1518    let day = day_of_year - (153 * month_prime + 2) / 5 + 1;
1519    let month = month_prime + if month_prime < 10 { 3 } else { -9 };
1520    year += i64::from(month <= 2);
1521    (year, month, day)
1522}
1523
1524fn map_load_name(name: &str, instance: Option<&str>) -> String {
1525    match instance {
1526        Some(instance) => crate::registry::physical_name(name, instance),
1527        None => format!("{name}/"),
1528    }
1529}
1530
1531fn require_rpc_invoke_map_load<'a>(
1532    flow: &crate::registry::RegisteredFlow,
1533    name: &str,
1534    instance: Option<&'a str>,
1535    expected_kind: crate::persistence::PersistenceKind,
1536) -> SdkResult<&'a str> {
1537    require_rpc_invoke_definition(flow, name, expected_kind)?;
1538    let instance = instance.ok_or_else(|| SdkError::FlowDefinition {
1539        message: "RpcInvokeOptions requires an exact map instance".to_string(),
1540    })?;
1541    crate::registry::validate_map_instance(instance).map_err(invalid)?;
1542    Ok(instance)
1543}
1544
1545fn require_rpc_invoke_definition(
1546    flow: &crate::registry::RegisteredFlow,
1547    name: &str,
1548    expected_kind: crate::persistence::PersistenceKind,
1549) -> SdkResult<()> {
1550    flow.persistence
1551        .get(name)
1552        .filter(|definition| definition.kind == expected_kind)
1553        .ok_or_else(|| SdkError::FlowDefinition {
1554            message: format!(
1555                "Flow {} does not register the selected map {name}",
1556                flow.name
1557            ),
1558        })?;
1559    Ok(())
1560}
1561
1562fn optional_seconds(duration: Option<Duration>) -> SdkResult<i32> {
1563    duration
1564        .map(seconds32)
1565        .transpose()
1566        .map(Option::unwrap_or_default)
1567}
1568
1569pub(crate) fn seconds32(duration: Duration) -> SdkResult<i32> {
1570    if duration.subsec_nanos() != 0 {
1571        return Err(invalid("Duration must use whole seconds"));
1572    }
1573    i32::try_from(duration.as_secs()).map_err(|_| invalid("Duration exceeds int32"))
1574}
1575
1576fn require_name(value: &str, kind: &str) -> SdkResult<()> {
1577    if value.is_empty() {
1578        Err(invalid(format!("{kind} is required")))
1579    } else {
1580        Ok(())
1581    }
1582}
1583
1584fn map_physical_name(name: &str, instance: &str) -> SdkResult<String> {
1585    crate::registry::validate_map_instance(instance).map_err(invalid)?;
1586    Ok(crate::registry::physical_name(name, instance))
1587}
1588
1589fn require_waitable_attribute(attribute_name: &str, is_hard_delete_on_null: bool) -> SdkResult<()> {
1590    if is_hard_delete_on_null {
1591        return Err(invalid(format!(
1592            "wait_for_attribute_match does not support hard-delete Attribute {attribute_name}"
1593        )));
1594    }
1595    Ok(())
1596}
1597
1598fn sdk_handler_error(error: impl std::fmt::Display) -> SdkError {
1599    invalid(error.to_string())
1600}
1601
1602pub(crate) fn invalid(message: impl Into<String>) -> SdkError {
1603    SdkError::InvalidArgument {
1604        message: message.into(),
1605    }
1606}
1607
1608fn service_error(error: impl std::fmt::Display) -> SdkError {
1609    SdkError::Service {
1610        service: ServiceError::local("client", error.to_string()),
1611    }
1612}
1613
1614#[cfg(test)]
1615mod tests;
1616
1617#[cfg(test)]
1618#[path = "client_pull_routing_tests.rs"]
1619mod pull_routing_tests;