bipolar_cache 0.4.0 → 0.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 590637758a1d41dcf7614360ca6c2b3497030558a34832091d710e5938079bae
4
- data.tar.gz: f1fe7e9f5644cab310a0239d521b40ad10286f33b802473f38f25ac09ba9a351
3
+ metadata.gz: 3bb404064cea70958b9164dce8ca19a2896ed8cceb9472af9e893a7d32a9ea83
4
+ data.tar.gz: 4cd2da217bb8347ea474949fc6a21d6accdb125e438f1091dbb746aa4c8ca4a3
5
5
  SHA512:
6
- metadata.gz: f2fbc688f2d14f886d03f64b85483ee09c8a23c400aba8c6d397aeb742887237c34b1de2d5b4702034a78e61204a6c76c76045be708c46069defe70375e0bdd6
7
- data.tar.gz: b2ba003ac34a61e45c2a7b57b3e3a138f867b2751b49fe4dd87998865658d0ee9c0c15672e9c695459e2cfad2fefbfa061e8b84364e5f8beb62904be71eea098
6
+ metadata.gz: 6c9ac5ae9a879b6e399a83f5a8ff8cdbdb4e7180917b52f40c60b9f38e2309c90f96e98cfe909e8e56527c40c94b76a0ffb3b09a2380aa4ab8f5213031069c5e
7
+ data.tar.gz: 5a1ee634187ad333b3d5d728d4e5cd957569f5806c4ffb1c666dbb066ec3884d484225ad36d856eb6fcc657bcd70ebf0fdcaba00ac9464662cd76c4070c3a7f3
data/CHANGELOG.md ADDED
@@ -0,0 +1,52 @@
1
+ # Changelog
2
+
3
+ ## 0.5.0 - 2026-10-14
4
+
5
+ ### Breaking changes
6
+
7
+ - `BipolarCache.read!` raises `ArgumentError` for missing, unknown or non-callable
8
+ keywords. Before, a missing `actual:` surfaced as a `NoMethodError` that `rescue:` swallowed,
9
+ and misspelled keywords were silently ignored.
10
+ - `BipolarCache.read!`: when only the cache write fails, the error goes to `rescue:` (or is
11
+ re-raised without one) but the value that was read is still returned instead of the `rescue:` result.
12
+ - Sequel plugin: procs passed to `bipolar_count_cache` run in the scope of the model instance
13
+ (`self` used to be the class), so they can call instance methods and associations directly.
14
+ - Sequel plugin: for `Sequel::Model` instances the default cache write is a bare dataset `UPDATE`
15
+ of the cache column. It no longer runs validations and hooks, saves other unsaved columns or
16
+ inserts an unsaved record. `increment!`/`decrement!` use `coalesce(col, 0) + n` in SQL, so
17
+ concurrent increments are not lost and a `NULL` column is handled. Your own `update:` is
18
+ still called with the new value.
19
+ - Sequel plugin: invalid options (wrong type, `chance` below 0, above 100 or `NaN`) raise
20
+ `ArgumentError` when `bipolar_count_cache` is declared instead of on first use.
21
+ - Sequel plugin: the `bcsp_*` helper methods are private. `fetch_or_build_procs` is gone: procs are
22
+ built per call instead of being memoized on the instance.
23
+
24
+ ### Changed
25
+
26
+ - `BipolarCache.read!`: `if:` is optional (default: caching enabled) and every
27
+ callable may be any object responding to `call`, not only a `Proc`.
28
+ - Sequel plugin: `if:`, `chance:` and `rescue:` accept a Symbol/String naming an instance method,
29
+ like `actual:`, `cached:` and `update:` already did. `if: :method` used to be ignored,
30
+ which left caching switched on.
31
+
32
+ ### Fixed
33
+
34
+ - Sequel plugin: two `bipolar_count_cache` declarations with the same association name no longer
35
+ share one set of procs.
36
+ - Sequel plugin: a `dup`/`clone` no longer reads and writes the original record's cache.
37
+ - Sequel plugin: reading a count on a frozen model no longer raises `FrozenError`.
38
+ - Sequel plugin: `refresh!` goes through `rescue:` like `increment!`/`decrement!`, and those two
39
+ no longer replace the original error with a `NoMethodError`.
40
+ - Sequel plugin: a model whose count was read can be `Marshal.dump`ed again. The memoized procs
41
+ lived in an instance variable and made `Marshal.dump` raise `TypeError`.
42
+ - Sequel plugin: with the default `chance`, a `NULL` cache column is a miss. It used to be a hit 10% of
43
+ the time and `comments_count` returned `nil`.
44
+ - Sequel plugin: the default cache write clears the cache column from `changed_columns`, so a later
45
+ `save`/`save_changes` no longer writes a stale value over a newer one.
46
+
47
+ ### Added
48
+
49
+ - Type signatures in `sig/bipolar_cache.rbs`.
50
+ - Tests against real `Sequel::Model` instances, both with Sequel's mock adapter and with an
51
+ in-memory SQLite database; `sequel` and `sqlite3` are development dependencies.
52
+ - Ruby 4.0 in the CI matrix.
data/README.md CHANGED
@@ -47,7 +47,7 @@ $ gem install bipolar_cache
47
47
 
