typed_cache 0.1.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 +7 -0
- checksums.yaml.gz.sig +0 -0
- data/LICENSE +201 -0
- data/README.md +168 -0
- data/examples.md +190 -0
- data/lib/typed_cache/backend.rb +16 -0
- data/lib/typed_cache/backends/active_support.rb +113 -0
- data/lib/typed_cache/backends/memory.rb +166 -0
- data/lib/typed_cache/backends.rb +34 -0
- data/lib/typed_cache/cache_builder.rb +77 -0
- data/lib/typed_cache/cache_key.rb +45 -0
- data/lib/typed_cache/cache_ref.rb +155 -0
- data/lib/typed_cache/clock.rb +23 -0
- data/lib/typed_cache/decorator.rb +12 -0
- data/lib/typed_cache/decorators.rb +35 -0
- data/lib/typed_cache/either.rb +121 -0
- data/lib/typed_cache/errors.rb +64 -0
- data/lib/typed_cache/instrumentation.rb +112 -0
- data/lib/typed_cache/maybe.rb +92 -0
- data/lib/typed_cache/namespace.rb +162 -0
- data/lib/typed_cache/registry.rb +55 -0
- data/lib/typed_cache/snapshot.rb +72 -0
- data/lib/typed_cache/store/instrumented.rb +83 -0
- data/lib/typed_cache/store.rb +152 -0
- data/lib/typed_cache/version.rb +5 -0
- data/lib/typed_cache.rb +58 -0
- data/sig/generated/typed_cache/backend.rbs +17 -0
- data/sig/generated/typed_cache/backends/active_support.rbs +56 -0
- data/sig/generated/typed_cache/backends/memory.rbs +95 -0
- data/sig/generated/typed_cache/backends.rbs +21 -0
- data/sig/generated/typed_cache/cache_builder.rbs +37 -0
- data/sig/generated/typed_cache/cache_key.rbs +33 -0
- data/sig/generated/typed_cache/cache_ref.rbs +91 -0
- data/sig/generated/typed_cache/clock.rbs +15 -0
- data/sig/generated/typed_cache/decorator.rbs +14 -0
- data/sig/generated/typed_cache/decorators.rbs +25 -0
- data/sig/generated/typed_cache/either.rbs +106 -0
- data/sig/generated/typed_cache/errors.rbs +51 -0
- data/sig/generated/typed_cache/instrumentation.rbs +30 -0
- data/sig/generated/typed_cache/maybe.rbs +85 -0
- data/sig/generated/typed_cache/namespace.rbs +130 -0
- data/sig/generated/typed_cache/registry.rbs +25 -0
- data/sig/generated/typed_cache/snapshot.rbs +50 -0
- data/sig/generated/typed_cache/store/instrumented.rbs +37 -0
- data/sig/generated/typed_cache/store.rbs +104 -0
- data/sig/generated/typed_cache/version.rbs +5 -0
- data/sig/generated/typed_cache.rbs +34 -0
- data/sig/handwritten/gems/zeitwerk/2.7/zeitwerk.rbs +9 -0
- data/typed_cache.gemspec +42 -0
- data.tar.gz.sig +0 -0
- metadata +228 -0
- metadata.gz.sig +0 -0
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TypedCache
|
4
|
+
# Base error class for TypedCache operations
|
5
|
+
class Error < StandardError; end
|
6
|
+
|
7
|
+
# Store operation errors (network, I/O, etc.)
|
8
|
+
class StoreError < Error
|
9
|
+
attr_reader :operation, :key, :original_error
|
10
|
+
|
11
|
+
# @rbs (Symbol, String, String, Exception?) -> void
|
12
|
+
def initialize(operation, key, message, original_error = nil)
|
13
|
+
super(message)
|
14
|
+
@operation = operation
|
15
|
+
@key = key
|
16
|
+
@original_error = original_error
|
17
|
+
end
|
18
|
+
|
19
|
+
# @rbs () -> String
|
20
|
+
def detailed_message
|
21
|
+
base = "#{operation.upcase} operation failed for key '#{key}': #{message}"
|
22
|
+
original_error ? "#{base} (#{original_error.class}: #{original_error.message})" : base
|
23
|
+
end
|
24
|
+
|
25
|
+
# @rbs () -> bool
|
26
|
+
def has_cause?
|
27
|
+
!@original_error.nil?
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Type safety violations
|
32
|
+
class TypeError < Error
|
33
|
+
attr_reader :expected_type, :actual_type, :value
|
34
|
+
|
35
|
+
# @rbs (String, String, untyped, String) -> void
|
36
|
+
def initialize(expected_type, actual_type, value, message)
|
37
|
+
super(message)
|
38
|
+
@expected_type = expected_type
|
39
|
+
@actual_type = actual_type
|
40
|
+
@value = value
|
41
|
+
end
|
42
|
+
|
43
|
+
# @rbs () -> String
|
44
|
+
def type_mismatch_message
|
45
|
+
"Expected #{expected_type}, got #{actual_type}"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Cache miss (when expecting a value to exist)
|
50
|
+
class CacheMissError < Error
|
51
|
+
attr_reader :key
|
52
|
+
|
53
|
+
# @rbs (CacheKey) -> void
|
54
|
+
def initialize(key)
|
55
|
+
super("Cache miss for key: #{key}")
|
56
|
+
@key = key
|
57
|
+
end
|
58
|
+
|
59
|
+
# @rbs () -> bool
|
60
|
+
def cache_miss?
|
61
|
+
true
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry/configurable'
|
4
|
+
|
5
|
+
module TypedCache
|
6
|
+
# Instrumentation hooks for ActiveSupport::Notifications integration
|
7
|
+
# All instrumentation is explicit and opt-in - no automatic behavior
|
8
|
+
module Instrumentation
|
9
|
+
class << self
|
10
|
+
# @rbs! type config = TypedCache::_TypedCacheInstrumentationConfig
|
11
|
+
|
12
|
+
# @rbs () -> config
|
13
|
+
def config
|
14
|
+
TypedCache.config.instrumentation
|
15
|
+
end
|
16
|
+
|
17
|
+
# Check if ActiveSupport::Notifications is available
|
18
|
+
# @rbs () -> bool
|
19
|
+
def notifications_available?
|
20
|
+
defined?(ActiveSupport::Notifications)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Main instrumentation method
|
24
|
+
#: [T] (String, String, String, Hash[Symbol, untyped]) { -> T } -> T
|
25
|
+
def instrument(operation, namespace, key, payload = {})
|
26
|
+
return yield unless config.enabled && notifications_available?
|
27
|
+
|
28
|
+
event_name = "#{operation}.#{config.namespace}"
|
29
|
+
start_time = current_time
|
30
|
+
|
31
|
+
begin
|
32
|
+
result = yield
|
33
|
+
|
34
|
+
# Determine success and extract metadata
|
35
|
+
success, snapshot_data = extract_result_metadata(result)
|
36
|
+
|
37
|
+
final_payload = base_payload(namespace, key, start_time).merge(payload).merge({
|
38
|
+
success: success,
|
39
|
+
**snapshot_data,
|
40
|
+
})
|
41
|
+
|
42
|
+
ActiveSupport::Notifications.instrument(event_name, final_payload) do
|
43
|
+
# This block is called by subscribers who want the result
|
44
|
+
result
|
45
|
+
end
|
46
|
+
|
47
|
+
result
|
48
|
+
rescue => error
|
49
|
+
error_payload = base_payload(namespace, key, start_time).merge(payload).merge({
|
50
|
+
success: false,
|
51
|
+
error: error.class.name,
|
52
|
+
error_message: error.message,
|
53
|
+
})
|
54
|
+
|
55
|
+
ActiveSupport::Notifications.instrument(event_name, error_payload)
|
56
|
+
raise
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
# Cross-platform current time helper (uses Time.current when available)
|
63
|
+
#: -> Time
|
64
|
+
def current_time
|
65
|
+
if Time.respond_to?(:current)
|
66
|
+
Time.current
|
67
|
+
else
|
68
|
+
Time.now
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# @rbs (String, String, Time) -> Hash[Symbol, untyped]
|
73
|
+
def base_payload(namespace, key, start_time)
|
74
|
+
{
|
75
|
+
namespace: namespace,
|
76
|
+
key: key,
|
77
|
+
duration: (current_time - start_time) * 1000.0, # milliseconds
|
78
|
+
store_type: nil, # Will be set by caller if available
|
79
|
+
}
|
80
|
+
end
|
81
|
+
|
82
|
+
# @rbs (Either[StandardError, Snapshot]) -> [bool, Hash[Symbol, untyped]]
|
83
|
+
def extract_result_metadata(result)
|
84
|
+
case result
|
85
|
+
when Either
|
86
|
+
if result.right?
|
87
|
+
snapshot = result.value
|
88
|
+
if snapshot.is_a?(Snapshot)
|
89
|
+
[true, {
|
90
|
+
cache_hit: snapshot.from_cache?,
|
91
|
+
cache_miss: !snapshot.from_cache?,
|
92
|
+
source: snapshot.source,
|
93
|
+
snapshot_age: snapshot.age,
|
94
|
+
},]
|
95
|
+
else
|
96
|
+
[true, { cache_hit: false, cache_miss: true }]
|
97
|
+
end
|
98
|
+
else
|
99
|
+
error_data = {
|
100
|
+
cache_hit: false,
|
101
|
+
cache_miss: true,
|
102
|
+
error_type: result.error.class.name,
|
103
|
+
}
|
104
|
+
[false, error_data]
|
105
|
+
end
|
106
|
+
else
|
107
|
+
[true, {}]
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TypedCache
|
4
|
+
# @rbs!
|
5
|
+
# type maybe[T] = (Nothing | Some[T]) & _Maybe[T]
|
6
|
+
|
7
|
+
module Maybe
|
8
|
+
include Kernel
|
9
|
+
|
10
|
+
class << self
|
11
|
+
#: [V](V) -> maybe[V]
|
12
|
+
def some(value) = Some.new(value)
|
13
|
+
#: -> maybe[bot]
|
14
|
+
def none = Nothing.new
|
15
|
+
|
16
|
+
#: [V](V? | maybe[V]) -> maybe[V]
|
17
|
+
def wrap(value)
|
18
|
+
case value
|
19
|
+
when Nothing, Some then value
|
20
|
+
when NilClass then Nothing.new
|
21
|
+
else
|
22
|
+
Some.new(value)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
alias [] some
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# @rbs!
|
31
|
+
# interface _Maybe[out V]
|
32
|
+
# def some?: -> bool
|
33
|
+
# def nothing?: -> bool
|
34
|
+
# def map: [T] () { (V) -> T } -> maybe[T]
|
35
|
+
# def bind: [T] () { (V) -> maybe[T] } -> maybe[T]
|
36
|
+
# alias flat_map bind
|
37
|
+
# end
|
38
|
+
|
39
|
+
# @rbs generic out V
|
40
|
+
class Some
|
41
|
+
# @rbs! include _Maybe[V]
|
42
|
+
|
43
|
+
attr_reader :value #: V
|
44
|
+
|
45
|
+
#: (V) -> void
|
46
|
+
def initialize(value)
|
47
|
+
@value = value
|
48
|
+
end
|
49
|
+
|
50
|
+
# @rbs override
|
51
|
+
#: -> TrueClass
|
52
|
+
def some? = true
|
53
|
+
# @rbs override
|
54
|
+
#: -> FalseClass
|
55
|
+
def nothing? = false
|
56
|
+
|
57
|
+
# @rbs override
|
58
|
+
#: [T] () { (V) -> T } -> maybe[T]
|
59
|
+
def map(&) = Some.new(yield(value))
|
60
|
+
|
61
|
+
# @rbs override
|
62
|
+
#: [T] () { (V) -> maybe[T] } -> maybe[T]
|
63
|
+
def bind(&) = yield(value)
|
64
|
+
|
65
|
+
alias flat_map bind
|
66
|
+
|
67
|
+
#: (Array[top]) -> ({ value: V })
|
68
|
+
def deconstruct_keys(keys)
|
69
|
+
{ value: }
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
class Nothing
|
74
|
+
# @rbs! include _Maybe[bot]
|
75
|
+
|
76
|
+
# @rbs override
|
77
|
+
#: -> FalseClass
|
78
|
+
def some? = false
|
79
|
+
# @rbs override
|
80
|
+
#: -> TrueClass
|
81
|
+
def nothing? = true
|
82
|
+
|
83
|
+
# @rbs override
|
84
|
+
#: [T] () { (V) -> T } -> maybe[T]
|
85
|
+
def map(&) = self
|
86
|
+
|
87
|
+
# @rbs override
|
88
|
+
#: [T] () { (V) -> maybe[T] } -> maybe[T]
|
89
|
+
def bind(&) = self
|
90
|
+
alias flat_map bind
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,162 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TypedCache
|
4
|
+
# Provides a type-safe, composable namespace abstraction for cache keys.
|
5
|
+
#
|
6
|
+
# The Namespace class allows you to create hierarchical namespaces for cache keys,
|
7
|
+
# ensuring that keys are properly scoped and collisions are avoided. Each Namespace
|
8
|
+
# instance can generate cache keys (via #key), create nested namespaces (via #nested),
|
9
|
+
# and traverse to parent namespaces (via #parent_namespace).
|
10
|
+
#
|
11
|
+
# Example:
|
12
|
+
# ns = TypedCache::Namespace.at("users")
|
13
|
+
# ns.key("123") # => #<TypedCache::CacheKey namespace=users key=123>
|
14
|
+
# ns2 = ns.nested("sessions")
|
15
|
+
# ns2.key("abc") # => #<TypedCache::CacheKey namespace=users:sessions key=abc>
|
16
|
+
#
|
17
|
+
# Namespaces are composable and immutable. The key factory can be customized for advanced use cases.
|
18
|
+
class Namespace
|
19
|
+
class << self
|
20
|
+
# Returns a new Namespace instance rooted at the given namespace string.
|
21
|
+
#
|
22
|
+
# @param namespace [String] the root namespace
|
23
|
+
# @return [Namespace] a new Namespace instance at the given root
|
24
|
+
#
|
25
|
+
# Example:
|
26
|
+
# TypedCache::Namespace.at("users") # => #<TypedCache::Namespace namespace=users>
|
27
|
+
#
|
28
|
+
# The returned Namespace can be further nested or used to generate cache keys.
|
29
|
+
#
|
30
|
+
# @rbs (String) -> Namespace
|
31
|
+
def at(namespace)
|
32
|
+
root.nested(namespace)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Returns the root Namespace instance (with an empty namespace).
|
36
|
+
#
|
37
|
+
# @return [Namespace] the root Namespace
|
38
|
+
#
|
39
|
+
# Example:
|
40
|
+
# TypedCache::Namespace.root # => #<TypedCache::Namespace namespace=>
|
41
|
+
#
|
42
|
+
# The root namespace is useful as a starting point for building nested namespaces.
|
43
|
+
#
|
44
|
+
# @rbs () -> Namespace
|
45
|
+
def root
|
46
|
+
new(TypedCache.config.default_namespace) { |ns, key| CacheKey.new(ns, key) }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Initializes a new Namespace instance with the given namespace string and key factory.
|
51
|
+
#
|
52
|
+
# @param namespace [String] the namespace string for this instance
|
53
|
+
# @param key_factory [Proc] a block that creates CacheKey instances from key strings
|
54
|
+
# @yield [key] the key string to create a CacheKey from
|
55
|
+
# @yieldreturn [CacheKey] the created cache key
|
56
|
+
#
|
57
|
+
# Example:
|
58
|
+
# Namespace.new("users") { |key| CacheKey.new("users", key) }
|
59
|
+
#
|
60
|
+
# @rbs (String) { (Namespace, String) -> CacheKey } -> void
|
61
|
+
def initialize(namespace, &key_factory)
|
62
|
+
@namespace = namespace
|
63
|
+
@key_factory = key_factory
|
64
|
+
end
|
65
|
+
|
66
|
+
# Creates a nested namespace by appending the given namespace to the current one.
|
67
|
+
#
|
68
|
+
# @param namespace [String] the namespace to append
|
69
|
+
# @param key_factory [Proc, nil] optional custom key factory for the nested namespace
|
70
|
+
# @return [Namespace] a new Namespace instance with the combined namespace
|
71
|
+
#
|
72
|
+
# Example:
|
73
|
+
# ns = Namespace.at("users")
|
74
|
+
# ns.nested("sessions") # => #<TypedCache::Namespace namespace=users:sessions>
|
75
|
+
#
|
76
|
+
# If no key_factory is provided, the parent's key factory is inherited.
|
77
|
+
#
|
78
|
+
# @rbs (String) ?{ (Namespace, String) -> CacheKey } -> Namespace
|
79
|
+
def nested(namespace, &key_factory)
|
80
|
+
key_factory ||= @key_factory
|
81
|
+
|
82
|
+
self.class.new("#{@namespace}:#{namespace}", &key_factory)
|
83
|
+
end
|
84
|
+
|
85
|
+
# Returns the parent namespace by removing the last namespace segment.
|
86
|
+
#
|
87
|
+
# @return [Namespace] the parent namespace, or self if already at root
|
88
|
+
#
|
89
|
+
# Example:
|
90
|
+
# ns = Namespace.at("users:sessions")
|
91
|
+
# ns.parent_namespace # => #<TypedCache::Namespace namespace=users>
|
92
|
+
#
|
93
|
+
# For root namespaces (empty string), returns self.
|
94
|
+
#
|
95
|
+
# @rbs () -> Namespace
|
96
|
+
def parent_namespace
|
97
|
+
return self if @namespace.empty?
|
98
|
+
|
99
|
+
case pathsep_idx = @namespace.rindex(':')
|
100
|
+
when nil
|
101
|
+
self.class.root
|
102
|
+
else
|
103
|
+
self.class.new(@namespace[...pathsep_idx])
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# Creates a cache key using the namespace's key factory.
|
108
|
+
#
|
109
|
+
# @param key [String] the key string to create a cache key from
|
110
|
+
# @return [CacheKey] the created cache key
|
111
|
+
#
|
112
|
+
# Example:
|
113
|
+
# ns = Namespace.at("users")
|
114
|
+
# ns.key("123") # => #<TypedCache::CacheKey namespace=users key=123>
|
115
|
+
#
|
116
|
+
# @rbs (String) -> CacheKey
|
117
|
+
def key(key)
|
118
|
+
@key_factory.call(self, key)
|
119
|
+
end
|
120
|
+
|
121
|
+
# @rbs () -> bool
|
122
|
+
def root? = @namespace.empty?
|
123
|
+
|
124
|
+
# Returns the namespace string representation.
|
125
|
+
#
|
126
|
+
# @return [String] the namespace string
|
127
|
+
#
|
128
|
+
# Example:
|
129
|
+
# ns = Namespace.at("users:sessions")
|
130
|
+
# ns.to_s # => "users:sessions"
|
131
|
+
#
|
132
|
+
# @rbs () -> String
|
133
|
+
def to_s
|
134
|
+
@namespace
|
135
|
+
end
|
136
|
+
|
137
|
+
# Returns a string representation of the Namespace instance for debugging.
|
138
|
+
#
|
139
|
+
# @return [String] a debug-friendly string representation
|
140
|
+
#
|
141
|
+
# Example:
|
142
|
+
# ns = Namespace.at("users")
|
143
|
+
# ns.inspect # => "#<TypedCache::Namespace namespace=users>"
|
144
|
+
#
|
145
|
+
# @rbs () -> String
|
146
|
+
def inspect
|
147
|
+
"#<#{self.class} #{@namespace}>"
|
148
|
+
end
|
149
|
+
|
150
|
+
# @rbs () -> Integer
|
151
|
+
def hash
|
152
|
+
[@namespace].hash
|
153
|
+
end
|
154
|
+
|
155
|
+
# @rbs (Object) -> bool
|
156
|
+
def ==(other)
|
157
|
+
other.is_a?(self.class) && other.to_s == to_s
|
158
|
+
end
|
159
|
+
|
160
|
+
alias eql? ==
|
161
|
+
end
|
162
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TypedCache
|
4
|
+
# Generic registry for managing class-based factories
|
5
|
+
# @rbs generic T
|
6
|
+
class Registry
|
7
|
+
# @rbs (String, Hash[Symbol, Class[T]]) -> void
|
8
|
+
def initialize(name, defaults = {})
|
9
|
+
@name = name
|
10
|
+
@registry = defaults.dup
|
11
|
+
end
|
12
|
+
|
13
|
+
# @rbs (Symbol, *untyped, **untyped) -> either[Error, T]
|
14
|
+
def resolve(key, *, **, &)
|
15
|
+
klass = @registry[key]
|
16
|
+
return Either.left(ArgumentError.new("Unknown #{@name}: #{key}")) unless klass
|
17
|
+
|
18
|
+
Either.right(klass.new(*, **))
|
19
|
+
rescue => e
|
20
|
+
Either.left(StoreError.new(
|
21
|
+
:"#{@name}_creation",
|
22
|
+
key.to_s,
|
23
|
+
"Failed to create #{@name} '#{key}': #{e.message}",
|
24
|
+
e,
|
25
|
+
))
|
26
|
+
end
|
27
|
+
|
28
|
+
# @rbs (Symbol) -> maybe[Class[T]]
|
29
|
+
def find(key)
|
30
|
+
klass = @registry[key]
|
31
|
+
return Maybe.none unless klass
|
32
|
+
|
33
|
+
Maybe.some(klass)
|
34
|
+
end
|
35
|
+
|
36
|
+
# @rbs (Symbol, Class[T]) -> either[Error, void]
|
37
|
+
def register(key, klass)
|
38
|
+
return Either.left(ArgumentError.new("#{@name.capitalize} name cannot be nil")) if key.nil?
|
39
|
+
return Either.left(ArgumentError.new("#{@name.capitalize} class cannot be nil")) if klass.nil?
|
40
|
+
|
41
|
+
@registry[key] = klass
|
42
|
+
Either.right(nil)
|
43
|
+
end
|
44
|
+
|
45
|
+
# @rbs () -> Array[Symbol]
|
46
|
+
def available
|
47
|
+
@registry.keys
|
48
|
+
end
|
49
|
+
|
50
|
+
# @rbs (Symbol) -> bool
|
51
|
+
def registered?(key)
|
52
|
+
@registry.key?(key)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TypedCache
|
4
|
+
# Immutable snapshot of a cached value with metadata about its source and age
|
5
|
+
# @rbs generic V
|
6
|
+
class Snapshot
|
7
|
+
attr_reader :value #: V
|
8
|
+
attr_reader :retrieved_at #: Time
|
9
|
+
attr_reader :source #: Symbol
|
10
|
+
|
11
|
+
#: (V, source: Symbol, retrieved_at: Time) -> void
|
12
|
+
def initialize(value, source:, retrieved_at: Time.now)
|
13
|
+
@value = value
|
14
|
+
@retrieved_at = retrieved_at
|
15
|
+
@source = source
|
16
|
+
end
|
17
|
+
|
18
|
+
# Age of the snapshot in seconds
|
19
|
+
#: -> Float
|
20
|
+
def age
|
21
|
+
Time.now - retrieved_at
|
22
|
+
end
|
23
|
+
|
24
|
+
# Whether this value came from cache
|
25
|
+
#: -> bool
|
26
|
+
def from_cache?
|
27
|
+
source == :cache
|
28
|
+
end
|
29
|
+
|
30
|
+
# Whether this value was freshly computed
|
31
|
+
#: -> bool
|
32
|
+
def computed?
|
33
|
+
source == :computed
|
34
|
+
end
|
35
|
+
|
36
|
+
# Whether this value was updated
|
37
|
+
#: -> bool
|
38
|
+
def updated?
|
39
|
+
source == :updated
|
40
|
+
end
|
41
|
+
|
42
|
+
# Map over the value while preserving snapshot metadata
|
43
|
+
#: [R] () { (V) -> R } -> Snapshot[R]
|
44
|
+
def map(&block)
|
45
|
+
new_value = yield(value)
|
46
|
+
Snapshot.new(new_value, source: source, retrieved_at: retrieved_at)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Bind over the value with Either error handling
|
50
|
+
#: [R] () { (V) -> either[Error, R] } -> either[Error, Snapshot[R]]
|
51
|
+
def bind(&block)
|
52
|
+
result = yield(value)
|
53
|
+
result.map { |new_value| Snapshot.new(new_value, source: source, retrieved_at: retrieved_at) }
|
54
|
+
end
|
55
|
+
|
56
|
+
alias flat_map bind
|
57
|
+
|
58
|
+
class << self
|
59
|
+
# Creates a snapshot for a computed value
|
60
|
+
#: [V] (V) -> Snapshot[V]
|
61
|
+
def computed(value)
|
62
|
+
new(value, source: :computed)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Creates a snapshot for an updated value
|
66
|
+
#: [V] (V) -> Snapshot[V]
|
67
|
+
def updated(value)
|
68
|
+
new(value, source: :updated)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TypedCache
|
4
|
+
# Decorator that adds instrumentation to any Store implementation
|
5
|
+
# This decorator can wrap any store to add ActiveSupport::Notifications
|
6
|
+
# @rbs generic V
|
7
|
+
class Store::Instrumented # rubocop:disable Style/ClassAndModuleChildren
|
8
|
+
include Decorator #[V]
|
9
|
+
|
10
|
+
extend Forwardable
|
11
|
+
|
12
|
+
attr_reader :store #: TypedCache::Store[V]
|
13
|
+
|
14
|
+
class << self
|
15
|
+
private
|
16
|
+
|
17
|
+
# @rbs (Symbol, ?operation: String) ?{ (*untyped, **untyped) -> String } -> void
|
18
|
+
def instrument(method_name, operation: method_name.to_s, &key_selector)
|
19
|
+
define_method(:"#{method_name}_with_instrumentation") do |*args, **kwargs, &block|
|
20
|
+
key = key_selector.call(*args, **kwargs) if key_selector # rubocop:disable Performance/RedundantBlockCall
|
21
|
+
|
22
|
+
Instrumentation.instrument(operation, namespace, key || 'n/a', store_type: store_type) do
|
23
|
+
send(:"#{method_name}_without_instrumentation", *args, **kwargs, &block)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
alias_method(:"#{method_name}_without_instrumentation", method_name)
|
28
|
+
alias_method(method_name, :"#{method_name}_with_instrumentation")
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
#: (TypedCache::Store[V]) -> void
|
33
|
+
def initialize(store)
|
34
|
+
@store = store
|
35
|
+
end
|
36
|
+
|
37
|
+
# @rbs override
|
38
|
+
#: -> String
|
39
|
+
def namespace
|
40
|
+
store.namespace
|
41
|
+
end
|
42
|
+
|
43
|
+
# @rbs override
|
44
|
+
#: -> String
|
45
|
+
def store_type
|
46
|
+
# Use polymorphism - delegate to the wrapped store
|
47
|
+
"instrumented(#{store.store_type})"
|
48
|
+
end
|
49
|
+
|
50
|
+
# @rbs override
|
51
|
+
#: (cache_key) -> either[Error, CacheRef[V]]
|
52
|
+
def ref(key)
|
53
|
+
CacheRef.new(self, key)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Additional methods that might exist on the wrapped store
|
57
|
+
def respond_to_missing?(method_name, include_private = false)
|
58
|
+
store.respond_to?(method_name, include_private) || super
|
59
|
+
end
|
60
|
+
|
61
|
+
def method_missing(method_name, *args, &block)
|
62
|
+
if store.respond_to?(method_name)
|
63
|
+
store.send(method_name, *args, &block)
|
64
|
+
else
|
65
|
+
super
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
Store.instance_methods(false).each do |method_name|
|
70
|
+
next if instance_methods(false).include?(method_name)
|
71
|
+
|
72
|
+
def_delegator :store, method_name
|
73
|
+
end
|
74
|
+
|
75
|
+
# Instrument core operations with proper key extraction
|
76
|
+
instrument(:get) { |key, *_| key }
|
77
|
+
instrument(:set) { |key, *_| key }
|
78
|
+
instrument(:delete) { |key, *_| key }
|
79
|
+
instrument(:fetch) { |key, *_| key }
|
80
|
+
instrument(:key?) { |key, *_| key }
|
81
|
+
instrument(:clear) { 'all' }
|
82
|
+
end
|
83
|
+
end
|