Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 141 additions & 0 deletions crates/client/src/activity.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
mod activity_execution_info;
mod activity_handle;

use crate::errors::ClientError;
pub use activity_execution_info::{
ActivityExecutionDescription, ActivityExecutionInfo, ActivityExecutionInfoLike,
ActivityExecutionStatus, PendingActivityState,
};
pub use activity_handle::ActivityHandle;
use futures_util::{Stream, StreamExt};
use std::{
pin::Pin,
task::{Context, Poll},
};
use temporalio_common::{
protos::temporal::api::{
activity::v1::ActivityExecutionListInfo,
workflowservice::v1::{
CountActivityExecutionsResponse, count_activity_executions_response,
},
},
search_attributes::{SearchAttributeError, SearchAttributeValue},
};

/// A stream of activity executions from a list query.
/// Internally paginates through results from the server.
pub struct ListActivitiesStream {
inner: Pin<Box<dyn Stream<Item = Result<Vec<ActivityExecutionListInfo>, ClientError>> + Send>>,
buffer: <Vec<ActivityExecutionListInfo> as IntoIterator>::IntoIter,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this could just be stored as a Vec more simply?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

buffer is stored as an iterator so that poll_next can take the next element efficiently. Storing as a reversed Vec or a VecDeque would require copying out of the original Vec inside the response - using into_iter avoids it.

@Sushisource Sushisource Jul 28, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that's right. You just pop_front() the VecDeque in poll_next(). No copy involved. Next stream element just becomes self.buffer = items.into(); (which is constant time and doesn't reallocate)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't know the last part. Going to change to VecDeque.

}

impl ListActivitiesStream {
pub(crate) fn new(
stream: impl Stream<Item = Result<Vec<ActivityExecutionListInfo>, ClientError>> + Send + 'static,
) -> Self {
Self {
inner: Box::pin(stream),
buffer: vec![].into_iter(),
}
}
}

impl Stream for ListActivitiesStream {
type Item = Result<ActivityExecutionInfo, ClientError>;

fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
loop {
if let Some(info) = self.buffer.next() {
return Poll::Ready(Some(Ok(info.into())));
}
match self.inner.poll_next_unpin(cx) {
Poll::Ready(Some(Ok(items))) => {
self.buffer = items.into_iter();
}
Poll::Ready(Some(Err(e))) => {
return Poll::Ready(Some(Err(e)));
}
Poll::Ready(None) => {
return Poll::Ready(None);
}
Poll::Pending => {
return Poll::Pending;
}
}
}
}
}

/// Result of an activity count operation.
///
/// If the query includes a group-by clause, `groups` will contain the aggregated
/// counts and `count` will be the sum of all group counts.
#[derive(Debug, Clone)]
pub struct ActivityExecutionCount {
count: usize,
groups: Vec<ActivityExecutionCountAggregationGroup>,
}

impl ActivityExecutionCount {
pub(crate) fn from_response(resp: CountActivityExecutionsResponse) -> Self {
Self {
count: resp.count as usize,
groups: resp
.groups
.into_iter()
.map(ActivityExecutionCountAggregationGroup::from_proto)
.collect(),
}
}

/// The approximate number of activities matching the query.
/// If grouping was applied, this is the sum of all group counts.
pub fn count(&self) -> usize {
self.count
}

/// The groups if the query had a group-by clause, or empty if not.
pub fn groups(&self) -> &[ActivityExecutionCountAggregationGroup] {
&self.groups
}
}

/// Aggregation group from an activity count query with a group-by clause.
#[derive(Debug, Clone)]
pub struct ActivityExecutionCountAggregationGroup {
raw: count_activity_executions_response::AggregationGroup,
}

impl ActivityExecutionCountAggregationGroup {
fn from_proto(proto: count_activity_executions_response::AggregationGroup) -> Self {
Self { raw: proto }
}

/// Retrieve a typed group value at `index`.
///
/// Returns `None` if the index is out of bounds or deserialization fails.
/// Use [`Self::try_get`] for explicit error handling.
pub fn get<T: SearchAttributeValue>(&self, index: usize) -> Option<T> {
self.try_get(index).ok().flatten()
}

/// Retrieve a typed group value at `index`, preserving deserialization
/// errors.
///
/// Returns `Ok(None)` if the index is out of bounds and `Err` if the
/// payload cannot be deserialized.
pub fn try_get<T: SearchAttributeValue>(
&self,
index: usize,
) -> Result<Option<T>, SearchAttributeError> {
match self.raw.group_values.get(index) {
Some(payload) => T::from_search_attribute_payload(payload).map(Some),
None => Ok(None),
}
}

/// The approximate number of workflows matching for this group.
pub fn count(&self) -> usize {
self.raw.count as usize
}
}
Loading
Loading