48
48
  ### Core API
49
49
 
50
- `BipolarCache.read!` accepts six keyword arguments, all callables (procs/lambdas):
50
+ `BipolarCache.read!` accepts six keyword arguments, all callables (procs, lambdas, or anything that responds to `call`):
51
51
 
52
52
  ```ruby
53
53
  result = BipolarCache.read!(
@@ -65,10 +65,19 @@ result = BipolarCache.read!(
65
65
  | `actual` | yes | Callable that returns the real value (e.g., a database query) |
66
66
  | `cached` | yes | Callable that returns the cached value |
67
67
  | `chance` | yes | Callable that receives the cached value and returns a float 0.0-1.0. Higher = more likely to use cache. |
68
- | `if` | yes | Callable that returns true/false. When false, always returns the actual value. |
68
+ | `if` | no | Callable that returns true/false. When false, always returns the actual value. Defaults to always enabled. |
69
69
  | `update` | yes | Callable that receives the actual value and persists it to the cache |
70
70
  | `rescue` | no | Callable that receives a `StandardError`. Without it, errors re-raise. |
71
71
 
72
+ Missing, unknown (e.g. a misspelled `chnace:`) or non-callable arguments raise `ArgumentError`, even when `rescue` is given.
73
+
74
+ #### Errors
75
+
76
+ - An error while reading (`if`, `cached`, `chance` or `actual`) goes to `rescue`, and its return value becomes the result.
77
+ - An error while writing the cache (`update`) is also passed to `rescue`, but the result is still the actual value
78
+ that was just read. A failed cache write never replaces a good answer.
79
+ - Without `rescue`, both kinds of error are re-raised.
80
+
72
81
  ### Sequel Plugin (alpha)
73
82
 
74
83
  For [Sequel](https://github.com/jeremyevans/sequel) models, the plugin generates all the procs automatically. Your model needs a `_count_cache` column (e.g., `comments_count_cache`):
@@ -91,8 +100,8 @@ This generates four instance methods:
91
100
  ```ruby
92
101
  user.comments_count # probabilistic: returns cached or actual count
93
102
  user.comments_count_refresh! # force: computes actual and updates cache
94
- user.comments_count_increment! # bump cache by 1 (or by: n)
95
- user.comments_count_decrement! # drop cache by 1 (or by: n)
103
+ user.comments_count_increment! # bump cache by 1 (or by: n), returns the new value
104
+ user.comments_count_decrement! # drop cache by 1 (or by: n), returns the new value
96
105
  ```
97
106
 
98
107
  #### Options
@@ -103,11 +112,34 @@ bipolar_count_cache :comments,
103
112
  chance: 75, # fixed probability as percentage (default: adaptive)
104
113
  actual: -> { comments_dataset.where(visible: true).count }, # custom actual proc
105
114
  cached: :my_cache_column, # custom cache column/method name
106
- update: ->(v) { set(my_col: v) }, # custom update proc
115
+ update: ->(v) { update(my_cache_column: v) }, # custom update proc
107
116
  rescue: ->(e) { log(e); nil }, # custom error handler
108
117
  if: -> { !new? } # disable caching for unsaved records
109
118
  ```
