aion_server/stream/
transcript_stream.rs1use aion_core::ActivityId;
30use aion_proto::{
31 PerWorkflowSubscription, ProtoWorkflowId, StreamedActivityEvent, TranscriptSubscription,
32 WireError,
33};
34use aion_store::ActivityStreamKey;
35use axum::extract::ws::{CloseFrame, Message, WebSocket, close_code};
36use futures::{SinkExt, StreamExt};
37
38use crate::activity_publisher::TranscriptStreamLagged;
39use crate::error::ServerError;
40use crate::namespace::{CallerIdentity, NamespaceOperation, SubscriptionScope, WorkflowTarget};
41use crate::state::ServerState;
42
43pub async fn serve_transcript_socket(
57 mut socket: WebSocket,
58 state: &ServerState,
59 caller: &CallerIdentity,
60 subscription: &TranscriptSubscription,
61) -> Result<(), ServerError> {
62 let key = match authorize_transcript(state, caller, subscription).await {
63 Ok(key) => key,
64 Err(error) => {
65 super::socket::send_wire_error(&mut socket, &error.to_wire_error()).await?;
66 return Err(error);
67 }
68 };
69
70 let publisher = state.transcript_publisher();
71 let mut live = publisher.subscribe(key.clone(), subscription.after_seq);
75
76 let from_seq = subscription
80 .after_seq
81 .map_or(0, |seq| seq.saturating_add(1));
82 let replay = publisher
83 .replay_from(&key, from_seq)
84 .await
85 .map_err(ServerError::from)?;
86 for record in replay {
87 if send_activity_frame(&mut socket, record.event)
88 .await?
89 .is_break()
90 {
91 return Ok(());
92 }
93 }
94
95 let (mut socket_tx, mut socket_rx) = socket.split();
96 loop {
97 tokio::select! {
98 client_message = socket_rx.next() => {
99 match client_message {
100 Some(Ok(Message::Close(_))) | None => {
105 return send_normal_close(&mut socket_tx).await;
106 }
107 Some(Ok(_other)) => {}
108 Some(Err(_error)) => return Ok(()),
109 }
110 }
111 item = live.next() => {
112 match item {
113 Some(Ok(event)) => {
114 if forward_live_frame(&mut socket_tx, event).await?.is_break() {
115 return Ok(());
116 }
117 }
118 Some(Err(TranscriptStreamLagged { skipped })) => {
119 return deliver_transcript_terminal(&mut socket_tx, skipped).await;
120 }
121 None => return send_normal_close(&mut socket_tx).await,
122 }
123 }
124 }
125 }
126}
127
128async fn authorize_transcript(
135 state: &ServerState,
136 caller: &CallerIdentity,
137 subscription: &TranscriptSubscription,
138) -> Result<ActivityStreamKey, ServerError> {
139 let workflow_id = decode_workflow_id(subscription.workflow_id.as_ref())?;
140 let activity_id = decode_activity_id(subscription)?;
141 gate_transcript_workflow(state, caller, &subscription.namespace, &workflow_id).await?;
142 Ok(ActivityStreamKey::new(
143 workflow_id,
144 activity_id,
145 subscription.attempt,
146 ))
147}
148
149pub(crate) async fn gate_transcript_workflow(
154 state: &ServerState,
155 caller: &CallerIdentity,
156 namespace: &str,
157 workflow_id: &aion_core::WorkflowId,
158) -> Result<(), ServerError> {
159 let per_workflow = PerWorkflowSubscription {
160 namespace: namespace.to_owned(),
161 workflow_id: Some(ProtoWorkflowId::from(workflow_id.clone())),
162 resume_from_seq: None,
163 };
164 let target = WorkflowTarget::workflow(workflow_id);
165 let scope = SubscriptionScope::PerWorkflow(&per_workflow, target);
166 let filter = aion::EventFilter {
167 workflow_id: Some(workflow_id.clone()),
168 ..aion::EventFilter::default()
169 };
170 let operation = NamespaceOperation::subscribe(scope, &filter);
171 let scoped = state.namespace_guard().scope(caller, &operation).await?;
175 drop(scoped);
176 Ok(())
177}
178
179fn decode_workflow_id(
180 workflow_id: Option<&ProtoWorkflowId>,
181) -> Result<aion_core::WorkflowId, ServerError> {
182 workflow_id
183 .cloned()
184 .ok_or_else(|| ServerError::Wire {
185 wire: WireError::invalid_input("transcript subscription workflow_id is missing"),
186 })?
187 .try_into()
188 .map_err(|wire| ServerError::Wire { wire })
189}
190
191fn decode_activity_id(subscription: &TranscriptSubscription) -> Result<ActivityId, ServerError> {
192 let activity_id = subscription.activity_id.ok_or_else(|| ServerError::Wire {
193 wire: WireError::invalid_input("transcript subscription activity_id is missing"),
194 })?;
195 Ok(ActivityId::from(activity_id))
196}
197
198async fn send_activity_frame(
202 socket: &mut WebSocket,
203 event: aion_core::ActivityEvent,
204) -> Result<std::ops::ControlFlow<()>, ServerError> {
205 let frame = encode_activity_frame(&event)?;
206 if socket.send(Message::Text(frame.into())).await.is_err() {
207 return Ok(std::ops::ControlFlow::Break(()));
208 }
209 Ok(std::ops::ControlFlow::Continue(()))
210}
211
212async fn forward_live_frame<Tx>(
216 socket_tx: &mut Tx,
217 event: aion_core::ActivityEvent,
218) -> Result<std::ops::ControlFlow<()>, ServerError>
219where
220 Tx: futures::Sink<Message> + Unpin,
221 <Tx as futures::Sink<Message>>::Error: std::fmt::Debug,
222{
223 let frame = match encode_activity_frame(&event) {
224 Ok(frame) => frame,
225 Err(error) => {
226 super::socket::send_wire_error(socket_tx, &error.to_wire_error()).await?;
227 return Err(error);
228 }
229 };
230 if socket_tx.send(Message::Text(frame.into())).await.is_err() {
231 return Ok(std::ops::ControlFlow::Break(()));
232 }
233 Ok(std::ops::ControlFlow::Continue(()))
234}
235
236fn encode_activity_frame(event: &aion_core::ActivityEvent) -> Result<String, ServerError> {
237 let frame = StreamedActivityEvent::new(event.clone());
238 serde_json::to_string(&frame).map_err(|source| ServerError::Wire {
239 wire: WireError::backend(format!(
240 "failed to serialize transcript event frame: {source}"
241 )),
242 })
243}
244
245async fn deliver_transcript_terminal<Tx>(
248 socket_tx: &mut Tx,
249 skipped: u64,
250) -> Result<(), ServerError>
251where
252 Tx: futures::Sink<Message> + Unpin,
253 <Tx as futures::Sink<Message>>::Error: std::fmt::Debug,
254{
255 let payload = serde_json::json!({
256 "error": { "code": "transcript_lagged", "skipped": skipped },
257 });
258 let payload = serde_json::to_string(&payload).map_err(|source| ServerError::Wire {
259 wire: WireError::backend(format!(
260 "failed to serialize transcript lag frame: {source}"
261 )),
262 })?;
263 if socket_tx.send(Message::Text(payload.into())).await.is_ok() {
264 let close = CloseFrame {
265 code: close_code::ERROR,
266 reason: "transcript_lagged".into(),
267 };
268 let close_result = socket_tx.send(Message::Close(Some(close))).await;
269 drop(close_result);
270 }
271 Err(ServerError::lagged_stream())
272}
273
274async fn send_normal_close<Tx>(socket_tx: &mut Tx) -> Result<(), ServerError>
276where
277 Tx: futures::Sink<Message> + Unpin,
278 <Tx as futures::Sink<Message>>::Error: std::fmt::Debug,
279{
280 let close = CloseFrame {
281 code: close_code::NORMAL,
282 reason: "subscription complete".into(),
283 };
284 let close_result = socket_tx.send(Message::Close(Some(close))).await;
285 drop(close_result);
286 Ok(())
287}