typed_cache 0.2.0 → 0.3.1

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.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/README.md +34 -12
  4. data/examples.md +51 -17
  5. data/lib/typed_cache/backends/active_support.rb +54 -9
  6. data/lib/typed_cache/backends/memory.rb +7 -8
  7. data/lib/typed_cache/cache_key.rb +9 -1
  8. data/lib/typed_cache/cache_ref.rb +16 -16
  9. data/lib/typed_cache/decorator.rb +9 -1
  10. data/lib/typed_cache/decorators/instrumented.rb +21 -8
  11. data/lib/typed_cache/either.rb +22 -0
  12. data/lib/typed_cache/instrumenter.rb +6 -6
  13. data/lib/typed_cache/instrumenters/mixins/namespaced_singleton.rb +3 -0
  14. data/lib/typed_cache/instrumenters.rb +2 -1
  15. data/lib/typed_cache/maybe.rb +18 -0
  16. data/lib/typed_cache/namespace.rb +33 -6
  17. data/lib/typed_cache/railtie.rb +15 -0
  18. data/lib/typed_cache/snapshot.rb +18 -10
  19. data/lib/typed_cache/store.rb +51 -19
  20. data/lib/typed_cache/version.rb +1 -1
  21. data/lib/typed_cache.rb +4 -0
  22. data/rbi/typed_cache/backend.rbi +9 -0
  23. data/rbi/typed_cache/backends/active_support.rbi +13 -0
  24. data/rbi/typed_cache/backends/memory.rbi +13 -0
  25. data/rbi/typed_cache/backends.rbi +19 -0
  26. data/rbi/typed_cache/cache_builder.rbi +23 -0
  27. data/rbi/typed_cache/cache_key.rbi +16 -0
  28. data/rbi/typed_cache/cache_ref.rbi +56 -0
  29. data/rbi/typed_cache/decorator.rbi +67 -0
  30. data/rbi/typed_cache/decorators/instrumented.rbi +13 -0
  31. data/rbi/typed_cache/decorators.rbi +19 -0
  32. data/rbi/typed_cache/either.rbi +122 -0
  33. data/rbi/typed_cache/errors.rbi +20 -0
  34. data/rbi/typed_cache/instrumenter.rbi +45 -0
  35. data/rbi/typed_cache/instrumenters/mixins/namedspaced_singleton.rbi +33 -0
  36. data/rbi/typed_cache/instrumenters.rbi +19 -0
  37. data/rbi/typed_cache/maybe.rbi +108 -0
  38. data/rbi/typed_cache/namespace.rbi +30 -0
  39. data/rbi/typed_cache/snapshot.rbi +54 -0
  40. data/rbi/typed_cache/store.rbi +71 -0
  41. data/rbi/typed_cache/version.rbi +5 -0
  42. data/rbi/typed_cache.rbi +49 -0
  43. data/sig/generated/typed_cache/backends/active_support.rbs +14 -2
  44. data/sig/generated/typed_cache/backends/memory.rbs +2 -2
  45. data/sig/generated/typed_cache/cache_key.rbs +5 -0
  46. data/sig/generated/typed_cache/cache_ref.rbs +4 -4
  47. data/sig/generated/typed_cache/decorator.rbs +4 -0
  48. data/sig/generated/typed_cache/decorators/instrumented.rbs +4 -4
  49. data/sig/generated/typed_cache/either.rbs +24 -0
  50. data/sig/generated/typed_cache/instrumenter.rbs +5 -5
  51. data/sig/generated/typed_cache/instrumenters/mixins/namespaced_singleton.rbs +3 -0
  52. data/sig/generated/typed_cache/instrumenters.rbs +2 -0
  53. data/sig/generated/typed_cache/maybe.rbs +20 -0
  54. data/sig/generated/typed_cache/namespace.rbs +24 -3
  55. data/sig/generated/typed_cache/railtie.rbs +6 -0
  56. data/sig/generated/typed_cache/snapshot.rbs +12 -6
  57. data/sig/generated/typed_cache/store.rbs +23 -8
  58. data/sig/generated/typed_cache.rbs +2 -0
  59. data/typed_cache.gemspec +1 -1
  60. data.tar.gz.sig +0 -0
  61. metadata +26 -3
  62. metadata.gz.sig +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2cd862f92c635a4ee2fa15fa26186e56226d9dca6a132ad7de2f96fc9935ee31
