Module: SafeMemoize::PublicMethods

Included in:
InstanceMethods
Defined in:
lib/safe_memoize/public_methods.rb

Overview

Public instance methods mixed into every class that prepends SafeMemoize.

Instance Method Summary collapse

Instance Method Details

#clear_memo_hooks(hook_type = nil) ⇒ void

This method returns an undefined value.

Removes all registered hooks, or only hooks of a specific type.

Parameters:

  • hook_type (Symbol, nil) (defaults to: nil)

    one of +:on_hit+, +:on_miss+, +:on_store+, +:on_expire+, +:on_evict+; when +nil+ all hooks are cleared



151
152
153
154
155
# File 'lib/safe_memoize/public_methods.rb', line 151

def clear_memo_hooks(hook_type = nil)
  with_memo_lock do
    _clear_memo_hooks(hook_type)
  end
end

#dump_memo(method_name = nil) ⇒ Hash

Exports live cache entries as a plain hash suitable for serialization.

Parameters:

  • method_name (Symbol, String, nil) (defaults to: nil)

    when given, exports only entries for that method; when +nil+, exports all methods

Returns:

  • (Hash)

    mapping cache keys to their cached values (expired entries excluded)



212
213
214
215
216
217
218
219
220
221
# File 'lib/safe_memoize/public_methods.rb', line 212

def dump_memo(method_name = nil)
  method_name = method_name&.to_sym

  with_memo_lock do
    cache = memo_cache_or_nil || {}
    entries = method_name ? cache.select { |key, _| key[0] == method_name } : cache.dup
    entries.select! { |_, record| memo_record_live?(record) }
    entries.transform_values { |record| memo_record_value(record) }
  end
end

#load_memo(snapshot) ⇒ nil

Restores cache entries from a snapshot produced by #dump_memo.

Existing entries are not cleared; snapshot keys are merged in. Each restored entry fires the +:on_store+ hook.

Parameters:

  • snapshot (Hash)

    a hash previously returned by #dump_memo

Returns:

  • (nil)

Raises:

  • (ArgumentError)

    if +snapshot+ is not a +Hash+



231
232
233
234
235
236
237
238
239
240
241
242
243
244
# File 'lib/safe_memoize/public_methods.rb', line 231

def load_memo(snapshot)
  raise ArgumentError, "snapshot must be a Hash" unless snapshot.is_a?(Hash)

  with_memo_lock do
    @__safe_memo_cache__ ||= {}
    snapshot.each do |cache_key, value|
      record = memo_record(value, expires_at: nil)
      @__safe_memo_cache__[cache_key] = record
      call_memo_hooks(:on_store, cache_key, record)
    end
  end

  nil
end

#memo_age(method_name, *args, **kwargs) ⇒ Float?

Returns how many seconds ago the entry was cached, or +nil+ if not cached.

Parameters:

  • method_name (Symbol, String)
  • args (Array)
  • kwargs (Hash)

Returns:

  • (Float, nil)


298
299
300
301
302
303
304
305
306
307
308
309
310
# File 'lib/safe_memoize/public_methods.rb', line 298

def memo_age(method_name, *args, **kwargs)
  cache_key = compute_cache_key(method_name, args, kwargs)

  with_memo_lock do
    record = memo_cache_record(cache_key)
    return nil unless record

    cached_at = record[:cached_at]
    return nil unless cached_at

    (Process.clock_gettime(Process::CLOCK_MONOTONIC) - cached_at).round(6)
  end
end

#memo_count(*method_name) ⇒ Integer

Returns the number of live cached entries for a method (or all methods).

Parameters:

  • method_name (Symbol, String, nil)

    when omitted, counts all methods

Returns:

  • (Integer)


51
52
53
54
55
56
57
# File 'lib/safe_memoize/public_methods.rb', line 51

def memo_count(*method_name)
  scoped_method = safe_memo_scoped_method(method_name)

  with_memo_lock do
    safe_memo_count_for(scoped_method)
  end
end

#memo_inspect(method_name, *args, **kwargs) ⇒ Hash?

Returns a detailed snapshot of a single cached entry, or +nil+ if not cached.

All reads are performed inside a single mutex hold.

Parameters:

  • method_name (Symbol, String)
  • args (Array)
  • kwargs (Hash)

Returns:

  • (Hash, nil)

    hash with keys +:cached+, +:value+, +:hits+, +:misses+, +:ttl_remaining+, +:age+, +:custom_key+, +:lru_position+; or +nil+ when the entry is not present



