1use std::collections::HashMap;
11use std::time::Instant;
12
13use futures::future::BoxFuture;
14use serde_json::Value;
15
16use crate::core::types::JsonRpcNotification;
17use crate::transport::oversized_transfer::progress_token_string;
18
19use super::constants::{
20 DEFAULT_MAX_BUFFERED_BYTES_PER_STREAM, DEFAULT_MAX_BUFFERED_CHUNKS_PER_STREAM,
21 DEFAULT_MAX_CONCURRENT_OPEN_STREAMS, DEFAULT_OPEN_STREAM_CLOSE_GRACE_PERIOD_MS,
22 DEFAULT_OPEN_STREAM_IDLE_TIMEOUT_MS, DEFAULT_OPEN_STREAM_PROBE_TIMEOUT_MS,
23};
24use super::errors::OpenStreamError;
25use super::frame::OpenStreamFrame;
26use super::session::{
27 FrameOutcome, KeepaliveAction, OpenStreamSession, OpenStreamSessionOptions, PublishFrame,
28};
29
30const LOG_TARGET: &str = "contextvm_sdk::transport::open_stream";
31
32pub type RegistryCloseHook = Box<dyn FnOnce() -> BoxFuture<'static, crate::Result<()>> + Send>;
34
35pub type RegistryAbortHook =
38 Box<dyn FnOnce(Option<String>) -> BoxFuture<'static, crate::Result<()>> + Send>;
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub struct OpenStreamRegistryPolicy {
47 pub max_concurrent_streams: usize,
49 pub max_buffered_chunks_per_stream: usize,
51 pub max_buffered_bytes_per_stream: usize,
53 pub idle_timeout_ms: u64,
55 pub probe_timeout_ms: u64,
57 pub close_grace_period_ms: u64,
59}
60
61impl Default for OpenStreamRegistryPolicy {
62 fn default() -> Self {
63 Self {
64 max_concurrent_streams: DEFAULT_MAX_CONCURRENT_OPEN_STREAMS,
65 max_buffered_chunks_per_stream: DEFAULT_MAX_BUFFERED_CHUNKS_PER_STREAM,
66 max_buffered_bytes_per_stream: DEFAULT_MAX_BUFFERED_BYTES_PER_STREAM,
67 idle_timeout_ms: DEFAULT_OPEN_STREAM_IDLE_TIMEOUT_MS,
68 probe_timeout_ms: DEFAULT_OPEN_STREAM_PROBE_TIMEOUT_MS,
69 close_grace_period_ms: DEFAULT_OPEN_STREAM_CLOSE_GRACE_PERIOD_MS,
70 }
71 }
72}
73
74#[derive(Default)]
76pub struct OpenStreamSessionInit {
77 pub publish_frame: Option<PublishFrame>,
79 pub on_close: Option<RegistryCloseHook>,
81 pub on_abort: Option<RegistryAbortHook>,
83}
84
85struct RegistryEntry {
87 session: OpenStreamSession,
88 on_close: Option<RegistryCloseHook>,
89 on_abort: Option<RegistryAbortHook>,
90}
91
92pub struct OpenStreamRegistry {
94 policy: OpenStreamRegistryPolicy,
95 sessions: HashMap<String, RegistryEntry>,
96}
97
98impl Default for OpenStreamRegistry {
99 fn default() -> Self {
100 Self::new()
101 }
102}
103
104impl OpenStreamRegistry {
105 pub fn new() -> Self {
107 Self::with_policy(OpenStreamRegistryPolicy::default())
108 }
109
110 pub fn with_policy(policy: OpenStreamRegistryPolicy) -> Self {
112 Self {
113 policy,
114 sessions: HashMap::new(),
115 }
116 }
117
118 pub fn size(&self) -> usize {
120 self.sessions.len()
121 }
122
123 pub fn get_session(&self, progress_token: &str) -> Option<OpenStreamSession> {
125 self.sessions
126 .get(progress_token)
127 .map(|entry| entry.session.clone())
128 }
129
130 pub fn is_open_stream_progress(notification: &JsonRpcNotification) -> bool {
134 notification
135 .params
136 .as_ref()
137 .and_then(|params| params.get("cvm"))
138 .map(OpenStreamFrame::is_frame_value)
139 .unwrap_or(false)
140 }
141
142 pub fn create_session(
144 &mut self,
145 progress_token: impl Into<String>,
146 ) -> Result<OpenStreamSession, OpenStreamError> {
147 self.create_session_with(progress_token, OpenStreamSessionInit::default())
148 }
149
150 pub fn create_session_with(
153 &mut self,
154 progress_token: impl Into<String>,
155 init: OpenStreamSessionInit,
156 ) -> Result<OpenStreamSession, OpenStreamError> {
157 let progress_token = progress_token.into();
158 if self.sessions.contains_key(&progress_token) {
159 return Err(OpenStreamError::Sequence(format!(
160 "Stream session already exists for {progress_token}"
161 )));
162 }
163 if self.sessions.len() >= self.policy.max_concurrent_streams {
164 return Err(OpenStreamError::Policy(
165 "Maximum concurrent open streams exceeded".to_string(),
166 ));
167 }
168
169 let session = OpenStreamSession::new(OpenStreamSessionOptions {
170 progress_token: progress_token.clone(),
171 max_buffered_chunks: self.policy.max_buffered_chunks_per_stream,
172 max_buffered_bytes: self.policy.max_buffered_bytes_per_stream as u64,
173 idle_timeout_ms: self.policy.idle_timeout_ms,
174 probe_timeout_ms: self.policy.probe_timeout_ms,
175 close_grace_period_ms: self.policy.close_grace_period_ms,
176 publish_frame: init.publish_frame,
177 });
178 self.sessions.insert(
179 progress_token,
180 RegistryEntry {
181 session: session.clone(),
182 on_close: init.on_close,
183 on_abort: init.on_abort,
184 },
185 );
186 Ok(session)
187 }
188
189 pub fn get_or_create_session(
192 &mut self,
193 progress_token: impl Into<String>,
194 ) -> Result<OpenStreamSession, OpenStreamError> {
195 let progress_token = progress_token.into();
196 if let Some(session) = self.get_session(&progress_token) {
197 return Ok(session);
198 }
199 self.create_session(progress_token)
200 }
201
202 pub async fn process_frame(
208 &mut self,
209 now: Instant,
210 notification: &JsonRpcNotification,
211 ) -> Result<FrameOutcome, OpenStreamError> {
212 let (progress_token, progress, frame) = parse_frame(notification)?;
213
214 if !self.sessions.contains_key(&progress_token) {
215 if frame.frame_type() != "start" {
216 return Err(OpenStreamError::Sequence(format!(
217 "Received {} frame before start for {progress_token}",
218 frame.frame_type()
219 )));
220 }
221 self.create_session_with(progress_token.clone(), OpenStreamSessionInit::default())?;
222 }
223
224 let session = self
227 .sessions
228 .get(&progress_token)
229 .expect("session present after create")
230 .session
231 .clone();
232
233 match session.process_frame(now, progress, frame) {
234 Ok(FrameOutcome::Closed) => {
235 self.run_close(&progress_token).await;
236 Ok(FrameOutcome::Closed)
237 }
238 Ok(FrameOutcome::Aborted(reason)) => {
239 self.run_abort(&progress_token, reason.clone()).await;
240 Ok(FrameOutcome::Aborted(reason))
241 }
242 Ok(other) => Ok(other),
243 Err(error) => {
244 session.fail(error.clone());
245 self.run_abort(&progress_token, Some(error.to_string()))
250 .await;
251 Err(error)
252 }
253 }
254 }
255
256 pub fn tick_all(&mut self, now: Instant) -> Vec<(String, KeepaliveAction)> {
262 let mut actions = Vec::new();
263 let mut aborted = Vec::new();
264 for (token, entry) in self.sessions.iter() {
265 match entry.session.tick(now) {
266 KeepaliveAction::None => {}
267 action => {
268 if matches!(action, KeepaliveAction::Abort(_)) {
269 aborted.push(token.clone());
270 }
271 actions.push((token.clone(), action));
272 }
273 }
274 }
275 for token in aborted {
276 self.sessions.remove(&token);
277 }
278 actions
279 }
280
281 pub fn clear(&mut self) {
283 for (_, entry) in self.sessions.drain() {
284 entry.session.dispose();
285 }
286 }
287
288 pub async fn consumer_abort(&mut self, progress_token: &str, reason: Option<String>) {
300 if let Some(entry) = self.sessions.remove(progress_token) {
301 entry
302 .session
303 .fail(OpenStreamError::abort(progress_token, reason.clone()));
304 if let Some(hook) = entry.on_abort {
305 if let Err(error) = hook(reason).await {
306 tracing::debug!(
307 target: LOG_TARGET,
308 token = %progress_token,
309 %error,
310 "open-stream on_abort hook errored during consumer abort"
311 );
312 }
313 }
314 }
315 }
316
317 async fn run_close(&mut self, progress_token: &str) {
319 if let Some(entry) = self.sessions.remove(progress_token) {
320 if let Some(hook) = entry.on_close {
321 if let Err(error) = hook().await {
322 tracing::debug!(
323 target: LOG_TARGET,
324 token = %progress_token,
325 %error,
326 "open-stream on_close hook errored"
327 );
328 }
329 }
330 }
331 }
332
333 async fn run_abort(&mut self, progress_token: &str, reason: Option<String>) {
335 if let Some(entry) = self.sessions.remove(progress_token) {
336 if let Some(hook) = entry.on_abort {
337 if let Err(error) = hook(reason).await {
338 tracing::debug!(
339 target: LOG_TARGET,
340 token = %progress_token,
341 %error,
342 "open-stream on_abort hook errored"
343 );
344 }
345 }
346 }
347 }
348}
349
350fn parse_frame(
353 notification: &JsonRpcNotification,
354) -> Result<(String, i64, OpenStreamFrame), OpenStreamError> {
355 let params = notification.params.as_ref().ok_or_else(|| {
356 OpenStreamError::Sequence("Open stream frame is missing params".to_string())
357 })?;
358 let frame = params
359 .get("cvm")
360 .and_then(OpenStreamFrame::from_cvm_value)
361 .ok_or_else(|| {
362 OpenStreamError::Sequence("Notification is not an open-stream frame".to_string())
363 })?;
364
365 let progress_token = params
366 .get("progressToken")
367 .and_then(progress_token_string)
368 .filter(|token| !token.is_empty())
369 .ok_or_else(|| {
370 OpenStreamError::Sequence("Open stream frame is missing progressToken".to_string())
371 })?;
372
373 let progress = parse_progress(params.get("progress"), &progress_token)?;
374 Ok((progress_token, progress, frame))
375}
376
377fn parse_progress(value: Option<&Value>, token: &str) -> Result<i64, OpenStreamError> {
379 let progress = match value {
380 Some(Value::Number(n)) => n.as_i64().or_else(|| {
381 n.as_f64()
382 .and_then(|f| if f.is_finite() { Some(f as i64) } else { None })
383 }),
384 _ => None,
385 };
386 progress.ok_or_else(|| {
387 OpenStreamError::Sequence(format!("Invalid progress value (token: {token})"))
388 })
389}
390
391#[cfg(test)]
392mod tests {
393 use super::*;
394 use crate::Error;
395 use futures::StreamExt;
396 use serde_json::json;
397
398 fn now() -> Instant {
399 Instant::now()
400 }
401
402 fn notif(token: &str, progress: u64, frame: OpenStreamFrame) -> JsonRpcNotification {
403 frame
404 .into_progress_notification(token, progress, None)
405 .unwrap()
406 }
407
408 fn start() -> OpenStreamFrame {
409 OpenStreamFrame::Start { content_type: None }
410 }
411
412 fn chunk(index: u64, data: &str) -> OpenStreamFrame {
413 OpenStreamFrame::Chunk {
414 chunk_index: index,
415 data: data.to_string(),
416 }
417 }
418
419 fn small_policy(max_concurrent: usize) -> OpenStreamRegistryPolicy {
420 OpenStreamRegistryPolicy {
421 max_concurrent_streams: max_concurrent,
422 max_buffered_chunks_per_stream: 4,
423 max_buffered_bytes_per_stream: 128,
424 ..OpenStreamRegistryPolicy::default()
425 }
426 }
427
428 #[tokio::test]
429 async fn enforces_max_concurrent_and_reuses_slot_after_close() {
430 let mut registry = OpenStreamRegistry::with_policy(small_policy(1));
431 let _first = registry.create_session("token-1").unwrap();
432 assert!(matches!(
433 registry.create_session("token-2").unwrap_err(),
434 OpenStreamError::Policy(_)
435 ));
436
437 registry
439 .process_frame(now(), ¬if("token-1", 1, start()))
440 .await
441 .unwrap();
442 registry
443 .process_frame(
444 now(),
445 ¬if(
446 "token-1",
447 2,
448 OpenStreamFrame::Close {
449 last_chunk_index: None,
450 },
451 ),
452 )
453 .await
454 .unwrap();
455
456 registry.create_session("token-2").unwrap();
457 assert_eq!(registry.size(), 1);
458 }
459
460 #[test]
461 fn get_or_create_reuses_the_same_session() {
462 let mut registry = OpenStreamRegistry::with_policy(small_policy(2));
463 let first = registry.get_or_create_session("token-shared").unwrap();
464 let second = registry.get_or_create_session("token-shared").unwrap();
465 assert!(first.shares_state_with(&second));
466 assert_eq!(registry.size(), 1);
467 }
468
469 #[test]
470 fn rejects_creating_a_duplicate_token() {
471 let mut registry = OpenStreamRegistry::with_policy(small_policy(4));
472 registry.create_session("token-dup").unwrap();
473 assert!(matches!(
476 registry.create_session("token-dup").unwrap_err(),
477 OpenStreamError::Sequence(_)
478 ));
479 assert_eq!(registry.size(), 1);
480 }
481
482 #[tokio::test]
483 async fn routes_a_duplicate_start_frame_to_a_sequence_error() {
484 let mut registry = OpenStreamRegistry::with_policy(small_policy(2));
488 registry
489 .process_frame(now(), ¬if("token-dup-start", 1, start()))
490 .await
491 .unwrap();
492 let err = registry
493 .process_frame(now(), ¬if("token-dup-start", 2, start()))
494 .await
495 .unwrap_err();
496 assert!(matches!(err, OpenStreamError::Sequence(_)));
497 assert!(registry.get_session("token-dup-start").is_none());
498 }
499
500 #[tokio::test]
501 async fn rejects_non_start_frames_for_unknown_tokens() {
502 let mut registry = OpenStreamRegistry::with_policy(small_policy(2));
503 let err = registry
504 .process_frame(now(), ¬if("token-missing-start", 1, chunk(0, "orphan")))
505 .await
506 .unwrap_err();
507 assert!(matches!(err, OpenStreamError::Sequence(_)));
508 assert!(registry.get_session("token-missing-start").is_none());
509 assert_eq!(registry.size(), 0);
510 }
511
512 #[tokio::test]
513 async fn terminates_session_when_frame_processing_fails_after_creation() {
514 let mut registry = OpenStreamRegistry::with_policy(OpenStreamRegistryPolicy {
515 max_buffered_bytes_per_stream: 4,
516 ..small_policy(2)
517 });
518 registry
519 .process_frame(now(), ¬if("token-fail", 1, start()))
520 .await
521 .unwrap();
522 let mut session = registry.get_session("token-fail").unwrap();
523
524 let err = registry
526 .process_frame(now(), ¬if("token-fail", 2, chunk(1, "hello")))
527 .await
528 .unwrap_err();
529 assert!(matches!(err, OpenStreamError::Sequence(_)));
530 assert!(registry.get_session("token-fail").is_none());
531
532 match session.next().await {
534 Some(Err(OpenStreamError::Sequence(_))) => {}
535 other => panic!("expected sequence error on the stream, got {other:?}"),
536 }
537 }
538
539 #[tokio::test]
540 async fn applies_default_buffering_limits() {
541 let mut registry = OpenStreamRegistry::new();
542 registry
543 .process_frame(now(), ¬if("token-chunks", 1, start()))
544 .await
545 .unwrap();
546 for i in 0..DEFAULT_MAX_BUFFERED_CHUNKS_PER_STREAM as u64 {
548 registry
549 .process_frame(now(), ¬if("token-chunks", i + 2, chunk(i + 1, "x")))
550 .await
551 .unwrap();
552 }
553 let err = registry
554 .process_frame(
555 now(),
556 ¬if(
557 "token-chunks",
558 DEFAULT_MAX_BUFFERED_CHUNKS_PER_STREAM as u64 + 2,
559 chunk(DEFAULT_MAX_BUFFERED_CHUNKS_PER_STREAM as u64 + 1, "x"),
560 ),
561 )
562 .await
563 .unwrap_err();
564 assert!(matches!(err, OpenStreamError::Sequence(_)));
565
566 let mut byte_registry = OpenStreamRegistry::new();
568 byte_registry
569 .process_frame(now(), ¬if("token-bytes", 1, start()))
570 .await
571 .unwrap();
572 let oversized = "x".repeat(DEFAULT_MAX_BUFFERED_BYTES_PER_STREAM + 1);
573 let err = byte_registry
574 .process_frame(now(), ¬if("token-bytes", 2, chunk(1, &oversized)))
575 .await
576 .unwrap_err();
577 assert!(matches!(err, OpenStreamError::Sequence(_)));
578 }
579
580 #[tokio::test]
581 async fn ping_frame_routes_to_a_pong_outcome() {
582 let mut registry = OpenStreamRegistry::new();
583 registry
584 .process_frame(now(), ¬if("token-timers", 1, start()))
585 .await
586 .unwrap();
587 let outcome = registry
588 .process_frame(
589 now(),
590 ¬if(
591 "token-timers",
592 2,
593 OpenStreamFrame::Ping {
594 nonce: "peer-nonce".to_string(),
595 },
596 ),
597 )
598 .await
599 .unwrap();
600 assert_eq!(outcome, FrameOutcome::SendPong("peer-nonce".to_string()));
601 }
602
603 #[tokio::test]
604 async fn clear_disposes_active_sessions() {
605 let mut registry = OpenStreamRegistry::with_policy(small_policy(2));
606 let session = registry.create_session("token-clear").unwrap();
607 registry
608 .process_frame(now(), ¬if("token-clear", 1, start()))
609 .await
610 .unwrap();
611
612 registry.clear();
613 assert_eq!(registry.size(), 0);
614 session.closed().await;
616 }
617
618 #[tokio::test]
619 async fn accepts_start_frame_with_advisory_metadata_omitted() {
620 let mut registry = OpenStreamRegistry::with_policy(small_policy(2));
621 let outcome = registry
622 .process_frame(now(), ¬if("token-advisory", 1, start()))
623 .await
624 .unwrap();
625 assert_eq!(outcome, FrameOutcome::None);
626 assert!(registry.get_session("token-advisory").is_some());
627
628 registry.clear();
629 assert_eq!(registry.size(), 0);
630 }
631
632 #[tokio::test]
633 async fn rejects_malformed_non_cep41_payloads() {
634 let malformed = [
636 json!({ "progressToken": "missing-cvm", "progress": 1 }),
637 json!({ "progressToken": "wrong-type", "progress": 1, "cvm": { "type": "other", "frameType": "start" } }),
638 json!({ "progressToken": "missing-frame-type", "progress": 1, "cvm": { "type": "open-stream" } }),
639 ];
640 for params in malformed {
641 let notification = JsonRpcNotification {
642 jsonrpc: "2.0".to_string(),
643 method: "notifications/progress".to_string(),
644 params: Some(params),
645 };
646 assert!(!OpenStreamRegistry::is_open_stream_progress(¬ification));
647 }
648 assert!(OpenStreamRegistry::is_open_stream_progress(¬if(
649 "ok",
650 1,
651 start()
652 )));
653
654 let mut registry = OpenStreamRegistry::new();
656 let bad = JsonRpcNotification {
657 jsonrpc: "2.0".to_string(),
658 method: "notifications/progress".to_string(),
659 params: Some(json!({ "progressToken": "t", "progress": 1 })),
660 };
661 assert!(matches!(
662 registry.process_frame(now(), &bad).await.unwrap_err(),
663 OpenStreamError::Sequence(_)
664 ));
665 }
666
667 #[tokio::test]
668 async fn rejects_accept_as_the_first_frame() {
669 let mut registry = OpenStreamRegistry::with_policy(small_policy(2));
670 let err = registry
671 .process_frame(
672 now(),
673 ¬if("token-orphan-accept", 1, OpenStreamFrame::Accept),
674 )
675 .await
676 .unwrap_err();
677 assert!(matches!(err, OpenStreamError::Sequence(_)));
678 assert!(registry.get_session("token-orphan-accept").is_none());
679 }
680
681 #[tokio::test]
682 async fn removes_session_even_when_on_close_hook_errors() {
683 let mut registry = OpenStreamRegistry::with_policy(small_policy(1));
684 let on_close: RegistryCloseHook = Box::new(|| {
685 Box::pin(async { Err::<(), Error>(Error::Other("close failed".to_string())) })
686 });
687 registry
688 .create_session_with(
689 "token-close-throws",
690 OpenStreamSessionInit {
691 on_close: Some(on_close),
692 ..Default::default()
693 },
694 )
695 .unwrap();
696
697 registry
698 .process_frame(now(), ¬if("token-close-throws", 1, start()))
699 .await
700 .unwrap();
701 registry
703 .process_frame(
704 now(),
705 ¬if(
706 "token-close-throws",
707 2,
708 OpenStreamFrame::Close {
709 last_chunk_index: None,
710 },
711 ),
712 )
713 .await
714 .unwrap();
715
716 assert!(registry.get_session("token-close-throws").is_none());
717 assert_eq!(registry.size(), 0);
718 }
719
720 #[tokio::test]
721 async fn consumer_abort_frees_slot_and_runs_hook() {
722 let mut registry = OpenStreamRegistry::with_policy(small_policy(1));
726 let fired = std::sync::Arc::new(std::sync::Mutex::new(Vec::<String>::new()));
727 let f = fired.clone();
728 let on_abort: RegistryAbortHook = Box::new(move |reason| {
729 let f = f.clone();
730 Box::pin(async move {
731 f.lock().unwrap().push(reason.unwrap_or_default());
732 Ok(())
733 })
734 });
735 let mut session = registry
736 .create_session_with(
737 "token-consumer-abort",
738 OpenStreamSessionInit {
739 on_abort: Some(on_abort),
740 ..Default::default()
741 },
742 )
743 .unwrap();
744 assert!(matches!(
746 registry.create_session("token-other").unwrap_err(),
747 OpenStreamError::Policy(_)
748 ));
749
750 registry
751 .consumer_abort("token-consumer-abort", Some("user cancelled".to_string()))
752 .await;
753
754 assert_eq!(registry.size(), 0);
755 assert!(registry.get_session("token-consumer-abort").is_none());
756 assert_eq!(*fired.lock().unwrap(), vec!["user cancelled".to_string()]);
757 match session.next().await {
759 Some(Err(OpenStreamError::Abort { reason, .. })) => {
760 assert_eq!(reason.as_deref(), Some("user cancelled"));
761 }
762 other => panic!("expected abort error on the stream, got {other:?}"),
763 }
764 registry.create_session("token-other").unwrap();
766 }
767
768 #[tokio::test]
769 async fn removes_session_even_when_on_abort_hook_errors() {
770 let mut registry = OpenStreamRegistry::with_policy(small_policy(1));
771 let on_abort: RegistryAbortHook = Box::new(|_reason| {
772 Box::pin(async { Err::<(), Error>(Error::Other("abort failed".to_string())) })
773 });
774 registry
775 .create_session_with(
776 "token-abort-throws",
777 OpenStreamSessionInit {
778 on_abort: Some(on_abort),
779 ..Default::default()
780 },
781 )
782 .unwrap();
783 let mut session = registry.get_session("token-abort-throws").unwrap();
784
785 registry
786 .process_frame(now(), ¬if("token-abort-throws", 1, start()))
787 .await
788 .unwrap();
789 registry
790 .process_frame(
791 now(),
792 ¬if(
793 "token-abort-throws",
794 2,
795 OpenStreamFrame::Abort {
796 reason: Some("boom".to_string()),
797 },
798 ),
799 )
800 .await
801 .unwrap();
802
803 assert!(registry.get_session("token-abort-throws").is_none());
804 assert_eq!(registry.size(), 0);
805 match session.next().await {
807 Some(Err(OpenStreamError::Abort { reason, .. })) => {
808 assert_eq!(reason.as_deref(), Some("boom"));
809 }
810 other => panic!("expected abort error on the stream, got {other:?}"),
811 }
812 }
813
814 #[tokio::test]
815 async fn fires_only_on_close_on_graceful_close_and_only_on_abort_on_abort() {
816 use std::sync::{Arc as StdArc, Mutex as StdMutex};
817
818 fn recording_hooks(
821 events: StdArc<StdMutex<Vec<String>>>,
822 token: &'static str,
823 ) -> (RegistryCloseHook, RegistryAbortHook) {
824 let close_events = events.clone();
825 let on_close: RegistryCloseHook = Box::new(move || {
826 Box::pin(async move {
827 close_events.lock().unwrap().push(format!("close:{token}"));
828 Ok(())
829 })
830 });
831 let on_abort: RegistryAbortHook = Box::new(move |_reason| {
832 Box::pin(async move {
833 events.lock().unwrap().push(format!("abort:{token}"));
834 Ok(())
835 })
836 });
837 (on_close, on_abort)
838 }
839
840 let events = StdArc::new(StdMutex::new(Vec::<String>::new()));
841 let mut registry = OpenStreamRegistry::with_policy(small_policy(4));
842
843 let (on_close, on_abort) = recording_hooks(events.clone(), "graceful");
845 registry
846 .create_session_with(
847 "tok-graceful",
848 OpenStreamSessionInit {
849 on_close: Some(on_close),
850 on_abort: Some(on_abort),
851 ..Default::default()
852 },
853 )
854 .unwrap();
855 registry
856 .process_frame(now(), ¬if("tok-graceful", 1, start()))
857 .await
858 .unwrap();
859 registry
860 .process_frame(
861 now(),
862 ¬if(
863 "tok-graceful",
864 2,
865 OpenStreamFrame::Close {
866 last_chunk_index: None,
867 },
868 ),
869 )
870 .await
871 .unwrap();
872
873 let (on_close, on_abort) = recording_hooks(events.clone(), "aborted");
875 registry
876 .create_session_with(
877 "tok-aborted",
878 OpenStreamSessionInit {
879 on_close: Some(on_close),
880 on_abort: Some(on_abort),
881 ..Default::default()
882 },
883 )
884 .unwrap();
885 registry
886 .process_frame(now(), ¬if("tok-aborted", 1, start()))
887 .await
888 .unwrap();
889 registry
890 .process_frame(
891 now(),
892 ¬if(
893 "tok-aborted",
894 2,
895 OpenStreamFrame::Abort {
896 reason: Some("bye".to_string()),
897 },
898 ),
899 )
900 .await
901 .unwrap();
902
903 let fired = events.lock().unwrap().clone();
905 assert_eq!(
906 fired,
907 vec!["close:graceful".to_string(), "abort:aborted".to_string()]
908 );
909 }
910}