1use crate::{
4 DomainError, DomainResult,
5 entities::{Frame, frame::FramePatch},
6 value_objects::{JsonData, JsonPath, Priority, SessionId, StreamId},
7};
8use chrono::{DateTime, Utc};
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11
12mod serde_session_id {
14 use crate::value_objects::SessionId;
15 use serde::{Deserialize, Deserializer, Serialize, Serializer};
16
17 pub fn serialize<S>(id: &SessionId, serializer: S) -> Result<S::Ok, S::Error>
18 where
19 S: Serializer,
20 {
21 id.as_uuid().serialize(serializer)
22 }
23
24 pub fn deserialize<'de, D>(deserializer: D) -> Result<SessionId, D::Error>
25 where
26 D: Deserializer<'de>,
27 {
28 let uuid = uuid::Uuid::deserialize(deserializer)?;
29 Ok(SessionId::from_uuid(uuid))
30 }
31}
32
33mod serde_stream_id {
35 use crate::value_objects::StreamId;
36 use serde::{Deserialize, Deserializer, Serialize, Serializer};
37
38 pub fn serialize<S>(id: &StreamId, serializer: S) -> Result<S::Ok, S::Error>
39 where
40 S: Serializer,
41 {
42 id.as_uuid().serialize(serializer)
43 }
44
45 pub fn deserialize<'de, D>(deserializer: D) -> Result<StreamId, D::Error>
46 where
47 D: Deserializer<'de>,
48 {
49 let uuid = uuid::Uuid::deserialize(deserializer)?;
50 Ok(StreamId::from_uuid(uuid))
51 }
52}
53
54mod serde_priority_map {
56 use crate::value_objects::Priority;
57 use serde::{Deserialize, Deserializer, Serialize, Serializer};
58 use std::collections::HashMap;
59
60 pub fn serialize<S>(map: &HashMap<String, Priority>, serializer: S) -> Result<S::Ok, S::Error>
61 where
62 S: Serializer,
63 {
64 let u8_map: HashMap<String, u8> = map.iter().map(|(k, v)| (k.clone(), v.value())).collect();
65 u8_map.serialize(serializer)
66 }
67
68 pub fn deserialize<'de, D>(deserializer: D) -> Result<HashMap<String, Priority>, D::Error>
69 where
70 D: Deserializer<'de>,
71 {
72 let u8_map: HashMap<String, u8> = HashMap::deserialize(deserializer)?;
73 u8_map
74 .into_iter()
75 .map(|(k, v)| {
76 Priority::new(v)
77 .map(|p| (k, p))
78 .map_err(serde::de::Error::custom)
79 })
80 .collect()
81 }
82}
83
84#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
86#[non_exhaustive]
87pub enum StreamState {
88 Preparing,
90 Streaming,
92 Completed,
94 Failed,
96 Cancelled,
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct StreamConfig {
103 pub max_frame_size: usize,
105 pub max_frames_per_batch: usize,
107 pub enable_compression: bool,
109 #[serde(with = "serde_priority_map")]
111 pub priority_rules: HashMap<String, Priority>,
112}
113
114impl Default for StreamConfig {
115 fn default() -> Self {
116 Self {
117 max_frame_size: 64 * 1024, max_frames_per_batch: 10,
119 enable_compression: true,
120 priority_rules: HashMap::new(),
121 }
122 }
123}
124
125#[derive(Debug, Clone, Default, Serialize, Deserialize)]
127pub struct StreamStats {
128 pub total_frames: u64,
130 pub skeleton_frames: u64,
132 pub patch_frames: u64,
134 pub complete_frames: u64,
136 pub error_frames: u64,
138 pub total_bytes: u64,
140 pub critical_bytes: u64,
142 pub high_priority_bytes: u64,
144 pub average_frame_size: f64,
146}
147
148#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct Stream {
151 #[serde(with = "serde_stream_id")]
152 id: StreamId,
153 #[serde(with = "serde_session_id")]
154 session_id: SessionId,
155 state: StreamState,
156 config: StreamConfig,
157 stats: StreamStats,
158 created_at: DateTime<Utc>,
159 updated_at: DateTime<Utc>,
160 completed_at: Option<DateTime<Utc>>,
161 next_sequence: u64,
162 source_data: Option<JsonData>,
163 metadata: HashMap<String, String>,
164}
165
166impl Stream {
167 pub fn new(session_id: SessionId, source_data: JsonData, config: StreamConfig) -> Self {
169 let now = Utc::now();
170
171 Self {
172 id: StreamId::new(),
173 session_id,
174 state: StreamState::Preparing,
175 config,
176 stats: StreamStats::default(),
177 created_at: now,
178 updated_at: now,
179 completed_at: None,
180 next_sequence: 1,
181 source_data: Some(source_data),
182 metadata: HashMap::new(),
183 }
184 }
185
186 pub fn id(&self) -> StreamId {
188 self.id
189 }
190
191 pub fn session_id(&self) -> SessionId {
193 self.session_id
194 }
195
196 pub fn state(&self) -> &StreamState {
198 &self.state
199 }
200
201 pub fn config(&self) -> &StreamConfig {
203 &self.config
204 }
205
206 pub fn stats(&self) -> &StreamStats {
208 &self.stats
209 }
210
211 pub fn created_at(&self) -> DateTime<Utc> {
213 self.created_at
214 }
215
216 pub fn updated_at(&self) -> DateTime<Utc> {
218 self.updated_at
219 }
220
221 pub fn completed_at(&self) -> Option<DateTime<Utc>> {
223 self.completed_at
224 }
225
226 pub fn source_data(&self) -> Option<&JsonData> {
228 self.source_data.as_ref()
229 }
230
231 pub fn metadata(&self) -> &HashMap<String, String> {
233 &self.metadata
234 }
235
236 pub fn add_metadata(&mut self, key: String, value: String) {
238 self.metadata.insert(key, value);
239 self.update_timestamp();
240 }
241
242 pub fn start_streaming(&mut self) -> DomainResult<()> {
244 match self.state {
245 StreamState::Preparing => {
246 self.state = StreamState::Streaming;
247 self.update_timestamp();
248 Ok(())
249 }
250 _ => Err(DomainError::InvalidStateTransition(format!(
251 "Cannot start streaming from state: {:?}",
252 self.state
253 ))),
254 }
255 }
256
257 pub fn complete(&mut self) -> DomainResult<()> {
259 match self.state {
260 StreamState::Streaming => {
261 self.state = StreamState::Completed;
262 self.completed_at = Some(Utc::now());
263 self.update_timestamp();
264 Ok(())
265 }
266 _ => Err(DomainError::InvalidStateTransition(format!(
267 "Cannot complete stream from state: {:?}",
268 self.state
269 ))),
270 }
271 }
272
273 pub fn fail(&mut self, error: String) -> DomainResult<()> {
275 match self.state {
276 StreamState::Preparing | StreamState::Streaming => {
277 self.state = StreamState::Failed;
278 self.completed_at = Some(Utc::now());
279 self.add_metadata("error".to_string(), error);
280 Ok(())
281 }
282 _ => Err(DomainError::InvalidStateTransition(format!(
283 "Cannot fail stream from state: {:?}",
284 self.state
285 ))),
286 }
287 }
288
289 pub fn cancel(&mut self) -> DomainResult<()> {
291 match self.state {
292 StreamState::Preparing | StreamState::Streaming => {
293 self.state = StreamState::Cancelled;
294 self.completed_at = Some(Utc::now());
295 self.update_timestamp();
296 Ok(())
297 }
298 _ => Err(DomainError::InvalidStateTransition(format!(
299 "Cannot cancel stream from state: {:?}",
300 self.state
301 ))),
302 }
303 }
304
305 pub fn create_skeleton_frame(&mut self) -> DomainResult<Frame> {
307 if !matches!(self.state, StreamState::Streaming) {
308 return Err(DomainError::InvalidStreamState(
309 "Stream must be in streaming state to create frames".to_string(),
310 ));
311 }
312
313 let skeleton_data = self.source_data.as_ref().ok_or_else(|| {
314 DomainError::InvalidStreamState("No source data available for skeleton".to_string())
315 })?;
316
317 let skeleton = self.generate_skeleton(skeleton_data)?;
318 let frame = Frame::skeleton(self.id, self.next_sequence, skeleton);
319
320 self.record_frame_created(&frame);
321
322 Ok(frame)
323 }
324
325 pub fn create_patch_frames(
327 &mut self,
328 priority_threshold: Priority,
329 max_frames: usize,
330 ) -> DomainResult<Vec<Frame>> {
331 let patches = self.extract_prioritized_patches(priority_threshold)?;
332 self.commit_patch_frames(patches, max_frames)
333 }
334
335 pub fn extract_prioritized_patches(
349 &self,
350 priority_threshold: Priority,
351 ) -> DomainResult<Vec<(FramePatch, Priority)>> {
352 if !matches!(self.state, StreamState::Streaming) {
353 return Err(DomainError::InvalidStreamState(
354 "Stream must be in streaming state to create frames".to_string(),
355 ));
356 }
357
358 let source_data = self.source_data.as_ref().ok_or_else(|| {
359 DomainError::InvalidStreamState("No source data available for patches".to_string())
360 })?;
361
362 self.extract_patches(source_data, priority_threshold)
363 }
364
365 pub fn commit_patch_frames(
387 &mut self,
388 patches: Vec<(FramePatch, Priority)>,
389 max_frames: usize,
390 ) -> DomainResult<Vec<Frame>> {
391 if !matches!(self.state, StreamState::Streaming) {
392 return Err(DomainError::InvalidStreamState(
393 "Stream must be in streaming state to create frames".to_string(),
394 ));
395 }
396
397 Self::chunk_patches_for_commit(patches, max_frames)
398 .into_iter()
399 .map(|(priority, frame_patches)| self.finalize_patch_frame(priority, frame_patches))
400 .collect()
401 }
402
403 pub fn finalize_patch_frame(
437 &mut self,
438 priority: Priority,
439 frame_patches: Vec<FramePatch>,
440 ) -> DomainResult<Frame> {
441 if !matches!(self.state, StreamState::Streaming) {
442 return Err(DomainError::InvalidStreamState(
443 "Stream must be in streaming state to create frames".to_string(),
444 ));
445 }
446
447 let frame = Frame::patch(self.id, self.next_sequence, priority, frame_patches)?;
448 self.record_frame_created(&frame);
449 Ok(frame)
450 }
451
452 pub fn create_completion_frame(&mut self, checksum: Option<String>) -> DomainResult<Frame> {
454 if !matches!(self.state, StreamState::Streaming) {
455 return Err(DomainError::InvalidStreamState(
456 "Stream must be in streaming state to create frames".to_string(),
457 ));
458 }
459
460 let frame = Frame::complete(self.id, self.next_sequence, checksum);
461 self.record_frame_created(&frame);
462
463 Ok(frame)
464 }
465
466 pub fn is_active(&self) -> bool {
468 matches!(self.state, StreamState::Preparing | StreamState::Streaming)
469 }
470
471 pub fn is_finished(&self) -> bool {
473 matches!(
474 self.state,
475 StreamState::Completed | StreamState::Failed | StreamState::Cancelled
476 )
477 }
478
479 pub fn duration(&self) -> Option<chrono::Duration> {
481 self.completed_at.map(|end| end - self.created_at)
482 }
483
484 pub fn progress(&self) -> f64 {
486 match self.state {
487 StreamState::Preparing => 0.0,
488 StreamState::Streaming => {
489 if self.stats.total_frames == 0 {
491 0.1 } else {
493 (self.stats.total_frames as f64 / 100.0).min(0.9)
495 }
496 }
497 StreamState::Completed => 1.0,
498 StreamState::Failed | StreamState::Cancelled => {
499 (self.stats.total_frames as f64 / 100.0).min(0.99)
501 }
502 }
503 }
504
505 pub fn update_config(&mut self, config: StreamConfig) -> DomainResult<()> {
507 if !self.is_active() {
508 return Err(DomainError::InvalidStreamState(
509 "Cannot update config of inactive stream".to_string(),
510 ));
511 }
512
513 self.config = config;
514 self.update_timestamp();
515 Ok(())
516 }
517
518 fn update_timestamp(&mut self) {
520 self.updated_at = Utc::now();
521 }
522
523 fn record_frame_created(&mut self, frame: &Frame) {
525 self.next_sequence += 1;
526 self.stats.total_frames += 1;
527
528 let frame_size = frame.estimated_size() as u64;
529 self.stats.total_bytes += frame_size;
530
531 match frame.frame_type() {
532 crate::entities::frame::FrameType::Skeleton => {
533 self.stats.skeleton_frames += 1;
534 self.stats.critical_bytes += frame_size;
535 }
536 crate::entities::frame::FrameType::Patch => {
537 self.stats.patch_frames += 1;
538 if frame.is_critical() {
539 self.stats.critical_bytes += frame_size;
540 } else if frame.is_high_priority() {
541 self.stats.high_priority_bytes += frame_size;
542 }
543 }
544 crate::entities::frame::FrameType::Complete => {
545 self.stats.complete_frames += 1;
546 self.stats.critical_bytes += frame_size;
547 }
548 crate::entities::frame::FrameType::Error => {
549 self.stats.error_frames += 1;
550 self.stats.critical_bytes += frame_size;
551 }
552 }
553
554 self.stats.average_frame_size =
556 self.stats.total_bytes as f64 / self.stats.total_frames as f64;
557
558 self.update_timestamp();
559 }
560
561 fn generate_skeleton(&self, data: &JsonData) -> DomainResult<JsonData> {
563 match data {
565 JsonData::Object(obj) => {
566 let mut skeleton = HashMap::new();
567 for (key, value) in obj.iter() {
568 skeleton.insert(
569 key.clone(),
570 match value {
571 JsonData::Array(_) => JsonData::Array(Vec::new()),
572 JsonData::Object(_) => self.generate_skeleton(value)?,
573 JsonData::Integer(_) => JsonData::Integer(0),
574 JsonData::Float(_) => JsonData::Float(0.0),
575 JsonData::String(_) => JsonData::Null,
576 JsonData::Bool(_) => JsonData::Bool(false),
577 JsonData::Null => JsonData::Null,
578 },
579 );
580 }
581 Ok(JsonData::Object(skeleton))
582 }
583 JsonData::Array(_) => Ok(JsonData::Array(Vec::new())),
584 _ => Ok(JsonData::Null),
585 }
586 }
587
588 fn extract_patches(
597 &self,
598 data: &JsonData,
599 threshold: Priority,
600 ) -> DomainResult<Vec<(crate::entities::frame::FramePatch, Priority)>> {
601 let mut patches = Vec::new();
602 self.collect_patches(data, &JsonPath::root(), threshold, &mut patches)?;
603 patches.sort_by_key(|p| core::cmp::Reverse(p.1));
606 Ok(patches)
607 }
608
609 fn collect_patches(
611 &self,
612 data: &JsonData,
613 path: &JsonPath,
614 threshold: Priority,
615 out: &mut Vec<(crate::entities::frame::FramePatch, Priority)>,
616 ) -> DomainResult<()> {
617 if let JsonData::Object(map) = data {
618 for (key, value) in map.iter() {
619 let Ok(child_path) = path.append_key(key) else {
623 continue;
624 };
625 self.collect_patches(value, &child_path, threshold, out)?;
626 }
627 return Ok(());
628 }
629
630 let priority = self.compute_priority(path, data);
631 if priority >= threshold {
632 let patch = crate::entities::frame::FramePatch::set(path.clone(), data.clone());
633 out.push((patch, priority));
634 }
635 Ok(())
636 }
637
638 fn compute_priority(&self, path: &JsonPath, value: &JsonData) -> Priority {
647 let mut cfg = crate::services::PriorityHeuristicConfig::default();
648 if !self.config.priority_rules.is_empty() {
649 cfg.overrides = self.config.priority_rules.clone();
650 }
651 crate::services::compute_priority(&cfg, path, value)
652 }
653
654 pub fn chunk_patches_for_commit(
705 patches: Vec<(FramePatch, Priority)>,
706 max_frames: usize,
707 ) -> Vec<(Priority, Vec<FramePatch>)> {
708 if patches.is_empty() || max_frames == 0 {
709 return Vec::new();
710 }
711
712 let chunk_size = patches.len().div_ceil(max_frames).max(1);
713
714 patches
715 .chunks(chunk_size)
716 .map(|chunk| {
717 let priority = chunk
718 .iter()
719 .map(|(_, p)| *p)
720 .max()
721 .unwrap_or(Priority::MEDIUM);
722
723 let frame_patches: Vec<FramePatch> =
724 chunk.iter().map(|(patch, _)| patch.clone()).collect();
725
726 (priority, frame_patches)
727 })
728 .collect()
729 }
730}
731
732#[cfg(test)]
733mod tests {
734 use super::*;
735
736 #[test]
737 fn test_stream_creation() {
738 let session_id = SessionId::new();
739 let source_data = serde_json::json!({
740 "users": [
741 {"id": 1, "name": "John"},
742 {"id": 2, "name": "Jane"}
743 ],
744 "total": 2
745 });
746
747 let stream = Stream::new(
748 session_id,
749 source_data.clone().into(),
750 StreamConfig::default(),
751 );
752
753 assert_eq!(stream.session_id(), session_id);
754 assert_eq!(stream.state(), &StreamState::Preparing);
755 assert!(stream.is_active());
756 assert!(!stream.is_finished());
757 assert_eq!(stream.progress(), 0.0);
758 }
759
760 #[test]
761 fn test_stream_state_transitions() {
762 let session_id = SessionId::new();
763 let source_data = serde_json::json!({});
764 let mut stream = Stream::new(session_id, source_data.into(), StreamConfig::default());
765
766 assert!(stream.start_streaming().is_ok());
768 assert_eq!(stream.state(), &StreamState::Streaming);
769
770 assert!(stream.complete().is_ok());
772 assert_eq!(stream.state(), &StreamState::Completed);
773 assert!(stream.is_finished());
774 assert_eq!(stream.progress(), 1.0);
775 }
776
777 #[test]
778 fn test_invalid_state_transitions() {
779 let session_id = SessionId::new();
780 let source_data = serde_json::json!({});
781 let mut stream = Stream::new(session_id, source_data.into(), StreamConfig::default());
782
783 assert!(stream.complete().is_err());
785
786 assert!(stream.start_streaming().is_ok());
788 assert!(stream.complete().is_ok());
789
790 assert!(stream.start_streaming().is_err());
792 }
793
794 #[test]
795 fn test_frame_creation() {
796 let session_id = SessionId::new();
797 let source_data = serde_json::json!({
798 "test": "data"
799 });
800 let mut stream = Stream::new(session_id, source_data.into(), StreamConfig::default());
801
802 assert!(stream.create_skeleton_frame().is_err());
804
805 assert!(stream.start_streaming().is_ok());
807 let skeleton = stream
808 .create_skeleton_frame()
809 .expect("Failed to create skeleton frame in test");
810
811 assert_eq!(
812 skeleton.frame_type(),
813 &crate::entities::frame::FrameType::Skeleton
814 );
815 assert_eq!(skeleton.sequence(), 1);
816 assert_eq!(stream.stats().skeleton_frames, 1);
817 }
818
819 #[test]
820 fn test_stream_metadata() {
821 let session_id = SessionId::new();
822 let source_data = serde_json::json!({});
823 let mut stream = Stream::new(session_id, source_data.into(), StreamConfig::default());
824
825 stream.add_metadata("source".to_string(), "api".to_string());
826 stream.add_metadata("version".to_string(), "1.0".to_string());
827
828 assert_eq!(stream.metadata().len(), 2);
829 assert_eq!(stream.metadata().get("source"), Some(&"api".to_string()));
830 }
831
832 #[test]
833 fn test_create_patch_frames_emits_frames_for_typical_payload() {
834 let session_id = SessionId::new();
835 let source_data = serde_json::json!({
836 "id": "abc-123",
837 "name": "Alice",
838 "items": [1, 2, 3]
839 });
840 let mut stream = Stream::new(session_id, source_data.into(), StreamConfig::default());
841
842 stream
843 .start_streaming()
844 .expect("stream must enter streaming state");
845
846 let frames = stream
847 .create_patch_frames(Priority::BACKGROUND, 16)
848 .expect("frame generation must succeed");
849
850 assert!(
851 !frames.is_empty(),
852 "extract_patches must produce at least one patch for non-empty source data"
853 );
854
855 let id_frame_priority_max = frames
856 .iter()
857 .map(|f| f.priority())
858 .max()
859 .expect("non-empty frames must have a max priority");
860 assert!(
861 id_frame_priority_max >= Priority::CRITICAL,
862 "frames carrying the `id` field must surface at critical priority"
863 );
864 }
865
866 #[test]
867 fn test_create_patch_frames_filters_below_threshold() {
868 let session_id = SessionId::new();
869 let source_data = serde_json::json!({
872 "analytics": {"clicks": 1, "views": 2}
873 });
874 let mut stream = Stream::new(session_id, source_data.into(), StreamConfig::default());
875
876 stream.start_streaming().expect("stream starts");
877 let frames = stream
878 .create_patch_frames(Priority::CRITICAL, 8)
879 .expect("frame generation must succeed");
880
881 assert!(
882 frames.is_empty(),
883 "patches below the priority threshold must be dropped"
884 );
885 }
886
887 #[test]
888 fn test_create_patch_frames_uses_max_priority_per_chunk() {
889 let session_id = SessionId::new();
890 let source_data = serde_json::json!({
892 "id": "x",
893 "name": "y",
894 "logs": "z"
895 });
896 let mut stream = Stream::new(session_id, source_data.into(), StreamConfig::default());
897
898 stream.start_streaming().expect("stream starts");
899 let frames = stream
901 .create_patch_frames(Priority::BACKGROUND, 1)
902 .expect("frame generation must succeed");
903
904 assert_eq!(frames.len(), 1, "max_frames=1 must yield a single frame");
905 assert_eq!(
906 frames[0].priority(),
907 Priority::CRITICAL,
908 "frame priority must reflect the highest-priority patch in the chunk"
909 );
910 }
911}