typed_cache 0.1.1 → 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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/README.md +139 -20
- data/examples.md +140 -50
- data/lib/typed_cache/backends/active_support.rb +50 -5
- data/lib/typed_cache/backends/memory.rb +14 -11
- data/lib/typed_cache/backends.rb +6 -8
- data/lib/typed_cache/cache_builder.rb +72 -19
- data/lib/typed_cache/cache_key.rb +11 -1
- data/lib/typed_cache/cache_ref.rb +20 -16
- data/lib/typed_cache/clock.rb +31 -14
- data/lib/typed_cache/decorator.rb +25 -0
- data/lib/typed_cache/decorators/instrumented.rb +92 -0
- data/lib/typed_cache/decorators.rb +7 -3
- data/lib/typed_cache/either.rb +22 -0
- data/lib/typed_cache/errors.rb +9 -1
- data/lib/typed_cache/instrumenter.rb +43 -0
- data/lib/typed_cache/instrumenters/active_support.rb +28 -0
- data/lib/typed_cache/instrumenters/mixins/namespaced_singleton.rb +55 -0
- data/lib/typed_cache/instrumenters/mixins.rb +8 -0
- data/lib/typed_cache/instrumenters/monitor.rb +27 -0
- data/lib/typed_cache/instrumenters/null.rb +26 -0
- data/lib/typed_cache/instrumenters.rb +39 -0
- 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/registry.rb +15 -0
- data/lib/typed_cache/snapshot.rb +18 -10
- data/lib/typed_cache/store.rb +50 -15
- data/lib/typed_cache/version.rb +1 -1
- data/lib/typed_cache.rb +34 -14
- 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/backends.rbs +2 -0
- data/sig/generated/typed_cache/cache_builder.rbs +13 -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/clock.rbs +19 -9
- data/sig/generated/typed_cache/decorator.rbs +12 -0
- data/sig/generated/typed_cache/decorators/instrumented.rbs +35 -0
- data/sig/generated/typed_cache/decorators.rbs +2 -0
- data/sig/generated/typed_cache/either.rbs +24 -0
- data/sig/generated/typed_cache/errors.rbs +2 -0
- data/sig/generated/typed_cache/instrumenter.rbs +31 -0
- data/sig/generated/typed_cache/instrumenters/active_support.rbs +20 -0
- data/sig/generated/typed_cache/instrumenters/mixins/namespaced_singleton.rbs +36 -0
- data/sig/generated/typed_cache/instrumenters/mixins.rbs +8 -0
- data/sig/generated/typed_cache/instrumenters/monitor.rbs +19 -0
- data/sig/generated/typed_cache/instrumenters/null.rbs +21 -0
- data/sig/generated/typed_cache/instrumenters.rbs +26 -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/registry.rbs +8 -0
- data/sig/generated/typed_cache/snapshot.rbs +12 -6
- data/sig/generated/typed_cache/store/instrumented.rbs +2 -6
- data/sig/generated/typed_cache/store.rbs +26 -8
- data/sig/generated/typed_cache.rbs +8 -6
- data/typed_cache.gemspec +5 -4
- data.tar.gz.sig +0 -0
- metadata +48 -27
- metadata.gz.sig +0 -0
- data/lib/typed_cache/instrumentation.rb +0 -112
- data/lib/typed_cache/store/instrumented.rb +0 -83
- data/sig/generated/typed_cache/instrumentation.rbs +0 -30
- data/sig/handwritten/gems/zeitwerk/2.7/zeitwerk.rbs +0 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 52137d39af663110c085558e5d6885fbe22d02b4a185f7a1ef3f4aa2dbe6a619
|
4
|
+
data.tar.gz: 030bf378a6effe782ab28fdb876c64c73d8997e5a3a9ec543735abe55973e421
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 38641da2ef43033f286833d84492b02f09dcfca291e7eab53c93bf6c9f737de5f077984a4f742de06c62a0c90db19d00bbc03cb032485143e9a542576fb5f90c
|
7
|
+
data.tar.gz: 2fb8cd40d84bb43a3f9905ccc7c0d30d853cdf0b030cf192a6e17517545ac94f256a3371f85bcd068e4f08d12c7a32a937f8eb3a87622b8b3a16429e772bd8bb
|
checksums.yaml.gz.sig
CHANGED
Binary file
|
data/README.md
CHANGED
@@ -1,9 +1,15 @@
|
|
1
1
|
# TypedCache
|
2
2
|
|
3
|
+