110
119
 
120
+ Procs run in the scope of the model instance, so they can call its methods and associations directly.
121
+ `actual`, `cached`, `update`, `if`, `chance` and `rescue` also accept a Symbol (or String) naming an
122
+ instance method, which receives the same arguments the proc would:
123
+
124
+ ```ruby
125
+ bipolar_count_cache :comments, if: :caching_enabled?, chance: :hit_chance, rescue: :report_error
126
+ ```
127
+
128
+ Options are checked when `bipolar_count_cache` is declared: a value of the wrong type, or a numeric `chance`
129
+ outside 0-100 (or `NaN`), raises `ArgumentError` at class-load time. A method name that the model doesn't
130
+ respond to raises `ArgumentError` the first time the cache is used.
131
+
132
+ #### How the cache is written
133
+
134
+ For a `Sequel::Model` the default write is a plain `UPDATE` of the cache column. It does not run validations or
135
+ hooks, does not save other unsaved changes on the record, and does nothing in the database for a record that
136
+ isn't saved yet (the new value only lives in memory). `increment!` and `decrement!` are done in SQL
137
+ (`cache = coalesce(cache, 0) + n`), so concurrent bumps don't overwrite each other and a `NULL` column counts as 0.
138
+
139
+ Passing your own `update:` replaces all of this: it is called with the new value, whatever it does.
140
+
141
+ By default errors are logged with `warn` and the method returns `0`; pass `rescue:` to change that.
142
+
111
143
  #### Chance values
112
144
 
113
145
  The `chance` option controls cache-hit probability:
@@ -3,156 +3,222 @@
3
3
  require_relative "../../bipolar_cache"
4
4
 
5
5
  module BipolarCache
6
+ # NOTE: inside this namespace a bare +Sequel+ is BipolarCache::Sequel;
7
+ # reach the gem with +::Sequel+.
6
8
  module Sequel
7
9
  module PluginAlpha
8
10
  module ClassMethods
9
11
  def bipolar_count_cache(name, **opts)
12
+ Options.validate!(opts)
10
13
  opts = opts.merge({ name: name })
11
14
  method_name = opts[:method] || "#{name}_count"
12
15
 
13
16
  define_method method_name do
14
- bcsp_cache(**fetch_or_build_procs(**opts))
17
+ bcsp_cache(**bcsp_build_procs(**opts))
15
18
  end
16
19
 
17
20
  define_method "#{method_name}_refresh!" do
18
- procs = fetch_or_build_procs(**opts)
19
- actual_value = procs[:actual].call
20
- procs[:update].call(actual_value) if actual_value != procs[:cached].call
21
- actual_value
21
+ procs = bcsp_build_procs(**opts)
22
+
23
+ bcsp_guarded(procs) do
24
+ actual_value = procs[:actual].call
25
+ procs[:update].call(actual_value) if actual_value != procs[:cached].call
26
+ actual_value
27
+ end
22
28
  end
23
29
 
24
30
  define_method "#{method_name}_increment!" do |by: 1|
25
- procs = fetch_or_build_procs(**opts)
26
- new_value = by + (procs[:cached].call || 0)
27
- procs[:update].call(new_value)
28
- new_value
29
- rescue StandardError => e
30
- raise unless procs[:rescue].is_a?(Proc)
31
-
32
- procs[:rescue].call(e)
31
+ bcsp_adjust_cache!(opts, by)
33
32
  end
34
33
 
35
34
  define_method "#{method_name}_decrement!" do |by: 1|
36
- procs = fetch_or_build_procs(**opts)
37
- new_value = (-by) + (procs[:cached].call || 0)
38
- procs[:update].call(new_value)
39
- new_value
40
- rescue StandardError => e
41
- raise unless procs[:rescue].is_a?(Proc)
42
-
43
- procs[:rescue].call(e)
35
+ bcsp_adjust_cache!(opts, -by)
44
36
  end
45
37
  end
46
38
  end
47
39
 
