philiprehberger-rate_limiter 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6a165ad8eaaff27921b80e53c761406c60f83f28ab2d985e720f284660eb9da7
4
+ data.tar.gz: 80ff04b248a0913de8d288c0426e3f37f8cf98442e3d4cbc2012e5b313838c53
5
+ SHA512:
6
+ metadata.gz: d59438c4d038a1a3ceec480e1d19596014ef291971531df1dbf97b2335f285ba95c0a959cc047aa3f4ff6c5a45f38ad5648d0813367f1bc7b6f5aa1a283b30dc
7
+ data.tar.gz: 9728caa5a94a01a6df61c2ed0fffe37a53010d51481e281b592b8d06dbf73f267deae0bc69001643126effad1163cf2669a5da0223289c8033f012143d556439
data/CHANGELOG.md ADDED
@@ -0,0 +1,21 @@
1
+ # Changelog
2
+
3
+ All notable changes to this gem will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] - 2026-03-10
11
+
12
+ ### Added
13
+ - Initial release
14
+ - Sliding window rate limiting with per-key tracking
15
+ - Token bucket rate limiting with automatic refill
16
+ - Thread-safe operations with Mutex
17
+ - `allow?` to check and consume in one call
18
+ - `peek` to check without consuming
19
+ - `remaining` to query available quota
20
+ - `reset` to clear state for a key
21
+ - Convenience factory methods on the module
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 philiprehberger
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,100 @@
1
+ # philiprehberger-rate_limiter
2
+
3
+ [![Tests](https://github.com/philiprehberger/rb-rate-limiter/actions/workflows/ci.yml/badge.svg)](https://github.com/philiprehberger/rb-rate-limiter/actions/workflows/ci.yml)
4
+ [![Gem Version](https://badge.fury.io/rb/philiprehberger-rate_limiter.svg)](https://rubygems.org/gems/philiprehberger-rate_limiter)
5
+
6
+ In-memory rate limiter with sliding window and token bucket algorithms, per-key tracking, and thread safety.
7
+
8
+ ## Requirements
9
+
10
+ - Ruby >= 3.1
11
+
12
+ ## Installation
13
+
14
+ Add to your Gemfile:
15
+
16
+ ```ruby
17
+ gem "philiprehberger-rate_limiter"
18
+ ```
19
+
20
+ Then run:
21
+
22
+ ```bash
23
+ bundle install
24
+ ```
25
+
26
+ Or install directly:
27
+
28
+ ```bash
29
+ gem install philiprehberger-rate_limiter
30
+ ```
31
+
32
+ ## Usage
33
+
34
+ ```ruby
35
+ require "philiprehberger/rate_limiter"
36
+ ```
37
+
38
+ ### Sliding Window
39
+
40
+ Limits the number of requests within a rolling time window.
41
+
42
+ ```ruby
43
+ limiter = Philiprehberger::RateLimiter.sliding_window(limit: 100, window: 60)
44
+
45
+ if limiter.allow?("user:123")
46
+ # Request is allowed
47
+ else
48
+ # Rate limit exceeded
49
+ end
50
+ ```
51
+
52
+ ### Token Bucket
53
+
54
+ Allows bursts up to a capacity, refilling at a steady rate.
55
+
56
+ ```ruby
57
+ limiter = Philiprehberger::RateLimiter.token_bucket(rate: 10, capacity: 50)
58
+
59
+ if limiter.allow?("api:key")
60
+ # Request is allowed
61
+ else
62
+ # Rate limit exceeded
63
+ end
64
+ ```
65
+
66
+ ### Peeking Without Consuming
67
+
68
+ ```ruby
69
+ limiter.peek("user:123") # => true/false (does not consume)
70
+ limiter.remaining("user:123") # => number of remaining requests/tokens
71
+ ```
72
+
73
+ ### Resetting a Key
74
+
75
+ ```ruby
76
+ limiter.reset("user:123")
77
+ ```
78
+
79
+ ## API
80
+
81
+ | Method | Description |
82
+ |--------|-------------|
83
+ | `RateLimiter.sliding_window(limit:, window:)` | Create a sliding window limiter |
84
+ | `RateLimiter.token_bucket(rate:, capacity:)` | Create a token bucket limiter |
85
+ | `#allow?(key)` | Check and consume one request/token; returns `true`/`false` |
86
+ | `#peek(key)` | Check availability without consuming |
87
+ | `#remaining(key)` | Return remaining request/token count |
88
+ | `#reset(key)` | Clear all state for a key |
89
+
90
+ ## Development
91
+
92
+ ```bash
93
+ bundle install
94
+ bundle exec rspec # Run tests
95
+ bundle exec rubocop # Check code style
96
+ ```
97
+
98
+ ## License
99
+
100
+ MIT
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Philiprehberger
4
+ module RateLimiter
5
+ class SlidingWindow
6
+ def initialize(limit:, window:)
7
+ @limit = limit
8
+ @window = window
9
+ @store = {}
10
+ @mutex = Mutex.new
11
+ end
12
+
13
+ def allow?(key)
14
+ @mutex.synchronize { try_acquire(key) }
15
+ end
16
+
17
+ def peek(key)
18
+ @mutex.synchronize { count_remaining(key).positive? }
19
+ end
20
+
21
+ def remaining(key)
22
+ @mutex.synchronize { count_remaining(key) }
23
+ end
24
+
25
+ def reset(key)
26
+ @mutex.synchronize { @store.delete(key.to_s) }
27
+ end
28
+
29
+ private
30
+
31
+ def try_acquire(key)
32
+ cleanup(key)
33
+ entries = fetch_entries(key)
34
+ return false if entries.length >= @limit
35
+
36
+ entries << now
37
+ true
38
+ end
39
+
40
+ def count_remaining(key)
41
+ cleanup(key)
42
+ entries = fetch_entries(key)
43
+ [@limit - entries.length, 0].max
44
+ end
45
+
46
+ def cleanup(key)
47
+ entries = fetch_entries(key)
48
+ cutoff = now - @window
49
+ entries.reject! { |ts| ts <= cutoff }
50
+ end
51
+
52
+ def fetch_entries(key)
53
+ @store[key.to_s] ||= []
54
+ end
55
+
56
+ def now
57
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Philiprehberger
4
+ module RateLimiter
5
+ class TokenBucket
6
+ def initialize(rate:, capacity:)
7
+ @rate = rate.to_f
8
+ @capacity = capacity.to_f
9
+ @store = {}
10
+ @mutex = Mutex.new
11
+ end
12
+
13
+ def allow?(key)
14
+ @mutex.synchronize { try_acquire(key) }
15
+ end
16
+
17
+ def peek(key)
18
+ @mutex.synchronize { token_count(key) >= 1.0 }
19
+ end
20
+
21
+ def remaining(key)
22
+ @mutex.synchronize { token_count(key).to_i }
23
+ end
24
+
25
+ def reset(key)
26
+ @mutex.synchronize { @store.delete(key.to_s) }
27
+ end
28
+
29
+ private
30
+
31
+ def try_acquire(key)
32
+ refill(key)
33
+ bucket = fetch_bucket(key)
34
+ return false if bucket[:tokens] < 1.0
35
+
36
+ bucket[:tokens] -= 1.0
37
+ true
38
+ end
39
+
40
+ def token_count(key)
41
+ refill(key)
42
+ bucket = fetch_bucket(key)
43
+ [bucket[:tokens], @capacity].min
44
+ end
45
+
46
+ def refill(key)
47
+ bucket = fetch_bucket(key)
48
+ elapsed = now - bucket[:last_refill]
49
+ bucket[:tokens] = [bucket[:tokens] + (elapsed * @rate), @capacity].min
50
+ bucket[:last_refill] = now
51
+ end
52
+
53
+ def fetch_bucket(key)
54
+ @store[key.to_s] ||= { tokens: @capacity, last_refill: now }
55
+ end
56
+
57
+ def now
58
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Philiprehberger
4
+ module RateLimiter
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "rate_limiter/version"
4
+ require_relative "rate_limiter/sliding_window"
5
+ require_relative "rate_limiter/token_bucket"
6
+
7
+ module Philiprehberger
8
+ module RateLimiter
9
+ class Error < StandardError; end
10
+
11
+ def self.sliding_window(limit:, window:)
12
+ SlidingWindow.new(limit: limit, window: window)
13
+ end
14
+
15
+ def self.token_bucket(rate:, capacity:)
16
+ TokenBucket.new(rate: rate, capacity: capacity)
17
+ end
18
+ end
19
+ end
metadata ADDED
@@ -0,0 +1,55 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: philiprehberger-rate_limiter
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Philip Rehberger
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-03-11 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: A zero-dependency Ruby gem for rate limiting with sliding window and
14
+ token bucket algorithms, per-key tracking, and thread safety.
15
+ email:
16
+ - me@philiprehberger.com
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - CHANGELOG.md
22
+ - LICENSE
23
+ - README.md
24
+ - lib/philiprehberger/rate_limiter.rb
25
+ - lib/philiprehberger/rate_limiter/sliding_window.rb
26
+ - lib/philiprehberger/rate_limiter/token_bucket.rb
27
+ - lib/philiprehberger/rate_limiter/version.rb
28
+ homepage: https://github.com/philiprehberger/rb-rate-limiter
29
+ licenses:
30
+ - MIT
31
+ metadata:
32
+ homepage_uri: https://github.com/philiprehberger/rb-rate-limiter
33
+ source_code_uri: https://github.com/philiprehberger/rb-rate-limiter
34
+ changelog_uri: https://github.com/philiprehberger/rb-rate-limiter/blob/main/CHANGELOG.md
35
+ rubygems_mfa_required: 'true'
36
+ post_install_message:
37
+ rdoc_options: []
38
+ require_paths:
39
+ - lib
40
+ required_ruby_version: !ruby/object:Gem::Requirement
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ version: '3.1'
45
+ required_rubygems_version: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: '0'
50
+ requirements: []
51
+ rubygems_version: 3.5.22
52
+ signing_key:
53
+ specification_version: 4
54
+ summary: In-memory rate limiter with sliding window and token bucket
55
+ test_files: []