Skip to main content

eredu_runtime/
realtime_payload_state.rs

1//! Atomic model-state and delayed-payload-history transactions.
2
3use eredu_core::{scheduler::SemanticStateTransaction, RealtimeSpeechConfig};
4
5use crate::{RealtimePayloadHistory, RealtimePayloadHistoryError};
6
7/// Canonical semantic model state paired with its delayed realtime payloads.
8#[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    /// Creates fresh model state with empty delayed-payload history for one schedule.
16    ///
17    /// This is the canonical request-local initialization path. Architecture and
18    /// facade composition can assemble neutral realtime state without asking a
19    /// concrete backend to understand delayed-frame semantics.
20    pub fn fresh(model_state: M, schedule: RealtimeSpeechConfig) -> Self {
21        Self {
22            model_state,
23            payload_history: RealtimePayloadHistory::new(schedule),
24        }
25    }
26
27    /// Pairs model state with history only when both use the exact selected schedule.
28    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    /// Returns canonical model/cache state.
41    pub const fn model_state(&self) -> &M {
42        &self.model_state
43    }
44
45    /// Mutably returns canonical model/cache state.
46    pub fn model_state_mut(&mut self) -> &mut M {
47        &mut self.model_state
48    }
49
50    /// Returns canonical delayed payload history.
51    pub const fn payload_history(&self) -> &RealtimePayloadHistory<P> {
52        &self.payload_history
53    }
54
55    /// Mutably returns canonical delayed payload history.
56    pub fn payload_history_mut(&mut self) -> &mut RealtimePayloadHistory<P> {
57        &mut self.payload_history
58    }
59
60    /// Mutably borrows model/cache state and payload history together.
61    pub fn parts_mut(&mut self) -> (&mut M, &mut RealtimePayloadHistory<P>) {
62        (&mut self.model_state, &mut self.payload_history)
63    }
64
65    /// Consumes canonical model state and payload history.
66    pub fn into_parts(self) -> (M, RealtimePayloadHistory<P>) {
67        (self.model_state, self.payload_history)
68    }
69}
70
71/// Unpublished model-state branch and its independently cloned payload history.
72#[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    /// Returns transition-local model/cache state.
80    pub const fn model_state(&self) -> &B {
81        &self.model_state
82    }
83
84    /// Mutably returns transition-local model/cache state.
85    pub fn model_state_mut(&mut self) -> &mut B {
86        &mut self.model_state
87    }
88
89    /// Returns transition-local delayed payload history.
90    pub const fn payload_history(&self) -> &RealtimePayloadHistory<P> {
91        &self.payload_history
92    }
93
94    /// Mutably returns transition-local delayed payload history.
95    pub fn payload_history_mut(&mut self) -> &mut RealtimePayloadHistory<P> {
96        &mut self.payload_history
97    }
98
99    /// Mutably borrows transition-local model/cache state and history together.
100    pub fn parts_mut(&mut self) -> (&mut B, &mut RealtimePayloadHistory<P>) {
101        (&mut self.model_state, &mut self.payload_history)
102    }
103
104    /// Consumes the unpublished model branch and payload history.
105    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/// Failure while publishing or discarding paired realtime semantic state.
153#[derive(Debug, thiserror::Error)]
154pub enum RealtimePayloadStateTransactionError<E: std::error::Error> {
155    /// The underlying model/cache transaction failed.
156    #[error("realtime model state transaction failed: {0}")]
157    Model(E),
158    /// The branch payload history no longer matches the canonical schedule.
159    #[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}