Class: TypedCache::Store

Inherits:
Object
  • Object
show all
Defined in:
lib/typed_cache/store.rb

Overview

Generic interface for type-safe cache storage implementations All stores are assumed to handle namespacing internally

This interface follows the Command-Query Separation principle:

  • Commands (set, delete, clear) perform actions and may return results

  • Queries (get, key?, fetch) ask questions without side effects

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(namespace, backend) ⇒ Store

Returns a new instance of Store.



40
41
42
43
# File 'lib/typed_cache/store.rb', line 40

def initialize(namespace, backend)
  @namespace = namespace
  @backend = backend
end

Instance Attribute Details

#backendObject (readonly)



37
38
39
# File 'lib/typed_cache/store.rb', line 37

def backend
  @backend
end

#namespaceObject

@rbs!

interface _Store[V]
  def read: (cache_key) -> either[Error, Snapshot[maybe[V]]]
  def read_all: (Array[cache_key]) -> either[Error, Hash[CacheKey, Snapshot[V]]]
  def ref: (cache_key) -> CacheRef[V]
  def write: (cache_key, V) -> either[Error, Snapshot[V]]
  def write_all: (Hash[cache_key, V]) -> either[Error, Hash[CacheKey, Snapshot[V]]]
  def delete: (cache_key) -> either[Error, maybe[V]]
  def key?: (cache_key) -> bool
  def clear: () -> void
  def fetch: (cache_key) { (CacheKey) -> V? } -> either[Error, Snapshot[maybe[V]]]
  def fetch_all: (Array[cache_key]) { (CacheKey) -> V? } -> either[Error, Array[Snapshot[V]]]
  def fetch_or_compute_all: (Array[cache_key]) { (Array[CacheKey]) -> Hash[CacheKey, V] } -> either[Error, Hash[CacheKey, Snapshot[V]]]
  def namespace: () -> Namespace
  def with_namespace: (Namespace) -> Store[V]
  def at_namespace: (Namespace) -> Store[V]
  def cache_for: [T] (Class[T], at: (Namespace | String | Array[String])) -> Store[T]
  def backend: () -> Backend[V]
end
include _Store[V]


36
37
38
# File 'lib/typed_cache/store.rb', line 36

def namespace
  @namespace
end

Instance Method Details

#cache_for(klass, at: nil) ⇒ Object



206
207
208
209
210
211
212
213
214
215
216
# File 'lib/typed_cache/store.rb', line 206

def cache_for(klass, at: nil)
  new_namespace =
    case at
    when Namespace then at
    when Array then namespace.join(*at)
    else
      namespace.nested(at.to_s)
    end

  clone.tap { |store| store.namespace = new_namespace }
end

#clearObject

Clears all values from the cache namespace (command operation)



127
128
129
# File 'lib/typed_cache/store.rb', line 127

def clear
  backend.clear
end

#delete(key) ⇒ Object

Removes a value from the cache, returning the removed value



109
110
111
112
113
114
115
116
# File 'lib/typed_cache/store.rb', line 109

def delete(key)
  key = namespaced_key(key)
  deleted_value = backend.delete(key)

  Either.right(Maybe.wrap(deleted_value))
rescue => e
  Either.left(StoreError.new(:delete, key, "Failed to delete from cache: #{e.message}", e))
end

#fetch(key, **kwargs, &block) ⇒ Object

Fetches a value from cache, computing and storing it if not found This is an atomic operation that combines read and write



134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# File 'lib/typed_cache/store.rb', line 134

def fetch(key, **kwargs, &block)
  key = namespaced_key(key)
  computed = false
  result = backend.fetch(key, **kwargs) do
    computed = true
    yield(key)
  end

  if result.nil?
    Either.right(Snapshot.cached(key, Maybe.none))
  else
    snapshot = computed ? Snapshot.computed(key, result) : Snapshot.cached(key, result)
    Either.right(snapshot.map { Maybe.wrap(_1) })
  end
rescue => e
  Either.left(StoreError.new(:fetch, key, "Failed to fetch from cache: #{e.message}", e))
end

#fetch_all(keys, **kwargs, &block) ⇒ Object



153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
# File 'lib/typed_cache/store.rb', line 153

def fetch_all(keys, **kwargs, &block)
  keys = keys.map { |key| namespaced_key(key) }
  computed_keys = Set.new
  fetched_values = backend.fetch_multi(keys, **kwargs) do |key|
    computed_keys << key
    yield(key)
  end

  Either.right(keys.to_h do |key|
    snapshot = computed_keys.include?(key) ? Snapshot.computed(key, fetched_values[key]) : Snapshot.cached(key, fetched_values[key])
    [key, snapshot]
  end)
