Skip to main content

oxihuman_core/
session_store.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Key-value session store with per-session namespacing and TTL support.
6
7use std::collections::HashMap;
8
9/// A single session containing key-value pairs.
10#[allow(dead_code)]
11#[derive(Debug, Clone)]
12pub struct Session {
13    pub id: String,
14    pub data: HashMap<String, String>,
15    pub created_at: u64,
16    pub expires_at: u64,
17}
18
19#[allow(dead_code)]
20impl Session {
21    pub fn new(id: &str, now: u64, ttl: u64) -> Self {
22        Self {
23            id: id.to_string(),
24            data: HashMap::new(),
25            created_at: now,
26            expires_at: now + ttl,
27        }
28    }
29
30    pub fn is_expired(&self, now: u64) -> bool {
31        now >= self.expires_at
32    }
33
34    pub fn set(&mut self, key: &str, value: &str) {
35        self.data.insert(key.to_string(), value.to_string());
36    }
37
38    pub fn get(&self, key: &str) -> Option<&str> {
39        self.data.get(key).map(|s| s.as_str())
40    }
41
42    pub fn remove(&mut self, key: &str) -> bool {
43        self.data.remove(key).is_some()
44    }
45
46    pub fn entry_count(&self) -> usize {
47        self.data.len()
48    }
49}
50
51/// The session store.
52#[allow(dead_code)]
53pub struct SessionStore {
54    sessions: HashMap<String, Session>,
55    now: u64,
56    default_ttl: u64,
57    evict_count: u64,
58}
59
60#[allow(dead_code)]
61impl SessionStore {
62    pub fn new(default_ttl: u64) -> Self {
63        Self {
64            sessions: HashMap::new(),
65            now: 0,
66            default_ttl,
67            evict_count: 0,
68        }
69    }
70
71    pub fn advance_time(&mut self, dt: u64) {
72        self.now += dt;
73    }
74
75    pub fn create_session(&mut self, id: &str) -> bool {
76        if self.sessions.contains_key(id) {
77            return false;
78        }
79        let s = Session::new(id, self.now, self.default_ttl);
80        self.sessions.insert(id.to_string(), s);
81        true
82    }
83
84    pub fn destroy_session(&mut self, id: &str) -> bool {
85        self.sessions.remove(id).is_some()
86    }
87
88    pub fn set(&mut self, session_id: &str, key: &str, value: &str) -> bool {
89        if let Some(s) = self.sessions.get_mut(session_id) {
90            if !s.is_expired(self.now) {
91                s.set(key, value);
92                return true;
93            }
94        }
95        false
96    }
97
98    pub fn get(&self, session_id: &str, key: &str) -> Option<&str> {
99        self.sessions
100            .get(session_id)
101            .filter(|s| !s.is_expired(self.now))
102            .and_then(|s| s.get(key))
103    }
104
105    pub fn evict_expired(&mut self) -> usize {
106        let before = self.sessions.len();
107        let now = self.now;
108        self.sessions.retain(|_, s| !s.is_expired(now));
109        let evicted = before - self.sessions.len();
110        self.evict_count += evicted as u64;
111        evicted
112    }
113
114    pub fn session_count(&self) -> usize {
115        self.sessions.len()
116    }
117
118    pub fn evict_count(&self) -> u64 {
119        self.evict_count
120    }
121
122    pub fn has_session(&self, id: &str) -> bool {
123        self.sessions.contains_key(id)
124    }
125
126    pub fn now(&self) -> u64 {
127        self.now
128    }
129
130    pub fn clear(&mut self) {
131        self.sessions.clear();
132    }
133}
134
135impl Default for SessionStore {
136    fn default() -> Self {
137        Self::new(3600)
138    }
139}
140
141pub fn new_session_store(default_ttl: u64) -> SessionStore {
142    SessionStore::new(default_ttl)
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148
149    #[test]
150    fn create_and_set() {
151        let mut store = new_session_store(1000);
152        store.create_session("sess1");
153        assert!(store.set("sess1", "user", "alice"));
154        assert_eq!(store.get("sess1", "user"), Some("alice"));
155    }
156
157    #[test]
158    fn expired_session_invisible() {
159        let mut store = new_session_store(10);
160        store.create_session("s");
161        store.set("s", "k", "v");
162        store.advance_time(20);
163        assert!(store.get("s", "k").is_none());
164    }
165
166    #[test]
167    fn evict_removes_expired() {
168        let mut store = new_session_store(5);
169        store.create_session("a");
170        store.advance_time(10);
171        store.evict_expired();
172        assert!(!store.has_session("a"));
173    }
174
175    #[test]
176    fn duplicate_session_rejected() {
177        let mut store = new_session_store(100);
178        assert!(store.create_session("x"));
179        assert!(!store.create_session("x"));
180    }
181
182    #[test]
183    fn destroy_session() {
184        let mut store = new_session_store(100);
185        store.create_session("s");
186        assert!(store.destroy_session("s"));
187        assert!(!store.has_session("s"));
188    }
189
190    #[test]
191    fn session_count() {
192        let mut store = new_session_store(100);
193        store.create_session("a");
194        store.create_session("b");
195        assert_eq!(store.session_count(), 2);
196    }
197
198    #[test]
199    fn evict_count_tracked() {
200        let mut store = new_session_store(1);
201        store.create_session("x");
202        store.advance_time(5);
203        store.evict_expired();
204        assert_eq!(store.evict_count(), 1);
205    }
206
207    #[test]
208    fn clear_all() {
209        let mut store = new_session_store(100);
210        store.create_session("a");
211        store.clear();
212        assert_eq!(store.session_count(), 0);
213    }
214
215    #[test]
216    fn get_missing_key() {
217        let mut store = new_session_store(100);
218        store.create_session("s");
219        assert!(store.get("s", "nope").is_none());
220    }
221
222    #[test]
223    fn advance_time_tracked() {
224        let mut store = new_session_store(100);
225        store.advance_time(50);
226        assert_eq!(store.now(), 50);
227    }
228}