4
- data.tar.gz: a862c34b4e86e838e4a30bfda8149357581782d4e2c64dd6a37882724930955d
3
+ metadata.gz: 68662a3e64b70950b86c466f7435aa4dc0ba5c31a87637244ece41685aa2cc00
4
+ data.tar.gz: 07fae7547c15ca5e7bfd900f9f74bb2f40443102bca143ce0f8cee8dc79c4069
5
5
  SHA512:
6
- metadata.gz: 2df948e5a9e9808c13547e940547484ad9cff0ce75085f3ee512a8ef866459dd791c7d1c85147b65dbb6ccbacea973cc412b44d3be12f7cb564c94afee452f2a
7
- data.tar.gz: 57e1c99cb2c11a82e1b9add65357a8abacdc0260ea1ca66c6cd3616ac13db73439b24c25643eed5425a1a23fb62984bf2feb4ce854c1a54f1bcbd790248e0f2d
6
+ metadata.gz: f81441fdceb975a4251d9f9d816cf71a40847e8ad1f9a3adea0d92075617ef0c9c85ed980b7707b9327221a75dc385459b954bd492e82ff08416fdb835fd6ba6
7
+ data.tar.gz: ef41b5392bb1f6adb0b2ccbf7f73ea6363c562b3e0621990f94c52445c048c598b9a0c875ad1d4ffaa03907576f68673614098281620d220ba96d6812b70531a
checksums.yaml.gz.sig CHANGED
Binary file
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # TypedCache
2
2
 
