safe_memoize 0.1.2 → 0.3.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 00d20095844739078f88cd287d4389b749f9953b9b6055939d733f2da139c19d
4
- data.tar.gz: 5c4336dbe5e3ee1e1e2b822b92a3abd4f6d5452dedd36d99706c54eb8528ed7a
3
+ metadata.gz: 85a5a69a8dbeb570065995485ece70d230e18cbf8b39fa204f261174707d7525
4
+ data.tar.gz: f83e4d9b0efb5552e6bf81d2924d0098caddef6748a734c382d0bae10b27ad06
5
5
  SHA512:
6
- metadata.gz: 5d14486d2a280e2e243e2c54184875d175447f8dc921ff9e152374e9f8327061b0b4ce6de0e9213871000c0950a8cab146db7d303a0de8242d081c30e54c53ce
7
- data.tar.gz: 67be0eb15e01782f7dc6b0a35cb79c24677f8d56ebf042a9dfdd49d9ec88a44c08711af24063150688c0b1ad9047c70a6c3754fd1cc662ba54a83000c1c5247a
6
+ metadata.gz: 0a0b5d2017ddd2061ced9215747b1f38ebbaa408f5f7196e5db4c0fd9a4fb5b25c33cdd7766c001dad6596b07f93d3f6c9211bdcdccc4fc9643d1f824b849d51
7
+ data.tar.gz: '09df76d2d919373d9c7a31c77698378454bb996c39554296105dfd3ffc47d7e907ede4cfd6259d3e43ffbf9dda30296564f480b004e1b3676293d7a0f9fd2fd4'
data/CHANGELOG.md CHANGED
@@ -1,5 +1,38 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.3.0] - 2026-05-15
4
+
5
+ - Add `on_memo_hit` hook that fires on every cache hit, completing the lifecycle API alongside `on_memo_expire` and `on_memo_evict`
6
+ - Add conditional memoization via `if:` and `unless:` options on `memoize`
7
+ - `if: ->(result) { ... }` — only caches when the lambda returns truthy
8
+ - `unless: ->(result) { ... }` — skips caching when the lambda returns truthy
9
+ - Uncached calls recompute on every invocation until the condition is met
10
+ - Compatible with `ttl:`, `max_size:`, hooks, and all inspection APIs
11
+ - Add LRU cache size limit via `max_size:` option on `memoize`
12
+ - Evicts the least-recently-used entry per method when the limit is reached
13
+ - Cache hits promote entries to most-recently-used, preventing premature eviction
14
+ - Fires the existing `on_evict` hook for LRU-evicted entries
15
+ - Self-healing: stale LRU references left by `reset_memo` are pruned automatically
16
+ - Compatible with `ttl:` option and all existing inspection/reset APIs
17
+ - Thread-safe under concurrent access
18
+
19
+ ## [0.2.0] - 2026-05-14
20
+
21
+ - Add optional TTL expiration support for memoized entries
22
+ - Add cache invalidation/expiration hooks for custom handlers
23
+ - `on_memo_expire` hook fires when TTL entries expire
24
+ - `on_memo_evict` hook fires when manually resetting cache entries
25
+ - `clear_memo_hooks` to remove registered hooks
26
+ - Add cache statistics and monitoring capabilities
27
+ - `cache_stats` for comprehensive cache metrics
28
+ - `cache_stats_for(method_name)` for per-method statistics
29
+ - `cache_hit_rate` and `cache_miss_rate` for performance analysis
30
+ - `cache_metrics_reset` to clear collected metrics
31
+ - Add manual cache key generation support
32
+ - `memoize_with_custom_key` to define custom cache key logic
33
+ - `clear_custom_keys` to remove custom key generators
34
+ - Support for complex and computed keys based on arguments
35
+
3
36
  ## [0.1.2] - 2026-05-13
4
37
 
5
38
  - Preserve public, protected, and private visibility for memoized methods
data/README.md CHANGED
@@ -27,6 +27,12 @@ SafeMemoize uses `Hash#key?` to distinguish "not yet cached" from "cached nil/fa
27
27
  - Includes a `memo_count` helper for cache size stats
28
28
  - Includes a `memo_keys` helper for inspecting cached signatures
29
29
  - Includes a `memo_values` helper for inspecting cached signatures and values