|
4
|
+

|
5
|
+

|
6
|
+
|
7
|
+

|
8
|
+
|
3
9
|
TypedCache is a lightweight, type-safe façade around your favourite Ruby cache
|
4
10
|
stores. It adds three things on top of the raw back-end implementation:
|
5
11
|
|
6
|
-
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")`.
|
7
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.
|
8
14
|
3. **Composable decorators** – behaviours like instrumentation can be layered
|
9
15
|
on without touching the underlying store.
|
@@ -35,26 +41,33 @@ If there are issues with unsigned gems, use `MediumSecurity` instead.
|
|
35
41
|
```ruby
|
36
42
|
require "typed_cache"
|
37
43
|
|
38
|
-
# Build
|
44
|
+
# 1. Build a store
|
39
45
|
store = TypedCache.builder
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
46
|
+
.with_backend(:memory, shared: true)
|
47
|
+
.with_instrumentation(:rails) # e.g. using ActiveSupport
|
48
|
+
.build
|
49
|
+
.value # unwrap Either for brevity
|
50
|
+
|
51
|
+
# 2. Get a reference to a key
|
52
|
+
user_ref = store.ref("users:123") # => CacheRef
|
53
|
+
|
54
|
+
# 3. Fetch and compute if absent
|
55
|
+
user_snapshot = user_ref.fetch do
|
56
|
+
puts "Cache miss! Computing..."
|
57
|
+
{ id: 123, name: "Jane" }
|
58
|
+
end.value # => Snapshot
|
59
|
+
|
60
|
+
puts "Found: #{user_snapshot.value} (from_cache?=#{user_snapshot.from_cache?})"
|
48
61
|
```
|
49
62
|
|
50
63
|
## Builder API
|
51
64
|
|
52
|
-
| Step
|
53
|
-
|
|
54
|
-
| `with_backend(:name, **opts)`
|
55
|
-
| `with_decorator(:key)`
|
56
|
-
| `with_instrumentation`
|
57
|
-
| `build`
|
65
|
+
| Step | Purpose |
|
66
|
+
| ------------------------------- | -------------------------------------------------------------- |
|
67
|
+
| `with_backend(:name, **opts)` | Mandatory. Configure the concrete **Backend** and its options. |
|
68
|
+
| `with_decorator(:key)` | Optional. Add a decorator by registry key. |
|
69
|
+
| `with_instrumentation(:source)` | Optional. Add instrumentation, e.g. `:rails` or `:dry`. |
|
70
|
+
| `build` | Returns `Either[Error, Store]`. |
|
58
71
|
|
59
72
|
### Back-ends vs Decorators
|
60
73
|
|
@@ -112,16 +125,122 @@ result.fold(
|
|
112
125
|
)
|
113
126
|
```
|
114
127
|
|
128
|
+
## The `CacheRef` and `Store` APIs
|
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. The `Store` also provides `fetch_all` for batch operations.
|
131
|
+
|
132
|
+
You get a `CacheRef` by calling `store.ref(key)`:
|
133
|
+
|
134
|
+
```ruby
|
135
|
+
user_ref = store.ref("users:123") # => #<TypedCache::CacheRef ...>
|
136
|
+
```
|
137
|
+
|
138
|
+
Now you can operate on it:
|
139
|
+
|
140
|
+
```ruby
|
141
|
+
# Fetch a value, computing it if it's missing
|
142
|
+
snapshot_either = user_ref.fetch do
|
143
|
+
{ id: 123, name: "Jane Doe" }
|
144
|
+
end
|
145
|
+
|
146
|
+
# The result is always an Either[Error, Snapshot]
|
147
|
+
snapshot_either.fold(
|
148
|
+
->(err) { warn "Something went wrong: #{err.message}" },
|
149
|
+
->(snapshot) { puts "Got value: #{snapshot.value} (from cache: #{snapshot.from_cache?})" }
|
150
|
+
)
|
151
|
+
|
152
|
+
# You can also map over values
|
153
|
+
name_either = user_ref.map { |user| user[:name] }
|
154
|
+
puts "User name is: #{name_either.value.value}" # unwrap Either, then Snapshot
|
155
|
+
```
|
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
|
+
|
179
|
+
The `CacheRef` API encourages a functional style and makes composing cache operations safe and predictable.
|
180
|
+
|
115
181
|
## Instrumentation
|
116
182
|
|
117
|
-
|
118
|
-
`TypedCache.config.instrumentation.enabled = true`:
|
183
|
+
TypedCache can publish events about cache operations using different instrumenters. To enable it, use the `with_instrumentation` method on the builder, specifying an instrumentation backend:
|
119
184
|
|
185
|
+
```ruby
|
186
|
+
# For ActiveSupport::Notifications (e.g. in Rails)
|
187
|
+
store = TypedCache.builder
|
188
|
+
.with_backend(:memory)
|
189
|
+
.with_instrumentation(:rails)
|
190
|
+
.build.value
|
191
|
+
|
192
|
+
# For Dry::Monitor
|
193
|
+
store = TypedCache.builder
|
194
|
+
.with_backend(:memory)
|
195
|
+
.with_instrumentation(:dry)
|
196
|
+
.build.value
|
120
197
|
```
|
121
|
-
|
198
|
+
|
199
|
+
Events are published to a topic like `typed_cache.<operation>` (e.g., `typed_cache.get`). The topic namespace can be configured.
|
200
|
+
|
201
|
+
Payload keys include: `:namespace, :key, :operation, :duration`, and `cache_hit`.
|
202
|
+
|
203
|
+
You can subscribe to these events like so:
|
204
|
+
|
205
|
+
```ruby
|
206
|
+
# Example for ActiveSupport
|
207
|
+
ActiveSupport::Notifications.subscribe("typed_cache.get") do |name, start, finish, id, payload|
|
208
|
+
|
209
|
+
# Or you can subscribe via the store object itself
|
210
|
+
instrumenter = store.instrumenter
|
211
|
+
instrumenter.subscribe("get") do |event|
|
212
|
+
payload = event.payload
|
213
|
+
puts "Cache GET for key #{payload[:key]} took #{payload[:duration]}ms. Hit? #{payload[:cache_hit]}"
|
214
|
+
end
|
122
215
|
```
|
123
216
|
|
124
|
-
|
217
|
+
If you call `with_instrumentation` with no arguments, it uses a `Null` instrumenter, which has no overhead.
|
218
|
+
|
219
|
+
### Custom Instrumenters
|
220
|
+
|
221
|
+
Just like with back-ends and decorators, you can write and register your own instrumenters. An instrumenter must implement an `instrument` and a `subscribe` method.
|
222
|
+
|
223
|
+
```ruby
|
224
|
+
class MyCustomInstrumenter
|
225
|
+
include TypedCache::Instrumenter
|
226
|
+
|
227
|
+
def instrument(operation, key, **payload, &block)
|
228
|
+
# ... your logic ...
|
229
|
+
end
|
230
|
+
|
231
|
+
def subscribe(event_name, **filters, &block)
|
232
|
+
# ... your logic ...
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
# Register it
|
237
|
+
TypedCache::Instrumenters.register(:custom, MyCustomInstrumenter)
|
238
|
+
|
239
|
+
# Use it
|
240
|
+
store = TypedCache.builder
|
241
|
+
.with_instrumentation(:custom)
|
242
|
+
# ...
|
243
|
+
```
|
125
244
|
|
126
245
|
## Further examples
|
127
246
|
|
data/examples.md
CHANGED
@@ -7,16 +7,19 @@ This document provides practical examples of using TypedCache in real applicatio
|
|
7
7
|
The simplest way to create a type-safe cache:
|
8
8
|
|
9
9
|
```ruby
|
10
|
-
|
11
|
-
|
12
|
-
cache_result = TypedCache.builder
|
10
|
+
# The builder can be pre-configured and reused
|
11
|
+
builder = TypedCache.builder
|
13
12
|
.with_backend(:memory, shared: true)
|
14
|
-
.with_instrumentation
|
15
|
-
|
13
|
+
.with_instrumentation(:rails)
|
14
|
+
|
15
|
+
# The store is built with a namespace
|
16
|
+
users_store = builder.build(TypedCache::Namespace.at("users")).value
|
16
17
|
|
17
|
-
|
18
|
-
|
19
|
-
|
18
|
+
# Get a reference to a key
|
19
|
+
user_ref = users_store.ref("123")
|
20
|
+
|
21
|
+
# Set a value
|
22
|
+
user_ref.set({ id: 123, name: "Jane" })
|
20
23
|
```
|
21
24
|
|
22
25
|
## Rails Integration
|
@@ -24,13 +27,17 @@ store.set(key, { id: 123, name: "Jane" })
|
|
24
27
|
Using TypedCache with `Rails.cache` and ActiveSupport notifications:
|
25
28
|
|
26
29
|
```ruby
|
27
|
-
|
30
|
+
# Use Rails.cache as the backend
|
31
|
+
builder = TypedCache.builder
|
28
32
|
.with_backend(:active_support, Rails.cache)
|
29
|
-
.with_instrumentation
|
30
|
-
.build # defaults to TypedCache.config.default_namespace
|
33
|
+
.with_instrumentation(:rails)
|
31
34
|
|
32
|
-
|
33
|
-
|
35
|
+
# Build a store for a specific part of your app
|
36
|
+
header_store = builder.build(TypedCache::Namespace.at("views:header")).value
|
37
|
+
|
38
|
+
# Use a ref to fetch/render
|
39
|
+
header_ref = header_store.ref("main")
|
40
|
+
header_html = header_ref.fetch { render_header }.value.value
|
34
41
|
```
|
35
42
|
|
36
43
|
## Pattern Matching
|
@@ -58,35 +65,106 @@ Reuse a preconfigured builder for several namespaces:
|
|
58
65
|
base_builder = TypedCache.builder
|
59
66
|
.with_backend(:memory, shared: true)
|
60
67
|
|
61
|
-
users_store
|
62
|
-
posts_store
|
68
|
+
users_store = base_builder.build(TypedCache::Namespace.at("users")).value
|
69
|
+
posts_store = base_builder.build(TypedCache::Namespace.at("posts")).value
|
63
70
|
comments_store = base_builder.build(TypedCache::Namespace.at("comments")).value
|
64
71
|
```
|
65
72
|
|
66
|
-
##
|
73
|
+
## Advanced Namespacing
|
67
74
|
|
68
|
-
|
75
|
+
You can create nested namespaces using variadic arguments to `Namespace.at` or by chaining `join`.
|
69
76
|
|
70
77
|
```ruby
|
71
|
-
#
|
72
|
-
|
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").set({ 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.
|
93
|
+
|
94
|
+
```ruby
|
95
|
+
ref = store.ref("some-key") # => CacheRef
|
96
|
+
```
|
97
|
+
|
98
|
+
### Get a value
|
73
99
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
100
|
+
The `get` method returns an `Either[Error, Snapshot]`.
|
101
|
+
|
102
|
+
```ruby
|
103
|
+
result = ref.get
|
104
|
+
result.fold(
|
105
|
+
->(error) { puts "Cache miss or error: #{error.message}" },
|
106
|
+
->(snapshot) { puts "Found: #{snapshot.value} (from cache: #{snapshot.from_cache?})" }
|
78
107
|
)
|
108
|
+
```
|
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
|
+
|
127
|
+
### Fetch (get or compute)
|
128
|
+
|
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.
|
130
|
+
|
131
|
+
```ruby
|
132
|
+
user = ref.fetch { expensive_user_lookup(123) }.value.value
|
133
|
+
```
|
134
|
+
|
135
|
+
### Mapping values
|
136
|
+
|
137
|
+
You can transform the value inside the cache reference without breaking the monadic chain.
|
138
|
+
|
139
|
+
```ruby
|
140
|
+
# user_ref holds { id: 1, name: "John" }
|
141
|
+
name_ref = user_ref.map { |user| user[:name] }
|
142
|
+
|
143
|
+
name_snapshot = name_ref.get.value # => Snapshot(value: "John", ...)
|
144
|
+
```
|
145
|
+
|
146
|
+
### Chaining operations with `bind`
|
79
147
|
|
80
|
-
|
81
|
-
user = cache.peek.value_or({ id: 0, name: "Anonymous" })
|
148
|
+
For more complex logic, you can use `bind` (or `flat_map`) to chain operations that return an `Either`.
|
82
149
|
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
150
|
+
```ruby
|
151
|
+
user_ref.bind do |user|
|
152
|
+
if user.active?
|
153
|
+
posts_ref.set(user.posts)
|
154
|
+
else
|
155
|
+
TypedCache::Either.left(StandardError.new("User is not active"))
|
156
|
+
end
|
87
157
|
end
|
88
158
|
```
|
89
159
|
|
160
|
+
### Getting the value or a default
|
161
|
+
|
162
|
+
If you just want the value and don't care about the `Snapshot` metadata, you can use `value_or`.
|
163
|
+
|
164
|
+
```ruby
|
165
|
+
user_name = user_ref.map { |u| u[:name] }.value_or("Anonymous")
|
166
|
+
```
|
167
|
+
|
90
168
|
## Custom Backend
|
91
169
|
|
92
170
|
Registering and using a custom cache backend:
|
@@ -126,35 +204,47 @@ cache = TypedCache.builder
|
|
126
204
|
.build.value
|
127
205
|
```
|
128
206
|
|
129
|
-
##
|
207
|
+
## Custom Instrumenter
|
208
|
+
|
209
|
+
You can create and register your own instrumenter to integrate with any monitoring or logging system. An instrumenter needs to implement `instrument` and `subscribe` methods.
|
210
|
+
|
211
|
+
Here's an example of a simple logging instrumenter:
|
130
212
|
|
131
213
|
```ruby
|
132
|
-
|
133
|
-
|
134
|
-
|
214
|
+
class LoggingInstrumenter
|
215
|
+
include TypedCache::Instrumenter
|
216
|
+
include TypedCache::Instrumenters::Mixins::NamespacedSingleton
|
217
|
+
|
218
|
+
def instrument(operation, key, **payload, &block)
|
219
|
+
puts "[CACHE] Starting: #{operation} for key #{key}"
|
220
|
+
result = block.call
|
221
|
+
puts "[CACHE] Finished: #{operation} for key #{key}"
|
222
|
+
result
|
223
|
+
end
|
224
|
+
|
225
|
+
def subscribe(operation, **_filters, &block)
|
226
|
+
# For simplicity, this example doesn't implement a full subscription model,
|
227
|
+
# but in a real-world scenario, you would store and manage callbacks here.
|
228
|
+
puts "[CACHE] Subscribed to '#{operation}'"
|
229
|
+
end
|
135
230
|
end
|
136
231
|
|
137
|
-
|
232
|
+
# Register the new instrumenter
|
233
|
+
TypedCache::Instrumenters.register(:logger, LoggingInstrumenter)
|
234
|
+
|
235
|
+
# Use it in the builder
|
236
|
+
logging_cache = TypedCache.builder
|
138
237
|
.with_backend(:memory)
|
139
|
-
.with_instrumentation
|
238
|
+
.with_instrumentation(:logger)
|
140
239
|
.build.value
|
141
240
|
|
142
|
-
|
143
|
-
|
241
|
+
# Subscribe to an event
|
242
|
+
logging_cache.instrumenter.subscribe("get")
|
144
243
|
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
config.default_namespace = "my_app"
|
150
|
-
end
|
151
|
-
```
|
152
|
-
|
153
|
-
You can register additional decorators or back-ends as needed:
|
154
|
-
|
155
|
-
```ruby
|
156
|
-
TypedCache::Decorators.register(:logger, MyLoggerDecorator)
|
157
|
-
TypedCache::Backends.register(:redis, MyRedisBackend)
|
244
|
+
# Operations will now be logged
|
245
|
+
logging_cache.ref("test").set("hello")
|
246
|
+
# => [CACHE] Starting: set for key test
|
247
|
+
# => [CACHE] Finished: set for key test
|
158
248
|
```
|
159
249
|
|
160
250
|
## Thread Safety
|
@@ -20,24 +20,24 @@ module TypedCache
|
|
20
20
|
|
21
21
|
# @rbs override
|
22
22
|
#: (cache_key) -> either[Error, Snapshot[V]]
|
23
|
-
def
|
23
|
+
def read(key)
|
24
24
|
cache_key_str = namespaced_key(key).to_s
|
25
25
|
raw_value = cache_store.read(cache_key_str, default_options)
|
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
34
|
#: (cache_key, V) -> either[Error, Snapshot[V]]
|
35
|
-
def
|
35
|
+
def write(key, value)
|
36
36
|
cache_key_str = namespaced_key(key).to_s
|
37
37
|
success = cache_store.write(cache_key_str, value, default_options)
|
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]) -> either[Error, Array[Snapshot[V]]]
|
50
|
+
def write_all(values)
|
51
|
+
results = cache_store.write_multi(values.map { |key, value| [namespaced_key(key).to_s, value] }.to_h, default_options)
|
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]) -> either[Error, Array[Snapshot[V]]]
|
74
|
+
def read_all(keys)
|
75
|
+
results = cache_store.read_multi(*keys.map { |key| namespaced_key(key).to_s }, default_options)
|
76
|
+
Either.right(results.map { |key, value| [key, Snapshot.cached(key, value)] }.to_h)
|
77
|
+
end
|
78
|
+
|
79
|
+
# @rbs override
|
80
|
+
#: (Array[cache_key]) { (CacheKey) -> V? } -> either[Error, Array[Snapshot[V]]]
|
81
|
+
def fetch_all(keys, &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) 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)
|
@@ -1,7 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'singleton'
|
4
|
-
|
4
|
+
|
5
|
+
require 'concurrent/map'
|
5
6
|
|
6
7
|
module TypedCache
|
7
8
|
class MemoryStoreRegistry
|
@@ -42,14 +43,16 @@ module TypedCache
|
|
42
43
|
# @rbs! attr_accessor expires_at: Time
|
43
44
|
# @rbs! attr_reader value: V
|
44
45
|
|
45
|
-
|
46
|
-
|
47
|
-
|
46
|
+
class << self
|
47
|
+
# @rbs (value: V, expires_in: Integer) -> Entry[V]
|
48
|
+
def expiring(value:, expires_in:)
|
49
|
+
new(value: value, expires_at: Clock.moment + expires_in)
|
50
|
+
end
|
48
51
|
end
|
49
52
|
|
50
53
|
# @rbs () -> bool
|
51
54
|
def expired?
|
52
|
-
Clock.
|
55
|
+
Clock.now >= expires_at
|
53
56
|
end
|
54
57
|
end
|
55
58
|
private_constant :Entry
|
@@ -66,7 +69,7 @@ module TypedCache
|
|
66
69
|
|
67
70
|
# @rbs override
|
68
71
|
#: (cache_key) -> either[Error, Snapshot[V]]
|
69
|
-
def
|
72
|
+
def read(key)
|
70
73
|
key = namespaced_key(key)
|
71
74
|
return Either.left(CacheMissError.new(key)) unless backing_store.key?(key)
|
72
75
|
|
@@ -77,17 +80,17 @@ module TypedCache
|
|
77
80
|
return Either.left(CacheMissError.new(key))
|
78
81
|
end
|
79
82
|
|
80
|
-
Either.right(Snapshot.
|
83
|
+
Either.right(Snapshot.cached(key, entry.value))
|
81
84
|
end
|
82
85
|
|
83
86
|
# @rbs override
|
84
87
|
#: (cache_key, V) -> either[Error, Snapshot[V]]
|
85
|
-
def
|
88
|
+
def write(key, value)
|
86
89
|
key = namespaced_key(key)
|
87
|
-
expires_at = Clock.
|
90
|
+
expires_at = Clock.now + @ttl
|
88
91
|
entry = Entry.new(value: value, expires_at: expires_at)
|
89
92
|
backing_store[key] = entry
|
90
|
-
Either.right(Snapshot.
|
93
|
+
Either.right(Snapshot.cached(key, value))
|
91
94
|
rescue => e
|
92
95
|
Either.left(StoreError.new(
|
93
96
|
:set,
|
@@ -105,7 +108,7 @@ module TypedCache
|
|
105
108
|
if entry.nil?
|
106
109
|
Either.left(CacheMissError.new(key))
|
107
110
|
else
|
108
|
-
Either.right(Snapshot.
|
111
|
+
Either.right(Snapshot.cached(key, entry.value))
|
109
112
|
end
|
110
113
|
end
|
111
114
|
|
data/lib/typed_cache/backends.rb
CHANGED
@@ -1,25 +1,23 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require '
|
4
|
-
require 'dry/types'
|
5
|
-
|
6
|
-
require_relative 'backends/memory'
|
7
|
-
require_relative 'backends/active_support'
|
3
|
+
require 'typed_cache/registry'
|
8
4
|
|
9
5
|
module TypedCache
|
10
6
|
module Backends
|
7
|
+
autoload :Memory, 'typed_cache/backends/memory'
|
8
|
+
autoload :ActiveSupport, 'typed_cache/backends/active_support'
|
9
|
+
|
10
|
+
# @api private
|
11
11
|
# Backend registry using composition
|
12
12
|
REGISTRY = Registry.new('backend', {
|
13
13
|
memory: Memory,
|
14
|
-
active_support: ActiveSupport,
|
15
14
|
}).freeze
|
16
15
|
|
17
|
-
private_constant :REGISTRY
|
18
|
-
|
19
16
|
class << self
|
20
17
|
extend Forwardable
|
21
18
|
delegate [:resolve, :register, :available, :registered?] => :registry
|
22
19
|
|
20
|
+
# @api private
|
23
21
|
#: -> Registry
|
24
22
|
def registry = REGISTRY
|
25
23
|
|