legion-cache 1.3.1 → 1.3.2

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: 657e7afe142d9420c0414c416fdf612ef75456262c91165d545112960bf44e71
4
- data.tar.gz: 137d4eb307b337bcda85f91db10fddc42319793a99cde3038fbf9799db960d78
3
+ metadata.gz: '0295f21258e4a7d824099be9c2c12a7c594d0db3e72dd8d5b5f823bb332e6869'
4
+ data.tar.gz: 5e581fc50c7bb13e52e9a38ae99a085af836ae21ec6d126d3a0d1fc6c79ad5aa
5
5
  SHA512:
6
- metadata.gz: e167c6f650ad38373f8794f9cfc209857fd5053b9751375cf77f7ae9537eab23c40cd33947cc656c64dd77e7832f1312987739b4c95c1d2873b5cdf377aeff0d
7
- data.tar.gz: 7e38075e7257b4b04fbdefb22de3da21d0055be631f8a6ca4f17d0c78f5669e153d0590aabd3742c05990f88c4e7c55d85409dfae90a5ec6741f36913af54f92
6
+ metadata.gz: dc08f391040cab62c39795bd1d477f7d8e47ba4b5ffe3e88856957b829876d37ae06e15a48ff5469652e3c3574d5da529bccb7f00e9df162caadcc4d50fd93e1
7
+ data.tar.gz: 67717d9fd58c0669e319f0b0de94dee0d8178f17d78ba3fac2afc142660e26668b6700ccbb370b88274f1295d7ab534627768f981f3e1c4f98f9aa5ac86ae3db
data/.gitignore CHANGED
@@ -16,3 +16,4 @@ legionio.key
16
16
  # logs and OS artifacts
17
17
  legion.log
18
18
  .DS_Store
19
+ .worktrees/
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.3.2] - 2026-03-20
4
+
5
+ ### Added
6
+ - `Legion::Cache::Cacheable` module for transparent method-level caching
7
+ - `cache_method` DSL: declare cached methods with TTL, scope, and key exclusions
8
+ - `build_cache_key`: deterministic MD5-based cache keys from module path + method + filtered args
9
+ - `bypass_local_method_cache:` kwarg for force-refresh on cached methods
10
+ - In-memory fallback store with TTL expiry when no cache backend is available
11
+ - `memory_clear!` class method for test isolation
12
+
3
13
  ## [1.3.1] - 2026-03-20
4
14
 