48
- def self.included(base)
49
- base.extend ClassMethods
50
- end
40
+ module Options
41
+ class << self
42
+ # Checks everything about the options that can be checked without an
43
+ # instance, so a bad declaration fails when the class is loaded.
44
+ def validate!(opts)
45
+ %i[actual cached update if rescue chance].each { |key| validate_option!(key, opts[key]) }
46
+ end
51
47
 
52
- def bcsp_cache(**procs)
53
- BipolarCache.read!(**procs)
48
+ # Turns a numeric chance into a probability: 0..1 is taken as is,
49
+ # anything above 1 is a percentage.
50
+ def chance_probability(chance)
51
+ raise ArgumentError, "chance must be a number, got NaN" if chance.is_a?(Float) && chance.nan?
52
+ raise ArgumentError, "chance must be >= 0, got #{chance}" if chance.negative?
53
+ raise ArgumentError, "chance must be <= 100, got #{chance}" if chance > 100
54
+
55
+ chance > 1 ? chance / 100.0 : chance
56
+ end
57
+
58
+ private
59
+
60
+ def validate_option!(key, value)
61
+ return if value.nil? || value.is_a?(Proc) || value.is_a?(Symbol) || value.is_a?(String)
62
+ return if key == :if && [true, false].include?(value)
63
+ return chance_probability(value) if key == :chance && (value.is_a?(Integer) || value.is_a?(Float))
64
+
65
+ raise ArgumentError, "#{key} has an unsupported value: #{value.inspect}"
66
+ end
67
+ end
54
68
  end
55
69
 
56
- def fetch_or_build_procs(**opts)
57
- # create cached store for instance procs, if none
58
- instance_variable_set(:@bcsp_proc_store, {}) unless instance_variable_defined?(:@bcsp_proc_store)
59
-
60
- if instance_variable_get(:@bcsp_proc_store)[opts[:name]].nil?
61
- instance_variable_get(:@bcsp_proc_store)[opts[:name]] =
62
- {
63
- actual: bcsp_proc_actual_from(**opts),
64
- cached: bcsp_proc_cached_from(**opts),
65
- update: bcsp_proc_update_from(**opts),
66
- chance: bcsp_proc_chance_from(**opts),
67
- rescue: bcsp_proc_rescue_from(**opts),
68
- if: bcsp_proc_if_from(**opts)
69
- }.freeze
70
+ # Turns the declaration options into the callables BipolarCache.read!
71
+ # expects, all bound to the instance they are built on.
72
+ module Builders
73
+ private
74
+
75
+ def bcsp_build_procs(**opts)
76
+ {
77
+ actual: bcsp_proc_actual_from(**opts),
78
+ cached: bcsp_proc_cached_from(**opts),
79
+ update: bcsp_proc_update_from(**opts),
80
+ chance: bcsp_proc_chance_from(**opts),
81
+ rescue: bcsp_proc_rescue_from(**opts),
82
+ if: bcsp_proc_if_from(**opts)
83
+ }.freeze
70
84
  end
71
85
 
72
- # stored procs
73
- instance_variable_get(:@bcsp_proc_store)[opts[:name]]
74
- end
86
+ # Procs run in the scope of the instance; Symbols and Strings name a
87
+ # method of the instance that receives the call arguments.
88
+ def bcsp_bind(key, option)
89
+ case option
90
+ when Proc
91
+ ->(*args) { instance_exec(*args, &option) }
92
+ when Symbol, String
93
+ unless respond_to?(option, true)
94
+ raise ArgumentError, "#{key}: #{option.inspect} is not a method of #{self.class} " \
95
+ "(expected a Proc or the name of an instance method)"
96
+ end
97
+
98
+ ->(*args) { send(option, *args) }
99
+ else
100
+ raise ArgumentError, "#{key} must be a Proc, Symbol or String, got #{option.inspect}"
101
+ end
102
+ end
103
+
104
+ def bcsp_proc_actual_from(**opts)
105
+ return bcsp_bind(:actual, opts[:actual]) unless opts[:actual].nil?
75
106
 
76
- def bcsp_proc_actual_from(**opts)
77
- case opts[:actual]
78
- when Proc
79
- opts[:actual]
80
- when String, Symbol
81
- -> { send(opts[:actual]) }
82
- else
83
107
  -> { send("#{opts[:name]}_dataset").count }
