@temporalio/core-bridge 1.15.0 → 1.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. package/Cargo.lock +172 -70
  2. package/lib/native.d.ts +1 -1
  3. package/package.json +2 -2
  4. package/releases/aarch64-apple-darwin/index.node +0 -0
  5. package/releases/aarch64-unknown-linux-gnu/index.node +0 -0
  6. package/releases/x86_64-apple-darwin/index.node +0 -0
  7. package/releases/x86_64-pc-windows-msvc/index.node +0 -0
  8. package/releases/x86_64-unknown-linux-gnu/index.node +0 -0
  9. package/sdk-core/.github/workflows/per-pr.yml +6 -6
  10. package/sdk-core/AGENTS.md +41 -30
  11. package/sdk-core/Cargo.toml +3 -0
  12. package/sdk-core/README.md +15 -9
  13. package/sdk-core/crates/client/Cargo.toml +4 -0
  14. package/sdk-core/crates/client/README.md +139 -0
  15. package/sdk-core/crates/client/src/async_activity_handle.rs +297 -0
  16. package/sdk-core/crates/client/src/callback_based.rs +7 -0
  17. package/sdk-core/crates/client/src/errors.rs +294 -0
  18. package/sdk-core/crates/client/src/{raw.rs → grpc.rs} +280 -159
  19. package/sdk-core/crates/client/src/lib.rs +920 -1326
  20. package/sdk-core/crates/client/src/metrics.rs +24 -33
  21. package/sdk-core/crates/client/src/options_structs.rs +457 -0
  22. package/sdk-core/crates/client/src/replaceable.rs +5 -4
  23. package/sdk-core/crates/client/src/request_extensions.rs +8 -9
  24. package/sdk-core/crates/client/src/retry.rs +99 -54
  25. package/sdk-core/crates/client/src/{worker/mod.rs → worker.rs} +1 -1
  26. package/sdk-core/crates/client/src/workflow_handle.rs +826 -0
  27. package/sdk-core/crates/common/Cargo.toml +61 -2
  28. package/sdk-core/crates/common/build.rs +742 -12
  29. package/sdk-core/crates/common/protos/api_upstream/.github/workflows/ci.yml +2 -0
  30. package/sdk-core/crates/common/protos/api_upstream/Makefile +2 -1
  31. package/sdk-core/crates/common/protos/api_upstream/buf.yaml +0 -3
  32. package/sdk-core/crates/common/protos/api_upstream/cmd/check-path-conflicts/main.go +137 -0
  33. package/sdk-core/crates/common/protos/api_upstream/openapi/openapiv2.json +1166 -770
  34. package/sdk-core/crates/common/protos/api_upstream/openapi/openapiv3.yaml +1243 -750
  35. package/sdk-core/crates/common/protos/api_upstream/temporal/api/deployment/v1/message.proto +2 -2
  36. package/sdk-core/crates/common/protos/api_upstream/temporal/api/enums/v1/workflow.proto +4 -3
  37. package/sdk-core/crates/common/protos/api_upstream/temporal/api/failure/v1/message.proto +1 -0
  38. package/sdk-core/crates/common/protos/api_upstream/temporal/api/history/v1/message.proto +4 -0
  39. package/sdk-core/crates/common/protos/api_upstream/temporal/api/namespace/v1/message.proto +6 -0
  40. package/sdk-core/crates/common/protos/api_upstream/temporal/api/nexus/v1/message.proto +16 -1
  41. package/sdk-core/crates/common/protos/api_upstream/temporal/api/workflowservice/v1/request_response.proto +64 -6
  42. package/sdk-core/crates/common/protos/api_upstream/temporal/api/workflowservice/v1/service.proto +88 -33
  43. package/sdk-core/crates/common/protos/local/temporal/sdk/core/nexus/nexus.proto +4 -2
  44. package/sdk-core/crates/common/protos/local/temporal/sdk/core/workflow_activation/workflow_activation.proto +4 -0
  45. package/sdk-core/crates/common/protos/local/temporal/sdk/core/workflow_commands/workflow_commands.proto +5 -5
  46. package/sdk-core/crates/common/src/activity_definition.rs +20 -0
  47. package/sdk-core/crates/common/src/data_converters.rs +770 -0
  48. package/sdk-core/crates/common/src/envconfig.rs +5 -0
  49. package/sdk-core/crates/common/src/lib.rs +15 -211
  50. package/sdk-core/crates/common/src/payload_visitor.rs +648 -0
  51. package/sdk-core/crates/common/src/priority.rs +110 -0
  52. package/sdk-core/crates/common/src/protos/canned_histories.rs +3 -0
  53. package/sdk-core/crates/common/src/protos/history_builder.rs +45 -0
  54. package/sdk-core/crates/common/src/protos/history_info.rs +2 -0
  55. package/sdk-core/crates/common/src/protos/mod.rs +122 -27
  56. package/sdk-core/crates/common/src/protos/task_token.rs +3 -3
  57. package/sdk-core/crates/common/src/protos/utilities.rs +11 -0
  58. package/sdk-core/crates/{sdk-core → common}/src/telemetry/log_export.rs +5 -7
  59. package/sdk-core/crates/common/src/telemetry/metrics/core.rs +125 -0
  60. package/sdk-core/crates/common/src/telemetry/metrics.rs +268 -223
  61. package/sdk-core/crates/{sdk-core → common}/src/telemetry/otel.rs +8 -13
  62. package/sdk-core/crates/{sdk-core → common}/src/telemetry/prometheus_meter.rs +49 -50
  63. package/sdk-core/crates/{sdk-core → common}/src/telemetry/prometheus_server.rs +2 -3
  64. package/sdk-core/crates/common/src/telemetry.rs +264 -4
  65. package/sdk-core/crates/common/src/worker.rs +68 -603
  66. package/sdk-core/crates/common/src/workflow_definition.rs +60 -0
  67. package/sdk-core/crates/macros/Cargo.toml +5 -1
  68. package/sdk-core/crates/macros/src/activities_definitions.rs +585 -0
  69. package/sdk-core/crates/macros/src/fsm_impl.rs +507 -0
  70. package/sdk-core/crates/macros/src/lib.rs +138 -512
  71. package/sdk-core/crates/macros/src/macro_utils.rs +106 -0
  72. package/sdk-core/crates/macros/src/workflow_definitions.rs +1224 -0
  73. package/sdk-core/crates/sdk/Cargo.toml +19 -6
  74. package/sdk-core/crates/sdk/README.md +415 -0
  75. package/sdk-core/crates/sdk/src/activities.rs +417 -0
  76. package/sdk-core/crates/sdk/src/interceptors.rs +1 -1
  77. package/sdk-core/crates/sdk/src/lib.rs +757 -442
  78. package/sdk-core/crates/sdk/src/workflow_context/options.rs +45 -35
  79. package/sdk-core/crates/sdk/src/workflow_context.rs +1033 -289
  80. package/sdk-core/crates/sdk/src/workflow_future.rs +277 -213
  81. package/sdk-core/crates/sdk/src/workflows.rs +711 -0
  82. package/sdk-core/crates/sdk-core/Cargo.toml +57 -64
  83. package/sdk-core/crates/sdk-core/benches/workflow_replay_bench.rs +41 -35
  84. package/sdk-core/crates/sdk-core/machine_coverage/ActivityMachine_Coverage.puml +1 -1
  85. package/sdk-core/crates/sdk-core/src/abstractions.rs +6 -10
  86. package/sdk-core/crates/sdk-core/src/core_tests/activity_tasks.rs +6 -5
  87. package/sdk-core/crates/sdk-core/src/core_tests/mod.rs +13 -15
  88. package/sdk-core/crates/sdk-core/src/core_tests/queries.rs +21 -25
  89. package/sdk-core/crates/sdk-core/src/core_tests/replay_flag.rs +7 -10
  90. package/sdk-core/crates/sdk-core/src/core_tests/updates.rs +14 -17
  91. package/sdk-core/crates/sdk-core/src/core_tests/workers.rs +493 -26
  92. package/sdk-core/crates/sdk-core/src/core_tests/workflow_tasks.rs +4 -8
  93. package/sdk-core/crates/sdk-core/src/ephemeral_server/mod.rs +7 -7
  94. package/sdk-core/crates/sdk-core/src/histfetch.rs +20 -10
  95. package/sdk-core/crates/sdk-core/src/lib.rs +41 -111
  96. package/sdk-core/crates/sdk-core/src/pollers/mod.rs +4 -9
  97. package/sdk-core/crates/sdk-core/src/pollers/poll_buffer.rs +118 -19
  98. package/sdk-core/crates/sdk-core/src/protosext/mod.rs +2 -2
  99. package/sdk-core/crates/sdk-core/src/replay/mod.rs +14 -5
  100. package/sdk-core/crates/sdk-core/src/telemetry/metrics.rs +179 -196
  101. package/sdk-core/crates/sdk-core/src/telemetry/mod.rs +3 -280
  102. package/sdk-core/crates/sdk-core/src/test_help/integ_helpers.rs +6 -9
  103. package/sdk-core/crates/sdk-core/src/test_help/unit_helpers.rs +3 -6
  104. package/sdk-core/crates/sdk-core/src/worker/activities/local_activities.rs +11 -14
  105. package/sdk-core/crates/sdk-core/src/worker/activities.rs +16 -19
  106. package/sdk-core/crates/sdk-core/src/worker/client/mocks.rs +9 -5
  107. package/sdk-core/crates/sdk-core/src/worker/client.rs +103 -81
  108. package/sdk-core/crates/sdk-core/src/worker/heartbeat.rs +7 -11
  109. package/sdk-core/crates/sdk-core/src/worker/mod.rs +1124 -229
  110. package/sdk-core/crates/sdk-core/src/worker/nexus.rs +145 -23
  111. package/sdk-core/crates/sdk-core/src/worker/slot_provider.rs +2 -2
  112. package/sdk-core/crates/sdk-core/src/worker/tuner/fixed_size.rs +2 -2
  113. package/sdk-core/crates/sdk-core/src/worker/tuner/resource_based.rs +13 -13
  114. package/sdk-core/crates/sdk-core/src/worker/tuner.rs +28 -8
  115. package/sdk-core/crates/sdk-core/src/worker/workflow/driven_workflow.rs +9 -3
  116. package/sdk-core/crates/sdk-core/src/worker/workflow/machines/upsert_search_attributes_state_machine.rs +21 -22
  117. package/sdk-core/crates/sdk-core/src/worker/workflow/machines/workflow_machines.rs +19 -4
  118. package/sdk-core/crates/sdk-core/src/worker/workflow/managed_run.rs +14 -18
  119. package/sdk-core/crates/sdk-core/src/worker/workflow/mod.rs +4 -6
  120. package/sdk-core/crates/sdk-core/src/worker/workflow/run_cache.rs +4 -7
  121. package/sdk-core/crates/sdk-core/src/worker/workflow/wft_extraction.rs +2 -4
  122. package/sdk-core/crates/sdk-core/src/worker/workflow/wft_poller.rs +8 -9
  123. package/sdk-core/crates/sdk-core/src/worker/workflow/workflow_stream.rs +1 -3
  124. package/sdk-core/crates/sdk-core/tests/activities_procmacro.rs +6 -0
  125. package/sdk-core/crates/sdk-core/tests/activities_trybuild/basic_pass.rs +54 -0
  126. package/sdk-core/crates/sdk-core/tests/activities_trybuild/invalid_self_type_fail.rs +18 -0
  127. package/sdk-core/crates/sdk-core/tests/activities_trybuild/invalid_self_type_fail.stderr +5 -0
  128. package/sdk-core/crates/sdk-core/tests/activities_trybuild/missing_context_fail.rs +14 -0
  129. package/sdk-core/crates/sdk-core/tests/activities_trybuild/missing_context_fail.stderr +5 -0
  130. package/sdk-core/crates/sdk-core/tests/activities_trybuild/multi_arg_pass.rs +48 -0
  131. package/sdk-core/crates/sdk-core/tests/activities_trybuild/no_input_pass.rs +14 -0
  132. package/sdk-core/crates/sdk-core/tests/activities_trybuild/no_return_type_pass.rs +19 -0
  133. package/sdk-core/crates/sdk-core/tests/cloud_tests.rs +14 -5
  134. package/sdk-core/crates/sdk-core/tests/common/activity_functions.rs +55 -0
  135. package/sdk-core/crates/sdk-core/tests/common/mod.rs +241 -196
  136. package/sdk-core/crates/sdk-core/tests/common/workflows.rs +41 -28
  137. package/sdk-core/crates/sdk-core/tests/global_metric_tests.rs +3 -5
  138. package/sdk-core/crates/sdk-core/tests/heavy_tests/fuzzy_workflow.rs +73 -64
  139. package/sdk-core/crates/sdk-core/tests/heavy_tests.rs +298 -252
  140. package/sdk-core/crates/sdk-core/tests/integ_tests/async_activity_client_tests.rs +230 -0
  141. package/sdk-core/crates/sdk-core/tests/integ_tests/client_tests.rs +94 -57
  142. package/sdk-core/crates/sdk-core/tests/integ_tests/data_converter_tests.rs +381 -0
  143. package/sdk-core/crates/sdk-core/tests/integ_tests/ephemeral_server_tests.rs +16 -12
  144. package/sdk-core/crates/sdk-core/tests/integ_tests/heartbeat_tests.rs +48 -40
  145. package/sdk-core/crates/sdk-core/tests/integ_tests/metrics_tests.rs +327 -255
  146. package/sdk-core/crates/sdk-core/tests/integ_tests/pagination_tests.rs +50 -45
  147. package/sdk-core/crates/sdk-core/tests/integ_tests/polling_tests.rs +147 -126
  148. package/sdk-core/crates/sdk-core/tests/integ_tests/queries_tests.rs +103 -89
  149. package/sdk-core/crates/sdk-core/tests/integ_tests/update_tests.rs +609 -453
  150. package/sdk-core/crates/sdk-core/tests/integ_tests/visibility_tests.rs +80 -62
  151. package/sdk-core/crates/sdk-core/tests/integ_tests/worker_heartbeat_tests.rs +360 -231
  152. package/sdk-core/crates/sdk-core/tests/integ_tests/worker_tests.rs +248 -185
  153. package/sdk-core/crates/sdk-core/tests/integ_tests/worker_versioning_tests.rs +52 -43
  154. package/sdk-core/crates/sdk-core/tests/integ_tests/workflow_client_tests.rs +180 -0
  155. package/sdk-core/crates/sdk-core/tests/integ_tests/workflow_tests/activities.rs +428 -315
  156. package/sdk-core/crates/sdk-core/tests/integ_tests/workflow_tests/cancel_external.rs +82 -56
  157. package/sdk-core/crates/sdk-core/tests/integ_tests/workflow_tests/cancel_wf.rs +56 -28
  158. package/sdk-core/crates/sdk-core/tests/integ_tests/workflow_tests/child_workflows.rs +364 -243
  159. package/sdk-core/crates/sdk-core/tests/integ_tests/workflow_tests/client_interactions.rs +552 -0
  160. package/sdk-core/crates/sdk-core/tests/integ_tests/workflow_tests/continue_as_new.rs +101 -42
  161. package/sdk-core/crates/sdk-core/tests/integ_tests/workflow_tests/determinism.rs +243 -147
  162. package/sdk-core/crates/sdk-core/tests/integ_tests/workflow_tests/eager.rs +98 -28
  163. package/sdk-core/crates/sdk-core/tests/integ_tests/workflow_tests/local_activities.rs +1475 -1036
  164. package/sdk-core/crates/sdk-core/tests/integ_tests/workflow_tests/modify_wf_properties.rs +73 -41
  165. package/sdk-core/crates/sdk-core/tests/integ_tests/workflow_tests/nexus.rs +397 -238
  166. package/sdk-core/crates/sdk-core/tests/integ_tests/workflow_tests/patches.rs +414 -189
  167. package/sdk-core/crates/sdk-core/tests/integ_tests/workflow_tests/queries.rs +415 -0
  168. package/sdk-core/crates/sdk-core/tests/integ_tests/workflow_tests/replay.rs +96 -36
  169. package/sdk-core/crates/sdk-core/tests/integ_tests/workflow_tests/resets.rs +154 -137
  170. package/sdk-core/crates/sdk-core/tests/integ_tests/workflow_tests/signals.rs +183 -105
  171. package/sdk-core/crates/sdk-core/tests/integ_tests/workflow_tests/stickyness.rs +85 -38
  172. package/sdk-core/crates/sdk-core/tests/integ_tests/workflow_tests/timers.rs +142 -40
  173. package/sdk-core/crates/sdk-core/tests/integ_tests/workflow_tests/upsert_search_attrs.rs +73 -54
  174. package/sdk-core/crates/sdk-core/tests/integ_tests/workflow_tests.rs +363 -226
  175. package/sdk-core/crates/sdk-core/tests/main.rs +17 -15
  176. package/sdk-core/crates/sdk-core/tests/manual_tests.rs +207 -152
  177. package/sdk-core/crates/sdk-core/tests/shared_tests/mod.rs +65 -34
  178. package/sdk-core/crates/sdk-core/tests/shared_tests/priority.rs +107 -84
  179. package/sdk-core/crates/sdk-core/tests/workflows_procmacro.rs +6 -0
  180. package/sdk-core/crates/sdk-core/tests/workflows_trybuild/async_query_fail.rs +26 -0
  181. package/sdk-core/crates/sdk-core/tests/workflows_trybuild/async_query_fail.stderr +5 -0
  182. package/sdk-core/crates/sdk-core/tests/workflows_trybuild/basic_pass.rs +49 -0
  183. package/sdk-core/crates/sdk-core/tests/workflows_trybuild/minimal_pass.rs +21 -0
  184. package/sdk-core/crates/sdk-core/tests/workflows_trybuild/mut_query_fail.rs +26 -0
  185. package/sdk-core/crates/sdk-core/tests/workflows_trybuild/mut_query_fail.stderr +5 -0
  186. package/sdk-core/crates/sdk-core/tests/workflows_trybuild/sync_run_fail.rs +21 -0
  187. package/sdk-core/crates/sdk-core/tests/workflows_trybuild/sync_run_fail.stderr +5 -0
  188. package/sdk-core/crates/sdk-core-c-bridge/Cargo.toml +7 -1
  189. package/sdk-core/crates/sdk-core-c-bridge/include/temporal-sdk-core-c-bridge.h +14 -14
  190. package/sdk-core/crates/sdk-core-c-bridge/src/client.rs +83 -74
  191. package/sdk-core/crates/sdk-core-c-bridge/src/metric.rs +9 -14
  192. package/sdk-core/crates/sdk-core-c-bridge/src/runtime.rs +1 -2
  193. package/sdk-core/crates/sdk-core-c-bridge/src/tests/context.rs +13 -13
  194. package/sdk-core/crates/sdk-core-c-bridge/src/tests/mod.rs +6 -6
  195. package/sdk-core/crates/sdk-core-c-bridge/src/tests/utils.rs +3 -4
  196. package/sdk-core/crates/sdk-core-c-bridge/src/worker.rs +62 -75
  197. package/sdk-core/rustfmt.toml +2 -1
  198. package/src/client.rs +205 -318
  199. package/src/metrics.rs +22 -30
  200. package/src/runtime.rs +4 -5
  201. package/src/worker.rs +16 -19
  202. package/ts/native.ts +1 -1
  203. package/sdk-core/crates/client/src/workflow_handle/mod.rs +0 -212
  204. package/sdk-core/crates/common/src/errors.rs +0 -85
  205. package/sdk-core/crates/common/tests/worker_task_types_test.rs +0 -129
  206. package/sdk-core/crates/sdk/src/activity_context.rs +0 -238
  207. package/sdk-core/crates/sdk/src/app_data.rs +0 -37
  208. package/sdk-core/crates/sdk-core/tests/integ_tests/activity_functions.rs +0 -5
  209. package/sdk-core/crates/sdk-core/tests/integ_tests/workflow_tests/appdata_propagation.rs +0 -61
