1use eredu_core::{scheduler::SemanticStateTransaction, RealtimeSpeechConfig};
4
5use crate::{RealtimePayloadHistory, RealtimePayloadHistoryError};
6
7#[derive(Debug, Clone, Eq, PartialEq)]
9pub struct RealtimePayloadState<M, P> {
10 model_state: M,
11 payload_history: RealtimePayloadHistory<P>,
12}
13
14impl<M, P> RealtimePayloadState<M, P> {
15 pub fn fresh(model_state: M, schedule: RealtimeSpeechConfig) -> Self {
21 Self {
22 model_state,
23 payload_history: RealtimePayloadHistory::new(schedule),
24 }
25 }
26
27 pub fn new(
29 model_state: M,
30 payload_history: RealtimePayloadHistory<P>,
31 schedule: &RealtimeSpeechConfig,
32 ) -> Result<Self, RealtimePayloadHistoryError> {
33 payload_history.validate_schedule(schedule)?;
34 Ok(Self {
35 model_state,
36 payload_history,
37 })
38 }
39
40 pub const fn model_state(&self) -> &M {
42 &self.model_state
43 }
44
45 pub fn model_state_mut(&mut self) -> &mut M {
47 &mut self.model_state
48 }
49
50 pub const fn payload_history(&self) -> &RealtimePayloadHistory<P> {
52 &self.payload_history
53 }
54
55 pub fn payload_history_mut(&mut self) -> &mut RealtimePayloadHistory<P> {
57 &mut self.payload_history
58 }
59
60 pub fn parts_mut(&mut self) -> (&mut M, &mut RealtimePayloadHistory<P>) {
62 (&mut self.model_state, &mut self.payload_history)
63 }
64
65 pub fn into_parts(self) -> (M, RealtimePayloadHistory<P>) {
67 (self.model_state, self.payload_history)
68 }
69}
70
71#[derive(Debug, Clone, Eq, PartialEq)]
73pub struct RealtimePayloadBranch<B, P> {
74 model_state: B,
75 payload_history: RealtimePayloadHistory<P>,
76}
77
78impl<B, P> RealtimePayloadBranch<B, P> {
79 pub const fn model_state(&self) -> &B {
81 &self.model_state
82 }
83
84 pub fn model_state_mut(&mut self) -> &mut B {
86 &mut self.model_state
87 }
88
89 pub const fn payload_history(&self) -> &RealtimePayloadHistory<P> {
91 &self.payload_history
92 }
93
94 pub fn payload_history_mut(&mut self) -> &mut RealtimePayloadHistory<P> {
96 &mut self.payload_history
97 }
98
99 pub fn parts_mut(&mut self) -> (&mut B, &mut RealtimePayloadHistory<P>) {
101 (&mut self.model_state, &mut self.payload_history)
102 }
103
104 pub fn into_parts(self) -> (B, RealtimePayloadHistory<P>) {
106 (self.model_state, self.payload_history)
107 }
108}
109
110impl<M, P> SemanticStateTransaction for RealtimePayloadState<M, P>
111where
112 M: SemanticStateTransaction,
113 M::Error: 'static,
114 P: Clone,
115{
116 type Branch = RealtimePayloadBranch<M::Branch, P>;
117 type Error = RealtimePayloadStateTransactionError<M::Error>;
118
119 fn branch(&self) -> Result<Self::Branch, Self::Error> {
120 let model_state = self.model_state.branch().map_err(Self::Error::Model)?;
121 Ok(RealtimePayloadBranch {
122 model_state,
123 payload_history: self.payload_history.clone(),
124 })
125 }
126
127 fn commit_branch(&mut self, branch: Self::Branch) -> Result<(), Self::Error> {
128 let RealtimePayloadBranch {
129 model_state,
130 payload_history,
131 } = branch;
132 self.payload_history
133 .validate_successor(&payload_history)
134 .map_err(Self::Error::PayloadHistory)?;
135 self.model_state
136 .commit_branch(model_state)
137 .map_err(Self::Error::Model)?;
138 self.payload_history = payload_history;
139 Ok(())
140 }
141
142 fn discard_branch(branch: Self::Branch) -> Result<(), Self::Error> {
143 let (model_state, _payload_history) = branch.into_parts();
144 M::discard_branch(model_state).map_err(Self::Error::Model)
145 }
146
147 fn permits_parallel_branches(&self) -> bool {
148 self.model_state.permits_parallel_branches()
149 }
150}
151
152#[derive(Debug, thiserror::Error)]
154pub enum RealtimePayloadStateTransactionError<E: std::error::Error> {
155 #[error("realtime model state transaction failed: {0}")]
157 Model(E),
158 #[error(transparent)]
160 PayloadHistory(RealtimePayloadHistoryError),
161}
162
163#[cfg(test)]
164mod tests {
165 use std::{cell::Cell, rc::Rc};
166
167 use eredu_core::{
168 scheduler::SemanticStateTransaction, RealtimeFrameConvention, RealtimeFrameSlot,
169 RealtimeSlotCoordinate,
170 };
171
172 use super::*;
173 use crate::{
174 RealtimePayloadContract, RealtimePayloadGeneration, RealtimePayloadOwnerIdentity,
175 TokenDomain,
176 };
177
178 fn schedule(text_delay: usize) -> RealtimeSpeechConfig {
179 RealtimeSpeechConfig::new(
180 2,
181 1,
182 1,
183 1,
184 0,
185 1,
186 RealtimeFrameConvention::FeedbackAlignedHistory,
187 vec![text_delay, 0, 1],
188 )
189 .unwrap()
190 }
191
192 fn text(position: usize) -> RealtimeSlotCoordinate {
193 RealtimeSlotCoordinate::new(position, RealtimeFrameSlot::Text)
194 }
195
196 #[test]
197 fn fresh_state_binds_model_and_empty_history_to_the_exact_schedule() {
198 let schedule = schedule(2);
199 let state = RealtimePayloadState::<_, String>::fresh(
200 ModelState {
201 value: 7,
202 fail_commit: false,
203 discards: Rc::new(Cell::new(0)),
204 },
205 schedule.clone(),
206 );
207
208 assert_eq!(state.model_state().value, 7);
209 assert_eq!(state.payload_history().schedule(), &schedule);
210 assert!(state.payload_history().contract().is_none());
211 }
212
213 fn contract(
214 schedule: RealtimeSpeechConfig,
215 batch: usize,
216 text_domain: usize,
217 audio_domain: usize,
218 generation: u64,
219 owner: u64,
220 ) -> RealtimePayloadContract {
221 RealtimePayloadContract::new(
222 schedule,
223 batch,
224 TokenDomain::new(text_domain),
225 TokenDomain::new(audio_domain),
226 RealtimePayloadGeneration::new(generation).unwrap(),
227 RealtimePayloadOwnerIdentity::new(owner).unwrap(),
228 )
229 .unwrap()
230 }
231
232 fn exact_contract() -> RealtimePayloadContract {
233 contract(schedule(2), 1, 2, 2, 1, 1)
234 }
235
236 #[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
237 #[error("mock model commit failed")]
238 struct ModelError;
239
240 #[derive(Debug, Clone)]
241 struct ModelState {
242 value: usize,
243 fail_commit: bool,
244 discards: Rc<Cell<usize>>,
245 }
246
247 #[derive(Debug, Clone)]
248 struct ModelBranch {
249 value: usize,
250 fail_commit: bool,
251 discards: Rc<Cell<usize>>,
252 }
253
254 impl SemanticStateTransaction for ModelState {
255 type Branch = ModelBranch;
256 type Error = ModelError;
257
258 fn branch(&self) -> Result<Self::Branch, Self::Error> {
259 Ok(ModelBranch {
260 value: self.value,
261 fail_commit: self.fail_commit,
262 discards: self.discards.clone(),
263 })
264 }
265
266 fn commit_branch(&mut self, branch: Self::Branch) -> Result<(), Self::Error> {
267 if branch.fail_commit {
268 return Err(ModelError);
269 }
270 self.value = branch.value;
271 Ok(())
272 }
273
274 fn discard_branch(branch: Self::Branch) -> Result<(), Self::Error> {
275 branch.discards.set(branch.discards.get() + 1);
276 Ok(())
277 }
278
279 fn permits_parallel_branches(&self) -> bool {
280 true
281 }
282 }
283
284 fn state(fail_commit: bool) -> RealtimePayloadState<ModelState, &'static str> {
285 let schedule = schedule(2);
286 let mut history = RealtimePayloadHistory::with_contract(exact_contract());
287 history.insert(&schedule, text(0), "canonical").unwrap();
288 RealtimePayloadState::new(
289 ModelState {
290 value: 1,
291 fail_commit,
292 discards: Rc::new(Cell::new(0)),
293 },
294 history,
295 &schedule,
296 )
297 .unwrap()
298 }
299
300 #[test]
301 fn successful_commit_publishes_model_and_payload_branch_together() {
302 let schedule = schedule(2);
303 let mut state = state(false);
304 let mut branch = state.branch().unwrap();
305 branch.model_state_mut().value = 7;
306 branch
307 .payload_history_mut()
308 .insert(&schedule, text(1), "branch")
309 .unwrap();
310
311 state.commit_branch(branch).unwrap();
312
313 assert_eq!(state.model_state().value, 7);
314 assert_eq!(
315 state.payload_history().required(&schedule, text(1)),
316 Ok(&"branch")
317 );
318 assert!(state.permits_parallel_branches());
319 }
320
321 #[test]
322 fn failed_model_commit_never_publishes_payload_history() {
323 let schedule = schedule(2);
324 let mut state = state(true);
325 let mut branch = state.branch().unwrap();
326 branch.model_state_mut().value = 9;
327 branch
328 .payload_history_mut()
329 .insert(&schedule, text(1), "unpublished")
330 .unwrap();
331
332 assert!(matches!(
333 state.commit_branch(branch),
334 Err(RealtimePayloadStateTransactionError::Model(ModelError))
335 ));
336 assert_eq!(state.model_state().value, 1);
337 assert_eq!(state.payload_history().get(&schedule, text(1)), Ok(None));
338 }
339
340 #[test]
341 fn discard_delegates_model_rollback_and_drops_cloned_history() {
342 let schedule = schedule(2);
343 let state = state(false);
344 let discards = state.model_state().discards.clone();
345 let mut branch = state.branch().unwrap();
346 branch
347 .payload_history_mut()
348 .insert(&schedule, text(1), "discarded")
349 .unwrap();
350
351 RealtimePayloadState::<ModelState, &'static str>::discard_branch(branch).unwrap();
352
353 assert_eq!(discards.get(), 1);
354 assert_eq!(state.payload_history().get(&schedule, text(1)), Ok(None));
355 }
356
357 #[test]
358 fn construction_rejects_mismatched_payload_schedule() {
359 let selected = schedule(2);
360 let wrong = schedule(1);
361 let history = RealtimePayloadHistory::<usize>::new(wrong);
362 let error = RealtimePayloadState::new(
363 ModelState {
364 value: 1,
365 fail_commit: false,
366 discards: Rc::new(Cell::new(0)),
367 },
368 history,
369 &selected,
370 )
371 .unwrap_err();
372
373 assert_eq!(error, RealtimePayloadHistoryError::ScheduleMismatch);
374 }
375
376 #[test]
377 fn first_bound_branch_contract_publishes_into_unbound_canonical_state() {
378 let schedule = schedule(2);
379 let history = RealtimePayloadHistory::new(schedule.clone());
380 let mut state = RealtimePayloadState::new(
381 ModelState {
382 value: 1,
383 fail_commit: false,
384 discards: Rc::new(Cell::new(0)),
385 },
386 history,
387 &schedule,
388 )
389 .unwrap();
390 let mut branch = state.branch().unwrap();
391 branch
392 .payload_history_mut()
393 .bind_or_validate_contract(&exact_contract())
394 .unwrap();
395 branch
396 .payload_history_mut()
397 .insert(&schedule, text(0), "first")
398 .unwrap();
399
400 state.commit_branch(branch).unwrap();
401
402 assert_eq!(state.payload_history().contract(), Some(&exact_contract()));
403 assert_eq!(
404 state.payload_history().required(&schedule, text(0)),
405 Ok(&"first")
406 );
407 }
408
409 #[test]
410 fn state_commit_rejects_every_payload_identity_perturbation_before_model_commit() {
411 let selected = schedule(2);
412 let wrong_schedule = schedule(1);
413 let candidates = [
414 (
415 contract(wrong_schedule, 1, 2, 2, 1, 1),
416 RealtimePayloadHistoryError::ScheduleMismatch,
417 ),
418 (
419 contract(selected.clone(), 2, 2, 2, 1, 1),
420 RealtimePayloadHistoryError::PayloadContract(
421 crate::RealtimePayloadContractError::BatchMismatch,
422 ),
423 ),
424 (
425 contract(selected.clone(), 1, 3, 2, 1, 1),
426 RealtimePayloadHistoryError::PayloadContract(
427 crate::RealtimePayloadContractError::TextDomainMismatch,
428 ),
429 ),
430 (
431 contract(selected.clone(), 1, 2, 3, 1, 1),
432 RealtimePayloadHistoryError::PayloadContract(
433 crate::RealtimePayloadContractError::AudioDomainMismatch,
434 ),
435 ),
436 (
437 contract(selected.clone(), 1, 2, 2, 2, 1),
438 RealtimePayloadHistoryError::PayloadContract(
439 crate::RealtimePayloadContractError::GenerationMismatch,
440 ),
441 ),
442 (
443 contract(selected.clone(), 1, 2, 2, 1, 2),
444 RealtimePayloadHistoryError::PayloadContract(
445 crate::RealtimePayloadContractError::OwnerMismatch,
446 ),
447 ),
448 ];
449
450 for (candidate, expected) in candidates {
451 let mut state = state(false);
452 let mut branch = state.branch().unwrap();
453 branch.model_state_mut().value = 9;
454 let mut wrong_history = RealtimePayloadHistory::with_contract(candidate);
455 let wrong_history_schedule = wrong_history.schedule().clone();
456 wrong_history
457 .insert(&wrong_history_schedule, text(0), "wrong")
458 .unwrap();
459 branch.payload_history = wrong_history;
460
461 assert!(matches!(
462 state.commit_branch(branch),
463 Err(RealtimePayloadStateTransactionError::PayloadHistory(error))
464 if error == expected
465 ));
466 assert_eq!(state.model_state().value, 1);
467 assert_eq!(state.payload_history().contract(), Some(&exact_contract()));
468 assert_eq!(
469 state.payload_history().required(&selected, text(0)),
470 Ok(&"canonical")
471 );
472 }
473 }
474
475 #[test]
476 fn bound_state_rejects_an_unbound_successor_before_model_commit() {
477 let schedule = schedule(2);
478 let mut state = state(false);
479 let mut branch = state.branch().unwrap();
480 branch.model_state_mut().value = 9;
481 branch.payload_history = RealtimePayloadHistory::new(schedule.clone());
482
483 assert!(matches!(
484 state.commit_branch(branch),
485 Err(RealtimePayloadStateTransactionError::PayloadHistory(
486 RealtimePayloadHistoryError::UnboundContract
487 ))
488 ));
489 assert_eq!(state.model_state().value, 1);
490 assert_eq!(
491 state.payload_history().required(&schedule, text(0)),
492 Ok(&"canonical")
493 );
494 }
495}