5
15
  ### Added
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Caching wrapper for the [LegionIO](https://github.com/LegionIO/LegionIO) framework. Provides a consistent interface for Memcached (via `dalli`) and Redis (via `redis` gem) with connection pooling. Driver selection is config-driven.
4
4
 
5
- **Version**: 1.3.1
5
+ **Version**: 1.3.2
6
6
 
7
7
  ## Installation
8
8
 
@@ -117,6 +117,33 @@ Both `server` (singular string) and `servers` (array) are accepted and merged. D
117
117
 
118
118
  Override via `Legion::Settings[:cache_local]`.
119
119
 
120
+ ## Method Caching
121
+
122
+ Runner modules can use `cache_method` to transparently cache method results with TTL:
123
+
124
+ ```ruby
125
+ module Runners::Presence
126
+ extend Legion::Cache::Cacheable
127
+
128
+ cache_method :get_presence, ttl: 300, exclude_from_key: [:token]
129
+
130
+ def get_presence(user_id: 'me', **)
131
+ conn = graph_connection(**)
132
+ response = conn.get("#{user_path(user_id)}/presence")
133
+ { availability: response.body['availability'], activity: response.body['activity'] }
134
+ end
135
+ end
136
+ ```
137
+
138
+ Every caller of `get_presence` gets cached results for 5 minutes. Use `bypass_local_method_cache: true` to force-refresh:
139
+
140
+ ```ruby
141
+ runner.get_presence(user_id: 'me') # cached
142
+ runner.get_presence(user_id: 'me', bypass_local_method_cache: true) # fresh
143
+ ```
144
+
145
+ Options: `ttl:` (seconds), `scope:` (`:local` or `:global`), `exclude_from_key:` (args to ignore in cache key). Falls back to in-memory store when no cache backend is available.
146
+
120
147
  ## Pool API
121
148
 
122
149
  ```ruby
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest'
4
+
5
+ module Legion
6
+ module Cache
7
+ module Cacheable
8
+ def self.extended(base)
9
+ base.instance_variable_set(:@cached_methods, {})
10
+ end
11
+
12
+ def cached_methods
13
+ @cached_methods ||= {}
14
+ end
15
+
16
+ def cache_method(method_name, ttl:, scope: :local, exclude_from_key: [])
17
+ exclude_from_key |= %i[token bypass_local_method_cache]
18
+ cached_methods[method_name] = { ttl: ttl, scope: scope, exclude_from_key: exclude_from_key }
19
+
20
+ mod_name = name || 'Anonymous'
21
+ config = cached_methods[method_name]
22
+
23
+ wrapper = Module.new do
24
+ define_method(method_name) do |bypass_local_method_cache: false, **kwargs|
25
+ key = Legion::Cache::Cacheable.build_cache_key(
26
+ mod_name, method_name, exclude: config[:exclude_from_key], **kwargs
27
+ )
28
+
29
+ unless bypass_local_method_cache
30
+ cached = Legion::Cache::Cacheable.cache_read(key, scope: config[:scope])
31
+ return cached unless cached.nil?
32
+ end
33
+
34
+ result = super(**kwargs)
35
+ Legion::Cache::Cacheable.cache_write(key, result, ttl: config[:ttl], scope: config[:scope])
36
+ result
37
+ end
38
+ end
39
+
40
+ prepend wrapper
41
+ end
42
+
43
+ def self.build_cache_key(mod_name, method_name, exclude:, **kwargs)
44
+ filtered = kwargs.except(*exclude)
45
+ args_hash = Digest::MD5.hexdigest(filtered.sort.to_s)
46
+ "#{mod_name}.#{method_name}.#{args_hash}"
47
+ end
48
+
49
+ def self.cache_read(key, scope:)
50
+ case scope
51
+ when :global
52
+ return Legion::Cache.get(key) if global_cache_available?
53
+
54
+ memory_read(key)
55
+ else
56
+ local_cache_read(key) || memory_read(key)
57
+ end
58
+ end
59
+
60
+ def self.cache_write(key, value, ttl:, scope:)
61
+ case scope
62
+ when :global
63
+ if global_cache_available?
64
+ Legion::Cache.set(key, value, ttl)
65
+ else
66
+ memory_write(key, value, ttl)
67
+ end
68
+ else
69
+ if local_cache_available?
70
+ local_cache_write(key, value, ttl)
71
+ else
72
+ memory_write(key, value, ttl)
73
+ end
74
+ end
75
+ end
76
+
77
+ def self.global_cache_available?
78
+ defined?(Legion::Cache) && Legion::Cache.respond_to?(:connected?) && Legion::Cache.connected?
79
+ end
80
+
81
+ def self.local_cache_available?
82
+ defined?(Legion::Cache::Local) && Legion::Cache::Local.respond_to?(:connected?) && Legion::Cache::Local.connected?
83
+ end
84
+
85
+ def self.local_cache_read(key)
86
+ return nil unless local_cache_available?
87
+
88
+ Legion::Cache::Local.get(key)
89
+ rescue StandardError
90
+ nil
91
+ end
92
+
93
+ def self.local_cache_write(key, value, ttl)
94
+ return unless local_cache_available?
95
+
96
+ Legion::Cache::Local.set(key, value, ttl)
97
+ rescue StandardError
98
+ nil
99
+ end
100
+
101
+ # In-memory fallback store (class-level, process-wide)
102
+ def self.memory_store
103
+ @memory_store ||= {}
104
+ end
105
+
106
+ def self.memory_read(key)
107
+ entry = memory_store[key]
108
+ return nil unless entry
109
+ return nil if Time.now.utc > entry[:expires_at]
110
+
111
+ entry[:value]
112
+ end
113
+
114
+ def self.memory_write(key, value, ttl)
115
+ memory_store[key] = { value: value, expires_at: Time.now.utc + ttl }
116
+ end
117
+
118
+ def self.memory_clear!
119
+ @memory_store = {}
120
+ end
121
+ end
122
+ end
123
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module Cache
5
- VERSION = '1.3.1'
5
+ VERSION = '1.3.2'
6
6
  end
7
7
  end
data/lib/legion/cache.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'legion/cache/version'
4
4
  require 'legion/cache/settings'
5
+ require 'legion/cache/cacheable'
5
6
 
6
7
  require 'legion/cache/memcached'
7
8
  require 'legion/cache/redis'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: legion-cache
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.1
4
+ version: 1.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -100,6 +100,7 @@ files:
100
100
  - README.md
101
101
  - legion-cache.gemspec
102
102
  - lib/legion/cache.rb
103
+ - lib/legion/cache/cacheable.rb
103
104
  - lib/legion/cache/local.rb
104
105
  - lib/legion/cache/memcached.rb
105
106
  - lib/legion/cache/pool.rb