Skip to main content

tear_types/
id.rs

1//! Typed identifiers — opaque, stable, BLAKE3-derived.
2//!
3//! Every entity in the tear universe (session, window, pane) carries
4//! a 64-bit identifier whose bytes are the first 8 of a BLAKE3 hash
5//! of a deterministic seed (creation timestamp + parent ID + monotonic
6//! counter). Two collisions per session are astronomically unlikely;
7//! across the fleet they're effectively impossible.
8//!
9//! Identifiers are `Copy`, `Eq`, `Hash`, `Ord` — usable as map keys
10//! and Vec indexes without trait gymnastics. `Display` produces a
11//! 16-character lowercase hex string (a la `git` short SHAs); `FromStr`
12//! parses it back. The same wire format crosses the daemon-RPC boundary
13//! so a `tear list` from one host produces IDs the next host can paste.
14
15use core::fmt;
16use core::str::FromStr;
17
18use serde::{Deserialize, Serialize};
19
20macro_rules! impl_typed_id {
21    ($(#[$meta:meta])* $name:ident) => {
22        $(#[$meta])*
23        #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
24        #[serde(transparent)]
25        pub struct $name(pub u64);
26
27        impl $name {
28            /// Mint a fresh ID from a seed string (commonly the creation
29            /// timestamp + parent + counter). Truncates the BLAKE3 hash
30            /// to 8 bytes — collision risk is negligible for the
31            /// session-scale population.
32            #[must_use]
33            pub fn from_seed(seed: &str) -> Self {
34                let h = blake3::hash(seed.as_bytes());
35                let bytes = h.as_bytes();
36                let id = u64::from_le_bytes([
37                    bytes[0], bytes[1], bytes[2], bytes[3],
38                    bytes[4], bytes[5], bytes[6], bytes[7],
39                ]);
40                Self(id)
41            }
42
43            /// The reserved "null" id — `0`. Distinct from any
44            /// from_seed() output because BLAKE3 never produces an
45            /// all-zero hash on a non-empty input.
46            pub const NULL: Self = Self(0);
47        }
48
49        impl fmt::Debug for $name {
50            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51                write!(f, concat!(stringify!($name), "({:016x})"), self.0)
52            }
53        }
54
55        impl fmt::Display for $name {
56            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57                write!(f, "{:016x}", self.0)
58            }
59        }
60
61        impl FromStr for $name {
62            type Err = anyhow::Error;
63            fn from_str(s: &str) -> Result<Self, Self::Err> {
64                let n = u64::from_str_radix(s, 16)
65                    .map_err(|e| anyhow::anyhow!(concat!("invalid ", stringify!($name), ": {}"), e))?;
66                Ok(Self(n))
67            }
68        }
69    };
70}
71
72impl_typed_id!(
73    /// Identifier for a [`crate::TearSession`]. Stable across the
74    /// daemon's lifetime; round-trips through the wire format.
75    SessionId
76);
77impl_typed_id!(
78    /// Identifier for a [`crate::TearWindow`]. Window IDs are unique
79    /// within a session; the daemon namespaces them so two sessions
80    /// can hold windows with the same id without collision.
81    WindowId
82);
83impl_typed_id!(
84    /// Identifier for a [`crate::TearPane`]. Pane IDs are unique
85    /// within a window. mado renders panes addressing them by this
86    /// id; the multiplexer drives PTYs keyed by it.
87    PaneId
88);
89impl_typed_id!(
90    /// Stable identity of a session *definition* — the durable,
91    /// project-stable identity of a latent shape.
92    ///
93    /// Unlike [`InstanceId`] (a spawn-unique daemon handle that changes
94    /// every restart), a `DefinitionId` is the SAME across daemon
95    /// restarts and across hosts: its inner `u64` IS the
96    /// `ishou_tokens` `stable_seed` of the project root — the very seed
97    /// praça's name derivation uses — so a definition keeps its
98    /// identity, and therefore its display name, forever.
99    ///
100    /// The whole point of the type is that a `fn` taking a
101    /// `DefinitionId` cannot be handed an [`InstanceId`]: confusing the
102    /// durable identity of a *definition* with the ephemeral handle of a
103    /// *running instance* is a compile error (E0308), not a runtime mix-up.
104    DefinitionId
105);
106
107impl DefinitionId {
108    /// The definition id for a project root. Equal to the project's
109    /// `name_seed` by construction — `inner == stable_seed(root)`, the
110    /// exact derivation praça's [`SessionRecord`](../../praca) uses — so
111    /// a definition's identity and its display name share one seed and
112    /// migration from the old records is lossless.
113    #[must_use]
114    pub fn from_project(root: &std::path::Path) -> Self {
115        Self(ishou_tokens::fleet_session_names::stable_seed(
116            root.to_string_lossy().as_bytes(),
117        ))
118    }
119}
120
121/// The ephemeral, spawn-unique daemon handle for a *live* session
122/// incarnation — exactly today's [`SessionId`] (`BLAKE3(name + now +
123/// counter)`, non-stable across restart). Kept as a transparent alias so
124/// the wire format and every existing call site stay byte-identical; the
125/// name documents that this is the LIVE handle, distinct from a
126/// [`DefinitionId`]. A daemon restart mints a *new* `InstanceId` for the
127/// same definition — the handle is the incarnation, not the identity.
128pub type InstanceId = SessionId;
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133
134    #[test]
135    fn from_seed_is_deterministic() {
136        assert_eq!(PaneId::from_seed("foo"), PaneId::from_seed("foo"));
137        assert_ne!(PaneId::from_seed("foo"), PaneId::from_seed("bar"));
138    }
139
140    #[test]
141    fn display_round_trips_through_from_str() {
142        let id = SessionId::from_seed("session-7");
143        let s = id.to_string();
144        assert_eq!(s.len(), 16);
145        let parsed: SessionId = s.parse().unwrap();
146        assert_eq!(id, parsed);
147    }
148
149    #[test]
150    fn null_is_distinct_from_any_seeded_id() {
151        let seeded = WindowId::from_seed("test");
152        assert_ne!(WindowId::NULL, seeded);
153        assert_eq!(WindowId::NULL.0, 0);
154    }
155
156    #[test]
157    fn definition_id_inner_is_the_project_name_seed() {
158        // Migration losslessness: DefinitionId(root).0 is BYTE-EXACTLY the
159        // `name_seed` praça's SessionRecord stores
160        // (stable_seed(root.to_string_lossy())). So an old record upgrades
161        // to a definition with its identity == its existing name_seed —
162        // the name never changes across the migration.
163        let root = std::path::Path::new("/code/pleme-io/mado");
164        let expected = ishou_tokens::fleet_session_names::stable_seed(
165            root.to_string_lossy().as_bytes(),
166        );
167        assert_eq!(DefinitionId::from_project(root).0, expected);
168    }
169
170    #[test]
171    fn definition_id_is_restart_and_call_stable() {
172        // Unlike a spawn-minted InstanceId, the same project root always
173        // yields the same DefinitionId — that's the whole point of the
174        // durable-vs-ephemeral split.
175        let root = std::path::Path::new("/x/y/z");
176        assert_eq!(DefinitionId::from_project(root), DefinitionId::from_project(root));
177    }
178
179    #[test]
180    fn instance_id_is_session_id_alias() {
181        // InstanceId is the SAME type as SessionId (zero wire churn) — they
182        // unify, so a SessionId flows wherever an InstanceId is expected.
183        let s: SessionId = SessionId(7);
184        let i: InstanceId = s;
185        assert_eq!(i.0, 7);
186    }
187}