84
108
  end
85
- end
86
109
 
87
- def bcsp_proc_cached_from(**opts)
88
- case opts[:cached]
89
- when Proc
90
- opts[:cached]
91
- when String, Symbol
92
- -> { send(opts[:cached]) }
93
- else
110
+ def bcsp_proc_cached_from(**opts)
111
+ return bcsp_bind(:cached, opts[:cached]) unless opts[:cached].nil?
112
+
94
113
  -> { send("#{opts[:name]}_count_cache") }
95
114
  end
96
- end
97
115
 
98
- def bcsp_proc_update_from(**opts)
99
- cache_name = if opts[:cached].is_a?(String) || opts[:cached].is_a?(Symbol)
100
- opts[:cached]
101
- else
102
- "#{opts[:name]}_count_cache"
103
- end
104
-
105
- case opts[:update]
106
- when Proc
107
- opts[:update]
108
- when String, Symbol
109
- ->(value) { send(opts[:update], value) }
110
- else
111
- ->(value) { update({ cache_name => value }) }
116
+ def bcsp_proc_update_from(**opts)
117
+ return bcsp_bind(:update, opts[:update]) unless opts[:update].nil?
118
+
119
+ column = bcsp_cache_column(opts)
120
+ ->(value) { bcsp_write_cache(column, value) }
112
121
  end
113
- end
114
122
 
115
- def bcsp_proc_chance_from(**opts)
116
- case opts[:chance]
117
- when Proc
118
- opts[:chance]
119
- when Integer, Float
120
- raise ArgumentError, "chance must be >= 0, got #{opts[:chance]}" if opts[:chance].negative?
121
- raise ArgumentError, "chance must be <= 100, got #{opts[:chance]}" if opts[:chance] > 100
122
-
123
- normalised_chance = if opts[:chance] > 1
124
- opts[:chance] / 100.0
125
- else
126
- opts[:chance]
127
- end
128
- ->(_value) { normalised_chance }
129
- else # default
130
- ->(value) { (value || 0) < 10 ? 0.1 : 0.9 }
123
+ def bcsp_proc_chance_from(**opts)
124
+ case opts[:chance]
125
+ when nil # default
126
+ lambda { |value|
127
+ next 0 if value.nil? # a missing cache is never a hit: it would hand back nil
128
+
129
+ value < 10 ? 0.1 : 0.9
130
+ }
131
+ when Integer, Float
132
+ probability = Options.chance_probability(opts[:chance])
133
+ ->(_value) { probability }
134
+ else
135
+ bcsp_bind(:chance, opts[:chance])
136
+ end
131
137
  end
132
- end
133
138
 
134
- def bcsp_proc_rescue_from(**opts)
135
- if opts[:rescue].is_a? Proc
136
- opts[:rescue]
137
- else # default
139
+ def bcsp_proc_rescue_from(**opts)
140
+ return bcsp_bind(:rescue, opts[:rescue]) unless opts[:rescue].nil?
141
+
138
142
  lambda { |e|
139
143
  warn "[BipolarCache] #{e.class}: #{e.message}"
140
144
  0
141
145
  }
142
146
  end
147
+
148
+ def bcsp_proc_if_from(**opts)
149
+ case opts[:if]
150
+ when nil # default
151
+ -> { true }
152
+ when true, false
153
+ value = opts[:if]
154
+ -> { value }
155
+ else
156
+ bcsp_bind(:if, opts[:if])
157
+ end
158
+ end
159
+
160
+ def bcsp_cache_column(opts)
161
+ case opts[:cached]
162
+ when String, Symbol then opts[:cached].to_sym
163
+ else :"#{opts[:name]}_count_cache"
164
+ end
165
+ end
143
166
  end
144
167
 
