discord_rda 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +398 -0
- data/lib/discord_rda/bot.rb +842 -0
- data/lib/discord_rda/cache/configurable_cache.rb +283 -0
- data/lib/discord_rda/cache/entity_cache.rb +184 -0
- data/lib/discord_rda/cache/memory_store.rb +143 -0
- data/lib/discord_rda/cache/redis_store.rb +136 -0
- data/lib/discord_rda/cache/store.rb +56 -0
- data/lib/discord_rda/connection/gateway_client.rb +383 -0
- data/lib/discord_rda/connection/invalid_bucket.rb +205 -0
- data/lib/discord_rda/connection/rate_limiter.rb +280 -0
- data/lib/discord_rda/connection/request_queue.rb +340 -0
- data/lib/discord_rda/connection/reshard_manager.rb +328 -0
- data/lib/discord_rda/connection/rest_client.rb +316 -0
- data/lib/discord_rda/connection/rest_proxy.rb +165 -0
- data/lib/discord_rda/connection/scalable_rest_client.rb +526 -0
- data/lib/discord_rda/connection/shard_manager.rb +223 -0
- data/lib/discord_rda/core/async_runtime.rb +108 -0
- data/lib/discord_rda/core/configuration.rb +194 -0
- data/lib/discord_rda/core/logger.rb +188 -0
- data/lib/discord_rda/core/snowflake.rb +121 -0
- data/lib/discord_rda/entity/attachment.rb +88 -0
- data/lib/discord_rda/entity/base.rb +103 -0
- data/lib/discord_rda/entity/channel.rb +446 -0
- data/lib/discord_rda/entity/channel_builder.rb +280 -0
- data/lib/discord_rda/entity/color.rb +253 -0
- data/lib/discord_rda/entity/embed.rb +221 -0
- data/lib/discord_rda/entity/emoji.rb +89 -0
- data/lib/discord_rda/entity/factory.rb +99 -0
- data/lib/discord_rda/entity/guild.rb +619 -0
- data/lib/discord_rda/entity/member.rb +263 -0
- data/lib/discord_rda/entity/message.rb +405 -0
- data/lib/discord_rda/entity/message_builder.rb +369 -0
- data/lib/discord_rda/entity/role.rb +157 -0
- data/lib/discord_rda/entity/support.rb +294 -0
- data/lib/discord_rda/entity/user.rb +231 -0
- data/lib/discord_rda/entity/value_objects.rb +263 -0
- data/lib/discord_rda/event/auto_moderation.rb +294 -0
- data/lib/discord_rda/event/base.rb +986 -0
- data/lib/discord_rda/event/bus.rb +225 -0
- data/lib/discord_rda/event/scheduled_event.rb +257 -0
- data/lib/discord_rda/hot_reload_manager.rb +303 -0
- data/lib/discord_rda/interactions/application_command.rb +436 -0
- data/lib/discord_rda/interactions/command_system.rb +484 -0
- data/lib/discord_rda/interactions/components.rb +464 -0
- data/lib/discord_rda/interactions/interaction.rb +553 -0
- data/lib/discord_rda/plugin/analytics_plugin.rb +528 -0
- data/lib/discord_rda/plugin/base.rb +190 -0
- data/lib/discord_rda/plugin/registry.rb +126 -0
- data/lib/discord_rda/version.rb +5 -0
- data/lib/discord_rda.rb +70 -0
- metadata +302 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DiscordRDA
|
|
4
|
+
# Configurable cache manager.
|
|
5
|
+
# By default, caches NOTHING - follows Discordeno philosophy.
|
|
6
|
+
# Users can opt-in to caching only what they need.
|
|
7
|
+
#
|
|
8
|
+
class ConfigurableCache
|
|
9
|
+
# Cache nothing strategy
|
|
10
|
+
STRATEGY_NONE = :none
|
|
11
|
+
|
|
12
|
+
# Cache everything strategy
|
|
13
|
+
STRATEGY_FULL = :full
|
|
14
|
+
|
|
15
|
+
# Custom strategy - user specifies what to cache
|
|
16
|
+
STRATEGY_CUSTOM = :custom
|
|
17
|
+
|
|
18
|
+
# @return [Symbol] Current cache strategy
|
|
19
|
+
attr_reader :strategy
|
|
20
|
+
|
|
21
|
+
# @return [CacheStore] Cache store backend
|
|
22
|
+
attr_reader :store
|
|
23
|
+
|
|
24
|
+
# @return [Logger] Logger instance
|
|
25
|
+
attr_reader :logger
|
|
26
|
+
|
|
27
|
+
# @return [Hash] Enabled cache types
|
|
28
|
+
attr_reader :enabled_caches
|
|
29
|
+
|
|
30
|
+
# @return [Array<Symbol>] Properties to cache per entity
|
|
31
|
+
attr_reader :cached_properties
|
|
32
|
+
|
|
33
|
+
# Initialize configurable cache
|
|
34
|
+
# @param strategy [Symbol] Cache strategy (:none, :full, :custom)
|
|
35
|
+
# @param store [CacheStore] Cache store backend
|
|
36
|
+
# @param logger [Logger] Logger instance
|
|
37
|
+
# @param enabled_caches [Array<Symbol>] Which entity types to cache (for :custom)
|
|
38
|
+
# @param cached_properties [Hash] Which properties to cache per entity
|
|
39
|
+
def initialize(
|
|
40
|
+
strategy: STRATEGY_NONE,
|
|
41
|
+
store: nil,
|
|
42
|
+
logger: nil,
|
|
43
|
+
enabled_caches: [],
|
|
44
|
+
cached_properties: {}
|
|
45
|
+
)
|
|
46
|
+
@strategy = strategy
|
|
47
|
+
@store = store || MemoryStore.new
|
|
48
|
+
@logger = logger
|
|
49
|
+
@enabled_caches = enabled_caches
|
|
50
|
+
@cached_properties = cached_properties
|
|
51
|
+
|
|
52
|
+
@logger&.info('Cache initialized', strategy: strategy, enabled: enabled_caches)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Cache an entity (only if enabled for this type)
|
|
56
|
+
# @param type [Symbol] Entity type
|
|
57
|
+
# @param id [String, Snowflake] Entity ID
|
|
58
|
+
# @param entity [Entity] Entity to cache
|
|
59
|
+
# @param ttl [Integer] Time to live in seconds
|
|
60
|
+
# @return [void]
|
|
61
|
+
def cache(type, id, entity, ttl: 300)
|
|
62
|
+
return unless should_cache?(type)
|
|
63
|
+
|
|
64
|
+
# If custom properties specified, only cache those
|
|
65
|
+
if @cached_properties[type]
|
|
66
|
+
entity = filter_properties(entity, @cached_properties[type])
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
key = "#{type}:#{id}"
|
|
70
|
+
@store.set(key, entity, ttl: ttl)
|
|
71
|
+
|
|
72
|
+
@logger&.debug('Cached entity', type: type, id: id, strategy: @strategy)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Get cached entity
|
|
76
|
+
# @param type [Symbol] Entity type
|
|
77
|
+
# @param id [String, Snowflake] Entity ID
|
|
78
|
+
# @return [Entity, nil] Cached entity or nil
|
|
79
|
+
def get(type, id)
|
|
80
|
+
return nil unless should_cache?(type)
|
|
81
|
+
|
|
82
|
+
key = "#{type}:#{id}"
|
|
83
|
+
@store.get(key)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Check if entity should be cached
|
|
87
|
+
# @param type [Symbol] Entity type
|
|
88
|
+
# @return [Boolean] True if should cache
|
|
89
|
+
def should_cache?(type)
|
|
90
|
+
case @strategy
|
|
91
|
+
when STRATEGY_NONE
|
|
92
|
+
false
|
|
93
|
+
when STRATEGY_FULL
|
|
94
|
+
true
|
|
95
|
+
when STRATEGY_CUSTOM
|
|
96
|
+
@enabled_caches.include?(type)
|
|
97
|
+
else
|
|
98
|
+
false
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Invalidate an entity
|
|
103
|
+
# @param type [Symbol] Entity type
|
|
104
|
+
# @param id [String, Snowflake] Entity ID
|
|
105
|
+
# @return [void]
|
|
106
|
+
def invalidate(type, id)
|
|
107
|
+
key = "#{type}:#{id}"
|
|
108
|
+
@store.delete(key)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Clear all cached data
|
|
112
|
+
# @return [void]
|
|
113
|
+
def clear
|
|
114
|
+
@store.clear
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Get cache statistics
|
|
118
|
+
# @return [Hash] Statistics
|
|
119
|
+
def stats
|
|
120
|
+
base_stats = @store.respond_to?(:stats) ? @store.stats : {}
|
|
121
|
+
|
|
122
|
+
{
|
|
123
|
+
strategy: @strategy,
|
|
124
|
+
enabled_caches: @enabled_caches,
|
|
125
|
+
**base_stats
|
|
126
|
+
}
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Create a new cache with different settings (immutable)
|
|
130
|
+
# @param overrides [Hash] Settings to override
|
|
131
|
+
# @return [ConfigurableCache] New cache instance
|
|
132
|
+
def with(**overrides)
|
|
133
|
+
self.class.new(
|
|
134
|
+
strategy: overrides.fetch(:strategy, @strategy),
|
|
135
|
+
store: overrides.fetch(:store, @store),
|
|
136
|
+
logger: overrides.fetch(:logger, @logger),
|
|
137
|
+
enabled_caches: overrides.fetch(:enabled_caches, @enabled_caches),
|
|
138
|
+
cached_properties: overrides.fetch(:cached_properties, @cached_properties)
|
|
139
|
+
)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
private
|
|
143
|
+
|
|
144
|
+
def filter_properties(entity, properties)
|
|
145
|
+
return entity unless entity.respond_to?(:to_h)
|
|
146
|
+
return entity if properties.nil? || properties.empty?
|
|
147
|
+
|
|
148
|
+
data = entity.to_h
|
|
149
|
+
# Support nested property selection with dot notation (e.g., "author.username")
|
|
150
|
+
filtered = {}
|
|
151
|
+
properties.each do |prop|
|
|
152
|
+
if prop.to_s.include?('.')
|
|
153
|
+
parts = prop.to_s.split('.')
|
|
154
|
+
current = data
|
|
155
|
+
current_filtered = filtered
|
|
156
|
+
|
|
157
|
+
parts.each_with_index do |part, idx|
|
|
158
|
+
if idx == parts.length - 1
|
|
159
|
+
current_filtered[part] = current[part] if current && current.key?(part)
|
|
160
|
+
else
|
|
161
|
+
current_filtered[part] ||= {}
|
|
162
|
+
current = current&.dig(part)
|
|
163
|
+
current_filtered = current_filtered[part]
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
else
|
|
167
|
+
filtered[prop.to_s] = data[prop.to_s] if data.key?(prop.to_s)
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Preserve ID if it's a filtered entity
|
|
172
|
+
filtered['id'] = data['id'] if data.key?('id') && !filtered.key?('id')
|
|
173
|
+
|
|
174
|
+
# Create new entity with filtered data
|
|
175
|
+
entity.class.new(filtered)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Advanced property filtering with transforms
|
|
179
|
+
# @param entity [Entity] Entity to filter
|
|
180
|
+
# @param config [Hash] Filter config with :only, :except, :transform options
|
|
181
|
+
# @return [Entity] Filtered entity
|
|
182
|
+
def advanced_filter(entity, config = {})
|
|
183
|
+
return entity unless entity.respond_to?(:to_h)
|
|
184
|
+
|
|
185
|
+
data = entity.to_h
|
|
186
|
+
|
|
187
|
+
# Apply :only filter
|
|
188
|
+
if config[:only]
|
|
189
|
+
data = data.slice(*config[:only].map(&:to_s))
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Apply :except filter
|
|
193
|
+
if config[:except]
|
|
194
|
+
data = data.except(*config[:except].map(&:to_s))
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Apply transforms
|
|
198
|
+
if config[:transform]
|
|
199
|
+
config[:transform].each do |key, transform|
|
|
200
|
+
key_str = key.to_s
|
|
201
|
+
data[key_str] = transform.call(data[key_str]) if data.key?(key_str)
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
entity.class.new(data)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Filter entities by custom predicate
|
|
209
|
+
# @param type [Symbol] Entity type
|
|
210
|
+
# @yield Block to filter entities
|
|
211
|
+
# @return [Array<Entity>] Filtered entities
|
|
212
|
+
def filter_by(type)
|
|
213
|
+
return [] unless block_given?
|
|
214
|
+
return [] unless should_cache?(type)
|
|
215
|
+
|
|
216
|
+
pattern = "#{type}:*"
|
|
217
|
+
all = @store.scan(pattern)
|
|
218
|
+
all.select { |_, entity| yield(entity) }.map { |_, entity| entity }
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Get filtered properties configuration for an entity type
|
|
222
|
+
# @param type [Symbol] Entity type
|
|
223
|
+
# @return [Array<Symbol>, nil] Properties to cache, or nil for all
|
|
224
|
+
def filter_for(type)
|
|
225
|
+
@cached_properties[type]
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# Set filtered properties for an entity type
|
|
229
|
+
# @param type [Symbol] Entity type
|
|
230
|
+
# @param properties [Array<Symbol>] Properties to cache
|
|
231
|
+
# @return [void]
|
|
232
|
+
def set_filter(type, *properties)
|
|
233
|
+
@cached_properties[type] = properties.flatten
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Clear filter for an entity type
|
|
237
|
+
# @param type [Symbol] Entity type
|
|
238
|
+
# @return [void]
|
|
239
|
+
def clear_filter(type)
|
|
240
|
+
@cached_properties.delete(type)
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Batch cache entities with property filtering
|
|
244
|
+
# @param type [Symbol] Entity type
|
|
245
|
+
# @param entities [Array<Entity>] Entities to cache
|
|
246
|
+
# @param ttl [Integer] Time to live in seconds
|
|
247
|
+
# @return [void]
|
|
248
|
+
def cache_batch(type, entities, ttl: 300)
|
|
249
|
+
return unless should_cache?(type)
|
|
250
|
+
|
|
251
|
+
entities.each do |entity|
|
|
252
|
+
cache(type, entity.id, entity, ttl: ttl) if entity.respond_to?(:id)
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Get multiple entities by IDs
|
|
257
|
+
# @param type [Symbol] Entity type
|
|
258
|
+
# @param ids [Array<String, Snowflake>] Entity IDs
|
|
259
|
+
# @return [Array<Entity>] Found entities
|
|
260
|
+
def get_many(type, ids)
|
|
261
|
+
return [] unless should_cache?(type)
|
|
262
|
+
|
|
263
|
+
ids.map { |id| get(type, id) }.compact
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Invalidate multiple entities
|
|
267
|
+
# @param type [Symbol] Entity type
|
|
268
|
+
# @param ids [Array<String, Snowflake>] Entity IDs
|
|
269
|
+
# @return [void]
|
|
270
|
+
def invalidate_many(type, ids)
|
|
271
|
+
ids.each { |id| invalidate(type, id) }
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# Invalidate by pattern
|
|
275
|
+
# @param pattern [String] Pattern to match (e.g., "guild:*:members")
|
|
276
|
+
# @return [Integer] Number of entries invalidated
|
|
277
|
+
def invalidate_pattern(pattern)
|
|
278
|
+
keys = @store.scan(pattern).map { |k, _| k }
|
|
279
|
+
keys.each { |k| @store.delete(k) }
|
|
280
|
+
keys.size
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
end
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DiscordRDA
|
|
4
|
+
# Typed entity cache with automatic invalidation.
|
|
5
|
+
# Provides methods for caching and retrieving Discord entities.
|
|
6
|
+
#
|
|
7
|
+
class EntityCache
|
|
8
|
+
# @return [CacheStore] Cache store
|
|
9
|
+
attr_reader :store
|
|
10
|
+
|
|
11
|
+
# @return [Logger] Logger instance
|
|
12
|
+
attr_reader :logger
|
|
13
|
+
|
|
14
|
+
# Entity TTLs in seconds
|
|
15
|
+
TTL = {
|
|
16
|
+
user: 300,
|
|
17
|
+
guild: 60,
|
|
18
|
+
channel: 300,
|
|
19
|
+
message: 60,
|
|
20
|
+
role: 300,
|
|
21
|
+
member: 120
|
|
22
|
+
}.freeze
|
|
23
|
+
|
|
24
|
+
# Initialize entity cache
|
|
25
|
+
# @param store [CacheStore] Cache store instance
|
|
26
|
+
# @param logger [Logger] Logger instance
|
|
27
|
+
def initialize(store, logger: nil)
|
|
28
|
+
@store = store
|
|
29
|
+
@logger = logger
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Cache a user
|
|
33
|
+
# @param user [User] User to cache
|
|
34
|
+
# @return [void]
|
|
35
|
+
def cache_user(user)
|
|
36
|
+
cache(:user, user.id, user)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Get a cached user
|
|
40
|
+
# @param user_id [String, Snowflake] User ID
|
|
41
|
+
# @return [User, nil] Cached user
|
|
42
|
+
def user(user_id)
|
|
43
|
+
get(:user, user_id)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Cache a guild
|
|
47
|
+
# @param guild [Guild] Guild to cache
|
|
48
|
+
# @return [void]
|
|
49
|
+
def cache_guild(guild)
|
|
50
|
+
cache(:guild, guild.id, guild)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Get a cached guild
|
|
54
|
+
# @param guild_id [String, Snowflake] Guild ID
|
|
55
|
+
# @return [Guild, nil] Cached guild
|
|
56
|
+
def guild(guild_id)
|
|
57
|
+
get(:guild, guild_id)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Cache a channel
|
|
61
|
+
# @param channel [Channel] Channel to cache
|
|
62
|
+
# @return [void]
|
|
63
|
+
def cache_channel(channel)
|
|
64
|
+
cache(:channel, channel.id, channel)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Get a cached channel
|
|
68
|
+
# @param channel_id [String, Snowflake] Channel ID
|
|
69
|
+
# @return [Channel, nil] Cached channel
|
|
70
|
+
def channel(channel_id)
|
|
71
|
+
get(:channel, channel_id)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Cache a message
|
|
75
|
+
# @param message [Message] Message to cache
|
|
76
|
+
# @return [void]
|
|
77
|
+
def cache_message(message)
|
|
78
|
+
cache(:message, message.id, message)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Get a cached message
|
|
82
|
+
# @param message_id [String, Snowflake] Message ID
|
|
83
|
+
# @return [Message, nil] Cached message
|
|
84
|
+
def message(message_id)
|
|
85
|
+
get(:message, message_id)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Cache a role
|
|
89
|
+
# @param role [Role] Role to cache
|
|
90
|
+
# @return [void]
|
|
91
|
+
def cache_role(role)
|
|
92
|
+
cache(:role, role.id, role)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Get a cached role
|
|
96
|
+
# @param role_id [String, Snowflake] Role ID
|
|
97
|
+
# @return [Role, nil] Cached role
|
|
98
|
+
def role(role_id)
|
|
99
|
+
get(:role, role_id)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Cache a member
|
|
103
|
+
# @param member [Member] Member to cache
|
|
104
|
+
# @param guild_id [String, Snowflake] Guild ID
|
|
105
|
+
# @return [void]
|
|
106
|
+
def cache_member(member, guild_id)
|
|
107
|
+
key = "#{guild_id}:#{member.id}"
|
|
108
|
+
cache(:member, key, member)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Get a cached member
|
|
112
|
+
# @param user_id [String, Snowflake] User ID
|
|
113
|
+
# @param guild_id [String, Snowflake] Guild ID
|
|
114
|
+
# @return [Member, nil] Cached member
|
|
115
|
+
def member(user_id, guild_id)
|
|
116
|
+
key = "#{guild_id}:#{user_id}"
|
|
117
|
+
get(:member, key)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Invalidate an entity
|
|
121
|
+
# @param type [Symbol] Entity type
|
|
122
|
+
# @param id [String, Snowflake] Entity ID
|
|
123
|
+
# @return [void]
|
|
124
|
+
def invalidate(type, id)
|
|
125
|
+
key = build_key(type, id)
|
|
126
|
+
@store.delete(key)
|
|
127
|
+
@logger&.debug('Invalidated cache', type: type, id: id)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Invalidate by guild ID
|
|
131
|
+
# @param guild_id [String, Snowflake] Guild ID
|
|
132
|
+
# @return [void]
|
|
133
|
+
def invalidate_guild(guild_id)
|
|
134
|
+
guild_key = guild_id.to_s
|
|
135
|
+
deleted_count = 0
|
|
136
|
+
|
|
137
|
+
# Delete the guild itself
|
|
138
|
+
@store.delete("guild:#{guild_key}")
|
|
139
|
+
deleted_count += 1
|
|
140
|
+
|
|
141
|
+
# Delete all members associated with this guild
|
|
142
|
+
if @store.respond_to?(:keys)
|
|
143
|
+
member_keys = @store.keys("member:#{guild_key}:*")
|
|
144
|
+
member_keys.each do |key|
|
|
145
|
+
@store.delete(key)
|
|
146
|
+
deleted_count += 1
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
@logger&.debug('Invalidated guild cache', guild_id: guild_id, deleted: deleted_count)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Clear all cached entities
|
|
154
|
+
# @return [void]
|
|
155
|
+
def clear
|
|
156
|
+
@store.clear
|
|
157
|
+
@logger&.info('Cleared entity cache')
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Get cache statistics
|
|
161
|
+
# @return [Hash] Statistics
|
|
162
|
+
def stats
|
|
163
|
+
@store.respond_to?(:stats) ? @store.stats : {}
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
private
|
|
167
|
+
|
|
168
|
+
def cache(type, id, entity)
|
|
169
|
+
key = build_key(type, id)
|
|
170
|
+
ttl = TTL[type]
|
|
171
|
+
@store.set(key, entity, ttl: ttl)
|
|
172
|
+
@logger&.debug('Cached entity', type: type, id: id)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def get(type, id)
|
|
176
|
+
key = build_key(type, id)
|
|
177
|
+
@store.get(key)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def build_key(type, id)
|
|
181
|
+
"#{type}:#{id}"
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'lru_redux'
|
|
4
|
+
|
|
5
|
+
module DiscordRDA
|
|
6
|
+
# In-memory LRU cache store.
|
|
7
|
+
# Provides fast, thread-safe caching with size limits and TTL.
|
|
8
|
+
#
|
|
9
|
+
class MemoryStore < CacheStore
|
|
10
|
+
# Default cache size
|
|
11
|
+
DEFAULT_SIZE = 10_000
|
|
12
|
+
|
|
13
|
+
# @return [Integer] Maximum cache size
|
|
14
|
+
attr_reader :max_size
|
|
15
|
+
|
|
16
|
+
# @return [Hash] TTL tracking
|
|
17
|
+
attr_reader :ttl_data
|
|
18
|
+
|
|
19
|
+
# Initialize memory store
|
|
20
|
+
# @param max_size [Integer] Maximum cache size
|
|
21
|
+
def initialize(max_size: DEFAULT_SIZE)
|
|
22
|
+
@max_size = max_size
|
|
23
|
+
@cache = LruRedux::Cache.new(max_size)
|
|
24
|
+
@ttl_data = {}
|
|
25
|
+
@mutex = Mutex.new
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Get a value from cache
|
|
29
|
+
# @param key [String] Cache key
|
|
30
|
+
# @return [Object, nil] Cached value or nil if expired/missing
|
|
31
|
+
def get(key)
|
|
32
|
+
@mutex.synchronize do
|
|
33
|
+
check_ttl(key)
|
|
34
|
+
@cache[key]
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Set a value in cache
|
|
39
|
+
# @param key [String] Cache key
|
|
40
|
+
# @param value [Object] Value to cache
|
|
41
|
+
# @param ttl [Integer] Time to live in seconds
|
|
42
|
+
# @return [void]
|
|
43
|
+
def set(key, value, ttl: nil)
|
|
44
|
+
@mutex.synchronize do
|
|
45
|
+
@cache[key] = value
|
|
46
|
+
@ttl_data[key] = Time.now.utc + ttl if ttl
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Delete a value from cache
|
|
51
|
+
# @param key [String] Cache key
|
|
52
|
+
# @return [void]
|
|
53
|
+
def delete(key)
|
|
54
|
+
@mutex.synchronize do
|
|
55
|
+
@cache.delete(key)
|
|
56
|
+
@ttl_data.delete(key)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Check if key exists and is not expired
|
|
61
|
+
# @param key [String] Cache key
|
|
62
|
+
# @return [Boolean] True if exists
|
|
63
|
+
def exist?(key)
|
|
64
|
+
@mutex.synchronize do
|
|
65
|
+
check_ttl(key)
|
|
66
|
+
@cache.key?(key)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Clear all cached values
|
|
71
|
+
# @return [void]
|
|
72
|
+
def clear
|
|
73
|
+
@mutex.synchronize do
|
|
74
|
+
@cache.clear
|
|
75
|
+
@ttl_data.clear
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Get current cache size
|
|
80
|
+
# @return [Integer] Number of cached items
|
|
81
|
+
def size
|
|
82
|
+
@mutex.synchronize do
|
|
83
|
+
clean_expired
|
|
84
|
+
@cache.size
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Get cache statistics
|
|
89
|
+
# @return [Hash] Statistics
|
|
90
|
+
def stats
|
|
91
|
+
@mutex.synchronize do
|
|
92
|
+
clean_expired
|
|
93
|
+
{
|
|
94
|
+
size: @cache.size,
|
|
95
|
+
max_size: @max_size,
|
|
96
|
+
ttl_entries: @ttl_data.size
|
|
97
|
+
}
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Get keys matching a pattern
|
|
102
|
+
# @param pattern [String, Regexp] Pattern to match (supports globs like "member:123:*")
|
|
103
|
+
# @return [Array<String>] Matching keys
|
|
104
|
+
def keys(pattern)
|
|
105
|
+
@mutex.synchronize do
|
|
106
|
+
clean_expired
|
|
107
|
+
|
|
108
|
+
regex = if pattern.is_a?(Regexp)
|
|
109
|
+
pattern
|
|
110
|
+
else
|
|
111
|
+
# Convert glob pattern to regex
|
|
112
|
+
regex_str = pattern.gsub('.', '\\.')
|
|
113
|
+
.gsub('*', '.*')
|
|
114
|
+
.gsub('?', '.')
|
|
115
|
+
Regexp.new("^#{regex_str}$")
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
@cache.keys.select { |k| k.match?(regex) }
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
private
|
|
123
|
+
|
|
124
|
+
def check_ttl(key)
|
|
125
|
+
ttl = @ttl_data[key]
|
|
126
|
+
return unless ttl
|
|
127
|
+
|
|
128
|
+
if Time.now.utc > ttl
|
|
129
|
+
@cache.delete(key)
|
|
130
|
+
@ttl_data.delete(key)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def clean_expired
|
|
135
|
+
now = Time.now.utc
|
|
136
|
+
expired = @ttl_data.select { |_, ttl| now > ttl }.keys
|
|
137
|
+
expired.each do |key|
|
|
138
|
+
@cache.delete(key)
|
|
139
|
+
@ttl_data.delete(key)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|