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}