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
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TypedCache
4
+ module Instrumenters
5
+ module Mixins
6
+ module NamespacedSingleton
7
+ class << self
8
+ # @rbs () -> Array[Class[Instrumenter & NamespacedSingleton]]
9
+ def all = @all ||= [] # rubocop:disable ThreadSafety
10
+
11
+ # @rbs (Class[Instrumenter & NamespacedSingleton]) -> void
12
+ def included(base)
13
+ base.singleton_class.class_eval do
14
+ alias private_new new
15
+ private(:private_new)
16
+ end
17
+
18
+ base.extend(ClassMethods)
19
+
20
+ all << base
21
+ end
22
+ end
23
+
24
+ # @rbs override
25
+ # @rbs () -> String
26
+ def namespace
27
+ @namespace
28
+ end
29
+
30
+ # @rbs (String | Namespace) -> void
31
+ def initialize(namespace)
32
+ @namespace = namespace.to_s
33
+ end
34
+
35
+ module ClassMethods
36
+ # @rbs (String | Namespace) -> class
37
+ def new(namespace: TypedCache.config.instrumentation.namespace)
38
+ namespace_cache.compute_if_absent(namespace.to_s) { private_new(namespace) }
39
+ end
40
+
41
+ # @rbs (String) -> maybe[class]
42
+ def get(namespace)
43
+ namespace_cache.get(namespace.to_s)
44
+ end
45
+
46
+ # @rbs () -> void
47
+ def clear_namespace_cache = namespace_cache.clear
48
+
49
+ # @rbs () -> Concurrent::Map[String, Class[Instrumenter & NamespacedSingleton]]
50
+ def namespace_cache = @namespace_cache ||= Concurrent::Map.new # rubocop:disable ThreadSafety
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TypedCache
4
+ module Instrumenters
5
+ module Mixins
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/monitor'
4
+
5
+ module TypedCache
6
+ module Instrumenters
7
+ class Monitor
8
+ include Instrumenter
9
+ include Mixins::NamespacedSingleton
10
+
11
+ # @rbs override
12
+ #: [R] (String, String, **untyped) { -> R } -> R
13
+ def instrument(operation, key, **payload, &block)
14
+ return yield unless enabled?
15
+
16
+ payload = build_payload(operation, key, **payload)
17
+ Dry::Monitor::Notifications.instrument(event_name(operation), payload, &block)
18
+ end
19
+
20
+ # @rbs override
21
+ # @rbs (String, **top) { (event) -> void } -> void
22
+ def subscribe(operation, **filters, &block)
23
+ Dry::Monitor::Notifications.subscribe(event_name(operation), **filters, &block)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'singleton'
4
+
5
+ module TypedCache
6
+ module Instrumenters
7
+ # A no-op implementation used when instrumentation is disabled.
8
+ # It fulfils the Instrumenter contract but simply yields.
9
+ class Null
10
+ include Instrumenter
11
+ include Mixins::NamespacedSingleton
12
+
13
+ # @rbs override
14
+ # [R] (String, String, **untyped) { -> R } -> R
15
+ def instrument(_operation, _key, **_payload)
16
+ yield
17
+ end
18
+
19
+ # @rbs override
20
+ # @rbs (String, **top) { (event) -> void } -> void
21
+ def subscribe(_event_name, **_filters, &_block)
22
+ # no-op
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'typed_cache/instrumenters/mixins'
4
+ require 'typed_cache/instrumenters/mixins/namespaced_singleton'
5
+
6
+ module TypedCache
7
+ module Instrumenters
8
+ autoload :ActiveSupport, 'typed_cache/instrumenters/active_support'
9
+ autoload :Monitor, 'typed_cache/instrumenters/monitor'
10
+ autoload :Null, 'typed_cache/instrumenters/null'
11
+
12
+ # @api private
13
+ # Registry mapping symbols to instrumenter classes. We can't reuse the generic
14
+ # Registry class directly because many instrumenters mix in `Singleton`,
15
+ # making `.new` inaccessible. Instead we implement a thin facade that
16
+ # returns either the singleton instance (preferred) or a fresh instance.
17
+ REGISTRY = Registry.new('instrumenter', {
18
+ dry: Monitor,
19
+ rails: ActiveSupport,
20
+ default: Null,
21
+ null: Null,
22
+ }) #: Registry[Symbol, Class[Instrumenter]]
23
+
24
+ class << self
25
+ extend Forwardable
26
+
27
+ # @api private
28
+ # @rbs () -> Registry[Symbol, Class[Instrumenter]]
29
+ def registry = REGISTRY
30
+
31
+ # @rbs! def register: (Symbol, Class[Instrumenter]) -> void
32
+ # @rbs! def resolve: (Symbol, **untyped) -> either[Error, Instrumenter]
33
+ # @rbs! def available: () -> Array[Symbol]
34
+ # @rbs! def registered?: (Symbol) -> Boolean
35
+
36
+ def_delegators :registry, :resolve, :available, :registered?, :register
37
+ end
38
+ end
39
+ end
@@ -33,6 +33,8 @@ module TypedCache
33
33
  # def nothing?: -> bool
