titanium_cache/
lib.rs

1//! Titan Cache - In-memory cache for Discord entities.
2//!
3//! This crate provides a high-performance concurrent cache using DashMap.
4
5use dashmap::DashMap;
6
7use titanium_model::{Channel, Guild, GuildMember, Role, Snowflake, User};
8
9/// Trait for cache implementations.
10pub trait Cache: Send + Sync {
11    fn guild(&self, id: Snowflake) -> Option<Guild<'static>>;
12    fn channel(&self, id: Snowflake) -> Option<Channel<'static>>;
13    fn user(&self, id: Snowflake) -> Option<User<'static>>;
14    fn member(&self, guild_id: Snowflake, user_id: Snowflake) -> Option<GuildMember<'static>>;
15    fn role(&self, id: Snowflake) -> Option<Role<'static>>;
16
17    fn insert_guild(&self, guild: Guild<'static>);
18    fn insert_channel(&self, channel: Channel<'static>);
19    fn insert_user(&self, user: User<'static>);
20    fn insert_member(&self, guild_id: Snowflake, member: GuildMember<'static>);
21    fn insert_role(&self, id: Snowflake, role: Role<'static>);
22}
23
24use std::time::{Duration, Instant};
25
26/// A cached item with a timestamp.
27struct CachedItem<T> {
28    value: T,
29    created_at: Instant,
30}
31
32impl<T> CachedItem<T> {
33    fn new(value: T) -> Self {
34        Self {
35            value,
36            created_at: Instant::now(),
37        }
38    }
39
40    fn is_expired(&self, ttl: Duration) -> bool {
41        self.created_at.elapsed() > ttl
42    }
43}
44
45/// In-memory cache for Discord entities with Time-To-Live (TTL).
46pub struct InMemoryCache {
47    guilds: DashMap<Snowflake, CachedItem<Guild<'static>>>,
48    channels: DashMap<Snowflake, CachedItem<Channel<'static>>>,
49    users: DashMap<Snowflake, CachedItem<User<'static>>>,
50    members: DashMap<(Snowflake, Snowflake), CachedItem<GuildMember<'static>>>,
51    roles: DashMap<Snowflake, CachedItem<Role<'static>>>,
52    ttl: Duration,
53}
54
55impl InMemoryCache {
56    /// Create a new empty cache with default TTL (1 hour).
57    pub fn new() -> Self {
58        Self::with_ttl(Duration::from_secs(3600))
59    }
60
61    /// Create a new cache with a custom TTL.
62    pub fn with_ttl(ttl: Duration) -> Self {
63        Self {
64            guilds: DashMap::new(),
65            channels: DashMap::new(),
66            users: DashMap::new(),
67            members: DashMap::new(),
68            roles: DashMap::new(),
69            ttl,
70        }
71    }
72
73    /// Garbage collect expired items.
74    ///
75    /// Returns the number of items removed.
76    pub fn sweep(&self) -> usize {
77        let count = 0;
78        let ttl = self.ttl;
79
80        self.guilds.retain(|_, v| !v.is_expired(ttl));
81        self.channels.retain(|_, v| !v.is_expired(ttl));
82        self.users.retain(|_, v| !v.is_expired(ttl));
83        self.members.retain(|_, v| !v.is_expired(ttl));
84        self.roles.retain(|_, v| !v.is_expired(ttl));
85
86        // Note: DashMap::retain doesn't return count easily without locking or iterating.
87        // For high performance, we trust retain does its job.
88        // If we strictly needed a count we would wrap/count, but for now 0 is returned
89        // to match signature or we can change signature.
90        count
91    }
92}
93
94impl Cache for InMemoryCache {
95    fn guild(&self, id: Snowflake) -> Option<Guild<'static>> {
96        self.guilds
97            .get(&id)
98            .filter(|i| !i.is_expired(self.ttl))
99            .map(|r| r.value.clone())
100    }
101
102    fn channel(&self, id: Snowflake) -> Option<Channel<'static>> {
103        self.channels
104            .get(&id)
105            .filter(|i| !i.is_expired(self.ttl))
106            .map(|r| r.value.clone())
107    }
108
109    fn user(&self, id: Snowflake) -> Option<User<'static>> {
110        self.users
111            .get(&id)
112            .filter(|i| !i.is_expired(self.ttl))
113            .map(|r| r.value.clone())
114    }
115
116    fn member(&self, guild_id: Snowflake, user_id: Snowflake) -> Option<GuildMember<'static>> {
117        self.members
118            .get(&(guild_id, user_id))
119            .filter(|i| !i.is_expired(self.ttl))
120            .map(|r| r.value.clone())
121    }
122
123    fn role(&self, id: Snowflake) -> Option<Role<'static>> {
124        self.roles
125            .get(&id)
126            .filter(|i| !i.is_expired(self.ttl))
127            .map(|r| r.value.clone())
128    }
129
130    fn insert_guild(&self, guild: Guild<'static>) {
131        for role in &guild.roles {
132            self.insert_role(role.id, role.clone());
133        }
134        self.guilds.insert(guild.id, CachedItem::new(guild));
135    }
136
137    fn insert_channel(&self, channel: Channel<'static>) {
138        self.channels.insert(channel.id, CachedItem::new(channel));
139    }
140
141    fn insert_user(&self, user: User<'static>) {
142        self.users.insert(user.id, CachedItem::new(user));
143    }
144
145    fn insert_member(&self, guild_id: Snowflake, member: GuildMember<'static>) {
146        if let Some(ref user) = member.user {
147            self.insert_user(user.clone());
148        }
149        self.members.insert(
150            (
151                guild_id,
152                member.user.as_ref().map(|u| u.id).unwrap_or_default(),
153            ),
154            CachedItem::new(member),
155        );
156    }
157
158    fn insert_role(&self, id: Snowflake, role: Role<'static>) {
159        self.roles.insert(id, CachedItem::new(role));
160    }
161}
162
163impl Default for InMemoryCache {
164    fn default() -> Self {
165        Self::new()
166    }
167}