387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
# File 'lib/safe_memoize/public_methods.rb', line 387

def memo_inspect(method_name, *args, **kwargs)
  method_name = method_name.to_sym
  cache_key = compute_cache_key(method_name, args, kwargs)

  with_memo_lock do
    record = memo_cache_record(cache_key)
    return nil unless record

    now = Process.clock_gettime(Process::CLOCK_MONOTONIC)

    ttl_remaining = if record[:expires_at]
      remaining = record[:expires_at] - now
      (remaining > 0) ? remaining.round(6) : 0
    end

    age = (now - record[:cached_at]).round(6) if record[:cached_at]

    metrics_key = safe_memo_cache_key(method_name, args, kwargs)
    entry_metrics = memo_metrics_store[metrics_key] || {hits: 0, misses: 0}

    custom_key = (cache_key.length == 2) ? cache_key[1] : nil

    lru_position = begin
      method_lru = lru_order_store[method_name]
      if method_lru&.key?(cache_key)
        keys = method_lru.keys
        keys.length - keys.index(cache_key)
      end
    end

    {
      cached: true,
      value: memo_record_value(record),
      hits: entry_metrics[:hits],
      misses: entry_metrics[:misses],
      ttl_remaining: ttl_remaining,
      age: age,
      custom_key: custom_key,
      lru_position: lru_position
    }
  end
end

#memo_keys(*method_name) ⇒ Array<Hash>

Returns metadata hashes describing each cached entry.

Each hash contains +:args+, +:kwargs+ (or +:custom_key+ for custom-keyed entries), and +:method+ when no +method_name+ filter is applied.

Parameters:

  • method_name (Symbol, String, nil)

    when omitted, returns entries for all methods

Returns:

  • (Array<Hash>)


66
67
68
69
70
71
72
# File 'lib/safe_memoize/public_methods.rb', line 66

def memo_keys(*method_name)
  scoped_method = safe_memo_scoped_method(method_name)

  with_memo_lock do
    safe_memo_keys_for(scoped_method)
  end
end

#memo_preload(method_name, *arg_sets) ⇒ Array

Calls the memoized method for each argument set and caches all results.

Equivalent to calling the method for each arg set individually, but expressed as a single call for clarity.

Examples:

obj.memo_preload(:find, [1], [2], [3])

Parameters:

  • method_name (Symbol, String)
  • arg_sets (Array<Array>)

    each element is an argument list for one call

Returns:

  • (Array)

    cached values in input order



200
201
202
203
204
205
# File 'lib/safe_memoize/public_methods.rb', line 200

def memo_preload(method_name, *arg_sets)
  method_name = method_name.to_sym
  arg_sets.map do |args|
    send(method_name, *Array(args))
  end
end

#memo_refresh(method_name, *args, **kwargs) ⇒ Object

Clears the cached entry and immediately re-calls the method to populate a fresh value.

Parameters:

  • method_name (Symbol, String)
  • args (Array)
  • kwargs (Hash)

Returns:

  • (Object)

    the freshly computed and cached value



286
287
288
289
290
# File 'lib/safe_memoize/public_methods.rb', line 286

def memo_refresh(method_name, *args, **kwargs)
  method_name = method_name.to_sym
  reset_memo(method_name, *args, **kwargs)
  send(method_name, *args, **kwargs)
end

#memo_stale?(method_name, *args, **kwargs) ⇒ Boolean

Returns +true+ if the entry exists but its TTL has elapsed.

Parameters:

  • method_name (Symbol, String)
  • args (Array)
  • kwargs (Hash)

Returns:

  • (Boolean)


318
319
320
321
322
323
324
325
326
327
328
329
330
# File 'lib/safe_memoize/public_methods.rb', line 318

def memo_stale?(method_name, *args, **kwargs)
  cache_key = compute_cache_key(method_name, args, kwargs)

  with_memo_lock do
    cache = memo_cache_or_nil
    return false unless cache

    record = cache[cache_key]
    return false unless record

    !memo_record_live?(record)
  end
end

#memo_touch(method_name, *args, ttl: nil, **kwargs) ⇒ Boolean

Resets the expiry clock on a live cached entry without recomputing its value.

