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,152 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TypedCache
|
4
|
+
# @rbs!
|
5
|
+
#
|
6
|
+
|
7
|
+
# Generic interface for type-safe cache storage implementations
|
8
|
+
# All stores are assumed to handle namespacing internally
|
9
|
+
#
|
10
|
+
# This interface follows the Command-Query Separation principle:
|
11
|
+
# - Commands (set, delete, clear) perform actions and may return results
|
12
|
+
# - Queries (get, key?, fetch) ask questions without side effects
|
13
|
+
#
|
14
|
+
# @rbs generic V
|
15
|
+
module Store
|
16
|
+
# @rbs! type cache_key = String | CacheKey
|
17
|
+
|
18
|
+
# @rbs!
|
19
|
+
# interface _Store[V]
|
20
|
+
# def get: (cache_key) -> either[Error, Snapshot[V]]
|
21
|
+
# def ref: (cache_key) -> CacheRef[V]
|
22
|
+
# def set: (cache_key, V) -> either[Error, Snapshot[V]]
|
23
|
+
# def delete: (cache_key) -> either[Error, Snapshot[V]]
|
24
|
+
# def key?: (cache_key) -> bool
|
25
|
+
# def clear: () -> maybe[Error]
|
26
|
+
# def fetch: (cache_key) { () -> V } -> either[Error, Snapshot[V]]
|
27
|
+
# def namespace: () -> Namespace
|
28
|
+
# def with_namespace: (Namespace) -> Store[V]
|
29
|
+
# def store_type: () -> String
|
30
|
+
# end
|
31
|
+
# include _Store[V]
|
32
|
+
|
33
|
+
# @rbs!
|
34
|
+
# interface _Decorator[V]
|
35
|
+
# def initialize: (Store[V]) -> void
|
36
|
+
# end
|
37
|
+
|
38
|
+
# @rbs (Store[V]) -> void
|
39
|
+
def initialize_copy(other)
|
40
|
+
super
|
41
|
+
@namespace = other.namespace
|
42
|
+
end
|
43
|
+
|
44
|
+
# Retrieves a value from the cache
|
45
|
+
# @rbs (cache_key) -> either[Error, Snapshot[V]]
|
46
|
+
def get(key)
|
47
|
+
Either.left(NotImplementedError.new("#{self.class} must implement #get"))
|
48
|
+
end
|
49
|
+
|
50
|
+
# Retrieves a cache reference for a key
|
51
|
+
# @rbs (cache_key) -> CacheRef[V]
|
52
|
+
def ref(key)
|
53
|
+
CacheRef.new(self, namespaced_key(key))
|
54
|
+
end
|
55
|
+
|
56
|
+
# Stores a value in the cache
|
57
|
+
# @rbs (cache_key, V) -> either[Error, Snapshot[V]]
|
58
|
+
def set(key, value)
|
59
|
+
Either.left(NotImplementedError.new("#{self.class} must implement #set"))
|
60
|
+
end
|
61
|
+
|
62
|
+
# Removes a value from the cache, returning the removed value
|
63
|
+
# @rbs (cache_key) -> either[Error, Snapshot[V]]
|
64
|
+
def delete(key)
|
65
|
+
Either.left(NotImplementedError.new("#{self.class} must implement #delete"))
|
66
|
+
end
|
67
|
+
|
68
|
+
# Checks if a key exists in the cache (query operation)
|
69
|
+
# @rbs (cache_key) -> bool
|
70
|
+
def key?(key)
|
71
|
+
false # Safe default - assume key doesn't exist
|
72
|
+
end
|
73
|
+
|
74
|
+
# Clears all values from the cache namespace (command operation)
|
75
|
+
# @rbs () -> maybe[Error]
|
76
|
+
def clear
|
77
|
+
Maybe.some(NotImplementedError.new("#{self.class} does not implement #clear"))
|
78
|
+
end
|
79
|
+
|
80
|
+
# Fetches a value from cache, computing and storing it if not found
|
81
|
+
# This is an atomic operation that combines get and set
|
82
|
+
# @rbs (cache_key) { () -> V } -> either[Error, Snapshot[V]]
|
83
|
+
def fetch(key, &block)
|
84
|
+
# Default implementation using get/set pattern
|
85
|
+
get_result = get(key)
|
86
|
+
return get_result if get_result.right?
|
87
|
+
|
88
|
+
# Only proceed if it's a cache miss
|
89
|
+
return get_result unless get_result.error.is_a?(CacheMissError)
|
90
|
+
|
91
|
+
# Compute and store new value
|
92
|
+
begin
|
93
|
+
computed_value = yield
|
94
|
+
set(key, computed_value)
|
95
|
+
Either.right(Snapshot.computed(computed_value))
|
96
|
+
rescue => e
|
97
|
+
Either.left(StoreError.new(:fetch, key, "Failed to compute value for key '#{key}': #{e.message}", e))
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Returns the namespace for this store (for instrumentation/debugging)
|
102
|
+
# @rbs () -> Namespace
|
103
|
+
def namespace
|
104
|
+
raise NotImplementedError, "#{self.class} must implement #namespace"
|
105
|
+
end
|
106
|
+
|
107
|
+
# Accepts a String segment or a fully-formed Namespace and returns a cloned
|
108
|
+
# store scoped to that namespace.
|
109
|
+
# @rbs (Namespace | String) -> Store[V]
|
110
|
+
def with_namespace(ns)
|
111
|
+
new_namespace =
|
112
|
+
case ns
|
113
|
+
when Namespace then ns
|
114
|
+
else
|
115
|
+
# treat as nested segment under the current namespace
|
116
|
+
namespace.nested(ns.to_s)
|
117
|
+
end
|
118
|
+
|
119
|
+
clone.tap { |store| store.namespace = new_namespace }
|
120
|
+
end
|
121
|
+
|
122
|
+
# Returns the store type identifier for instrumentation/debugging
|
123
|
+
# @rbs () -> String
|
124
|
+
def store_type
|
125
|
+
snake_case(self.class.name.split('::').last)
|
126
|
+
end
|
127
|
+
|
128
|
+
protected
|
129
|
+
|
130
|
+
attr_writer :namespace #: Namespace
|
131
|
+
|
132
|
+
private
|
133
|
+
|
134
|
+
#: (String) -> String
|
135
|
+
def snake_case(string)
|
136
|
+
string
|
137
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
138
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
139
|
+
.tr('-', '_')
|
140
|
+
.downcase
|
141
|
+
end
|
142
|
+
|
143
|
+
#: (cache_key) -> CacheKey
|
144
|
+
def namespaced_key(key)
|
145
|
+
key.is_a?(CacheKey) ? key : CacheKey.new(namespace, key)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
# @rbs! type backend[V] = Store::_Store[V]
|
150
|
+
# @rbs! type decorator[V] = backend[V] & Store::_Decorator[V]
|
151
|
+
# @rbs! type store[V] = backend[V] | decorator[V]
|
152
|
+
end
|
data/lib/typed_cache.rb
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'zeitwerk'
|
4
|
+
require 'dry-configurable'
|
5
|
+
require 'concurrent-ruby'
|
6
|
+
|
7
|
+
# Load core registry before Zeitwerk
|
8
|
+
require_relative 'typed_cache/registry'
|
9
|
+
require_relative 'typed_cache/errors'
|
10
|
+
|
11
|
+
Zeitwerk::Loader.for_gem.setup
|
12
|
+
|
13
|
+
module TypedCache
|
14
|
+
extend Dry::Configurable
|
15
|
+
|
16
|
+
# @rbs!
|
17
|
+
# interface _TypedCacheInstrumentationConfig
|
18
|
+
# def enabled: -> bool
|
19
|
+
# def namespace: -> String
|
20
|
+
# end
|
21
|
+
|
22
|
+
# @rbs!
|
23
|
+
# interface _TypedCacheConfig
|
24
|
+
# def default_namespace: -> String
|
25
|
+
# def instrumentation: -> _TypedCacheInstrumentationConfig
|
26
|
+
# end
|
27
|
+
|
28
|
+
# @rbs! type typed_cache_config = _TypedCacheConfig
|
29
|
+
|
30
|
+
# Configuration
|
31
|
+
setting :default_namespace, default: 'typed_cache'
|
32
|
+
|
33
|
+
setting :instrumentation do
|
34
|
+
setting :enabled, default: false
|
35
|
+
setting :namespace, default: 'typed_cache'
|
36
|
+
end
|
37
|
+
|
38
|
+
class << self
|
39
|
+
# Returns a CacheBuilder with the fluent interface
|
40
|
+
# @rbs [V] () -> CacheBuilder[V]
|
41
|
+
def builder
|
42
|
+
CacheBuilder.new(config, Backends, Decorators)
|
43
|
+
end
|
44
|
+
|
45
|
+
# @rbs! def config: -> _TypedCacheConfig
|
46
|
+
|
47
|
+
# @rbs () -> singleton(Instrumentation)
|
48
|
+
def instrumentation
|
49
|
+
Instrumentation
|
50
|
+
end
|
51
|
+
|
52
|
+
# @rbs () -> Registry[backend[untyped]]
|
53
|
+
def backends = Backends
|
54
|
+
|
55
|
+
# @rbs () -> Register[decorator[untyped]]
|
56
|
+
def decorators = Decorators
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# Generated from lib/typed_cache/backend.rb with RBS::Inline
|
2
|
+
|
3
|
+
module TypedCache
|
4
|
+
# Marker mixin for concrete cache back-ends.
|
5
|
+
# A Backend *is* a Store, but the reverse is not necessarily true (decorators also
|
6
|
+
# include Store). By tagging back-ends with this module we can type-check and
|
7
|
+
# register them separately from decorators.
|
8
|
+
#
|
9
|
+
# Back-ends should *not* assume they wrap another store – they are the leaf nodes
|
10
|
+
# that actually persist data.
|
11
|
+
# @rbs generic V
|
12
|
+
module Backend[V]
|
13
|
+
include Store[V]
|
14
|
+
|
15
|
+
include Store::_Store[V]
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# Generated from lib/typed_cache/backends/active_support.rb with RBS::Inline
|
2
|
+
|
3
|
+
module TypedCache
|
4
|
+
module Backends
|
5
|
+
# Adapter that wraps any ActiveSupport::Cache::Store to work with TypedCache
|
6
|
+
# @rbs generic V
|
7
|
+
class ActiveSupport[V]
|
8
|
+
include Backend[V]
|
9
|
+
|
10
|
+
attr_reader namespace: Namespace
|
11
|
+
|
12
|
+
attr_reader cache_store: ::ActiveSupport::Cache::Store
|
13
|
+
|
14
|
+
attr_reader default_options: Hash[Symbol, top]
|
15
|
+
|
16
|
+
# : (Namespace, ::ActiveSupport::Cache::Store, ?Hash[Symbol, top]) -> void
|
17
|
+
def initialize: (Namespace, ::ActiveSupport::Cache::Store, ?Hash[Symbol, top]) -> void
|
18
|
+
|
19
|
+
# @rbs override
|
20
|
+
# : (cache_key) -> either[Error, Snapshot[V]]
|
21
|
+
def get: ...
|
22
|
+
|
23
|
+
# @rbs override
|
24
|
+
# : (cache_key, V) -> either[Error, Snapshot[V]]
|
25
|
+
def set: ...
|
26
|
+
|
27
|
+
# @rbs override
|
28
|
+
# : (cache_key) -> either[Error, Snapshot[V]]
|
29
|
+
def delete: ...
|
30
|
+
|
31
|
+
# @rbs override
|
32
|
+
# : (cache_key) -> bool
|
33
|
+
def key?: ...
|
34
|
+
|
35
|
+
# @rbs override
|
36
|
+
# : -> maybe[Error]
|
37
|
+
def clear: ...
|
38
|
+
|
39
|
+
# @rbs override
|
40
|
+
# : -> String
|
41
|
+
def store_type: ...
|
42
|
+
|
43
|
+
# : (Hash[Symbol, top]) -> ActiveSupport[V]
|
44
|
+
def with_options: (Hash[Symbol, top]) -> ActiveSupport[V]
|
45
|
+
|
46
|
+
# : -> ::ActiveSupport::Cache::Store
|
47
|
+
def raw_cache: () -> ::ActiveSupport::Cache::Store
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
# Regex patterns that match keys for this namespace (with trailing colon)
|
52
|
+
# : -> Array[Regexp]
|
53
|
+
def namespace_prefix_patterns: () -> Array[Regexp]
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# Generated from lib/typed_cache/backends/memory.rb with RBS::Inline
|
2
|
+
|
3
|
+
module TypedCache
|
4
|
+
class MemoryStoreRegistry
|
5
|
+
include Singleton
|
6
|
+
|
7
|
+
extend Forwardable
|
8
|
+
|
9
|
+
# : -> void
|
10
|
+
def initialize: () -> void
|
11
|
+
end
|
12
|
+
|
13
|
+
module Backends
|
14
|
+
# A type-safe memory store implementation with built-in namespacing
|
15
|
+
# @rbs generic V
|
16
|
+
class Memory[V]
|
17
|
+
include Backend[V]
|
18
|
+
|
19
|
+
interface _HashLike[K, V]
|
20
|
+
def []: (K) -> V?
|
21
|
+
|
22
|
+
def []=: (K, V) -> V
|
23
|
+
|
24
|
+
def delete: (K) -> V?
|
25
|
+
|
26
|
+
def key?: (K) -> bool
|
27
|
+
|
28
|
+
def keys: () -> Array[K]
|
29
|
+
end
|
30
|
+
|
31
|
+
type hash_like[K, V] = _HashLike[K, V]
|
32
|
+
|
33
|
+
# @private
|
34
|
+
# @rbs generic V
|
35
|
+
class Entry[V] < Dry::Struct
|
36
|
+
attr_accessor expires_at: Time
|
37
|
+
|
38
|
+
attr_reader value: V
|
39
|
+
|
40
|
+
# @rbs (value: V, expires_in: Integer) -> Entry[V]
|
41
|
+
def self.expiring: (value: V, expires_in: Integer) -> Entry[V]
|
42
|
+
|
43
|
+
# @rbs () -> bool
|
44
|
+
def expired?: () -> bool
|
45
|
+
end
|
46
|
+
|
47
|
+
attr_reader namespace: Namespace
|
48
|
+
|
49
|
+
attr_reader ttl: Namespace
|
50
|
+
|
51
|
+
attr_reader backing_store: hash_like[CacheKey, Entry[V]]
|
52
|
+
|
53
|
+
# : (Namespace, shared: bool, ttl: Integer) -> void
|
54
|
+
def initialize: (Namespace, shared: bool, ttl: Integer) -> void
|
55
|
+
|
56
|
+
# @rbs override
|
57
|
+
# : (cache_key) -> either[Error, Snapshot[V]]
|
58
|
+
def get: ...
|
59
|
+
|
60
|
+
# @rbs override
|
61
|
+
# : (cache_key, V) -> either[Error, Snapshot[V]]
|
62
|
+
def set: ...
|
63
|
+
|
64
|
+
# @rbs override
|
65
|
+
# : (cache_key) -> either[Error, Snapshot[V]]
|
66
|
+
def delete: ...
|
67
|
+
|
68
|
+
# @rbs override
|
69
|
+
# : (cache_key) -> bool
|
70
|
+
def key?: ...
|
71
|
+
|
72
|
+
# @rbs override
|
73
|
+
# : -> maybe[Error]
|
74
|
+
def clear: ...
|
75
|
+
|
76
|
+
# @rbs override
|
77
|
+
# : -> String
|
78
|
+
def store_type: ...
|
79
|
+
|
80
|
+
# : -> Integer
|
81
|
+
def size: () -> Integer
|
82
|
+
|
83
|
+
# : -> Array[CacheKey]
|
84
|
+
def keys: () -> Array[CacheKey]
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
# : -> Hash[CacheKey, Entry[V]]
|
89
|
+
def namespaced_entries: () -> Hash[CacheKey, Entry[V]]
|
90
|
+
|
91
|
+
# : -> void
|
92
|
+
def purge_expired_keys: () -> void
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# Generated from lib/typed_cache/backends.rb with RBS::Inline
|
2
|
+
|
3
|
+
module TypedCache
|
4
|
+
module Backends
|
5
|
+
# Backend registry using composition
|
6
|
+
REGISTRY: untyped
|
7
|
+
|
8
|
+
extend Forwardable
|
9
|
+
|
10
|
+
# : -> Registry
|
11
|
+
def self.registry: () -> Registry
|
12
|
+
|
13
|
+
def resolve: (Symbol, *untyped, **untyped) -> either[Error, Store[untyped]]
|
14
|
+
|
15
|
+
def available: () -> Array[Symbol]
|
16
|
+
|
17
|
+
def register: (Symbol, Class) -> either[Error, void]
|
18
|
+
|
19
|
+
def registered?: (Symbol) -> bool
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# Generated from lib/typed_cache/cache_builder.rb with RBS::Inline
|
2
|
+
|
3
|
+
module TypedCache
|
4
|
+
class CacheBuilder
|
5
|
+
type config = TypedCache::typed_cache_config
|
6
|
+
|
7
|
+
# @rbs (config, Registry[backend[untyped]], Registry[decorator[untyped]]) -> void
|
8
|
+
def initialize: (config, Registry[backend[untyped]], Registry[decorator[untyped]]) -> void
|
9
|
+
|
10
|
+
# Builds the cache - the only method that can fail
|
11
|
+
# @rbs (?Namespace) -> either[Error, Store[V]]
|
12
|
+
def build: (?Namespace) -> either[Error, Store[V]]
|
13
|
+
|
14
|
+
# Familiar Ruby fluent interface - always succeeds
|
15
|
+
# Invalid configurations are caught during build()
|
16
|
+
# @rbs (Symbol, *untyped, **untyped) -> self
|
17
|
+
def with_backend: (Symbol, *untyped, **untyped) -> self
|
18
|
+
|
19
|
+
# Adds an arbitrary decorator by registry key
|
20
|
+
# @rbs (Symbol) -> self
|
21
|
+
def with_decorator: (Symbol) -> self
|
22
|
+
|
23
|
+
# @rbs () -> self
|
24
|
+
def with_instrumentation: () -> self
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
# @rbs (Namespace) -> either[Error, Store[V]]
|
29
|
+
def validate_and_build: (Namespace) -> either[Error, Store[V]]
|
30
|
+
|
31
|
+
# @rbs (Namespace) -> either[Error, Store[V]]
|
32
|
+
def create_store: (Namespace) -> either[Error, Store[V]]
|
33
|
+
|
34
|
+
# @rbs (Store[V]) -> either[Error, Store[V]]
|
35
|
+
def apply_decorators: (Store[V]) -> either[Error, Store[V]]
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# Generated from lib/typed_cache/cache_key.rb with RBS::Inline
|
2
|
+
|
3
|
+
module TypedCache
|
4
|
+
class CacheKey
|
5
|
+
extend Forwardable
|
6
|
+
|
7
|
+
attr_reader namespace: Namespace
|
8
|
+
|
9
|
+
attr_reader key: String
|
10
|
+
|
11
|
+
# @rbs (Namespace, String) -> void
|
12
|
+
def initialize: (Namespace, String) -> void
|
13
|
+
|
14
|
+
# @rbs (Namespace) -> bool
|
15
|
+
def belongs_to?: (Namespace) -> bool
|
16
|
+
|
17
|
+
# @rbs () -> String
|
18
|
+
def to_s: () -> String
|
19
|
+
|
20
|
+
alias cache_key to_s
|
21
|
+
|
22
|
+
# @rbs () -> String
|
23
|
+
def inspect: () -> String
|
24
|
+
|
25
|
+
# @rbs () -> Integer
|
26
|
+
def hash: () -> Integer
|
27
|
+
|
28
|
+
# @rbs (Object) -> bool
|
29
|
+
def ==: (Object) -> bool
|
30
|
+
|
31
|
+
alias eql? ==
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# Generated from lib/typed_cache/cache_ref.rb with RBS::Inline
|
2
|
+
|
3
|
+
module TypedCache
|
4
|
+
# A monadic wrapper for cached values that provides safe access with rich error context.
|
5
|
+
# All operations return Either[Error, Snapshot[V]] to provide detailed information about
|
6
|
+
# cache operations and the source of values.
|
7
|
+
#
|
8
|
+
# @rbs generic V
|
9
|
+
class CacheRef[V]
|
10
|
+
attr_reader store: Store[V]
|
11
|
+
|
12
|
+
attr_reader key: CacheKey
|
13
|
+
|
14
|
+
# : (Store[V], CacheKey) -> void
|
15
|
+
def initialize: (Store[V], CacheKey) -> void
|
16
|
+
|
17
|
+
# Gets a value from the cache as a snapshot
|
18
|
+
# : -> either[Error, Snapshot[V]]
|
19
|
+
def get: () -> either[Error, Snapshot[V]]
|
20
|
+
|
21
|
+
# Sets a value in the cache and returns it as an updated snapshot
|
22
|
+
# : (V) -> either[Error, Snapshot[V]]
|
23
|
+
def set: (V) -> either[Error, Snapshot[V]]
|
24
|
+
|
25
|
+
# Deletes the value from the cache and returns the deleted value as a snapshot
|
26
|
+
# : -> either[Error, Snapshot[V]]
|
27
|
+
def delete: () -> either[Error, Snapshot[V]]
|
28
|
+
|
29
|
+
# Fetches a value from cache, computing and storing it if not found
|
30
|
+
# The snapshot indicates whether the value came from cache or was computed
|
31
|
+
# : () { -> V } -> either[Error, Snapshot[V]]
|
32
|
+
def fetch: () { () -> V } -> either[Error, Snapshot[V]]
|
33
|
+
|
34
|
+
# Checks if the cache contains a value for this key
|
35
|
+
# : -> bool
|
36
|
+
def present?: () -> bool
|
37
|
+
|
38
|
+
# Checks if the cache is empty for this key
|
39
|
+
# : -> bool
|
40
|
+
def empty?: () -> bool
|
41
|
+
|
42
|
+
# Maps over the cached value if it exists, preserving snapshot metadata
|
43
|
+
# : [R] () { (V) -> R } -> either[Error, Snapshot[R]]
|
44
|
+
def map: [R] () { (V) -> R } -> either[Error, Snapshot[R]]
|
45
|
+
|
46
|
+
# Binds over the cached value, allowing for monadic composition with snapshots
|
47
|
+
# : [R] () { (V) -> either[Error, R] } -> either[Error, Snapshot[R]]
|
48
|
+
def bind: [R] () { (V) -> either[Error, R] } -> either[Error, Snapshot[R]]
|
49
|
+
|
50
|
+
alias flat_map bind
|
51
|
+
|
52
|
+
# Updates the cached value using the provided block
|
53
|
+
# Returns the updated value as a snapshot with source=:updated
|
54
|
+
# : () { (V) -> V } -> either[Error, Snapshot[V]]
|
55
|
+
def update: () { (V) -> V } -> either[Error, Snapshot[V]]
|
56
|
+
|
57
|
+
# Returns the cached value or a default if the cache is empty/errored
|
58
|
+
# : (V) -> V
|
59
|
+
def value_or: (V) -> V
|
60
|
+
|
61
|
+
# Returns a Maybe containing the cached value, or None if not present
|
62
|
+
# This provides a more functional approach than value_or
|
63
|
+
# : -> maybe[V]
|
64
|
+
def value_maybe: () -> maybe[V]
|
65
|
+
|
66
|
+
# Computes and caches a value if the cache is currently empty
|
67
|
+
# Returns existing snapshot if present, computed snapshot if cache miss, error otherwise
|
68
|
+
# : () { -> V } -> either[Error, Snapshot[V]]
|
69
|
+
def compute_if_absent: () { () -> V } -> either[Error, Snapshot[V]]
|
70
|
+
|
71
|
+
# Creates a new CacheRef with the same store but different key
|
72
|
+
# : [R] (String) -> CacheRef[R]
|
73
|
+
def with_key: [R] (String) -> CacheRef[R]
|
74
|
+
|
75
|
+
# Creates a scoped CacheRef by appending to the current key path
|
76
|
+
# : [R] (String) -> CacheRef[R]
|
77
|
+
def scope: [R] (String) -> CacheRef[R]
|
78
|
+
|
79
|
+
# Pattern matching support for Either[Error, Snapshot[V]] results
|
80
|
+
# : [R] () { (Error) -> R } () { (Snapshot[V]) -> R } -> R
|
81
|
+
def fold: (untyped left_fn, untyped right_fn) -> untyped
|
82
|
+
|
83
|
+
# Convenience method to work with the snapshot directly
|
84
|
+
# : [R] () { (Snapshot[V]) -> R } -> either[Error, R]
|
85
|
+
def with_snapshot: [R] () { (Snapshot[V]) -> R } -> either[Error, R]
|
86
|
+
|
87
|
+
# Convenience method to work with just the value (losing snapshot context)
|
88
|
+
# : [R] () { (V) -> R } -> either[Error, R]
|
89
|
+
def with: [R] () { (V) -> R } -> either[Error, R]
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# Generated from lib/typed_cache/clock.rb with RBS::Inline
|
2
|
+
|
3
|
+
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
|
+
# Retrieves the current time. If ActiveSupport's `Time.current` is
|
8
|
+
# available, it will be used to respect the configured timezone. Otherwise,
|
9
|
+
# it falls back to the system's `Time.now`.
|
10
|
+
#
|
11
|
+
# @return [Time] The current time.
|
12
|
+
# @rbs () -> Time
|
13
|
+
def self.moment: () -> Time
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# Generated from lib/typed_cache/decorator.rb with RBS::Inline
|
2
|
+
|
3
|
+
module TypedCache
|
4
|
+
# Marker mixin for cache store decorators. A decorator behaves exactly like a
|
5
|
+
# Store but must accept another Store instance in its constructor.
|
6
|
+
# @rbs generic V
|
7
|
+
module Decorator[V]
|
8
|
+
include Store[V]
|
9
|
+
|
10
|
+
include Store::_Store[V]
|
11
|
+
|
12
|
+
include Store::_Decorator[V]
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# Generated from lib/typed_cache/decorators.rb with RBS::Inline
|
2
|
+
|
3
|
+
module TypedCache
|
4
|
+
# Holds store decorators (e.g., instrumentation wrappers) that can be composed
|
5
|
+
# by the CacheBuilder. Decorators must conform to the same API as the wrapped
|
6
|
+
# store (see `TypedCache::Store`) and accept the store instance as their first
|
7
|
+
# constructor argument.
|
8
|
+
#
|
9
|
+
# Example:
|
10
|
+
# Decorators.register(:my_decorator, MyDecorator)
|
11
|
+
# cache = TypedCache.builder
|
12
|
+
# .with_backend { |reg, ns| reg.resolve(:memory, ns).value }
|
13
|
+
# .with_decorator(:my_decorator)
|
14
|
+
# .build.value
|
15
|
+
module Decorators
|
16
|
+
# Default decorator set – starts with instrumentation only, but this registry
|
17
|
+
# lets end-users register their own via `Decorators.register`.
|
18
|
+
REGISTRY: untyped
|
19
|
+
|
20
|
+
extend Forwardable
|
21
|
+
|
22
|
+
# @rbs () -> Registry[Store[untyped]]
|
23
|
+
def self.registry: () -> Registry[Store[untyped]]
|
24
|
+
end
|
25
|
+
end
|