Skip to main content

aion_client/
transcript.rs

1//! Typed client subscription for one activity-attempt transcript stream.
2//!
3//! This is the sole caller-side implementation of the transcript WebSocket
4//! contract. Consumers receive canonical activity events, plus an explicit lag
5//! item that tells them to reattach from their last durable `store_seq`.
6
7use std::pin::Pin;
8
9use aion_core::{ActivityEvent, ActivityId, RunId, WorkflowId};
10use aion_proto::{ProtoActivityId, ProtoRunId, ProtoWorkflowId, TranscriptSubscription};
11use futures::Stream;
12
13use crate::{Client, ClientError};
14
15/// Boxed stream returned by [`Client::subscribe_transcript`].
16pub type TranscriptStream =
17    Pin<Box<dyn Stream<Item = Result<TranscriptStreamItem, ClientError>> + Send>>;
18
19/// One decoded server frame on a transcript subscription.
20#[derive(Clone, Debug, PartialEq)]
21pub enum TranscriptStreamItem {
22    /// One canonical activity transcript event.
23    Event(Box<ActivityEvent>),
24    /// The server's per-subscription broadcast receiver lagged. The caller can
25    /// recover this leg by reattaching with its last applied durable sequence.
26    Lagged {
27        /// Number of live broadcast records skipped before the socket closed.
28        skipped: u64,
29    },
30}
31
32/// Full identity and optional durable resume cursor for one transcript stream.
33#[derive(Clone, Debug, PartialEq, Eq)]
34pub struct TranscriptTarget {
35    /// Workflow containing the activity.
36    pub workflow_id: WorkflowId,
37    /// Concrete workflow generation.
38    pub run_id: RunId,
39    /// Activity within the run.
40    pub activity_id: ActivityId,
41    /// Attempt within the activity.
42    pub attempt: u32,
43    /// Highest durable `store_seq` already applied. `None` requests the full
44    /// retained splice; ephemeral events do not advance this cursor.
45    pub after_seq: Option<u64>,
46}
47
48impl TranscriptTarget {
49    fn subscription(self, namespace: &str) -> TranscriptSubscription {
50        TranscriptSubscription {
51            namespace: namespace.to_owned(),
52            workflow_id: Some(ProtoWorkflowId::from(self.workflow_id)),
53            run_id: Some(ProtoRunId::from(self.run_id)),
54            activity_id: Some(ProtoActivityId::from(self.activity_id)),
55            attempt: self.attempt,
56            after_seq: self.after_seq,
57        }
58    }
59}
60
61impl Client {
62    /// Opens one typed transcript WebSocket subscription.
63    ///
64    /// Event frames and lag frames are decoded strictly. An unknown kind,
65    /// unknown field, malformed body, terminal wire error, or abnormal socket
66    /// close is returned as a named [`ClientError`]; no frame is discarded.
67    /// A [`TranscriptStreamItem::Lagged`] item is recoverable per leg: callers
68    /// should announce it and open a fresh subscription with their last applied
69    /// `store_seq` as [`TranscriptTarget::after_seq`].
70    ///
71    /// # Errors
72    ///
73    /// Returns [`ClientError::InvalidArgument`] for a missing/invalid stream
74    /// endpoint, [`ClientError::Unauthenticated`] for a rejected upgrade, or
75    /// [`ClientError::Unavailable`] when the socket cannot be established.
76    pub async fn subscribe_transcript(
77        &self,
78        target: TranscriptTarget,
79    ) -> Result<TranscriptStream, ClientError> {
80        crate::transport::transcript_ws::open(&self.config, target.subscription(self.namespace()))
81            .await
82    }
83}