safe_memoize 1.2.0 → 1.4.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.
- checksums.yaml +4 -4
- data/.github/FUNDING.yml +15 -0
- data/CHANGELOG.md +39 -0
- data/README.md +383 -1
- data/ROADMAP.md +31 -4
- data/lib/safe_memoize/cache_metrics_methods.rb +4 -5
- data/lib/safe_memoize/class_methods.rb +243 -36
- data/lib/safe_memoize/configuration.rb +8 -0
- data/lib/safe_memoize/custom_key_methods.rb +11 -2
- data/lib/safe_memoize/extension.rb +88 -0
- data/lib/safe_memoize/fiber_local_methods.rb +2 -1
- data/lib/safe_memoize/hooks_methods.rb +2 -0
- data/lib/safe_memoize/inspection_methods.rb +34 -5
- data/lib/safe_memoize/public_methods.rb +3 -2
- data/lib/safe_memoize/public_metrics_methods.rb +4 -3
- data/lib/safe_memoize/version.rb +1 -1
- data/lib/safe_memoize.rb +141 -0
- data/sig/safe_memoize.rbs +35 -2
- metadata +3 -1
|
@@ -13,7 +13,8 @@ module SafeMemoize
|
|
|
13
13
|
|
|
14
14
|
def memo_matcher_for(method_name, args, kwargs)
|
|
15
15
|
if args.empty? && kwargs.empty?
|
|
16
|
-
|
|
16
|
+
effective = resolve_memo_key_name(method_name)
|
|
17
|
+
->(key) { key[0] == effective }
|
|
17
18
|
else
|
|
18
19
|
cache_key = compute_cache_key(method_name, args, kwargs)
|
|
19
20
|
->(key) { key == cache_key }
|
|
@@ -28,7 +29,8 @@ module SafeMemoize
|
|
|
28
29
|
entries = cache.to_a
|
|
29
30
|
return entries unless method_name
|
|
30
31
|
|
|
31
|
-
|
|
32
|
+
effective = resolve_memo_key_name(method_name)
|
|
33
|
+
entries.select { |(cache_key, _)| cache_key[0] == effective }
|
|
32
34
|
end
|
|
33
35
|
|
|
34
36
|
def safe_memo_count_for(method_name)
|
|
@@ -57,14 +59,14 @@ module SafeMemoize
|
|
|
57
59
|
# Custom keys are [method, custom_key] (2 elements); default keys are
|
|
58
60
|
# [method, args, kwargs] (3 elements). Detect and surface accordingly.
|
|
59
61
|
if cache_key.length == 2
|
|
60
|
-
|
|
62
|
+
effective_name, custom_key = cache_key
|
|
61
63
|
payload = {custom_key: custom_key}
|
|
62
64
|
else
|
|
63
|
-
|
|
65
|
+
effective_name, args, kwargs = cache_key
|
|
64
66
|
payload = {args: args, kwargs: kwargs}
|
|
65
67
|
end
|
|
66
68
|
|
|
67
|
-
payload[:method] =
|
|
69
|
+
payload[:method] = safe_memo_bare_method_name(effective_name) if include_method
|
|
68
70
|
payload[:value] = memo_record_value(value) if include_value
|
|
69
71
|
payload
|
|
70
72
|
end
|
|
@@ -73,6 +75,33 @@ module SafeMemoize
|
|
|
73
75
|
[method_name.to_sym, deep_freeze_copy(args), deep_freeze_copy(kwargs)]
|
|
74
76
|
end
|
|
75
77
|
|
|
78
|
+
# Returns the active namespace string for a bare method name, or nil.
|
|
79
|
+
# Priority: per-method namespace: option > class safe_memoize_namespace > global.
|
|
80
|
+
#
|
|
81
|
+
# Uses instance_variable_get throughout so this is safe to call from worker
|
|
82
|
+
# Ractors (reads only; lazy-init ivars must not be triggered in that context).
|
|
83
|
+
def __safe_memo_resolve_namespace__(method_name)
|
|
84
|
+
ns_map = self.class.instance_variable_get(:@__safe_memo_method_namespaces__)
|
|
85
|
+
(ns_map && ns_map[method_name]) ||
|
|
86
|
+
self.class.instance_variable_get(:@__safe_memoize_namespace__) ||
|
|
87
|
+
SafeMemoize.instance_variable_get(:@configuration)&.namespace
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Returns the effective cache key sym for a bare method name, applying any
|
|
91
|
+
# active namespace (per-method > class-level > global).
|
|
92
|
+
def resolve_memo_key_name(method_name)
|
|
93
|
+
ns = __safe_memo_resolve_namespace__(method_name)
|
|
94
|
+
ns ? :"#{ns}:#{method_name}" : method_name
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Strips the namespace prefix from an effective key sym, returning the bare
|
|
98
|
+
# method name. When no namespace is present the sym is returned unchanged.
|
|
99
|
+
def safe_memo_bare_method_name(key_sym)
|
|
100
|
+
s = key_sym.to_s
|
|
101
|
+
colon_idx = s.index(":")
|
|
102
|
+
colon_idx ? s[(colon_idx + 1)..].to_sym : key_sym
|
|
103
|
+
end
|
|
104
|
+
|
|
76
105
|
def deep_freeze_copy(obj)
|
|
77
106
|
case obj
|
|
78
107
|
when Array
|
|
@@ -214,7 +214,8 @@ module SafeMemoize
|
|
|
214
214
|
|
|
215
215
|
with_memo_lock do
|
|
216
216
|
cache = memo_cache_or_nil || {}
|
|
217
|
-
|
|
217
|
+
effective = method_name && resolve_memo_key_name(method_name)
|
|
218
|
+
entries = effective ? cache.select { |key, _| key[0] == effective } : cache.dup
|
|
218
219
|
entries.select! { |_, record| memo_record_live?(record) }
|
|
219
220
|
entries.transform_values { |record| memo_record_value(record) }
|
|
220
221
|
end
|
|
@@ -401,7 +402,7 @@ module SafeMemoize
|
|
|
401
402
|
|
|
402
403
|
age = (now - record[:cached_at]).round(6) if record[:cached_at]
|
|
403
404
|
|
|
404
|
-
metrics_key =
|
|
405
|
+
metrics_key = compute_cache_key(method_name, args, kwargs)
|
|
405
406
|
entry_metrics = memo_metrics_store[metrics_key] || {hits: 0, misses: 0}
|
|
406
407
|
|
|
407
408
|
custom_key = (cache_key.length == 2) ? cache_key[1] : nil
|
|
@@ -26,7 +26,8 @@ module SafeMemoize
|
|
|
26
26
|
method_name = method_name.to_sym
|
|
27
27
|
|
|
28
28
|
with_memo_lock do
|
|
29
|
-
|
|
29
|
+
effective = resolve_memo_key_name(method_name)
|
|
30
|
+
metrics = memo_metrics_store.select { |key, _| key[0] == effective }
|
|
30
31
|
return empty_stats.merge(method: method_name) if metrics.empty?
|
|
31
32
|
|
|
32
33
|
aggregate_metrics(metrics, include_method: false).merge(method: method_name)
|
|
@@ -73,13 +74,13 @@ module SafeMemoize
|
|
|
73
74
|
avg_time = total_misses.zero? ? 0.0 : (total_time / total_misses).round(6)
|
|
74
75
|
|
|
75
76
|
entries = metrics.map do |cache_key, stats|
|
|
76
|
-
|
|
77
|
+
effective_name, args, _kwargs = cache_key
|
|
77
78
|
entry_calls = stats[:hits] + stats[:misses]
|
|
78
79
|
entry_hit_rate = entry_calls.zero? ? 0.0 : (stats[:hits].to_f / entry_calls * 100).round(2)
|
|
79
80
|
|
|
80
81
|
entry = {args: args, hits: stats[:hits], misses: stats[:misses],
|
|
81
82
|
hit_rate: entry_hit_rate, computation_time: stats[:total_time].round(6)}
|
|
82
|
-
entry[:method] =
|
|
83
|
+
entry[:method] = safe_memo_bare_method_name(effective_name) if include_method
|
|
83
84
|
entry
|
|
84
85
|
end
|
|
85
86
|
|
data/lib/safe_memoize/version.rb
CHANGED
data/lib/safe_memoize.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "safe_memoize/version"
|
|
4
4
|
require_relative "safe_memoize/configuration"
|
|
5
|
+
require_relative "safe_memoize/extension"
|
|
5
6
|
require_relative "safe_memoize/stores/base"
|
|
6
7
|
require_relative "safe_memoize/stores/memory"
|
|
7
8
|
require_relative "safe_memoize/adapters/statsd"
|
|
@@ -54,6 +55,19 @@ module SafeMemoize
|
|
|
54
55
|
# Rescue this to catch any error raised by the library itself.
|
|
55
56
|
class Error < StandardError; end
|
|
56
57
|
|
|
58
|
+
# @api private — sentinel distinguishing "not passed" from explicit nil/false in memoize kwargs
|
|
59
|
+
UNSET = Object.new.freeze
|
|
60
|
+
|
|
61
|
+
# @api private
|
|
62
|
+
SHARED_CACHE_REGISTRY = {}
|
|
63
|
+
# @api private
|
|
64
|
+
SHARED_CACHE_MUTEX = Mutex.new
|
|
65
|
+
|
|
66
|
+
# @api private
|
|
67
|
+
EXTENSION_REGISTRY = {}
|
|
68
|
+
# @api private
|
|
69
|
+
EXTENSION_MUTEX = Mutex.new
|
|
70
|
+
|
|
57
71
|
include InstanceMethods
|
|
58
72
|
|
|
59
73
|
# @api private
|
|
@@ -102,4 +116,131 @@ module SafeMemoize
|
|
|
102
116
|
handler = configuration.on_deprecation
|
|
103
117
|
handler ? handler.call(text) : warn(text)
|
|
104
118
|
end
|
|
119
|
+
|
|
120
|
+
# Returns the named shared cache store, creating a new in-process
|
|
121
|
+
# {Stores::Memory} instance if one has not been registered under +name+.
|
|
122
|
+
#
|
|
123
|
+
# Use {register_shared_cache} to supply a custom adapter (e.g. Redis) before
|
|
124
|
+
# any class that references the same name is loaded.
|
|
125
|
+
#
|
|
126
|
+
# @param name [String] the logical cache name
|
|
127
|
+
# @return [Stores::Base]
|
|
128
|
+
def self.shared_cache(name)
|
|
129
|
+
SHARED_CACHE_MUTEX.synchronize do
|
|
130
|
+
SHARED_CACHE_REGISTRY[name] ||= Stores::Memory.new
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Registers a custom store under +name+, replacing any existing entry.
|
|
135
|
+
#
|
|
136
|
+
# Must be called *before* any class that references +name+ via +shared_cache:+
|
|
137
|
+
# is loaded, because the store is captured at +memoize+ definition time.
|
|
138
|
+
#
|
|
139
|
+
# @param name [String] the logical cache name
|
|
140
|
+
# @param store [Stores::Base] any {Stores::Base} subclass instance
|
|
141
|
+
# @return [Stores::Base] the registered store
|
|
142
|
+
# @raise [ArgumentError] if +store+ is not a {Stores::Base} instance
|
|
143
|
+
def self.register_shared_cache(name, store)
|
|
144
|
+
unless store.is_a?(Stores::Base)
|
|
145
|
+
raise ArgumentError, "store must be a SafeMemoize::Stores::Base instance (got #{store.class})"
|
|
146
|
+
end
|
|
147
|
+
SHARED_CACHE_MUTEX.synchronize { SHARED_CACHE_REGISTRY[name] = store }
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Clears all entries in the named shared cache without removing it from the
|
|
151
|
+
# registry. A no-op when no cache is registered under +name+.
|
|
152
|
+
#
|
|
153
|
+
# @param name [String]
|
|
154
|
+
# @return [void]
|
|
155
|
+
def self.clear_shared_cache(name)
|
|
156
|
+
SHARED_CACHE_MUTEX.synchronize { SHARED_CACHE_REGISTRY[name]&.clear }
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Removes the named shared cache from the registry entirely.
|
|
160
|
+
# Subsequent +shared_cache(name)+ calls will create a fresh store.
|
|
161
|
+
#
|
|
162
|
+
# @param name [String]
|
|
163
|
+
# @return [Stores::Base, nil] the removed store, or +nil+ if not present
|
|
164
|
+
def self.drop_shared_cache(name)
|
|
165
|
+
SHARED_CACHE_MUTEX.synchronize { SHARED_CACHE_REGISTRY.delete(name) }
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Returns a snapshot of the current registry as a plain +Hash+.
|
|
169
|
+
#
|
|
170
|
+
# @return [Hash{String => Stores::Base}]
|
|
171
|
+
def self.shared_caches
|
|
172
|
+
SHARED_CACHE_MUTEX.synchronize { SHARED_CACHE_REGISTRY.dup }
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Removes all named shared caches from the registry.
|
|
176
|
+
#
|
|
177
|
+
# Useful in test suite +after+ hooks to prevent state leaking between examples.
|
|
178
|
+
#
|
|
179
|
+
# @return [void]
|
|
180
|
+
def self.reset_shared_caches!
|
|
181
|
+
SHARED_CACHE_MUTEX.synchronize { SHARED_CACHE_REGISTRY.clear }
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Registers an extension under +name+.
|
|
185
|
+
#
|
|
186
|
+
# An extension is any Ruby object that optionally responds to the
|
|
187
|
+
# {Extension} interface: {Extension#handled_options}, {Extension#process_memoize_option},
|
|
188
|
+
# and {Extension#dispatch_cache_event}. Use {Extension} as a mixin to get a
|
|
189
|
+
# convenient DSL for defining these.
|
|
190
|
+
#
|
|
191
|
+
# @param name [Symbol, String] a unique identifier for this extension
|
|
192
|
+
# @param extension [Object] the extension object or module
|
|
193
|
+
# @return [Object] the registered extension
|
|
194
|
+
def self.register_extension(name, extension)
|
|
195
|
+
EXTENSION_MUTEX.synchronize { EXTENSION_REGISTRY[name.to_sym] = extension }
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Removes an extension from the registry.
|
|
199
|
+
#
|
|
200
|
+
# @param name [Symbol, String]
|
|
201
|
+
# @return [Object, nil] the removed extension, or +nil+ if not present
|
|
202
|
+
def self.unregister_extension(name)
|
|
203
|
+
EXTENSION_MUTEX.synchronize { EXTENSION_REGISTRY.delete(name.to_sym) }
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Returns a snapshot of all registered extensions as a +Hash+.
|
|
207
|
+
#
|
|
208
|
+
# @return [Hash{Symbol => Object}]
|
|
209
|
+
def self.extensions
|
|
210
|
+
EXTENSION_MUTEX.synchronize { EXTENSION_REGISTRY.dup }
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Removes all registered extensions.
|
|
214
|
+
#
|
|
215
|
+
# Useful in test suite +after+ hooks to prevent state leaking between examples.
|
|
216
|
+
#
|
|
217
|
+
# @return [void]
|
|
218
|
+
def self.reset_extensions!
|
|
219
|
+
EXTENSION_MUTEX.synchronize { EXTENSION_REGISTRY.clear }
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Returns the first registered extension that declares it handles +option_name+,
|
|
223
|
+
# or +nil+ if none does.
|
|
224
|
+
#
|
|
225
|
+
# @param option_name [Symbol]
|
|
226
|
+
# @return [Object, nil]
|
|
227
|
+
def self.extension_for_option(option_name)
|
|
228
|
+
sym = option_name.to_sym
|
|
229
|
+
EXTENSION_MUTEX.synchronize do
|
|
230
|
+
EXTENSION_REGISTRY.values.find do |ext|
|
|
231
|
+
ext.respond_to?(:handled_options) && ext.handled_options.include?(sym)
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Dispatches a cache lifecycle event to all registered extensions that
|
|
237
|
+
# respond to +dispatch_cache_event+.
|
|
238
|
+
#
|
|
239
|
+
# @api private
|
|
240
|
+
def self.dispatch_extension_events(event_type, klass, method_name, cache_key, record)
|
|
241
|
+
exts = EXTENSION_MUTEX.synchronize { EXTENSION_REGISTRY.values.dup }
|
|
242
|
+
exts.each do |ext|
|
|
243
|
+
ext.dispatch_cache_event(event_type, klass, method_name, cache_key, record) if ext.respond_to?(:dispatch_cache_event)
|
|
244
|
+
end
|
|
245
|
+
end
|
|
105
246
|
end
|
data/sig/safe_memoize.rbs
CHANGED
|
@@ -18,11 +18,37 @@ module SafeMemoize
|
|
|
18
18
|
@__safe_memo_shared_mutex__: Mutex?
|
|
19
19
|
@__safe_memo_shared_lru_order__: Hash[Symbol, Hash[memo_key, true]]?
|
|
20
20
|
|
|
21
|
+
UNSET: untyped
|
|
22
|
+
SHARED_CACHE_REGISTRY: Hash[String, Stores::Base]
|
|
23
|
+
SHARED_CACHE_MUTEX: Mutex
|
|
24
|
+
EXTENSION_REGISTRY: Hash[Symbol, untyped]
|
|
25
|
+
EXTENSION_MUTEX: Mutex
|
|
26
|
+
|
|
21
27
|
def self.prepended: (Class base) -> void
|
|
22
28
|
def self.configure: () { (Configuration) -> void } -> void
|
|
23
29
|
def self.configuration: () -> Configuration
|
|
24
30
|
def self.reset_configuration!: () -> Configuration
|
|
25
31
|
def self.deprecate: (String subject, message: String, horizon: String) -> void
|
|
32
|
+
def self.shared_cache: (String name) -> Stores::Base
|
|
33
|
+
def self.register_shared_cache: (String name, Stores::Base store) -> Stores::Base
|
|
34
|
+
def self.clear_shared_cache: (String name) -> void
|
|
35
|
+
def self.drop_shared_cache: (String name) -> Stores::Base?
|
|
36
|
+
def self.shared_caches: () -> Hash[String, Stores::Base]
|
|
37
|
+
def self.reset_shared_caches!: () -> void
|
|
38
|
+
def self.register_extension: (Symbol | String name, untyped extension) -> untyped
|
|
39
|
+
def self.unregister_extension: (Symbol | String name) -> untyped?
|
|
40
|
+
def self.extensions: () -> Hash[Symbol, untyped]
|
|
41
|
+
def self.reset_extensions!: () -> void
|
|
42
|
+
def self.extension_for_option: (Symbol option_name) -> untyped?
|
|
43
|
+
def self.dispatch_extension_events: (Symbol event_type, Class klass, Symbol method_name, untyped cache_key, untyped record) -> void
|
|
44
|
+
|
|
45
|
+
module Extension
|
|
46
|
+
def handles_option: (Symbol option_name) { (untyped value, Symbol method_name, Hash[Symbol, untyped] all_options) -> Hash[Symbol, untyped]? } -> void
|
|
47
|
+
def on_cache_event: (*Symbol event_types) { (Class klass, Symbol method_name, untyped cache_key, untyped record) -> void } -> void
|
|
48
|
+
def handled_options: () -> Array[Symbol]
|
|
49
|
+
def process_memoize_option: (Symbol option_name, untyped value, Symbol method_name, Hash[Symbol, untyped] all_options) -> Hash[Symbol, untyped]
|
|
50
|
+
def dispatch_cache_event: (Symbol event_type, Class klass, Symbol method_name, untyped cache_key, untyped record) -> void
|
|
51
|
+
end
|
|
26
52
|
|
|
27
53
|
class Configuration
|
|
28
54
|
attr_accessor default_ttl: Numeric?
|
|
@@ -33,15 +59,19 @@ module SafeMemoize
|
|
|
33
59
|
attr_accessor statsd_client: untyped
|
|
34
60
|
attr_accessor opentelemetry_tracer: untyped
|
|
35
61
|
attr_accessor default_store: Stores::Base?
|
|
62
|
+
attr_accessor namespace: String?
|
|
36
63
|
|
|
37
64
|
def initialize: () -> void
|
|
38
65
|
end
|
|
39
66
|
|
|
40
67
|
module ClassMethods
|
|
41
|
-
def memoize: (Symbol | String method_name, ?ttl: Numeric?, ?max_size: Integer?, ?ttl_refresh: bool, ?if: (^(untyped result) -> boolish)?, ?unless: (^(untyped result) -> boolish)?, ?shared: bool, ?key: (^(*untyped args, **untyped kwargs) -> untyped)?, ?store: Stores::Base?, ?fiber_local: bool, ?ractor_safe: bool) -> void
|
|
68
|
+
def memoize: (Symbol | String method_name, ?ttl: Numeric?, ?max_size: Integer?, ?ttl_refresh: bool, ?if: (^(untyped result) -> boolish)?, ?unless: (^(untyped result) -> boolish)?, ?shared: bool, ?key: (^(*untyped args, **untyped kwargs) -> untyped)?, ?store: Stores::Base?, ?fiber_local: bool, ?ractor_safe: bool, ?namespace: String?, ?shared_cache: String?, ?cache_bust: (^() -> untyped) | Symbol | nil, ?copy_on_read: bool, **untyped extension_options) -> void
|
|
42
69
|
def safe_memoize_store: () -> Stores::Base?
|
|
43
70
|
def safe_memoize_store=: (Stores::Base?) -> Stores::Base?
|
|
44
|
-
def
|
|
71
|
+
def safe_memoize_namespace: () -> String?
|
|
72
|
+
def safe_memoize_namespace=: (String?) -> String?
|
|
73
|
+
def safe_memoize_options: (**untyped opts) -> Hash[Symbol, untyped]?
|
|
74
|
+
def memoize_all: (?except: Array[Symbol | String], ?only: Array[Symbol | String], ?include_protected: bool, ?include_private: bool, ?ttl: Numeric?, ?max_size: Integer?, ?if: (^(untyped result) -> boolish)?, ?unless: (^(untyped result) -> boolish)?, ?shared: bool, ?key: (^(*untyped args, **untyped kwargs) -> untyped)?, ?fiber_local: bool, ?namespace: String?, ?shared_cache: String?, ?copy_on_read: bool) -> void
|
|
45
75
|
def reset_shared_memo: (Symbol | String method_name, *untyped args, **untyped kwargs) -> void
|
|
46
76
|
def reset_all_shared_memos: () -> void
|
|
47
77
|
def shared_memoized?: (Symbol | String method_name, *untyped args, **untyped kwargs) -> bool
|
|
@@ -55,6 +85,9 @@ module SafeMemoize
|
|
|
55
85
|
def __safe_memo_shared_mutex__: () -> Mutex
|
|
56
86
|
def __safe_memo_shared_lru_order__: () -> Hash[Symbol, Hash[memo_key, true]]
|
|
57
87
|
def __safe_memo_class_key_generators__: () -> Hash[Symbol, Proc]
|
|
88
|
+
def __safe_memo_method_namespaces__: () -> Hash[Symbol, String]
|
|
89
|
+
def __safe_memo_class_cache_bust_generators__: () -> Hash[Symbol, Proc | Symbol]
|
|
90
|
+
def __safe_memoize_defaults__: () -> Hash[Symbol, untyped]?
|
|
58
91
|
def memoized_method_visibility: (Symbol method_name) -> Symbol
|
|
59
92
|
end
|
|
60
93
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: safe_memoize
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Chuck Smith
|
|
@@ -39,6 +39,7 @@ executables: []
|
|
|
39
39
|
extensions: []
|
|
40
40
|
extra_rdoc_files: []
|
|
41
41
|
files:
|
|
42
|
+
- ".github/FUNDING.yml"
|
|
42
43
|
- ".github/workflows/ci.yml"
|
|
43
44
|
- ".github/workflows/release.yml"
|
|
44
45
|
- ".yardopts"
|
|
@@ -61,6 +62,7 @@ files:
|
|
|
61
62
|
- lib/safe_memoize/class_methods.rb
|
|
62
63
|
- lib/safe_memoize/configuration.rb
|
|
63
64
|
- lib/safe_memoize/custom_key_methods.rb
|
|
65
|
+
- lib/safe_memoize/extension.rb
|
|
64
66
|
- lib/safe_memoize/fiber_local_methods.rb
|
|
65
67
|
- lib/safe_memoize/hooks_methods.rb
|
|
66
68
|
- lib/safe_memoize/inspection_methods.rb
|