34
34
  # def map: [T] () { (V) -> T } -> maybe[T]
35
35
  # def bind: [T] () { (V) -> maybe[T] } -> maybe[T]
36
+ # def value_or: [T] (T) -> T
37
+ # def value_or_raise!: -> V
36
38
  # alias flat_map bind
37
39
  # end
38
40
 
@@ -62,6 +64,14 @@ module TypedCache
62
64
  #: [T] () { (V) -> maybe[T] } -> maybe[T]
63
65
  def bind(&) = yield(value)
64
66
 
67
+ # @rbs override
68
+ #: [T] (T) -> T
69
+ def value_or(default) = value
70
+
71
+ # @rbs override
72
+ #: -> V
73
+ def value_or_raise! = value
74
+
65
75
  alias flat_map bind
66
76
 
67
77
  #: (Array[top]) -> ({ value: V })
@@ -88,5 +98,13 @@ module TypedCache
88
98
  #: [T] () { (V) -> maybe[T] } -> maybe[T]
89
99
  def bind(&) = self
90
100
  alias flat_map bind
101
+
102
+ # @rbs override
103
+ #: [T] (T) -> T
104
+ def value_or(default) = default
105
+
106
+ # @rbs override
107
+ #: -> V
108
+ def value_or_raise! = raise TypedCache::TypeError, 'Nothing has no value'
91
109
  end
92
110
  end
@@ -20,16 +20,17 @@ module TypedCache
20
20
  # Returns a new Namespace instance rooted at the given namespace string.
21
21
  #
22
22
  # @param namespace [String] the root namespace
23
+ # @param namespaces [Array<String>] additional namespaces to join
23
24
  # @return [Namespace] a new Namespace instance at the given root
24
25
  #
25
26
  # Example:
26
- # TypedCache::Namespace.at("users") # => #<TypedCache::Namespace namespace=users>
27
+ # TypedCache::Namespace.at("users", "sessions") # => #<TypedCache::Namespace namespace=users:sessions>
27
28
  #
28
29
  # The returned Namespace can be further nested or used to generate cache keys.
29
30
  #
30
- # @rbs (String) -> Namespace
31
- def at(namespace)
32
- root.nested(namespace)
31
+ # @rbs (String, *String) -> Namespace
32
+ def at(namespace, *namespaces)
33
+ root.join(namespace, *namespaces)
33
34
  end
34
35
 
35
36
  # Returns the root Namespace instance (with an empty namespace).
@@ -79,7 +80,26 @@ module TypedCache
79
80
  def nested(namespace, &key_factory)
80
81
  key_factory ||= @key_factory
81
82
 
82
- self.class.new("#{@namespace}:#{namespace}", &key_factory)
83
+ self.class.new([@namespace, namespace].join(delimiter), &key_factory)
84
+ end
85
+
86
+ # Creates a new namespace by joining the current namespace with the given namespaces.
87
+ #
88
+ # @param namespaces [Array<String>] the namespaces to join
89
+ # @param key_factory [Proc, nil] optional custom key factory for the joined namespace
90
+ # @return [Namespace] a new Namespace instance with the combined namespace
91
+ #
92
+ # Example:
93
+ # ns = Namespace.at("users")
94
+ # ns.join("sessions", "admin") # => #<TypedCache::Namespace namespace=users:sessions:admin>
95
+ #
96
+ # If no key_factory is provided, the parent's key factory is inherited.
97
+ #
98
+ # @rbs (*String) ?{ (Namespace, String) -> CacheKey } -> Namespace
99
+ def join(*namespaces, &key_factory)
100
+ key_factory ||= @key_factory
101
+
102
+ self.class.new([@namespace, *namespaces].join(delimiter), &key_factory)
83
103
  end