30
+ - Optional TTL expiration support for cached entries
31
+ - Optional LRU cache size limit per method via `max_size:`
32
+ - Conditional caching via `if:` and `unless:` predicates
33
+ - Lifecycle hooks for hit, eviction, and expiration events
34
+ - Per-instance cache metrics (hit rate, miss rate, computation time)
35
+ - Custom cache key generation per method
30
36
  - Block arguments bypass cache (blocks aren't comparable)
31
37
 
32
38
  ## Installation
@@ -129,6 +135,164 @@ obj.reset_memo(:search, "ruby", page: 2) # Clears one positional/keyword c
129
135
  obj.reset_all_memos # Clears all memoized values
130
136
  ```
131
137
 
138
+ ### Lifecycle hooks
139
+
140
+ Register callbacks that fire when cached entries are evicted or expire.
141
+
142
+ **`on_memo_evict`** fires when an entry is removed via `reset_memo`, `reset_all_memos`, or LRU eviction:
143
+
144
+ ```ruby
145
+ obj.on_memo_evict do |cache_key, record|
146
+ Rails.logger.info("Evicted #{cache_key[0]}(#{cache_key[1].join(", ")}), was: #{record[:value].inspect}")
147
+ end
148
+ ```
149
+
150
+ **`on_memo_hit`** fires on every cache hit:
151
+
152
+ ```ruby
153
+ obj.on_memo_hit do |cache_key, record|
154
+ StatsD.increment("cache.hit", tags: ["method:#{cache_key[0]}"])
155
+ end
156
+ ```
157
+
158
+ **`on_memo_expire`** fires when a TTL entry is detected as expired (on the next call or during inspection):
159
+
160
+ ```ruby
161
+ obj.on_memo_expire do |cache_key, record|
162
+ Rails.logger.debug("TTL expired: #{cache_key[0]}")
163
+ end
164
+ ```
165
+
166
+ Multiple hooks of the same type can be registered and all will fire. Remove them with `clear_memo_hooks`:
167
+
168
+ ```ruby
169
+ obj.clear_memo_hooks(:on_evict) # Clears evict hooks only
170
+ obj.clear_memo_hooks(:on_expire) # Clears expire hooks only
171
+ obj.clear_memo_hooks # Clears all hooks
172
+ ```
173
+
174
+ Hooks are per-instance and do not affect other objects of the same class.
175
+
176
+ ### TTL expiration
177
+
178
+ ```ruby
179
+ class QuoteService
180
+ prepend SafeMemoize
181
+
182
+ def current_quote
183
+ fetch_quote_from_api
184
+ end
185
+ memoize :current_quote, ttl: 60
186
+ end
187
+ ```
188
+
189
+ With a TTL, cached values expire automatically after the given number of seconds. The next call recomputes and refreshes the cache.
190
+
191
+ ### LRU cache size limit
192
+
193
+ Pass `max_size:` to cap how many entries a method can hold. When the limit is reached the least-recently-used entry is evicted to make room:
194
+
195
+ ```ruby
196
+ class ProductService
197
+ prepend SafeMemoize
198
+
199
+ def find(id)
200
+ Product.find(id)
201
+ end
202
+ memoize :find, max_size: 100
203
+ end
204
+ ```
205
+
206
+ Cache hits count as recent access, so a frequently-read entry will never be the one evicted:
207
+
208
+ ```ruby
209
+ svc = ProductService.new
210
+ svc.find(1) # miss — cached
211
+ svc.find(2) # miss — cached
212
+ svc.find(1) # hit — promotes 1 to most-recently-used; 2 is now LRU
213
+ svc.find(3) # miss — evicts 2 (LRU), caches 3
214
+ ```
215
+
216
+ `max_size:` combines with `ttl:` — LRU eviction applies within the TTL window, and entries also expire normally when the TTL elapses:
217
+
218
+ ```ruby
219
+ memoize :find, max_size: 50, ttl: 300
220
+ ```
221
+
222
+ The `on_evict` hook fires for LRU-evicted entries the same way it does for manual `reset_memo` calls.
223
+
224
+ ### Conditional caching
225
+
226
+ Use `if:` to cache a result only when the predicate returns truthy, or `unless:` to skip caching when it returns truthy. Calls that don't satisfy the condition recompute every time until they do.
227
+
228
+ ```ruby
229
+ class UserService
230
+ prepend SafeMemoize
231
+
232
+ # Don't cache nil — retries on every call until a user is found
233
+ def find(id)
234
+ User.find_by(id: id)
235
+ end
236
+ memoize :find, if: ->(result) { !result.nil? }
237
+ end
238
+ ```
239
+
240
+ ```ruby
241
+ class DataService
242
+ prepend SafeMemoize
243
+
244
+ # Don't cache error responses
245
+ def fetch(key)
246
+ api_client.get(key)
247
+ end
248
+ memoize :fetch, unless: ->(result) { result.is_a?(ErrorResponse) }
249
+ end
250
+ ```
251
+
252
+ Both options accept any callable and compose with `ttl:` and `max_size:`:
253
+
254
+ ```ruby
255
+ memoize :find, if: ->(result) { !result.nil? }, ttl: 60, max_size: 500
256
+ ```
257
+
258
+ ### Custom cache keys
259
+
260
+ By default the cache key is derived from the method name and all arguments. Use `memoize_with_custom_key` on an instance to control exactly what makes two calls equivalent:
261
+
262
+ ```ruby
263
+ class ReportService
264
+ prepend SafeMemoize
265
+
266
+ def generate(user_id, options)
267
+ build_report(user_id, options)
268
+ end
269
+ memoize :generate
270
+ end
271
+
272
+ svc = ReportService.new
273
+
274
+ # Cache only by user_id — ignore the options hash entirely
275
+ svc.memoize_with_custom_key(:generate) { |user_id, _options| user_id }
276
+
277
+ svc.generate(42, {format: :pdf}) # computes and caches
278
+ svc.generate(42, {format: :csv}) # cache hit — same user_id, options ignored
279
+ ```
280
+
281
+ The block can return any comparable value — a scalar, array, or hash:
282
+
283
+ ```ruby
284
+ svc.memoize_with_custom_key(:generate) do |user_id, options|
285
+ {user: user_id, locale: options[:locale]}
286
+ end
287
+ ```
288
+
289
+ Custom key generators are per-instance and can be cleared at any time:
290
+
291
+ ```ruby
292
+ svc.clear_custom_keys(:generate) # Remove generator for one method
293
+ svc.clear_custom_keys # Remove all custom key generators
294
+ ```
295
+
132
296
  ### Cache inspection
133
297
 
134
298
  ```ruby
@@ -147,6 +311,33 @@ obj.memo_values # Cached signatures and values for all
147
311
  obj.memo_values(:search) # Cached signatures and values for one method
148
312
  ```
149
313
 
314
+ ### Cache metrics
315
+
316
+ Each instance tracks hits, misses, and computation time automatically.
317
+
318
+ ```ruby
319
+ obj.cache_stats
320
+ # => {
321
+ # total_hits: 42,
322
+ # total_misses: 8,
323
+ # hit_rate: 84.0,
324
+ # miss_rate: 16.0,
325
+ # average_computation_time: 0.012345,
326
+ # entries: [
327
+ # { method: :find, args: [1], hits: 10, misses: 1,
328
+ # hit_rate: 90.91, computation_time: 0.005 },
329
+ # ...
330
+ # ]
331
+ # }
332
+
333
+ obj.cache_stats_for(:find) # Stats scoped to one method
334
+ obj.cache_hit_rate # => 84.0 (percentage)
335
+ obj.cache_miss_rate # => 16.0 (percentage)
336
+ obj.cache_metrics_reset # Clears all collected metrics
337
+ ```
338
+
339
+ Metrics are per-instance and reset independently from the cache itself — clearing metrics does not evict cached values.
340
+
150
341
  ## How It Works
151
342
 
152
343
  SafeMemoize uses Ruby's `prepend` mechanism. When you call `memoize :method_name`, it creates an anonymous module with a wrapper method and prepends it onto your class. The wrapper calls `super` to invoke the original method and stores the result in a per-instance hash. Thread safety is achieved with a per-instance `Mutex` using double-check locking.
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SafeMemoize
4
+ module CacheMetricsMethods
5
+ private
6
+
7
+ def memo_metrics_store
8
+ @__safe_memo_metrics__ ||= {}
9
+ end
10
+
11
+ def record_cache_hit(method_name, args)
12
+ cache_key = safe_memo_cache_key(method_name, args, {})
13
+ metrics = memo_metrics_store
14
+ metrics[cache_key] ||= {hits: 0, misses: 0, total_time: 0.0}
15
+ metrics[cache_key][:hits] += 1
16
+ end
17
+
18
+ def record_cache_miss(method_name, args, computation_time)
19
+ cache_key = safe_memo_cache_key(method_name, args, {})
20
+ metrics = memo_metrics_store
21
+ metrics[cache_key] ||= {hits: 0, misses: 0, total_time: 0.0}
22
+ metrics[cache_key][:misses] += 1
23
+ metrics[cache_key][:total_time] += computation_time
24
+ end
25
+
26
+ def _reset_cache_metrics
27
+ @__safe_memo_metrics__ = {}
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SafeMemoize
4
+ module CacheRecordMethods
5
+ private
6
+
7
+ def memo_ttl(ttl)
8
+ return nil if ttl.nil?
9
+
10
+ ttl = Float(ttl)
11
+ raise ArgumentError, "ttl must be non-negative" if ttl < 0
12
+
13
+ ttl
14
+ rescue ArgumentError, TypeError
15
+ raise ArgumentError, "ttl must be a non-negative number"
16
+ end
17
+
18
+ def memo_expires_at(ttl)
19
+ return nil unless ttl
20
+
21
+ Process.clock_gettime(Process::CLOCK_MONOTONIC) + ttl
22
+ end
23
+
24
+ def memo_record(value, expires_at:)
25
+ {value: value, expires_at: expires_at}
26
+ end
27
+
28
+ def memo_record_value(record)
29
+ record[:value]
30
+ end
31
+
32
+ def memo_record_live?(record)
33
+ return false unless record
34
+
35
+ expires_at = record[:expires_at]
36
+ return true unless expires_at
37
+
38
+ expires_at > Process.clock_gettime(Process::CLOCK_MONOTONIC)
39
+ end
40
+
41
+ def memo_prune_expired_entries!(cache)
42
+ cache.delete_if do |cache_key, record|
43
+ if !memo_record_live?(record)
44
+ call_memo_hooks(:on_expire, cache_key, record)
45
+ true
46
+ else
47
+ false
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SafeMemoize
4
+ module CacheStoreMethods
5
+ private
6
+
7
+ def with_memo_lock
8
+ if defined?(@__safe_memo_mutex__) && @__safe_memo_mutex__
9
+ @__safe_memo_mutex__.synchronize { yield }
10
+ else
11
+ yield
12
+ end
13
+ end
14
+
15
+ def memo_cache_or_nil
16
+ return nil unless defined?(@__safe_memo_cache__)
17
+
18
+ @__safe_memo_cache__
19
+ end
20
+
21
+ def memo_cache_hit?(cache_key)
22
+ !!memo_cache_record(cache_key)
23
+ end
24
+
25
+ def memo_cache_record(cache_key)
26
+ cache = memo_cache_or_nil
27
+ return nil unless cache
28
+
29
+ record = cache[cache_key]
30
+ return nil unless memo_record_live?(record)
31
+
32
+ record
33
+ end
34
+
35
+ def memo_cache_read(cache_key)
36
+ record = memo_cache_record(cache_key)
37
+ return nil unless record
38
+
39
+ memo_record_value(record)
40
+ end
41
+
42
+ def memo_fetch_or_store(cache_key, expires_at: nil)
43
+ memo_mutex!.synchronize do
44
+ @__safe_memo_cache__ ||= {}
45
+
46
+ record = @__safe_memo_cache__[cache_key]
47
+
48
+ if memo_record_live?(record)
49
+ memo_record_value(record)
50
+ else
51
+ value = yield
52
+ @__safe_memo_cache__[cache_key] = memo_record(value, expires_at: expires_at)
53
+
54
+ value
55
+ end
56
+ end
57
+ end
58
+
59
+ def memo_mutex!
60
+ @__safe_memo_mutex__ ||= Mutex.new
61
+ end
62
+
63
+ def with_memo_cache
64
+ cache = memo_cache_or_nil
65
+ return nil unless cache
66
+
67
+ yield cache
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SafeMemoize
4
+ module ClassMethods
5
+ def memoize(method_name, ttl: nil, max_size: nil, if: nil, unless: nil)
6
+ method_name = method_name.to_sym
7
+ visibility = memoized_method_visibility(method_name)
8
+
9
+ cond_if = binding.local_variable_get(:if)
10
+ cond_unless = binding.local_variable_get(:unless)
11
+
12
+ ttl = if ttl.nil?
13
+ nil
14
+ else
15
+ ttl = Float(ttl)
16
+ raise ArgumentError, "ttl must be non-negative" if ttl < 0
17
+
18
+ ttl
19
+ end
20
+
21
+ max_size = if max_size.nil?
22
+ nil
23
+ else
24
+ raise ArgumentError, "max_size must be a positive integer" unless max_size.is_a?(Integer)
25
+ raise ArgumentError, "max_size must be positive" unless max_size > 0
26
+
27
+ max_size
28
+ end
29
+
30
+ if cond_if && cond_unless
31
+ raise ArgumentError, "cannot specify both :if and :unless"
32
+ end
33
+ raise ArgumentError, ":if must be callable" if cond_if && !cond_if.respond_to?(:call)
34
+ raise ArgumentError, ":unless must be callable" if cond_unless && !cond_unless.respond_to?(:call)
35
+
36
+ # Normalize to a single "should cache?" predicate
37
+ condition = if cond_if
38
+ cond_if
39
+ elsif cond_unless
40
+ ->(result) { !cond_unless.call(result) }
41
+ end
42
+
43
+ expires_at = ttl && Process.clock_gettime(Process::CLOCK_MONOTONIC) + ttl
44
+
45
+ mod = Module.new do
46
+ define_method(method_name) do |*args, **kwargs, &block|
47
+ # Blocks bypass cache entirely — they aren't comparable
48
+ return super(*args, **kwargs, &block) if block
49
+
50
+ cache_key = compute_cache_key(method_name, args, kwargs)
51
+
52
+ if max_size || condition
53
+ # Locked path: used when LRU tracking or conditional storage is needed.
54
+ memo_mutex!.synchronize do
55
+ record = memo_cache_record(cache_key)
56
+ if record
57
+ lru_touch(method_name, cache_key) if max_size
58
+ record_cache_hit(method_name, args)
59
+ call_memo_hooks(:on_hit, cache_key, record)
60
+ memo_record_value(record)
61
+ else
62
+ start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
63
+ value = super(*args, **kwargs)
64
+ elapsed_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
65
+
66
+ if !condition || condition.call(value)
67
+ lru_evict_if_over_limit(method_name, max_size) if max_size
68
+ @__safe_memo_cache__ ||= {}
69
+ @__safe_memo_cache__[cache_key] = memo_record(value, expires_at: expires_at)
70
+ lru_touch(method_name, cache_key) if max_size
71
+ end
72
+ record_cache_miss(method_name, args, elapsed_time)
73
+
74
+ value
75
+ end
76
+ end
77
+ else
78
+ # Fast path: check without lock
79
+ if (record = memo_cache_record(cache_key))
80
+ record_cache_hit(method_name, args)
81
+ call_memo_hooks(:on_hit, cache_key, record)
82
+ return memo_record_value(record)
83
+ end
84
+
85
+ # Cache miss - compute and store
86
+ start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
87
+ result = memo_fetch_or_store(cache_key, expires_at: expires_at) { super(*args, **kwargs) }
88
+ elapsed_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
89
+
90
+ with_memo_lock do
91
+ record_cache_miss(method_name, args, elapsed_time)
92
+ end
93
+
94
+ result
95
+ end
96
+ end
97
+
98
+ send(visibility, method_name)
99
+ end
100
+
101
+ prepend mod
102
+ end
103
+
104
+ private
105
+
106
+ def memoized_method_visibility(method_name)
107
+ return :private if private_method_defined?(method_name)
108
+ return :protected if protected_method_defined?(method_name)
109
+
110
+ :public
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SafeMemoize
4
+ module CustomKeyMethods
5
+ private
6
+
7
+ def custom_key_store
8
+ @__safe_memo_custom_keys__ ||= {}
9
+ end
10
+
11
+ def register_custom_key(method_name, &block)
12
+ raise ArgumentError, "block required" unless block
13
+
14
+ method_name = method_name.to_sym
15
+ custom_key_store[method_name] = block
16
+ end
17
+
18
+ def compute_cache_key(method_name, args, kwargs)
19
+ method_name = method_name.to_sym
20
+
21
+ # Check if a custom key generator is registered
22
+ custom_key_block = custom_key_store[method_name]
23
+
24
+ if custom_key_block
25
+ # Call the custom key generator with args and kwargs
26
+ custom_key = custom_key_block.call(*args, **kwargs)
27
+ # Wrap in a standard format: [method, custom_key]
28
+ [method_name, custom_key]
29
+ else
30
+ # Use default key generation
31
+ safe_memo_cache_key(method_name, args, kwargs)
32
+ end
33
+ end
34
+
35
+ def _clear_custom_keys
36
+ @__safe_memo_custom_keys__ = {}
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SafeMemoize
4
+ module HooksMethods
5
+ private
6
+
7
+ def memo_hook_store
8
+ @__safe_memo_hooks__ ||= {on_expire: [], on_evict: [], on_hit: []}
9
+ end
10
+
11
+ def register_memo_hook(hook_type, &block)
12
+ raise ArgumentError, "block required" unless block
13
+
14
+ valid_hooks = [:on_expire, :on_evict, :on_hit]
15
+ raise ArgumentError, "invalid hook type: #{hook_type}" unless valid_hooks.include?(hook_type)
16
+
17
+ memo_hook_store[hook_type] << block
18
+ end
19
+
20
+ def call_memo_hooks(hook_type, cache_key, record)
21
+ hooks = memo_hook_store[hook_type] || []
22
+ hooks.each { |hook| hook.call(cache_key, record) }
23
+ end
24
+
25
+ def _clear_memo_hooks(hook_type = nil)
26
+ if hook_type
27
+ memo_hook_store[hook_type] = []
28
+ else
29
+ @__safe_memo_hooks__ = {on_expire: [], on_evict: [], on_hit: []}
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SafeMemoize
4
+ module InspectionMethods
5
+ private
6
+
7
+ def safe_memo_scoped_method(method_name)
8
+ raise ArgumentError, "expected 0 or 1 arguments" if method_name.length > 1
9
+
10
+ method_name.first&.to_sym
11
+ end
12
+
13
+ def memo_matcher_for(method_name, args, kwargs)
14
+ if args.empty? && kwargs.empty?
15
+ ->(key) { key[0] == method_name }
16
+ else
17
+ cache_key = safe_memo_cache_key(method_name, args, kwargs)
18
+ ->(key) { key == cache_key }
19
+ end
20
+ end
21
+
22
+ def memo_entries_for(method_name)
23
+ cache = memo_cache_or_nil
24
+ return [] unless cache
25
+
26
+ memo_prune_expired_entries!(cache)
27
+ entries = cache.to_a
28
+ return entries unless method_name
29
+
30
+ entries.select { |(cache_key, _)| cache_key[0] == method_name }
31
+ end
32
+
33
+ def safe_memo_count_for(method_name)
34
+ memo_entries_for(method_name).length
35
+ end
36
+
37
+ def safe_memo_keys_for(method_name)
38
+ entries = memo_entries_for(method_name)
39
+ include_method = method_name.nil?
40
+
41
+ entries.map do |(cache_key, value)|
42
+ memo_projection(cache_key, value, include_method: include_method, include_value: false)
43
+ end
44
+ end
45
+
46
+ def safe_memo_values_for(method_name)
47
+ entries = memo_entries_for(method_name)
48
+ include_method = method_name.nil?
49
+
50
+ entries.map do |(cache_key, value)|
51
+ memo_projection(cache_key, value, include_method: include_method, include_value: true)
52
+ end
53
+ end
54
+
55
+ def memo_projection(cache_key, value, include_method:, include_value:)
56
+ method_name, args, kwargs = cache_key
57
+
58
+ payload = {args: args, kwargs: kwargs}
59
+ payload[:method] = method_name if include_method
60
+ payload[:value] = memo_record_value(value) if include_value
61
+ payload
62
+ end
63
+
64
+ def safe_memo_cache_key(method_name, args, kwargs)
65
+ [method_name.to_sym, args, kwargs]
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SafeMemoize
4
+ module InstanceMethods
5
+ include PublicMethods
6
+ include CacheStoreMethods
7
+ include CacheRecordMethods
8
+ include InspectionMethods
9
+ include HooksMethods
10
+ include CacheMetricsMethods
11
+ include PublicMetricsMethods
12
+ include CustomKeyMethods
13
+ include PublicCustomKeyMethods
14
+ include LruMethods
15
+ end
16
+ end