1use std::any::TypeId;
10use std::collections::BTreeMap;
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, AttributeWrite,
17 FlowAlreadyStartedOptions, FlowConfig as ProtoFlowConfig, FlowResetStepMethod, FlowResetType,
18 FlowRetryPolicy, FlowStartOptions, FlowStatus as ProtoFlowStatus,
19 FlowTimeoutPolicy as ProtoFlowTimeoutPolicy, GetAttributesRequest, GetFlowSummaryRequest,
20 IdReusePolicy as ProtoIdReusePolicy, InvokeRpcRequest, PublishToChannelRequest,
21 ReadStreamRequest, ResetFlowRequest, SearchFlowsRequest, SetAttributesRequest,
22 SkipTimerRequest, StartFlowRequest, StepDurability as ProtoStepDurability, StopFlowRequest,
23 StopType as ProtoStopType, TriggerContinueAsNewRequest, UpdateFlowConfigRequest,
24 WaitForAttributeCondition, WaitForAttributeEqual, WaitForAttributeRequest, WaitForFlowRequest,
25 WaitForStepCompletionRequest, WorkerTarget as ProtoWorkerTarget, WriteStreamRequest,
26 wait_for_attribute_condition,
27};
28use tokio::runtime::Runtime;
29use tonic::transport::Endpoint;
30use uuid::Uuid;
31
32use crate::sdk_error::{FlowTargetRequirement, ServiceError};
33use crate::stop_flow_options::StopType;
34use crate::time_travel_options::{TimeTravelPoint, TimeTravelStepMethod};
35use crate::value_hydrator::ValueHydrator;
36use crate::value_mapper;
37use crate::worker_dispatcher::map_step_options;
38use crate::{
39 ActiveStepSearchMode, Attribute, AttributeMap, BlobCache, Channel, ChannelMap, ClientOptions,
40 Flow, FlowConfig, FlowErrorType, FlowInfo, FlowResult, FlowStatus, FlowTimeoutPolicy,
41 IdReusePolicy, Registry, RetryPolicy, Rpc, SdkError, SdkResult, SearchFlowEntry,
42 SearchFlowsPage, StartFlowOptions, StepCompletion, StepDurability, StepExecutionId,
43 StopFlowOptions, Stream, StreamMessage, TimeTravelOptions, TimerId, Value, WorkerTarget,
44};
45
46pub struct Client {
66 runtime: Runtime,
67 registry: Registry,
68 service: FlowServiceClient<tonic::transport::Channel>,
69 hydrator: ValueHydrator,
70 options: ClientOptions,
71}
72
73impl Client {
74 pub fn new(registry: Registry, blob_cache: Arc<BlobCache>, options: ClientOptions) -> Self {
82 Self::try_new(registry, blob_cache, options)
83 .unwrap_or_else(|error| panic!("cannot create Dex Client: {error}"))
84 }
85
86 pub fn try_new(
93 registry: Registry,
94 blob_cache: Arc<BlobCache>,
95 options: ClientOptions,
96 ) -> SdkResult<Self> {
97 let runtime = Runtime::new().map_err(service_error)?;
98 let endpoint = Endpoint::from_shared(endpoint_address(options.server_address_value()))
99 .map_err(service_error)?;
100 let service = {
101 let _runtime_guard = runtime.enter();
102 FlowServiceClient::new(endpoint.connect_lazy())
103 };
104 let hydrator = ValueHydrator::new(service.clone(), blob_cache);
105 Ok(Self {
106 runtime,
107 registry,
108 service,
109 hydrator,
110 options,
111 })
112 }
113
114 pub fn start_flow<SomeFlow: Flow>(
123 &self,
124 flow: &SomeFlow,
125 flow_id: &str,
126 input: SomeFlow::StartInput,
127 ) -> SdkResult<String> {
128 self.start_flow_with_options(flow, flow_id, input, StartFlowOptions::new())
129 }
130
131 pub fn start_flow_with_options<SomeFlow: Flow>(
140 &self,
141 flow: &SomeFlow,
142 flow_id: &str,
143 input: SomeFlow::StartInput,
144 options: StartFlowOptions,
145 ) -> SdkResult<String> {
146 require_name(flow_id, "Flow ID")?;
147 let registered = self.registry.flow(flow.flow_type())?;
148 if registered.type_id != TypeId::of::<SomeFlow>() {
149 return Err(SdkError::FlowDefinition {
150 message: format!(
151 "Flow {} does not match the registered Rust type",
152 flow.flow_type()
153 ),
154 });
155 }
156 let (start_step_type, step_input, step_options) = match ®istered.start_step {
157 Some(step) => (
158 step.name.to_string(),
159 Some(value_mapper::encode(&input)?),
160 Some(
161 map_step_options(
162 registered,
163 registered
164 .handler
165 .step_options(step.name)
166 .map_err(sdk_handler_error)?,
167 )
168 .map_err(sdk_handler_error)?,
169 ),
170 ),
171 None => (String::new(), None, None),
172 };
173 let flow_timeout_seconds = optional_seconds(options.timeout)?;
174 let flow_timeout_policy =
175 map_flow_timeout_policy(registered, flow_timeout_seconds, options.timeout_policy)?;
176 let request = StartFlowRequest {
177 flow_id: flow_id.to_string(),
178 flow_type: registered.name.to_string(),
179 flow_timeout_seconds,
180 flow_timeout_policy,
181 start_step_type,
182 step_input,
183 step_options,
184 flow_start_options: Some(self.map_start_options(registered, &options)?),
185 request_id: options
186 .request_id
187 .clone()
188 .unwrap_or_else(|| Uuid::new_v4().to_string()),
189 };
190 let mut service = self.service.clone();
191 self.runtime.block_on(async {
192 service
193 .start_flow(request)
194 .await
195 .map(|response| response.into_inner().run_id)
196 .map_err(|status| {
197 SdkError::from_status(
198 status,
199 "start_flow",
200 Some(flow_id),
201 FlowTargetRequirement::None,
202 )
203 })
204 })
205 }
206
207 pub fn invoke_rpc<Input: Value, Output: Value>(
214 &self,
215 flow_id: &str,
216 rpc: Rpc<Input, Output>,
217 input: Input,
218 ) -> SdkResult<Output> {
219 self.do_invoke_rpc(flow_id, rpc.name(), &input)
220 }
221
222 pub fn invoke_rpc_without_input<Output: Value>(
226 &self,
227 flow_id: &str,
228 rpc: Rpc<(), Output>,
229 ) -> SdkResult<Output> {
230 self.do_invoke_rpc(flow_id, rpc.name(), &())
231 }
232
233 pub fn get_attribute<T: Value>(
237 &self,
238 flow_id: &str,
239 attribute: &Attribute<T>,
240 ) -> SdkResult<Option<T>> {
241 self.get_attribute_value(flow_id, attribute.name())
242 }
243
244 pub fn get_attribute_map_instance<T: Value>(
248 &self,
249 flow_id: &str,
250 attribute: &AttributeMap<T>,
251 instance: &str,
252 ) -> SdkResult<Option<T>> {
253 self.get_attribute_value(
254 flow_id,
255 &crate::registry::physical_name(attribute.name(), instance),
256 )
257 }
258
259 pub fn set_attribute<T: Value>(
265 &self,
266 flow_id: &str,
267 attribute: &Attribute<T>,
268 value: T,
269 ) -> SdkResult<()> {
270 self.set_attribute_value(
271 flow_id,
272 attribute.name(),
273 &value,
274 attribute.index().map(|index| index.proto_config(false)),
275 attribute.sync_config(),
276 )
277 }
278
279 pub fn set_attribute_map_instance<T: Value>(
285 &self,
286 flow_id: &str,
287 attribute: &AttributeMap<T>,
288 instance: &str,
289 value: T,
290 ) -> SdkResult<()> {
291 self.set_attribute_value(
292 flow_id,
293 &crate::registry::physical_name(attribute.name(), instance),
294 &value,
295 attribute.index().map(|index| index.proto_config(true)),
296 attribute.sync_config(),
297 )
298 }
299
300 pub fn publish<T: Value>(
302 &self,
303 flow_id: &str,
304 channel: &Channel<T>,
305 value: T,
306 ) -> SdkResult<()> {
307 self.publish_values(flow_id, channel.name(), [value])
308 }
309
310 pub fn publish_many<T: Value>(
312 &self,
313 flow_id: &str,
314 channel: &Channel<T>,
315 values: impl IntoIterator<Item = T>,
316 ) -> SdkResult<()> {
317 self.publish_values(flow_id, channel.name(), values)
318 }
319
320 pub fn publish_map<T: Value>(
322 &self,
323 flow_id: &str,
324 channel: &ChannelMap<T>,
325 instance: &str,
326 values: impl IntoIterator<Item = T>,
327 ) -> SdkResult<()> {
328 self.publish_values(
329 flow_id,
330 &crate::registry::physical_name(channel.name(), instance),
331 values,
332 )
333 }
334
335 pub fn write_stream<T: Value>(
345 &self,
346 flow_id: &str,
347 stream: &Stream<T>,
348 idempotency_key: &str,
349 value: T,
350 ) -> SdkResult<()> {
351 require_name(flow_id, "Flow ID")?;
352 require_name(idempotency_key, "Stream idempotency key")?;
353 if idempotency_key.contains('#') {
354 return Err(invalid("Stream client idempotency key must not contain #"));
355 }
356 let flow_type = self.registry.flow_for_stream(stream)?.name.to_string();
357 let request = WriteStreamRequest {
358 flow_id: flow_id.to_string(),
359 flow_type,
360 stream_name: stream.name().to_string(),
361 max_estimated_bytes: stream.max_estimated_bytes(),
362 value: Some(value_mapper::encode(&value)?),
363 idempotency_key: idempotency_key.to_string(),
364 };
365 self.call_empty(
366 "write_stream",
367 Some(flow_id),
368 FlowTargetRequirement::None,
369 |mut service| async move { service.write_stream(request).await },
370 )
371 }
372
373 pub fn read_stream<T: Value>(
377 &self,
378 flow_id: &str,
379 stream: &Stream<T>,
380 resume_token: &str,
381 ) -> SdkResult<StreamMessage<T>> {
382 self.read_stream_result(flow_id, stream, resume_token, None)
383 }
384
385 pub fn read_stream_with_timeout<T: Value>(
392 &self,
393 flow_id: &str,
394 stream: &Stream<T>,
395 resume_token: &str,
396 timeout: Duration,
397 ) -> SdkResult<StreamMessage<T>> {
398 self.read_stream_result(flow_id, stream, resume_token, Some(timeout))
399 }
400
401 pub fn wait_for_flow(&self, flow_id: &str) -> SdkResult<FlowResult> {
408 self.wait_for_flow_result(flow_id, None)
409 }
410
411 pub fn wait_for_flow_with_timeout(
418 &self,
419 flow_id: &str,
420 timeout: Duration,
421 ) -> SdkResult<FlowResult> {
422 self.wait_for_flow_result(flow_id, Some(timeout))
423 }
424
425 pub fn describe_flow(&self, flow_id: &str) -> SdkResult<FlowInfo> {
431 let mut service = self.service.clone();
432 let response = self.runtime.block_on(async {
433 service
434 .get_flow_summary(GetFlowSummaryRequest {
435 flow_id: flow_id.to_string(),
436 run_id: String::new(),
437 })
438 .await
439 .map(|response| response.into_inner())
440 .map_err(|status| {
441 SdkError::from_status(
442 status,
443 "describe_flow",
444 Some(flow_id),
445 FlowTargetRequirement::Existing,
446 )
447 })
448 })?;
449 let execution = response
450 .flow_execution_id
451 .ok_or_else(|| service_error("GetFlowSummary omitted FlowExecutionID"))?;
452 Ok(FlowInfo {
453 flow_id: execution.flow_id,
454 run_id: execution.run_id,
455 flow_type: response.flow_type,
456 status: map_flow_status(response.flow_status)?,
457 started_at: response
458 .start_time
459 .map_or(SystemTime::UNIX_EPOCH, timestamp),
460 })
461 }
462
463 pub fn search_flows(&self, query: &str, page_size: i32) -> SdkResult<SearchFlowsPage> {
468 self.search_flows_page(query, page_size, "")
469 }
470
471 pub fn search_flows_page(
476 &self,
477 query: &str,
478 page_size: i32,
479 next_page_token: &str,
480 ) -> SdkResult<SearchFlowsPage> {
481 if page_size < 0 {
482 return Err(invalid("search page size must not be negative"));
483 }
484 let mut service = self.service.clone();
485 self.runtime.block_on(async {
486 let response = service
487 .search_flows(SearchFlowsRequest {
488 query: query.to_string(),
489 page_size,
490 next_page_token: next_page_token.to_string(),
491 })
492 .await
493 .map(|response| response.into_inner())
494 .map_err(|status| {
495 SdkError::from_status(status, "search_flows", None, FlowTargetRequirement::None)
496 })?;
497 let values = response
498 .flow_runs
499 .iter()
500 .flat_map(|entry| entry.indexed_attributes.iter())
501 .map(|attribute| {
502 attribute
503 .value
504 .clone()
505 .ok_or_else(|| invalid("Indexed Attribute has no Value"))
506 })
507 .collect::<SdkResult<Vec<_>>>()?;
508 let mut values = self.hydrator.hydrate_all(values).await?.into_iter();
509 let mut flows = Vec::with_capacity(response.flow_runs.len());
510 for entry in response.flow_runs {
511 let mut indexed_attributes = BTreeMap::new();
512 for attribute in entry.indexed_attributes {
513 let value = values
514 .next()
515 .ok_or_else(|| invalid("Indexed Attribute hydration count mismatch"))?;
516 indexed_attributes.insert(attribute.key, value_mapper::decode_untyped(&value)?);
517 }
518 flows.push(SearchFlowEntry {
519 flow_id: entry.flow_id,
520 run_id: entry.run_id,
521 flow_type: entry.flow_type,
522 status: map_flow_status(entry.flow_status)?,
523 started_at: entry.start_time.map(timestamp),
524 closed_at: entry.close_time.map(timestamp),
525 indexed_attributes,
526 });
527 }
528 Ok(SearchFlowsPage {
529 flows,
530 next_page_token: response.next_page_token,
531 })
532 })
533 }
534
535 pub fn stop_flow(&self, flow_id: &str, options: StopFlowOptions) -> SdkResult<()> {
541 let stop_type = match options.stop_type {
542 StopType::Cancel => ProtoStopType::Cancel,
543 StopType::Terminate => ProtoStopType::Terminate,
544 StopType::Fail => ProtoStopType::Fail,
545 };
546 self.call_empty(
547 "stop_flow",
548 Some(flow_id),
549 FlowTargetRequirement::Active,
550 |mut service| async move {
551 service
552 .stop_flow(StopFlowRequest {
553 flow_id: flow_id.to_string(),
554 run_id: String::new(),
555 reason: options.reason.unwrap_or_default(),
556 stop_type: stop_type as i32,
557 })
558 .await
559 },
560 )
561 }
562
563 pub fn time_travel(&self, flow_id: &str, options: TimeTravelOptions) -> SdkResult<String> {
572 let mut request = ResetFlowRequest {
573 flow_id: flow_id.to_string(),
574 run_id: String::new(),
575 reset_type: FlowResetType::Unspecified as i32,
576 reason: options.reason.unwrap_or_default(),
577 history_event_time: String::new(),
578 step_type: String::new(),
579 step_execution_id: String::new(),
580 skip_writes_reapply: options.skip_writes_reapply,
581 step_method: FlowResetStepMethod::Unspecified as i32,
582 };
583 match options.point {
584 TimeTravelPoint::Beginning => request.reset_type = FlowResetType::Beginning as i32,
585 TimeTravelPoint::HistoryEventTime(time) => {
586 request.reset_type = FlowResetType::HistoryEventTime as i32;
587 request.history_event_time = rfc3339(time)?;
588 }
589 TimeTravelPoint::StepType(step_type) => {
590 request.reset_type = FlowResetType::StepType as i32;
591 request.step_type = step_type.to_string();
592 }
593 TimeTravelPoint::StepExecution(execution, method) => {
594 request.reset_type = FlowResetType::StepExecutionId as i32;
595 request.step_execution_id =
596 format!("{}-{}", execution.step_type, execution.execution_number);
597 request.step_method = match method {
598 TimeTravelStepMethod::WaitFor => FlowResetStepMethod::WaitFor as i32,
599 TimeTravelStepMethod::Execute => FlowResetStepMethod::Execute as i32,
600 };
601 }
602 }
603 let mut service = self.service.clone();
604 self.runtime.block_on(async {
605 service
606 .reset_flow(request)
607 .await
608 .map(|response| response.into_inner().run_id)
609 .map_err(|status| {
610 SdkError::from_status(
611 status,
612 "time_travel",
613 Some(flow_id),
614 FlowTargetRequirement::Existing,
615 )
616 })
617 })
618 }
619
620 pub fn skip_timer(
627 &self,
628 flow_id: &str,
629 step_execution: StepExecutionId,
630 timer: TimerId,
631 ) -> SdkResult<()> {
632 let (timer_condition_id, timer_condition_index) = match timer {
633 TimerId::ConditionId(id) => (id, None),
634 TimerId::ConditionIndex(index) => (
635 String::new(),
636 Some(i32::try_from(index).map_err(|_| invalid("timer index exceeds int32"))?),
637 ),
638 };
639 self.call_empty(
640 "skip_timer",
641 Some(flow_id),
642 FlowTargetRequirement::Active,
643 |mut service| async move {
644 service
645 .skip_timer(SkipTimerRequest {
646 flow_id: flow_id.to_string(),
647 run_id: String::new(),
648 step_execution_id: format!(
649 "{}-{}",
650 step_execution.step_type, step_execution.execution_number
651 ),
652 timer_condition_id,
653 timer_condition_index,
654 })
655 .await
656 },
657 )
658 }
659
660 pub fn wait_for_step_completion(
667 &self,
668 flow_id: &str,
669 step_execution: StepExecutionId,
670 timeout: Duration,
671 ) -> SdkResult<()> {
672 let wait_time_seconds = seconds32(timeout)?;
673 self.call_empty(
674 "wait_for_step_completion",
675 Some(flow_id),
676 FlowTargetRequirement::Active,
677 |mut service| async move {
678 service
679 .wait_for_step_completion(WaitForStepCompletionRequest {
680 flow_id: flow_id.to_string(),
681 step_type: step_execution.step_type.to_string(),
682 step_execution_number: step_execution.execution_number.to_string(),
683 wait_time_seconds,
684 request_id: Uuid::new_v4().to_string(),
685 })
686 .await
687 },
688 )
689 }
690
691 pub fn wait_for_attribute_equal<T: Value>(
697 &self,
698 flow_id: &str,
699 attribute: &Attribute<T>,
700 expected: T,
701 timeout: Duration,
702 ) -> SdkResult<()> {
703 self.wait_for_attribute_value(flow_id, attribute.name(), &expected, timeout)
704 }
705
706 pub fn wait_for_attribute_map_instance_equal<T: Value>(
711 &self,
712 flow_id: &str,
713 attribute: &AttributeMap<T>,
714 instance: &str,
715 expected: T,
716 timeout: Duration,
717 ) -> SdkResult<()> {
718 self.wait_for_attribute_value(
719 flow_id,
720 &crate::registry::physical_name(attribute.name(), instance),
721 &expected,
722 timeout,
723 )
724 }
725
726 fn wait_for_attribute_value<T: Value>(
727 &self,
728 flow_id: &str,
729 key: &str,
730 expected: &T,
731 timeout: Duration,
732 ) -> SdkResult<()> {
733 let value = value_mapper::encode(expected)?;
734 if !matches!(
735 value.kind,
736 Some(dex_protocol::dex::value::Kind::StringValue(_))
737 | Some(dex_protocol::dex::value::Kind::BoolValue(_))
738 | Some(dex_protocol::dex::value::Kind::IntValue(_))
739 | Some(dex_protocol::dex::value::Kind::DoubleValue(_))
740 ) {
741 return Err(invalid(
742 "wait_for_attribute_equal supports only string, boolean, or number values",
743 ));
744 }
745 let wait_time_seconds = seconds32(timeout)?;
746 self.call_empty(
747 "wait_for_attribute_equal",
748 Some(flow_id),
749 FlowTargetRequirement::Active,
750 |mut service| async move {
751 service
752 .wait_for_attribute(WaitForAttributeRequest {
753 flow_id: flow_id.to_string(),
754 run_id: String::new(),
755 condition: Some(WaitForAttributeCondition {
756 kind: Some(wait_for_attribute_condition::Kind::Equal(
757 WaitForAttributeEqual {
758 key: key.to_string(),
759 value: Some(value),
760 },
761 )),
762 }),
763 wait_time_seconds,
764 request_id: Uuid::new_v4().to_string(),
765 })
766 .await
767 },
768 )
769 }
770
771 pub fn update_flow_config(&self, flow_id: &str, config: FlowConfig) -> SdkResult<()> {
775 let flow_config = self.map_flow_config(Some(&config))?;
776 self.call_empty(
777 "update_flow_config",
778 Some(flow_id),
779 FlowTargetRequirement::Active,
780 |mut service| async move {
781 service
782 .update_flow_config(UpdateFlowConfigRequest {
783 flow_id: flow_id.to_string(),
784 run_id: String::new(),
785 flow_config: Some(flow_config),
786 })
787 .await
788 },
789 )
790 }
791
792 pub fn trigger_continue_as_new(&self, flow_id: &str) -> SdkResult<()> {
794 self.call_empty(
795 "trigger_continue_as_new",
796 Some(flow_id),
797 FlowTargetRequirement::Active,
798 |mut service| async move {
799 service
800 .trigger_continue_as_new(TriggerContinueAsNewRequest {
801 flow_id: flow_id.to_string(),
802 run_id: String::new(),
803 })
804 .await
805 },
806 )
807 }
808
809 pub fn health_check(&self) -> SdkResult<()> {
813 self.call_empty(
814 "health_check",
815 None,
816 FlowTargetRequirement::None,
817 |mut service| async move { service.health_check(()).await },
818 )
819 }
820
821 fn do_invoke_rpc<Input: Value, Output: Value>(
822 &self,
823 flow_id: &str,
824 rpc_name: &str,
825 input: &Input,
826 ) -> SdkResult<Output> {
827 let rpc = self.registry.rpc(rpc_name)?;
828 let request = InvokeRpcRequest {
829 flow_id: flow_id.to_string(),
830 run_id: String::new(),
831 rpc_name: rpc_name.to_string(),
832 input: Some(value_mapper::encode(input)?),
833 timeout_seconds: optional_seconds(rpc.timeout)?,
834 lock_attribute_keys: rpc.locks.iter().map(|lock| lock.physical_name()).collect(),
835 request_id: Uuid::new_v4().to_string(),
836 };
837 let mut service = self.service.clone();
838 let output = self.runtime.block_on(async {
839 service
840 .invoke_rpc(request)
841 .await
842 .map(|response| response.into_inner().output)
843 .map_err(|status| {
844 SdkError::from_status(
845 status,
846 "invoke_rpc",
847 Some(flow_id),
848 FlowTargetRequirement::Active,
849 )
850 })
851 })?;
852 let output = output.ok_or_else(|| invalid("InvokeRPC omitted output"))?;
853 let output = self.runtime.block_on(self.hydrator.hydrate(output))?;
854 value_mapper::decode(&output)
855 }
856
857 fn get_attribute_value<T: Value>(&self, flow_id: &str, key: &str) -> SdkResult<Option<T>> {
858 let mut service = self.service.clone();
859 let response = self.runtime.block_on(async {
860 service
861 .get_attributes(GetAttributesRequest {
862 flow_id: flow_id.to_string(),
863 run_id: String::new(),
864 keys: vec![key.to_string()],
865 all_keys: false,
866 })
867 .await
868 .map(|response| response.into_inner())
869 .map_err(|status| {
870 SdkError::from_status(
871 status,
872 "get_attribute",
873 Some(flow_id),
874 FlowTargetRequirement::Existing,
875 )
876 })
877 })?;
878 let Some(entry) = response.attributes.into_iter().next() else {
879 return Ok(None);
880 };
881 let value = entry
882 .value
883 .ok_or_else(|| invalid("GetAttributes returned an empty Value"))?;
884 let value = self.runtime.block_on(self.hydrator.hydrate(value))?;
885 value_mapper::decode(&value).map(Some)
886 }
887
888 fn set_attribute_value<T: Value>(
889 &self,
890 flow_id: &str,
891 key: &str,
892 value: &T,
893 index_config: Option<dex_protocol::dex::IndexConfig>,
894 sync_config: Option<dex_protocol::dex::AttributeSyncConfig>,
895 ) -> SdkResult<()> {
896 let write = AttributeWrite {
897 key: key.to_string(),
898 value: Some(value_mapper::encode(value)?),
899 index_config,
900 sync_config,
901 };
902 self.call_empty(
903 "set_attribute",
904 Some(flow_id),
905 FlowTargetRequirement::Active,
906 |mut service| async move {
907 service
908 .set_attributes(SetAttributesRequest {
909 flow_id: flow_id.to_string(),
910 run_id: String::new(),
911 attributes: vec![write],
912 request_id: Uuid::new_v4().to_string(),
913 })
914 .await
915 },
916 )
917 }
918
919 fn publish_values<T: Value>(
920 &self,
921 flow_id: &str,
922 channel_name: &str,
923 values: impl IntoIterator<Item = T>,
924 ) -> SdkResult<()> {
925 let messages = values
926 .into_iter()
927 .map(|value| {
928 Ok(dex_protocol::dex::ChannelMessage {
929 channel_name: channel_name.to_string(),
930 value: Some(value_mapper::encode(&value)?),
931 })
932 })
933 .collect::<SdkResult<Vec<_>>>()?;
934 self.call_empty(
935 "publish",
936 Some(flow_id),
937 FlowTargetRequirement::Active,
938 |mut service| async move {
939 service
940 .publish_to_channel(PublishToChannelRequest {
941 flow_id: flow_id.to_string(),
942 run_id: String::new(),
943 messages,
944 })
945 .await
946 },
947 )
948 }
949
950 fn read_stream_result<T: Value>(
951 &self,
952 flow_id: &str,
953 stream: &Stream<T>,
954 resume_token: &str,
955 timeout: Option<Duration>,
956 ) -> SdkResult<StreamMessage<T>> {
957 require_name(flow_id, "Flow ID")?;
958 let flow_type = self.registry.flow_for_stream(stream)?.name.to_string();
959 let wait_time_seconds = optional_seconds(timeout)?;
960 let request = ReadStreamRequest {
961 flow_id: flow_id.to_string(),
962 flow_type,
963 stream_name: stream.name().to_string(),
964 resume_token: resume_token.to_string(),
965 wait_time_seconds,
966 };
967 let mut service = self.service.clone();
968 let response = self.runtime.block_on(async {
969 service
970 .read_stream(request)
971 .await
972 .map(|response| response.into_inner())
973 .map_err(|status| {
974 SdkError::from_status(
975 status,
976 "read_stream",
977 Some(flow_id),
978 FlowTargetRequirement::None,
979 )
980 })
981 })?;
982 let message = response
983 .message
984 .ok_or_else(|| invalid("ReadStream omitted its message"))?;
985 if message.resume_token.is_empty() {
986 return Err(invalid("ReadStream returned an empty resume token"));
987 }
988 let value = message
989 .value
990 .ok_or_else(|| invalid("ReadStream omitted its Value"))?;
991 let created_time = message
992 .created_time
993 .map(timestamp)
994 .ok_or_else(|| invalid("ReadStream omitted its creation time"))?;
995 Ok(StreamMessage {
996 value: value_mapper::decode(&value)?,
997 resume_token: message.resume_token,
998 created_time,
999 idempotency_key: message.idempotency_key,
1000 })
1001 }
1002
1003 fn wait_for_flow_result(
1004 &self,
1005 flow_id: &str,
1006 timeout: Option<Duration>,
1007 ) -> SdkResult<FlowResult> {
1008 let mut service = self.service.clone();
1009 let response = self.runtime.block_on(async {
1010 service
1011 .wait_for_flow(WaitForFlowRequest {
1012 flow_id: flow_id.to_string(),
1013 run_id: String::new(),
1014 needs_results: true,
1015 wait_time_seconds: optional_seconds(timeout)?,
1016 })
1017 .await
1018 .map(|response| response.into_inner())
1019 .map_err(|status| {
1020 SdkError::from_status(
1021 status,
1022 "wait_for_flow",
1023 Some(flow_id),
1024 FlowTargetRequirement::Existing,
1025 )
1026 })
1027 });
1028 let response = response?;
1029 let mut outputs = Vec::with_capacity(response.results.len());
1030 for result in &response.results {
1031 outputs.push(result.completed_step_output.clone().ok_or_else(|| {
1032 SdkError::ValueMapping {
1033 message: "Step completion output is required".to_string(),
1034 }
1035 })?);
1036 }
1037 let outputs = self.runtime.block_on(self.hydrator.hydrate_all(outputs))?;
1038 let completions = response
1039 .results
1040 .into_iter()
1041 .zip(outputs)
1042 .map(|(result, output)| {
1043 StepCompletion::new(
1044 result.completed_step_type,
1045 result.completed_step_execution_id,
1046 output,
1047 )
1048 })
1049 .collect();
1050 Ok(FlowResult::new(
1051 map_flow_status(response.flow_status)?,
1052 map_flow_error_type(response.error_type),
1053 (!response.error_message.is_empty()).then_some(response.error_message),
1054 completions,
1055 ))
1056 }
1057
1058 fn map_start_options(
1059 &self,
1060 flow: &crate::registry::RegisteredFlow,
1061 options: &StartFlowOptions,
1062 ) -> SdkResult<FlowStartOptions> {
1063 let attributes = options
1064 .attributes
1065 .iter()
1066 .map(|attribute| {
1067 let logical_name = attribute.key.split('/').next().unwrap_or(&attribute.key);
1068 if !flow.persistence.contains_key(logical_name) {
1069 return Err(invalid(format!(
1070 "initial Attribute is not registered: {}",
1071 attribute.key
1072 )));
1073 }
1074 Ok(AttributeWrite {
1075 key: attribute.key.clone(),
1076 value: Some(attribute.value.encode()?),
1077 index_config: attribute.index_config.clone(),
1078 sync_config: attribute.sync_config,
1079 })
1080 })
1081 .collect::<SdkResult<Vec<_>>>()?;
1082 let config = options.config_override.as_ref();
1083 let flow_config_override =
1084 if config.is_some() || self.options.worker_target_value().is_some() {
1085 Some(self.map_flow_config(config)?)
1086 } else {
1087 None
1088 };
1089 #[allow(clippy::needless_update)]
1090 let flow_start_options = FlowStartOptions {
1091 id_reuse_policy: match options.id_reuse_policy {
1092 IdReusePolicy::Default => ProtoIdReusePolicy::Unspecified,
1093 IdReusePolicy::AllowIfPreviousFailed => {
1094 ProtoIdReusePolicy::AllowIfPreviousExistsAbnormally
1095 }
1096 IdReusePolicy::AllowIfNotRunning => ProtoIdReusePolicy::AllowIfNoRunning,
1097 IdReusePolicy::Disallow => ProtoIdReusePolicy::DisallowReuse,
1098 IdReusePolicy::TerminateIfRunning => ProtoIdReusePolicy::AllowTerminateIfRunning,
1099 } as i32,
1100 flow_start_delay_seconds: optional_seconds(options.start_delay)?,
1101 retry_policy: options
1102 .retry_policy
1103 .clone()
1104 .map(map_flow_retry)
1105 .transpose()?,
1106 attributes,
1107 flow_config_override,
1108 flow_already_started_options: Some(FlowAlreadyStartedOptions {
1109 ignore_already_started_error: options.ignore_already_started,
1110 }),
1111 ..Default::default()
1112 };
1113 Ok(flow_start_options)
1114 }
1115
1116 fn map_flow_config(&self, config: Option<&FlowConfig>) -> SdkResult<ProtoFlowConfig> {
1117 map_flow_config(config, self.options.worker_target_value())
1118 }
1119
1120 fn call_empty<Response, Future, Call>(
1121 &self,
1122 operation: &'static str,
1123 flow_id: Option<&str>,
1124 requirement: FlowTargetRequirement,
1125 call: Call,
1126 ) -> SdkResult<()>
1127 where
1128 Call: FnOnce(FlowServiceClient<tonic::transport::Channel>) -> Future,
1129 Future: std::future::Future<Output = Result<tonic::Response<Response>, tonic::Status>>,
1130 {
1131 let service = self.service.clone();
1132 self.runtime
1133 .block_on(call(service))
1134 .map(|_| ())
1135 .map_err(|status| SdkError::from_status(status, operation, flow_id, requirement))
1136 }
1137}
1138
1139pub(crate) fn map_flow_config(
1140 config: Option<&FlowConfig>,
1141 default_target: Option<&WorkerTarget>,
1142) -> SdkResult<ProtoFlowConfig> {
1143 let target = config
1144 .and_then(|config| config.worker_target.as_ref())
1145 .or(default_target);
1146 Ok(ProtoFlowConfig {
1147 active_step_search_mode: config
1148 .and_then(|config| config.active_step_search_mode)
1149 .map(|mode| match mode {
1150 ActiveStepSearchMode::All => ProtoSearchMode::EnabledForAll as i32,
1151 ActiveStepSearchMode::WithWaitFor => {
1152 ProtoSearchMode::EnabledForStepsWithWaitFor as i32
1153 }
1154 ActiveStepSearchMode::Disabled => ProtoSearchMode::Disabled as i32,
1155 }),
1156 continue_as_new_threshold: config
1157 .and_then(|config| config.continue_as_new_threshold)
1158 .map(i32::try_from)
1159 .transpose()
1160 .map_err(|_| invalid("continue-as-new threshold exceeds int32"))?,
1161 continue_as_new_page_size_in_bytes: config
1162 .and_then(|config| config.continue_as_new_page_size_bytes)
1163 .map(i32::try_from)
1164 .transpose()
1165 .map_err(|_| invalid("continue-as-new page size exceeds int32"))?,
1166 step_durability: config
1167 .and_then(|config| config.step_durability)
1168 .map(|durability| {
1169 (match durability {
1170 StepDurability::Default => ProtoStepDurability::Unspecified,
1171 StepDurability::Sync => ProtoStepDurability::Sync,
1172 StepDurability::Async => ProtoStepDurability::Async,
1173 }) as i32
1174 }),
1175 worker_target: target.map(map_worker_target),
1176 attribute_store_names: config.and_then(|config| {
1177 config
1178 .attribute_store_names
1179 .as_ref()
1180 .map(|names| AttributeStoreNames {
1181 names: names.clone(),
1182 })
1183 }),
1184 })
1185}
1186
1187pub(crate) fn map_flow_retry(retry: RetryPolicy) -> SdkResult<FlowRetryPolicy> {
1188 Ok(FlowRetryPolicy {
1189 initial_interval_seconds: optional_seconds(retry.initial_interval)?,
1190 backoff_coefficient: retry.backoff_coefficient.unwrap_or_default() as f32,
1191 maximum_interval_seconds: optional_seconds(retry.maximum_interval)?,
1192 maximum_attempts: i32::try_from(retry.maximum_attempts.unwrap_or_default())
1193 .map_err(|_| invalid("maximum attempts exceed int32"))?,
1194 })
1195}
1196
1197fn map_worker_target(target: &WorkerTarget) -> ProtoWorkerTarget {
1198 ProtoWorkerTarget {
1199 address: target.address().to_string(),
1200 is_headless_address: target.is_headless(),
1201 }
1202}
1203
1204pub(crate) fn map_flow_status(status: i32) -> SdkResult<FlowStatus> {
1205 match ProtoFlowStatus::try_from(status).ok() {
1206 Some(ProtoFlowStatus::Running) => Ok(FlowStatus::Running),
1207 Some(ProtoFlowStatus::Completed) => Ok(FlowStatus::Completed),
1208 Some(ProtoFlowStatus::Failed) => Ok(FlowStatus::Failed),
1209 Some(ProtoFlowStatus::ServerSideTimeoutInternalOnly) => {
1210 Ok(FlowStatus::ServerSideTimeoutInternalOnly)
1211 }
1212 Some(ProtoFlowStatus::Terminated) => Ok(FlowStatus::Terminated),
1213 Some(ProtoFlowStatus::Canceled) => Ok(FlowStatus::Canceled),
1214 Some(ProtoFlowStatus::ContinuedAsNew) => Ok(FlowStatus::ContinuedAsNew),
1215 _ => Err(invalid(format!("unknown Flow status {status}"))),
1216 }
1217}
1218
1219pub(crate) fn map_flow_error_type(error_type: i32) -> Option<FlowErrorType> {
1220 use dex_protocol::dex::FlowErrorType as ProtoFlowErrorType;
1221
1222 match ProtoFlowErrorType::try_from(error_type).ok() {
1223 Some(ProtoFlowErrorType::StepDecisionFailingFlow) => {
1224 Some(FlowErrorType::StepDecisionFailed)
1225 }
1226 Some(ProtoFlowErrorType::ClientApiFailingFlow) => Some(FlowErrorType::ClientApiFailed),
1227 Some(ProtoFlowErrorType::WorkerApiFail) => Some(FlowErrorType::WorkerApiFailed),
1228 Some(ProtoFlowErrorType::InvalidUserFlowCode) => Some(FlowErrorType::InvalidUserFlowCode),
1229 Some(ProtoFlowErrorType::Internal) => Some(FlowErrorType::Internal),
1230 Some(ProtoFlowErrorType::FlowTimeout) => Some(FlowErrorType::FlowTimeout),
1231 _ => None,
1232 }
1233}
1234
1235pub(crate) fn map_flow_timeout_policy(
1236 flow: &crate::registry::RegisteredFlow,
1237 timeout_seconds: i32,
1238 policy: FlowTimeoutPolicy,
1239) -> SdkResult<i32> {
1240 if timeout_seconds == 0 {
1241 if policy != FlowTimeoutPolicy::Default {
1242 return Err(invalid("Flow timeout policy requires a positive timeout"));
1243 }
1244 return Ok(ProtoFlowTimeoutPolicy::Unspecified as i32);
1245 }
1246 let resolved = match policy {
1247 FlowTimeoutPolicy::Default if flow.handler.has_timeout_handler() => {
1248 FlowTimeoutPolicy::Handler
1249 }
1250 FlowTimeoutPolicy::Default => FlowTimeoutPolicy::Fail,
1251 configured => configured,
1252 };
1253 if resolved == FlowTimeoutPolicy::Handler && !flow.handler.has_timeout_handler() {
1254 return Err(invalid(format!(
1255 "Flow {} has no timeout handler",
1256 flow.name
1257 )));
1258 }
1259 Ok(match resolved {
1260 FlowTimeoutPolicy::Default => ProtoFlowTimeoutPolicy::Unspecified,
1261 FlowTimeoutPolicy::Fail => ProtoFlowTimeoutPolicy::Fail,
1262 FlowTimeoutPolicy::Cancel => ProtoFlowTimeoutPolicy::Cancel,
1263 FlowTimeoutPolicy::Handler => ProtoFlowTimeoutPolicy::Handler,
1264 } as i32)
1265}
1266
1267fn timestamp(timestamp: prost_types::Timestamp) -> SystemTime {
1268 if timestamp.seconds >= 0 {
1269 SystemTime::UNIX_EPOCH
1270 + Duration::from_secs(timestamp.seconds as u64)
1271 + Duration::from_nanos(timestamp.nanos.max(0) as u64)
1272 } else {
1273 SystemTime::UNIX_EPOCH - Duration::from_secs(timestamp.seconds.unsigned_abs())
1274 }
1275}
1276
1277fn rfc3339(time: SystemTime) -> SdkResult<String> {
1278 let duration = time
1279 .duration_since(SystemTime::UNIX_EPOCH)
1280 .map_err(service_error)?;
1281 let days = (duration.as_secs() / 86_400) as i64;
1282 let seconds = duration.as_secs() % 86_400;
1283 let (year, month, day) = civil_from_days(days);
1284 let hour = seconds / 3_600;
1285 let minute = seconds % 3_600 / 60;
1286 let second = seconds % 60;
1287 Ok(format!(
1288 "{year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}Z"
1289 ))
1290}
1291
1292fn civil_from_days(days: i64) -> (i64, i64, i64) {
1293 let days = days + 719_468;
1294 let era = if days >= 0 { days } else { days - 146_096 } / 146_097;
1295 let day_of_era = days - era * 146_097;
1296 let year_of_era =
1297 (day_of_era - day_of_era / 1_460 + day_of_era / 36_524 - day_of_era / 146_096) / 365;
1298 let mut year = year_of_era + era * 400;
1299 let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
1300 let month_prime = (5 * day_of_year + 2) / 153;
1301 let day = day_of_year - (153 * month_prime + 2) / 5 + 1;
1302 let month = month_prime + if month_prime < 10 { 3 } else { -9 };
1303 year += i64::from(month <= 2);
1304 (year, month, day)
1305}
1306
1307fn optional_seconds(duration: Option<Duration>) -> SdkResult<i32> {
1308 duration
1309 .map(seconds32)
1310 .transpose()
1311 .map(Option::unwrap_or_default)
1312}
1313
1314fn seconds32(duration: Duration) -> SdkResult<i32> {
1315 if duration.subsec_nanos() != 0 {
1316 return Err(invalid("Duration must use whole seconds"));
1317 }
1318 i32::try_from(duration.as_secs()).map_err(|_| invalid("Duration exceeds int32"))
1319}
1320
1321fn endpoint_address(address: &str) -> String {
1322 if address.contains("://") {
1323 address.to_string()
1324 } else {
1325 format!("http://{address}")
1326 }
1327}
1328
1329fn require_name(value: &str, kind: &str) -> SdkResult<()> {
1330 if value.is_empty() {
1331 Err(invalid(format!("{kind} is required")))
1332 } else {
1333 Ok(())
1334 }
1335}
1336
1337fn sdk_handler_error(error: impl std::fmt::Display) -> SdkError {
1338 invalid(error.to_string())
1339}
1340
1341fn invalid(message: impl Into<String>) -> SdkError {
1342 SdkError::InvalidArgument {
1343 message: message.into(),
1344 }
1345}
1346
1347fn service_error(error: impl std::fmt::Display) -> SdkError {
1348 SdkError::Service {
1349 service: ServiceError::local("client", error.to_string()),
1350 }
1351}
1352
1353#[cfg(test)]
1354mod tests {
1355 use super::map_flow_config;
1356 use crate::FlowConfig;
1357
1358 #[test]
1359 fn attribute_store_names_preserve_protocol_presence() {
1360 let absent = map_flow_config(Some(&FlowConfig::new()), None).expect("map absent config");
1361 let named = map_flow_config(
1362 Some(
1363 &FlowConfig::new()
1364 .attribute_store_names(vec!["profiles".to_owned(), "audit".to_owned()]),
1365 ),
1366 None,
1367 )
1368 .expect("map named config");
1369 let disabled =
1370 map_flow_config(Some(&FlowConfig::new().attribute_store_names(vec![])), None)
1371 .expect("map disabled config");
1372
1373 assert_eq!(absent.attribute_store_names, None);
1374 assert_eq!(
1375 named.attribute_store_names.expect("named stores").names,
1376 vec!["profiles", "audit"]
1377 );
1378 assert_eq!(
1379 disabled
1380 .attribute_store_names
1381 .expect("disabled stores")
1382 .names,
1383 Vec::<String>::new()
1384 );
1385 }
1386}