84
104
 
85
105
  # Returns the parent namespace by removing the last namespace segment.
@@ -96,7 +116,7 @@ module TypedCache
96
116
  def parent_namespace
97
117
  return self if @namespace.empty?
98
118
 
99
- case pathsep_idx = @namespace.rindex(':')
119
+ case pathsep_idx = @namespace.rindex(delimiter)
100
120
  when nil
101
121
  self.class.root
102
122
  else
@@ -158,5 +178,12 @@ module TypedCache
158
178
  end
159
179
 
160
180
  alias eql? ==
181
+
182
+ private
183
+
184
+ # @rbs (String) -> String
185
+ def delimiter
186
+ TypedCache.config.cache_delimiter
187
+ end
161
188
  end
162
189
  end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TypedCache
4
+ class Railtie < ::Rails::Railtie
5
+ config.to_prepare do
6
+ # Set the default instrumenter to Rails
7
+ ::TypedCache.configure do |config|
8
+ config.instrumentation.instrumenter = :rails
9
+ end
10
+
11
+ # Register the ActiveSupport backend
12
+ ::TypedCache.backends.register(:active_support, ::TypedCache::Instrumenters::ActiveSupport)
13
+ end
14
+ end
15
+ end
@@ -10,6 +10,17 @@ module TypedCache
10
10
  @registry = defaults.dup
11
11
  end
12
12
 
13
+ # @rbs (Registry[T]) -> Registry[T]
14
+ def initialize_copy(other)
15
+ super
16
+ @registry = other.registry.dup
17
+ end
18
+
19
+ # @rbs () -> void
20
+ def clear
21
+ @registry.clear
22
+ end
23
+
13
24
  # @rbs (Symbol, *untyped, **untyped) -> either[Error, T]
14
25
  def resolve(key, *, **, &)
15
26
  klass = @registry[key]
@@ -51,5 +62,9 @@ module TypedCache
51
62
  def registered?(key)
52
63
  @registry.key?(key)
53
64
  end
65
+
66
+ protected
67
+
68
+ attr_reader :registry #: Hash[Symbol, Class[T]]
54
69
  end
55
70
  end
@@ -4,12 +4,14 @@ module TypedCache
4
4
  # Immutable snapshot of a cached value with metadata about its source and age
5
5
  # @rbs generic V
6
6
  class Snapshot
7
+ attr_reader :key #: CacheKey
7
8
  attr_reader :value #: V
8
9
  attr_reader :retrieved_at #: Time
9
10
  attr_reader :source #: Symbol
10
11
 
11
- #: (V, source: Symbol, retrieved_at: Time) -> void
12
- def initialize(value, source:, retrieved_at: Time.now)
12
+ #: (CacheKey, V, source: Symbol, retrieved_at: Time) -> void
13
+ def initialize(key, value, source:, retrieved_at: Time.now)
14
+ @key = key
13
15
  @value = value
14
16
  @retrieved_at = retrieved_at
15
17
  @source = source
@@ -43,29 +45,35 @@ module TypedCache
43
45
  #: [R] () { (V) -> R } -> Snapshot[R]
44
46
  def map(&block)
45
47
  new_value = yield(value)
46
- Snapshot.new(new_value, source: source, retrieved_at: retrieved_at)
48
+ Snapshot.new(key, new_value, source: source, retrieved_at: retrieved_at)
47
49
  end
48
50
 
49
51
  # Bind over the value with Either error handling
50
52
  #: [R] () { (V) -> either[Error, R] } -> either[Error, Snapshot[R]]
51
53
  def bind(&block)
52
54
  result = yield(value)
53
- result.map { |new_value| Snapshot.new(new_value, source: source, retrieved_at: retrieved_at) }
55
+ result.map { |new_value| Snapshot.new(key, new_value, source: source, retrieved_at: retrieved_at) }
54
56
  end
55
57
 
56
58
  alias flat_map bind
57
59
 
58
60
  class << self
