1use crate::decision::AdBreakDecision;
13use crate::error::{Error, Result};
14use crate::splice::ConditionedSplicePoint;
15use alloc::collections::BTreeMap;
16use alloc::string::String;
17
18#[derive(Debug, Clone, PartialEq)]
22pub struct BreakState {
23 pub decision: AdBreakDecision,
25 pub entered_at: ConditionedSplicePoint,
27 pub resumed_at: Option<ConditionedSplicePoint>,
29}
30
31impl BreakState {
32 pub fn new(decision: AdBreakDecision, entered_at: ConditionedSplicePoint) -> Self {
34 BreakState {
35 decision,
36 entered_at,
37 resumed_at: None,
38 }
39 }
40
41 pub fn is_resumed(&self) -> bool {
43 self.resumed_at.is_some()
44 }
45}
46
47#[derive(Debug, Clone, Default)]
57pub struct SessionStore {
58 sessions: BTreeMap<String, BreakState>,
59}
60
61impl SessionStore {
62 pub fn new() -> Self {
64 SessionStore {
65 sessions: BTreeMap::new(),
66 }
67 }
68
69 pub fn begin_break(&mut self, session_id: impl Into<String>, state: BreakState) -> Result<()> {
73 let session_id = session_id.into();
74 if let Some(existing) = self.sessions.get(&session_id)
75 && !existing.is_resumed()
76 {
77 return Err(Error::BreakAlreadyActive(session_id));
78 }
79 self.sessions.insert(session_id, state);
80 Ok(())
81 }
82
83 pub fn resume(&mut self, session_id: &str, resumed_at: ConditionedSplicePoint) -> Result<()> {
86 let state = self
87 .sessions
88 .get_mut(session_id)
89 .ok_or_else(|| Error::NoActiveBreak(session_id.into()))?;
90 state.resumed_at = Some(resumed_at);
91 Ok(())
92 }
93
94 pub fn get(&self, session_id: &str) -> Option<&BreakState> {
98 self.sessions.get(session_id)
99 }
100
101 pub fn end(&mut self, session_id: &str) -> Option<BreakState> {
104 self.sessions.remove(session_id)
105 }
106
107 pub fn len(&self) -> usize {
109 self.sessions.len()
110 }
111
112 pub fn is_empty(&self) -> bool {
114 self.sessions.is_empty()
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121 use crate::splice::condition_splice_point;
122
123 fn splice_point() -> ConditionedSplicePoint {
124 condition_splice_point(1_000, &[1_000], 0).unwrap()
125 }
126
127 #[test]
128 fn begin_get_resume_end_lifecycle() {
129 let mut store = SessionStore::new();
130 assert!(store.is_empty());
131
132 let decision = AdBreakDecision::single_asset("b1", "https://x/a.m3u8");
133 store
134 .begin_break("s1", BreakState::new(decision.clone(), splice_point()))
135 .unwrap();
136 assert_eq!(store.len(), 1);
137 assert!(!store.get("s1").unwrap().is_resumed());
138
139 store.resume("s1", splice_point()).unwrap();
140 assert!(store.get("s1").unwrap().is_resumed());
141
142 let evicted = store.end("s1").unwrap();
143 assert_eq!(evicted.decision, decision);
144 assert!(store.is_empty());
145 }
146
147 #[test]
148 fn cannot_begin_a_break_while_one_is_already_active() {
149 let mut store = SessionStore::new();
150 let decision = AdBreakDecision::single_asset("b1", "https://x/a.m3u8");
151 store
152 .begin_break("s1", BreakState::new(decision.clone(), splice_point()))
153 .unwrap();
154
155 let err = store
156 .begin_break("s1", BreakState::new(decision, splice_point()))
157 .unwrap_err();
158 assert!(matches!(err, Error::BreakAlreadyActive(ref s) if s == "s1"));
159 }
160
161 #[test]
162 fn begin_break_is_allowed_again_after_resume() {
163 let mut store = SessionStore::new();
164 let decision = AdBreakDecision::single_asset("b1", "https://x/a.m3u8");
165 store
166 .begin_break("s1", BreakState::new(decision.clone(), splice_point()))
167 .unwrap();
168 store.resume("s1", splice_point()).unwrap();
169
170 store
172 .begin_break("s1", BreakState::new(decision, splice_point()))
173 .unwrap();
174 assert_eq!(store.len(), 1);
175 }
176
177 #[test]
178 fn resume_without_an_active_break_errors() {
179 let mut store = SessionStore::new();
180 let err = store.resume("nope", splice_point()).unwrap_err();
181 assert!(matches!(err, Error::NoActiveBreak(ref s) if s == "nope"));
182 }
183}