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.
Files changed (87) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/README.md +139 -20
  4. data/examples.md +140 -50
  5. data/lib/typed_cache/backends/active_support.rb +50 -5
  6. data/lib/typed_cache/backends/memory.rb +14 -11
  7. data/lib/typed_cache/backends.rb +6 -8
  8. data/lib/typed_cache/cache_builder.rb +72 -19
  9. data/lib/typed_cache/cache_key.rb +11 -1
  10. data/lib/typed_cache/cache_ref.rb +20 -16
  11. data/lib/typed_cache/clock.rb +31 -14
  12. data/lib/typed_cache/decorator.rb +25 -0
  13. data/lib/typed_cache/decorators/instrumented.rb +92 -0
  14. data/lib/typed_cache/decorators.rb +7 -3
  15. data/lib/typed_cache/either.rb +22 -0
  16. data/lib/typed_cache/errors.rb +9 -1
  17. data/lib/typed_cache/instrumenter.rb +43 -0
  18. data/lib/typed_cache/instrumenters/active_support.rb +28 -0
  19. data/lib/typed_cache/instrumenters/mixins/namespaced_singleton.rb +55 -0
  20. data/lib/typed_cache/instrumenters/mixins.rb +8 -0
  21. data/lib/typed_cache/instrumenters/monitor.rb +27 -0
  22. data/lib/typed_cache/instrumenters/null.rb +26 -0
  23. data/lib/typed_cache/instrumenters.rb +39 -0
  24. data/lib/typed_cache/maybe.rb +18 -0
  25. data/lib/typed_cache/namespace.rb +33 -6
  26. data/lib/typed_cache/railtie.rb +15 -0
  27. data/lib/typed_cache/registry.rb +15 -0
  28. data/lib/typed_cache/snapshot.rb +18 -10
  29. data/lib/typed_cache/store.rb +50 -15
  30. data/lib/typed_cache/version.rb +1 -1
  31. data/lib/typed_cache.rb +34 -14
  32. data/rbi/typed_cache/backend.rbi +9 -0
  33. data/rbi/typed_cache/backends/active_support.rbi +13 -0
  34. data/rbi/typed_cache/backends/memory.rbi +13 -0
  35. data/rbi/typed_cache/backends.rbi +19 -0
  36. data/rbi/typed_cache/cache_builder.rbi +23 -0
  37. data/rbi/typed_cache/cache_key.rbi +16 -0
  38. data/rbi/typed_cache/cache_ref.rbi +56 -0
  39. data/rbi/typed_cache/decorator.rbi +67 -0
  40. data/rbi/typed_cache/decorators/instrumented.rbi +13 -0
  41. data/rbi/typed_cache/decorators.rbi +19 -0
  42. data/rbi/typed_cache/either.rbi +122 -0
  43. data/rbi/typed_cache/errors.rbi +20 -0
  44. data/rbi/typed_cache/instrumenter.rbi +45 -0
  45. data/rbi/typed_cache/instrumenters/mixins/namedspaced_singleton.rbi +33 -0
  46. data/rbi/typed_cache/instrumenters.rbi +19 -0
  47. data/rbi/typed_cache/maybe.rbi +108 -0
  48. data/rbi/typed_cache/namespace.rbi +30 -0
  49. data/rbi/typed_cache/snapshot.rbi +54 -0
  50. data/rbi/typed_cache/store.rbi +71 -0
  51. data/rbi/typed_cache/version.rbi +5 -0
  52. data/rbi/typed_cache.rbi +49 -0
  53. data/sig/generated/typed_cache/backends/active_support.rbs +14 -2
  54. data/sig/generated/typed_cache/backends/memory.rbs +2 -2
  55. data/sig/generated/typed_cache/backends.rbs +2 -0
  56. data/sig/generated/typed_cache/cache_builder.rbs +13 -2
  57. data/sig/generated/typed_cache/cache_key.rbs +5 -0
  58. data/sig/generated/typed_cache/cache_ref.rbs +4 -4
  59. data/sig/generated/typed_cache/clock.rbs +19 -9
  60. data/sig/generated/typed_cache/decorator.rbs +12 -0
  61. data/sig/generated/typed_cache/decorators/instrumented.rbs +35 -0
  62. data/sig/generated/typed_cache/decorators.rbs +2 -0
  63. data/sig/generated/typed_cache/either.rbs +24 -0
  64. data/sig/generated/typed_cache/errors.rbs +2 -0
  65. data/sig/generated/typed_cache/instrumenter.rbs +31 -0
  66. data/sig/generated/typed_cache/instrumenters/active_support.rbs +20 -0
  67. data/sig/generated/typed_cache/instrumenters/mixins/namespaced_singleton.rbs +36 -0
  68. data/sig/generated/typed_cache/instrumenters/mixins.rbs +8 -0
  69. data/sig/generated/typed_cache/instrumenters/monitor.rbs +19 -0
  70. data/sig/generated/typed_cache/instrumenters/null.rbs +21 -0
  71. data/sig/generated/typed_cache/instrumenters.rbs +26 -0
  72. data/sig/generated/typed_cache/maybe.rbs +20 -0
  73. data/sig/generated/typed_cache/namespace.rbs +24 -3
  74. data/sig/generated/typed_cache/railtie.rbs +6 -0
  75. data/sig/generated/typed_cache/registry.rbs +8 -0
  76. data/sig/generated/typed_cache/snapshot.rbs +12 -6
  77. data/sig/generated/typed_cache/store/instrumented.rbs +2 -6
  78. data/sig/generated/typed_cache/store.rbs +26 -8
  79. data/sig/generated/typed_cache.rbs +8 -6
  80. data/typed_cache.gemspec +5 -4
  81. data.tar.gz.sig +0 -0
  82. metadata +48 -27
  83. metadata.gz.sig +0 -0
  84. data/lib/typed_cache/instrumentation.rb +0 -112
  85. data/lib/typed_cache/store/instrumented.rb +0 -83
  86. data/sig/generated/typed_cache/instrumentation.rbs +0 -30
  87. 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
