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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/README.md +34 -12
- data/examples.md +51 -17
- data/lib/typed_cache/backends/active_support.rb +54 -9
- data/lib/typed_cache/backends/memory.rb +7 -8
- data/lib/typed_cache/cache_key.rb +9 -1
- data/lib/typed_cache/cache_ref.rb +16 -16
- data/lib/typed_cache/decorator.rb +9 -1
- data/lib/typed_cache/decorators/instrumented.rb +21 -8
- data/lib/typed_cache/either.rb +22 -0
- data/lib/typed_cache/instrumenter.rb +6 -6
- data/lib/typed_cache/instrumenters/mixins/namespaced_singleton.rb +3 -0
- data/lib/typed_cache/instrumenters.rb +2 -1
- data/lib/typed_cache/maybe.rb +18 -0
- data/lib/typed_cache/namespace.rb +33 -6
- data/lib/typed_cache/railtie.rb +15 -0
- data/lib/typed_cache/snapshot.rb +18 -10
- data/lib/typed_cache/store.rb +51 -19
- data/lib/typed_cache/version.rb +1 -1
- data/lib/typed_cache.rb +4 -0
- data/rbi/typed_cache/backend.rbi +9 -0
- data/rbi/typed_cache/backends/active_support.rbi +13 -0
- data/rbi/typed_cache/backends/memory.rbi +13 -0
- data/rbi/typed_cache/backends.rbi +19 -0
- data/rbi/typed_cache/cache_builder.rbi +23 -0
- data/rbi/typed_cache/cache_key.rbi +16 -0
- data/rbi/typed_cache/cache_ref.rbi +56 -0
- data/rbi/typed_cache/decorator.rbi +67 -0
- data/rbi/typed_cache/decorators/instrumented.rbi +13 -0
- data/rbi/typed_cache/decorators.rbi +19 -0
- data/rbi/typed_cache/either.rbi +122 -0
- data/rbi/typed_cache/errors.rbi +20 -0
- data/rbi/typed_cache/instrumenter.rbi +45 -0
- data/rbi/typed_cache/instrumenters/mixins/namedspaced_singleton.rbi +33 -0
- data/rbi/typed_cache/instrumenters.rbi +19 -0
- data/rbi/typed_cache/maybe.rbi +108 -0
- data/rbi/typed_cache/namespace.rbi +30 -0
- data/rbi/typed_cache/snapshot.rbi +54 -0
- data/rbi/typed_cache/store.rbi +71 -0
- data/rbi/typed_cache/version.rbi +5 -0
- data/rbi/typed_cache.rbi +49 -0
- data/sig/generated/typed_cache/backends/active_support.rbs +14 -2
- data/sig/generated/typed_cache/backends/memory.rbs +2 -2
- data/sig/generated/typed_cache/cache_key.rbs +5 -0
- data/sig/generated/typed_cache/cache_ref.rbs +4 -4
- data/sig/generated/typed_cache/decorator.rbs +4 -0
- data/sig/generated/typed_cache/decorators/instrumented.rbs +4 -4
- data/sig/generated/typed_cache/either.rbs +24 -0
- data/sig/generated/typed_cache/instrumenter.rbs +5 -5
- data/sig/generated/typed_cache/instrumenters/mixins/namespaced_singleton.rbs +3 -0
- data/sig/generated/typed_cache/instrumenters.rbs +2 -0
- data/sig/generated/typed_cache/maybe.rbs +20 -0
- data/sig/generated/typed_cache/namespace.rbs +24 -3
- data/sig/generated/typed_cache/railtie.rbs +6 -0
- data/sig/generated/typed_cache/snapshot.rbs +12 -6
- data/sig/generated/typed_cache/store.rbs +23 -8
- data/sig/generated/typed_cache.rbs +2 -0
- data/typed_cache.gemspec +1 -1
- data.tar.gz.sig +0 -0
- metadata +26 -3
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 68662a3e64b70950b86c466f7435aa4dc0ba5c31a87637244ece41685aa2cc00
|
4
|
+
data.tar.gz: 07fae7547c15ca5e7bfd900f9f74bb2f40443102bca143ce0f8cee8dc79c4069
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-

|
3
|
+
[](https://rubygems.org/gems/typed_cache)
|
4
4
|

|
5
5
|

|
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 #
|
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
|
102
|
-
puts "[cache]
|
103
|
-
@store.
|
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`
|
128
|
+
## The `CacheRef` and `Store` APIs
|
129
129
|
|
130
|
-
While you can call `
|
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.
|
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.
|
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("
|
211
|
+
instrumenter.subscribe("write") do |event|
|
190
212
|
payload = event.payload
|
191
|
-
puts "Cache
|
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.
|
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.
|
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
|
-
##
|
73
|
+
## Advanced Namespacing
|
74
74
|
|
75
|
-
|
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
|
-
###
|
98
|
+
### Read a value
|
82
99
|
|
83
|
-
The `
|
100
|
+
The `read` method returns an `Either[Error, Snapshot]`.
|
84
101
|
|
85
102
|
```ruby
|
86
|
-
result = ref.
|
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.
|
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.
|
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
|
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
|
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("
|
242
|
+
logging_cache.instrumenter.subscribe("write")
|
209
243
|
|
210
244
|
# Operations will now be logged
|
211
|
-
logging_cache.ref("test").
|
212
|
-
# => [CACHE] Starting:
|
213
|
-
# => [CACHE] Finished:
|
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.
|
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.
|
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
|
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.
|
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
|
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.
|
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
|
-
|
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
|
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.
|
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
|
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.
|
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.
|
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
|
-
|
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
|
26
|
-
store.
|
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
|
32
|
-
store.
|
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.
|
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.
|
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
|
-
|
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
|
-
|
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
|
-
|
78
|
+
read.bind do |snapshot|
|
79
79
|
new_value = yield(snapshot.value)
|
80
|
-
|
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
|
-
|
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
|
-
|
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] (
|
142
|
+
#: [R] (^(Error) -> R, ^(Snapshot[V]) -> R) -> R
|
143
143
|
def fold(left_fn, right_fn)
|
144
|
-
|
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
|
-
|
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
|
-
|
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(:
|
73
|
-
instrument(:
|
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
|
data/lib/typed_cache/either.rb
CHANGED
@@ -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))
|