greentic_session/
model.rs

1use serde::{Deserialize, Serialize};
2use std::collections::HashSet;
3use time::{Duration, OffsetDateTime};
4use uuid::Uuid;
5
6/// Unique identifier for a session.
7#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
8pub struct SessionId(pub Uuid);
9
10impl SessionId {
11    /// Generates a fresh session identifier.
12    pub fn new() -> Self {
13        Self(Uuid::new_v4())
14    }
15
16    /// Returns the raw UUID.
17    pub fn as_uuid(&self) -> &Uuid {
18        &self.0
19    }
20}
21
22impl Default for SessionId {
23    fn default() -> Self {
24        Self::new()
25    }
26}
27
28/// Stable key used for routing (derived from connectors’ events).
29#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
30pub struct SessionKey(pub String);
31
32impl SessionKey {
33    /// Borrows the underlying key as `&str`.
34    pub fn as_str(&self) -> &str {
35        &self.0
36    }
37}
38
39/// Cursor tracks where to resume a flow.
40#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
41pub struct SessionCursor {
42    pub flow_id: String,
43    pub node_id: String,
44    pub wait_reason: Option<String>,
45    pub outbox_seq: u64,
46}
47
48/// Outbox entry, deduped by (seq, payload hash).
49#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
50pub struct OutboxEntry {
51    pub seq: u64,
52    pub payload_sha256: [u8; 32],
53    pub created_at: OffsetDateTime,
54}
55
56#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
57pub struct SessionMeta {
58    pub tenant_id: String,
59    pub team_id: Option<String>,
60    pub user_id: Option<String>,
61    pub labels: serde_json::Map<String, serde_json::Value>,
62}
63
64#[derive(Clone, Debug, Serialize, Deserialize)]
65pub struct Session {
66    pub id: SessionId,
67    pub key: SessionKey,
68    pub cursor: SessionCursor,
69    pub meta: SessionMeta,
70    pub outbox: Vec<OutboxEntry>,
71    pub updated_at: OffsetDateTime,
72    pub ttl_secs: u32,
73}
74
75impl Session {
76    /// Returns the tenant identifier for convenience.
77    pub fn tenant_id(&self) -> &str {
78        &self.meta.tenant_id
79    }
80
81    /// Applies in-place cleanup such as outbox deduplication and ttl normalization.
82    pub fn normalize(&mut self) {
83        self.dedupe_outbox();
84        if self.ttl_secs == 0 {
85            // A zero TTL is treated as "never expire".
86            self.ttl_secs = 0;
87        }
88    }
89
90    /// Deduplicates the outbox by `(seq, payload_sha256)` while maintaining first-wins ordering.
91    pub fn dedupe_outbox(&mut self) {
92        let mut seen = HashSet::new();
93        self.outbox
94            .retain(|entry| seen.insert((entry.seq, entry.payload_sha256)));
95    }
96
97    /// Returns the computed expiry deadline based on `updated_at` + `ttl_secs`.
98    pub fn expires_at(&self) -> Option<OffsetDateTime> {
99        if self.ttl_secs == 0 {
100            return None;
101        }
102        let ttl = Duration::seconds(self.ttl_secs as i64);
103        Some(self.updated_at + ttl)
104    }
105}
106
107/// Compare-And-Set token; increments on each write.
108#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
109pub struct Cas(pub u64);
110
111impl Cas {
112    /// Initial CAS value assigned to newly created records.
113    pub const fn initial() -> Self {
114        Self(1)
115    }
116
117    /// Sentinel CAS used when the value is absent.
118    pub const fn none() -> Self {
119        Self(0)
120    }
121
122    /// Returns the raw CAS counter.
123    pub const fn value(self) -> u64 {
124        self.0
125    }
126
127    /// Produces the next CAS value.
128    pub const fn next(self) -> Self {
129        Self(self.0.wrapping_add(1))
130    }
131}
132
133impl From<u64> for Cas {
134    fn from(value: u64) -> Self {
135        Self(value)
136    }
137}
138
139impl From<Cas> for u64 {
140    fn from(cas: Cas) -> Self {
141        cas.0
142    }
143}