1use aion_core::{ActivityId, RunId};
30use aion_integrations::envelope_delta::EnvelopeDeltaDecoder;
31use aion_proto::{
32 PerWorkflowSubscription, ProtoRunId, ProtoWorkflowId, StreamedActivityEvent,
33 TranscriptSubscription, WireError,
34};
35use aion_store::ActivityStreamKey;
36use axum::extract::ws::{CloseFrame, Message, WebSocket, close_code};
37use futures::{SinkExt, StreamExt};
38
39use crate::activity_publisher::TranscriptStreamLagged;
40use crate::error::ServerError;
41use crate::namespace::{CallerIdentity, NamespaceOperation, SubscriptionScope, WorkflowTarget};
42use crate::state::ServerState;
43
44pub async fn serve_transcript_socket(
58 mut socket: WebSocket,
59 state: &ServerState,
60 caller: &CallerIdentity,
61 subscription: &TranscriptSubscription,
62) -> Result<(), ServerError> {
63 let key = match authorize_transcript(state, caller, subscription).await {
64 Ok(key) => key,
65 Err(error) => {
66 super::socket::send_wire_error(&mut socket, &error.to_wire_error()).await?;
67 return Err(error);
68 }
69 };
70
71 let publisher = state.transcript_publisher();
72 let mut live = publisher.subscribe(key.clone(), subscription.after_seq);
76
77 let from_seq = subscription
81 .after_seq
82 .map_or(0, |seq| seq.saturating_add(1));
83 let replay = publisher
84 .replay_from(&key, from_seq)
85 .await
86 .map_err(ServerError::from)?;
87 let mut envelopes = EnvelopeDeltaDecoder::new();
91 for mut record in replay {
92 resolve_frame(&mut envelopes, &mut record.event);
93 if send_activity_frame(&mut socket, record.event)
94 .await?
95 .is_break()
96 {
97 return Ok(());
98 }
99 }
100
101 let (mut socket_tx, mut socket_rx) = socket.split();
102 loop {
103 tokio::select! {
104 client_message = socket_rx.next() => {
105 match client_message {
106 Some(Ok(Message::Close(_))) | None => {
111 return send_normal_close(&mut socket_tx).await;
112 }
113 Some(Ok(_other)) => {}
114 Some(Err(_error)) => return Ok(()),
115 }
116 }
117 item = live.next() => {
118 match item {
119 Some(Ok(mut event)) => {
120 resolve_frame(&mut envelopes, &mut event);
121 if forward_live_frame(&mut socket_tx, event).await?.is_break() {
122 return Ok(());
123 }
124 }
125 Some(Err(TranscriptStreamLagged { skipped })) => {
126 return deliver_transcript_terminal(&mut socket_tx, skipped).await;
127 }
128 None => return send_normal_close(&mut socket_tx).await,
129 }
130 }
131 }
132 }
133}
134
135fn resolve_frame(envelopes: &mut EnvelopeDeltaDecoder, event: &mut aion_core::ActivityEvent) {
141 if let Some(report) = crate::transcript_resolve::resolve_event(envelopes, event) {
142 crate::transcript_resolve::note_unresolved("ws:transcript", std::slice::from_ref(&report));
143 }
144}
145
146async fn authorize_transcript(
153 state: &ServerState,
154 caller: &CallerIdentity,
155 subscription: &TranscriptSubscription,
156) -> Result<ActivityStreamKey, ServerError> {
157 let workflow_id = decode_workflow_id(subscription.workflow_id.as_ref())?;
158 let run_id = decode_run_id(subscription.run_id.as_ref())?;
159 let activity_id = decode_activity_id(subscription)?;
160 gate_transcript_workflow(state, caller, &subscription.namespace, &workflow_id).await?;
161 Ok(ActivityStreamKey::new(
162 workflow_id,
163 run_id,
164 activity_id,
165 subscription.attempt,
166 ))
167}
168
169pub(crate) async fn gate_transcript_workflow(
174 state: &ServerState,
175 caller: &CallerIdentity,
176 namespace: &str,
177 workflow_id: &aion_core::WorkflowId,
178) -> Result<(), ServerError> {
179 let per_workflow = PerWorkflowSubscription {
180 namespace: namespace.to_owned(),
181 workflow_id: Some(ProtoWorkflowId::from(workflow_id.clone())),
182 resume_from_seq: None,
183 };
184 let target = WorkflowTarget::workflow(workflow_id);
185 let scope = SubscriptionScope::PerWorkflow(&per_workflow, target);
186 let filter = aion::EventFilter {
187 workflow_id: Some(workflow_id.clone()),
188 ..aion::EventFilter::default()
189 };
190 let operation = NamespaceOperation::subscribe(scope, &filter);
191 let scoped = state.namespace_guard().scope(caller, &operation).await?;
195 drop(scoped);
196 Ok(())
197}
198
199fn decode_workflow_id(
200 workflow_id: Option<&ProtoWorkflowId>,
201) -> Result<aion_core::WorkflowId, ServerError> {
202 workflow_id
203 .cloned()
204 .ok_or_else(|| ServerError::Wire {
205 wire: WireError::invalid_input("transcript subscription workflow_id is missing"),
206 })?
207 .try_into()
208 .map_err(|wire| ServerError::Wire { wire })
209}
210
211fn decode_run_id(run_id: Option<&ProtoRunId>) -> Result<RunId, ServerError> {
217 run_id
218 .cloned()
219 .ok_or_else(|| ServerError::Wire {
220 wire: WireError::invalid_input("transcript subscription run_id is missing"),
221 })?
222 .try_into()
223 .map_err(|wire| ServerError::Wire { wire })
224}
225
226fn decode_activity_id(subscription: &TranscriptSubscription) -> Result<ActivityId, ServerError> {
227 let activity_id = subscription.activity_id.ok_or_else(|| ServerError::Wire {
228 wire: WireError::invalid_input("transcript subscription activity_id is missing"),
229 })?;
230 Ok(ActivityId::from(activity_id))
231}
232
233async fn send_activity_frame(
237 socket: &mut WebSocket,
238 event: aion_core::ActivityEvent,
239) -> Result<std::ops::ControlFlow<()>, ServerError> {
240 let frame = encode_activity_frame(&event)?;
241 if socket.send(Message::Text(frame.into())).await.is_err() {
242 return Ok(std::ops::ControlFlow::Break(()));
243 }
244 Ok(std::ops::ControlFlow::Continue(()))
245}
246
247async fn forward_live_frame<Tx>(
251 socket_tx: &mut Tx,
252 event: aion_core::ActivityEvent,
253) -> Result<std::ops::ControlFlow<()>, ServerError>
254where
255 Tx: futures::Sink<Message> + Unpin,
256 <Tx as futures::Sink<Message>>::Error: std::fmt::Debug,
257{
258 let frame = match encode_activity_frame(&event) {
259 Ok(frame) => frame,
260 Err(error) => {
261 super::socket::send_wire_error(socket_tx, &error.to_wire_error()).await?;
262 return Err(error);
263 }
264 };
265 if socket_tx.send(Message::Text(frame.into())).await.is_err() {
266 return Ok(std::ops::ControlFlow::Break(()));
267 }
268 Ok(std::ops::ControlFlow::Continue(()))
269}
270
271fn encode_activity_frame(event: &aion_core::ActivityEvent) -> Result<String, ServerError> {
272 let frame = StreamedActivityEvent::new(event.clone());
273 serde_json::to_string(&frame).map_err(|source| ServerError::Wire {
274 wire: WireError::backend(format!(
275 "failed to serialize transcript event frame: {source}"
276 )),
277 })
278}
279
280async fn deliver_transcript_terminal<Tx>(
283 socket_tx: &mut Tx,
284 skipped: u64,
285) -> Result<(), ServerError>
286where
287 Tx: futures::Sink<Message> + Unpin,
288 <Tx as futures::Sink<Message>>::Error: std::fmt::Debug,
289{
290 let payload = serde_json::json!({
291 "error": { "code": "transcript_lagged", "skipped": skipped },
292 });
293 let payload = serde_json::to_string(&payload).map_err(|source| ServerError::Wire {
294 wire: WireError::backend(format!(
295 "failed to serialize transcript lag frame: {source}"
296 )),
297 })?;
298 if socket_tx.send(Message::Text(payload.into())).await.is_ok() {
299 let close = CloseFrame {
300 code: close_code::ERROR,
301 reason: "transcript_lagged".into(),
302 };
303 let close_result = socket_tx.send(Message::Close(Some(close))).await;
304 drop(close_result);
305 }
306 Err(ServerError::lagged_stream())
307}
308
309async fn send_normal_close<Tx>(socket_tx: &mut Tx) -> Result<(), ServerError>
311where
312 Tx: futures::Sink<Message> + Unpin,
313 <Tx as futures::Sink<Message>>::Error: std::fmt::Debug,
314{
315 let close = CloseFrame {
316 code: close_code::NORMAL,
317 reason: "subscription complete".into(),
318 };
319 let close_result = socket_tx.send(Message::Close(Some(close))).await;
320 drop(close_result);
321 Ok(())
322}