weathercock 1.0.0 → 1.2.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/CHANGELOG.md +21 -2
- data/README.md +28 -5
- data/Steepfile +13 -0
- data/lib/weathercock/key_builder.rb +46 -0
- data/lib/weathercock/scorable.rb +15 -78
- data/lib/weathercock/scorer.rb +104 -0
- data/lib/weathercock/version.rb +1 -1
- data/lib/weathercock.rb +2 -0
- data/sig/weathercock/key_builder.rbs +13 -0
- data/sig/weathercock/scorable.rbs +25 -0
- data/sig/weathercock/scorer.rbs +20 -0
- data/sig/weathercock.rbs +15 -1
- metadata +8 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e6f2f450b510bee3dbea48bf0b4e7e37ef24040d82141caaabe45d6dce3c3cb6
|
|
4
|
+
data.tar.gz: ab78b22090a84493d97b3f5deca74963f150d3ea21af0b47408babb64fdb7985
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e10e7fee6c2e629c994c097920af3226a3f6fdde65a18aed9a85b5556ae6d0a4b4abf78029b833d50d88581745f2517911177ebaee78bbe7de6aef2c78eacb49
|
|
7
|
+
data.tar.gz: 4a2089ecaceb03a54cb1462504f9b946145b81837db87c4a497650164180c56533246f851ab0c1e1c8f84a337f06faf767c782960e8a9ac6273149f0e69f5766
|
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## v1.2.0 - 2026-09-16
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- `union` now reuses an existing destination key instead of always recomputing it, so its 900 second TTL works as a cache
|
|
8
|
+
- `union_dest` now includes `decay_factor` so decayed and non-decayed results no longer share a cache entry
|
|
9
|
+
|
|
10
|
+
## v1.1.0 - 2026-04-17
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `#rank` returns the 1-indexed ranking position of an instance; returns `nil` if unranked
|
|
15
|
+
- `#remove_hits` removes all recorded hits for an instance across all keys
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- Bucket count unified to 90 across all time granularities; data is retained longer
|
|
20
|
+
- `hit_counts` now uses a single ZMSCORE call instead of N ZSCORE calls
|
|
21
|
+
|
|
22
|
+
## v1.0.0 - 2026-04-15
|
|
4
23
|
|
|
5
24
|
### Added
|
|
6
25
|
|
|
@@ -10,6 +29,6 @@
|
|
|
10
29
|
|
|
11
30
|
- `require "weathercock"` now loads `Weathercock::Scorable` automatically
|
|
12
31
|
|
|
13
|
-
##
|
|
32
|
+
## v0.1.0 - 2026-04-15
|
|
14
33
|
|
|
15
34
|
Initial release.
|
data/README.md
CHANGED
|
@@ -53,18 +53,21 @@ myapp:article:views:2026-04 # monthly (TTL: 3 years)
|
|
|
53
53
|
|
|
54
54
|
```ruby
|
|
55
55
|
# All-time ranking
|
|
56
|
-
Article.top(:views)
|
|
56
|
+
Article.top(:views, limit: 10)
|
|
57
57
|
# => ["42", "7", "133", ...]
|
|
58
58
|
|
|
59
59
|
# Top article IDs by views over the last 7 days
|
|
60
|
-
Article.top(:views, days: 7)
|
|
60
|
+
Article.top(:views, days: 7, limit: 10)
|
|
61
61
|
|
|
62
62
|
# Using hours or months
|
|
63
|
-
Article.top(:views, hours: 24)
|
|
64
|
-
Article.top(:views, months: 3)
|
|
63
|
+
Article.top(:views, hours: 24, limit: 10)
|
|
64
|
+
Article.top(:views, months: 3, limit: 10)
|
|
65
65
|
|
|
66
66
|
# Exponential time decay (recent hits weighted higher)
|
|
67
|
-
Article.top(:views, days: 7, decay_factor: 0.9)
|
|
67
|
+
Article.top(:views, days: 7, decay_factor: 0.9, limit: 10)
|
|
68
|
+
|
|
69
|
+
# Retrieve all results
|
|
70
|
+
Article.top(:views, limit: nil)
|
|
68
71
|
```
|
|
69
72
|
|
|
70
73
|
### Counting
|
|
@@ -85,6 +88,26 @@ Article.hit_counts(:views, ids: [1, 2, 3], days: 7)
|
|
|
85
88
|
# => {"1" => 42, "2" => 15, "3" => 7}
|
|
86
89
|
```
|
|
87
90
|
|
|
91
|
+
### Ranking position
|
|
92
|
+
|
|
93
|
+
Get the 1-indexed rank of a specific instance:
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
article.rank(:views) # all-time
|
|
97
|
+
article.rank(:views, days: 7)
|
|
98
|
+
# => 1
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Returns `nil` if the item has no hits.
|
|
102
|
+
|
|
103
|
+
### Removing hits
|
|
104
|
+
|
|
105
|
+
Remove all recorded hits for an instance across all keys:
|
|
106
|
+
|
|
107
|
+
```ruby
|
|
108
|
+
article.remove_hits(:views)
|
|
109
|
+
```
|
|
110
|
+
|
|
88
111
|
## Development
|
|
89
112
|
|
|
90
113
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
data/Steepfile
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "date"
|
|
4
|
+
|
|
5
|
+
module Weathercock
|
|
6
|
+
class KeyBuilder
|
|
7
|
+
def initialize(namespace:, klass:)
|
|
8
|
+
@namespace = namespace
|
|
9
|
+
@klass_key = klass.name.gsub("::", "_").downcase
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def base(event)
|
|
13
|
+
"#{@namespace}:#{@klass_key}:#{event}"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def total(base)
|
|
17
|
+
"#{base}:total"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def bucket(base, type, time)
|
|
21
|
+
case type
|
|
22
|
+
when :hours then "#{base}:#{time.strftime("%Y-%m-%d-%H")}"
|
|
23
|
+
when :days then "#{base}:#{time.strftime("%Y-%m-%d")}"
|
|
24
|
+
when :months then "#{base}:#{time.strftime("%Y-%m")}"
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def window_keys(base, type, count)
|
|
29
|
+
now = Time.now
|
|
30
|
+
case type
|
|
31
|
+
when :hours
|
|
32
|
+
count.times.map { |i| bucket(base, type, now - (i * 3600)) }
|
|
33
|
+
when :days
|
|
34
|
+
count.times.map { |i| bucket(base, type, now - (i * 86400)) }
|
|
35
|
+
when :months
|
|
36
|
+
d = Date.new(now.year, now.month)
|
|
37
|
+
count.times.map { |i| bucket(base, type, d << i) }
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def union_dest(base, type, count, decay_factor: nil)
|
|
42
|
+
dest = "#{base}:top:#{type}:#{count}"
|
|
43
|
+
decay_factor ? "#{dest}:decay:#{decay_factor}" : dest
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
data/lib/weathercock/scorable.rb
CHANGED
|
@@ -1,102 +1,39 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "date"
|
|
4
|
-
require_relative "../weathercock"
|
|
5
|
-
|
|
6
3
|
module Weathercock
|
|
7
4
|
module Scorable
|
|
8
|
-
WEATHERCOCK_BUCKET_TTLS = {
|
|
9
|
-
hours: 3 * 24 * 3600,
|
|
10
|
-
days: 3 * 30 * 86400,
|
|
11
|
-
months: 3 * 12 * 30 * 86400
|
|
12
|
-
}.freeze
|
|
13
|
-
|
|
14
5
|
def self.included(base)
|
|
15
6
|
base.extend(ClassMethods)
|
|
16
7
|
end
|
|
17
8
|
|
|
18
9
|
module ClassMethods
|
|
19
|
-
def
|
|
20
|
-
|
|
21
|
-
return Weathercock.config.redis.call("ZRANGE", "#{weathercock_base_key(event)}:total", 0, stop, "REV") if window.empty?
|
|
22
|
-
|
|
23
|
-
dest = weathercock_union(event, window, decay_factor: decay_factor)
|
|
24
|
-
Weathercock.config.redis.call("ZRANGE", dest, 0, stop, "REV")
|
|
10
|
+
def weathercock_scorer
|
|
11
|
+
@weathercock_scorer ||= Scorer.new(klass: self)
|
|
25
12
|
end
|
|
26
13
|
|
|
27
|
-
def
|
|
28
|
-
|
|
29
|
-
dest = window.empty? ? "#{weathercock_base_key(event)}:total" : weathercock_union(event, window)
|
|
30
|
-
ids.to_h { |id| [id.to_s, (redis.call("ZSCORE", dest, id.to_s) || "0").to_i] }
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def weathercock_base_key(event)
|
|
34
|
-
"#{Weathercock.config.namespace}:#{name.gsub("::", "_").downcase}:#{event}"
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
private
|
|
38
|
-
|
|
39
|
-
def weathercock_union(event, window, decay_factor: nil)
|
|
40
|
-
redis = Weathercock.config.redis
|
|
41
|
-
base = weathercock_base_key(event)
|
|
42
|
-
type, count = window.first
|
|
43
|
-
keys = weathercock_window_keys(base, type, count)
|
|
44
|
-
|
|
45
|
-
dest = "#{base}:top:#{type}:#{count}"
|
|
46
|
-
weights = decay_factor ? count.times.map { |i| (decay_factor**i).round(10) } : nil
|
|
47
|
-
zunionstore_args = ["ZUNIONSTORE", dest, keys.size, *keys]
|
|
48
|
-
zunionstore_args += ["WEIGHTS", *weights] if weights
|
|
49
|
-
redis.call(*zunionstore_args)
|
|
50
|
-
redis.call("EXPIRE", dest, 900)
|
|
51
|
-
dest
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def weathercock_window_keys(base, type, count)
|
|
55
|
-
now = Time.now
|
|
56
|
-
case type
|
|
57
|
-
when :hours
|
|
58
|
-
count.times.map { |i| weathercock_bucket_key(base, type, now - (i * 3600)) }
|
|
59
|
-
when :days
|
|
60
|
-
count.times.map { |i| weathercock_bucket_key(base, type, now - (i * 86400)) }
|
|
61
|
-
when :months
|
|
62
|
-
d = Date.new(now.year, now.month)
|
|
63
|
-
count.times.map { |i| weathercock_bucket_key(base, type, d << i) }
|
|
64
|
-
end
|
|
14
|
+
def top(event, limit:, decay_factor: nil, **window)
|
|
15
|
+
weathercock_scorer.top(event, limit: limit, decay_factor: decay_factor, **window)
|
|
65
16
|
end
|
|
66
17
|
|
|
67
|
-
def
|
|
68
|
-
|
|
69
|
-
when :hours then "#{base}:#{time.strftime("%Y-%m-%d-%H")}"
|
|
70
|
-
when :days then "#{base}:#{time.strftime("%Y-%m-%d")}"
|
|
71
|
-
when :months then "#{base}:#{time.strftime("%Y-%m")}"
|
|
72
|
-
end
|
|
18
|
+
def hit_counts(event, ids:, **window)
|
|
19
|
+
weathercock_scorer.hit_counts(event, ids: ids, **window)
|
|
73
20
|
end
|
|
74
21
|
end
|
|
75
22
|
|
|
76
23
|
def hit(event, increment: 1)
|
|
77
|
-
|
|
78
|
-
redis = Weathercock.config.redis
|
|
79
|
-
base = self.class.weathercock_base_key(event)
|
|
80
|
-
|
|
81
|
-
redis.pipelined do |p|
|
|
82
|
-
p.call("ZINCRBY", "#{base}:total", increment, id.to_s)
|
|
83
|
-
WEATHERCOCK_BUCKET_TTLS.each do |type, ttl|
|
|
84
|
-
key = self.class.send(:weathercock_bucket_key, base, type, now)
|
|
85
|
-
p.call("ZINCRBY", key, increment, id.to_s)
|
|
86
|
-
p.call("EXPIRE", key, ttl)
|
|
87
|
-
end
|
|
88
|
-
end
|
|
24
|
+
self.class.weathercock_scorer.hit(id, event, increment: increment)
|
|
89
25
|
end
|
|
90
26
|
|
|
91
27
|
def hit_count(event, **window)
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
28
|
+
self.class.weathercock_scorer.hit_count(id, event, **window)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def rank(event, **window)
|
|
32
|
+
self.class.weathercock_scorer.rank(id, event, **window)
|
|
33
|
+
end
|
|
96
34
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
score ? score.to_i : 0
|
|
35
|
+
def remove_hits(event)
|
|
36
|
+
self.class.weathercock_scorer.remove_hits(id, event)
|
|
100
37
|
end
|
|
101
38
|
end
|
|
102
39
|
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Weathercock
|
|
4
|
+
class Scorer
|
|
5
|
+
BUCKET_COUNT = 90
|
|
6
|
+
|
|
7
|
+
BUCKET_TTLS = {
|
|
8
|
+
hours: BUCKET_COUNT * 3600,
|
|
9
|
+
days: BUCKET_COUNT * 86400,
|
|
10
|
+
months: BUCKET_COUNT * 30 * 86400
|
|
11
|
+
}.freeze
|
|
12
|
+
|
|
13
|
+
def initialize(klass:)
|
|
14
|
+
@redis = Weathercock.config.redis
|
|
15
|
+
@key_builder = KeyBuilder.new(namespace: Weathercock.config.namespace, klass: klass)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def hit(id, event, increment: 1)
|
|
19
|
+
now = Time.now
|
|
20
|
+
base = @key_builder.base(event)
|
|
21
|
+
|
|
22
|
+
@redis.pipelined do |p|
|
|
23
|
+
p.call("ZINCRBY", @key_builder.total(base), increment, id.to_s)
|
|
24
|
+
BUCKET_TTLS.each do |type, ttl|
|
|
25
|
+
key = @key_builder.bucket(base, type, now)
|
|
26
|
+
p.call("ZINCRBY", key, increment, id.to_s)
|
|
27
|
+
p.call("EXPIRE", key, ttl)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def remove_hits(id, event)
|
|
33
|
+
base = @key_builder.base(event)
|
|
34
|
+
|
|
35
|
+
@redis.pipelined do |p|
|
|
36
|
+
p.call("ZREM", @key_builder.total(base), id.to_s)
|
|
37
|
+
BUCKET_TTLS.each_key do |type|
|
|
38
|
+
@key_builder.window_keys(base, type, BUCKET_COUNT).each do |key|
|
|
39
|
+
p.call("ZREM", key, id.to_s)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def hit_count(id, event, **window)
|
|
46
|
+
base = @key_builder.base(event)
|
|
47
|
+
if window.empty?
|
|
48
|
+
score = @redis.call("ZSCORE", @key_builder.total(base), id.to_s)
|
|
49
|
+
return score ? score.to_i : 0
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
dest = union(event, window)
|
|
53
|
+
score = @redis.call("ZSCORE", dest, id.to_s)
|
|
54
|
+
score ? score.to_i : 0
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def top(event, limit:, decay_factor: nil, **window)
|
|
58
|
+
base = @key_builder.base(event)
|
|
59
|
+
stop = limit ? limit - 1 : -1
|
|
60
|
+
return @redis.call("ZRANGE", @key_builder.total(base), 0, stop, "REV") if window.empty?
|
|
61
|
+
|
|
62
|
+
dest = union(event, window, decay_factor: decay_factor)
|
|
63
|
+
@redis.call("ZRANGE", dest, 0, stop, "REV")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def rank(id, event, **window)
|
|
67
|
+
base = @key_builder.base(event)
|
|
68
|
+
dest = window.empty? ? @key_builder.total(base) : union(event, window)
|
|
69
|
+
r = @redis.call("ZREVRANK", dest, id.to_s)
|
|
70
|
+
r ? r + 1 : nil
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def hit_counts(event, ids:, **window)
|
|
74
|
+
base = @key_builder.base(event)
|
|
75
|
+
dest = window.empty? ? @key_builder.total(base) : union(event, window)
|
|
76
|
+
scores = @redis.call("ZMSCORE", dest, *ids.map(&:to_s))
|
|
77
|
+
ids.zip(scores).to_h do |id, score|
|
|
78
|
+
[id.to_s, Integer(score || "0")] #: [String, Integer]
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def union(event, window, decay_factor: nil)
|
|
85
|
+
base = @key_builder.base(event)
|
|
86
|
+
type, count = window.first
|
|
87
|
+
dest = if decay_factor
|
|
88
|
+
@key_builder.union_dest(base, type, count, decay_factor: decay_factor)
|
|
89
|
+
else
|
|
90
|
+
@key_builder.union_dest(base, type, count)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
unless @redis.call("EXISTS", dest) == 1
|
|
94
|
+
keys = @key_builder.window_keys(base, type, count)
|
|
95
|
+
weights = decay_factor ? count.times.map { |i| (decay_factor**i).round(10) } : nil
|
|
96
|
+
zunionstore_args = ["ZUNIONSTORE", dest, keys.size, *keys]
|
|
97
|
+
zunionstore_args += ["WEIGHTS", *weights] if weights
|
|
98
|
+
@redis.call(*zunionstore_args)
|
|
99
|
+
end
|
|
100
|
+
@redis.call("EXPIRE", dest, 900)
|
|
101
|
+
dest
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
data/lib/weathercock/version.rb
CHANGED
data/lib/weathercock.rb
CHANGED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
class Weathercock::KeyBuilder
|
|
2
|
+
def initialize: (namespace: String, klass: Module) -> void
|
|
3
|
+
|
|
4
|
+
def base: (Symbol) -> String
|
|
5
|
+
|
|
6
|
+
def total: (String) -> String
|
|
7
|
+
|
|
8
|
+
def bucket: (String, :hours | :days | :months, Date | Time) -> String
|
|
9
|
+
|
|
10
|
+
def window_keys: (String, :hours | :days | :months, Integer) -> Array[String]
|
|
11
|
+
|
|
12
|
+
def union_dest: (String, :hours | :days | :months, Integer, ?decay_factor: Float) -> String
|
|
13
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module Weathercock::Scorable
|
|
2
|
+
def self.included: (Module) -> void
|
|
3
|
+
# steep infers self.class in instance methods as singleton(Scorable),
|
|
4
|
+
# so declare weathercock_scorer here to satisfy the type checker.
|
|
5
|
+
def self.weathercock_scorer: () -> Weathercock::Scorer
|
|
6
|
+
|
|
7
|
+
# Expected to be provided by the including class (e.g. ActiveRecord).
|
|
8
|
+
def id: () -> Weathercock::id
|
|
9
|
+
|
|
10
|
+
def hit: (Symbol, ?increment: Integer) -> void
|
|
11
|
+
|
|
12
|
+
def hit_count: (Symbol, ?hours: Integer, ?days: Integer, ?months: Integer) -> Integer
|
|
13
|
+
|
|
14
|
+
def rank: (Symbol, ?hours: Integer, ?days: Integer, ?months: Integer) -> Integer?
|
|
15
|
+
|
|
16
|
+
def remove_hits: (Symbol) -> void
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
module Weathercock::Scorable::ClassMethods : Module
|
|
20
|
+
def weathercock_scorer: () -> Weathercock::Scorer
|
|
21
|
+
|
|
22
|
+
def top: (Symbol, limit: Integer?, ?decay_factor: Float, ?hours: Integer, ?days: Integer, ?months: Integer) -> Array[String]
|
|
23
|
+
|
|
24
|
+
def hit_counts: (Symbol, ids: Array[Weathercock::id], ?hours: Integer, ?days: Integer, ?months: Integer) -> Hash[String, Integer]
|
|
25
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
class Weathercock::Scorer
|
|
2
|
+
BUCKET_COUNT: Integer
|
|
3
|
+
BUCKET_TTLS: Hash[:hours | :days | :months, Integer]
|
|
4
|
+
|
|
5
|
+
def initialize: (klass: Module) -> void
|
|
6
|
+
|
|
7
|
+
def hit: (Weathercock::id, Symbol, ?increment: Integer) -> void
|
|
8
|
+
|
|
9
|
+
def hit_count: (Weathercock::id, Symbol, ?hours: Integer, ?days: Integer, ?months: Integer) -> Integer
|
|
10
|
+
|
|
11
|
+
def union: (Symbol, Hash[untyped, untyped], ?decay_factor: Float) -> String
|
|
12
|
+
|
|
13
|
+
def top: (Symbol, limit: Integer?, ?decay_factor: Float, ?hours: Integer, ?days: Integer, ?months: Integer) -> Array[String]
|
|
14
|
+
|
|
15
|
+
def hit_counts: (Symbol, ids: Array[Weathercock::id], ?hours: Integer, ?days: Integer, ?months: Integer) -> Hash[String, Integer]
|
|
16
|
+
|
|
17
|
+
def rank: (Weathercock::id, Symbol, ?hours: Integer, ?days: Integer, ?months: Integer) -> Integer?
|
|
18
|
+
|
|
19
|
+
def remove_hits: (Weathercock::id, Symbol) -> void
|
|
20
|
+
end
|
data/sig/weathercock.rbs
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
module Weathercock
|
|
2
2
|
VERSION: String
|
|
3
|
-
|
|
3
|
+
|
|
4
|
+
type id = Integer | String
|
|
5
|
+
|
|
6
|
+
class Error < StandardError
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
class Config
|
|
10
|
+
attr_accessor namespace: String
|
|
11
|
+
attr_accessor redis: untyped
|
|
12
|
+
|
|
13
|
+
def initialize: () -> void
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.configure: () { (Config) -> void } -> void
|
|
17
|
+
def self.config: () -> Config
|
|
4
18
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: weathercock
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Takafumi ONAKA
|
|
@@ -49,11 +49,17 @@ files:
|
|
|
49
49
|
- LICENSE.txt
|
|
50
50
|
- README.md
|
|
51
51
|
- Rakefile
|
|
52
|
+
- Steepfile
|
|
52
53
|
- compose.yaml
|
|
53
54
|
- lib/weathercock.rb
|
|
55
|
+
- lib/weathercock/key_builder.rb
|
|
54
56
|
- lib/weathercock/scorable.rb
|
|
57
|
+
- lib/weathercock/scorer.rb
|
|
55
58
|
- lib/weathercock/version.rb
|
|
56
59
|
- sig/weathercock.rbs
|
|
60
|
+
- sig/weathercock/key_builder.rbs
|
|
61
|
+
- sig/weathercock/scorable.rbs
|
|
62
|
+
- sig/weathercock/scorer.rbs
|
|
57
63
|
homepage: https://github.com/onk/weathercock
|
|
58
64
|
licenses:
|
|
59
65
|
- MIT
|
|
@@ -75,7 +81,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
75
81
|
- !ruby/object:Gem::Version
|
|
76
82
|
version: '0'
|
|
77
83
|
requirements: []
|
|
78
|
-
rubygems_version: 4.0.
|
|
84
|
+
rubygems_version: 4.0.16
|
|
79
85
|
specification_version: 4
|
|
80
86
|
summary: Hit counter and popularity tracking using Valkey/Redis Sorted Sets
|
|
81
87
|
test_files: []
|