- @backend_name = nil
14
- @backend_args = []
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.default_namespace))
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
- @backend_name = name
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
- @decorators << name
57
+ def with_decorator(name, **options)
58
+ @decorator_configs << DecoratorConfig.new(name:, options:)
39
59
  self
40
60
  end
41
61
 
42
- # @rbs () -> self
43
- def with_instrumentation
44
- with_decorator(:instrumented)
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 @backend_name
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(@backend_name, namespace, *@backend_args, **@backend_options)
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 @decorators.empty?
90
+ return Either.right(store) if @decorator_configs.empty?
67
91
 
68
- @decorators.reduce(Either.right(store)) do |result, decorator_name|
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(decorator_name, current_store)
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
- "#{@namespace}:#{@key}"
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 get
22
- store.get(key)
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 set(value)
28
- store.set(key, value)
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.get(key).right?
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.get(key).left?
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
- get.map { |snapshot| snapshot.map(&block) }
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
- get.bind { |snapshot| snapshot.bind(&block) }
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
- get.bind do |snapshot|
78
+ read.bind do |snapshot|
75
79
  new_value = yield(snapshot.value)
76
- set(new_value)
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
- get.fold(
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
- get.fold(
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] () { (Error) -> R } () { (Snapshot[V]) -> R } -> R
142
+ #: [R] (^(Error) -> R, ^(Snapshot[V]) -> R) -> R
139
143
  def fold(left_fn, right_fn)
140
- get.fold(left_fn, right_fn)
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
- get.map(&block)
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
- get.map { |snapshot| yield(snapshot.value) }
156
+ read.map { |snapshot| yield(snapshot.value) }
153
157
  end
154
158
  end
155
159
  end
@@ -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
- # A simple, testable wrapper around Time to provide a consistent way of
5
- # getting the current time, respecting ActiveSupport's time zone when available.
6
- class Clock
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
- # Retrieves the current time. If ActiveSupport's `Time.current` is
9
- # available, it will be used to respect the configured timezone. Otherwise,
10
- # it falls back to the system's `Time.now`.
11
- #
12
- # @return [Time] The current time.
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 moment
15
- if Time.respond_to?(:current)
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: Store::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
@@ -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))
@@ -2,7 +2,13 @@
2
2
 
3
3
  module TypedCache
4
4
  # Base error class for TypedCache operations
5
- class Error < StandardError; end
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