61
+ # Creates a snapshot for a cached value
62
+ #: [V] (CacheKey, V) -> Snapshot[V]
63
+ def cached(key, value)
64
+ new(key, value, source: :cache)
65
+ end
66
+
59
67
  # Creates a snapshot for a computed value
60
- #: [V] (V) -> Snapshot[V]
61
- def computed(value)
62
- new(value, source: :computed)
68
+ #: [V] (CacheKey, V) -> Snapshot[V]
69
+ def computed(key, value)
70
+ new(key, value, source: :computed)
63
71
  end
64
72
 
65
73
  # Creates a snapshot for an updated value
66
- #: [V] (V) -> Snapshot[V]
67
- def updated(value)
68
- new(value, source: :updated)
74
+ #: [V] (CacheKey, V) -> Snapshot[V]
75
+ def updated(key, value)
76
+ new(key, value, source: :updated)
69
77
  end
70
78
  end
71
79
  end
@@ -17,13 +17,16 @@ module TypedCache
17
17
 
18
18
  # @rbs!
19
19
  # interface _Store[V]
20
- # def get: (cache_key) -> either[Error, Snapshot[V]]
20
+ # def read: (cache_key) -> either[Error, Snapshot[V]]
21
+ # def read_all: (Array[cache_key]) -> either[Error, Array[Snapshot[V]]]
21
22
  # def ref: (cache_key) -> CacheRef[V]
22
- # def set: (cache_key, V) -> either[Error, Snapshot[V]]
23
+ # def write: (cache_key, V) -> either[Error, Snapshot[V]]
24
+ # def write_all: (Hash[cache_key, V]) -> either[Error, Array[Snapshot[V]]]
23
25
  # def delete: (cache_key) -> either[Error, Snapshot[V]]
24
26
  # def key?: (cache_key) -> bool
25
27
  # def clear: () -> maybe[Error]
26
- # def fetch: (cache_key) { () -> V } -> either[Error, Snapshot[V]]
28
+ # def fetch: (cache_key) { () -> V? } -> either[Error, Snapshot[maybe[V]]]
29
+ # def fetch_all: (Array[cache_key]) { (CacheKey) -> V? } -> either[Error, Array[Snapshot[V]]]
27
30
  # def namespace: () -> Namespace
28
31
  # def with_namespace: (Namespace) -> Store[V]
29
32
  # def store_type: () -> String
@@ -43,8 +46,17 @@ module TypedCache
43
46
 
44
47
  # Retrieves a value from the cache
45
48
  # @rbs (cache_key) -> either[Error, Snapshot[V]]
46
- def get(key)
47
- Either.left(NotImplementedError.new("#{self.class} must implement #get"))
49
+ def read(key)
50
+ Either.left(NotImplementedError.new("#{self.class} must implement #read"))
51
+ end
52
+
53
+ # @rbs (Array[cache_key]) -> either[Error, Hash[cache_key, Snapshot[V]]]
54
+ def read_all(keys)
55
+ keys.map { |key| read(key) }.reduce(Either.right({})) do |acc, result|
56
+ acc.bind do |values|
57
+ result.map { |value| values.merge(value.key => value) }
58
+ end
59
+ end
48
60
  end
49
61
 
50
62
  # Retrieves a cache reference for a key
@@ -55,8 +67,17 @@ module TypedCache
55
67
 
56
68
  # Stores a value in the cache
57
69
  # @rbs (cache_key, V) -> either[Error, Snapshot[V]]
58
- def set(key, value)
59
- Either.left(NotImplementedError.new("#{self.class} must implement #set"))
70
+ def write(key, value)
71
+ Either.left(NotImplementedError.new("#{self.class} must implement #write"))
72
+ end
73
+
74
+ # @rbs (Hash[cache_key, V]) -> either[Error, Hash[cache_key, Snapshot[V]]]
75
+ def write_all(values)
76
+ values.map { |key, value| write(key, value) }.reduce(Either.right({})) do |acc, result|
77
+ acc.bind do |values|
78
+ result.map { |value| values.merge(value.key => value) }
79
+ end
80
+ end
60
81
  end
61
82
 
62
83
  # Removes a value from the cache, returning the removed value
@@ -78,26 +99,39 @@ module TypedCache
78
99
  end
79
100
 
80
101
  # Fetches a value from cache, computing and storing it if not found
