1use crate::{
4 application::{ApplicationError, ApplicationResult, commands::*, handlers::CommandHandlerGat},
5 domain::{
6 aggregates::StreamSession,
7 config::limits::{MAX_FRAMES_PER_REQUEST, MAX_SESSION_TIMEOUT_SECONDS},
8 entities::Frame,
9 ports::{
10 DictionaryStore, EventPublisherGat, FrameStoreGat, NoopDictionaryStore,
11 StreamRepositoryGat,
12 },
13 value_objects::{SessionId, StreamId},
14 },
15 infrastructure::adapters::InMemoryFrameStore,
16};
17use std::sync::Arc;
18
19pub struct SessionCommandHandler<R, P, F = InMemoryFrameStore>
30where
31 R: StreamRepositoryGat + 'static,
32 P: EventPublisherGat + 'static,
33 F: FrameStoreGat + 'static,
34{
35 repository: Arc<R>,
36 event_publisher: Arc<P>,
37 #[cfg_attr(
38 not(all(feature = "compression", not(target_arch = "wasm32"))),
39 allow(dead_code)
40 )]
41 dictionary_store: Arc<dyn DictionaryStore>,
42 frame_store: Arc<F>,
43}
44
45impl<R, P, F> std::fmt::Debug for SessionCommandHandler<R, P, F>
46where
47 R: StreamRepositoryGat + 'static,
48 P: EventPublisherGat + 'static,
49 F: FrameStoreGat + 'static,
50{
51 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52 f.debug_struct("SessionCommandHandler")
53 .finish_non_exhaustive()
54 }
55}
56
57impl<R, P> SessionCommandHandler<R, P, InMemoryFrameStore>
58where
59 R: StreamRepositoryGat + 'static,
60 P: EventPublisherGat + 'static,
61{
62 pub fn new(repository: Arc<R>, event_publisher: Arc<P>) -> Self {
70 Self::with_dictionary_store(repository, event_publisher, Arc::new(NoopDictionaryStore))
71 }
72
73 pub fn with_dictionary_store(
76 repository: Arc<R>,
77 event_publisher: Arc<P>,
78 dictionary_store: Arc<dyn DictionaryStore>,
79 ) -> Self {
80 Self::with_stores(
81 repository,
82 event_publisher,
83 dictionary_store,
84 Arc::new(InMemoryFrameStore::new()),
85 )
86 }
87}
88
89impl<R, P, F> SessionCommandHandler<R, P, F>
90where
91 R: StreamRepositoryGat + 'static,
92 P: EventPublisherGat + 'static,
93 F: FrameStoreGat + 'static,
94{
95 pub fn with_stores(
99 repository: Arc<R>,
100 event_publisher: Arc<P>,
101 dictionary_store: Arc<dyn DictionaryStore>,
102 frame_store: Arc<F>,
103 ) -> Self {
104 Self {
105 repository,
106 event_publisher,
107 dictionary_store,
108 frame_store,
109 }
110 }
111
112 #[cfg(all(feature = "compression", not(target_arch = "wasm32")))]
120 async fn train_from_frames(&self, session_id: SessionId, frames: &[Frame]) {
121 for frame in frames {
122 if let Ok(bytes) = serde_json::to_vec(frame.payload()) {
123 let _ = self
124 .dictionary_store
125 .train_if_ready(session_id, bytes)
126 .await;
127 }
128 }
129 }
130
131 async fn save_and_publish(&self, session: &mut StreamSession) -> ApplicationResult<()>
139 where
140 R: StreamRepositoryGat + Send + Sync,
141 P: EventPublisherGat + Send + Sync,
142 {
143 let events: Vec<_> = session.take_events().into_iter().collect();
144 self.repository
145 .save_session(session.clone())
146 .await
147 .map_err(ApplicationError::Domain)?;
148 self.event_publisher
149 .publish_batch(events)
150 .await
151 .map_err(ApplicationError::Domain)?;
152 Ok(())
153 }
154
155 async fn persist_frames_grouped_by_stream(&self, frames: &[Frame]) -> ApplicationResult<()>
158 where
159 F: FrameStoreGat + Send + Sync,
160 {
161 if frames.is_empty() {
162 return Ok(());
163 }
164 let mut buckets: std::collections::HashMap<StreamId, Vec<Frame>> =
165 std::collections::HashMap::new();
166 for frame in frames {
167 buckets
168 .entry(frame.stream_id())
169 .or_default()
170 .push(frame.clone());
171 }
172 for (stream_id, group) in buckets {
173 self.frame_store
174 .append_frames(stream_id, group)
175 .await
176 .map_err(ApplicationError::Domain)?;
177 }
178 Ok(())
179 }
180}
181
182impl<R, P, F> CommandHandlerGat<CreateSessionCommand> for SessionCommandHandler<R, P, F>
183where
184 R: StreamRepositoryGat + Send + Sync,
185 P: EventPublisherGat + Send + Sync,
186 F: FrameStoreGat + Send + Sync,
187{
188 type Response = SessionId;
189
190 type HandleFuture<'a>
191 = impl std::future::Future<Output = ApplicationResult<Self::Response>> + Send + 'a
192 where
193 Self: 'a;
194
195 fn handle(&self, command: CreateSessionCommand) -> Self::HandleFuture<'_> {
196 async move {
197 CommandValidator::validate_create_session(&command)
198 .map_err(|errors| ApplicationError::Validation(errors.join("; ")))?;
199
200 let mut session = StreamSession::new(command.config);
202
203 if let (Some(client_info), user_agent, ip_address) =
205 (command.client_info, command.user_agent, command.ip_address)
206 {
207 session.set_client_info(client_info, user_agent, ip_address);
208 }
209
210 session.activate().map_err(ApplicationError::Domain)?;
212
213 let session_id = session.id();
214
215 self.save_and_publish(&mut session).await?;
216
217 Ok(session_id)
218 }
219 }
220}
221
222impl<R, P, F> CommandHandlerGat<CreateStreamCommand> for SessionCommandHandler<R, P, F>
223where
224 R: StreamRepositoryGat + Send + Sync,
225 P: EventPublisherGat + Send + Sync,
226 F: FrameStoreGat + Send + Sync,
227{
228 type Response = StreamId;
229
230 type HandleFuture<'a>
231 = impl std::future::Future<Output = ApplicationResult<Self::Response>> + Send + 'a
232 where
233 Self: 'a;
234
235 fn handle(&self, command: CreateStreamCommand) -> Self::HandleFuture<'_> {
236 async move {
237 CommandValidator::validate_create_stream(&command)
238 .map_err(|errors| ApplicationError::Validation(errors.join("; ")))?;
239
240 let (stream_id, events) = self
245 .repository
246 .create_stream_atomic(
247 command.session_id.into(),
248 command.source_data,
249 command.config,
250 )
251 .await
252 .map_err(|e| match e {
253 crate::domain::DomainError::SessionNotFound(_) => ApplicationError::NotFound(
254 format!("Session {} not found", command.session_id),
255 ),
256 other => ApplicationError::Domain(other),
257 })?;
258
259 self.event_publisher
260 .publish_batch(events)
261 .await
262 .map_err(ApplicationError::Domain)?;
263
264 Ok(stream_id)
265 }
266 }
267}
268
269impl<R, P, F> CommandHandlerGat<StartStreamCommand> for SessionCommandHandler<R, P, F>
270where
271 R: StreamRepositoryGat + Send + Sync,
272 P: EventPublisherGat + Send + Sync,
273 F: FrameStoreGat + Send + Sync,
274{
275 type Response = ();
276
277 type HandleFuture<'a>
278 = impl std::future::Future<Output = ApplicationResult<Self::Response>> + Send + 'a
279 where
280 Self: 'a;
281
282 fn handle(&self, command: StartStreamCommand) -> Self::HandleFuture<'_> {
283 async move {
284 let events = self
286 .repository
287 .start_stream_atomic(command.session_id.into(), command.stream_id.into())
288 .await
289 .map_err(|e| match e {
290 crate::domain::DomainError::SessionNotFound(_) => ApplicationError::NotFound(
291 format!("Session {} not found", command.session_id),
292 ),
293 other => ApplicationError::Domain(other),
294 })?;
295
296 self.event_publisher
297 .publish_batch(events)
298 .await
299 .map_err(ApplicationError::Domain)?;
300
301 Ok(())
302 }
303 }
304}
305
306impl<R, P, F> CommandHandlerGat<CompleteStreamCommand> for SessionCommandHandler<R, P, F>
307where
308 R: StreamRepositoryGat + Send + Sync,
309 P: EventPublisherGat + Send + Sync,
310 F: FrameStoreGat + Send + Sync,
311{
312 type Response = ();
313
314 type HandleFuture<'a>
315 = impl std::future::Future<Output = ApplicationResult<Self::Response>> + Send + 'a
316 where
317 Self: 'a;
318
319 fn handle(&self, command: CompleteStreamCommand) -> Self::HandleFuture<'_> {
320 async move {
321 let events = self
325 .repository
326 .complete_stream_atomic(command.session_id.into(), command.stream_id.into())
327 .await
328 .map_err(|e| match e {
329 crate::domain::DomainError::SessionNotFound(_) => ApplicationError::NotFound(
330 format!("Session {} not found", command.session_id),
331 ),
332 other => ApplicationError::Domain(other),
333 })?;
334
335 self.event_publisher
336 .publish_batch(events)
337 .await
338 .map_err(ApplicationError::Domain)?;
339
340 Ok(())
341 }
342 }
343}
344
345impl<R, P, F> CommandHandlerGat<GenerateFramesCommand> for SessionCommandHandler<R, P, F>
346where
347 R: StreamRepositoryGat + Send + Sync,
348 P: EventPublisherGat + Send + Sync,
349 F: FrameStoreGat + Send + Sync,
350{
351 type Response = Vec<Frame>;
352
353 type HandleFuture<'a>
354 = impl std::future::Future<Output = ApplicationResult<Self::Response>> + Send + 'a
355 where
356 Self: 'a;
357
358 fn handle(&self, command: GenerateFramesCommand) -> Self::HandleFuture<'_> {
359 async move {
360 CommandValidator::validate_generate_frames(&command)
361 .map_err(|errors| ApplicationError::Validation(errors.join("; ")))?;
362
363 let priority = command
366 .priority_threshold
367 .try_into()
368 .map_err(ApplicationError::Domain)?;
369
370 let (frames, events) = self
372 .repository
373 .create_stream_patch_frames_atomic(
374 command.session_id.into(),
375 command.stream_id.into(),
376 priority,
377 command.max_frames,
378 )
379 .await
380 .map_err(|e| match e {
381 crate::domain::DomainError::SessionNotFound(_) => ApplicationError::NotFound(
382 format!("Session {} not found", command.session_id),
383 ),
384 crate::domain::DomainError::StreamNotFound(_) => ApplicationError::NotFound(
385 format!("Stream {} not found", command.stream_id),
386 ),
387 other => ApplicationError::Domain(other),
388 })?;
389
390 self.event_publisher
397 .publish_batch(events)
398 .await
399 .map_err(ApplicationError::Domain)?;
400
401 #[cfg(feature = "metrics")]
405 metrics::counter!("pjs_frames_total").increment(frames.len() as u64);
406
407 #[cfg(all(feature = "compression", not(target_arch = "wasm32")))]
410 self.train_from_frames(command.session_id.into(), &frames)
411 .await;
412
413 self.frame_store
415 .append_frames(command.stream_id.into(), frames.clone())
416 .await
417 .map_err(ApplicationError::Domain)?;
418
419 Ok(frames)
420 }
421 }
422}
423
424impl<R, P, F> CommandHandlerGat<BatchGenerateFramesCommand> for SessionCommandHandler<R, P, F>
425where
426 R: StreamRepositoryGat + Send + Sync,
427 P: EventPublisherGat + Send + Sync,
428 F: FrameStoreGat + Send + Sync,
429{
430 type Response = Vec<Frame>;
431
432 type HandleFuture<'a>
433 = impl std::future::Future<Output = ApplicationResult<Self::Response>> + Send + 'a
434 where
435 Self: 'a;
436
437 fn handle(&self, command: BatchGenerateFramesCommand) -> Self::HandleFuture<'_> {
438 async move {
439 let priority = command
448 .priority_threshold
449 .try_into()
450 .map_err(ApplicationError::Domain)?;
451
452 let (frames, events) = self
453 .repository
454 .batch_generate_frames_atomic(
455 command.session_id.into(),
456 priority,
457 command.max_frames,
458 )
459 .await
460 .map_err(|e| match e {
461 crate::domain::DomainError::SessionNotFound(_) => ApplicationError::NotFound(
462 format!("Session {} not found", command.session_id),
463 ),
464 other => ApplicationError::Domain(other),
465 })?;
466
467 self.event_publisher
468 .publish_batch(events)
469 .await
470 .map_err(ApplicationError::Domain)?;
471
472 #[cfg(feature = "metrics")]
476 metrics::counter!("pjs_frames_total").increment(frames.len() as u64);
477
478 #[cfg(all(feature = "compression", not(target_arch = "wasm32")))]
481 self.train_from_frames(command.session_id.into(), &frames)
482 .await;
483
484 self.persist_frames_grouped_by_stream(&frames).await?;
487
488 Ok(frames)
489 }
490 }
491}
492
493impl<R, P, F> CommandHandlerGat<CloseSessionCommand> for SessionCommandHandler<R, P, F>
494where
495 R: StreamRepositoryGat + Send + Sync,
496 P: EventPublisherGat + Send + Sync,
497 F: FrameStoreGat + Send + Sync,
498{
499 type Response = ();
500
501 type HandleFuture<'a>
502 = impl std::future::Future<Output = ApplicationResult<Self::Response>> + Send + 'a
503 where
504 Self: 'a;
505
506 fn handle(&self, command: CloseSessionCommand) -> Self::HandleFuture<'_> {
507 async move {
508 let events = self
510 .repository
511 .close_session_atomic(command.session_id.into())
512 .await
513 .map_err(|e| match e {
514 crate::domain::DomainError::SessionNotFound(_) => ApplicationError::NotFound(
515 format!("Session {} not found", command.session_id),
516 ),
517 other => ApplicationError::Domain(other),
518 })?;
519
520 self.event_publisher
521 .publish_batch(events)
522 .await
523 .map_err(ApplicationError::Domain)?;
524
525 Ok(())
526 }
527 }
528}
529
530pub struct CommandValidator;
540
541impl CommandValidator {
542 pub fn validate_create_session(command: &CreateSessionCommand) -> Result<(), Vec<String>> {
544 let mut errors = Vec::new();
545
546 if command.config.max_concurrent_streams == 0 {
547 errors.push("max_concurrent_streams must be greater than 0".to_string());
548 }
549
550 if command.config.session_timeout_seconds == 0 {
551 errors.push("session_timeout_seconds must be greater than 0".to_string());
552 }
553
554 if command.config.session_timeout_seconds > MAX_SESSION_TIMEOUT_SECONDS {
555 errors.push(format!(
556 "session_timeout_seconds cannot exceed {MAX_SESSION_TIMEOUT_SECONDS}"
557 ));
558 }
559
560 if errors.is_empty() {
561 Ok(())
562 } else {
563 Err(errors)
564 }
565 }
566
567 pub fn validate_create_stream(command: &CreateStreamCommand) -> Result<(), Vec<String>> {
569 let mut errors = Vec::new();
570
571 if command.source_data.is_null() {
572 errors.push("source_data cannot be null".to_string());
573 }
574
575 if errors.is_empty() {
576 Ok(())
577 } else {
578 Err(errors)
579 }
580 }
581
582 pub fn validate_generate_frames(command: &GenerateFramesCommand) -> Result<(), Vec<String>> {
584 let mut errors = Vec::new();
585
586 if command.max_frames == 0 {
587 errors.push("max_frames must be greater than 0".to_string());
588 }
589
590 if command.max_frames > MAX_FRAMES_PER_REQUEST {
591 errors.push(format!("max_frames cannot exceed {MAX_FRAMES_PER_REQUEST}"));
592 }
593
594 if errors.is_empty() {
595 Ok(())
596 } else {
597 Err(errors)
598 }
599 }
600}
601
602#[cfg(test)]
603mod tests {
604 use super::*;
605 use crate::domain::{
606 aggregates::stream_session::SessionConfig, events::DomainEvent, ports::EventPublisherGat,
607 };
608 use crate::test_support::MockRepository;
609
610 struct MockEventPublisher;
611
612 impl EventPublisherGat for MockEventPublisher {
613 type PublishFuture<'a>
614 = impl std::future::Future<Output = crate::domain::DomainResult<()>> + Send + 'a
615 where
616 Self: 'a;
617
618 type PublishBatchFuture<'a>
619 = impl std::future::Future<Output = crate::domain::DomainResult<()>> + Send + 'a
620 where
621 Self: 'a;
622
623 fn publish(&self, _event: DomainEvent) -> Self::PublishFuture<'_> {
624 async move { Ok(()) }
625 }
626
627 fn publish_batch(&self, _events: Vec<DomainEvent>) -> Self::PublishBatchFuture<'_> {
628 async move { Ok(()) }
629 }
630 }
631
632 struct RecordingEventPublisher {
636 batches: parking_lot::Mutex<Vec<Vec<DomainEvent>>>,
637 }
638
639 impl RecordingEventPublisher {
640 fn new() -> Self {
641 Self {
642 batches: parking_lot::Mutex::new(Vec::new()),
643 }
644 }
645
646 fn batches(&self) -> Vec<Vec<DomainEvent>> {
647 self.batches.lock().clone()
648 }
649 }
650
651 impl EventPublisherGat for RecordingEventPublisher {
652 type PublishFuture<'a>
653 = impl std::future::Future<Output = crate::domain::DomainResult<()>> + Send + 'a
654 where
655 Self: 'a;
656
657 type PublishBatchFuture<'a>
658 = impl std::future::Future<Output = crate::domain::DomainResult<()>> + Send + 'a
659 where
660 Self: 'a;
661
662 fn publish(&self, event: DomainEvent) -> Self::PublishFuture<'_> {
663 async move {
664 self.batches.lock().push(vec![event]);
665 Ok(())
666 }
667 }
668
669 fn publish_batch(&self, events: Vec<DomainEvent>) -> Self::PublishBatchFuture<'_> {
670 async move {
671 self.batches.lock().push(events);
672 Ok(())
673 }
674 }
675 }
676
677 struct TrackingEventPublisher {
681 published: parking_lot::Mutex<Vec<DomainEvent>>,
682 }
683
684 impl TrackingEventPublisher {
685 fn new() -> Self {
686 Self {
687 published: parking_lot::Mutex::new(Vec::new()),
688 }
689 }
690
691 fn published_events(&self) -> Vec<DomainEvent> {
692 self.published.lock().clone()
693 }
694 }
695
696 impl EventPublisherGat for TrackingEventPublisher {
697 type PublishFuture<'a>
698 = impl std::future::Future<Output = crate::domain::DomainResult<()>> + Send + 'a
699 where
700 Self: 'a;
701
702 type PublishBatchFuture<'a>
703 = impl std::future::Future<Output = crate::domain::DomainResult<()>> + Send + 'a
704 where
705 Self: 'a;
706
707 fn publish(&self, event: DomainEvent) -> Self::PublishFuture<'_> {
708 async move {
709 self.published.lock().push(event);
710 Ok(())
711 }
712 }
713
714 fn publish_batch(&self, events: Vec<DomainEvent>) -> Self::PublishBatchFuture<'_> {
715 async move {
716 self.published.lock().extend(events);
717 Ok(())
718 }
719 }
720 }
721
722 struct FailingFrameStore;
726
727 impl crate::domain::ports::FrameStoreGat for FailingFrameStore {
728 type AppendFramesFuture<'a>
729 = impl std::future::Future<Output = crate::domain::DomainResult<()>> + Send + 'a
730 where
731 Self: 'a;
732
733 type GetFramesFuture<'a>
734 = impl std::future::Future<
735 Output = crate::domain::DomainResult<crate::domain::ports::FrameStorePage>,
736 > + Send
737 + 'a
738 where
739 Self: 'a;
740
741 type DeleteFramesForStreamFuture<'a>
742 = impl std::future::Future<Output = crate::domain::DomainResult<()>> + Send + 'a
743 where
744 Self: 'a;
745
746 fn append_frames(
747 &self,
748 _stream_id: StreamId,
749 _frames: Vec<Frame>,
750 ) -> Self::AppendFramesFuture<'_> {
751 async move {
752 Err(crate::domain::DomainError::InvalidInput(
753 "simulated frame store failure".to_string(),
754 ))
755 }
756 }
757
758 fn get_frames(
759 &self,
760 _stream_id: StreamId,
761 _since_sequence: Option<u64>,
762 _priority_filter: Option<crate::domain::value_objects::Priority>,
763 _limit: Option<usize>,
764 ) -> Self::GetFramesFuture<'_> {
765 async move {
766 Ok(crate::domain::ports::FrameStorePage {
767 frames: Vec::new(),
768 total_matching: 0,
769 })
770 }
771 }
772
773 fn delete_frames_for_stream(
774 &self,
775 _stream_id: StreamId,
776 ) -> Self::DeleteFramesForStreamFuture<'_> {
777 async move { Ok(()) }
778 }
779 }
780 #[tokio::test]
781 async fn test_create_session_command() {
782 let repository = Arc::new(MockRepository::new());
783 let event_publisher = Arc::new(MockEventPublisher);
784 let handler = SessionCommandHandler::new(repository.clone(), event_publisher);
785
786 let command = CreateSessionCommand {
787 config: SessionConfig::default(),
788 client_info: Some("test-client".to_string()),
789 user_agent: None,
790 ip_address: None,
791 };
792
793 let result = handler.handle(command).await;
794 assert!(result.is_ok());
795
796 let session_id = result.unwrap();
797
798 let saved_session = repository.find_session(session_id).await.unwrap();
800 assert!(saved_session.is_some());
801 }
802
803 #[tokio::test]
804 async fn test_session_command_handler_creation() {
805 let repository = Arc::new(MockRepository::new());
806 let event_publisher = Arc::new(MockEventPublisher);
807 let handler = SessionCommandHandler::new(repository.clone(), event_publisher.clone());
808
809 assert!(std::ptr::eq(
810 handler.repository.as_ref(),
811 repository.as_ref()
812 ));
813 assert!(std::ptr::eq(
814 handler.event_publisher.as_ref(),
815 event_publisher.as_ref()
816 ));
817 }
818
819 #[tokio::test]
820 async fn test_create_session_with_full_client_info() {
821 let repository = Arc::new(MockRepository::new());
822 let event_publisher = Arc::new(MockEventPublisher);
823 let handler = SessionCommandHandler::new(repository.clone(), event_publisher);
824
825 let command = CreateSessionCommand {
826 config: SessionConfig::default(),
827 client_info: Some("test-client".to_string()),
828 user_agent: Some("Mozilla/5.0".to_string()),
829 ip_address: Some("192.168.1.1".to_string()),
830 };
831
832 let result = handler.handle(command).await;
833 assert!(result.is_ok());
834
835 let session_id = result.unwrap();
836 let saved_session = repository.find_session(session_id).await.unwrap();
837 assert!(saved_session.is_some());
838 }
839
840 #[tokio::test]
841 async fn test_create_session_without_client_info() {
842 let repository = Arc::new(MockRepository::new());
843 let event_publisher = Arc::new(MockEventPublisher);
844 let handler = SessionCommandHandler::new(repository, event_publisher);
845
846 let command = CreateSessionCommand {
847 config: SessionConfig::default(),
848 client_info: None,
849 user_agent: None,
850 ip_address: None,
851 };
852
853 let result = handler.handle(command).await;
854 assert!(result.is_ok());
855 }
856
857 #[tokio::test]
858 async fn test_create_stream_command() {
859 let repository = Arc::new(MockRepository::new());
860 let event_publisher = Arc::new(MockEventPublisher);
861 let handler = SessionCommandHandler::new(repository.clone(), event_publisher);
862
863 let create_session_cmd = CreateSessionCommand {
865 config: SessionConfig::default(),
866 client_info: None,
867 user_agent: None,
868 ip_address: None,
869 };
870
871 let session_id = handler.handle(create_session_cmd).await.unwrap();
872
873 let create_stream_cmd = CreateStreamCommand {
875 session_id: session_id.into(),
876 source_data: serde_json::json!({"test": "data"}).into(),
877 config: None,
878 };
879
880 let result = handler.handle(create_stream_cmd).await;
881 assert!(result.is_ok());
882
883 let stream_id = result.unwrap();
884 assert_ne!(stream_id, StreamId::new()); }
886
887 #[tokio::test]
888 async fn test_create_stream_with_config() {
889 let repository = Arc::new(MockRepository::new());
890 let event_publisher = Arc::new(MockEventPublisher);
891 let handler = SessionCommandHandler::new(repository.clone(), event_publisher);
892
893 let session_id = handler
895 .handle(CreateSessionCommand {
896 config: SessionConfig::default(),
897 client_info: None,
898 user_agent: None,
899 ip_address: None,
900 })
901 .await
902 .unwrap();
903
904 let stream_config = crate::domain::entities::stream::StreamConfig::default();
906 let create_stream_cmd = CreateStreamCommand {
907 session_id: session_id.into(),
908 source_data: serde_json::json!({"test": "data"}).into(),
909 config: Some(stream_config),
910 };
911
912 let result = handler.handle(create_stream_cmd).await;
913 assert!(result.is_ok());
914 }
915
916 #[tokio::test]
917 async fn test_create_stream_session_not_found() {
918 let repository = Arc::new(MockRepository::new());
919 let event_publisher = Arc::new(MockEventPublisher);
920 let handler = SessionCommandHandler::new(repository, event_publisher);
921
922 let non_existent_session_id = SessionId::new();
923 let create_stream_cmd = CreateStreamCommand {
924 session_id: non_existent_session_id.into(),
925 source_data: serde_json::json!({"test": "data"}).into(),
926 config: None,
927 };
928
929 let result = handler.handle(create_stream_cmd).await;
930 assert!(result.is_err());
931 assert!(matches!(
932 result.err().unwrap(),
933 ApplicationError::NotFound(_)
934 ));
935 }
936
937 #[tokio::test]
938 async fn test_start_stream_command() {
939 let repository = Arc::new(MockRepository::new());
940 let event_publisher = Arc::new(MockEventPublisher);
941 let handler = SessionCommandHandler::new(repository.clone(), event_publisher);
942
943 let session_id = handler
945 .handle(CreateSessionCommand {
946 config: SessionConfig::default(),
947 client_info: None,
948 user_agent: None,
949 ip_address: None,
950 })
951 .await
952 .unwrap();
953
954 let stream_id = handler
955 .handle(CreateStreamCommand {
956 session_id: session_id.into(),
957 source_data: serde_json::json!({"test": "data"}).into(),
958 config: None,
959 })
960 .await
961 .unwrap();
962
963 let start_stream_cmd = StartStreamCommand {
965 session_id: session_id.into(),
966 stream_id: stream_id.into(),
967 };
968
969 let result = handler.handle(start_stream_cmd).await;
970 assert!(result.is_ok());
971 }
972
973 #[tokio::test]
974 async fn test_start_stream_session_not_found() {
975 let repository = Arc::new(MockRepository::new());
976 let event_publisher = Arc::new(MockEventPublisher);
977 let handler = SessionCommandHandler::new(repository, event_publisher);
978
979 let start_stream_cmd = StartStreamCommand {
980 session_id: SessionId::new().into(),
981 stream_id: StreamId::new().into(),
982 };
983
984 let result = handler.handle(start_stream_cmd).await;
985 assert!(result.is_err());
986 assert!(matches!(
987 result.err().unwrap(),
988 ApplicationError::NotFound(_)
989 ));
990 }
991
992 #[tokio::test]
993 async fn test_complete_stream_command() {
994 let repository = Arc::new(MockRepository::new());
995 let event_publisher = Arc::new(MockEventPublisher);
996 let handler = SessionCommandHandler::new(repository.clone(), event_publisher);
997
998 let session_id = handler
1000 .handle(CreateSessionCommand {
1001 config: SessionConfig::default(),
1002 client_info: None,
1003 user_agent: None,
1004 ip_address: None,
1005 })
1006 .await
1007 .unwrap();
1008
1009 let stream_id = handler
1010 .handle(CreateStreamCommand {
1011 session_id: session_id.into(),
1012 source_data: serde_json::json!({"test": "data"}).into(),
1013 config: None,
1014 })
1015 .await
1016 .unwrap();
1017
1018 handler
1019 .handle(StartStreamCommand {
1020 session_id: session_id.into(),
1021 stream_id: stream_id.into(),
1022 })
1023 .await
1024 .unwrap();
1025
1026 let complete_stream_cmd = CompleteStreamCommand {
1028 session_id: session_id.into(),
1029 stream_id: stream_id.into(),
1030 checksum: Some("abc123".to_string()),
1031 };
1032
1033 let result = handler.handle(complete_stream_cmd).await;
1034 assert!(result.is_ok());
1035 }
1036
1037 #[tokio::test]
1038 async fn test_complete_stream_without_checksum() {
1039 let repository = Arc::new(MockRepository::new());
1040 let event_publisher = Arc::new(MockEventPublisher);
1041 let handler = SessionCommandHandler::new(repository.clone(), event_publisher);
1042
1043 let session_id = handler
1045 .handle(CreateSessionCommand {
1046 config: SessionConfig::default(),
1047 client_info: None,
1048 user_agent: None,
1049 ip_address: None,
1050 })
1051 .await
1052 .unwrap();
1053
1054 let stream_id = handler
1055 .handle(CreateStreamCommand {
1056 session_id: session_id.into(),
1057 source_data: serde_json::json!({"test": "data"}).into(),
1058 config: None,
1059 })
1060 .await
1061 .unwrap();
1062
1063 handler
1064 .handle(StartStreamCommand {
1065 session_id: session_id.into(),
1066 stream_id: stream_id.into(),
1067 })
1068 .await
1069 .unwrap();
1070
1071 let complete_stream_cmd = CompleteStreamCommand {
1073 session_id: session_id.into(),
1074 stream_id: stream_id.into(),
1075 checksum: None,
1076 };
1077
1078 let result = handler.handle(complete_stream_cmd).await;
1079 assert!(result.is_ok());
1080 }
1081
1082 #[tokio::test]
1083 async fn test_close_session_command() {
1084 let repository = Arc::new(MockRepository::new());
1085 let event_publisher = Arc::new(MockEventPublisher);
1086 let handler = SessionCommandHandler::new(repository.clone(), event_publisher);
1087
1088 let session_id = handler
1090 .handle(CreateSessionCommand {
1091 config: SessionConfig::default(),
1092 client_info: None,
1093 user_agent: None,
1094 ip_address: None,
1095 })
1096 .await
1097 .unwrap();
1098
1099 let close_session_cmd = CloseSessionCommand {
1101 session_id: session_id.into(),
1102 };
1103
1104 let result = handler.handle(close_session_cmd).await;
1105 assert!(result.is_ok());
1106 }
1107
1108 #[tokio::test]
1109 async fn test_close_session_not_found() {
1110 let repository = Arc::new(MockRepository::new());
1111 let event_publisher = Arc::new(MockEventPublisher);
1112 let handler = SessionCommandHandler::new(repository, event_publisher);
1113
1114 let close_session_cmd = CloseSessionCommand {
1115 session_id: SessionId::new().into(),
1116 };
1117
1118 let result = handler.handle(close_session_cmd).await;
1119 assert!(result.is_err());
1120 assert!(matches!(
1121 result.err().unwrap(),
1122 ApplicationError::NotFound(_)
1123 ));
1124 }
1125
1126 #[cfg(all(feature = "compression", not(target_arch = "wasm32")))]
1127 mod dictionary_wiring {
1128 use super::*;
1132 use crate::{
1133 compression::zstd::N_TRAIN,
1134 domain::{
1135 entities::Frame,
1136 ports::{DictionaryFuture, DictionaryStore},
1137 },
1138 infrastructure::repositories::InMemoryDictionaryStore,
1139 security::CompressionBombDetector,
1140 };
1141 use pjson_rs_domain::value_objects::{JsonData, StreamId};
1142 use std::sync::atomic::{AtomicUsize, Ordering};
1143
1144 struct CountingDictionaryStore {
1147 inner: InMemoryDictionaryStore,
1148 calls: AtomicUsize,
1149 }
1150
1151 impl CountingDictionaryStore {
1152 fn new() -> Self {
1153 Self {
1154 inner: InMemoryDictionaryStore::new(
1155 Arc::new(CompressionBombDetector::default()),
1156 64 * 1024,
1157 ),
1158 calls: AtomicUsize::new(0),
1159 }
1160 }
1161
1162 fn call_count(&self) -> usize {
1163 self.calls.load(Ordering::SeqCst)
1164 }
1165 }
1166
1167 impl DictionaryStore for CountingDictionaryStore {
1168 fn get_dictionary<'a>(
1169 &'a self,
1170 session_id: SessionId,
1171 ) -> DictionaryFuture<'a, Option<Arc<crate::compression::zstd::ZstdDictionary>>>
1172 {
1173 self.inner.get_dictionary(session_id)
1174 }
1175
1176 fn train_if_ready<'a>(
1177 &'a self,
1178 session_id: SessionId,
1179 sample: Vec<u8>,
1180 ) -> DictionaryFuture<'a, ()> {
1181 self.calls.fetch_add(1, Ordering::SeqCst);
1182 self.inner.train_if_ready(session_id, sample)
1183 }
1184 }
1185
1186 fn make_patch_frame(stream_id: StreamId, sequence: u64, n: usize) -> Frame {
1187 let patch = crate::domain::entities::frame::FramePatch::set(
1188 pjson_rs_domain::value_objects::JsonPath::new(format!("$.items[{n}]")).unwrap(),
1189 JsonData::Integer(n as i64),
1190 );
1191 Frame::patch(
1192 stream_id,
1193 sequence,
1194 pjson_rs_domain::value_objects::Priority::HIGH,
1195 vec![patch],
1196 )
1197 .unwrap()
1198 }
1199
1200 #[tokio::test]
1201 async fn test_train_from_frames_records_each_payload() {
1202 let store = Arc::new(CountingDictionaryStore::new());
1203 let handler = SessionCommandHandler::with_dictionary_store(
1204 Arc::new(MockRepository::new()),
1205 Arc::new(MockEventPublisher),
1206 store.clone(),
1207 );
1208
1209 let session_id = SessionId::new();
1210 let stream_id = StreamId::new();
1211 let frames: Vec<Frame> = (0..5)
1212 .map(|i| make_patch_frame(stream_id, i as u64, i))
1213 .collect();
1214
1215 handler.train_from_frames(session_id, &frames).await;
1216
1217 assert_eq!(
1218 store.call_count(),
1219 5,
1220 "every accepted frame must feed train_if_ready"
1221 );
1222 }
1223
1224 #[tokio::test]
1225 async fn test_train_from_frames_fires_dictionary_after_threshold() {
1226 let store = Arc::new(InMemoryDictionaryStore::new(
1227 Arc::new(CompressionBombDetector::default()),
1228 64 * 1024,
1229 ));
1230 let handler = SessionCommandHandler::with_dictionary_store(
1231 Arc::new(MockRepository::new()),
1232 Arc::new(MockEventPublisher),
1233 store.clone(),
1234 );
1235
1236 let session_id = SessionId::new();
1237 let stream_id = StreamId::new();
1238 let frames: Vec<Frame> = (0..N_TRAIN)
1239 .map(|i| make_patch_frame(stream_id, i as u64, i))
1240 .collect();
1241
1242 handler.train_from_frames(session_id, &frames).await;
1243
1244 let dict = store.get_dictionary(session_id).await.unwrap();
1245 assert!(
1246 dict.is_some(),
1247 "dictionary must be trained once N_TRAIN frame payloads have been ingested"
1248 );
1249 }
1250 }
1251
1252 #[tokio::test]
1253 async fn test_generate_frames_persists_into_frame_store() {
1254 use crate::domain::ports::FrameStoreGat;
1255 use crate::infrastructure::adapters::InMemoryFrameStore;
1256
1257 let repository = Arc::new(MockRepository::new());
1258 let event_publisher = Arc::new(MockEventPublisher);
1259 let frame_store = Arc::new(InMemoryFrameStore::new());
1260
1261 let handler = SessionCommandHandler::with_stores(
1262 repository.clone(),
1263 event_publisher,
1264 Arc::new(crate::domain::ports::NoopDictionaryStore),
1265 frame_store.clone(),
1266 );
1267
1268 let session_id = handler
1270 .handle(CreateSessionCommand {
1271 config: SessionConfig::default(),
1272 client_info: None,
1273 user_agent: None,
1274 ip_address: None,
1275 })
1276 .await
1277 .unwrap();
1278
1279 let stream_id = handler
1280 .handle(CreateStreamCommand {
1281 session_id: session_id.into(),
1282 source_data: serde_json::json!({"items": [1, 2, 3, 4]}).into(),
1283 config: None,
1284 })
1285 .await
1286 .unwrap();
1287
1288 handler
1290 .handle(StartStreamCommand {
1291 session_id: session_id.into(),
1292 stream_id: stream_id.into(),
1293 })
1294 .await
1295 .unwrap();
1296
1297 let frames = handler
1300 .handle(GenerateFramesCommand {
1301 session_id: session_id.into(),
1302 stream_id: stream_id.into(),
1303 priority_threshold: crate::application::dto::PriorityDto::new(1).unwrap(),
1304 max_frames: 8,
1305 })
1306 .await
1307 .unwrap();
1308
1309 assert!(
1310 !frames.is_empty(),
1311 "command must produce at least one frame"
1312 );
1313
1314 let page = frame_store
1315 .get_frames(stream_id, None, None, None)
1316 .await
1317 .unwrap();
1318 assert_eq!(
1319 page.frames.len(),
1320 frames.len(),
1321 "every frame returned by the command must be persisted",
1322 );
1323 assert_eq!(page.total_matching, frames.len());
1324 }
1325
1326 #[tokio::test]
1327 async fn test_generate_frames_publishes_events_before_frame_store_failure() {
1328 let repository = Arc::new(MockRepository::new());
1334 let event_publisher = Arc::new(TrackingEventPublisher::new());
1335 let frame_store = Arc::new(FailingFrameStore);
1336
1337 let handler = SessionCommandHandler::with_stores(
1338 repository.clone(),
1339 event_publisher.clone(),
1340 Arc::new(crate::domain::ports::NoopDictionaryStore),
1341 frame_store,
1342 );
1343
1344 let session_id = handler
1345 .handle(CreateSessionCommand {
1346 config: SessionConfig::default(),
1347 client_info: None,
1348 user_agent: None,
1349 ip_address: None,
1350 })
1351 .await
1352 .unwrap();
1353
1354 let stream_id = handler
1355 .handle(CreateStreamCommand {
1356 session_id: session_id.into(),
1357 source_data: serde_json::json!({"items": [1, 2, 3, 4]}).into(),
1358 config: None,
1359 })
1360 .await
1361 .unwrap();
1362
1363 handler
1364 .handle(StartStreamCommand {
1365 session_id: session_id.into(),
1366 stream_id: stream_id.into(),
1367 })
1368 .await
1369 .unwrap();
1370
1371 let events_before = event_publisher.published_events().len();
1375
1376 let result = handler
1377 .handle(GenerateFramesCommand {
1378 session_id: session_id.into(),
1379 stream_id: stream_id.into(),
1380 priority_threshold: crate::application::dto::PriorityDto::new(1).unwrap(),
1381 max_frames: 8,
1382 })
1383 .await;
1384
1385 assert!(
1386 result.is_err(),
1387 "FailingFrameStore must cause the command to fail"
1388 );
1389
1390 let events_after = event_publisher.published_events().len();
1391 assert!(
1392 events_after > events_before,
1393 "FramesBatched must be published even though append_frames failed \
1394 (was {events_before}, now {events_after})"
1395 );
1396 }
1397
1398 #[tokio::test]
1399 async fn test_create_session_rejects_zero_max_concurrent_streams() {
1400 let repository = Arc::new(MockRepository::new());
1401 let event_publisher = Arc::new(MockEventPublisher);
1402 let handler = SessionCommandHandler::new(repository, event_publisher);
1403
1404 let config = SessionConfig {
1405 max_concurrent_streams: 0,
1406 ..Default::default()
1407 };
1408
1409 let result = handler
1410 .handle(CreateSessionCommand {
1411 config,
1412 client_info: None,
1413 user_agent: None,
1414 ip_address: None,
1415 })
1416 .await;
1417
1418 assert!(matches!(result, Err(ApplicationError::Validation(_))));
1419 }
1420
1421 #[tokio::test]
1422 async fn test_create_session_rejects_zero_session_timeout() {
1423 let repository = Arc::new(MockRepository::new());
1424 let event_publisher = Arc::new(MockEventPublisher);
1425 let handler = SessionCommandHandler::new(repository, event_publisher);
1426
1427 let config = SessionConfig {
1428 session_timeout_seconds: 0,
1429 ..Default::default()
1430 };
1431
1432 let result = handler
1433 .handle(CreateSessionCommand {
1434 config,
1435 client_info: None,
1436 user_agent: None,
1437 ip_address: None,
1438 })
1439 .await;
1440
1441 assert!(matches!(result, Err(ApplicationError::Validation(_))));
1442 }
1443
1444 #[tokio::test]
1445 async fn test_create_stream_rejects_null_source_data_before_session_lookup() {
1446 let repository = Arc::new(MockRepository::new());
1447 let event_publisher = Arc::new(MockEventPublisher);
1448 let handler = SessionCommandHandler::new(repository, event_publisher);
1449
1450 let result = handler
1453 .handle(CreateStreamCommand {
1454 session_id: SessionId::new().into(),
1455 source_data: serde_json::Value::Null.into(),
1456 config: None,
1457 })
1458 .await;
1459
1460 assert!(matches!(result, Err(ApplicationError::Validation(_))));
1461 }
1462
1463 #[tokio::test]
1464 async fn test_generate_frames_rejects_zero_max_frames() {
1465 let repository = Arc::new(MockRepository::new());
1466 let event_publisher = Arc::new(MockEventPublisher);
1467 let handler = SessionCommandHandler::new(repository, event_publisher);
1468
1469 let result = handler
1470 .handle(GenerateFramesCommand {
1471 session_id: SessionId::new().into(),
1472 stream_id: StreamId::new().into(),
1473 priority_threshold: crate::application::dto::PriorityDto::new(1).unwrap(),
1474 max_frames: 0,
1475 })
1476 .await;
1477
1478 assert!(matches!(result, Err(ApplicationError::Validation(_))));
1479 }
1480
1481 #[tokio::test]
1482 async fn test_generate_frames_rejects_max_frames_over_limit() {
1483 let repository = Arc::new(MockRepository::new());
1484 let event_publisher = Arc::new(MockEventPublisher);
1485 let handler = SessionCommandHandler::new(repository, event_publisher);
1486
1487 let result = handler
1488 .handle(GenerateFramesCommand {
1489 session_id: SessionId::new().into(),
1490 stream_id: StreamId::new().into(),
1491 priority_threshold: crate::application::dto::PriorityDto::new(1).unwrap(),
1492 max_frames: MAX_FRAMES_PER_REQUEST + 1,
1493 })
1494 .await;
1495
1496 assert!(matches!(result, Err(ApplicationError::Validation(_))));
1497 }
1498
1499 #[tokio::test]
1500 async fn test_batch_generate_frames_persists_into_frame_store() {
1501 use crate::domain::ports::FrameStoreGat;
1502 use crate::infrastructure::adapters::InMemoryFrameStore;
1503
1504 let repository = Arc::new(MockRepository::new());
1505 let event_publisher = Arc::new(MockEventPublisher);
1506 let frame_store = Arc::new(InMemoryFrameStore::new());
1507
1508 let handler = SessionCommandHandler::with_stores(
1509 repository.clone(),
1510 event_publisher,
1511 Arc::new(crate::domain::ports::NoopDictionaryStore),
1512 frame_store.clone(),
1513 );
1514
1515 let session_id = handler
1516 .handle(CreateSessionCommand {
1517 config: SessionConfig::default(),
1518 client_info: None,
1519 user_agent: None,
1520 ip_address: None,
1521 })
1522 .await
1523 .unwrap();
1524
1525 let stream_id = handler
1526 .handle(CreateStreamCommand {
1527 session_id: session_id.into(),
1528 source_data: serde_json::json!({"items": [1, 2, 3, 4]}).into(),
1529 config: None,
1530 })
1531 .await
1532 .unwrap();
1533
1534 handler
1535 .handle(StartStreamCommand {
1536 session_id: session_id.into(),
1537 stream_id: stream_id.into(),
1538 })
1539 .await
1540 .unwrap();
1541
1542 let frames = handler
1546 .handle(BatchGenerateFramesCommand {
1547 session_id: session_id.into(),
1548 priority_threshold: crate::application::dto::PriorityDto::new(1).unwrap(),
1549 max_frames: 8,
1550 })
1551 .await
1552 .unwrap();
1553
1554 assert!(
1555 !frames.is_empty(),
1556 "command must produce at least one frame"
1557 );
1558
1559 let page = frame_store
1560 .get_frames(stream_id, None, None, None)
1561 .await
1562 .unwrap();
1563 assert_eq!(
1564 page.frames.len(),
1565 frames.len(),
1566 "every frame returned by the command must be persisted",
1567 );
1568 }
1569
1570 #[tokio::test]
1571 async fn test_batch_generate_frames_session_not_found() {
1572 let repository = Arc::new(MockRepository::new());
1573 let event_publisher = Arc::new(MockEventPublisher);
1574 let handler = SessionCommandHandler::new(repository, event_publisher);
1575
1576 let result = handler
1577 .handle(BatchGenerateFramesCommand {
1578 session_id: SessionId::new().into(),
1579 priority_threshold: crate::application::dto::PriorityDto::new(1).unwrap(),
1580 max_frames: 8,
1581 })
1582 .await;
1583
1584 assert!(matches!(result, Err(ApplicationError::NotFound(_))));
1585 }
1586
1587 #[tokio::test]
1588 async fn test_batch_generate_frames_no_streaming_streams_returns_empty() {
1589 let repository = Arc::new(MockRepository::new());
1590 let event_publisher = Arc::new(MockEventPublisher);
1591 let handler = SessionCommandHandler::new(repository, event_publisher);
1592
1593 let session_id = handler
1594 .handle(CreateSessionCommand {
1595 config: SessionConfig::default(),
1596 client_info: None,
1597 user_agent: None,
1598 ip_address: None,
1599 })
1600 .await
1601 .unwrap();
1602
1603 let stream_id = handler
1604 .handle(CreateStreamCommand {
1605 session_id: session_id.into(),
1606 source_data: serde_json::json!({"a": 1}).into(),
1607 config: None,
1608 })
1609 .await
1610 .unwrap();
1611 handler
1612 .handle(StartStreamCommand {
1613 session_id: session_id.into(),
1614 stream_id: stream_id.into(),
1615 })
1616 .await
1617 .unwrap();
1618 handler
1619 .handle(CompleteStreamCommand {
1620 session_id: session_id.into(),
1621 stream_id: stream_id.into(),
1622 checksum: None,
1623 })
1624 .await
1625 .unwrap();
1626
1627 let frames = handler
1630 .handle(BatchGenerateFramesCommand {
1631 session_id: session_id.into(),
1632 priority_threshold: crate::application::dto::PriorityDto::new(1).unwrap(),
1633 max_frames: 8,
1634 })
1635 .await
1636 .unwrap();
1637
1638 assert!(frames.is_empty());
1639 }
1640
1641 #[tokio::test]
1648 async fn test_batch_generate_frames_priority_threshold_filters_low_priority_patches() {
1649 let repository = Arc::new(MockRepository::new());
1650 let event_publisher = Arc::new(MockEventPublisher);
1651 let handler = SessionCommandHandler::new(repository, event_publisher);
1652
1653 let session_id = handler
1654 .handle(CreateSessionCommand {
1655 config: SessionConfig::default(),
1656 client_info: None,
1657 user_agent: None,
1658 ip_address: None,
1659 })
1660 .await
1661 .unwrap();
1662
1663 let stream_id = handler
1664 .handle(CreateStreamCommand {
1665 session_id: session_id.into(),
1666 source_data: serde_json::json!({"logs": "background noise"}).into(),
1667 config: None,
1668 })
1669 .await
1670 .unwrap();
1671 handler
1672 .handle(StartStreamCommand {
1673 session_id: session_id.into(),
1674 stream_id: stream_id.into(),
1675 })
1676 .await
1677 .unwrap();
1678
1679 let frames = handler
1680 .handle(BatchGenerateFramesCommand {
1681 session_id: session_id.into(),
1682 priority_threshold: crate::application::dto::PriorityDto::new(
1683 crate::domain::value_objects::Priority::HIGH.value(),
1684 )
1685 .unwrap(),
1686 max_frames: 8,
1687 })
1688 .await
1689 .unwrap();
1690
1691 assert!(
1692 frames.is_empty(),
1693 "a HIGH priority_threshold must filter out the BACKGROUND-priority \
1694 `logs` patch instead of falling back to Priority::BACKGROUND"
1695 );
1696 }
1697
1698 #[tokio::test]
1699 async fn test_batch_generate_frames_max_frames_zero_returns_empty() {
1700 let repository = Arc::new(MockRepository::new());
1701 let event_publisher = Arc::new(MockEventPublisher);
1702 let handler = SessionCommandHandler::new(repository, event_publisher);
1703
1704 let session_id = handler
1705 .handle(CreateSessionCommand {
1706 config: SessionConfig::default(),
1707 client_info: None,
1708 user_agent: None,
1709 ip_address: None,
1710 })
1711 .await
1712 .unwrap();
1713
1714 let stream_id = handler
1715 .handle(CreateStreamCommand {
1716 session_id: session_id.into(),
1717 source_data: serde_json::json!({"a": 1}).into(),
1718 config: None,
1719 })
1720 .await
1721 .unwrap();
1722 handler
1723 .handle(StartStreamCommand {
1724 session_id: session_id.into(),
1725 stream_id: stream_id.into(),
1726 })
1727 .await
1728 .unwrap();
1729
1730 let frames = handler
1734 .handle(BatchGenerateFramesCommand {
1735 session_id: session_id.into(),
1736 priority_threshold: crate::application::dto::PriorityDto::new(1).unwrap(),
1737 max_frames: 0,
1738 })
1739 .await
1740 .unwrap();
1741
1742 assert!(frames.is_empty());
1743 }
1744
1745 #[tokio::test]
1753 async fn test_batch_generate_frames_closed_session_returns_domain_error() {
1754 let repository = Arc::new(MockRepository::new());
1755 let event_publisher = Arc::new(MockEventPublisher);
1756 let handler = SessionCommandHandler::new(repository, event_publisher);
1757
1758 let session_id = handler
1759 .handle(CreateSessionCommand {
1760 config: SessionConfig::default(),
1761 client_info: None,
1762 user_agent: None,
1763 ip_address: None,
1764 })
1765 .await
1766 .unwrap();
1767
1768 handler
1769 .handle(CloseSessionCommand {
1770 session_id: session_id.into(),
1771 })
1772 .await
1773 .unwrap();
1774
1775 let result = handler
1776 .handle(BatchGenerateFramesCommand {
1777 session_id: session_id.into(),
1778 priority_threshold: crate::application::dto::PriorityDto::new(1).unwrap(),
1779 max_frames: 8,
1780 })
1781 .await;
1782
1783 assert!(matches!(
1784 result,
1785 Err(ApplicationError::Domain(
1786 crate::domain::DomainError::InvalidSessionState(_)
1787 ))
1788 ));
1789 }
1790
1791 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
1798 async fn test_batch_generate_frames_concurrent_with_complete_stream_loses_no_update() {
1799 let repository = Arc::new(MockRepository::new());
1800 let event_publisher = Arc::new(MockEventPublisher);
1801 let handler = Arc::new(SessionCommandHandler::new(repository, event_publisher));
1802
1803 let session_id = handler
1804 .handle(CreateSessionCommand {
1805 config: SessionConfig::default(),
1806 client_info: None,
1807 user_agent: None,
1808 ip_address: None,
1809 })
1810 .await
1811 .unwrap();
1812
1813 let frame_stream_id = handler
1820 .handle(CreateStreamCommand {
1821 session_id: session_id.into(),
1822 source_data: serde_json::json!({"a": 1, "b": 2}).into(),
1823 config: None,
1824 })
1825 .await
1826 .unwrap();
1827 handler
1828 .handle(StartStreamCommand {
1829 session_id: session_id.into(),
1830 stream_id: frame_stream_id.into(),
1831 })
1832 .await
1833 .unwrap();
1834
1835 let complete_stream_id = handler
1836 .handle(CreateStreamCommand {
1837 session_id: session_id.into(),
1838 source_data: serde_json::json!({"c": 3}).into(),
1839 config: None,
1840 })
1841 .await
1842 .unwrap();
1843 handler
1844 .handle(StartStreamCommand {
1845 session_id: session_id.into(),
1846 stream_id: complete_stream_id.into(),
1847 })
1848 .await
1849 .unwrap();
1850
1851 const N: usize = 25;
1852 let barrier = Arc::new(tokio::sync::Barrier::new(N + 1));
1853
1854 let mut handles = Vec::with_capacity(N + 1);
1855 for _ in 0..N {
1856 let handler = Arc::clone(&handler);
1857 let barrier = Arc::clone(&barrier);
1858 handles.push(tokio::spawn(async move {
1859 barrier.wait().await;
1860 handler
1861 .handle(BatchGenerateFramesCommand {
1862 session_id: session_id.into(),
1863 priority_threshold: crate::application::dto::PriorityDto::new(1).unwrap(),
1864 max_frames: 2,
1865 })
1866 .await
1867 .unwrap()
1868 }));
1869 }
1870 {
1871 let handler = Arc::clone(&handler);
1872 let barrier = Arc::clone(&barrier);
1873 handles.push(tokio::spawn(async move {
1874 barrier.wait().await;
1875 handler
1876 .handle(CompleteStreamCommand {
1877 session_id: session_id.into(),
1878 stream_id: complete_stream_id.into(),
1879 checksum: None,
1880 })
1881 .await
1882 .unwrap();
1883 vec![]
1884 }));
1885 }
1886
1887 let mut total_frames = 0usize;
1888 for handle in handles {
1889 total_frames += handle.await.unwrap().len();
1890 }
1891 assert_eq!(total_frames, N * 2);
1892
1893 let session = handler
1894 .repository
1895 .find_session(session_id)
1896 .await
1897 .unwrap()
1898 .unwrap();
1899 assert_eq!(session.stats().total_frames, (N * 2) as u64);
1900 assert_eq!(session.stats().completed_streams, 1);
1901 }
1902
1903 #[tokio::test]
1904 async fn test_create_session_rejects_session_timeout_that_would_panic_chrono() {
1905 let repository = Arc::new(MockRepository::new());
1906 let event_publisher = Arc::new(MockEventPublisher);
1907 let handler = SessionCommandHandler::new(repository, event_publisher);
1908
1909 let config = SessionConfig {
1913 session_timeout_seconds: 9_223_372_036_854_776,
1914 ..Default::default()
1915 };
1916
1917 let result = handler
1918 .handle(CreateSessionCommand {
1919 config,
1920 client_info: None,
1921 user_agent: None,
1922 ip_address: None,
1923 })
1924 .await;
1925
1926 assert!(matches!(result, Err(ApplicationError::Validation(_))));
1927 }
1928
1929 #[tokio::test]
1930 async fn test_create_session_rejects_session_timeout_that_would_wrap_negative() {
1931 let repository = Arc::new(MockRepository::new());
1932 let event_publisher = Arc::new(MockEventPublisher);
1933 let handler = SessionCommandHandler::new(repository, event_publisher);
1934
1935 let config = SessionConfig {
1938 session_timeout_seconds: u64::MAX,
1939 ..Default::default()
1940 };
1941
1942 let result = handler
1943 .handle(CreateSessionCommand {
1944 config,
1945 client_info: None,
1946 user_agent: None,
1947 ip_address: None,
1948 })
1949 .await;
1950
1951 assert!(matches!(result, Err(ApplicationError::Validation(_))));
1952 }
1953
1954 #[tokio::test]
1959 async fn test_save_and_publish_does_not_leak_pending_events_across_commands() {
1960 let repository = Arc::new(MockRepository::new());
1961 let event_publisher = Arc::new(RecordingEventPublisher::new());
1962 let handler = SessionCommandHandler::new(repository.clone(), event_publisher.clone());
1963
1964 let session_id = handler
1965 .handle(CreateSessionCommand {
1966 config: SessionConfig::default(),
1967 client_info: None,
1968 user_agent: None,
1969 ip_address: None,
1970 })
1971 .await
1972 .unwrap();
1973
1974 let after_create_session = repository.find_session(session_id).await.unwrap().unwrap();
1975 assert!(
1976 after_create_session.pending_events().is_empty(),
1977 "persisted session must not carry undrained events after CreateSessionCommand"
1978 );
1979
1980 let stream_id = handler
1981 .handle(CreateStreamCommand {
1982 session_id: session_id.into(),
1983 source_data: serde_json::json!({"test": "data"}).into(),
1984 config: None,
1985 })
1986 .await
1987 .unwrap();
1988
1989 let after_create_stream = repository.find_session(session_id).await.unwrap().unwrap();
1990 assert!(
1991 after_create_stream.pending_events().is_empty(),
1992 "persisted session must not carry undrained events after CreateStreamCommand"
1993 );
1994
1995 handler
1996 .handle(StartStreamCommand {
1997 session_id: session_id.into(),
1998 stream_id: stream_id.into(),
1999 })
2000 .await
2001 .unwrap();
2002
2003 let after_start_stream = repository.find_session(session_id).await.unwrap().unwrap();
2004 assert!(
2005 after_start_stream.pending_events().is_empty(),
2006 "persisted session must not carry undrained events after StartStreamCommand"
2007 );
2008
2009 let batches = event_publisher.batches();
2010 assert_eq!(
2011 batches.len(),
2012 3,
2013 "each command's save_and_publish call must publish exactly one batch"
2014 );
2015 let (create_session_events, create_stream_events, start_stream_events) =
2016 (&batches[0], &batches[1], &batches[2]);
2017
2018 assert_eq!(
2022 create_session_events.len(),
2023 1,
2024 "CreateSessionCommand must publish only its own SessionActivated event"
2025 );
2026 assert_eq!(
2027 create_stream_events.len(),
2028 1,
2029 "CreateStreamCommand must publish only its own StreamCreated event, not a republish of CreateSessionCommand's"
2030 );
2031 assert_eq!(
2032 start_stream_events.len(),
2033 1,
2034 "StartStreamCommand must publish only its own StreamStarted event, not a republish of earlier commands'"
2035 );
2036
2037 for event in create_stream_events {
2038 assert!(
2039 !create_session_events.contains(event),
2040 "CreateStreamCommand republished an event from CreateSessionCommand: {event:?}"
2041 );
2042 }
2043 for event in start_stream_events {
2044 assert!(
2045 !create_session_events.contains(event) && !create_stream_events.contains(event),
2046 "StartStreamCommand republished an earlier command's event: {event:?}"
2047 );
2048 }
2049 }
2050}