Skip to main content

forge_primitives/
session_slot.rs

1//! `SessionSlot` - the `(org, project, label)` triple that names a
2//! session, and the routing key the view <-> workspace boundary
3//! addresses one session task by.
4//!
5//! The triple names the slot; the session id names the occupant. A
6//! slot is stable for the life of a project's lead session or a
7//! worker's: `/new`, `/resume` and a restart swap the occupant and
8//! leave the slot alone.
9//!
10//! Lives in forge-primitives so the same routing key flows through
11//! every layer (view, workspace, agent) without each crate growing its
12//! own near-identical newtype.
13
14use crate::workers::LEAD_LABEL;
15
16/// The `(org, project, label)` triple naming one session. `org` and
17/// `project` are the `forge.toml` names - the same pair the redb
18/// `sessions` table is keyed by - and `label` carries the role:
19/// [`LEAD_LABEL`] for a project's lead, the worker's own label
20/// otherwise.
21#[derive(Clone, Debug, Hash, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
22pub struct SessionSlot {
23    org: String,
24    project: String,
25    label: String,
26}
27
28impl SessionSlot {
29    pub fn new(
30        org: impl Into<String>,
31        project: impl Into<String>,
32        label: impl Into<String>,
33    ) -> Self {
34        Self { org: org.into(), project: project.into(), label: label.into() }
35    }
36
37    /// A project's lead slot.
38    pub fn lead(org: impl Into<String>, project: impl Into<String>) -> Self {
39        Self::new(org, project, LEAD_LABEL)
40    }
41
42    /// A worker's slot: its project plus the label that names it.
43    pub fn worker(
44        org: impl Into<String>,
45        project: impl Into<String>,
46        label: impl Into<String>,
47    ) -> Self {
48        Self::new(org, project, label)
49    }
50
51    /// A slot from a label that may be absent, `None` meaning the
52    /// project's lead. The shape the cron and Gotify routers hold,
53    /// where a durable entry's `team_role` names a worker or nothing.
54    pub fn for_label(org: &str, project: &str, label: Option<&str>) -> Self {
55        match label {
56            Some(label) => Self::new(org, project, label),
57            None => Self::lead(org, project),
58        }
59    }
60
61    pub fn org(&self) -> &str {
62        &self.org
63    }
64
65    pub fn project(&self) -> &str {
66        &self.project
67    }
68
69    pub fn label(&self) -> &str {
70        &self.label
71    }
72
73    pub fn is_lead(&self) -> bool {
74        self.label == LEAD_LABEL
75    }
76
77    /// The slot's display string, `org/project/label`. For tracing and
78    /// for the few surfaces that need one string to render; it is not
79    /// a key and nothing parses it back.
80    pub fn display(&self) -> String {
81        format!("{}/{}/{}", self.org, self.project, self.label)
82    }
83
84    /// Test-only constructor for fixtures across crate boundaries
85    /// (workspace, server and harness tests), where a fixture wants a
86    /// distinct slot from one opaque name. Gated behind the
87    /// `test-helpers` Cargo feature so production builds don't carry
88    /// the helper.
89    #[cfg(feature = "test-helpers")]
90    pub fn from_str_for_test(s: impl Into<String>) -> Self {
91        Self::worker("TestOrg", "test-project", s)
92    }
93}