145
- def bcsp_proc_if_from(**opts)
146
- case opts[:if]
147
- when Proc
148
- opts[:if]
149
- when TrueClass, FalseClass
150
- value = opts[:if]
151
- -> { value }
152
- else # default
153
- -> { true }
168
+ include Builders
169
+
170
+ def self.included(base)
171
+ base.extend ClassMethods
172
+ end
173
+
174
+ private
175
+
176
+ def bcsp_cache(**procs)
177
+ BipolarCache.read!(**procs)
178
+ end
179
+
180
+ def bcsp_guarded(procs)
181
+ yield
182
+ rescue StandardError => e
183
+ procs[:rescue].call(e)
184
+ end
185
+
186
+ def bcsp_adjust_cache!(opts, delta)
187
+ procs = bcsp_build_procs(**opts)
188
+
189
+ bcsp_guarded(procs) do
190
+ new_value = (procs[:cached].call || 0) + delta
191
+
192
+ if opts[:update].nil? && bcsp_sequel_model?
193
+ column = bcsp_cache_column(opts)
194
+ relative = ::Sequel.function(:coalesce, ::Sequel[column], 0) + delta
195
+ bcsp_write_cache(column, new_value, relative)
196
+ else
197
+ procs[:update].call(new_value)
198
+ end
199
+
200
+ new_value
154
201
  end
155
202
  end
203
+
204
+ def bcsp_sequel_model?
205
+ defined?(::Sequel::Model) && is_a?(::Sequel::Model)
206
+ end
207
+
208
+ # Default cache writer. A Sequel model gets a bare UPDATE of the cache
209
+ # column: no validations, hooks or other unsaved columns are involved,
210
+ # and +expression+ lets the database do the arithmetic. Anything else
211
+ # is asked to #update itself.
212
+ def bcsp_write_cache(column, value, expression = value)
213
+ return update({ column => value }) unless bcsp_sequel_model?
214
+
215
+ this.update(column => expression) unless new?
216
+ return if frozen?
217
+
218
+ values[column] = value
219
+ # the value is now in the database; a later save must not write it again
220
+ changed_columns.delete(column)
221
+ end
156
222
  end
157
223
  end
158
224
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BipolarCache
4
- VERSION = "0.4.0"
4
+ VERSION = "0.5.0"
5
5
  end
data/lib/bipolar_cache.rb CHANGED
@@ -5,6 +5,11 @@ require_relative "bipolar_cache/version"
5
5
  module BipolarCache
6
6
  class Error < StandardError; end
7
7
 
8
+ REQUIRED_KEYS = %i[actual cached chance update].freeze
9
+ OPTIONAL_KEYS = %i[if rescue].freeze
10
+ ALWAYS = -> { true }
11
+ private_constant :REQUIRED_KEYS, :OPTIONAL_KEYS, :ALWAYS
12
+
8
13
  ##
9
14
  # It may read actual value,
10
15
  # it may read cached value,
@@ -12,29 +17,60 @@ module BipolarCache
12
17
  # It depends on chance.
13
18
  # Call it at your peril.
14
19
  #
15
- # @param actual [Proc] callable to perform an operation (cacheable)
16
- # @param cached [Proc] callable to read cached value
17
- # @param chance [Proc(Object)] callable to compute chance of cache hit
18
- # @param if [Proc] callable to enable/disable caching
19
- # @param rescue [Proc(StandardError)] callable to be invoked on exception
20
- # @param update [Proc(Object)] callable to update cached value
20
+ # Errors raised while reading are passed to +rescue+ (re-raised without it).
21
+ # An error raised while writing the cache is reported to +rescue+ the same
22
+ # way, but the value that was read is still returned.
23
+ #
24
+ # @param actual [#call] callable to perform an operation (cacheable)
25
+ # @param cached [#call] callable to read cached value
26
+ # @param chance [#call(Object)] callable to compute chance of cache hit
27
+ # @param if [#call] callable to enable/disable caching (default: enabled)
28
+ # @param rescue [#call(StandardError)] callable to be invoked on exception
29
+ # @param update [#call(Object)] callable to update cached value
21
30
  # @return [Object]
31
+ # @raise [ArgumentError] on missing, unknown or non-callable arguments
22
32
  def self.read!(**procs)
23
- return procs[:actual].call unless procs[:if].call
33
+ procs = normalize!(procs)
34
+ rescuer = procs[:rescue]
24
35
 