81
- # This is an atomic operation that combines get and set
102
+ # This is an atomic operation that combines read and write
82
103
  # @rbs (cache_key) { () -> V } -> either[Error, Snapshot[V]]
83
104
  def fetch(key, &block)
84
- # Default implementation using get/set pattern
85
- get_result = get(key)
86
- return get_result if get_result.right?
105
+ # Default implementation using read/write pattern
106
+ read_result = read(key)
107
+ return read_result if read_result.right?
87
108
 
88
109
  # Only proceed if it's a cache miss
89
- return get_result unless get_result.error.is_a?(CacheMissError)
110
+ return read_result unless read_result.error.is_a?(CacheMissError)
90
111
 
91
112
  # Compute and store new value
92
113
  begin
93
114
  computed_value = yield
94
- set(key, computed_value)
95
- Either.right(Snapshot.computed(computed_value))
115
+ write(key, computed_value)
116
+ Either.right(Snapshot.computed(key, computed_value))
96
117
  rescue => e
97
118
  Either.left(StoreError.new(:fetch, key, "Failed to compute value for key '#{key}': #{e.message}", e))
98
119
  end
99
120
  end
100
121
 
122
+ # @rbs (Array[cache_key]) { (CacheKey) -> V } -> either[Error, Array[Snapshot[V]]]
123
+ def fetch_all(keys, &block)
124
+ keys = keys.map { |key| namespaced_key(key) }
125
+ keys.reduce(Either.right([])) do |acc, key|
126
+ acc.bind do |values|
127
+ fetch(key) { yield(key) }.map { |value| values + [value] }
128
+ end
129
+ end
130
+ end
131
+
132
+ # @rbs () -> Instrumenter
133
+ def instrumenter = Instrumenters::Null.instance
134
+
101
135
  # Returns the namespace for this store (for instrumentation/debugging)
102
136
  # @rbs () -> Namespace
103
137
  def namespace
@@ -106,11 +140,12 @@ module TypedCache
106
140
 
107
141
  # Accepts a String segment or a fully-formed Namespace and returns a cloned
108
142
  # store scoped to that namespace.
109
- # @rbs (Namespace | String) -> Store[V]
143
+ #: (Namespace | String | Array[String]) -> Store[V]
110
144
  def with_namespace(ns)
111
145
  new_namespace =
112
146
  case ns
113
147
  when Namespace then ns
148
+ when Array then namespace.join(*ns)
114
149
  else
115
150
  # treat as nested segment under the current namespace
116
151
  namespace.nested(ns.to_s)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TypedCache
4
- VERSION = '0.1.1'
4
+ VERSION = '0.3.0'
5
5
  end
data/lib/typed_cache.rb CHANGED
@@ -1,18 +1,35 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'zeitwerk'
4
- require 'dry-configurable'
5
- require 'concurrent-ruby'
3
+ require 'dry/configurable'
4
+ require 'dry/struct'
5
+ require 'dry/types'
6
6
 
7
- # Load core registry before Zeitwerk
8
- require_relative 'typed_cache/registry'
9
- require_relative 'typed_cache/errors'
7
+ require 'typed_cache/either'
8
+ require 'typed_cache/maybe'
10
9
 
11
- Zeitwerk::Loader.for_gem.setup
10
+ require 'typed_cache/cache_key'
11
+ require 'typed_cache/errors'
12
+ require 'typed_cache/namespace'
13
+ require 'typed_cache/registry'
14
+ require 'typed_cache/clock'
15
+
16
+ require 'typed_cache/store'
17
+ require 'typed_cache/instrumenter'
18
+ require 'typed_cache/backend'
19
+ require 'typed_cache/decorator'
20
+
21
+ require 'typed_cache/snapshot'
22
+ require 'typed_cache/cache_ref'
23
+
24
+ require 'typed_cache/cache_builder'
12
25
 
13
26
  module TypedCache
14
27
  extend Dry::Configurable
15
28
 
29
+ autoload :Backends, 'typed_cache/backends'
30
+ autoload :Decorators, 'typed_cache/decorators'
31
+ autoload :Instrumenters, 'typed_cache/instrumenters'
32
+
16
33
  # @rbs!
17
34
  # interface _TypedCacheInstrumentationConfig
18
35
  # def enabled: -> bool
@@ -22,6 +39,7 @@ module TypedCache
22
39
  # @rbs!