3
- ![Gem Version](https://img.shields.io/gem/v/typed_cache?style=flat-square&logo=rubygems)
3
+ [![Gem Version](https://img.shields.io/gem/v/typed_cache?style=flat-square&logo=rubygems)](https://rubygems.org/gems/typed_cache)
4
4
  ![GitHub Release Date](https://img.shields.io/github/release-date/glossawy/typed_cache?style=flat-square&label=released&logo=semanticrelease)
5
5
  ![GitHub last commit](https://img.shields.io/github/last-commit/glossawy/typed_cache?style=flat-square&logo=git)
6
6
 
@@ -9,7 +9,7 @@
9
9
  TypedCache is a lightweight, type-safe façade around your favourite Ruby cache
10
10
  stores. It adds three things on top of the raw back-end implementation:
11
11
 
12
- 1. **Namespacing** – hierarchical `Namespace` helpers prevent key collisions.
12
+ 1. **Namespacing** – hierarchical `Namespace` helpers prevent key collisions. You can create nested namespaces easily, like `Namespace.at("users", "profiles", "avatars")`.
13
13
  2. **Stronger types** – RBS signatures as well as monadic types like `Either`, `Maybe`, and `Snapshot` wrap cache results so you always know whether you have a value, an error, or a cache-miss.
14
14
  3. **Composable decorators** – behaviours like instrumentation can be layered
15
15
  on without touching the underlying store.
@@ -88,7 +88,7 @@ TypedCache::Decorators.available # => [:instrumented]
88
88
  ```ruby
89
89
  class RedisBackend
90
90
  include TypedCache::Backend
91
- # … implement #get, #set, etc.
91
+ # … implement #read, #write, etc.
92
92
  end
93
93
 
94
94
  TypedCache::Backends.register(:redis, RedisBackend)
@@ -98,9 +98,9 @@ TypedCache::Backends.register(:redis, RedisBackend)
98
98
  class LogDecorator
99
99
  include TypedCache::Decorator
100
100
  def initialize(store) = @store = store
101
- def set(key, value)
102
- puts "[cache] SET #{key}"
103
- @store.set(key, value)
101
+ def write(key, value)
102
+ puts "[cache] WRITE #{key}"
103
+ @store.write(key, value)
104
104
  end
105
105
  # delegate the rest …
106
106
  end
@@ -125,9 +125,9 @@ result.fold(
125
125
  )
126
126
  ```
127
127
 
128
- ## The `CacheRef` API
128
+ ## The `CacheRef` and `Store` APIs
129
129
 
130
- While you can call `get`, `set`, and `fetch` directly on the `store`, the more powerful way to work with TypedCache is via the `CacheRef` object. It provides a rich, monadic API for a single cache key.
130
+ While you can call `read`, `write`, and `fetch` directly on the `store`, the more powerful way to work with TypedCache is via the `CacheRef` object. It provides a rich, monadic API for a single cache key. The `Store` also provides `fetch_all` for batch operations.
131
131
 
132
132
  You get a `CacheRef` by calling `store.ref(key)`:
133
133
 
@@ -154,6 +154,28 @@ name_either = user_ref.map { |user| user[:name] }
154
154
  puts "User name is: #{name_either.value.value}" # unwrap Either, then Snapshot
155
155
  ```
156
156
 
157
+ ### Batch Operations with `fetch_all`
158
+
159
+ For retrieving multiple keys at once, the `Store` provides a `fetch_all` method. This is more efficient than fetching keys one by one, especially with remote back-ends like Redis.
160
+
161
+ It takes a list of keys and a block to compute the values for any missing keys.
162
+
163
+ ```ruby
164
+ user_refs = store.fetch_all("users:123", "users:456") do |missing_key|
165
+ # This block is called for each cache miss
166
+ user_id = missing_key.split(":").last
167
+ puts "Cache miss for #{missing_key}! Computing..."
168
+ { id: user_id, name: "Fetched User #{user_id}" }
169
+ end
170
+
171
+ user_refs.each do |key, snapshot_either|
172
+ snapshot_either.fold(
173
+ ->(err) { warn "Error for #{key}: #{err.message}" },
174
+ ->(snapshot) { puts "Got value for #{key}: #{snapshot.value}" }
175
+ )
176
+ end
177
+ ```
178
+
157
179
  The `CacheRef` API encourages a functional style and makes composing cache operations safe and predictable.
158
180
 
159
181
  ## Instrumentation
@@ -174,7 +196,7 @@ store = TypedCache.builder
174
196
  .build.value
175
197
  ```
176
198
 
177
- Events are published to a topic like `typed_cache.<operation>` (e.g., `typed_cache.get`). The topic namespace can be configured.
199
+ Events are published to a topic like `typed_cache.<operation>` (e.g., `typed_cache.write`). The topic namespace can be configured.
178
200
 
179
201
  Payload keys include: `:namespace, :key, :operation, :duration`, and `cache_hit`.
180
202
 
@@ -182,13 +204,13 @@ You can subscribe to these events like so:
182
204
 
183
205
  ```ruby
184
206
  # Example for ActiveSupport
185
- ActiveSupport::Notifications.subscribe("typed_cache.get") do |name, start, finish, id, payload|
207
+ ActiveSupport::Notifications.subscribe("typed_cache.write") do |name, start, finish, id, payload|
186
208
 
187
209
  # Or you can subscribe via the store object itself
188
210
  instrumenter = store.instrumenter
189
- instrumenter.subscribe("get") do |event|
211
+ instrumenter.subscribe("write") do |event|
190
212
  payload = event.payload
191
- puts "Cache GET for key #{payload[:key]} took #{payload[:duration]}ms. Hit? #{payload[:cache_hit]}"
213
+ puts "Cache WRITE for key #{payload[:key]} took #{payload[:duration]}ms. Hit? #{payload[:cache_hit]}"
192
214
  end
193
215
  ```
194
216
 
data/examples.md CHANGED
@@ -19,7 +19,7 @@ users_store = builder.build(TypedCache::Namespace.at("users")).value
19
19
  user_ref = users_store.ref("123")
20
20
 
21
21
  # Set a value
22
- user_ref.set({ id: 123, name: "Jane" })
22
+ user_ref.write({ id: 123, name: "Jane" })
23
23
  ```
24
24
 
25
25
  ## Rails Integration
@@ -51,7 +51,7 @@ result = TypedCache.builder
51
51
 
52
52
  case result
53
53
  in TypedCache::Either::Right(store)
54
- store.set(store.namespace.key("greeting"), "Hello")
54
+ store.write(store.namespace.key("greeting"), "Hello")
55
55
  in TypedCache::Either::Left(error)
56
56
  warn "Failed to set up cache: #{error.message}"
57
57
  end
@@ -70,26 +70,60 @@ posts_store = base_builder.build(TypedCache::Namespace.at("posts")).value
70
70
  comments_store = base_builder.build(TypedCache::Namespace.at("comments")).value
71
71
  ```
72
72
 
73
- ## CacheRef API
73
+ ## Advanced Namespacing
74
74
 
75
- The `CacheRef` is the most powerful way to interact with a cache key.
75
+ You can create nested namespaces using variadic arguments to `Namespace.at` or by chaining `join`.
76
+
77
+ ```ruby
78
+ # These are equivalent
79
+ ns1 = TypedCache::Namespace.at("app", "v1", "users")
80
+ ns2 = TypedCache::Namespace.at("app").join("v1").join("users")
81
+
82
+ puts ns1.to_s # => "app:v1:users"
83
+ puts ns2.to_s # => "app:v1:users"
84
+
85
+ # You can then build a store with this complex namespace
86
+ user_store = base_builder.build(ns1).value
87
+ user_store.ref("123").write({ name: "Deeply Nested" })
88
+ ```
89
+
90
+ ## CacheRef and Store APIs
91
+
92
+ The `CacheRef` is the most powerful way to interact with a single cache key, while the `Store` provides batch operations.
76
93
 
77
94
  ```ruby
78
95
  ref = store.ref("some-key") # => CacheRef
79
96
  ```
80
97
 
81
- ### Get a value
98
+ ### Read a value
82
99
 
83
- The `get` method returns an `Either[Error, Snapshot]`.
100
+ The `read` method returns an `Either[Error, Snapshot]`.
84
101
 
85
102
  ```ruby
86
- result = ref.get
103
+ result = ref.read
87
104
  result.fold(
88
105
  ->(error) { puts "Cache miss or error: #{error.message}" },
89
106
  ->(snapshot) { puts "Found: #{snapshot.value} (from cache: #{snapshot.from_cache?})" }
90
107
  )
91
108
  ```
92
109
 
110
+ ### Fetch all (get or compute)
111
+
112
+ The `fetch_all` method on the `store` is used for bulk-retrieving items. It gets all existing values from the cache and for the ones that are missing, it runs the block, stores the result, and returns it.
113
+
114
+ ```ruby
115
+ results = store.fetch_all("user:1", "user:2") do |missing_key|
116
+ # logic to compute the value for a missing key
117
+ "computed-#{missing_key}"
118
+ end
119
+
120
+ results.each do |key, snapshot_either|
121
+ snapshot_either.map do |snapshot|
122
+ puts "#{key} -> #{snapshot.value} (from_cache?=#{snapshot.from_cache?})"
123
+ end
124
+ end
125
+ ```
126
+
93
127
  ### Fetch (get or compute)
94
128
 
95
129
  The `fetch` method is the most common operation. It gets a value from the cache, but if it's missing, it runs the block, stores the result, and returns it.
@@ -106,7 +140,7 @@ You can transform the value inside the cache reference without breaking the mona
106
140
  # user_ref holds { id: 1, name: "John" }
107
141
  name_ref = user_ref.map { |user| user[:name] }
108
142
 
109
- name_snapshot = name_ref.get.value # => Snapshot(value: "John", ...)
143
+ name_snapshot = name_ref.read.value # => Snapshot(value: "John", ...)
110
144
  ```
111
145
 
112
146
  ### Chaining operations with `bind`
@@ -116,7 +150,7 @@ For more complex logic, you can use `bind` (or `flat_map`) to chain operations t
116
150
  ```ruby
117
151
  user_ref.bind do |user|
118
152
  if user.active?
119
- posts_ref.set(user.posts)
153
+ posts_ref.write(user.posts)
120
154
  else
121
155
  TypedCache::Either.left(StandardError.new("User is not active"))
122
156
  end
@@ -144,12 +178,12 @@ class SimpleStore
144
178
  @data = {}
145
179
  end
146
180
 
147
- def get(key)
181
+ def read(key)
148
182
  value = @data[key]
149
183
  value ? TypedCache::Either.right(value) : TypedCache::Either.left(TypedCache::CacheMissError.new(key))
150
184
  end
151
185
 
152
- def set(key, value)
186
+ def write(key, value)
153
187
  @data[key] = value
154
188
  TypedCache::Either.right(value)
155
189
  end
@@ -205,12 +239,12 @@ logging_cache = TypedCache.builder
205
239
  .build.value
206
240
 
207
241
  # Subscribe to an event
208
- logging_cache.instrumenter.subscribe("get")
242
+ logging_cache.instrumenter.subscribe("write")
209
243
 
210
244
  # Operations will now be logged
211
- logging_cache.ref("test").set("hello")
212
- # => [CACHE] Starting: set for key test
213
- # => [CACHE] Finished: set for key test
245
+ logging_cache.ref("test").write("hello")
246
+ # => [CACHE] Starting: write for key test
247
+ # => [CACHE] Finished: write for key test
214
248
  ```
215
249
 
216
250
  ## Thread Safety
@@ -223,7 +257,7 @@ shared_store = TypedCache.builder
223
257
  .build.value
224
258
 
225
259
  threads = 10.times.map do |i|
226
- Thread.new { shared_store.set(shared_store.namespace.key(i.to_s), "data_") }
260
+ Thread.new { shared_store.write(shared_store.namespace.key(i.to_s), "data_") }
227
261
  end
228
262
  threads.each(&:join)
229
263
  ```
@@ -239,7 +273,7 @@ spec_store = TypedCache.builder
239
273
 
240
274
  RSpec.describe "cache" do
241
275
  it "stores data" do
242
- result = spec_store.set(spec_store.namespace.key("id"), 1)
276
+ result = spec_store.write(spec_store.namespace.key("id"), 1)
243
277
  expect(result).to be_right
244
278
  end
245
279
  end
@@ -19,25 +19,25 @@ module TypedCache
19
19
  end
20
20
 
21
21
  # @rbs override
22
- #: (cache_key) -> either[Error, Snapshot[V]]
23
- def get(key)
22
+ #: (cache_key, **top) -> either[Error, Snapshot[V]]
23
+ def read(key, **kwargs)
24
24
  cache_key_str = namespaced_key(key).to_s
25
- raw_value = cache_store.read(cache_key_str, default_options)
25
+ raw_value = cache_store.read(cache_key_str, default_options.merge(**kwargs))
26
26
  return Either.left(CacheMissError.new(key)) if raw_value.nil?
27
27
 
28
- Either.right(Snapshot.new(raw_value, source: :cache))
28
+ Either.right(Snapshot.cached(key, raw_value))
29
29
  rescue => e
30
30
  Either.left(StoreError.new(:get, key, "Failed to read from cache: #{e.message}", e))
31
31
  end
32
32
 
33
33
  # @rbs override
34
- #: (cache_key, V) -> either[Error, Snapshot[V]]
35
- def set(key, value)
34
+ #: (cache_key, V, **top) -> either[Error, Snapshot[V]]
35
+ def write(key, value, **kwargs)
36
36
  cache_key_str = namespaced_key(key).to_s
37
- success = cache_store.write(cache_key_str, value, default_options)
37
+ success = cache_store.write(cache_key_str, value, default_options.merge(**kwargs))
38
38
 
39
39
  if success
40
- Either.right(Snapshot.new(value, source: :cache))
40
+ Either.right(Snapshot.cached(key, value))
41
41
  else
42
42
  Either.left(StoreError.new(:set, key, 'Failed to write to cache', nil))
43
43
  end
@@ -45,10 +45,19 @@ module TypedCache
45
45
  Either.left(StoreError.new(:set, key, "Failed to write to cache: #{e.message}", e))
46
46
  end
47
47
 
48
+ # @rbs override
49
+ #: (Hash[cache_key, V], **top) -> either[Error, Array[Snapshot[V]]]
50
+ def write_all(values, **kwargs)
51
+ results = cache_store.write_multi(values.map { |key, value| [namespaced_key(key).to_s, value] }.to_h, default_options.merge(**kwargs))
52
+ Either.right(results.map { |key, value| Snapshot.cached(key, value) })
53
+ rescue => e
54
+ Either.left(StoreError.new(:set_all, values, "Failed to write to cache: #{e.message}", e))
55
+ end
56
+
48
57
  # @rbs override
49
58
  #: (cache_key) -> either[Error, Snapshot[V]]
50
59
  def delete(key)
51
- get(key).fold(
60
+ read(key).fold(
52
61
  ->(error) { Either.left(error) },
53
62
  ->(snapshot) {
54
63
  cache_key_str = namespaced_key(key).to_s
@@ -60,6 +69,42 @@ module TypedCache
60
69
  Either.left(StoreError.new(:delete, key, "Failed to delete from cache: #{e.message}", e))
61
70
  end
62
71
 
72
+ # @rbs override
73
+ #: (Array[cache_key], **top) -> either[Error, Array[Snapshot[V]]]
74
+ def read_all(keys, **kwargs)
75
+ results = cache_store.read_multi(*keys.map { |key| namespaced_key(key).to_s }, default_options.merge(**kwargs))
76
+ Either.right(results.map { |key, value| [key, Snapshot.cached(key, value)] }.to_h)
77
+ end
78
+
79
+ # @rbs override
80
+ #: (Array[cache_key], **top) { (CacheKey) -> V? } -> either[Error, Array[Snapshot[V]]]
81
+ def fetch_all(keys, **kwargs, &block)
82
+ cache_keys = keys.map { |key| namespaced_key(key) }
83
+ key_map = cache_keys.index_by(&:to_s)
84
+
85
+ computed_keys = Set.new #: Set[String]
86
+ results = cache_store.fetch_multi(*key_map.keys, default_options.merge(**kwargs)) do |key|
87
+ computed_keys << key
88
+ yield(key_map[key])
89
+ end
90
+
91
+ snapshots = [] #: Array[Snapshot[V]]
92
+
93
+ results.each do |key, value|
94
+ maybe_value = Maybe.wrap(value)
95
+ snapshots <<
96
+ if computed_keys.include?(key)
97
+ Snapshot.computed(key, maybe_value)
98
+ else
99
+ Snapshot.cached(key, maybe_value)
100
+ end
101
+ end
102
+
103
+ Either.right(snapshots)
104
+ rescue StandardError => e
105
+ Either.left(StoreError.new(:fetch_all, keys, "Failed to fetch from cache: #{e.message}", e))
106
+ end
107
+
63
108
  # @rbs override
64
109
  #: (cache_key) -> bool
65
110
  def key?(key)
@@ -68,8 +68,8 @@ module TypedCache
68
68
  end
69
69
 
70
70
  # @rbs override
71
- #: (cache_key) -> either[Error, Snapshot[V]]
72
- def get(key)
71
+ #: (cache_key, **top) -> either[Error, Snapshot[V]]
72
+ def read(key, **kwargs)
73
73
  key = namespaced_key(key)
74
74
  return Either.left(CacheMissError.new(key)) unless backing_store.key?(key)
75
75
 
@@ -80,17 +80,16 @@ module TypedCache
80
80
  return Either.left(CacheMissError.new(key))
81
81
  end
82
82
 
83
- Either.right(Snapshot.new(entry.value, source: :cache))
83
+ Either.right(Snapshot.cached(key, entry.value))
84
84
  end
85
85
 
86
86
  # @rbs override
87
- #: (cache_key, V) -> either[Error, Snapshot[V]]
88
- def set(key, value)
87
+ #: (cache_key, V, expires_in: Integer, expires_at: Time, **top) -> either[Error, Snapshot[V]]
88
+ def write(key, value, expires_in: @ttl, expires_at: Clock.now + expires_in, **kwargs)
89
89
  key = namespaced_key(key)
90
- expires_at = Clock.now + @ttl
91
90
  entry = Entry.new(value: value, expires_at: expires_at)
92
91
  backing_store[key] = entry
93
- Either.right(Snapshot.new(value, source: :cache))
92
+ Either.right(Snapshot.cached(key, value))
94
93
  rescue => e
95
94
  Either.left(StoreError.new(
96
95
  :set,
@@ -108,7 +107,7 @@ module TypedCache
108
107
  if entry.nil?
109
108
  Either.left(CacheMissError.new(key))
110
109
  else
111
- Either.right(Snapshot.new(entry.value, source: :cache))
110
+ Either.right(Snapshot.cached(key, entry.value))
112
111
  end
113
112
  end
114
113
 
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'forwardable'
3
4
  require_relative 'namespace'
4
5
 
5
6
  module TypedCache
@@ -22,7 +23,7 @@ module TypedCache
22
23
 
23
24
  # @rbs () -> String
24
25
  def to_s
25
- "#{@namespace}:#{@key}"
26
+ [@namespace.to_s, @key].join(delimiter)
26
27
  end
27
28
 
28
29
  alias cache_key to_s
@@ -43,5 +44,12 @@ module TypedCache
43
44
  end
44
45
 
45
46
  alias eql? ==
47
+
48
+ private
49
+
50
+ # @rbs (String) -> String
51
+ def delimiter
52
+ TypedCache.config.cache_delimiter
53
+ end
46
54
  end
47
55
  end
@@ -22,14 +22,14 @@ module TypedCache
22
22
 
23
23
  # Gets a value from the cache as a snapshot
24
24
  #: -> either[Error, Snapshot[V]]
25
- def get
26
- store.get(key)
25
+ def read
26
+ store.read(key)
27
27
  end
28
28
 
29
29
  # Sets a value in the cache and returns it as an updated snapshot
30
30
  #: (V) -> either[Error, Snapshot[V]]
31
- def set(value)
32
- store.set(key, value)
31
+ def write(value)
32
+ store.write(key, value)
33
33
  end
34
34
 
35
35
  # Deletes the value from the cache and returns the deleted value as a snapshot
@@ -48,25 +48,25 @@ module TypedCache
48
48
  # Checks if the cache contains a value for this key
49
49
  #: -> bool
50
50
  def present?
51
- store.get(key).right?
51
+ store.read(key).right?
52
52
  end
53
53
 
54
54
  # Checks if the cache is empty for this key
55
55
  #: -> bool
56
56
  def empty?
57
- store.get(key).left?
57
+ store.read(key).left?
58
58
  end
59
59
 
60
60
  # Maps over the cached value if it exists, preserving snapshot metadata
61
61
  #: [R] () { (V) -> R } -> either[Error, Snapshot[R]]
62
62
  def map(&block)
63
- get.map { |snapshot| snapshot.map(&block) }
63
+ read.map { |snapshot| snapshot.map(&block) }
64
64
  end
65
65
 
66
66
  # Binds over the cached value, allowing for monadic composition with snapshots
67
67
  #: [R] () { (V) -> either[Error, R] } -> either[Error, Snapshot[R]]
68
68
  def bind(&block)
69
- get.bind { |snapshot| snapshot.bind(&block) }
69
+ read.bind { |snapshot| snapshot.bind(&block) }
70
70
  end
71
71
 
72
72
  alias flat_map bind
@@ -75,9 +75,9 @@ module TypedCache
75
75
  # Returns the updated value as a snapshot with source=:updated
76
76
  #: () { (V) -> V } -> either[Error, Snapshot[V]]
77
77
  def update(&block)
78
- get.bind do |snapshot|
78
+ read.bind do |snapshot|
79
79
  new_value = yield(snapshot.value)
80
- set(new_value)
80
+ write(new_value)
81
81
  rescue => e
82
82
  Either.left(StoreError.new(
83
83
  :update,
@@ -91,7 +91,7 @@ module TypedCache
91
91
  # Returns the cached value or a default if the cache is empty/errored
92
92
  #: (V) -> V
93
93
  def value_or(default)
94
- get.fold(
94
+ read.fold(
95
95
  ->(_error) { default },
96
96
  ->(snapshot) { snapshot.value },
97
97
  )
@@ -101,7 +101,7 @@ module TypedCache
101
101
  # This provides a more functional approach than value_or
102
102
  #: -> maybe[V]
103
103
  def value_maybe
104
- get.fold(
104
+ read.fold(
105
105
  ->(_error) { Maybe.none },
106
106
  ->(snapshot) { Maybe.some(snapshot.value) },
107
107
  )
@@ -139,21 +139,21 @@ module TypedCache
139
139
  end
140
140
 
141
141
  # Pattern matching support for Either[Error, Snapshot[V]] results
142
- #: [R] () { (Error) -> R } () { (Snapshot[V]) -> R } -> R
142
+ #: [R] (^(Error) -> R, ^(Snapshot[V]) -> R) -> R
143
143
  def fold(left_fn, right_fn)
144
- get.fold(left_fn, right_fn)
144
+ read.fold(left_fn, right_fn)
145
145
  end
146
146
 
147
147
  # Convenience method to work with the snapshot directly
148
148
  #: [R] () { (Snapshot[V]) -> R } -> either[Error, R]
149
149
  def with_snapshot(&block)
150
- get.map(&block)
150
+ read.map(&block)
151
151
  end
152
152
 
153
153
  # Convenience method to work with just the value (losing snapshot context)
154
154
  #: [R] () { (V) -> R } -> either[Error, R]
155
155
  def with(&block)
156
- get.map { |snapshot| yield(snapshot.value) }
156
+ read.map { |snapshot| yield(snapshot.value) }
157
157
  end
158
158
  end
159
159
  end
@@ -23,7 +23,15 @@ module TypedCache
23
23
  # @rbs override
24
24
  #: (cache_key) -> either[Error, CacheRef[V]]
25
25
  def ref(key)
26
- CacheRef.new(self, key)
26
+ CacheRef.new(self, namespaced_key(key))
27
+ end
28
+
29
+ # @rbs override
30
+ #: (self) -> void
31
+ def initialize_copy(other)
32
+ super
33
+
34
+ @store = other.store.clone
27
35
  end
28
36
  end
29
37
  end
@@ -24,9 +24,15 @@ module TypedCache
24
24
 
25
25
  class_eval(<<~RUBY, __FILE__, __LINE__ + 1)
26
26
  def #{alias_prefix}_with_instrumentation(...)
27
+ return #{alias_prefix}_without_instrumentation(...) if @in_instrumentation
28
+
27
29
  key = #{alias_prefix}_instrumentation_key(...)
28
30
  instrumenter.instrument(:"#{operation}", key, store_type: store_type) do
31
+ @in_instrumentation = true
32
+
29
33
  #{alias_prefix}_without_instrumentation(...)
34
+ ensure
35
+ @in_instrumentation = false
30
36
  end
31
37
  end
32
38
  RUBY
@@ -40,6 +46,16 @@ module TypedCache
40
46
  def initialize(store, instrumenter:)
41
47
  @store = store
42
48
  @instrumenter = instrumenter
49
+
50
+ # Avoid instrumenting the cache calls themselves, fetch_all may call fetch for example
51
+ @in_instrumentation = false
52
+ end
53
+
54
+ # @rbs override
55
+ #: (self) -> self
56
+ def initialize_copy(other)
57
+ super
58
+ @instrumenter = other.instrumenter
43
59
  end
44
60
 
45
61
  # @rbs override
@@ -49,12 +65,6 @@ module TypedCache
49
65
  "instrumented(#{store.store_type})"
50
66
  end
51
67
 
52
- # @rbs override
53
- # @rbs (key) -> CacheRef[V]
54
- def ref(key)
55
- CacheRef.new(self, key)
56
- end
57
-
58
68
  # Additional methods that might exist on the wrapped store
59
69
  def respond_to_missing?(method_name, include_private = false)
60
70
  store.respond_to?(method_name, include_private) || super
@@ -69,10 +79,13 @@ module TypedCache
69
79
  end
70
80
 
71
81
  # Instrument core operations with proper key extraction
72
- instrument(:get) { |key, *_| key }
73
- instrument(:set) { |key, *_| key }
82
+ instrument(:read) { |key, *_| key }
83
+ instrument(:read_all) { |keys, *_| keys.map(&:to_s).join('_') }
84
+ instrument(:write) { |key, *_| key }
85
+ instrument(:write_all) { |values, *_| values.map { |key, _| key.to_s }.join('_') }
74
86
  instrument(:delete) { |key, *_| key }
75
87
  instrument(:fetch) { |key, *_| key }
88
+ instrument(:fetch_all) { |keys, *_| keys.map(&:to_s).join('_') }
76
89
  instrument(:key?) { |key, *_| key }
77
90
  instrument(:clear) { 'all' }
78
91
  end
@@ -27,6 +27,8 @@ module TypedCache
27
27
  # @rbs! interface _Either[out E, out R]
28
28
  # def left?: -> bool
29
29
  # def right?: -> bool
30
+ # def right_or_else: (^(E) -> void) -> R
31
+ # def right_or_raise!: -> R
30
32
  # def map: [T] () { (R) -> T } -> either[E, T]
31
33
  # def bind: [E2, R2] () { (R) -> either[E2, R2] } -> either[E | E2, R2]
32
34
  # def map_left: [F] () { (E) -> F } -> either[F, R]
@@ -39,6 +41,8 @@ module TypedCache
39
41
 
40
42
  attr_reader :error #: E
41
43
 
44
+ alias value error
45
+
42
46
  #: (E) -> void
43
47
  def initialize(error)
44
48
  @error = error
@@ -52,6 +56,14 @@ module TypedCache
52
56
  #: -> false
53
57
  def right? = false
54
58
 
59
+ # @rbs override
60
+ #: (^(E) -> void) -> bot
61
+ def right_or_else(&) = yield(error)
62
+
63
+ # @rbs override
64
+ #: -> bot
65
+ def right_or_raise! = raise(error)
66
+
55
67
  # @rbs override
56
68
  #: [T] () { (R) -> T } -> either[E, T]
57
69
  def map(&) = self
@@ -82,6 +94,8 @@ module TypedCache
82
94
 
83
95
  attr_reader :value #: R
84
96
 
97
+ alias result value
98
+
85
99
  #: (R) -> void
86
100
  def initialize(value)
87
101
  @value = value
@@ -95,6 +109,14 @@ module TypedCache
95
109
  #: -> true
96
110
  def right? = true
97
111
 
112
+ # @rbs override
113
+ #: (^(E) -> void) -> R
114
+ def right_or_else(&) = value
115
+
116
+ # @rbs override
117
+ #: -> R
118
+ def right_or_raise! = value
119
+
98
120
  # @rbs override
99
121
  #: [T] () { (R) -> T } -> either[E, T]
100
122
  def map(&) = Right.new(yield(value))