@@ -7,12 +7,15 @@
7
7
  #[macro_use]
8
8
  extern crate tracing;
9
9
 
10
+ mod async_activity_handle;
10
11
  pub mod callback_based;
12
+ pub mod errors;
13
+ pub mod grpc;
11
14
  mod metrics;
15
+ mod options_structs;
12
16
  /// Visible only for tests
13
17
  #[doc(hidden)]
14
18
  pub mod proxy;
15
- mod raw;
16
19
  mod replaceable;
17
20
  pub mod request_extensions;
18
21
  mod retry;
@@ -21,60 +24,68 @@ mod workflow_handle;
21
24
 
22
25
  pub use crate::{
23
26
  proxy::HttpConnectProxyOptions,
24
- retry::{CallType, RETRYABLE_ERROR_CODES, RetryClient},
27
+ retry::{CallType, RETRYABLE_ERROR_CODES},
25
28
  };
29
+ pub use async_activity_handle::{
30
+ ActivityHeartbeatResponse, ActivityIdentifier, AsyncActivityHandle,
31
+ };
32
+
26
33
  pub use metrics::{LONG_REQUEST_LATENCY_HISTOGRAM_NAME, REQUEST_LATENCY_HISTOGRAM_NAME};
27
- pub use raw::{CloudService, HealthService, OperatorService, TestService, WorkflowService};
34
+ pub use options_structs::*;
28
35
  pub use replaceable::SharedReplaceableClient;
36
+ pub use retry::RetryOptions;
29
37
  pub use tonic;
30
38
  pub use workflow_handle::{
31
- GetWorkflowResultOptions, WorkflowExecutionInfo, WorkflowExecutionResult, WorkflowHandle,
39
+ UntypedQuery, UntypedSignal, UntypedUpdate, UntypedWorkflow, UntypedWorkflowHandle,
40
+ WorkflowExecutionDescription, WorkflowExecutionInfo, WorkflowExecutionResult, WorkflowHandle,
41
+ WorkflowHistory, WorkflowUpdateHandle,
32
42
  };
33
43
 
34
44
  use crate::{
45
+ grpc::{
46
+ AttachMetricLabels, CloudService, HealthService, OperatorService, TestService,
47
+ WorkflowService,
48
+ },
35
49
  metrics::{ChannelOrGrpcOverride, GrpcMetricSvc, MetricsContext},
36
- raw::AttachMetricLabels,
37
50
  request_extensions::RequestExt,
38
- sealed::WfHandleClient,
39
51
  worker::ClientWorkerSet,
40
- workflow_handle::UntypedWorkflowHandle,
41
52
  };
42
- use backoff::{ExponentialBackoff, SystemClock, exponential};
43
- use http::{Uri, uri::InvalidUri};
53
+ use errors::*;
54
+ use futures_util::{stream, stream::Stream};
55
+ use http::Uri;
44
56
  use parking_lot::RwLock;
45
57
  use std::{
46
- collections::HashMap,
47
- fmt::{Debug, Formatter},
48
- ops::{Deref, DerefMut},
58
+ collections::{HashMap, VecDeque},
59
+ fmt::Debug,
60
+ pin::Pin,
49
61
  str::FromStr,
50
62
  sync::{Arc, OnceLock},
51
- time::{Duration, Instant},
63
+ task::{Context, Poll},
64
+ time::{Duration, SystemTime},
52
65
  };
53
66
  use temporalio_common::{
67
+ WorkflowDefinition,
68
+ data_converters::{DataConverter, SerializationContextData},
54
69
  protos::{
55
- TaskToken,
56
70
  coresdk::IntoPayloadsExt,
57
71
  grpc::health::v1::health_client::HealthClient,
72
+ proto_ts_to_system_time,
58
73
  temporal::api::{
59
74
  cloud::cloudservice::v1::cloud_service_client::CloudServiceClient,
60
- common::{
61
- self,
62
- v1::{Header, Payload, Payloads, RetryPolicy, WorkflowExecution, WorkflowType},
63
- },
64
- enums::v1::{
65
- ArchivalState, TaskQueueKind, WorkflowIdConflictPolicy, WorkflowIdReusePolicy,
66
- },
67
- filter::v1::StartTimeFilter,
75
+ common::v1::{Memo, Payload, SearchAttributes, WorkflowType},
76
+ enums::v1::{TaskQueueKind, WorkflowExecutionStatus},
77
+ errordetails::v1::WorkflowExecutionAlreadyStartedFailure,
68
78
  operatorservice::v1::operator_service_client::OperatorServiceClient,
69
- query::v1::WorkflowQuery,
70
- replication::v1::ClusterReplicationConfig,
71
79
  taskqueue::v1::TaskQueue,
72
80
  testservice::v1::test_service_client::TestServiceClient,
73
- update,
74
- workflowservice::v1::{workflow_service_client::WorkflowServiceClient, *},
81
+ workflow::v1 as workflow,
82
+ workflowservice::v1::{
83
+ count_workflow_executions_response, workflow_service_client::WorkflowServiceClient,
84
+ *,
85
+ },
75
86
  },
87
+ utilities::decode_status_detail,
76
88
  },
77
- telemetry::metrics::TemporalMeter,
78
89
  };
