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}