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,166 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'singleton'
|
4
|
+
require 'forwardable'
|
5
|
+
|
6
|
+
module TypedCache
|
7
|
+
class MemoryStoreRegistry
|
8
|
+
include Singleton
|
9
|
+
extend Forwardable
|
10
|
+
|
11
|
+
def_delegators :@backing_store, :[], :[]=, :delete, :key?, :keys
|
12
|
+
|
13
|
+
#: -> void
|
14
|
+
def initialize
|
15
|
+
@backing_store = Concurrent::Map.new
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
module Backends
|
20
|
+
# A type-safe memory store implementation with built-in namespacing
|
21
|
+
# @rbs generic V
|
22
|
+
class Memory
|
23
|
+
include Backend #[V]
|
24
|
+
|
25
|
+
# @rbs!
|
26
|
+
# interface _HashLike[K, V]
|
27
|
+
# def []: (K) -> V?
|
28
|
+
# def []=: (K, V) -> V
|
29
|
+
# def delete: (K) -> V?
|
30
|
+
# def key?: (K) -> bool
|
31
|
+
# def keys: () -> Array[K]
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# type hash_like[K, V] = _HashLike[K, V]
|
35
|
+
|
36
|
+
# @private
|
37
|
+
# @rbs generic V
|
38
|
+
class Entry < Dry::Struct
|
39
|
+
attribute :value, Dry.Types::Any
|
40
|
+
attribute :expires_at, Dry.Types::Time
|
41
|
+
|
42
|
+
# @rbs! attr_accessor expires_at: Time
|
43
|
+
# @rbs! attr_reader value: V
|
44
|
+
|
45
|
+
# @rbs (value: V, expires_in: Integer) -> Entry[V]
|
46
|
+
def self.expiring(value:, expires_in:)
|
47
|
+
new(value: value, expires_at: Clock.moment + expires_in)
|
48
|
+
end
|
49
|
+
|
50
|
+
# @rbs () -> bool
|
51
|
+
def expired?
|
52
|
+
Clock.moment >= expires_at
|
53
|
+
end
|
54
|
+
end
|
55
|
+
private_constant :Entry
|
56
|
+
|
57
|
+
attr_reader :namespace, :ttl #: Namespace, Integer
|
58
|
+
attr_reader :backing_store #: hash_like[CacheKey, Entry[V]]
|
59
|
+
|
60
|
+
#: (Namespace, shared: bool, ttl: Integer) -> void
|
61
|
+
def initialize(namespace, shared: false, ttl: 600)
|
62
|
+
@namespace = namespace
|
63
|
+
@ttl = ttl
|
64
|
+
@backing_store = shared ? MemoryStoreRegistry.instance : {}
|
65
|
+
end
|
66
|
+
|
67
|
+
# @rbs override
|
68
|
+
#: (cache_key) -> either[Error, Snapshot[V]]
|
69
|
+
def get(key)
|
70
|
+
key = namespaced_key(key)
|
71
|
+
return Either.left(CacheMissError.new(key)) unless backing_store.key?(key)
|
72
|
+
|
73
|
+
entry = backing_store[key]
|
74
|
+
|
75
|
+
if entry.expired?
|
76
|
+
backing_store.delete(key)
|
77
|
+
return Either.left(CacheMissError.new(key))
|
78
|
+
end
|
79
|
+
|
80
|
+
Either.right(Snapshot.new(entry.value, source: :cache))
|
81
|
+
end
|
82
|
+
|
83
|
+
# @rbs override
|
84
|
+
#: (cache_key, V) -> either[Error, Snapshot[V]]
|
85
|
+
def set(key, value)
|
86
|
+
key = namespaced_key(key)
|
87
|
+
expires_at = Clock.moment + @ttl
|
88
|
+
entry = Entry.new(value: value, expires_at: expires_at)
|
89
|
+
backing_store[key] = entry
|
90
|
+
Either.right(Snapshot.new(value, source: :cache))
|
91
|
+
rescue => e
|
92
|
+
Either.left(StoreError.new(
|
93
|
+
:set,
|
94
|
+
key,
|
95
|
+
"Failed to store value for key '#{key}': #{e.message}",
|
96
|
+
e,
|
97
|
+
))
|
98
|
+
end
|
99
|
+
|
100
|
+
# @rbs override
|
101
|
+
#: (cache_key) -> either[Error, Snapshot[V]]
|
102
|
+
def delete(key)
|
103
|
+
key = namespaced_key(key)
|
104
|
+
entry = backing_store.delete(key)
|
105
|
+
if entry.nil?
|
106
|
+
Either.left(CacheMissError.new(key))
|
107
|
+
else
|
108
|
+
Either.right(Snapshot.new(entry.value, source: :cache))
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# @rbs override
|
113
|
+
#: (cache_key) -> bool
|
114
|
+
def key?(key)
|
115
|
+
key = namespaced_key(key)
|
116
|
+
return false unless backing_store.key?(key) && key.belongs_to?(namespace)
|
117
|
+
|
118
|
+
entry = backing_store[key]
|
119
|
+
!entry.expired?
|
120
|
+
end
|
121
|
+
|
122
|
+
# @rbs override
|
123
|
+
#: -> maybe[Error]
|
124
|
+
def clear
|
125
|
+
keys_to_delete = backing_store.keys.select { |k| k.belongs_to?(namespace) }
|
126
|
+
keys_to_delete.each { |key| backing_store.delete(key) }
|
127
|
+
Maybe.none
|
128
|
+
rescue => e
|
129
|
+
Maybe.some(e)
|
130
|
+
end
|
131
|
+
|
132
|
+
# @rbs override
|
133
|
+
#: -> String
|
134
|
+
def store_type
|
135
|
+
'memory'
|
136
|
+
end
|
137
|
+
|
138
|
+
#: -> Integer
|
139
|
+
def size
|
140
|
+
purge_expired_keys
|
141
|
+
backing_store.keys.count { |k| k.belongs_to?(namespace) }
|
142
|
+
end
|
143
|
+
|
144
|
+
#: -> Array[CacheKey]
|
145
|
+
def keys
|
146
|
+
purge_expired_keys
|
147
|
+
backing_store.keys
|
148
|
+
.select { |k| k.belongs_to?(namespace) }
|
149
|
+
end
|
150
|
+
|
151
|
+
private
|
152
|
+
|
153
|
+
#: -> Hash[CacheKey, Entry[V]]
|
154
|
+
def namespaced_entries
|
155
|
+
backing_store.select { |key, _entry| key.belongs_to?(namespace) }
|
156
|
+
end
|
157
|
+
|
158
|
+
#: -> void
|
159
|
+
def purge_expired_keys
|
160
|
+
namespaced_entries.each do |key, entry|
|
161
|
+
backing_store.delete(key) if entry.expired?
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry/struct'
|
4
|
+
require 'dry/types'
|
5
|
+
|
6
|
+
require_relative 'backends/memory'
|
7
|
+
require_relative 'backends/active_support'
|
8
|
+
|
9
|
+
module TypedCache
|
10
|
+
module Backends
|
11
|
+
# Backend registry using composition
|
12
|
+
REGISTRY = Registry.new('backend', {
|
13
|
+
memory: Memory,
|
14
|
+
active_support: ActiveSupport,
|
15
|
+
}).freeze
|
16
|
+
|
17
|
+
private_constant :REGISTRY
|
18
|
+
|
19
|
+
class << self
|
20
|
+
extend Forwardable
|
21
|
+
delegate [:resolve, :register, :available, :registered?] => :registry
|
22
|
+
|
23
|
+
#: -> Registry
|
24
|
+
def registry = REGISTRY
|
25
|
+
|
26
|
+
# Convenience method delegating to registry
|
27
|
+
# @rbs!
|
28
|
+
# def resolve: (Symbol, *untyped, **untyped) -> either[Error, Store[untyped]]
|
29
|
+
# def available: -> Array[Symbol]
|
30
|
+
# def register: (Symbol, Class) -> either[Error, void]
|
31
|
+
# def registered?: (Symbol) -> bool
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TypedCache
|
4
|
+
class CacheBuilder
|
5
|
+
# @rbs! type config = TypedCache::typed_cache_config
|
6
|
+
|
7
|
+
# @rbs (config, Registry[backend[untyped]], Registry[decorator[untyped]]) -> void
|
8
|
+
def initialize(config, backend_registry = Backends, decorator_registry = Decorators)
|
9
|
+
@config = config
|
10
|
+
@backend_registry = backend_registry
|
11
|
+
@decorator_registry = decorator_registry
|
12
|
+
|
13
|
+
@backend_name = nil
|
14
|
+
@backend_args = []
|
15
|
+
@backend_options = {}
|
16
|
+
@decorators = []
|
17
|
+
end
|
18
|
+
|
19
|
+
# Builds the cache - the only method that can fail
|
20
|
+
# @rbs (?Namespace) -> either[Error, Store[V]]
|
21
|
+
def build(namespace = Namespace.at(@config.default_namespace))
|
22
|
+
validate_and_build(namespace)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Familiar Ruby fluent interface - always succeeds
|
26
|
+
# Invalid configurations are caught during build()
|
27
|
+
# @rbs (Symbol, *untyped, **untyped) -> self
|
28
|
+
def with_backend(name, *args, **options)
|
29
|
+
@backend_name = name
|
30
|
+
@backend_args = args
|
31
|
+
@backend_options = options
|
32
|
+
self
|
33
|
+
end
|
34
|
+
|
35
|
+
# Adds an arbitrary decorator by registry key
|
36
|
+
# @rbs (Symbol) -> self
|
37
|
+
def with_decorator(name)
|
38
|
+
@decorators << name
|
39
|
+
self
|
40
|
+
end
|
41
|
+
|
42
|
+
# @rbs () -> self
|
43
|
+
def with_instrumentation
|
44
|
+
with_decorator(:instrumented)
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
# @rbs (Namespace) -> either[Error, Store[V]]
|
50
|
+
def validate_and_build(namespace)
|
51
|
+
create_store(namespace).bind do |store|
|
52
|
+
apply_decorators(store)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# @rbs (Namespace) -> either[Error, Store[V]]
|
57
|
+
def create_store(namespace)
|
58
|
+
return Either.left(ArgumentError.new('Backend not configured')) unless @backend_name
|
59
|
+
|
60
|
+
# Prepend namespace to the arguments for the backend constructor
|
61
|
+
@backend_registry.resolve(@backend_name, namespace, *@backend_args, **@backend_options)
|
62
|
+
end
|
63
|
+
|
64
|
+
# @rbs (Store[V]) -> either[Error, Store[V]]
|
65
|
+
def apply_decorators(store)
|
66
|
+
return Either.right(store) if @decorators.empty?
|
67
|
+
|
68
|
+
@decorators.reduce(Either.right(store)) do |result, decorator_name|
|
69
|
+
result.bind do |current_store|
|
70
|
+
@decorator_registry.resolve(decorator_name, current_store)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
rescue => e
|
74
|
+
Either.left(StoreError.new(:decorator_application, 'decorator', "Failed to apply decorator: #{e.message}", e))
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TypedCache
|
4
|
+
class CacheKey
|
5
|
+
extend Forwardable
|
6
|
+
|
7
|
+
attr_reader :namespace #: Namespace
|
8
|
+
attr_reader :key #: String
|
9
|
+
|
10
|
+
# @rbs (Namespace, String) -> void
|
11
|
+
def initialize(namespace, key)
|
12
|
+
@namespace = namespace
|
13
|
+
@key = key
|
14
|
+
end
|
15
|
+
|
16
|
+
# @rbs (Namespace) -> bool
|
17
|
+
def belongs_to?(namespace)
|
18
|
+
@namespace.to_s.start_with?(namespace.to_s)
|
19
|
+
end
|
20
|
+
|
21
|
+
# @rbs () -> String
|
22
|
+
def to_s
|
23
|
+
"#{@namespace}:#{@key}"
|
24
|
+
end
|
25
|
+
|
26
|
+
alias cache_key to_s
|
27
|
+
|
28
|
+
# @rbs () -> String
|
29
|
+
def inspect
|
30
|
+
"#<#{self.class} namespace=#{@namespace} key=#{@key}>"
|
31
|
+
end
|
32
|
+
|
33
|
+
# @rbs () -> Integer
|
34
|
+
def hash
|
35
|
+
[@namespace, @key].hash
|
36
|
+
end
|
37
|
+
|
38
|
+
# @rbs (Object) -> bool
|
39
|
+
def ==(other)
|
40
|
+
other.is_a?(self.class) && other.namespace == @namespace && other.key == @key
|
41
|
+
end
|
42
|
+
|
43
|
+
alias eql? ==
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,155 @@
|
|
1
|
+
# frozen_string_literal: true
|
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
|
10
|
+
attr_reader :store #: Store[V]
|
11
|
+
attr_reader :key #: CacheKey
|
12
|
+
|
13
|
+
#: (Store[V], CacheKey) -> void
|
14
|
+
def initialize(store, key)
|
15
|
+
@store = store
|
16
|
+
@key = key
|
17
|
+
end
|
18
|
+
|
19
|
+
# Gets a value from the cache as a snapshot
|
20
|
+
#: -> either[Error, Snapshot[V]]
|
21
|
+
def get
|
22
|
+
store.get(key)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Sets a value in the cache and returns it as an updated snapshot
|
26
|
+
#: (V) -> either[Error, Snapshot[V]]
|
27
|
+
def set(value)
|
28
|
+
store.set(key, value)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Deletes the value from the cache and returns the deleted value as a snapshot
|
32
|
+
#: -> either[Error, Snapshot[V]]
|
33
|
+
def delete
|
34
|
+
store.delete(key)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Fetches a value from cache, computing and storing it if not found
|
38
|
+
# The snapshot indicates whether the value came from cache or was computed
|
39
|
+
#: () { -> V } -> either[Error, Snapshot[V]]
|
40
|
+
def fetch(&block)
|
41
|
+
store.fetch(key, &block)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Checks if the cache contains a value for this key
|
45
|
+
#: -> bool
|
46
|
+
def present?
|
47
|
+
store.get(key).right?
|
48
|
+
end
|
49
|
+
|
50
|
+
# Checks if the cache is empty for this key
|
51
|
+
#: -> bool
|
52
|
+
def empty?
|
53
|
+
store.get(key).left?
|
54
|
+
end
|
55
|
+
|
56
|
+
# Maps over the cached value if it exists, preserving snapshot metadata
|
57
|
+
#: [R] () { (V) -> R } -> either[Error, Snapshot[R]]
|
58
|
+
def map(&block)
|
59
|
+
get.map { |snapshot| snapshot.map(&block) }
|
60
|
+
end
|
61
|
+
|
62
|
+
# Binds over the cached value, allowing for monadic composition with snapshots
|
63
|
+
#: [R] () { (V) -> either[Error, R] } -> either[Error, Snapshot[R]]
|
64
|
+
def bind(&block)
|
65
|
+
get.bind { |snapshot| snapshot.bind(&block) }
|
66
|
+
end
|
67
|
+
|
68
|
+
alias flat_map bind
|
69
|
+
|
70
|
+
# Updates the cached value using the provided block
|
71
|
+
# Returns the updated value as a snapshot with source=:updated
|
72
|
+
#: () { (V) -> V } -> either[Error, Snapshot[V]]
|
73
|
+
def update(&block)
|
74
|
+
get.bind do |snapshot|
|
75
|
+
new_value = yield(snapshot.value)
|
76
|
+
set(new_value)
|
77
|
+
rescue => e
|
78
|
+
Either.left(StoreError.new(
|
79
|
+
:update,
|
80
|
+
key,
|
81
|
+
"Failed to update value: #{e.message}",
|
82
|
+
e,
|
83
|
+
))
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# Returns the cached value or a default if the cache is empty/errored
|
88
|
+
#: (V) -> V
|
89
|
+
def value_or(default)
|
90
|
+
get.fold(
|
91
|
+
->(_error) { default },
|
92
|
+
->(snapshot) { snapshot.value },
|
93
|
+
)
|
94
|
+
end
|
95
|
+
|
96
|
+
# Returns a Maybe containing the cached value, or None if not present
|
97
|
+
# This provides a more functional approach than value_or
|
98
|
+
#: -> maybe[V]
|
99
|
+
def value_maybe
|
100
|
+
get.fold(
|
101
|
+
->(_error) { Maybe.none },
|
102
|
+
->(snapshot) { Maybe.some(snapshot.value) },
|
103
|
+
)
|
104
|
+
end
|
105
|
+
|
106
|
+
# Computes and caches a value if the cache is currently empty
|
107
|
+
# Returns existing snapshot if present, computed snapshot if cache miss, error otherwise
|
108
|
+
#: () { -> V } -> either[Error, Snapshot[V]]
|
109
|
+
def compute_if_absent(&block)
|
110
|
+
fetch(&block).fold(
|
111
|
+
->(error) {
|
112
|
+
Either.left(StoreError.new(
|
113
|
+
:compute_if_absent,
|
114
|
+
key,
|
115
|
+
"Failed to compute value: #{error.message}",
|
116
|
+
error,
|
117
|
+
))
|
118
|
+
},
|
119
|
+
->(snapshot) { Either.right(snapshot) },
|
120
|
+
)
|
121
|
+
end
|
122
|
+
|
123
|
+
# Creates a new CacheRef with the same store but different key
|
124
|
+
#: [R] (String) -> CacheRef[R]
|
125
|
+
def with_key(new_key)
|
126
|
+
CacheRef.new(store, store.namespace.key(new_key))
|
127
|
+
end
|
128
|
+
|
129
|
+
# Creates a scoped CacheRef by appending to the current key path
|
130
|
+
#: [R] (String) -> CacheRef[R]
|
131
|
+
def scope(scope_key)
|
132
|
+
new_namespace = store.namespace.nested(key.key)
|
133
|
+
new_store = store.with_namespace(new_namespace)
|
134
|
+
CacheRef.new(new_store, new_namespace.key(scope_key))
|
135
|
+
end
|
136
|
+
|
137
|
+
# Pattern matching support for Either[Error, Snapshot[V]] results
|
138
|
+
#: [R] () { (Error) -> R } () { (Snapshot[V]) -> R } -> R
|
139
|
+
def fold(left_fn, right_fn)
|
140
|
+
get.fold(left_fn, right_fn)
|
141
|
+
end
|
142
|
+
|
143
|
+
# Convenience method to work with the snapshot directly
|
144
|
+
#: [R] () { (Snapshot[V]) -> R } -> either[Error, R]
|
145
|
+
def with_snapshot(&block)
|
146
|
+
get.map(&block)
|
147
|
+
end
|
148
|
+
|
149
|
+
# Convenience method to work with just the value (losing snapshot context)
|
150
|
+
#: [R] () { (V) -> R } -> either[Error, R]
|
151
|
+
def with(&block)
|
152
|
+
get.map { |snapshot| yield(snapshot.value) }
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
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
|
+
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.
|
13
|
+
# @rbs () -> Time
|
14
|
+
def moment
|
15
|
+
if Time.respond_to?(:current)
|
16
|
+
Time.current
|
17
|
+
else
|
18
|
+
Time.now
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
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
|
8
|
+
include Store #[V]
|
9
|
+
# @rbs! include Store::_Store[V]
|
10
|
+
# @rbs! include Store::_Decorator[V]
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
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
|
+
#
|
16
|
+
module Decorators
|
17
|
+
# Default decorator set – starts with instrumentation only, but this registry
|
18
|
+
# lets end-users register their own via `Decorators.register`.
|
19
|
+
REGISTRY = Registry.new('decorator', {
|
20
|
+
instrumented: Store::Instrumented,
|
21
|
+
}).freeze
|
22
|
+
|
23
|
+
private_constant :REGISTRY
|
24
|
+
|
25
|
+
class << self
|
26
|
+
extend Forwardable
|
27
|
+
|
28
|
+
# Delegate common registry helpers
|
29
|
+
delegate [:resolve, :register, :available, :registered?] => :registry
|
30
|
+
|
31
|
+
# @rbs () -> Registry[Store[untyped]]
|
32
|
+
def registry = REGISTRY
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TypedCache
|
4
|
+
# @rbs! type either[out E, out R] = (Left[E] | Right[R]) & _Either[E, R]
|
5
|
+
|
6
|
+
module Either
|
7
|
+
include Kernel
|
8
|
+
|
9
|
+
class << self
|
10
|
+
#: [E] (E) -> either[E, bot]
|
11
|
+
def left(error) = Left.new(error)
|
12
|
+
#: [R] (R) -> either[bot, R]
|
13
|
+
def right(value) = Right.new(value)
|
14
|
+
|
15
|
+
#: [E, R] (E | R) -> either[E, R]
|
16
|
+
def wrap(value, error_class = StandardError)
|
17
|
+
case value
|
18
|
+
when Left, Right then value
|
19
|
+
when error_class then Left.new(value)
|
20
|
+
else
|
21
|
+
Right.new(value)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# @rbs! interface _Either[out E, out R]
|
28
|
+
# def left?: -> bool
|
29
|
+
# def right?: -> bool
|
30
|
+
# def map: [T] () { (R) -> T } -> either[E, T]
|
31
|
+
# def bind: [E2, R2] () { (R) -> either[E2, R2] } -> either[E | E2, R2]
|
32
|
+
# def map_left: [F] () { (E) -> F } -> either[F, R]
|
33
|
+
# def fold: [T, E, R] (^(E) -> T, ^(R) -> T) -> T
|
34
|
+
# end
|
35
|
+
|
36
|
+
# @rbs generic out E
|
37
|
+
class Left
|
38
|
+
# @rbs! include _Either[E, bot]
|
39
|
+
|
40
|
+
attr_reader :error #: E
|
41
|
+
|
42
|
+
#: (E) -> void
|
43
|
+
def initialize(error)
|
44
|
+
@error = error
|
45
|
+
end
|
46
|
+
|
47
|
+
# @rbs override
|
48
|
+
#: -> true
|
49
|
+
def left? = true
|
50
|
+
|
51
|
+
# @rbs override
|
52
|
+
#: -> false
|
53
|
+
def right? = false
|
54
|
+
|
55
|
+
# @rbs override
|
56
|
+
#: [T] () { (R) -> T } -> either[E, T]
|
57
|
+
def map(&) = self
|
58
|
+
|
59
|
+
# @rbs override
|
60
|
+
#: [E2, R2] () { (R) -> either[E2, R2] } -> either[E | E2, R2]
|
61
|
+
def bind(&) = self
|
62
|
+
|
63
|
+
# @rbs override
|
64
|
+
#: [F] () { (E) -> F } -> either[F, R]
|
65
|
+
def map_left(&) = Left.new(yield(error))
|
66
|
+
|
67
|
+
# @rbs override
|
68
|
+
#: [T, E, R] (^(E) -> T, ^(R) -> T) -> T
|
69
|
+
def fold(left_fn, right_fn)
|
70
|
+
left_fn.call(error)
|
71
|
+
end
|
72
|
+
|
73
|
+
#: (Array[top]) -> ({ error: E })
|
74
|
+
def deconstruct_keys(keys)
|
75
|
+
{ error: }
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# @rbs generic out R
|
80
|
+
class Right
|
81
|
+
# @rbs! include _Either[bot, R]
|
82
|
+
|
83
|
+
attr_reader :value #: R
|
84
|
+
|
85
|
+
#: (R) -> void
|
86
|
+
def initialize(value)
|
87
|
+
@value = value
|
88
|
+
end
|
89
|
+
|
90
|
+
# @rbs override
|
91
|
+
#: -> false
|
92
|
+
def left? = false
|
93
|
+
|
94
|
+
# @rbs override
|
95
|
+
#: -> true
|
96
|
+
def right? = true
|
97
|
+
|
98
|
+
# @rbs override
|
99
|
+
#: [T] () { (R) -> T } -> either[E, T]
|
100
|
+
def map(&) = Right.new(yield(value))
|
101
|
+
|
102
|
+
# @rbs override
|
103
|
+
#: [E2, R2] () { (R) -> either[E2, R2] } -> either[E | E2, R2]
|
104
|
+
def bind(&) = yield(value)
|
105
|
+
|
106
|
+
# @rbs override
|
107
|
+
#: [F] () { (E) -> F } -> either[F, R]
|
108
|
+
def map_left(&) = self
|
109
|
+
|
110
|
+
# @rbs override
|
111
|
+
#: [T, E, R] (^(E) -> T, ^(R) -> T) -> T
|
112
|
+
def fold(left_fn, right_fn)
|
113
|
+
right_fn.call(value)
|
114
|
+
end
|
115
|
+
|
116
|
+
#: (Array[top]) -> ({ value: R })
|
117
|
+
def deconstruct_keys(keys)
|
118
|
+
{ value: }
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|