Skip to main content

starlang_core/
node.rs

1//! Node identity types for distributed Starlang.
2//!
3//! These types identify nodes in a Starlang cluster:
4//!
5//! - [`NodeId`] - Numeric identifier for a node (used for display only)
6//! - [`NodeName`] - Human-readable name like "node1@localhost"
7//! - [`NodeInfo`] - Complete node information including connection details
8//!
9//! Node identity is stored as an [`Atom`] for efficient comparison and
10//! globally unambiguous PID addressing.
11
12use serde::{Deserialize, Serialize};
13use starlang_atom::Atom;
14use std::fmt;
15use std::net::SocketAddr;
16use std::sync::OnceLock;
17
18/// Local node identifier constant (for display purposes).
19pub const LOCAL_NODE_ID: u32 = 0;
20
21/// The atom representing the local/uninitialized node.
22/// This is used before distribution is initialized.
23static LOCAL_NODE_ATOM: OnceLock<Atom> = OnceLock::new();
24
25/// Global storage for this node's identity.
26static THIS_NODE: OnceLock<NodeIdentity> = OnceLock::new();
27
28/// Get the atom representing "no node" / local node before distribution init.
29fn local_node_atom() -> Atom {
30    *LOCAL_NODE_ATOM.get_or_init(|| Atom::new(""))
31}
32
33/// A node identifier.
34///
35/// Each node in a Starlang cluster has a unique numeric ID. The local node
36/// always has ID 0. Remote nodes are assigned IDs starting from 1 when
37/// connections are established.
38///
39/// # Examples
40///
41/// ```
42/// use starlang_core::NodeId;
43///
44/// let local = NodeId::local();
45/// assert!(local.is_local());
46///
47/// let remote = NodeId::new(1);
48/// assert!(!remote.is_local());
49/// ```
50#[derive(Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
51pub struct NodeId(u32);
52
53impl NodeId {
54    /// Creates a new `NodeId` with the given value.
55    #[inline]
56    pub const fn new(id: u32) -> Self {
57        Self(id)
58    }
59
60    /// Returns the local node ID (always 0).
61    #[inline]
62    pub const fn local() -> Self {
63        Self(LOCAL_NODE_ID)
64    }
65
66    /// Returns the raw u32 value.
67    #[inline]
68    pub const fn as_u32(&self) -> u32 {
69        self.0
70    }
71
72    /// Returns `true` if this is the local node.
73    #[inline]
74    pub const fn is_local(&self) -> bool {
75        self.0 == LOCAL_NODE_ID
76    }
77}
78
79impl From<u32> for NodeId {
80    fn from(id: u32) -> Self {
81        Self(id)
82    }
83}
84
85impl From<NodeId> for u32 {
86    fn from(id: NodeId) -> Self {
87        id.0
88    }
89}
90
91impl fmt::Debug for NodeId {
92    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
93        write!(f, "NodeId({})", self.0)
94    }
95}
96
97impl fmt::Display for NodeId {
98    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99        write!(f, "{}", self.0)
100    }
101}
102
103/// A human-readable node name.
104///
105/// Node names follow the format `name@host`, similar to Erlang node names.
106/// For example: `"node1@localhost"` or `"chat-server@192.168.1.100"`.
107///
108/// # Examples
109///
110/// ```
111/// use starlang_core::NodeName;
112///
113/// let name = NodeName::new("node1@localhost");
114/// assert_eq!(name.short_name(), "node1");
115/// assert_eq!(name.host(), "localhost");
116/// ```
117#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
118pub struct NodeName(String);
119
120impl NodeName {
121    /// Creates a new node name.
122    ///
123    /// The name should be in the format `name@host`.
124    pub fn new(name: impl Into<String>) -> Self {
125        Self(name.into())
126    }
127
128    /// Returns the full node name.
129    pub fn as_str(&self) -> &str {
130        &self.0
131    }
132
133    /// Returns the short name (before the @).
134    ///
135    /// Returns the full name if no @ is present.
136    pub fn short_name(&self) -> &str {
137        self.0.split('@').next().unwrap_or(&self.0)
138    }
139
140    /// Returns the host part (after the @).
141    ///
142    /// Returns an empty string if no @ is present.
143    pub fn host(&self) -> &str {
144        self.0.split('@').nth(1).unwrap_or("")
145    }
146}
147
148impl fmt::Debug for NodeName {
149    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150        write!(f, "NodeName({:?})", self.0)
151    }
152}
153
154impl fmt::Display for NodeName {
155    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
156        write!(f, "{}", self.0)
157    }
158}
159
160impl From<&str> for NodeName {
161    fn from(s: &str) -> Self {
162        Self::new(s)
163    }
164}
165
166impl From<String> for NodeName {
167    fn from(s: String) -> Self {
168        Self::new(s)
169    }
170}
171
172/// Complete information about a node.
173///
174/// This includes the node's name, numeric ID, network address,
175/// and creation number (for distinguishing node restarts).
176#[derive(Clone, Debug, Serialize, Deserialize)]
177pub struct NodeInfo {
178    /// The node's human-readable name.
179    pub name: NodeName,
180    /// The node's numeric ID.
181    pub id: NodeId,
182    /// The node's network address for distribution.
183    pub addr: Option<SocketAddr>,
184    /// Creation number - incremented on each node restart.
185    pub creation: u32,
186}
187
188impl NodeInfo {
189    /// Creates new node info.
190    pub fn new(
191        name: impl Into<NodeName>,
192        id: NodeId,
193        addr: Option<SocketAddr>,
194        creation: u32,
195    ) -> Self {
196        Self {
197            name: name.into(),
198            id,
199            addr,
200            creation,
201        }
202    }
203}
204
205/// The identity of this node.
206///
207/// Set once at startup and never changes.
208#[derive(Clone, Debug)]
209pub struct NodeIdentity {
210    /// This node's name as a string.
211    pub name: NodeName,
212    /// This node's name as an atom (for efficient PID comparison).
213    pub name_atom: Atom,
214    /// This node's creation number.
215    pub creation: u32,
216}
217
218/// Initialize this node's identity.
219///
220/// This should be called once at startup. Returns an error if already initialized.
221///
222/// # Examples
223///
224/// ```
225/// use starlang_core::node::{init_node, NodeName};
226///
227/// // Only call once at startup
228/// // init_node(NodeName::new("node1@localhost"), 0);
229/// ```
230pub fn init_node(name: NodeName, creation: u32) -> Result<(), NodeIdentity> {
231    let name_atom = Atom::new(name.as_str());
232    THIS_NODE.set(NodeIdentity {
233        name,
234        name_atom,
235        creation,
236    })
237}
238
239/// Get this node's name.
240///
241/// Returns `None` if the node hasn't been initialized with distribution enabled.
242pub fn node_name() -> Option<&'static NodeName> {
243    THIS_NODE.get().map(|n| &n.name)
244}
245
246/// Get this node's name as an Atom.
247///
248/// Returns the empty atom if distribution hasn't been initialized.
249/// This is used for PID node field comparison.
250pub fn node_name_atom() -> Atom {
251    THIS_NODE
252        .get()
253        .map(|n| n.name_atom)
254        .unwrap_or_else(local_node_atom)
255}
256
257/// Get this node's creation number.
258///
259/// Returns 0 if not initialized.
260pub fn node_creation() -> u32 {
261    THIS_NODE.get().map(|n| n.creation).unwrap_or(0)
262}
263
264/// Returns `true` if distribution has been initialized.
265pub fn is_distributed() -> bool {
266    THIS_NODE.get().is_some()
267}
268
269#[cfg(test)]
270mod tests {
271    use super::*;
272
273    #[test]
274    fn test_node_id_local() {
275        let local = NodeId::local();
276        assert!(local.is_local());
277        assert_eq!(local.as_u32(), 0);
278    }
279
280    #[test]
281    fn test_node_id_remote() {
282        let remote = NodeId::new(5);
283        assert!(!remote.is_local());
284        assert_eq!(remote.as_u32(), 5);
285    }
286
287    #[test]
288    fn test_node_name_parsing() {
289        let name = NodeName::new("mynode@example.com");
290        assert_eq!(name.short_name(), "mynode");
291        assert_eq!(name.host(), "example.com");
292    }
293
294    #[test]
295    fn test_node_name_no_at() {
296        let name = NodeName::new("standalone");
297        assert_eq!(name.short_name(), "standalone");
298        assert_eq!(name.host(), "");
299    }
300
301    #[test]
302    fn test_node_id_serialization() {
303        let id = NodeId::new(42);
304        let bytes = postcard::to_allocvec(&id).unwrap();
305        let decoded: NodeId = postcard::from_bytes(&bytes).unwrap();
306        assert_eq!(id, decoded);
307    }
308}