Parameters:

  • method_name (Symbol, String)
  • args (Array)
  • ttl (Numeric, nil) (defaults to: nil)

    new TTL to apply; when +nil+, uses the original TTL derived from the entry's +cached_at+ and +expires_at+ timestamps

  • kwargs (Hash)

Returns:

  • (Boolean)

    +true+ if the entry existed and was touched; +false+ otherwise



254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
# File 'lib/safe_memoize/public_methods.rb', line 254

def memo_touch(method_name, *args, ttl: nil, **kwargs)
  method_name = method_name.to_sym
  cache_key = compute_cache_key(method_name, args, kwargs)

  with_memo_lock do
    cache = memo_cache_or_nil
    return false unless cache

    record = cache[cache_key]
    return false unless record && memo_record_live?(record)

    now = Process.clock_gettime(Process::CLOCK_MONOTONIC)

    effective_ttl = if ttl
      ttl
    elsif record[:expires_at] && record[:cached_at]
      record[:expires_at] - record[:cached_at]
    end

    record[:expires_at] = effective_ttl ? now + effective_ttl : nil
    record[:cached_at] = now
    true
  end
end

#memo_ttl_remaining(method_name, *args, **kwargs) ⇒ Float?

Returns the number of seconds until the cached entry expires.

Parameters:

  • method_name (Symbol, String)
  • args (Array)
  • kwargs (Hash)

Returns:

  • (Float)

    seconds remaining (may be 0 if already expired)

  • (nil)

    if the entry has no TTL or is not cached



32
33
34
35
36
37
38
39
40
41
42
43
44
45
# File 'lib/safe_memoize/public_methods.rb', line 32

def memo_ttl_remaining(method_name, *args, **kwargs)
  cache_key = compute_cache_key(method_name, args, kwargs)

  with_memo_lock do
    record = memo_cache_record(cache_key)
    return 0 unless record

    expires_at = record[:expires_at]
    return nil unless expires_at

    remaining = expires_at - Process.clock_gettime(Process::CLOCK_MONOTONIC)
    (remaining > 0) ? remaining.round(6) : 0
  end
end

#memo_values(*method_name) ⇒ Array<Hash>

Returns metadata hashes including the cached value for each entry.

Each hash contains all fields from #memo_keys plus +:value+.

Parameters:

  • method_name (Symbol, String, nil)

    when omitted, returns entries for all methods

Returns:

  • (Array<Hash>)


80
81
82
83
84
85
86
# File 'lib/safe_memoize/public_methods.rb', line 80

def memo_values(*method_name)
  scoped_method = safe_memo_scoped_method(method_name)

  with_memo_lock do
    safe_memo_values_for(scoped_method)
  end
end

#memoized?(method_name, *args, **kwargs, &block) ⇒ Boolean

Returns +true+ if the given call is currently cached (and not expired).

Always returns +false+ when a block is provided, because block-taking methods cannot be safely keyed by arguments alone.

Parameters:

  • method_name (Symbol, String)
  • args (Array)

    positional arguments used to look up the entry

  • kwargs (Hash)

    keyword arguments used to look up the entry

Returns:

  • (Boolean)


15
16
17
18
19
20
21
22
23
# File 'lib/safe_memoize/public_methods.rb', line 15

def memoized?(method_name, *args, **kwargs, &block)
  return false if block

  cache_key = compute_cache_key(method_name, args, kwargs)

  with_memo_lock do
    memo_cache_hit?(cache_key)
  end
end

#on_memo_evict {|cache_key, record| ... } ⇒ void

This method returns an undefined value.

Registers a hook that fires when an LRU eviction occurs.

Yields:

  • (cache_key, record)

Raises:

  • (ArgumentError)

    if no block is given



106
107
108
109
110
# File 'lib/safe_memoize/public_methods.rb', line 106

def on_memo_evict(&block)
  raise ArgumentError, "block required" unless block

  register_memo_hook(:on_evict, &block)
end

#on_memo_expire {|cache_key, record| ... } ⇒ void

This method returns an undefined value.

Registers a hook that fires on every cache hit.

Yields:

  • (cache_key, record)

    called synchronously inside the cache lock

Yield Parameters:

  • cache_key (Array)

    the internal cache key

  • record (Hash)

    the cache record (+:value+, +:expires_at+, +:cached_at+)

Raises:

  • (ArgumentError)

    if no block is given



95
96
97
98
99
# File 'lib/safe_memoize/public_methods.rb', line 95

def on_memo_expire(&block)
  raise ArgumentError, "block required" unless block

  register_memo_hook(:on_expire, &block)
