titanium_cache/
lib.rs

1//! Titanium Cache - High-performance in-memory cache for Discord entities.
2//!
3//! This crate provides [`InMemoryCache`] for caching Discord entities like
4//! guilds, channels, users, and members.
5//!
6//! # Features
7//!
8//! - **Lock-Free Concurrency**: Uses [`DashMap`] for concurrent read/write access
9//! - **TTL Support**: Automatic expiration of stale entries (default: 1 hour)
10//! - **Garbage Collection**: [`InMemoryCache::sweep`] removes expired entries
11//! - **Arc-Wrapped Values**: Cheap cloning for multi-consumer scenarios
12//!
13//! # Example
14//!
15//! ```no_run
16//! use titanium_cache::{Cache, InMemoryCache};
17//! use std::time::Duration;
18//! use std::sync::Arc;
19//!
20//! // Create cache with custom TTL
21//! let cache = InMemoryCache::with_ttl(Duration::from_secs(600));
22//!
23//! // Insert and retrieve (example with mock data)
24//! // cache.insert_user(Arc::new(user));
25//! // let user = cache.user(user_id);
26//! ```
27//!
28//! # Memory Management
29//!
30//! The cache stores all entities in memory. For large bots, consider:
31//! - Using shorter TTL values
32//! - Calling [`InMemoryCache::sweep`] periodically
33//! - Implementing a custom [`Cache`] trait with Redis/database backing
34
35use dashmap::DashMap;
36use std::sync::Arc;
37use std::time::{Duration, Instant};
38
39use titanium_model::{Channel, Guild, GuildMember, Role, Snowflake, User};
40
41/// Trait for cache implementations.
42///
43/// Implement this trait to provide custom caching backends (e.g., Redis, database).
44/// The default implementation is [`InMemoryCache`].
45pub trait Cache: Send + Sync {
46    /// Get a guild by ID.
47    fn guild(&self, id: Snowflake) -> Option<Arc<Guild<'static>>>;
48    /// Get a channel by ID.
49    fn channel(&self, id: Snowflake) -> Option<Arc<Channel<'static>>>;
50    /// Get a user by ID.
51    fn user(&self, id: Snowflake) -> Option<Arc<User<'static>>>;
52    /// Get a guild member by guild and user ID.
53    fn member(&self, guild_id: Snowflake, user_id: Snowflake) -> Option<Arc<GuildMember<'static>>>;
54    /// Get a role by ID.
55    fn role(&self, id: Snowflake) -> Option<Arc<Role<'static>>>;
56
57    /// Insert a guild into the cache.
58    fn insert_guild(&self, guild: Arc<Guild<'static>>);
59    /// Insert a channel into the cache.
60    fn insert_channel(&self, channel: Arc<Channel<'static>>);
61    /// Insert a user into the cache.
62    fn insert_user(&self, user: Arc<User<'static>>);
63    /// Insert a guild member into the cache.
64    fn insert_member(&self, guild_id: Snowflake, member: Arc<GuildMember<'static>>);
65    /// Insert a role into the cache.
66    fn insert_role(&self, id: Snowflake, role: Arc<Role<'static>>);
67
68    /// Remove a guild from the cache.
69    fn remove_guild(&self, id: Snowflake) -> Option<Arc<Guild<'static>>>;
70    /// Remove a channel from the cache.
71    fn remove_channel(&self, id: Snowflake) -> Option<Arc<Channel<'static>>>;
72    /// Remove a user from the cache.
73    fn remove_user(&self, id: Snowflake) -> Option<Arc<User<'static>>>;
74    /// Remove a guild member from the cache.
75    fn remove_member(
76        &self,
77        guild_id: Snowflake,
78        user_id: Snowflake,
79    ) -> Option<Arc<GuildMember<'static>>>;
80    /// Remove a role from the cache.
81    fn remove_role(&self, id: Snowflake) -> Option<Arc<Role<'static>>>;
82}
83
84/// A cached item with a creation timestamp for TTL tracking.
85struct CachedItem<T> {
86    value: T,
87    created_at: Instant,
88}
89
90impl<T> CachedItem<T> {
91    fn new(value: T) -> Self {
92        Self {
93            value,
94            created_at: Instant::now(),
95        }
96    }
97
98    fn is_expired(&self, ttl: Duration) -> bool {
99        self.created_at.elapsed() > ttl
100    }
101}
102
103/// In-memory cache for Discord entities with Time-To-Live (TTL).
104///
105/// Uses [`DashMap`] for lock-free concurrent access. All stored values
106/// are wrapped in [`Arc`] for efficient sharing.
107///
108/// # TTL Behavior
109///
110/// - Default TTL is 1 hour
111/// - Expired items are filtered out on read
112/// - Call [`InMemoryCache::sweep`] to remove expired items and free memory
113pub struct InMemoryCache {
114    guilds: DashMap<Snowflake, CachedItem<Arc<Guild<'static>>>>,
115    channels: DashMap<Snowflake, CachedItem<Arc<Channel<'static>>>>,
116    users: DashMap<Snowflake, CachedItem<Arc<User<'static>>>>,
117    members: DashMap<(Snowflake, Snowflake), CachedItem<Arc<GuildMember<'static>>>>,
118    roles: DashMap<Snowflake, CachedItem<Arc<Role<'static>>>>,
119    ttl: Duration,
120}
121
122impl InMemoryCache {
123    /// Create a new empty cache with default TTL (1 hour).
124    pub fn new() -> Self {
125        Self::with_ttl(Duration::from_secs(3600))
126    }
127
128    /// Create a new cache with a custom TTL.
129    pub fn with_ttl(ttl: Duration) -> Self {
130        Self {
131            guilds: DashMap::new(),
132            channels: DashMap::new(),
133            users: DashMap::new(),
134            members: DashMap::new(),
135            roles: DashMap::new(),
136            ttl,
137        }
138    }
139
140    /// Garbage collect expired items.
141    ///
142    /// Returns the number of items removed.
143    pub fn sweep(&self) -> usize {
144        let ttl = self.ttl;
145        let before = self.guilds.len()
146            + self.channels.len()
147            + self.users.len()
148            + self.members.len()
149            + self.roles.len();
150
151        self.guilds.retain(|_, v| !v.is_expired(ttl));
152        self.channels.retain(|_, v| !v.is_expired(ttl));
153        self.users.retain(|_, v| !v.is_expired(ttl));
154        self.members.retain(|_, v| !v.is_expired(ttl));
155        self.roles.retain(|_, v| !v.is_expired(ttl));
156
157        let after = self.guilds.len()
158            + self.channels.len()
159            + self.users.len()
160            + self.members.len()
161            + self.roles.len();
162        before.saturating_sub(after)
163    }
164}
165
166impl Cache for InMemoryCache {
167    fn guild(&self, id: Snowflake) -> Option<Arc<Guild<'static>>> {
168        self.guilds
169            .get(&id)
170            .filter(|i| !i.is_expired(self.ttl))
171            .map(|r| r.value.clone())
172    }
173
174    fn channel(&self, id: Snowflake) -> Option<Arc<Channel<'static>>> {
175        self.channels
176            .get(&id)
177            .filter(|i| !i.is_expired(self.ttl))
178            .map(|r| r.value.clone())
179    }
180
181    fn user(&self, id: Snowflake) -> Option<Arc<User<'static>>> {
182        self.users
183            .get(&id)
184            .filter(|i| !i.is_expired(self.ttl))
185            .map(|r| r.value.clone())
186    }
187
188    fn member(&self, guild_id: Snowflake, user_id: Snowflake) -> Option<Arc<GuildMember<'static>>> {
189        self.members
190            .get(&(guild_id, user_id))
191            .filter(|i| !i.is_expired(self.ttl))
192            .map(|r| r.value.clone())
193    }
194
195    fn role(&self, id: Snowflake) -> Option<Arc<Role<'static>>> {
196        self.roles
197            .get(&id)
198            .filter(|i| !i.is_expired(self.ttl))
199            .map(|r| r.value.clone())
200    }
201
202    fn insert_guild(&self, guild: Arc<Guild<'static>>) {
203        for role in &guild.roles {
204            self.insert_role(role.id, Arc::new(role.clone()));
205        }
206        self.guilds.insert(guild.id, CachedItem::new(guild));
207    }
208
209    fn insert_channel(&self, channel: Arc<Channel<'static>>) {
210        self.channels.insert(channel.id, CachedItem::new(channel));
211    }
212
213    fn insert_user(&self, user: Arc<User<'static>>) {
214        self.users.insert(user.id, CachedItem::new(user));
215    }
216
217    fn insert_member(&self, guild_id: Snowflake, member: Arc<GuildMember<'static>>) {
218        if let Some(ref user) = member.user {
219            self.insert_user(Arc::new(user.clone()));
220        }
221        self.members.insert(
222            (
223                guild_id,
224                member.user.as_ref().map(|u| u.id).unwrap_or_default(),
225            ),
226            CachedItem::new(member),
227        );
228    }
229
230    fn insert_role(&self, id: Snowflake, role: Arc<Role<'static>>) {
231        self.roles.insert(id, CachedItem::new(role));
232    }
233
234    fn remove_guild(&self, id: Snowflake) -> Option<Arc<Guild<'static>>> {
235        self.guilds.remove(&id).map(|(_, v)| v.value)
236    }
237
238    fn remove_channel(&self, id: Snowflake) -> Option<Arc<Channel<'static>>> {
239        self.channels.remove(&id).map(|(_, v)| v.value)
240    }
241
242    fn remove_user(&self, id: Snowflake) -> Option<Arc<User<'static>>> {
243        self.users.remove(&id).map(|(_, v)| v.value)
244    }
245
246    fn remove_member(
247        &self,
248        guild_id: Snowflake,
249        user_id: Snowflake,
250    ) -> Option<Arc<GuildMember<'static>>> {
251        self.members
252            .remove(&(guild_id, user_id))
253            .map(|(_, v)| v.value)
254    }
255
256    fn remove_role(&self, id: Snowflake) -> Option<Arc<Role<'static>>> {
257        self.roles.remove(&id).map(|(_, v)| v.value)
258    }
259}
260
261impl Default for InMemoryCache {
262    fn default() -> Self {
263        Self::new()
264    }
265}