23
40
  # interface _TypedCacheConfig
24
41
  # def default_namespace: -> String
42
+ # def cache_delimiter: -> String
25
43
  # def instrumentation: -> _TypedCacheInstrumentationConfig
26
44
  # end
27
45
 
@@ -29,10 +47,12 @@ module TypedCache
29
47
 
30
48
  # Configuration
31
49
  setting :default_namespace, default: 'typed_cache'
50
+ setting :cache_delimiter, default: ':'
32
51
 
33
52
  setting :instrumentation do
34
53
  setting :enabled, default: false
35
54
  setting :namespace, default: 'typed_cache'
55
+ setting :instrumenter, default: :default
36
56
  end
37
57
 
38
58
  class << self
@@ -44,15 +64,15 @@ module TypedCache
44
64
 
45
65
  # @rbs! def config: -> _TypedCacheConfig
46
66
 
47
- # @rbs () -> singleton(Instrumentation)
48
- def instrumentation
49
- Instrumentation
50
- end
51
-
52
- # @rbs () -> Registry[backend[untyped]]
67
+ # @rbs () -> singleton(Backends)
53
68
  def backends = Backends
54
69
 
55
- # @rbs () -> Register[decorator[untyped]]
70
+ # @rbs () -> singleton(Decorators)
56
71
  def decorators = Decorators
72
+
73
+ # @rbs () -> singleton(Instrumenters)
74
+ def instrumenters = Instrumenters
57
75
  end
58
76
  end
77
+
78
+ require 'typed_cache/railtie' if defined?(Rails::Railtie)
@@ -0,0 +1,9 @@
1
+ # typed: strict
2
+
3
+ module TypedCache
4
+ module Backend
5
+ extend T::Generic
6
+ include ::TypedCache::Store
7
+ CachedType = type_member
8
+ end
9
+ end
@@ -0,0 +1,13 @@
1
+ # typed: strict
2
+
3
+ module TypedCache
4
+ module Backends
5
+ module ActiveSupport
6
+ extend T::Generic
7
+
8
+ include ::TypedCache::Backend
9
+
10
+ CachedType = type_member
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # typed: strict
2
+
3
+ module TypedCache
4
+ module Backends
5
+ module Memory
6
+ extend T::Generic
7
+
8
+ include ::TypedCache::Backend
9
+
10
+ CachedType = type_member
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,19 @@
1
+ # typed: strict
2
+
3
+ module TypedCache
4
+ module Backends
5
+ class << self
6
+ sig { params(name: Symbol, klass: T::Class[::TypedCache::Backend[T.anything]]).returns(T.self_type) }
7
+ def register(name, klass); end
8
+
9
+ sig { params(name: Symbol, args: T::Array[T.anything], options: T::Hash[Symbol, T.anything]).returns(T.self_type) }
10
+ def resolve(name, *args, **options); end
11
+
12
+ sig { returns(T::Array[T::Class[::TypedCache::Backend[T.anything]]]) }
13
+ def available; end
14
+
15
+ sig { params(name: Symbol).returns(T::Boolean) }
16
+ def registered?(name); end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,23 @@
1
+ # typed: strict
2
+
3
+ module TypedCache
4
+ class CacheBuilder
5
+ Error = T.type_alias { TypedCache::Error }
6
+ Config = T.type_alias { ::TypedCache::Private::Configuration }
7
+ InstrumenterSource = T.type_alias { T.any(Symbol, ::TypedCache::Instrumenter) }
8
+
9
+ private_constant :Config, :Error, :InstrumenterSource
10
+
11
+ sig { params(namespace: ::TypedCache::Namespace).returns(::TypedCache::Either[Error, ::TypedCache::Store[T.anything]]) }
12
+ def build(namespace = T.unsafe(nil)); end
13
+
14
+ sig { params(name: Symbol, args: T.untyped, options: T::Hash[Symbol, T.anything]).returns(T.self_type) }
15
+ def with_backend(name, *args, **options); end
16
+
17
+ sig { params(name: Symbol, options: T::Hash[Symbol, T.anything]).returns(T.self_type) }
18
+ def with_decorator(name, **options); end
19
+
20
+ sig { params(source: InstrumenterSource).returns(T.self_type) }
21
+ def with_instrumentation(source = T.unsafe(nil)); end
22
+ end
23
+ end