gsm_session/
lib.rs

1mod memory;
2#[cfg(feature = "redis-store")]
3mod redis_store;
4
5use std::{env, sync::Arc};
6
7use anyhow::Result;
8use async_trait::async_trait;
9use greentic_types::{TenantCtx, session::SessionKey};
10use serde::{Deserialize, Serialize};
11use time::OffsetDateTime;
12#[cfg(not(feature = "redis-store"))]
13use tracing::warn;
14
15pub use memory::MemorySessionStore;
16#[cfg(feature = "redis-store")]
17pub use redis_store::RedisSessionStore;
18
19/// Shared session store handle used across services.
20pub type SharedSessionStore = Arc<dyn SessionStore>;
21
22/// Scope describing a user conversation on a channel.
23#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
24pub struct ConversationScope {
25    pub env: String,
26    pub tenant: String,
27    pub platform: String,
28    pub chat_id: String,
29    pub user_id: String,
30    #[serde(default, skip_serializing_if = "Option::is_none")]
31    pub thread_id: Option<String>,
32}
33
34impl ConversationScope {
35    pub fn new(
36        env: impl Into<String>,
37        tenant: impl Into<String>,
38        platform: impl Into<String>,
39        chat_id: impl Into<String>,
40        user_id: impl Into<String>,
41        thread_id: Option<String>,
42    ) -> Self {
43        Self {
44            env: env.into(),
45            tenant: tenant.into(),
46            platform: platform.into(),
47            chat_id: chat_id.into(),
48            user_id: user_id.into(),
49            thread_id,
50        }
51    }
52
53    pub fn cache_key(&self) -> String {
54        format!(
55            "{}:{}:{}:{}:{}:{}",
56            self.env,
57            self.tenant,
58            self.platform,
59            self.chat_id,
60            self.user_id,
61            self.thread_id.as_deref().unwrap_or_default()
62        )
63    }
64}
65
66/// Serialized snapshot captured for a session.
67#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct SessionSnapshot {
69    pub tenant_ctx: TenantCtx,
70    pub flow_id: String,
71    pub cursor_node: String,
72    pub context_json: String,
73}
74
75/// Persisted record tying a session identifier with its flow snapshot.
76#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct SessionRecord {
78    pub key: SessionKey,
79    pub scope: ConversationScope,
80    pub snapshot: SessionSnapshot,
81    pub updated_unix_ms: i128,
82}
83
84impl SessionRecord {
85    pub fn new(key: SessionKey, scope: ConversationScope, snapshot: SessionSnapshot) -> Self {
86        Self {
87            key,
88            scope,
89            snapshot,
90            updated_unix_ms: OffsetDateTime::now_utc().unix_timestamp_nanos() / 1_000_000,
91        }
92    }
93}
94
95#[async_trait]
96pub trait SessionStore: Send + Sync {
97    async fn save(&self, record: SessionRecord) -> Result<()>;
98    async fn get(&self, key: &SessionKey) -> Result<Option<SessionRecord>>;
99    async fn find_by_scope(&self, scope: &ConversationScope) -> Result<Option<SessionRecord>>;
100    async fn delete(&self, key: &SessionKey) -> Result<()>;
101}
102
103/// Returns an in-memory session store wrapped in an [`Arc`].
104pub fn shared_memory_store() -> SharedSessionStore {
105    Arc::new(MemorySessionStore::new())
106}
107
108/// Builds a session store from environment variables.
109///
110/// If `SESSION_REDIS_URL` is present and the `redis-store` feature is enabled, a Redis-backed
111/// store is created. Otherwise, the function falls back to the in-memory implementation.
112pub async fn store_from_env() -> Result<SharedSessionStore> {
113    match env::var("SESSION_REDIS_URL") {
114        Ok(url) => {
115            let namespace = env::var("SESSION_NAMESPACE").unwrap_or_else(|_| "gsm".into());
116            build_redis_store(&url, &namespace).await
117        }
118        Err(_) => Ok(shared_memory_store()),
119    }
120}
121
122#[cfg(feature = "redis-store")]
123async fn build_redis_store(url: &str, namespace: &str) -> Result<SharedSessionStore> {
124    let store = RedisSessionStore::connect(url, namespace).await?;
125    Ok(Arc::new(store))
126}
127
128#[cfg(not(feature = "redis-store"))]
129async fn build_redis_store(_url: &str, _namespace: &str) -> Result<SharedSessionStore> {
130    warn!("redis-store feature disabled; using in-memory session store");
131    Ok(shared_memory_store())
132}