25
- cached_value = procs[:cached].call
36
+ begin
37
+ return procs[:actual].call unless procs[:if].call
38
+
39
+ cached_value = procs[:cached].call
40
+ return cached_value if procs[:chance].call(cached_value) > rand
26
41
 
27
- if procs[:chance].call(cached_value) > rand
28
- cached_value
29
- else
30
42
  actual_value = procs[:actual].call
43
+ rescue StandardError => e
44
+ raise unless rescuer
45
+
46
+ return rescuer.call(e)
47
+ end
48
+
49
+ begin
31
50
  procs[:update].call(actual_value) if cached_value != actual_value
51
+ rescue StandardError => e
52
+ raise unless rescuer
53
+
54
+ rescuer.call(e)
55
+ end
56
+
57
+ actual_value
58
+ end
59
+
60
+ def self.normalize!(procs)
61
+ procs = procs.compact
62
+
63
+ unknown = procs.keys - REQUIRED_KEYS - OPTIONAL_KEYS
64
+ raise ArgumentError, "unknown keywords: #{unknown.map(&:inspect).join(", ")}" unless unknown.empty?
65
+
66
+ missing = REQUIRED_KEYS - procs.keys
67
+ raise ArgumentError, "missing keywords: #{missing.map(&:inspect).join(", ")}" unless missing.empty?
32
68
 
33
- actual_value
69
+ procs.each do |key, callable|
70
+ raise ArgumentError, "#{key} must respond to #call, got #{callable.inspect}" unless callable.respond_to?(:call)
34
71
  end
35
- rescue StandardError => e
36
- raise unless procs[:rescue].is_a?(Proc)
37
72
 
38
- procs[:rescue].call(e)
73
+ { if: ALWAYS }.merge(procs)
39
74
  end
75
+ private_class_method :normalize!
40
76
  end
@@ -1,4 +1,50 @@
1
1
  module BipolarCache
2
2
  VERSION: String
3
- # See the writing guide of rbs: https://github.com/ruby/rbs#guides
3
+
4
+ class Error < StandardError
5
+ end
6
+
7
+ interface _Call
8
+ def call: () -> untyped
9
+ end
10
+
11
+ interface _CallWithValue
12
+ def call: (untyped value) -> untyped
13
+ end
14
+
15
+ interface _CallWithError
16
+ def call: (StandardError error) -> untyped
17
+ end
18
+
19
+ def self.read!: (
20
+ actual: _Call,
21
+ cached: _Call,
22
+ chance: _CallWithValue,
23
+ update: _CallWithValue,
24
+ ?if: _Call?,
25
+ ?rescue: _CallWithError?
26
+ ) -> untyped
27
+
28
+ module Sequel
29
+ module PluginAlpha
30
+ # A Proc runs in the scope of the model instance; a Symbol or String
31
+ # names an instance method that receives the call arguments.
32
+ type callable = Proc | Symbol | String
33
+
34
+ def self.included: (Module base) -> void
35
+
36
+ module ClassMethods
37
+ def bipolar_count_cache: (
38
+ Symbol | String name,
39
+ ?method: Symbol | String,
40
+ ?actual: callable?,
41
+ ?cached: callable?,
42
+ ?update: callable?,
43
+ ?chance: callable | Integer | Float | nil,
44
+ ?if: callable | bool | nil,
45
+ ?rescue: callable?
46
+ ) -> void
47
+ end
48
+ end
49
+ end
4
50
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bipolar_cache
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andriy Tyurnikov
@@ -18,6 +18,7 @@ extensions: []
18
18
  extra_rdoc_files: []
19
19
  files:
20
20
  - ".rubocop.yml"
21
+ - CHANGELOG.md
21
22
  - README.md
22
23
  - Rakefile
23
24
  - lib/bipolar_cache.rb
@@ -44,7 +45,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
44
45
  - !ruby/object:Gem::Version
45
46
  version: '0'
46
47
  requirements: []
47
- rubygems_version: 4.0.8
48
+ rubygems_version: 4.0.20
48
49
  specification_version: 4
49
50
  summary: BipolarCache. Probabilistic caching toolkit.
50
51
  test_files: []