79
90
  use tonic::{
80
91
  Code, IntoRequest,
@@ -89,308 +100,150 @@ use tonic::{
89
100
  transport::{Certificate, Channel, Endpoint, Identity},
90
101
  };
91
102
  use tower::ServiceBuilder;
92
- use url::Url;
93
103
  use uuid::Uuid;
94
104
 
95
105
  static CLIENT_NAME_HEADER_KEY: &str = "client-name";
96
106
  static CLIENT_VERSION_HEADER_KEY: &str = "client-version";
97
107
  static TEMPORAL_NAMESPACE_HEADER_KEY: &str = "temporal-namespace";
98
108
 
109
+ #[doc(hidden)]
99
110
  /// Key used to communicate when a GRPC message is too large
100
111
  pub static MESSAGE_TOO_LARGE_KEY: &str = "message-too-large";
112
+ #[doc(hidden)]
101
113
  /// Key used to indicate a error was returned by the retryer because of the short-circuit predicate
102
114
  pub static ERROR_RETURNED_DUE_TO_SHORT_CIRCUIT: &str = "short-circuit";
103
115
 
104
116
  /// The server times out polls after 60 seconds. Set our timeout to be slightly beyond that.
105
117
  const LONG_POLL_TIMEOUT: Duration = Duration::from_secs(70);
106
118
  const OTHER_CALL_TIMEOUT: Duration = Duration::from_secs(30);
119
+ const VERSION: &str = env!("CARGO_PKG_VERSION");
107
120
 
108
- type Result<T, E = tonic::Status> = std::result::Result<T, E>;
109
-
110
- /// Options for the connection to the temporal server. Construct with [ClientOptions::builder]
111
- #[derive(Clone, Debug, bon::Builder)]
112
- #[non_exhaustive]
113
- #[builder(on(String, into), state_mod(vis = "pub"))]
114
- pub struct ClientOptions {
115
- /// The URL of the Temporal server to connect to
116
- #[builder(into)]
117
- pub target_url: Url,
118
-
119
- /// The name of the SDK being implemented on top of core. Is set as `client-name` header in
120
- /// all RPC calls
121
- pub client_name: String,
122
-
123
- /// The version of the SDK being implemented on top of core. Is set as `client-version` header
124
- /// in all RPC calls. The server decides if the client is supported based on this.
125
- pub client_version: String,
126
-
127
- /// A human-readable string that can identify this process. Defaults to empty string.
128
- #[builder(default)]
129
- pub identity: String,
130
-
131
- /// If specified, use TLS as configured by the [TlsOptions] struct. If this is set core will
132
- /// attempt to use TLS when connecting to the Temporal server. Lang SDK is expected to pass any
133
- /// certs or keys as bytes, loading them from disk itself if needed.
134
- pub tls_options: Option<TlsOptions>,
135
-
136
- /// Retry configuration for the server client. Default is [RetryOptions::default]
137
- #[builder(default)]
138
- pub retry_options: RetryOptions,
139
-
140
- /// If set, override the origin used when connecting. May be useful in rare situations where tls
141
- /// verification needs to use a different name from what should be set as the `:authority`
142
- /// header. If [TlsOptions::domain] is set, and this is not, this will be set to
143
- /// `https://<domain>`, effectively making the `:authority` header consistent with the domain
144
- /// override.
145
- pub override_origin: Option<Uri>,
146
-
147
- /// If set, HTTP2 gRPC keep alive will be enabled.
148
- /// To enable with default settings, use `.keep_alive(ClientKeepAliveConfig::default())`.
149
- #[builder(required, default = Some(ClientKeepAliveOptions::default()))]
150
- pub keep_alive: Option<ClientKeepAliveOptions>,
151
-
152
- /// HTTP headers to include on every RPC call.
153
- ///
154
- /// These must be valid gRPC metadata keys, and must not be binary metadata keys (ending in
155
- /// `-bin). To set binary headers, use [ClientOptions::binary_headers]. Invalid header keys or
156
- /// values will cause an error to be returned when connecting.
157
- pub headers: Option<HashMap<String, String>>,
158
-
159
- /// HTTP headers to include on every RPC call as binary gRPC metadata (encoded as base64).
160
- ///
161
- /// These must be valid binary gRPC metadata keys (and end with a `-bin` suffix). Invalid
162
- /// header keys will cause an error to be returned when connecting.
163
- pub binary_headers: Option<HashMap<String, Vec<u8>>>,
164
-
165
- /// API key which is set as the "Authorization" header with "Bearer " prepended. This will only
166
- /// be applied if the headers don't already have an "Authorization" header.
167
- pub api_key: Option<String>,
168
-
169
- /// HTTP CONNECT proxy to use for this client.
170
- pub http_connect_proxy: Option<HttpConnectProxyOptions>,
171
-
172
- /// If set true, error code labels will not be included on request failure metrics.
173
- #[builder(default)]
174
- pub disable_error_code_metric_tags: bool,
175
-
176
- /// If set true, get_system_info will not be called upon connection
177
- #[builder(default)]
178
- pub skip_get_system_info: bool,
179
- }
180
-
181
- /// Configuration options for TLS
182
- #[derive(Clone, Debug, Default)]
183
- pub struct TlsOptions {
184
- /// Bytes representing the root CA certificate used by the server. If not set, and the server's
185
- /// cert is issued by someone the operating system trusts, verification will still work (ex:
186
- /// Cloud offering).
187
- pub server_root_ca_cert: Option<Vec<u8>>,
188
- /// Sets the domain name against which to verify the server's TLS certificate. If not provided,
189
- /// the domain name will be extracted from the URL used to connect.
190
- pub domain: Option<String>,
191
- /// TLS info for the client. If specified, core will attempt to use mTLS.
192
- pub client_tls_options: Option<ClientTlsOptions>,
193
- }
194
-
195
- /// If using mTLS, both the client cert and private key must be specified, this contains them.
121
+ /// A connection to the Temporal service.
122
+ ///
123
+ /// Cloning a connection is cheap (single Arc increment). The underlying connection is shared
124
+ /// between clones.
196
125
  #[derive(Clone)]
197
- pub struct ClientTlsOptions {
198
- /// The certificate for this client, encoded as PEM
199
- pub client_cert: Vec<u8>,
200
- /// The private key for this client, encoded as PEM
201
- pub client_private_key: Vec<u8>,
202
- }
203
-
204
- /// Client keep alive configuration.
205
- #[derive(Clone, Debug)]
206
- pub struct ClientKeepAliveOptions {
207
- /// Interval to send HTTP2 keep alive pings.
208
- pub interval: Duration,
209
- /// Timeout that the keep alive must be responded to within or the connection will be closed.
210
- pub timeout: Duration,
211
- }
212
-
213
- impl Default for ClientKeepAliveOptions {
214
- fn default() -> Self {
215
- Self {
216
- interval: Duration::from_secs(30),
217
- timeout: Duration::from_secs(15),
218
- }
219
- }
220
- }
221
-
222
- /// Configuration for retrying requests to the server
223
- #[derive(Clone, Debug, PartialEq)]
224
- pub struct RetryOptions {
225
- /// initial wait time before the first retry.
226
- pub initial_interval: Duration,
227
- /// randomization jitter that is used as a multiplier for the current retry interval
228
- /// and is added or subtracted from the interval length.
229
- pub randomization_factor: f64,
230
- /// rate at which retry time should be increased, until it reaches max_interval.
231
- pub multiplier: f64,
232
- /// maximum amount of time to wait between retries.
233
- pub max_interval: Duration,
234
- /// maximum total amount of time requests should be retried for, if None is set then no limit
235
- /// will be used.
236
- pub max_elapsed_time: Option<Duration>,
237
- /// maximum number of retry attempts.
238
- pub max_retries: usize,
126
+ pub struct Connection {
127
+ inner: Arc<ConnectionInner>,
239
128
  }
240
129
 
241
- impl Default for RetryOptions {
242
- fn default() -> Self {
243
- Self {
244
- initial_interval: Duration::from_millis(100), // 100 ms wait by default.
245
- randomization_factor: 0.2, // +-20% jitter.
246
- multiplier: 1.7, // each next retry delay will increase by 70%
247
- max_interval: Duration::from_secs(5), // until it reaches 5 seconds.
248
- max_elapsed_time: Some(Duration::from_secs(10)), // 10 seconds total allocated time for all retries.
249
- max_retries: 10,
250
- }
251
- }
130
+ #[derive(Clone)]
131
+ struct ConnectionInner {
132
+ service: TemporalServiceClient,
133
+ retry_options: RetryOptions,
134
+ identity: String,
135
+ headers: Arc<RwLock<ClientHeaders>>,
136
+ client_name: String,
137
+ client_version: String,
138
+ /// Capabilities as read from the `get_system_info` RPC call made on client connection
139
+ capabilities: Option<get_system_info_response::Capabilities>,
140
+ workers: Arc<ClientWorkerSet>,
252
141
  }
253
142
 
254
- impl RetryOptions {
255
- pub(crate) const fn task_poll_retry_policy() -> Self {
256
- Self {
257
- initial_interval: Duration::from_millis(200),
258
- randomization_factor: 0.2,
259
- multiplier: 2.0,
260
- max_interval: Duration::from_secs(10),
261
- max_elapsed_time: None,
262
- max_retries: 0,
263
- }
264
- }
265
-
266
- pub(crate) const fn throttle_retry_policy() -> Self {
267
- Self {
268
- initial_interval: Duration::from_secs(1),
269
- randomization_factor: 0.2,
270
- multiplier: 2.0,
271
- max_interval: Duration::from_secs(10),
272
- max_elapsed_time: None,
273
- max_retries: 0,
274
- }
275
- }
276
-
277
- /// A retry policy that never retires
278
- pub const fn no_retries() -> Self {
279
- Self {
280
- initial_interval: Duration::from_secs(0),
281
- randomization_factor: 0.0,
282
- multiplier: 1.0,
283
- max_interval: Duration::from_secs(0),
284
- max_elapsed_time: None,
285
- max_retries: 1,
286
- }
287
- }
143
+ impl Connection {
144
+ /// Connect to a Temporal service.
145
+ pub async fn connect(options: ConnectionOptions) -> Result<Self, ClientConnectError> {
146
+ let service = if let Some(service_override) = options.service_override {
147
+ GrpcMetricSvc {
148
+ inner: ChannelOrGrpcOverride::GrpcOverride(service_override),
149
+ metrics: options.metrics_meter.clone().map(MetricsContext::new),
150
+ disable_errcode_label: options.disable_error_code_metric_tags,
151
+ }
152
+ } else {
153
+ let channel = Channel::from_shared(options.target.to_string())?;
154
+ let channel = add_tls_to_channel(options.tls_options.as_ref(), channel).await?;
155
+ let channel = if let Some(keep_alive) = options.keep_alive.as_ref() {
156
+ channel
157
+ .keep_alive_while_idle(true)
158
+ .http2_keep_alive_interval(keep_alive.interval)
159
+ .keep_alive_timeout(keep_alive.timeout)
160
+ } else {
161
+ channel
162
+ };
163
+ let channel = if let Some(origin) = options.override_origin.clone() {
164
+ channel.origin(origin)
165
+ } else {
166
+ channel
167
+ };
168
+ // If there is a proxy, we have to connect that way
169
+ let channel = if let Some(proxy) = options.http_connect_proxy.as_ref() {
170
+ proxy.connect_endpoint(&channel).await?
171
+ } else {
172
+ channel.connect().await?
173
+ };
174
+ ServiceBuilder::new()
175
+ .layer_fn(move |channel| GrpcMetricSvc {
176
+ inner: ChannelOrGrpcOverride::Channel(channel),
177
+ metrics: options.metrics_meter.clone().map(MetricsContext::new),
178
+ disable_errcode_label: options.disable_error_code_metric_tags,
179
+ })
180
+ .service(channel)
181
+ };
288
182
 
289
- pub(crate) fn into_exp_backoff<C>(self, clock: C) -> exponential::ExponentialBackoff<C> {
290
- exponential::ExponentialBackoff {
291
- current_interval: self.initial_interval,
292
- initial_interval: self.initial_interval,
293
- randomization_factor: self.randomization_factor,
294
- multiplier: self.multiplier,
295
- max_interval: self.max_interval,
296
- max_elapsed_time: self.max_elapsed_time,
297
- clock,
298
- start_time: Instant::now(),
299
- }
300
- }
301
- }
183
+ let headers = Arc::new(RwLock::new(ClientHeaders {
184
+ user_headers: parse_ascii_headers(options.headers.clone().unwrap_or_default())?,
185
+ user_binary_headers: parse_binary_headers(
186
+ options.binary_headers.clone().unwrap_or_default(),
187
+ )?,
188
+ api_key: options.api_key.clone(),
189
+ }));
190
+ let interceptor = ServiceCallInterceptor {
191
+ client_name: options.client_name.clone(),
192
+ client_version: options.client_version.clone(),
193
+ headers: headers.clone(),
194
+ };
195
+ let svc = InterceptedService::new(service, interceptor);
196
+ let mut svc_client = TemporalServiceClient::new(svc);
302
197
 
303
- impl From<RetryOptions> for ExponentialBackoff {
304
- fn from(c: RetryOptions) -> Self {
305
- c.into_exp_backoff(SystemClock::default())
198
+ let capabilities = if !options.skip_get_system_info {
199
+ match svc_client
200
+ .get_system_info(GetSystemInfoRequest::default().into_request())
201
+ .await
202
+ {
203
+ Ok(sysinfo) => sysinfo.into_inner().capabilities,
204
+ Err(status) => match status.code() {
205
+ Code::Unimplemented => None,
206
+ _ => return Err(ClientConnectError::SystemInfoCallError(status)),
207
+ },
208
+ }
209
+ } else {
210
+ None
211
+ };
212
+ Ok(Self {
213
+ inner: Arc::new(ConnectionInner {
214
+ service: svc_client,
215
+ retry_options: options.retry_options,
216
+ identity: options.identity,
217
+ headers,
218
+ client_name: options.client_name,
219
+ client_version: options.client_version,
220
+ capabilities,
221
+ workers: Arc::new(ClientWorkerSet::new()),
222
+ }),
223
+ })
306
224
  }
307
- }
308
225
 
309
- impl Debug for ClientTlsOptions {
310
- // Intentionally omit details here since they could leak a key if ever printed
311
- fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
312
- write!(f, "ClientTlsOptions(..)")
226
+ /// Set API key, overwriting any previous one.
227
+ pub fn set_api_key(&self, api_key: Option<String>) {
228
+ self.inner.headers.write().api_key = api_key;
313
229
  }
314
- }
315
-
316
- /// Errors thrown while attempting to establish a connection to the server
317
- #[derive(thiserror::Error, Debug)]
318
- pub enum ClientInitError {
319
- /// Invalid URI. Configuration error, fatal.
320
- #[error("Invalid URI: {0:?}")]
321
- InvalidUri(#[from] InvalidUri),
322
- /// Invalid gRPC metadata headers. Configuration error.
323
- #[error("Invalid headers: {0}")]
324
- InvalidHeaders(#[from] InvalidHeaderError),
325
- /// Server connection error. Crashing and restarting the worker is likely best.
326
- #[error("Server connection error: {0:?}")]
327
- TonicTransportError(#[from] tonic::transport::Error),
328
- /// We couldn't successfully make the `get_system_info` call at connection time to establish
329
- /// server capabilities / verify server is responding.
330
- #[error("`get_system_info` call error after connection: {0:?}")]
331
- SystemInfoCallError(tonic::Status),
332
- }
333
-
334
- /// Errors thrown when a gRPC metadata header is invalid.
335
- #[derive(thiserror::Error, Debug)]
336
- pub enum InvalidHeaderError {
337
- /// A binary header key was invalid
338
- #[error("Invalid binary header key '{key}': {source}")]
339
- InvalidBinaryHeaderKey {
340
- /// The invalid key
341
- key: String,
342
- /// The source error from tonic
343
- source: tonic::metadata::errors::InvalidMetadataKey,
344
- },
345
- /// An ASCII header key was invalid
346
- #[error("Invalid ASCII header key '{key}': {source}")]
347
- InvalidAsciiHeaderKey {
348
- /// The invalid key
349
- key: String,
350
- /// The source error from tonic
351
- source: tonic::metadata::errors::InvalidMetadataKey,
352
- },
353
- /// An ASCII header value was invalid
354
- #[error("Invalid ASCII header value for key '{key}': {source}")]
355
- InvalidAsciiHeaderValue {
356
- /// The key
357
- key: String,
358
- /// The invalid value
359
- value: String,
360
- /// The source error from tonic
361
- source: tonic::metadata::errors::InvalidMetadataValue,
362
- },
363
- }
364
230
 
365
- /// A client with [ClientOptions] attached, which can be passed to initialize workers,
366
- /// or can be used directly. Is cheap to clone.
367
- #[derive(Clone, Debug)]
368
- pub struct ConfiguredClient<C> {
369
- client: C,
370
- options: Arc<ClientOptions>,
371
- headers: Arc<RwLock<ClientHeaders>>,
372
- /// Capabilities as read from the `get_system_info` RPC call made on client connection
373
- capabilities: Option<get_system_info_response::Capabilities>,
374
- workers: Arc<ClientWorkerSet>,
375
- }
376
-
377
- impl<C> ConfiguredClient<C> {
378
231
  /// Set HTTP request headers overwriting previous headers.
379
232
  ///
380
- /// This will not affect headers set via [ClientOptions::binary_headers].
233
+ /// This will not affect headers set via [ConnectionOptions::binary_headers].
381
234
  ///
382
235
  /// # Errors
383
236
  ///
384
237
  /// Will return an error if any of the provided keys or values are not valid gRPC metadata.
385
238
  /// If an error is returned, the previous headers will remain unchanged.
386
239
  pub fn set_headers(&self, headers: HashMap<String, String>) -> Result<(), InvalidHeaderError> {
387
- self.headers.write().user_headers = parse_ascii_headers(headers)?;
240
+ self.inner.headers.write().user_headers = parse_ascii_headers(headers)?;
388
241
  Ok(())
389
242
  }
390
243
 
391
244
  /// Set binary HTTP request headers overwriting previous headers.
392
245
  ///
393
- /// This will not affect headers set via [ClientOptions::headers].
246
+ /// This will not affect headers set via [ConnectionOptions::headers].
394
247
  ///
395
248
  /// # Errors
396
249
  ///
@@ -400,34 +253,80 @@ impl<C> ConfiguredClient<C> {
400
253
  &self,
401
254
  binary_headers: HashMap<String, Vec<u8>>,
402
255
  ) -> Result<(), InvalidHeaderError> {
403
- self.headers.write().user_binary_headers = parse_binary_headers(binary_headers)?;
256
+ self.inner.headers.write().user_binary_headers = parse_binary_headers(binary_headers)?;
404
257
  Ok(())
405
258
  }
406
259
 
407
- /// Set API key, overwriting previous
408
- pub fn set_api_key(&self, api_key: Option<String>) {
409
- self.headers.write().api_key = api_key;
260
+ /// Returns the value used for the `client-name` header by this connection.
261
+ pub fn client_name(&self) -> &str {
262
+ &self.inner.client_name
410
263
  }
411
264
 
412
- /// Returns the options the client is configured with
413
- pub fn options(&self) -> &ClientOptions {
414
- &self.options
265
+ /// Returns the value used for the `client-version` header by this connection.
266
+ pub fn client_version(&self) -> &str {
267
+ &self.inner.client_version
415
268
  }
416
269
 
417
270
  /// Returns the server capabilities we (may have) learned about when establishing an initial
418
271
  /// connection
419
272
  pub fn capabilities(&self) -> Option<&get_system_info_response::Capabilities> {
420
- self.capabilities.as_ref()
273
+ self.inner.capabilities.as_ref()
274
+ }
275
+
276
+ /// Get a mutable reference to the retry options.
277
+ ///
278
+ /// Note: If this connection has been cloned, this will copy-on-write to avoid
279
+ /// affecting other clones.
280
+ pub fn retry_options_mut(&mut self) -> &mut RetryOptions {
281
+ &mut Arc::make_mut(&mut self.inner).retry_options
282
+ }
283
+
284
+ /// Get a reference to the connection identity.
285
+ pub fn identity(&self) -> &str {
286
+ &self.inner.identity
287
+ }
288
+
289
+ /// Get a mutable reference to the connection identity.
290
+ ///
291
+ /// Note: If this connection has been cloned, this will copy-on-write to avoid
292
+ /// affecting other clones.
293
+ pub fn identity_mut(&mut self) -> &mut String {
294
+ &mut Arc::make_mut(&mut self.inner).identity
421
295
  }
422
296
 
423
- /// Returns a cloned reference to a registry with workers using this client instance
297
+ /// Returns a reference to a registry with workers using this client instance.
424
298
  pub fn workers(&self) -> Arc<ClientWorkerSet> {
425
- self.workers.clone()
299
+ self.inner.workers.clone()
426
300
  }
427
301
 
428
- /// Returns the worker grouping key, this should be unique across each client
302
+ /// Returns the client-wide key.
429
303
  pub fn worker_grouping_key(&self) -> Uuid {
430
- self.workers.worker_grouping_key()
304
+ self.inner.workers.worker_grouping_key()
305
+ }
306
+
307
+ /// Get the underlying workflow service client for making raw gRPC calls.
308
+ pub fn workflow_service(&self) -> Box<dyn WorkflowService> {
309
+ self.inner.service.workflow_service()
310
+ }
311
+
312
+ /// Get the underlying operator service client for making raw gRPC calls.
313
+ pub fn operator_service(&self) -> Box<dyn OperatorService> {
314
+ self.inner.service.operator_service()
315
+ }
316
+
317
+ /// Get the underlying cloud service client for making raw gRPC calls.
318
+ pub fn cloud_service(&self) -> Box<dyn CloudService> {
319
+ self.inner.service.cloud_service()
320
+ }
321
+
322
+ /// Get the underlying test service client for making raw gRPC calls.
323
+ pub fn test_service(&self) -> Box<dyn TestService> {
324
+ self.inner.service.test_service()
325
+ }
326
+
327
+ /// Get the underlying health service client for making raw gRPC calls.
328
+ pub fn health_service(&self) -> Box<dyn HealthService> {
329
+ self.inner.service.health_service()
431
330
  }
432
331
  }
433
332
 
@@ -463,166 +362,42 @@ impl ClientHeaders {
463
362
  }
464
363
  }
465
364
 
466
- // The configured client is effectively a "smart" (dumb) pointer
467
- impl<C> Deref for ConfiguredClient<C> {
468
- type Target = C;
469
-
470
- fn deref(&self) -> &Self::Target {
471
- &self.client
472
- }
473
- }
474
-
475
- impl<C> DerefMut for ConfiguredClient<C> {
476
- fn deref_mut(&mut self) -> &mut Self::Target {
477
- &mut self.client
478
- }
479
- }
480
-
481
- impl ClientOptions {
482
- /// Attempt to establish a connection to the Temporal server in a specific namespace. The
483
- /// returned client is bound to that namespace.
484
- pub async fn connect(
485
- &self,
486
- namespace: impl Into<String>,
487
- metrics_meter: Option<TemporalMeter>,
488
- ) -> Result<RetryClient<Client>, ClientInitError> {
489
- let client = self.connect_no_namespace(metrics_meter).await?.into_inner();
490
- let client = Client::new(client, namespace.into());
491
- let retry_client = RetryClient::new(client, self.retry_options.clone());
492
- Ok(retry_client)
493
- }
494
-
495
- /// Attempt to establish a connection to the Temporal server and return a gRPC client which is
496
- /// intercepted with retry, default headers functionality, and metrics if provided.
497
- ///
498
- /// See [RetryClient] for more
499
- pub async fn connect_no_namespace(
500
- &self,
501
- metrics_meter: Option<TemporalMeter>,
502
- ) -> Result<RetryClient<ConfiguredClient<TemporalServiceClient>>, ClientInitError> {
503
- self.connect_no_namespace_with_service_override(metrics_meter, None)
504
- .await
505
- }
506
-
507
- /// Attempt to establish a connection to the Temporal server and return a gRPC client which is
508
- /// intercepted with retry, default headers functionality, and metrics if provided. If a
509
- /// service_override is present, network-specific options are ignored and the callback is
510
- /// invoked for each gRPC call.
511
- ///
512
- /// See [RetryClient] for more
513
- pub async fn connect_no_namespace_with_service_override(
514
- &self,
515
- metrics_meter: Option<TemporalMeter>,
516
- service_override: Option<callback_based::CallbackBasedGrpcService>,
517
- ) -> Result<RetryClient<ConfiguredClient<TemporalServiceClient>>, ClientInitError> {
518
- let service = if let Some(service_override) = service_override {
519
- GrpcMetricSvc {
520
- inner: ChannelOrGrpcOverride::GrpcOverride(service_override),
521
- metrics: metrics_meter.clone().map(MetricsContext::new),
522
- disable_errcode_label: self.disable_error_code_metric_tags,
523
- }
365
+ /// If TLS is configured, set the appropriate options on the provided channel and return it.
366
+ /// Passes it through if TLS options not set.
367
+ async fn add_tls_to_channel(
368
+ tls_options: Option<&TlsOptions>,
369
+ mut channel: Endpoint,
370
+ ) -> Result<Endpoint, ClientConnectError> {
371
+ if let Some(tls_cfg) = tls_options {
372
+ let mut tls = tonic::transport::ClientTlsConfig::new();
373
+
374
+ if let Some(root_cert) = &tls_cfg.server_root_ca_cert {
375
+ let server_root_ca_cert = Certificate::from_pem(root_cert);
376
+ tls = tls.ca_certificate(server_root_ca_cert);
524
377
  } else {
525
- let channel = Channel::from_shared(self.target_url.to_string())?;
526
- let channel = self.add_tls_to_channel(channel).await?;
527
- let channel = if let Some(keep_alive) = self.keep_alive.as_ref() {
528
- channel
529
- .keep_alive_while_idle(true)
530
- .http2_keep_alive_interval(keep_alive.interval)
531
- .keep_alive_timeout(keep_alive.timeout)
532
- } else {
533
- channel
534
- };
535
- let channel = if let Some(origin) = self.override_origin.clone() {
536
- channel.origin(origin)
537
- } else {
538
- channel
539
- };
540
- // If there is a proxy, we have to connect that way
541
- let channel = if let Some(proxy) = self.http_connect_proxy.as_ref() {
542
- proxy.connect_endpoint(&channel).await?
543
- } else {
544
- channel.connect().await?
545
- };
546
- ServiceBuilder::new()
547
- .layer_fn(move |channel| GrpcMetricSvc {
548
- inner: ChannelOrGrpcOverride::Channel(channel),
549
- metrics: metrics_meter.clone().map(MetricsContext::new),
550
- disable_errcode_label: self.disable_error_code_metric_tags,
551
- })
552
- .service(channel)
553
- };
554
-
555
- let headers = Arc::new(RwLock::new(ClientHeaders {
556
- user_headers: parse_ascii_headers(self.headers.clone().unwrap_or_default())?,
557
- user_binary_headers: parse_binary_headers(
558
- self.binary_headers.clone().unwrap_or_default(),
559
- )?,
560
- api_key: self.api_key.clone(),
561
- }));
562
- let interceptor = ServiceCallInterceptor {
563
- opts: self.clone(),
564
- headers: headers.clone(),
565
- };
566
- let svc = InterceptedService::new(service, interceptor);
567
-
568
- let mut client = ConfiguredClient {
569
- headers,
570
- client: TemporalServiceClient::new(svc),
571
- options: Arc::new(self.clone()),
572
- capabilities: None,
573
- workers: Arc::new(ClientWorkerSet::new()),
574
- };
575
- if !self.skip_get_system_info {
576
- match client
577
- .get_system_info(GetSystemInfoRequest::default().into_request())
578
- .await
579
- {
580
- Ok(sysinfo) => {
581
- client.capabilities = sysinfo.into_inner().capabilities;
582
- }
583
- Err(status) => match status.code() {
584
- Code::Unimplemented => {}
585
- _ => return Err(ClientInitError::SystemInfoCallError(status)),
586
- },
587
- };
378
+ tls = tls.with_native_roots();
588
379
  }
589
- Ok(RetryClient::new(client, self.retry_options.clone()))
590
- }
591
380
 
592
- /// If TLS is configured, set the appropriate options on the provided channel and return it.
593
- /// Passes it through if TLS options not set.
594
- async fn add_tls_to_channel(&self, mut channel: Endpoint) -> Result<Endpoint, ClientInitError> {
595
- if let Some(tls_cfg) = &self.tls_options {
596
- let mut tls = tonic::transport::ClientTlsConfig::new();
381
+ if let Some(domain) = &tls_cfg.domain {
382
+ tls = tls.domain_name(domain);
597
383
 
598
- if let Some(root_cert) = &tls_cfg.server_root_ca_cert {
599
- let server_root_ca_cert = Certificate::from_pem(root_cert);
600
- tls = tls.ca_certificate(server_root_ca_cert);
601
- } else {
602
- tls = tls.with_native_roots();
603
- }
604
-
605
- if let Some(domain) = &tls_cfg.domain {
606
- tls = tls.domain_name(domain);
607
-
608
- // This song and dance ultimately is just to make sure the `:authority` header ends
609
- // up correct on requests while we use TLS. Setting the header directly in our
610
- // interceptor doesn't work since seemingly it is overridden at some point by
611
- // something lower level.
612
- let uri: Uri = format!("https://{domain}").parse()?;
613
- channel = channel.origin(uri);
614
- }
615
-
616
- if let Some(client_opts) = &tls_cfg.client_tls_options {
617
- let client_identity =
618
- Identity::from_pem(&client_opts.client_cert, &client_opts.client_private_key);
619
- tls = tls.identity(client_identity);
620
- }
384
+ // This song and dance ultimately is just to make sure the `:authority` header ends
385
+ // up correct on requests while we use TLS. Setting the header directly in our
386
+ // interceptor doesn't work since seemingly it is overridden at some point by
387
+ // something lower level.
388
+ let uri: Uri = format!("https://{domain}").parse()?;
389
+ channel = channel.origin(uri);
390
+ }
621
391
 
622
- return channel.tls_config(tls).map_err(Into::into);
392
+ if let Some(client_opts) = &tls_cfg.client_tls_options {
393
+ let client_identity =
394
+ Identity::from_pem(&client_opts.client_cert, &client_opts.client_private_key);
395
+ tls = tls.identity(client_identity);
623
396
  }
624
- Ok(channel)
397
+
398
+ return channel.tls_config(tls).map_err(Into::into);
625
399
  }
400
+ Ok(channel)
626
401
  }
627
402
 
628
403
  fn parse_ascii_headers(
@@ -679,7 +454,8 @@ fn parse_binary_headers(
679
454
  /// Interceptor which attaches common metadata (like "client-name") to every outgoing call
680
455
  #[derive(Clone)]
681
456
  pub struct ServiceCallInterceptor {
682
- opts: ClientOptions,
457
+ client_name: String,
458
+ client_version: String,
683
459
  /// Only accessed as a reader
684
460
  headers: Arc<RwLock<ClientHeaders>>,
685
461
  }
@@ -695,8 +471,7 @@ impl Interceptor for ServiceCallInterceptor {
695
471
  if !metadata.contains_key(CLIENT_NAME_HEADER_KEY) {
696
472
  metadata.insert(
697
473
  CLIENT_NAME_HEADER_KEY,
698
- self.opts
699
- .client_name
474
+ self.client_name
700
475
  .parse()
701
476
  .unwrap_or_else(|_| MetadataValue::from_static("")),
702
477
  );
@@ -704,8 +479,7 @@ impl Interceptor for ServiceCallInterceptor {
704
479
  if !metadata.contains_key(CLIENT_VERSION_HEADER_KEY) {
705
480
  metadata.insert(
706
481
  CLIENT_VERSION_HEADER_KEY,
707
- self.opts
708
- .client_version
482
+ self.client_version
709
483
  .parse()
710
484
  .unwrap_or_else(|_| MetadataValue::from_static("")),
711
485
  );
@@ -794,78 +568,145 @@ impl TemporalServiceClient {
794
568
  }
795
569
 
796
570
  /// Get the underlying workflow service client
797
- pub fn workflow_svc(&self) -> Box<dyn WorkflowService> {
571
+ pub fn workflow_service(&self) -> Box<dyn WorkflowService> {
798
572
  self.workflow_svc_client.clone()
799
573
  }
800
574
  /// Get the underlying operator service client
801
- pub fn operator_svc(&self) -> Box<dyn OperatorService> {
575
+ pub fn operator_service(&self) -> Box<dyn OperatorService> {
802
576
  self.operator_svc_client.clone()
803
577
  }
804
578
  /// Get the underlying cloud service client
805
- pub fn cloud_svc(&self) -> Box<dyn CloudService> {
579
+ pub fn cloud_service(&self) -> Box<dyn CloudService> {
806
580
  self.cloud_svc_client.clone()
807
581
  }
808
582
  /// Get the underlying test service client
809
- pub fn test_svc(&self) -> Box<dyn TestService> {
583
+ pub fn test_service(&self) -> Box<dyn TestService> {
810
584
  self.test_svc_client.clone()
811
585
  }
812
586
  /// Get the underlying health service client
813
- pub fn health_svc(&self) -> Box<dyn HealthService> {
587
+ pub fn health_service(&self) -> Box<dyn HealthService> {
814
588
  self.health_svc_client.clone()
815
589
  }
816
590
  }
817
591
 
818
- /// Contains an instance of a namespace-bound client for interacting with the Temporal server
592
+ /// Contains an instance of a namespace-bound client for interacting with the Temporal server.
593
+ /// Cheap to clone.
819
594
  #[derive(Clone)]
820
595
  pub struct Client {
821
- /// Client for interacting with workflow service
822
- inner: ConfiguredClient<TemporalServiceClient>,
823
- /// The namespace this client interacts with
824
- namespace: String,
596
+ connection: Connection,
597
+ options: Arc<ClientOptions>,
825
598
  }
826
599
 
827
600
  impl Client {
828
- /// Create a new client from an existing configured lower level client and a namespace
829
- pub fn new(client: ConfiguredClient<TemporalServiceClient>, namespace: String) -> Self {
830
- Client {
831
- inner: client,
832
- namespace,
833
- }
601
+ /// Create a new client from a connection and options.
602
+ ///
603
+ /// Currently infallible, but returns a `Result` for future extensibility
604
+ /// (e.g., interceptor or plugin validation).
605
+ pub fn new(connection: Connection, options: ClientOptions) -> Result<Self, ClientNewError> {
606
+ Ok(Client {
607
+ connection,
608
+ options: Arc::new(options),
609
+ })
834
610
  }
835
611
 
836
612
  /// Return the options this client was initialized with
837
613
  pub fn options(&self) -> &ClientOptions {
838
- &self.inner.options
614
+ &self.options
839
615
  }
840
616
 
841
- /// Return the options this client was initialized with mutably
617
+ /// Return this client's options mutably.
618
+ ///
619
+ /// Note: If this client has been cloned, this will copy-on-write to avoid affecting other
620
+ /// clones.
842
621
  pub fn options_mut(&mut self) -> &mut ClientOptions {
843
- Arc::make_mut(&mut self.inner.options)
622
+ Arc::make_mut(&mut self.options)
844
623
  }
845
624
 
846
- /// Returns a reference to the underlying client
847
- pub fn inner(&self) -> &ConfiguredClient<TemporalServiceClient> {
848
- &self.inner
625
+ /// Returns a reference to the underlying connection
626
+ pub fn connection(&self) -> &Connection {
627
+ &self.connection
849
628
  }
850
629
 
851
- /// Consumes self and returns the underlying client
852
- pub fn into_inner(self) -> ConfiguredClient<TemporalServiceClient> {
853
- self.inner
630
+ /// Returns a mutable reference to the underlying connection
631
+ pub fn connection_mut(&mut self) -> &mut Connection {
632
+ &mut self.connection
854
633
  }
634
+ }
855
635
 
856
- /// Returns the client-wide key
857
- pub fn worker_grouping_key(&self) -> Uuid {
858
- self.inner.worker_grouping_key()
636
+ // High-level workflow operations on Client.
637
+ // These forward to the internal WorkflowClientTrait blanket impl which is
638
+ // available because Client implements WorkflowService + NamespacedClient + Clone.
639
+ impl Client {
640
+ /// Start a workflow execution.
641
+ ///
642
+ /// Returns a [`WorkflowHandle`] that can be used to interact with the workflow
643
+ /// (e.g., get its result, send signals, query, etc.).
644
+ pub async fn start_workflow<W>(
645
+ &self,
646
+ workflow: W,
647
+ input: W::Input,
648
+ options: WorkflowStartOptions,
649
+ ) -> Result<WorkflowHandle<Self, W>, WorkflowStartError>
650
+ where
651
+ W: WorkflowDefinition,
652
+ W::Input: Send,
653
+ {
654
+ WorkflowClientTrait::start_workflow(self, workflow, input, options).await
655
+ }
656
+
657
+ /// Get a handle to an existing workflow.
658
+ ///
659
+ /// For untyped access, use `get_workflow_handle::<UntypedWorkflow>(...)`.
660
+ pub fn get_workflow_handle<W: WorkflowDefinition>(
661
+ &self,
662
+ workflow_id: impl Into<String>,
663
+ ) -> WorkflowHandle<Self, W> {
664
+ WorkflowClientTrait::get_workflow_handle(self, workflow_id)
665
+ }
666
+
667
+ /// List workflows matching a query.
668
+ ///
669
+ /// Returns a stream that lazily paginates through results.
670
+ /// Use `limit` in options to cap the number of results returned.
671
+ pub fn list_workflows(
672
+ &self,
673
+ query: impl Into<String>,
674
+ opts: WorkflowListOptions,
675
+ ) -> ListWorkflowsStream {
676
+ WorkflowClientTrait::list_workflows(self, query, opts)
677
+ }
678
+
679
+ /// Count workflows matching a query.
680
+ pub async fn count_workflows(
681
+ &self,
682
+ query: impl Into<String>,
683
+ opts: WorkflowCountOptions,
684
+ ) -> Result<WorkflowExecutionCount, ClientError> {
685
+ WorkflowClientTrait::count_workflows(self, query, opts).await
686
+ }
687
+
688
+ /// Get a handle to complete an activity asynchronously.
689
+ ///
690
+ /// An activity returning `ActivityError::WillCompleteAsync` can be completed with this handle.
691
+ pub fn get_async_activity_handle(
692
+ &self,
693
+ identifier: ActivityIdentifier,
694
+ ) -> AsyncActivityHandle<Self> {
695
+ WorkflowClientTrait::get_async_activity_handle(self, identifier)
859
696
  }
860
697
  }
861
698
 
862
699
  impl NamespacedClient for Client {
863
700
  fn namespace(&self) -> String {
864
- self.namespace.clone()
701
+ self.options.namespace.clone()
865
702
  }
866
703
 
867
704
  fn identity(&self) -> String {
868
- self.inner.options.identity.clone()
705
+ self.connection.identity().to_owned()
706
+ }
707
+
708
+ fn data_converter(&self) -> &DataConverter {
709
+ &self.options.data_converter
869
710
  }
870
711
  }
871
712
 
@@ -889,268 +730,60 @@ impl Namespace {
889
730
  }
890
731
  }
891
732
 
892
- /// Default workflow execution retention for a Namespace is 3 days
893
- const DEFAULT_WORKFLOW_EXECUTION_RETENTION_PERIOD: Duration = Duration::from_secs(60 * 60 * 24 * 3);
894
-
895
- /// Helper struct for `register_namespace`.
896
- #[derive(Clone, bon::Builder)]
897
- #[builder(on(String, into))]
898
- pub struct RegisterNamespaceOptions {
899
- /// Name (required)
900
- pub namespace: String,
901
- /// Description (required)
902
- pub description: String,
903
- /// Owner's email
904
- #[builder(default)]
905
- pub owner_email: String,
906
- /// Workflow execution retention period
907
- #[builder(default = DEFAULT_WORKFLOW_EXECUTION_RETENTION_PERIOD)]
908
- pub workflow_execution_retention_period: Duration,
909
- /// Cluster settings
910
- #[builder(default)]
911
- pub clusters: Vec<ClusterReplicationConfig>,
912
- /// Active cluster name
913
- #[builder(default)]
914
- pub active_cluster_name: String,
915
- /// Custom Data
916
- #[builder(default)]
917
- pub data: HashMap<String, String>,
918
- /// Security Token
919
- #[builder(default)]
920
- pub security_token: String,
921
- /// Global namespace
922
- #[builder(default)]
923
- pub is_global_namespace: bool,
924
- /// History Archival setting
925
- #[builder(default = ArchivalState::Unspecified)]
926
- pub history_archival_state: ArchivalState,
927
- /// History Archival uri
928
- #[builder(default)]
929
- pub history_archival_uri: String,
930
- /// Visibility Archival setting
931
- #[builder(default = ArchivalState::Unspecified)]
932
- pub visibility_archival_state: ArchivalState,
933
- /// Visibility Archival uri
934
- #[builder(default)]
935
- pub visibility_archival_uri: String,
936
- }
937
-
938
- impl From<RegisterNamespaceOptions> for RegisterNamespaceRequest {
939
- fn from(val: RegisterNamespaceOptions) -> Self {
940
- RegisterNamespaceRequest {
941
- namespace: val.namespace,
942
- description: val.description,
943
- owner_email: val.owner_email,
944
- workflow_execution_retention_period: val
945
- .workflow_execution_retention_period
946
- .try_into()
947
- .ok(),
948
- clusters: val.clusters,
949
- active_cluster_name: val.active_cluster_name,
950
- data: val.data,
951
- security_token: val.security_token,
952
- is_global_namespace: val.is_global_namespace,
953
- history_archival_state: val.history_archival_state as i32,
954
- history_archival_uri: val.history_archival_uri,
955
- visibility_archival_state: val.visibility_archival_state as i32,
956
- visibility_archival_uri: val.visibility_archival_uri,
957
- }
958
- }
959
- }
960
-
961
- // Note: The cluster_names custom setter from derive_builder is not supported in bon.
962
- // Users should manually construct the clusters vector if needed.
963
-
964
- /// Helper struct for `signal_with_start_workflow_execution`.
965
- #[derive(Clone, bon::Builder)]
966
- #[builder(on(String, into))]
967
- pub struct SignalWithStartOptions {
968
- /// Input payload for the workflow run
969
- pub input: Option<Payloads>,
970
- /// Task Queue to target (required)
971
- pub task_queue: String,
972
- /// Workflow id for the workflow run
973
- pub workflow_id: String,
974
- /// Workflow type for the workflow run
975
- pub workflow_type: String,
976
- /// Request id for idempotency/deduplication
977
- pub request_id: Option<String>,
978
- /// The signal name to send (required)
979
- pub signal_name: String,
980
- /// Payloads for the signal
981
- pub signal_input: Option<Payloads>,
982
- /// Headers for the signal
983
- pub signal_header: Option<Header>,
984
- }
985
-
986
733
  /// This trait provides higher-level friendlier interaction with the server.
987
734
  /// See the [WorkflowService] trait for a lower-level client.
988
- #[async_trait::async_trait]
989
- pub trait WorkflowClientTrait: NamespacedClient {
990
- /// Starts workflow execution.
991
- async fn start_workflow(
992
- &self,
993
- input: Vec<Payload>,
994
- task_queue: String,
995
- workflow_id: String,
996
- workflow_type: String,
997
- request_id: Option<String>,
998
- options: WorkflowOptions,
999
- ) -> Result<StartWorkflowExecutionResponse>;
1000
-
1001
- /// Notifies the server that workflow tasks for a given workflow should be sent to the normal
1002
- /// non-sticky task queue. This normally happens when workflow has been evicted from the cache.
1003
- async fn reset_sticky_task_queue(
735
+ pub(crate) trait WorkflowClientTrait: NamespacedClient {
736
+ /// Start a workflow execution.
737
+ fn start_workflow<W>(
1004
738
  &self,
1005
- workflow_id: String,
1006
- run_id: String,
1007
- ) -> Result<ResetStickyTaskQueueResponse>;
1008
-
1009
- /// Complete activity task by sending response to the server. `task_token` contains activity
1010
- /// identifier that would've been received from polling for an activity task. `result` is a blob
1011
- /// that contains activity response.
1012
- async fn complete_activity_task(
1013
- &self,
1014
- task_token: TaskToken,
1015
- result: Option<Payloads>,
1016
- ) -> Result<RespondActivityTaskCompletedResponse>;
1017
-
1018
- /// Report activity task heartbeat by sending details to the server. `task_token` contains
1019
- /// activity identifier that would've been received from polling for an activity task. `result`
1020
- /// contains `cancel_requested` flag, which if set to true indicates that activity has been
1021
- /// cancelled.
1022
- async fn record_activity_heartbeat(
1023
- &self,
1024
- task_token: TaskToken,
1025
- details: Option<Payloads>,
1026
- ) -> Result<RecordActivityTaskHeartbeatResponse>;
1027
-
1028
- /// Cancel activity task by sending response to the server. `task_token` contains activity
1029
- /// identifier that would've been received from polling for an activity task. `details` is a
1030
- /// blob that provides arbitrary user defined cancellation info.
1031
- async fn cancel_activity_task(
1032
- &self,
1033
- task_token: TaskToken,
1034
- details: Option<Payloads>,
1035
- ) -> Result<RespondActivityTaskCanceledResponse>;
1036
-
1037
- /// Send a signal to a certain workflow instance
1038
- async fn signal_workflow_execution(
1039
- &self,
1040
- workflow_id: String,
1041
- run_id: String,
1042
- signal_name: String,
1043
- payloads: Option<Payloads>,
1044
- request_id: Option<String>,
1045
- ) -> Result<SignalWorkflowExecutionResponse>;
1046
-
1047
- /// Send signal and start workflow transcationally
1048
- //#TODO maybe lift the Signal type from sdk::workflow_context::options
1049
- #[allow(clippy::too_many_arguments)]
1050
- async fn signal_with_start_workflow_execution(
1051
- &self,
1052
- options: SignalWithStartOptions,
1053
- workflow_options: WorkflowOptions,
1054
- ) -> Result<SignalWithStartWorkflowExecutionResponse>;
739
+ workflow: W,
740
+ input: W::Input,
741
+ options: WorkflowStartOptions,
742
+ ) -> impl Future<Output = Result<WorkflowHandle<Self, W>, WorkflowStartError>>
743
+ where
744
+ Self: Sized,
745
+ W: WorkflowDefinition,
746
+ W::Input: Send;
1055
747
 
1056
- /// Request a query of a certain workflow instance
1057
- async fn query_workflow_execution(
748
+ /// Get a handle to an existing workflow. `run_id` may be left blank to specify the most recent
749
+ /// execution having the provided `workflow_id`.
750
+ ///
751
+ /// For untyped access, use `get_workflow_handle::<UntypedWorkflow>(...)`.
752
+ ///
753
+ /// See also [WorkflowHandle::new], for specifying namespace or first_execution_run_id.
754
+ fn get_workflow_handle<W: WorkflowDefinition>(
1058
755
  &self,
1059
- workflow_id: String,
1060
- run_id: String,
1061
- query: WorkflowQuery,
1062
- ) -> Result<QueryWorkflowResponse>;
756
+ workflow_id: impl Into<String>,
757
+ ) -> WorkflowHandle<Self, W>
758
+ where
759
+ Self: Sized;
1063
760
 
1064
- /// Get information about a workflow run
1065
- async fn describe_workflow_execution(
761
+ /// List workflows matching a query.
762
+ /// Returns a stream that lazily paginates through results.
763
+ /// Use `limit` in options to cap the number of results returned.
764
+ fn list_workflows(
1066
765
  &self,
1067
- workflow_id: String,
1068
- run_id: Option<String>,
1069
- ) -> Result<DescribeWorkflowExecutionResponse>;
766
+ query: impl Into<String>,
767
+ opts: WorkflowListOptions,
768
+ ) -> ListWorkflowsStream;
1070
769
 
1071
- /// Get history for a particular workflow run
1072
- async fn get_workflow_execution_history(
770
+ /// Count workflows matching a query.
771
+ fn count_workflows(
1073
772
  &self,
1074
- workflow_id: String,
1075
- run_id: Option<String>,
1076
- page_token: Vec<u8>,
1077
- ) -> Result<GetWorkflowExecutionHistoryResponse>;
773
+ query: impl Into<String>,
774
+ opts: WorkflowCountOptions,
775
+ ) -> impl Future<Output = Result<WorkflowExecutionCount, ClientError>>;
1078
776
 
1079
- /// Cancel a currently executing workflow
1080
- async fn cancel_workflow_execution(
1081
- &self,
1082
- workflow_id: String,
1083
- run_id: Option<String>,
1084
- reason: String,
1085
- request_id: Option<String>,
1086
- ) -> Result<RequestCancelWorkflowExecutionResponse>;
1087
-
1088
- /// Terminate a currently executing workflow
1089
- async fn terminate_workflow_execution(
777
+ /// Get a handle to complete an activity asynchronously.
778
+ ///
779
+ /// An activity returning `ActivityError::WillCompleteAsync` can be completed with this handle.
780
+ fn get_async_activity_handle(
1090
781
  &self,
1091
- workflow_id: String,
1092
- run_id: Option<String>,
1093
- ) -> Result<TerminateWorkflowExecutionResponse>;
1094
-
1095
- /// Register a new namespace
1096
- async fn register_namespace(
1097
- &self,
1098
- options: RegisterNamespaceOptions,
1099
- ) -> Result<RegisterNamespaceResponse>;
1100
-
1101
- /// Lists all available namespaces
1102
- async fn list_namespaces(&self) -> Result<ListNamespacesResponse>;
1103
-
1104
- /// Query namespace details
1105
- async fn describe_namespace(&self, namespace: Namespace) -> Result<DescribeNamespaceResponse>;
1106
-
1107
- /// List open workflow executions with Standard Visibility filtering
1108
- async fn list_open_workflow_executions(
1109
- &self,
1110
- max_page_size: i32,
1111
- next_page_token: Vec<u8>,
1112
- start_time_filter: Option<StartTimeFilter>,
1113
- filters: Option<list_open_workflow_executions_request::Filters>,
1114
- ) -> Result<ListOpenWorkflowExecutionsResponse>;
1115
-
1116
- /// List closed workflow executions Standard Visibility filtering
1117
- async fn list_closed_workflow_executions(
1118
- &self,
1119
- max_page_size: i32,
1120
- next_page_token: Vec<u8>,
1121
- start_time_filter: Option<StartTimeFilter>,
1122
- filters: Option<list_closed_workflow_executions_request::Filters>,
1123
- ) -> Result<ListClosedWorkflowExecutionsResponse>;
1124
-
1125
- /// List workflow executions with Advanced Visibility filtering
1126
- async fn list_workflow_executions(
1127
- &self,
1128
- page_size: i32,
1129
- next_page_token: Vec<u8>,
1130
- query: String,
1131
- ) -> Result<ListWorkflowExecutionsResponse>;
1132
-
1133
- /// List archived workflow executions
1134
- async fn list_archived_workflow_executions(
1135
- &self,
1136
- page_size: i32,
1137
- next_page_token: Vec<u8>,
1138
- query: String,
1139
- ) -> Result<ListArchivedWorkflowExecutionsResponse>;
1140
-
1141
- /// Get Cluster Search Attributes
1142
- async fn get_search_attributes(&self) -> Result<GetSearchAttributesResponse>;
1143
-
1144
- /// Send an Update to a workflow execution
1145
- async fn update_workflow_execution(
1146
- &self,
1147
- workflow_id: String,
1148
- run_id: String,
1149
- name: String,
1150
- wait_policy: update::v1::WaitPolicy,
1151
- args: Option<Payloads>,
1152
- ) -> Result<UpdateWorkflowExecutionResponse>;
1153
- }
782
+ identifier: ActivityIdentifier,
783
+ ) -> AsyncActivityHandle<Self>
784
+ where
785
+ Self: Sized;
786
+ }
1154
787
 
1155
788
  /// A client that is bound to a namespace
1156
789
  pub trait NamespacedClient {
@@ -1158,172 +791,260 @@ pub trait NamespacedClient {
1158
791
  fn namespace(&self) -> String;
1159
792
  /// Returns the client identity
1160
793
  fn identity(&self) -> String;
794
+ /// Returns the data converter for serializing/deserializing payloads.
795
+ /// Default implementation returns a static default converter.
796
+ fn data_converter(&self) -> &DataConverter {
797
+ static DEFAULT: OnceLock<DataConverter> = OnceLock::new();
798
+ DEFAULT.get_or_init(DataConverter::default)
799
+ }
1161
800
  }
1162
801
 
1163
- /// Optional fields supplied at the start of workflow execution
1164
- #[derive(Debug, Clone, Default)]
1165
- pub struct WorkflowOptions {
1166
- /// Set the policy for reusing the workflow id
1167
- pub id_reuse_policy: WorkflowIdReusePolicy,
802
+ /// A workflow execution returned from list operations.
803
+ /// This represents information about a workflow present in visibility.
804
+ #[derive(Debug, Clone)]
805
+ pub struct WorkflowExecution {
806
+ raw: workflow::WorkflowExecutionInfo,
807
+ }
808
+
809
+ impl WorkflowExecution {
810
+ /// Create a new WorkflowExecution from the raw proto.
811
+ pub fn new(raw: workflow::WorkflowExecutionInfo) -> Self {
812
+ Self { raw }
813
+ }
814
+
815
+ /// The workflow ID.
816
+ pub fn id(&self) -> &str {
817
+ self.raw
818
+ .execution
819
+ .as_ref()
820
+ .map(|e| e.workflow_id.as_str())
821
+ .unwrap_or("")
822
+ }
823
+
824
+ /// The run ID.
825
+ pub fn run_id(&self) -> &str {
826
+ self.raw
827
+ .execution
828
+ .as_ref()
829
+ .map(|e| e.run_id.as_str())
830
+ .unwrap_or("")
831
+ }
832
+
833
+ /// The workflow type name.
834
+ pub fn workflow_type(&self) -> &str {
835
+ self.raw
836
+ .r#type
837
+ .as_ref()
838
+ .map(|t| t.name.as_str())
839
+ .unwrap_or("")
840
+ }
841
+
842
+ /// The current status of the workflow execution.
843
+ pub fn status(&self) -> WorkflowExecutionStatus {
844
+ self.raw.status()
845
+ }
846
+
847
+ /// When the workflow was created.
848
+ pub fn start_time(&self) -> Option<SystemTime> {
849
+ self.raw
850
+ .start_time
851
+ .as_ref()
852
+ .and_then(proto_ts_to_system_time)
853
+ }
854
+
855
+ /// When the workflow run started or should start.
856
+ pub fn execution_time(&self) -> Option<SystemTime> {
857
+ self.raw
858
+ .execution_time
859
+ .as_ref()
860
+ .and_then(proto_ts_to_system_time)
861
+ }
862
+
863
+ /// When the workflow was closed, if closed.
864
+ pub fn close_time(&self) -> Option<SystemTime> {
865
+ self.raw
866
+ .close_time
867
+ .as_ref()
868
+ .and_then(proto_ts_to_system_time)
869
+ }
870
+
871
+ /// The task queue the workflow runs on.
872
+ pub fn task_queue(&self) -> &str {
873
+ &self.raw.task_queue
874
+ }
875
+
876
+ /// Number of events in history.
877
+ pub fn history_length(&self) -> i64 {
878
+ self.raw.history_length
879
+ }
1168
880
 
1169
- /// Set the policy for how to resolve conflicts with running policies.
1170
- /// NOTE: This is ignored for child workflows.
1171
- pub id_conflict_policy: WorkflowIdConflictPolicy,
881
+ /// Workflow memo.
882
+ pub fn memo(&self) -> Option<&Memo> {
883
+ self.raw.memo.as_ref()
884
+ }
1172
885
 
1173
- /// Optionally set the execution timeout for the workflow
1174
- /// <https://docs.temporal.io/workflows/#workflow-execution-timeout>
1175
- pub execution_timeout: Option<Duration>,
886
+ /// Parent workflow ID, if this is a child workflow.
887
+ pub fn parent_id(&self) -> Option<&str> {
888
+ self.raw
889
+ .parent_execution
890
+ .as_ref()
891
+ .map(|e| e.workflow_id.as_str())
892
+ }
1176
893
 
1177
- /// Optionally indicates the default run timeout for a workflow run
1178
- pub run_timeout: Option<Duration>,
894
+ /// Parent run ID, if this is a child workflow.
895
+ pub fn parent_run_id(&self) -> Option<&str> {
896
+ self.raw
897
+ .parent_execution
898
+ .as_ref()
899
+ .map(|e| e.run_id.as_str())
900
+ }
1179
901
 
1180
- /// Optionally indicates the default task timeout for a workflow run
1181
- pub task_timeout: Option<Duration>,
902
+ /// Search attributes on the workflow.
903
+ pub fn search_attributes(&self) -> Option<&SearchAttributes> {
904
+ self.raw.search_attributes.as_ref()
905
+ }
1182
906
 
1183
- /// Optionally set a cron schedule for the workflow
1184
- pub cron_schedule: Option<String>,
907
+ /// Access the raw proto for additional fields not exposed via accessors.
908
+ pub fn raw(&self) -> &workflow::WorkflowExecutionInfo {
909
+ &self.raw
910
+ }
1185
911
 
1186
- /// Optionally associate extra search attributes with a workflow
1187
- pub search_attributes: Option<HashMap<String, Payload>>,
912
+ /// Consume the wrapper and return the raw proto.
913
+ pub fn into_raw(self) -> workflow::WorkflowExecutionInfo {
914
+ self.raw
915
+ }
916
+ }
1188
917
 
1189
- /// Optionally enable Eager Workflow Start, a latency optimization using local workers
1190
- /// NOTE: Experimental
1191
- pub enable_eager_workflow_start: bool,
918
+ impl From<workflow::WorkflowExecutionInfo> for WorkflowExecution {
919
+ fn from(raw: workflow::WorkflowExecutionInfo) -> Self {
920
+ Self::new(raw)
921
+ }
922
+ }
1192
923
 
1193
- /// Optionally set a retry policy for the workflow
1194
- pub retry_policy: Option<RetryPolicy>,
924
+ /// A stream of workflow executions from a list query.
925
+ /// Internally paginates through results from the server.
926
+ pub struct ListWorkflowsStream {
927
+ inner: Pin<Box<dyn Stream<Item = Result<WorkflowExecution, ClientError>> + Send>>,
928
+ }
1195
929
 
1196
- /// Links to associate with the workflow. Ex: References to a nexus operation.
1197
- pub links: Vec<common::v1::Link>,
930
+ impl ListWorkflowsStream {
931
+ fn new(
932
+ inner: Pin<Box<dyn Stream<Item = Result<WorkflowExecution, ClientError>> + Send>>,
933
+ ) -> Self {
934
+ Self { inner }
935
+ }
936
+ }
1198
937
 
1199
- /// Callbacks that will be invoked upon workflow completion. For, ex, completing nexus
1200
- /// operations.
1201
- pub completion_callbacks: Vec<common::v1::Callback>,
938
+ impl Stream for ListWorkflowsStream {
939
+ type Item = Result<WorkflowExecution, ClientError>;
1202
940
 
1203
- /// Priority for the workflow
1204
- pub priority: Option<Priority>,
941
+ fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
942
+ self.inner.as_mut().poll_next(cx)
943
+ }
1205
944
  }
1206
945
 
1207
- /// Priority contains metadata that controls relative ordering of task processing
1208
- /// when tasks are backlogged in a queue. Initially, Priority will be used in
1209
- /// activity and workflow task queues, which are typically where backlogs exist.
1210
- /// Other queues in the server (such as transfer and timer queues) and rate
1211
- /// limiting decisions do not use Priority, but may in the future.
946
+ /// Result of a workflow count operation.
1212
947
  ///
1213
- /// Priority is attached to workflows and activities. Activities and child
1214
- /// workflows inherit Priority from the workflow that created them, but may
1215
- /// override fields when they are started or modified. For each field of a
1216
- /// Priority on an activity/workflow, not present or equal to zero/empty string
1217
- /// means to inherit the value from the calling workflow, or if there is no
1218
- /// calling workflow, then use the default (documented below).
1219
- ///
1220
- /// Despite being named "Priority", this message will also contains fields that
1221
- /// control "fairness" mechanisms.
1222
- ///
1223
- /// The overall semantics of Priority are:
1224
- /// (more will be added here later)
1225
- /// 1. First, consider "priority_key": lower number goes first.
1226
- #[derive(Debug, Clone, Default, PartialEq)]
1227
- pub struct Priority {
1228
- /// Priority key is a positive integer from 1 to n, where smaller integers
1229
- /// correspond to higher priorities (tasks run sooner). In general, tasks in
1230
- /// a queue should be processed in close to priority order, although small
1231
- /// deviations are possible.
1232
- ///
1233
- /// The maximum priority value (minimum priority) is determined by server
1234
- /// configuration, and defaults to 5.
1235
- ///
1236
- /// The default priority is (min+max)/2. With the default max of 5 and min of
1237
- /// 1, that comes out to 3.
1238
- pub priority_key: u32,
1239
-
1240
- /// Fairness key is a short string that's used as a key for a fairness
1241
- /// balancing mechanism. It may correspond to a tenant id, or to a fixed
1242
- /// string like "high" or "low". The default is the empty string.
1243
- ///
1244
- /// The fairness mechanism attempts to dispatch tasks for a given key in
1245
- /// proportion to its weight. For example, using a thousand distinct tenant
1246
- /// ids, each with a weight of 1.0 (the default) will result in each tenant
1247
- /// getting a roughly equal share of task dispatch throughput.
1248
- ///
1249
- /// (Note: this does not imply equal share of worker capacity! Fairness
1250
- /// decisions are made based on queue statistics, not
1251
- /// current worker load.)
1252
- ///
1253
- /// As another example, using keys "high" and "low" with weight 9.0 and 1.0
1254
- /// respectively will prefer dispatching "high" tasks over "low" tasks at a
1255
- /// 9:1 ratio, while allowing either key to use all worker capacity if the
1256
- /// other is not present.
1257
- ///
1258
- /// All fairness mechanisms, including rate limits, are best-effort and
1259
- /// probabilistic. The results may not match what a "perfect" algorithm with
1260
- /// infinite resources would produce. The more unique keys are used, the less
1261
- /// accurate the results will be.
1262
- ///
1263
- /// Fairness keys are limited to 64 bytes.
1264
- pub fairness_key: String,
1265
-
1266
- /// Fairness weight for a task can come from multiple sources for
1267
- /// flexibility. From highest to lowest precedence:
1268
- /// 1. Weights for a small set of keys can be overridden in task queue
1269
- /// configuration with an API.
1270
- /// 2. It can be attached to the workflow/activity in this field.
1271
- /// 3. The default weight of 1.0 will be used.
1272
- ///
1273
- /// Weight values are clamped by the server to the range [0.001, 1000].
1274
- pub fairness_weight: f32,
948
+ /// If the query includes a group-by clause, `groups` will contain the aggregated
949
+ /// counts and `count` will be the sum of all group counts.
950
+ #[derive(Debug, Clone)]
951
+ pub struct WorkflowExecutionCount {
952
+ count: usize,
953
+ groups: Vec<WorkflowCountAggregationGroup>,
1275
954
  }
1276
955
 
1277
- impl From<Priority> for common::v1::Priority {
1278
- fn from(priority: Priority) -> Self {
1279
- common::v1::Priority {
1280
- priority_key: priority.priority_key as i32,
1281
- fairness_key: priority.fairness_key,
1282
- fairness_weight: priority.fairness_weight,
956
+ impl WorkflowExecutionCount {
957
+ pub(crate) fn from_response(resp: CountWorkflowExecutionsResponse) -> Self {
958
+ Self {
959
+ count: resp.count as usize,
960
+ groups: resp
961
+ .groups
962
+ .into_iter()
963
+ .map(WorkflowCountAggregationGroup::from_proto)
964
+ .collect(),
1283
965
  }
1284
966
  }
967
+
968
+ /// The approximate number of workflows matching the query.
969
+ /// If grouping was applied, this is the sum of all group counts.
970
+ pub fn count(&self) -> usize {
971
+ self.count
972
+ }
973
+
974
+ /// The groups if the query had a group-by clause, or empty if not.
975
+ pub fn groups(&self) -> &[WorkflowCountAggregationGroup] {
976
+ &self.groups
977
+ }
978
+ }
979
+
980
+ /// Aggregation group from a workflow count query with a group-by clause.
981
+ #[derive(Debug, Clone)]
982
+ pub struct WorkflowCountAggregationGroup {
983
+ group_values: Vec<Payload>,
984
+ count: usize,
1285
985
  }
1286
986
 
1287
- impl From<common::v1::Priority> for Priority {
1288
- fn from(priority: common::v1::Priority) -> Self {
987
+ impl WorkflowCountAggregationGroup {
988
+ fn from_proto(proto: count_workflow_executions_response::AggregationGroup) -> Self {
1289
989
  Self {
1290
- priority_key: priority.priority_key as u32,
1291
- fairness_key: priority.fairness_key,
1292
- fairness_weight: priority.fairness_weight,
990
+ group_values: proto.group_values,
991
+ count: proto.count as usize,
1293
992
  }
1294
993
  }
994
+
995
+ /// The search attribute values for this group.
996
+ pub fn group_values(&self) -> &[Payload] {
997
+ &self.group_values
998
+ }
999
+
1000
+ /// The approximate number of workflows matching for this group.
1001
+ pub fn count(&self) -> usize {
1002
+ self.count
1003
+ }
1295
1004
  }
1296
1005
 
1297
- #[async_trait::async_trait]
1298
1006
  impl<T> WorkflowClientTrait for T
1299
1007
  where
1300
1008
  T: WorkflowService + NamespacedClient + Clone + Send + Sync + 'static,
1301
1009
  {
1302
- async fn start_workflow(
1010
+ async fn start_workflow<W>(
1303
1011
  &self,
1304
- input: Vec<Payload>,
1305
- task_queue: String,
1306
- workflow_id: String,
1307
- workflow_type: String,
1308
- request_id: Option<String>,
1309
- options: WorkflowOptions,
1310
- ) -> Result<StartWorkflowExecutionResponse> {
1311
- Ok(self
1312
- .clone()
1313
- .start_workflow_execution(
1314
- StartWorkflowExecutionRequest {
1315
- namespace: self.namespace(),
1316
- input: input.into_payloads(),
1317
- workflow_id,
1012
+ workflow: W,
1013
+ input: W::Input,
1014
+ options: WorkflowStartOptions,
1015
+ ) -> Result<WorkflowHandle<Self, W>, WorkflowStartError>
1016
+ where
1017
+ W: WorkflowDefinition,
1018
+ W::Input: Send,
1019
+ {
1020
+ let payloads = self
1021
+ .data_converter()
1022
+ .to_payloads(&SerializationContextData::Workflow, &input)
1023
+ .await?;
1024
+ let namespace = self.namespace();
1025
+ let workflow_id = options.workflow_id.clone();
1026
+ let task_queue_name = options.task_queue.clone();
1027
+
1028
+ let run_id = if let Some(start_signal) = options.start_signal {
1029
+ // Use signal-with-start when a start_signal is provided
1030
+ let res = WorkflowService::signal_with_start_workflow_execution(
1031
+ &mut self.clone(),
1032
+ SignalWithStartWorkflowExecutionRequest {
1033
+ namespace: namespace.clone(),
1034
+ workflow_id: workflow_id.clone(),
1318
1035
  workflow_type: Some(WorkflowType {
1319
- name: workflow_type,
1036
+ name: workflow.name().to_string(),
1320
1037
  }),
1321
1038
  task_queue: Some(TaskQueue {
1322
- name: task_queue,
1323
- kind: TaskQueueKind::Unspecified as i32,
1039
+ name: task_queue_name,
1040
+ kind: TaskQueueKind::Normal as i32,
1324
1041
  normal_name: "".to_string(),
1325
1042
  }),
1326
- request_id: request_id.unwrap_or_else(|| Uuid::new_v4().to_string()),
1043
+ input: payloads.into_payloads(),
1044
+ signal_name: start_signal.signal_name,
1045
+ signal_input: start_signal.input,
1046
+ identity: self.identity(),
1047
+ request_id: Uuid::new_v4().to_string(),
1327
1048
  workflow_id_reuse_policy: options.id_reuse_policy as i32,
1328
1049
  workflow_id_conflict_policy: options.id_conflict_policy as i32,
1329
1050
  workflow_execution_timeout: options
@@ -1333,482 +1054,203 @@ where
1333
1054
  workflow_task_timeout: options.task_timeout.and_then(|d| d.try_into().ok()),
1334
1055
  search_attributes: options.search_attributes.map(|d| d.into()),
1335
1056
  cron_schedule: options.cron_schedule.unwrap_or_default(),
1336
- request_eager_execution: options.enable_eager_workflow_start,
1337
- retry_policy: options.retry_policy,
1338
- links: options.links,
1339
- completion_callbacks: options.completion_callbacks,
1340
- priority: options.priority.map(Into::into),
1057
+ header: options.header.or(start_signal.header),
1341
1058
  ..Default::default()
1342
1059
  }
1343
1060
  .into_request(),
1344
1061
  )
1345
1062
  .await?
1346
- .into_inner())
1347
- }
1348
-
1349
- async fn reset_sticky_task_queue(
1350
- &self,
1351
- workflow_id: String,
1352
- run_id: String,
1353
- ) -> Result<ResetStickyTaskQueueResponse> {
1354
- let request = ResetStickyTaskQueueRequest {
1355
- namespace: self.namespace(),
1356
- execution: Some(WorkflowExecution {
1357
- workflow_id,
1358
- run_id,
1359
- }),
1063
+ .into_inner();
1064
+ res.run_id
1065
+ } else {
1066
+ // Normal start workflow
1067
+ let res = self
1068
+ .clone()
1069
+ .start_workflow_execution(
1070
+ StartWorkflowExecutionRequest {
1071
+ namespace: namespace.clone(),
1072
+ input: payloads.into_payloads(),
1073
+ workflow_id: workflow_id.clone(),
1074
+ workflow_type: Some(WorkflowType {
1075
+ name: workflow.name().to_string(),
1076
+ }),
1077
+ task_queue: Some(TaskQueue {
1078
+ name: task_queue_name,
1079
+ kind: TaskQueueKind::Unspecified as i32,
1080
+ normal_name: "".to_string(),
1081
+ }),
1082
+ request_id: Uuid::new_v4().to_string(),
1083
+ workflow_id_reuse_policy: options.id_reuse_policy as i32,
1084
+ workflow_id_conflict_policy: options.id_conflict_policy as i32,
1085
+ workflow_execution_timeout: options
1086
+ .execution_timeout
1087
+ .and_then(|d| d.try_into().ok()),
1088
+ workflow_run_timeout: options.run_timeout.and_then(|d| d.try_into().ok()),
1089
+ workflow_task_timeout: options.task_timeout.and_then(|d| d.try_into().ok()),
1090
+ search_attributes: options.search_attributes.map(|d| d.into()),
1091
+ cron_schedule: options.cron_schedule.unwrap_or_default(),
1092
+ request_eager_execution: options.enable_eager_workflow_start,
1093
+ retry_policy: options.retry_policy,
1094
+ links: options.links,
1095
+ completion_callbacks: options.completion_callbacks,
1096
+ priority: Some(options.priority.into()),
1097
+ header: options.header,
1098
+ ..Default::default()
1099
+ }
1100
+ .into_request(),
1101
+ )
1102
+ .await
1103
+ .map_err(|status| {
1104
+ if status.code() == Code::AlreadyExists {
1105
+ let run_id =
1106
+ decode_status_detail::<WorkflowExecutionAlreadyStartedFailure>(
1107
+ status.details(),
1108
+ )
1109
+ .map(|f| f.run_id);
1110
+ WorkflowStartError::AlreadyStarted {
1111
+ run_id,
1112
+ source: status,
1113
+ }
1114
+ } else {
1115
+ WorkflowStartError::Rpc(status)
1116
+ }
1117
+ })?
1118
+ .into_inner();
1119
+ res.run_id
1360
1120
  };
1361
- Ok(
1362
- WorkflowService::reset_sticky_task_queue(&mut self.clone(), request.into_request())
1363
- .await?
1364
- .into_inner(),
1365
- )
1366
- }
1367
-
1368
- async fn complete_activity_task(
1369
- &self,
1370
- task_token: TaskToken,
1371
- result: Option<Payloads>,
1372
- ) -> Result<RespondActivityTaskCompletedResponse> {
1373
- Ok(self
1374
- .clone()
1375
- .respond_activity_task_completed(
1376
- RespondActivityTaskCompletedRequest {
1377
- task_token: task_token.0,
1378
- result,
1379
- identity: self.identity(),
1380
- namespace: self.namespace(),
1381
- ..Default::default()
1382
- }
1383
- .into_request(),
1384
- )
1385
- .await?
1386
- .into_inner())
1387
- }
1388
-
1389
- async fn record_activity_heartbeat(
1390
- &self,
1391
- task_token: TaskToken,
1392
- details: Option<Payloads>,
1393
- ) -> Result<RecordActivityTaskHeartbeatResponse> {
1394
- Ok(self
1395
- .clone()
1396
- .record_activity_task_heartbeat(
1397
- RecordActivityTaskHeartbeatRequest {
1398
- task_token: task_token.0,
1399
- details,
1400
- identity: self.identity(),
1401
- namespace: self.namespace(),
1402
- }
1403
- .into_request(),
1404
- )
1405
- .await?
1406
- .into_inner())
1407
- }
1408
-
1409
- async fn cancel_activity_task(
1410
- &self,
1411
- task_token: TaskToken,
1412
- details: Option<Payloads>,
1413
- ) -> Result<RespondActivityTaskCanceledResponse> {
1414
- Ok(self
1415
- .clone()
1416
- .respond_activity_task_canceled(
1417
- RespondActivityTaskCanceledRequest {
1418
- task_token: task_token.0,
1419
- details,
1420
- identity: self.identity(),
1421
- namespace: self.namespace(),
1422
- ..Default::default()
1423
- }
1424
- .into_request(),
1425
- )
1426
- .await?
1427
- .into_inner())
1428
- }
1429
-
1430
- async fn signal_workflow_execution(
1431
- &self,
1432
- workflow_id: String,
1433
- run_id: String,
1434
- signal_name: String,
1435
- payloads: Option<Payloads>,
1436
- request_id: Option<String>,
1437
- ) -> Result<SignalWorkflowExecutionResponse> {
1438
- Ok(WorkflowService::signal_workflow_execution(
1439
- &mut self.clone(),
1440
- SignalWorkflowExecutionRequest {
1441
- namespace: self.namespace(),
1442
- workflow_execution: Some(WorkflowExecution {
1443
- workflow_id,
1444
- run_id,
1445
- }),
1446
- signal_name,
1447
- input: payloads,
1448
- identity: self.identity(),
1449
- request_id: request_id.unwrap_or_else(|| Uuid::new_v4().to_string()),
1450
- ..Default::default()
1451
- }
1452
- .into_request(),
1453
- )
1454
- .await?
1455
- .into_inner())
1456
- }
1457
1121
 
1458
- async fn signal_with_start_workflow_execution(
1459
- &self,
1460
- options: SignalWithStartOptions,
1461
- workflow_options: WorkflowOptions,
1462
- ) -> Result<SignalWithStartWorkflowExecutionResponse> {
1463
- Ok(WorkflowService::signal_with_start_workflow_execution(
1464
- &mut self.clone(),
1465
- SignalWithStartWorkflowExecutionRequest {
1466
- namespace: self.namespace(),
1467
- workflow_id: options.workflow_id,
1468
- workflow_type: Some(WorkflowType {
1469
- name: options.workflow_type,
1470
- }),
1471
- task_queue: Some(TaskQueue {
1472
- name: options.task_queue,
1473
- kind: TaskQueueKind::Normal as i32,
1474
- normal_name: "".to_string(),
1475
- }),
1476
- input: options.input,
1477
- signal_name: options.signal_name,
1478
- signal_input: options.signal_input,
1479
- identity: self.identity(),
1480
- request_id: options
1481
- .request_id
1482
- .unwrap_or_else(|| Uuid::new_v4().to_string()),
1483
- workflow_id_reuse_policy: workflow_options.id_reuse_policy as i32,
1484
- workflow_id_conflict_policy: workflow_options.id_conflict_policy as i32,
1485
- workflow_execution_timeout: workflow_options
1486
- .execution_timeout
1487
- .and_then(|d| d.try_into().ok()),
1488
- workflow_run_timeout: workflow_options.run_timeout.and_then(|d| d.try_into().ok()),
1489
- workflow_task_timeout: workflow_options
1490
- .task_timeout
1491
- .and_then(|d| d.try_into().ok()),
1492
- search_attributes: workflow_options.search_attributes.map(|d| d.into()),
1493
- cron_schedule: workflow_options.cron_schedule.unwrap_or_default(),
1494
- header: options.signal_header,
1495
- ..Default::default()
1496
- }
1497
- .into_request(),
1498
- )
1499
- .await?
1500
- .into_inner())
1501
- }
1502
-
1503
- async fn query_workflow_execution(
1504
- &self,
1505
- workflow_id: String,
1506
- run_id: String,
1507
- query: WorkflowQuery,
1508
- ) -> Result<QueryWorkflowResponse> {
1509
- Ok(self
1510
- .clone()
1511
- .query_workflow(
1512
- QueryWorkflowRequest {
1513
- namespace: self.namespace(),
1514
- execution: Some(WorkflowExecution {
1515
- workflow_id,
1516
- run_id,
1517
- }),
1518
- query: Some(query),
1519
- query_reject_condition: 1,
1520
- }
1521
- .into_request(),
1522
- )
1523
- .await?
1524
- .into_inner())
1525
- }
1526
-
1527
- async fn describe_workflow_execution(
1528
- &self,
1529
- workflow_id: String,
1530
- run_id: Option<String>,
1531
- ) -> Result<DescribeWorkflowExecutionResponse> {
1532
- Ok(WorkflowService::describe_workflow_execution(
1533
- &mut self.clone(),
1534
- DescribeWorkflowExecutionRequest {
1535
- namespace: self.namespace(),
1536
- execution: Some(WorkflowExecution {
1537
- workflow_id,
1538
- run_id: run_id.unwrap_or_default(),
1539
- }),
1540
- }
1541
- .into_request(),
1542
- )
1543
- .await?
1544
- .into_inner())
1122
+ Ok(WorkflowHandle::new(
1123
+ self.clone(),
1124
+ WorkflowExecutionInfo {
1125
+ namespace,
1126
+ workflow_id,
1127
+ run_id: Some(run_id.clone()),
1128
+ first_execution_run_id: Some(run_id),
1129
+ },
1130
+ ))
1545
1131
  }
1546
1132
 
1547
- async fn get_workflow_execution_history(
1133
+ fn get_workflow_handle<W: WorkflowDefinition>(
1548
1134
  &self,
1549
- workflow_id: String,
1550
- run_id: Option<String>,
1551
- page_token: Vec<u8>,
1552
- ) -> Result<GetWorkflowExecutionHistoryResponse> {
1553
- Ok(WorkflowService::get_workflow_execution_history(
1554
- &mut self.clone(),
1555
- GetWorkflowExecutionHistoryRequest {
1135
+ workflow_id: impl Into<String>,
1136
+ ) -> WorkflowHandle<Self, W>
1137
+ where
1138
+ Self: Sized,
1139
+ {
1140
+ WorkflowHandle::new(
1141
+ self.clone(),
1142
+ WorkflowExecutionInfo {
1556
1143
  namespace: self.namespace(),
1557
- execution: Some(WorkflowExecution {
1558
- workflow_id,
1559
- run_id: run_id.unwrap_or_default(),
1560
- }),
1561
- next_page_token: page_token,
1562
- ..Default::default()
1563
- }
1564
- .into_request(),
1144
+ workflow_id: workflow_id.into(),
1145
+ run_id: None,
1146
+ first_execution_run_id: None,
1147
+ },
1565
1148
  )
1566
- .await?
1567
- .into_inner())
1568
1149
  }
1569
1150
 
1570
- async fn cancel_workflow_execution(
1151
+ fn list_workflows(
1571
1152
  &self,
1572
- workflow_id: String,
1573
- run_id: Option<String>,
1574
- reason: String,
1575
- request_id: Option<String>,
1576
- ) -> Result<RequestCancelWorkflowExecutionResponse> {
1577
- Ok(self
1578
- .clone()
1579
- .request_cancel_workflow_execution(
1580
- RequestCancelWorkflowExecutionRequest {
1581
- namespace: self.namespace(),
1582
- workflow_execution: Some(WorkflowExecution {
1583
- workflow_id,
1584
- run_id: run_id.unwrap_or_default(),
1585
- }),
1586
- identity: self.identity(),
1587
- request_id: request_id.unwrap_or_else(|| Uuid::new_v4().to_string()),
1588
- first_execution_run_id: "".to_string(),
1589
- reason,
1590
- links: vec![],
1153
+ query: impl Into<String>,
1154
+ opts: WorkflowListOptions,
1155
+ ) -> ListWorkflowsStream {
1156
+ let client = self.clone();
1157
+ let namespace = self.namespace();
1158
+ let query = query.into();
1159
+ let limit = opts.limit;
1160
+
1161
+ // State: (next_page_token, buffer, yielded_count, exhausted)
1162
+ let initial_state = (Vec::new(), VecDeque::new(), 0, false);
1163
+
1164
+ let stream = stream::unfold(
1165
+ initial_state,
1166
+ move |(next_page_token, mut buffer, mut yielded, exhausted)| {
1167
+ let mut client = client.clone();
1168
+ let namespace = namespace.clone();
1169
+ let query = query.clone();
1170
+
1171
+ async move {
1172
+ if let Some(l) = limit
1173
+ && yielded >= l
1174
+ {
1175
+ return None;
1176
+ }
1177
+
1178
+ if let Some(exec) = buffer.pop_front() {
1179
+ yielded += 1;
1180
+ return Some((Ok(exec), (next_page_token, buffer, yielded, exhausted)));
1181
+ }
1182
+
1183
+ if exhausted {
1184
+ return None;
1185
+ }
1186
+
1187
+ let response = WorkflowService::list_workflow_executions(
1188
+ &mut client,
1189
+ ListWorkflowExecutionsRequest {
1190
+ namespace,
1191
+ page_size: 0, // Use server default
1192
+ next_page_token: next_page_token.clone(),
1193
+ query,
1194
+ }
1195
+ .into_request(),
1196
+ )
1197
+ .await;
1198
+
1199
+ match response {
1200
+ Ok(resp) => {
1201
+ let resp = resp.into_inner();
1202
+ let new_exhausted = resp.next_page_token.is_empty();
1203
+ let new_token = resp.next_page_token;
1204
+
1205
+ buffer = resp
1206
+ .executions
1207
+ .into_iter()
1208
+ .map(WorkflowExecution::from)
1209
+ .collect();
1210
+
1211
+ if let Some(exec) = buffer.pop_front() {
1212
+ yielded += 1;
1213
+ Some((Ok(exec), (new_token, buffer, yielded, new_exhausted)))
1214
+ } else {
1215
+ None
1216
+ }
1217
+ }
1218
+ Err(e) => Some((Err(e.into()), (next_page_token, buffer, yielded, true))),
1219
+ }
1591
1220
  }
1592
- .into_request(),
1593
- )
1594
- .await?
1595
- .into_inner())
1596
- }
1597
-
1598
- async fn terminate_workflow_execution(
1599
- &self,
1600
- workflow_id: String,
1601
- run_id: Option<String>,
1602
- ) -> Result<TerminateWorkflowExecutionResponse> {
1603
- Ok(WorkflowService::terminate_workflow_execution(
1604
- &mut self.clone(),
1605
- TerminateWorkflowExecutionRequest {
1606
- namespace: self.namespace(),
1607
- workflow_execution: Some(WorkflowExecution {
1608
- workflow_id,
1609
- run_id: run_id.unwrap_or_default(),
1610
- }),
1611
- reason: "".to_string(),
1612
- details: None,
1613
- identity: self.identity(),
1614
- first_execution_run_id: "".to_string(),
1615
- links: vec![],
1616
- }
1617
- .into_request(),
1618
- )
1619
- .await?
1620
- .into_inner())
1621
- }
1622
-
1623
- async fn register_namespace(
1624
- &self,
1625
- options: RegisterNamespaceOptions,
1626
- ) -> Result<RegisterNamespaceResponse> {
1627
- let req = Into::<RegisterNamespaceRequest>::into(options);
1628
- Ok(
1629
- WorkflowService::register_namespace(&mut self.clone(), req.into_request())
1630
- .await?
1631
- .into_inner(),
1632
- )
1633
- }
1634
-
1635
- async fn list_namespaces(&self) -> Result<ListNamespacesResponse> {
1636
- Ok(WorkflowService::list_namespaces(
1637
- &mut self.clone(),
1638
- ListNamespacesRequest::default().into_request(),
1639
- )
1640
- .await?
1641
- .into_inner())
1642
- }
1643
-
1644
- async fn describe_namespace(&self, namespace: Namespace) -> Result<DescribeNamespaceResponse> {
1645
- Ok(WorkflowService::describe_namespace(
1646
- &mut self.clone(),
1647
- namespace.into_describe_namespace_request().into_request(),
1648
- )
1649
- .await?
1650
- .into_inner())
1651
- }
1652
-
1653
- async fn list_open_workflow_executions(
1654
- &self,
1655
- maximum_page_size: i32,
1656
- next_page_token: Vec<u8>,
1657
- start_time_filter: Option<StartTimeFilter>,
1658
- filters: Option<list_open_workflow_executions_request::Filters>,
1659
- ) -> Result<ListOpenWorkflowExecutionsResponse> {
1660
- Ok(WorkflowService::list_open_workflow_executions(
1661
- &mut self.clone(),
1662
- ListOpenWorkflowExecutionsRequest {
1663
- namespace: self.namespace(),
1664
- maximum_page_size,
1665
- next_page_token,
1666
- start_time_filter,
1667
- filters,
1668
- }
1669
- .into_request(),
1670
- )
1671
- .await?
1672
- .into_inner())
1673
- }
1674
-
1675
- async fn list_closed_workflow_executions(
1676
- &self,
1677
- maximum_page_size: i32,
1678
- next_page_token: Vec<u8>,
1679
- start_time_filter: Option<StartTimeFilter>,
1680
- filters: Option<list_closed_workflow_executions_request::Filters>,
1681
- ) -> Result<ListClosedWorkflowExecutionsResponse> {
1682
- Ok(WorkflowService::list_closed_workflow_executions(
1683
- &mut self.clone(),
1684
- ListClosedWorkflowExecutionsRequest {
1685
- namespace: self.namespace(),
1686
- maximum_page_size,
1687
- next_page_token,
1688
- start_time_filter,
1689
- filters,
1690
- }
1691
- .into_request(),
1692
- )
1693
- .await?
1694
- .into_inner())
1695
- }
1221
+ },
1222
+ );
1696
1223
 
1697
- async fn list_workflow_executions(
1698
- &self,
1699
- page_size: i32,
1700
- next_page_token: Vec<u8>,
1701
- query: String,
1702
- ) -> Result<ListWorkflowExecutionsResponse> {
1703
- Ok(WorkflowService::list_workflow_executions(
1704
- &mut self.clone(),
1705
- ListWorkflowExecutionsRequest {
1706
- namespace: self.namespace(),
1707
- page_size,
1708
- next_page_token,
1709
- query,
1710
- }
1711
- .into_request(),
1712
- )
1713
- .await?
1714
- .into_inner())
1224
+ ListWorkflowsStream::new(Box::pin(stream))
1715
1225
  }
1716
1226
 
1717
- async fn list_archived_workflow_executions(
1227
+ async fn count_workflows(
1718
1228
  &self,
1719
- page_size: i32,
1720
- next_page_token: Vec<u8>,
1721
- query: String,
1722
- ) -> Result<ListArchivedWorkflowExecutionsResponse> {
1723
- Ok(WorkflowService::list_archived_workflow_executions(
1229
+ query: impl Into<String>,
1230
+ _opts: WorkflowCountOptions,
1231
+ ) -> Result<WorkflowExecutionCount, ClientError> {
1232
+ let resp = WorkflowService::count_workflow_executions(
1724
1233
  &mut self.clone(),
1725
- ListArchivedWorkflowExecutionsRequest {
1234
+ CountWorkflowExecutionsRequest {
1726
1235
  namespace: self.namespace(),
1727
- page_size,
1728
- next_page_token,
1729
- query,
1236
+ query: query.into(),
1730
1237
  }
1731
1238
  .into_request(),
1732
1239
  )
1733
1240
  .await?
1734
- .into_inner())
1735
- }
1241
+ .into_inner();
1736
1242
 
1737
- async fn get_search_attributes(&self) -> Result<GetSearchAttributesResponse> {
1738
- Ok(WorkflowService::get_search_attributes(
1739
- &mut self.clone(),
1740
- GetSearchAttributesRequest {}.into_request(),
1741
- )
1742
- .await?
1743
- .into_inner())
1243
+ Ok(WorkflowExecutionCount::from_response(resp))
1744
1244
  }
1745
1245
 
1746
- async fn update_workflow_execution(
1747
- &self,
1748
- workflow_id: String,
1749
- run_id: String,
1750
- name: String,
1751
- wait_policy: update::v1::WaitPolicy,
1752
- args: Option<Payloads>,
1753
- ) -> Result<UpdateWorkflowExecutionResponse> {
1754
- Ok(WorkflowService::update_workflow_execution(
1755
- &mut self.clone(),
1756
- UpdateWorkflowExecutionRequest {
1757
- namespace: self.namespace(),
1758
- workflow_execution: Some(WorkflowExecution {
1759
- workflow_id,
1760
- run_id,
1761
- }),
1762
- wait_policy: Some(wait_policy),
1763
- request: Some(update::v1::Request {
1764
- meta: Some(update::v1::Meta {
1765
- update_id: "".into(),
1766
- identity: self.identity(),
1767
- }),
1768
- input: Some(update::v1::Input {
1769
- header: None,
1770
- name,
1771
- args,
1772
- }),
1773
- }),
1774
- ..Default::default()
1775
- }
1776
- .into_request(),
1777
- )
1778
- .await?
1779
- .into_inner())
1780
- }
1781
- }
1782
-
1783
- mod sealed {
1784
- use crate::{WorkflowClientTrait, WorkflowService};
1785
- pub trait WfHandleClient: WorkflowClientTrait + WorkflowService {}
1786
- impl<T> WfHandleClient for T where T: WorkflowClientTrait + WorkflowService {}
1787
- }
1788
-
1789
- /// Additional methods for workflow clients
1790
- pub trait WfClientExt: WfHandleClient + Sized + Clone {
1791
- /// Create an untyped handle for a workflow execution, which can be used to do things like
1792
- /// wait for that workflow's result. `run_id` may be left blank to target the latest run.
1793
- fn get_untyped_workflow_handle(
1794
- &self,
1795
- workflow_id: impl Into<String>,
1796
- run_id: impl Into<String>,
1797
- ) -> UntypedWorkflowHandle<Self> {
1798
- let rid = run_id.into();
1799
- UntypedWorkflowHandle::new(
1800
- self.clone(),
1801
- WorkflowExecutionInfo {
1802
- namespace: self.namespace(),
1803
- workflow_id: workflow_id.into(),
1804
- run_id: if rid.is_empty() { None } else { Some(rid) },
1805
- },
1806
- )
1246
+ fn get_async_activity_handle(&self, identifier: ActivityIdentifier) -> AsyncActivityHandle<Self>
1247
+ where
1248
+ Self: Sized,
1249
+ {
1250
+ AsyncActivityHandle::new(self.clone(), identifier)
1807
1251
  }
1808
1252
  }
1809
1253
 
1810
- impl<T> WfClientExt for T where T: WfHandleClient + Clone + Sized {}
1811
-
1812
1254
  macro_rules! dbg_panic {
1813
1255
  ($($arg:tt)*) => {
1814
1256
  use tracing::error;
@@ -1822,16 +1264,10 @@ pub(crate) use dbg_panic;
1822
1264
  mod tests {
1823
1265
  use super::*;
1824
1266
  use tonic::metadata::Ascii;
1267
+ use url::Url;
1825
1268
 
1826
1269
  #[test]
1827
1270
  fn applies_headers() {
1828
- let opts = ClientOptions::builder()
1829
- .identity("enchicat".to_string())
1830
- .target_url(Url::parse("https://smolkitty").unwrap())
1831
- .client_name("cute-kitty".to_string())
1832
- .client_version("0.1.0".to_string())
1833
- .build();
1834
-
1835
1271
  // Initial header set
1836
1272
  let headers = Arc::new(RwLock::new(ClientHeaders {
1837
1273
  user_headers: HashMap::new(),
@@ -1847,7 +1283,8 @@ mod tests {
1847
1283
  vec![1, 2, 3].try_into().unwrap(),
1848
1284
  );
1849
1285
  let mut interceptor = ServiceCallInterceptor {
1850
- opts,
1286
+ client_name: "cute-kitty".to_string(),
1287
+ client_version: "0.1.0".to_string(),
1851
1288
  headers: headers.clone(),
1852
1289
  };
1853
1290
 
@@ -1959,9 +1396,8 @@ mod tests {
1959
1396
 
1960
1397
  #[test]
1961
1398
  fn keep_alive_defaults() {
1962
- let opts = ClientOptions::builder()
1399
+ let opts = ConnectionOptions::new(Url::parse("https://smolkitty").unwrap())
1963
1400
  .identity("enchicat".to_string())
1964
- .target_url(Url::parse("https://smolkitty").unwrap())
1965
1401
  .client_name("cute-kitty".to_string())
1966
1402
  .client_version("0.1.0".to_string())
1967
1403
  .build();
@@ -1975,9 +1411,8 @@ mod tests {
1975
1411
  );
1976
1412
 
1977
1413
  // Can be explicitly set to None
1978
- let opts = ClientOptions::builder()
1414
+ let opts = ConnectionOptions::new(Url::parse("https://smolkitty").unwrap())
1979
1415
  .identity("enchicat".to_string())
1980
- .target_url(Url::parse("https://smolkitty").unwrap())
1981
1416
  .client_name("cute-kitty".to_string())
1982
1417
  .client_version("0.1.0".to_string())
1983
1418
  .keep_alive(None)
@@ -1985,4 +1420,163 @@ mod tests {
1985
1420
  dbg!(&opts.keep_alive);
1986
1421
  assert!(opts.keep_alive.is_none());
1987
1422
  }
1423
+
1424
+ mod list_workflows_tests {
1425
+ use super::*;
1426
+ use futures_util::{FutureExt, StreamExt};
1427
+ use std::sync::atomic::{AtomicUsize, Ordering};
1428
+ use temporalio_common::protos::temporal::api::common::v1::WorkflowExecution as ProtoWorkflowExecution;
1429
+ use tonic::{Request, Response};
1430
+
1431
+ #[derive(Clone)]
1432
+ struct MockListWorkflowsClient {
1433
+ call_count: Arc<AtomicUsize>,
1434
+ // Returns this many workflows per page
1435
+ page_size: usize,
1436
+ // Total workflows available
1437
+ total_workflows: usize,
1438
+ }
1439
+
1440
+ impl NamespacedClient for MockListWorkflowsClient {
1441
+ fn namespace(&self) -> String {
1442
+ "test-namespace".to_string()
1443
+ }
1444
+ fn identity(&self) -> String {
1445
+ "test-identity".to_string()
1446
+ }
1447
+ }
1448
+
1449
+ impl WorkflowService for MockListWorkflowsClient {
1450
+ fn list_workflow_executions(
1451
+ &mut self,
1452
+ request: Request<ListWorkflowExecutionsRequest>,
1453
+ ) -> futures_util::future::BoxFuture<
1454
+ '_,
1455
+ Result<Response<ListWorkflowExecutionsResponse>, tonic::Status>,
1456
+ > {
1457
+ self.call_count.fetch_add(1, Ordering::SeqCst);
1458
+ let req = request.into_inner();
1459
+
1460
+ // Determine offset from page token
1461
+ let offset: usize = if req.next_page_token.is_empty() {
1462
+ 0
1463
+ } else {
1464
+ String::from_utf8(req.next_page_token)
1465
+ .unwrap()
1466
+ .parse()
1467
+ .unwrap()
1468
+ };
1469
+
1470
+ let remaining = self.total_workflows.saturating_sub(offset);
1471
+ let count = remaining.min(self.page_size);
1472
+ let new_offset = offset + count;
1473
+
1474
+ let executions: Vec<_> = (offset..offset + count)
1475
+ .map(|i| workflow::WorkflowExecutionInfo {
1476
+ execution: Some(ProtoWorkflowExecution {
1477
+ workflow_id: format!("wf-{i}"),
1478
+ run_id: format!("run-{i}"),
1479
+ }),
1480
+ r#type: Some(WorkflowType {
1481
+ name: "TestWorkflow".to_string(),
1482
+ }),
1483
+ task_queue: "test-queue".to_string(),
1484
+ ..Default::default()
1485
+ })
1486
+ .collect();
1487
+
1488
+ let next_page_token = if new_offset < self.total_workflows {
1489
+ new_offset.to_string().into_bytes()
1490
+ } else {
1491
+ vec![]
1492
+ };
1493
+
1494
+ async move {
1495
+ Ok(Response::new(ListWorkflowExecutionsResponse {
1496
+ executions,
1497
+ next_page_token,
1498
+ }))
1499
+ }
1500
+ .boxed()
1501
+ }
1502
+ }
1503
+
1504
+ #[tokio::test]
1505
+ async fn list_workflows_paginates_through_all_results() {
1506
+ let call_count = Arc::new(AtomicUsize::new(0));
1507
+ let client = MockListWorkflowsClient {
1508
+ call_count: call_count.clone(),
1509
+ page_size: 3,
1510
+ total_workflows: 10,
1511
+ };
1512
+
1513
+ let stream = client.list_workflows("", WorkflowListOptions::default());
1514
+ let results: Vec<_> = stream.collect().await;
1515
+
1516
+ assert_eq!(results.len(), 10);
1517
+ for (i, result) in results.iter().enumerate() {
1518
+ let wf = result.as_ref().unwrap();
1519
+ assert_eq!(wf.id(), format!("wf-{i}"));
1520
+ assert_eq!(wf.run_id(), format!("run-{i}"));
1521
+ }
1522
+ // Should have made 4 calls: pages of 3, 3, 3, 1
1523
+ assert_eq!(call_count.load(Ordering::SeqCst), 4);
1524
+ }
1525
+
1526
+ #[tokio::test]
1527
+ async fn list_workflows_respects_limit() {
1528
+ let call_count = Arc::new(AtomicUsize::new(0));
1529
+ let client = MockListWorkflowsClient {
1530
+ call_count: call_count.clone(),
1531
+ page_size: 3,
1532
+ total_workflows: 10,
1533
+ };
1534
+
1535
+ let opts = WorkflowListOptions::builder().limit(5).build();
1536
+ let stream = client.list_workflows("", opts);
1537
+ let results: Vec<_> = stream.collect().await;
1538
+
1539
+ assert_eq!(results.len(), 5);
1540
+ for (i, result) in results.iter().enumerate() {
1541
+ let wf = result.as_ref().unwrap();
1542
+ assert_eq!(wf.id(), format!("wf-{i}"));
1543
+ }
1544
+ // Should have made 2 calls: 1 page of 3, then 2 more from next page
1545
+ assert_eq!(call_count.load(Ordering::SeqCst), 2);
1546
+ }
1547
+
1548
+ #[tokio::test]
1549
+ async fn list_workflows_limit_less_than_page_size() {
1550
+ let call_count = Arc::new(AtomicUsize::new(0));
1551
+ let client = MockListWorkflowsClient {
1552
+ call_count: call_count.clone(),
1553
+ page_size: 10,
1554
+ total_workflows: 100,
1555
+ };
1556
+
1557
+ let opts = WorkflowListOptions::builder().limit(3).build();
1558
+ let stream = client.list_workflows("", opts);
1559
+ let results: Vec<_> = stream.collect().await;
1560
+
1561
+ assert_eq!(results.len(), 3);
1562
+ // Only 1 call needed since limit < page_size
1563
+ assert_eq!(call_count.load(Ordering::SeqCst), 1);
1564
+ }
1565
+
1566
+ #[tokio::test]
1567
+ async fn list_workflows_empty_results() {
1568
+ let call_count = Arc::new(AtomicUsize::new(0));
1569
+ let client = MockListWorkflowsClient {
1570
+ call_count: call_count.clone(),
1571
+ page_size: 10,
1572
+ total_workflows: 0,
1573
+ };
1574
+
1575
+ let stream = client.list_workflows("", WorkflowListOptions::default());
1576
+ let results: Vec<_> = stream.collect().await;
1577
+
1578
+ assert_eq!(results.len(), 0);
1579
+ assert_eq!(call_count.load(Ordering::SeqCst), 1);
1580
+ }
1581
+ }
1988
1582
  }