rescue => e
  Either.left(StoreError.new(:fetch_all, keys, "Failed to fetch from cache: #{e.message}", e))
end

#fetch_or_compute_all(keys, **kwargs, &block) ⇒ Object



170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
# File 'lib/typed_cache/store.rb', line 170

def fetch_or_compute_all(keys, **kwargs, &block)
  keys = keys.map { |key| namespaced_key(key) }
  cached_values = backend.read_multi(keys, **kwargs)
  missing_keys = keys - cached_values.keys

  if missing_keys.any?
    computed_values = yield(missing_keys)
    backend.write_multi(computed_values, **kwargs)
  end

  cached_values.transform_values! { |value| Snapshot.cached(key, value) }
  computed_values.transform_values! { |value| Snapshot.computed(key, value) }

  cached_values.merge(computed_values)
end

#initialize_copy(other) ⇒ Object



46
47
48
49
# File 'lib/typed_cache/store.rb', line 46

def initialize_copy(other)
  super
  @namespace = other.namespace
end

#inspectObject



224
# File 'lib/typed_cache/store.rb', line 224

def inspect = "Store(#{namespace}, #{backend.inspect})"

#instrumenterObject



187
# File 'lib/typed_cache/store.rb', line 187

def instrumenter = @instrumenter ||= Instrumenters::Null.new(namespace:)

#key?(key) ⇒ Boolean

Checks if a key exists in the cache (query operation)

Returns:

  • (Boolean)


120
121
122
123
# File 'lib/typed_cache/store.rb', line 120

def key?(key)
  key = namespaced_key(key)
  backend.key?(key)
end

#read(key, **kwargs) ⇒ Object

Retrieves a value from the cache



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/typed_cache/store.rb', line 53

def read(key, **kwargs)
  key = namespaced_key(key)

  value = backend.read(key, **kwargs)

  Either.right(
    if value.nil?
      Snapshot.cached(key, Maybe.none)
    else
      Snapshot.cached(key, Maybe.some(value))
    end,
  )
rescue => e
  Either.left(StoreError.new(:read, key, "Failed to read from cache: #{e.message}", e))
end

#read_all(keys, **kwargs) ⇒ Object



70
71
72
73
74
75
76
77
78
79
# File 'lib/typed_cache/store.rb', line 70

def read_all(keys, **kwargs)
  keys = keys.map { |key| namespaced_key(key) }
  cached_values = backend.read_multi(keys, **kwargs)

  keys.filter_map do |key|
    next unless cached_values.key?(key.to_s)

    [key, Snapshot.cached(key, cached_values[key.to_s])]
  end.to_h
end

#ref(key) ⇒ Object

Retrieves a cache reference for a key



83
84
85
# File 'lib/typed_cache/store.rb', line 83

def ref(key)
  CacheRef.new(self, namespaced_key(key))
end

#to_sObject



221
# File 'lib/typed_cache/store.rb', line 221

def to_s = "Store(#{namespace})"

#with_namespace(ns) ⇒ Object Also known as: at_namespace

Accepts a String segment or a fully-formed Namespace and returns a cloned store scoped to that namespace. : (Namespace | String | Array) -> Store



192
193
194
195
196
197
198
199
200
201
202
203
# File 'lib/typed_cache/store.rb', line 192

def with_namespace(ns)
  new_namespace =
    case ns
    when Namespace then ns
    when Array then namespace.join(*ns)
    else
      # treat as nested segment under the current namespace
      namespace.nested(ns.to_s)
    end

  clone.tap { |store| store.namespace = new_namespace }
end

#write(key, value, **kwargs) ⇒ Object

Stores a value in the cache



89
90
91
92
93
94
95
96
# File 'lib/typed_cache/store.rb', line 89

def write(key, value, **kwargs)
  key = namespaced_key(key)
  backend.write(key, value, **kwargs)

  Either.right(Snapshot.cached(key, value))
rescue => e
  Either.left(StoreError.new(:write, key, "Failed to write to cache: #{e.message}", e))
end

#write_all(values, **kwargs) ⇒ Object



100
101
102
103
104
105
# File 'lib/typed_cache/store.rb', line 100

def write_all(values, **kwargs)
  values.transform_keys! { |key| namespaced_key(key) }

  written_values = backend.write_multi(values, **kwargs)
  written_values.transform_values { |value| Snapshot.cached(key, value) }
end