Skip to main content

tear_types/
live.rs

1//! The live half of the session model — a *running* incarnation and its
2//! durability.
3//!
4//! A [`LiveSession`] is what the daemon owns: real PTYs, a mutable
5//! layout, scrollback. It is reached by *instantiating* a definition
6//! (see praça's `SessionDefinition` + the `instantiate` morphism), and a
7//! daemon restart does NOT resurrect it — it re-instantiates the
8//! definition under a *fresh* [`InstanceId`]. The [`Durability`] marker
9//! is how that impossibility is made typed: there is no value of
10//! `Durability` that means "survives a restart", so a live session
11//! claiming restart-durability is **unrepresentable** (pressure-test
12//! illegal state #6). The only thing that survives a restart is the
13//! definition (durable, in praça's store); the live processes are, by
14//! type, process-bound.
15
16use serde::{Deserialize, Serialize};
17
18use crate::{
19    id::{DefinitionId, InstanceId},
20    session::TearSession,
21};
22
23/// Durability of a live session's runtime state. The marker exists to
24/// make one illegal claim unrepresentable: a [`LiveSession`]'s PTYs,
25/// layout, and scrollback live in the daemon process and die with it, so
26/// there is no `Durable` / `SurvivesRestart` arm to construct. "Restart
27/// the session" is therefore not "resurrect these processes" (no value
28/// expresses that) but "re-instantiate the definition" — a fresh
29/// incarnation. New arms would only ever describe *finer* process-bound
30/// lifetimes, never a restart-surviving one.
31#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
32#[serde(rename_all = "kebab-case")]
33pub enum Durability {
34    /// State lives in the daemon process and is lost when it exits.
35    /// Recovery is re-instantiation of the definition, never resurrection.
36    ProcessBound,
37}
38
39impl Default for Durability {
40    fn default() -> Self {
41        Self::ProcessBound
42    }
43}
44
45/// A running session incarnation: the shipped [`TearSession`] runtime
46/// state plus the typed link back to the [`DefinitionId`] it was
47/// instantiated from, plus its [`Durability`] marker.
48///
49/// `LiveSession` is a *graceful extension* of [`TearSession`] — it embeds
50/// it as-is rather than re-modelling windows/panes — and adds exactly the
51/// two facts the pressure-test found missing: which definition this live
52/// session realizes (illegal state #1/#5 — the typed live→definition
53/// link, so a stale handle isn't conflated with a durable identity), and
54/// that it is process-bound (illegal state #6).
55#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
56pub struct LiveSession {
57    /// The definition this incarnation was instantiated from. A restart
58    /// re-instantiates THIS definition under a new [`InstanceId`]; the
59    /// link is how the daemon knows which definition a live session
60    /// realizes (and how 1 definition → N live instances is tracked, in
61    /// praça's `InstanceRegistry`).
62    pub definition: DefinitionId,
63    /// Durability marker — always [`Durability::ProcessBound`]; there is
64    /// no restart-surviving value to set it to.
65    #[serde(default)]
66    pub durability: Durability,
67    /// The runtime session state (windows, panes, live layout,
68    /// scrollback-bearing pane ids). Embedded as-is.
69    pub session: TearSession,
70}
71
72impl LiveSession {
73    /// Construct a live session from a freshly-spawned [`TearSession`] and
74    /// the definition it realizes. Always [`Durability::ProcessBound`].
75    #[must_use]
76    pub fn new(definition: DefinitionId, session: TearSession) -> Self {
77        Self {
78            definition,
79            durability: Durability::ProcessBound,
80            session,
81        }
82    }
83
84    /// This incarnation's spawn-unique handle — the embedded session's id,
85    /// not a stored duplicate (no drift between two id fields). Typed as
86    /// [`InstanceId`] to document that it is the LIVE handle, distinct
87    /// from `self.definition`.
88    #[must_use]
89    pub fn instance(&self) -> InstanceId {
90        self.session.id
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97    use crate::id::{DefinitionId, SessionId, WindowId};
98    use crate::session::{SessionSource, SessionState};
99    use std::collections::BTreeMap;
100    use std::path::Path;
101
102    fn sample_session() -> TearSession {
103        // Compile-checked literal — if TearSession gains/changes a field
104        // this test breaks at COMPILE time (a forcing function), not at
105        // runtime parse.
106        TearSession {
107            id: SessionId(1234),
108            name: "demo".into(),
109            windows: BTreeMap::new(),
110            panes: BTreeMap::new(),
111            active_window: WindowId::NULL,
112            state: SessionState::Active,
113            created_at_unix: 0,
114            description: String::new(),
115            source: SessionSource::Human,
116            freio: crate::freio::Freio::Released,
117        }
118    }
119
120    #[test]
121    fn durability_has_no_restart_surviving_value() {
122        // The whole guarantee in one line: the only constructible value is
123        // ProcessBound. (If a `Durable` arm were ever added, this match
124        // would fail to compile — the forcing function.)
125        let d = Durability::default();
126        match d {
127            Durability::ProcessBound => {}
128        }
129    }
130
131    #[test]
132    fn live_session_links_to_its_definition_and_is_process_bound() {
133        let def = DefinitionId::from_project(Path::new("/code/pleme-io/mado"));
134        let live = LiveSession::new(def, sample_session());
135        assert_eq!(live.definition, def);
136        assert_eq!(live.durability, Durability::ProcessBound);
137        // instance() reads through to the embedded session — one id, no drift.
138        assert_eq!(live.instance(), live.session.id);
139    }
140
141    #[test]
142    fn live_session_serde_round_trips() {
143        let def = DefinitionId::from_project(Path::new("/x"));
144        let live = LiveSession::new(def, sample_session());
145        let json = serde_json::to_string(&live).unwrap();
146        let back: LiveSession = serde_json::from_str(&json).unwrap();
147        assert_eq!(live, back);
148    }
149}