end

#on_memo_hit {|cache_key, record| ... } ⇒ void

This method returns an undefined value.

Registers a hook that fires on every cache hit.

Yields:

  • (cache_key, record)

Raises:

  • (ArgumentError)

    if no block is given



117
118
119
120
121
# File 'lib/safe_memoize/public_methods.rb', line 117

def on_memo_hit(&block)
  raise ArgumentError, "block required" unless block

  register_memo_hook(:on_hit, &block)
end

#on_memo_miss {|cache_key, record| ... } ⇒ void

This method returns an undefined value.

Registers a hook that fires on every cache miss (before the value is stored).

Yields:

  • (cache_key, record)

Raises:

  • (ArgumentError)

    if no block is given



128
129
130
131
132
# File 'lib/safe_memoize/public_methods.rb', line 128

def on_memo_miss(&block)
  raise ArgumentError, "block required" unless block

  register_memo_hook(:on_miss, &block)
end

#on_memo_store {|cache_key, record| ... } ⇒ void

This method returns an undefined value.

Registers a hook that fires whenever a value is written to the cache (miss, #warm_memo, or #load_memo).

Yields:

  • (cache_key, record)

Raises:

  • (ArgumentError)

    if no block is given



140
141
142
143
144
# File 'lib/safe_memoize/public_methods.rb', line 140

def on_memo_store(&block)
  raise ArgumentError, "block required" unless block

  register_memo_hook(:on_store, &block)
end

#reset_all_memosvoid

This method returns an undefined value.

Clears all cached entries for every method on this instance. Each evicted entry fires the +:on_evict+ hook.



365
366
367
368
369
370
371
372
373
374
375
# File 'lib/safe_memoize/public_methods.rb', line 365

def reset_all_memos
  with_memo_lock do
    if defined?(@__safe_memo_cache__) && @__safe_memo_cache__
      @__safe_memo_cache__.each do |key, record|
        call_memo_hooks(:on_evict, key, record)
      end
    end
    @__safe_memo_cache__ = {}
    lru_clear_all
  end
end

#reset_memo(method_name, *args, **kwargs) ⇒ void

This method returns an undefined value.

Removes one or all cached entries for a method.

When called with only +method_name+, all entries for that method are cleared. When called with +method_name+ and arguments, only the exact matching entry is cleared. Each evicted entry fires the +:on_evict+ hook.

Parameters:

  • method_name (Symbol, String)
  • args (Array)

    positional arguments identifying a specific entry

  • kwargs (Hash)

    keyword arguments identifying a specific entry



342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
# File 'lib/safe_memoize/public_methods.rb', line 342

def reset_memo(method_name, *args, **kwargs)
  method_name = method_name.to_sym

  matcher = memo_matcher_for(method_name, args, kwargs)

  with_memo_lock do
    with_memo_cache do |cache|
      cache.delete_if do |key, record|
        if matcher.call(key)
          call_memo_hooks(:on_evict, key, record)
          true
        else
          false
        end
      end
    end
  end
end

#warm_memo(method_name, *args, ttl: nil, **kwargs) { ... } ⇒ Object

Pre-populates a cache entry with the value returned by the block without calling the memoized method itself.

Useful for warming caches from a serialized snapshot or an external source.

Examples:

obj.warm_memo(:find, 42) { User.new(id: 42, name: "cached") }

Parameters:

  • method_name (Symbol, String)
  • args (Array)

    positional arguments that identify the cache slot

  • ttl (Numeric, nil) (defaults to: nil)

    optional expiry for the warmed entry

  • kwargs (Hash)

    keyword arguments that identify the cache slot

Yields:

  • [] must return the value to store

Returns:

  • (Object)

    the value returned by the block

Raises:

  • (ArgumentError)

    if no block is given



172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
# File 'lib/safe_memoize/public_methods.rb', line 172

def warm_memo(method_name, *args, ttl: nil, **kwargs, &block)
  raise ArgumentError, "block required" unless block

  method_name = method_name.to_sym
  cache_key = compute_cache_key(method_name, args, kwargs)
  value = block.call

  with_memo_lock do
    @__safe_memo_cache__ ||= {}
    record = memo_record(value, expires_at: memo_expires_at(ttl))
    @__safe_memo_cache__[cache_key] = record
    call_memo_hooks(:on_store, cache_key, record)
  end

  value
end