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
@@ -1,8 +1,32 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'typed_cache/namespace'
|
4
|
+
require 'typed_cache/cache_key'
|
5
|
+
require 'typed_cache/errors'
|
6
|
+
|
7
|
+
require 'typed_cache/store'
|
8
|
+
require 'typed_cache/backends'
|
9
|
+
require 'typed_cache/decorators'
|
10
|
+
require 'typed_cache/instrumenters'
|
11
|
+
|
12
|
+
require 'dry/struct'
|
13
|
+
require 'dry/types'
|
14
|
+
|
3
15
|
module TypedCache
|
4
16
|
class CacheBuilder
|
5
17
|
# @rbs! type config = TypedCache::typed_cache_config
|
18
|
+
# @rbs! type instrumenter_source = :default | :dry | :rails | Instrumenter
|
19
|
+
|
20
|
+
class BackendConfig < Dry::Struct
|
21
|
+
attribute :name, Dry.Types::Symbol
|
22
|
+
attribute :args, Dry.Types::Array.of(Dry.Types::Any)
|
23
|
+
attribute :options, Dry.Types::Hash.map(Dry.Types::Symbol, Dry.Types::Any)
|
24
|
+
end
|
25
|
+
|
26
|
+
class DecoratorConfig < Dry::Struct
|
27
|
+
attribute :name, Dry.Types::Symbol
|
28
|
+
attribute :options, Dry.Types::Hash.map(Dry.Types::Symbol, Dry.Types::Any)
|
29
|
+
end
|
6
30
|
|
7
31
|
# @rbs (config, Registry[backend[untyped]], Registry[decorator[untyped]]) -> void
|
8
32
|
def initialize(config, backend_registry = Backends, decorator_registry = Decorators)
|
@@ -10,15 +34,13 @@ module TypedCache
|
|
10
34
|
@backend_registry = backend_registry
|
11
35
|
@decorator_registry = decorator_registry
|
12
36
|
|
13
|
-
@
|
14
|
-
@
|
15
|
-
@backend_options = {}
|
16
|
-
@decorators = []
|
37
|
+
@backend_config = nil
|
38
|
+
@decorator_configs = []
|
17
39
|
end
|
18
40
|
|
19
41
|
# Builds the cache - the only method that can fail
|
20
42
|
# @rbs (?Namespace) -> either[Error, Store[V]]
|
21
|
-
def build(namespace = Namespace.at(@config.
|
43
|
+
def build(namespace = Namespace.at(@config.instrumentation.namespace))
|
22
44
|
validate_and_build(namespace)
|
23
45
|
end
|
24
46
|
|
@@ -26,22 +48,22 @@ module TypedCache
|
|
26
48
|
# Invalid configurations are caught during build()
|
27
49
|
# @rbs (Symbol, *untyped, **untyped) -> self
|
28
50
|
def with_backend(name, *args, **options)
|
29
|
-
@
|
30
|
-
@backend_args = args
|
31
|
-
@backend_options = options
|
51
|
+
@backend_config = BackendConfig.new(name:, args:, options:)
|
32
52
|
self
|
33
53
|
end
|
34
54
|
|
35
55
|
# Adds an arbitrary decorator by registry key
|
36
56
|
# @rbs (Symbol) -> self
|
37
|
-
def with_decorator(name)
|
38
|
-
@
|
57
|
+
def with_decorator(name, **options)
|
58
|
+
@decorator_configs << DecoratorConfig.new(name:, options:)
|
39
59
|
self
|
40
60
|
end
|
41
61
|
|
42
|
-
#
|
43
|
-
|
44
|
-
|
62
|
+
# Adds instrumentation using the specified strategy.
|
63
|
+
# @rbs (instrumenter_source) -> either[Error, self]
|
64
|
+
def with_instrumentation(source = :default)
|
65
|
+
@instrumenter_source = source
|
66
|
+
self
|
45
67
|
end
|
46
68
|
|
47
69
|
private
|
@@ -49,29 +71,60 @@ module TypedCache
|
|
49
71
|
# @rbs (Namespace) -> either[Error, Store[V]]
|
50
72
|
def validate_and_build(namespace)
|
51
73
|
create_store(namespace).bind do |store|
|
52
|
-
apply_decorators(store)
|
74
|
+
apply_decorators(store).bind do |decorated_store|
|
75
|
+
apply_instrumentation(decorated_store)
|
76
|
+
end
|
53
77
|
end
|
54
78
|
end
|
55
79
|
|
56
80
|
# @rbs (Namespace) -> either[Error, Store[V]]
|
57
81
|
def create_store(namespace)
|
58
|
-
return Either.left(ArgumentError.new('Backend not configured')) unless @
|
82
|
+
return Either.left(ArgumentError.new('Backend not configured')) unless @backend_config
|
59
83
|
|
60
84
|
# Prepend namespace to the arguments for the backend constructor
|
61
|
-
@backend_registry.resolve(@
|
85
|
+
@backend_registry.resolve(@backend_config.name, namespace, *@backend_config.args, **@backend_config.options)
|
62
86
|
end
|
63
87
|
|
64
88
|
# @rbs (Store[V]) -> either[Error, Store[V]]
|
65
89
|
def apply_decorators(store)
|
66
|
-
return Either.right(store) if @
|
90
|
+
return Either.right(store) if @decorator_configs.empty?
|
67
91
|
|
68
|
-
@
|
92
|
+
names = @decorator_configs.map(&:name)
|
93
|
+
|
94
|
+
name_counts = names.tally
|
95
|
+
duplicates = name_counts.keys.select { |name| name_counts[name] > 1 }
|
96
|
+
return Either.left(ArgumentError.new("Duplicate decorator: #{duplicates.join(", ")}")) if duplicates.any?
|
97
|
+
|
98
|
+
@decorator_configs.reduce(Either.right(store)) do |result, decorator_config|
|
69
99
|
result.bind do |current_store|
|
70
|
-
@decorator_registry.resolve(
|
100
|
+
@decorator_registry.resolve(decorator_config.name, current_store, **decorator_config.options)
|
71
101
|
end
|
72
102
|
end
|
73
103
|
rescue => e
|
74
104
|
Either.left(StoreError.new(:decorator_application, 'decorator', "Failed to apply decorator: #{e.message}", e))
|
75
105
|
end
|
106
|
+
|
107
|
+
def apply_instrumentation(store)
|
108
|
+
return Either.right(store) unless @instrumenter_source
|
109
|
+
|
110
|
+
instrumenter =
|
111
|
+
case @instrumenter_source
|
112
|
+
when Symbol
|
113
|
+
Instrumenters.resolve(@instrumenter_source, namespace: @config.default_namespace)
|
114
|
+
when Instrumenter
|
115
|
+
Either.right(@instrumenter_source)
|
116
|
+
else
|
117
|
+
Either.left(TypedCache::TypeError.new(
|
118
|
+
':default | :dry | :rails | Instrumenter',
|
119
|
+
@instrumenter_source.class.name,
|
120
|
+
@instrumenter_source,
|
121
|
+
"Invalid instrumenter source: #{@instrumenter_source.inspect}",
|
122
|
+
))
|
123
|
+
end
|
124
|
+
|
125
|
+
instrumenter.bind do |i|
|
126
|
+
@decorator_registry.resolve(:instrumented, store, instrumenter: i)
|
127
|
+
end
|
128
|
+
end
|
76
129
|
end
|
77
130
|
end
|
@@ -1,5 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'forwardable'
|
4
|
+
require_relative 'namespace'
|
5
|
+
|
3
6
|
module TypedCache
|
4
7
|
class CacheKey
|
5
8
|
extend Forwardable
|
@@ -20,7 +23,7 @@ module TypedCache
|
|
20
23
|
|
21
24
|
# @rbs () -> String
|
22
25
|
def to_s
|
23
|
-
|
26
|
+
[@namespace.to_s, @key].join(delimiter)
|
24
27
|
end
|
25
28
|
|
26
29
|
alias cache_key to_s
|
@@ -41,5 +44,12 @@ module TypedCache
|
|
41
44
|
end
|
42
45
|
|
43
46
|
alias eql? ==
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
# @rbs (String) -> String
|
51
|
+
def delimiter
|
52
|
+
TypedCache.config.cache_delimiter
|
53
|
+
end
|
44
54
|
end
|
45
55
|
end
|
@@ -1,5 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative 'cache_key'
|
4
|
+
require_relative 'store'
|
5
|
+
require_relative 'snapshot'
|
6
|
+
|
3
7
|
module TypedCache
|
4
8
|
# A monadic wrapper for cached values that provides safe access with rich error context.
|
5
9
|
# All operations return Either[Error, Snapshot[V]] to provide detailed information about
|
@@ -18,14 +22,14 @@ module TypedCache
|
|
18
22
|
|
19
23
|
# Gets a value from the cache as a snapshot
|
20
24
|
#: -> either[Error, Snapshot[V]]
|
21
|
-
def
|
22
|
-
store.
|
25
|
+
def read
|
26
|
+
store.read(key)
|
23
27
|
end
|
24
28
|
|
25
29
|
# Sets a value in the cache and returns it as an updated snapshot
|
26
30
|
#: (V) -> either[Error, Snapshot[V]]
|
27
|
-
def
|
28
|
-
store.
|
31
|
+
def write(value)
|
32
|
+
store.write(key, value)
|
29
33
|
end
|
30
34
|
|
31
35
|
# Deletes the value from the cache and returns the deleted value as a snapshot
|
@@ -44,25 +48,25 @@ module TypedCache
|
|
44
48
|
# Checks if the cache contains a value for this key
|
45
49
|
#: -> bool
|
46
50
|
def present?
|
47
|
-
store.
|
51
|
+
store.read(key).right?
|
48
52
|
end
|
49
53
|
|
50
54
|
# Checks if the cache is empty for this key
|
51
55
|
#: -> bool
|
52
56
|
def empty?
|
53
|
-
store.
|
57
|
+
store.read(key).left?
|
54
58
|
end
|
55
59
|
|
56
60
|
# Maps over the cached value if it exists, preserving snapshot metadata
|
57
61
|
#: [R] () { (V) -> R } -> either[Error, Snapshot[R]]
|
58
62
|
def map(&block)
|
59
|
-
|
63
|
+
read.map { |snapshot| snapshot.map(&block) }
|
60
64
|
end
|
61
65
|
|
62
66
|
# Binds over the cached value, allowing for monadic composition with snapshots
|
63
67
|
#: [R] () { (V) -> either[Error, R] } -> either[Error, Snapshot[R]]
|
64
68
|
def bind(&block)
|
65
|
-
|
69
|
+
read.bind { |snapshot| snapshot.bind(&block) }
|
66
70
|
end
|
67
71
|
|
68
72
|
alias flat_map bind
|
@@ -71,9 +75,9 @@ module TypedCache
|
|
71
75
|
# Returns the updated value as a snapshot with source=:updated
|
72
76
|
#: () { (V) -> V } -> either[Error, Snapshot[V]]
|
73
77
|
def update(&block)
|
74
|
-
|
78
|
+
read.bind do |snapshot|
|
75
79
|
new_value = yield(snapshot.value)
|
76
|
-
|
80
|
+
write(new_value)
|
77
81
|
rescue => e
|
78
82
|
Either.left(StoreError.new(
|
79
83
|
:update,
|
@@ -87,7 +91,7 @@ module TypedCache
|
|
87
91
|
# Returns the cached value or a default if the cache is empty/errored
|
88
92
|
#: (V) -> V
|
89
93
|
def value_or(default)
|
90
|
-
|
94
|
+
read.fold(
|
91
95
|
->(_error) { default },
|
92
96
|
->(snapshot) { snapshot.value },
|
93
97
|
)
|
@@ -97,7 +101,7 @@ module TypedCache
|
|
97
101
|
# This provides a more functional approach than value_or
|
98
102
|
#: -> maybe[V]
|
99
103
|
def value_maybe
|
100
|
-
|
104
|
+
read.fold(
|
101
105
|
->(_error) { Maybe.none },
|
102
106
|
->(snapshot) { Maybe.some(snapshot.value) },
|
103
107
|
)
|
@@ -135,21 +139,21 @@ module TypedCache
|
|
135
139
|
end
|
136
140
|
|
137
141
|
# Pattern matching support for Either[Error, Snapshot[V]] results
|
138
|
-
#: [R] (
|
142
|
+
#: [R] (^(Error) -> R, ^(Snapshot[V]) -> R) -> R
|
139
143
|
def fold(left_fn, right_fn)
|
140
|
-
|
144
|
+
read.fold(left_fn, right_fn)
|
141
145
|
end
|
142
146
|
|
143
147
|
# Convenience method to work with the snapshot directly
|
144
148
|
#: [R] () { (Snapshot[V]) -> R } -> either[Error, R]
|
145
149
|
def with_snapshot(&block)
|
146
|
-
|
150
|
+
read.map(&block)
|
147
151
|
end
|
148
152
|
|
149
153
|
# Convenience method to work with just the value (losing snapshot context)
|
150
154
|
#: [R] () { (V) -> R } -> either[Error, R]
|
151
155
|
def with(&block)
|
152
|
-
|
156
|
+
read.map { |snapshot| yield(snapshot.value) }
|
153
157
|
end
|
154
158
|
end
|
155
159
|
end
|
data/lib/typed_cache/clock.rb
CHANGED
@@ -1,22 +1,39 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'dry/struct'
|
4
|
+
require 'dry/types'
|
5
|
+
|
3
6
|
module TypedCache
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
+
module Clock
|
8
|
+
# @rbs generic R
|
9
|
+
class Measured < Dry::Struct
|
10
|
+
# @rbs! def start: () -> Float
|
11
|
+
# @rbs! def end: () -> Float
|
12
|
+
# @rbs! def result: () -> [R]
|
13
|
+
|
14
|
+
attribute :start, Dry.Types::Float
|
15
|
+
attribute :end, Dry.Types::Float
|
16
|
+
attribute :result, Dry.Types.Instance(Object) #: [R]
|
17
|
+
|
18
|
+
# @rbs! def initialize: (start: Float, end: Float, result: [R]) -> void
|
19
|
+
|
20
|
+
#: -> Float
|
21
|
+
def duration
|
22
|
+
self.end - start
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
7
26
|
class << self
|
8
|
-
#
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
27
|
+
# @rbs [R]() { () -> R } -> Measured[R]
|
28
|
+
def measure(&)
|
29
|
+
start = now
|
30
|
+
result = yield
|
31
|
+
Measured.new(start:, end: now, result:)
|
32
|
+
end
|
33
|
+
|
13
34
|
# @rbs () -> Time
|
14
|
-
def
|
15
|
-
|
16
|
-
Time.current
|
17
|
-
else
|
18
|
-
Time.now
|
19
|
-
end
|
35
|
+
def now
|
36
|
+
Time.now
|
20
37
|
end
|
21
38
|
end
|
22
39
|
end
|
@@ -1,12 +1,37 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'forwardable'
|
4
|
+
|
3
5
|
module TypedCache
|
4
6
|
# Marker mixin for cache store decorators. A decorator behaves exactly like a
|
5
7
|
# Store but must accept another Store instance in its constructor.
|
6
8
|
# @rbs generic V
|
7
9
|
module Decorator
|
10
|
+
extend Forwardable
|
11
|
+
|
8
12
|
include Store #[V]
|
9
13
|
# @rbs! include Store::_Store[V]
|
10
14
|
# @rbs! include Store::_Decorator[V]
|
15
|
+
|
16
|
+
# @rbs!
|
17
|
+
# def store: -> Store[V]
|
18
|
+
|
19
|
+
Store.instance_methods(false).each do |method_name|
|
20
|
+
def_delegator :store, method_name
|
21
|
+
end
|
22
|
+
|
23
|
+
# @rbs override
|
24
|
+
#: (cache_key) -> either[Error, CacheRef[V]]
|
25
|
+
def ref(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
|
35
|
+
end
|
11
36
|
end
|
12
37
|
end
|
@@ -0,0 +1,92 @@
|
|
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 Decorators::Instrumented # rubocop:disable Style/ClassAndModuleChildren
|
8
|
+
include Decorator #[V]
|
9
|
+
|
10
|
+
extend Forwardable
|
11
|
+
|
12
|
+
attr_reader :store #: TypedCache::Store[V]
|
13
|
+
attr_reader :instrumenter #: Instrumenter
|
14
|
+
|
15
|
+
class << self
|
16
|
+
private
|
17
|
+
|
18
|
+
# @rbs (Symbol, ?operation: String) ?{ (*untyped, **untyped) -> String } -> void
|
19
|
+
def instrument(method_name, operation: method_name.to_s, &key_selector)
|
20
|
+
key_selector ||= ->(*_args, **_kwargs, &_block) { 'n/a' }
|
21
|
+
alias_prefix = method_name.to_s.delete('?!')
|
22
|
+
|
23
|
+
define_method(:"#{alias_prefix}_instrumentation_key", &key_selector)
|
24
|
+
|
25
|
+
class_eval(<<~RUBY, __FILE__, __LINE__ + 1)
|
26
|
+
def #{alias_prefix}_with_instrumentation(...)
|
27
|
+
return #{alias_prefix}_without_instrumentation(...) if @in_instrumentation
|
28
|
+
|
29
|
+
key = #{alias_prefix}_instrumentation_key(...)
|
30
|
+
instrumenter.instrument(:"#{operation}", key, store_type: store_type) do
|
31
|
+
@in_instrumentation = true
|
32
|
+
|
33
|
+
#{alias_prefix}_without_instrumentation(...)
|
34
|
+
ensure
|
35
|
+
@in_instrumentation = false
|
36
|
+
end
|
37
|
+
end
|
38
|
+
RUBY
|
39
|
+
|
40
|
+
alias_method(:"#{alias_prefix}_without_instrumentation", method_name)
|
41
|
+
alias_method(method_name, :"#{alias_prefix}_with_instrumentation")
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
#: (TypedCache::Store[V], instrumenter: Instrumenter) -> void
|
46
|
+
def initialize(store, instrumenter:)
|
47
|
+
@store = store
|
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
|
59
|
+
end
|
60
|
+
|
61
|
+
# @rbs override
|
62
|
+
#: -> String
|
63
|
+
def store_type
|
64
|
+
# Use polymorphism - delegate to the wrapped store
|
65
|
+
"instrumented(#{store.store_type})"
|
66
|
+
end
|
67
|
+
|
68
|
+
# Additional methods that might exist on the wrapped store
|
69
|
+
def respond_to_missing?(method_name, include_private = false)
|
70
|
+
store.respond_to?(method_name, include_private) || super
|
71
|
+
end
|
72
|
+
|
73
|
+
def method_missing(method_name, *args, &block)
|
74
|
+
if store.respond_to?(method_name)
|
75
|
+
store.send(method_name, *args, &block)
|
76
|
+
else
|
77
|
+
super
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# Instrument core operations with proper key extraction
|
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('_') }
|
86
|
+
instrument(:delete) { |key, *_| key }
|
87
|
+
instrument(:fetch) { |key, *_| key }
|
88
|
+
instrument(:fetch_all) { |keys, *_| keys.map(&:to_s).join('_') }
|
89
|
+
instrument(:key?) { |key, *_| key }
|
90
|
+
instrument(:clear) { 'all' }
|
91
|
+
end
|
92
|
+
end
|
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'typed_cache/registry'
|
4
|
+
|
3
5
|
module TypedCache
|
4
6
|
# Holds store decorators (e.g., instrumentation wrappers) that can be composed
|
5
7
|
# by the CacheBuilder. Decorators must conform to the same API as the wrapped
|
@@ -14,20 +16,22 @@ module TypedCache
|
|
14
16
|
# .build.value
|
15
17
|
#
|
16
18
|
module Decorators
|
19
|
+
autoload :Instrumented, 'typed_cache/decorators/instrumented'
|
20
|
+
|
21
|
+
# @api private
|
17
22
|
# Default decorator set – starts with instrumentation only, but this registry
|
18
23
|
# lets end-users register their own via `Decorators.register`.
|
19
24
|
REGISTRY = Registry.new('decorator', {
|
20
|
-
instrumented:
|
25
|
+
instrumented: Instrumented,
|
21
26
|
}).freeze
|
22
27
|
|
23
|
-
private_constant :REGISTRY
|
24
|
-
|
25
28
|
class << self
|
26
29
|
extend Forwardable
|
27
30
|
|
28
31
|
# Delegate common registry helpers
|
29
32
|
delegate [:resolve, :register, :available, :registered?] => :registry
|
30
33
|
|
34
|
+
# @api private
|
31
35
|
# @rbs () -> Registry[Store[untyped]]
|
32
36
|
def registry = REGISTRY
|
33
37
|
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))
|
data/lib/typed_cache/errors.rb
CHANGED
@@ -2,7 +2,13 @@
|
|
2
2
|
|
3
3
|
module TypedCache
|
4
4
|
# Base error class for TypedCache operations
|
5
|
-
class Error < StandardError
|
5
|
+
class Error < StandardError
|
6
|
+
# @rbs (*untyped) -> void
|
7
|
+
def initialize(*args)
|
8
|
+
super(*args)
|
9
|
+
set_backtrace(caller(2))
|
10
|
+
end
|
11
|
+
end
|
6
12
|
|
7
13
|
# Store operation errors (network, I/O, etc.)
|
8
14
|
class StoreError < Error
|
@@ -14,6 +20,8 @@ module TypedCache
|
|
14
20
|
@operation = operation
|
15
21
|
@key = key
|
16
22
|
@original_error = original_error
|
23
|
+
|
24
|
+
set_backtrace(original_error.backtrace) if original_error
|
17
25
|
end
|
18
26
|
|
19
27
|
# @rbs () -> String
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TypedCache
|
4
|
+
# Instrumenter for cache operations
|
5
|
+
module Instrumenter
|
6
|
+
# @rbs! type event = Dry::Events::Event | ActiveSupport::Notifications::Event
|
7
|
+
|
8
|
+
# @rbs [R](String, String, **untyped) { -> R } -> R
|
9
|
+
def instrument(event_name, key, **payload, &)
|
10
|
+
raise NotImplementedError, "#{self.class} must implement #instrument"
|
11
|
+
end
|
12
|
+
|
13
|
+
# @rbs (String, **untyped) { (event) -> void } -> void
|
14
|
+
def subscribe(event_name, **filters, &block)
|
15
|
+
raise NotImplementedError, "#{self.class} must implement #subscribe"
|
16
|
+
end
|
17
|
+
|
18
|
+
#: -> String
|
19
|
+
def namespace
|
20
|
+
config.namespace
|
21
|
+
end
|
22
|
+
|
23
|
+
# @rbs () -> bool
|
24
|
+
def enabled? = config.enabled
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
# @rbs (String, String, **untyped) -> Hash[Symbol, untyped]
|
29
|
+
def build_payload(operation, key, **payload)
|
30
|
+
{ namespace:, key:, operation: }.merge(payload)
|
31
|
+
end
|
32
|
+
|
33
|
+
# @rbs (String) -> String
|
34
|
+
def event_name(operation)
|
35
|
+
"#{namespace}.#{operation}"
|
36
|
+
end
|
37
|
+
|
38
|
+
# @rbs () -> TypedCache::_TypedCacheInstrumentationConfig
|
39
|
+
def config
|
40
|
+
TypedCache.config.instrumentation
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/notifications'
|
4
|
+
|
5
|
+
module TypedCache
|
6
|
+
module Instrumenters
|
7
|
+
# Instrumenter for ActiveSupport::Notifications
|
8
|
+
class ActiveSupport
|
9
|
+
include Instrumenter
|
10
|
+
include Mixins::NamespacedSingleton
|
11
|
+
|
12
|
+
# @rbs override
|
13
|
+
#: [R] (String, String, Hash[Symbol, untyped]) { -> R } -> R
|
14
|
+
def instrument(operation, key, **payload, &block)
|
15
|
+
return yield unless enabled?
|
16
|
+
|
17
|
+
payload = build_payload(operation, key, **payload)
|
18
|
+
::ActiveSupport::Notifications.instrument(event_name(operation), **payload, &block)
|
19
|
+
end
|
20
|
+
|
21
|
+
# @rbs override
|
22
|
+
# @rbs (String, **top) { (event) -> void } -> void
|
23
|
+
def subscribe(operation, **filters, &block)
|
24
|
+
::ActiveSupport::Notifications.monotonic_subscribe(event_name(operation), &block)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|