weathercock 1.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cc7554f2d86d4cd3499ea0aa89d77bc327e9667b16257b234f00391d82118f8b
4
- data.tar.gz: 7687c05fe5724b8ec134bfbaf9fc4689b799415f368627a85de790b8ddcd7073
3
+ metadata.gz: e6f2f450b510bee3dbea48bf0b4e7e37ef24040d82141caaabe45d6dce3c3cb6
4
+ data.tar.gz: ab78b22090a84493d97b3f5deca74963f150d3ea21af0b47408babb64fdb7985
5
5
  SHA512:
6
- metadata.gz: 9bb7bf12377be72a41e51a5de234475e2cbab1d3a8333b47865da959053d482b5bcb762d4c13fa00ed142a95bf64c531e2b04a22a608969440a7feb45dd881dc
7
- data.tar.gz: 90e5eafb164fe1fdc2b11167b28a9f9a6fda24dc1be2ed98e987505b05bb4aabe7f8bb7b66b9141e658bdba0e70cc36a808be3a557a42ff19c944ef115d020ff
6
+ metadata.gz: e10e7fee6c2e629c994c097920af3226a3f6fdde65a18aed9a85b5556ae6d0a4b4abf78029b833d50d88581745f2517911177ebaee78bbe7de6aef2c78eacb49
7
+ data.tar.gz: 4a2089ecaceb03a54cb1462504f9b946145b81837db87c4a497650164180c56533246f851ab0c1e1c8f84a337f06faf767c782960e8a9ac6273149f0e69f5766
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
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
+
3
10
  ## v1.1.0 - 2026-04-17
4
11
 
5
12
  ### Added
data/Steepfile ADDED
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ D = Steep::Diagnostic
4
+
5
+ target :lib do
6
+ signature "sig"
7
+
8
+ check "lib"
9
+
10
+ library "date"
11
+
12
+ configure_code_diagnostics(D::Ruby.default)
13
+ end
@@ -38,8 +38,9 @@ module Weathercock
38
38
  end
39
39
  end
40
40
 
41
- def union_dest(base, type, count)
42
- "#{base}:top:#{type}:#{count}"
41
+ def union_dest(base, type, count, decay_factor: nil)
42
+ dest = "#{base}:top:#{type}:#{count}"
43
+ decay_factor ? "#{dest}:decay:#{decay_factor}" : dest
43
44
  end
44
45
  end
45
46
  end
@@ -74,7 +74,9 @@ module Weathercock
74
74
  base = @key_builder.base(event)
75
75
  dest = window.empty? ? @key_builder.total(base) : union(event, window)
76
76
  scores = @redis.call("ZMSCORE", dest, *ids.map(&:to_s))
77
- ids.zip(scores).to_h { |id, score| [id.to_s, (score || "0").to_i] }
77
+ ids.zip(scores).to_h do |id, score|
78
+ [id.to_s, Integer(score || "0")] #: [String, Integer]
79
+ end
78
80
  end
79
81
 
80
82
  private
@@ -82,13 +84,19 @@ module Weathercock
82
84
  def union(event, window, decay_factor: nil)
83
85
  base = @key_builder.base(event)
84
86
  type, count = window.first
85
- keys = @key_builder.window_keys(base, type, count)
86
- dest = @key_builder.union_dest(base, type, count)
87
-
88
- weights = decay_factor ? count.times.map { |i| (decay_factor**i).round(10) } : nil
89
- zunionstore_args = ["ZUNIONSTORE", dest, keys.size, *keys]
90
- zunionstore_args += ["WEIGHTS", *weights] if weights
91
- @redis.call(*zunionstore_args)
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
92
100
  @redis.call("EXPIRE", dest, 900)
93
101
  dest
94
102
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Weathercock
4
- VERSION = "1.1.0"
4
+ VERSION = "1.2.0"
5
5
  end
@@ -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
- # See the writing guide of rbs: https://github.com/ruby/rbs#guides
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.1.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Takafumi ONAKA
@@ -49,6 +49,7 @@ files:
49
49
  - LICENSE.txt
50
50
  - README.md
51
51
  - Rakefile
52
+ - Steepfile
52
53
  - compose.yaml
53
54
  - lib/weathercock.rb
54
55
  - lib/weathercock/key_builder.rb
@@ -56,6 +57,9 @@ files:
56
57
  - lib/weathercock/scorer.rb
57
58
  - lib/weathercock/version.rb
58
59
  - sig/weathercock.rbs
60
+ - sig/weathercock/key_builder.rbs
61
+ - sig/weathercock/scorable.rbs
62
+ - sig/weathercock/scorer.rbs
59
63
  homepage: https://github.com/onk/weathercock
60
64
  licenses:
61
65
  - MIT
@@ -77,7 +81,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
77
81
  - !ruby/object:Gem::Version
78
82
  version: '0'
79
83
  requirements: []
80
- rubygems_version: 4.0.6
84
+ rubygems_version: 4.0.16
81
85
  specification_version: 4
82
86
  summary: Hit counter and popularity tracking using Valkey/Redis Sorted Sets
83
87
  test_files: []