whoisonline 0.1.1 → 0.1.4

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: 4cfe52ed55db885ffcecbedd560e7bedcce71c1e4e89f88b46c738347a792cbd
4
- data.tar.gz: fda9b8968db797d1a4abee3d6c2024cec383497f709c880a421ce191125f7b69
3
+ metadata.gz: dcd954e1560a78b4047064f6b1359d22f159b08fd86f3c8cea97283f8a7f2210
4
+ data.tar.gz: '06558961aad1873ceb47cf7799049603a9994376a9a2a9ed0b54eeb43b688207'
5
5
  SHA512:
6
- metadata.gz: 0cec3f4acbff9888c28029323d31cb20e12de31b04164871c553be2bcb2d78b40bda8cf0159cec732a982552c104436b79dac6c3732189464ceb19b98130ce5f
7
- data.tar.gz: 3dff65d6deebd7dc2cd96b5d1bea5f050be32cc662817625b1bb4ecf7c6bc686388aa97ad5bfa1f8cffe3c4f1293de21b42c47a65194f882fa3bea6f311bd18f
6
+ metadata.gz: 139becc8c6d5cdd18578023ba4a9c751a6f9803ec2396d73a229e6421d4b21543d3de4327b37513cc9075c3c5f9deb21d1f4e0900a03e84ac24db374d219af71
7
+ data.tar.gz: a2bd79ab7b77f9524865030f2af74cc011187191d1403968e094a26ed663866fa83d6e2a9a1a41512e3bfb805e2c7f61afa43a46e76dc98e2b7a6aede6b8096c
data/README.md CHANGED
@@ -1,91 +1,356 @@
1
- # WhoIsOnline
1
+ # WhoIsOnline 💚
2
2
 
3
- Track “who is online right now?” in Rails 7/8 using Redis TTL. No database writes, production-safe, and auto-hooks into controllers via a Rails Engine.
3
+ <div align="center">
4
4
 
5
- ## Features
6
- - Rails Engine auto-includes a controller concern to mark users online.
7
- - Works with `current_user` from any auth system (Devise, custom, etc.).
8
- - TTL-based presence in Redis, no tables required.
9
- - Throttled Redis writes to reduce load (configurable).
10
- - Safe SCAN-based counting; no `KEYS`.
11
- - Configurable Redis client, TTL, throttle duration, user id method, controller accessor, and namespace.
5
+ ![WhoIsOnline](https://img.shields.io/badge/WhoIsOnline-v0.1.3-brightgreen)
6
+ ![Rails](https://img.shields.io/badge/Rails-7%2B-red)
7
+ ![Ruby](https://img.shields.io/badge/Ruby-3.1%2B-red)
8
+ ![License](https://img.shields.io/badge/License-MIT-blue)
9
+ ![Gem](https://img.shields.io/gem/v/whoisonline?color=blue)
12
10
 
13
- ## Installation
14
- Add to your Gemfile:
11
+ **Track "who is online right now?" in Rails using Redis TTL. Zero database writes, production-ready, and auto-hooks into controllers.**
12
+
13
+ [Installation](#-installation) • [Quick Start](#-quick-start) • [Configuration](#-configuration) • [API](#-public-api) • [Examples](#-examples)
14
+
15
+ </div>
16
+
17
+ ---
18
+
19
+ ## ✨ Features
20
+
21
+ - 🚀 **Zero Setup** - Rails Engine auto-includes controller concern
22
+ - 🔌 **Works with Any Auth** - Devise, custom, or any `current_user` method
23
+ - ⚡ **Redis TTL-Based** - No database tables required
24
+ - 👁️ **Smart Tracking** - Only tracks when tab is visible/active
25
+ - 🔄 **Automatic Offline Detection** - Marks users offline when browser closes
26
+ - ⏱️ **Throttled Writes** - Configurable to reduce Redis load
27
+ - 🔍 **SCAN-Based** - Safe counting without blocking Redis (`KEYS` not used)
28
+ - 🎛️ **Highly Configurable** - Redis client, TTL, throttle, heartbeat interval, and more
29
+
30
+ ---
31
+
32
+ ## 📦 Installation
33
+
34
+ ### From RubyGems (Recommended)
35
+
36
+ Add to your `Gemfile`:
15
37
 
16
38
  ```ruby
17
- gem "whoisonline", github: "rails-to-rescue/whoisonline"
39
+ gem "whoisonline"
18
40
  ```
19
41
 
20
- Or install directly:
42
+ Then run:
21
43
 
22
44
  ```bash
23
- bundle add whoisonline
45
+ bundle install
46
+ ```
47
+
48
+ ### From GitHub (Development)
49
+
50
+ ```ruby
51
+ gem "whoisonline", github: "KapilDevPal/WhoIsOnline"
24
52
  ```
25
53
 
26
- ## Quick Start
27
- Create an initializer `config/initializers/whoisonline.rb`:
54
+ ---
55
+
56
+ ## 🚀 Quick Start
57
+
58
+ ### 1. Create Initializer
59
+
60
+ Create `config/initializers/whoisonline.rb`:
28
61
 
29
62
  ```ruby
30
63
  WhoIsOnline.configure do |config|
31
64
  config.redis = -> { Redis.new(url: ENV.fetch("REDIS_URL")) }
32
65
  config.ttl = 5.minutes
33
66
  config.throttle = 60.seconds
34
- config.user_id_method = :id
67
+ config.heartbeat_interval = 60.seconds # Heartbeat when tab is visible
35
68
  end
36
69
  ```
37
70
 
38
- The engine auto-adds a concern that runs after each controller action to mark the `current_user` as online. Nothing else is required.
71
+ ### 2. Add to Layout (Optional but Recommended)
72
+
73
+ Add to your main layout (`app/views/layouts/application.html.erb`):
74
+
75
+ ```erb
76
+ <!DOCTYPE html>
77
+ <html>
78
+ <head>
79
+ <title>My App</title>
80
+ <%= csrf_meta_tags %>
81
+ </head>
82
+ <body>
83
+ <%= yield %>
84
+ <%= whoisonline_offline_script %>
85
+ </body>
86
+ </html>
87
+ ```
88
+
89
+ **That's it!** The engine automatically tracks users online after each controller action.
90
+
91
+ ---
92
+
93
+ ## 📖 Public API
94
+
95
+ ```ruby
96
+ # Mark user online (auto-called by controller concern)
97
+ WhoIsOnline.track(user)
98
+
99
+ # Mark user offline immediately
100
+ WhoIsOnline.offline(user)
101
+
102
+ # Check if user is online
103
+ WhoIsOnline.online?(user) # => true/false
104
+
105
+ # Get count of online users
106
+ WhoIsOnline.count # => 42
107
+
108
+ # Get array of online user IDs
109
+ WhoIsOnline.user_ids # => ["1", "2", "3"]
110
+
111
+ # Get ActiveRecord relation of online users
112
+ WhoIsOnline.users(User) # => User.where(id: [...])
113
+ ```
114
+
115
+ ---
39
116
 
40
- ## Public API
41
- - `WhoIsOnline.track(user)` – mark a user online (auto-called by the controller concern).
42
- - `WhoIsOnline.online?(user)` – boolean.
43
- - `WhoIsOnline.count` – number of online users (via SCAN).
44
- - `WhoIsOnline.user_ids` – array of ids (strings by default).
45
- - `WhoIsOnline.users(User)` – ActiveRecord relation for convenience.
117
+ ## ⚙️ Configuration
46
118
 
47
- ## Configuration
48
119
  ```ruby
49
120
  WhoIsOnline.configure do |config|
121
+ # Redis connection (required)
50
122
  config.redis = -> { Redis.new(url: ENV.fetch("REDIS_URL", "redis://127.0.0.1:6379/0")) }
51
- config.ttl = 5.minutes # how long a user stays online without activity
52
- config.throttle = 60.seconds # minimum time between Redis writes per user
53
- config.user_id_method = :id # how to pull an ID from the user object
54
- config.current_user_method = :current_user # method on controllers
55
- config.namespace = "whoisonline:user"
56
- config.auto_hook = true # disable if you prefer manual tracking
57
- config.logger = Rails.logger if defined?(Rails)
123
+
124
+ # TTL settings
125
+ config.ttl = 5.minutes # How long user stays online without activity
126
+ config.throttle = 60.seconds # Minimum time between Redis writes per user
127
+
128
+ # User identification
129
+ config.user_id_method = :id # Method to get user ID
130
+ config.current_user_method = :current_user # Method on controllers to get user
131
+
132
+ # Heartbeat (for visible tab tracking)
133
+ config.heartbeat_interval = 60.seconds # Heartbeat frequency when tab is visible
134
+
135
+ # Advanced
136
+ config.namespace = "whoisonline:user" # Redis key namespace
137
+ config.auto_hook = true # Auto-include controller concern
138
+ config.logger = Rails.logger # Logger instance
58
139
  end
59
140
  ```
60
141
 
61
- ## Performance Notes
62
- - Uses `SET key value EX ttl` for O(1) writes.
63
- - Throttling prevents hot users from spamming Redis.
64
- - Counting and user listing use `SCAN` to avoid blocking Redis (`KEYS` is not used).
65
- - Namespace keeps presence keys isolated; use a dedicated Redis db/cluster for large scale.
142
+ ### Configuration Options
143
+
144
+ | Option | Default | Description |
145
+ |--------|---------|-------------|
146
+ | `ttl` | `5.minutes` | How long a user stays online without activity |
147
+ | `throttle` | `60.seconds` | Minimum time between Redis writes per user |
148
+ | `heartbeat_interval` | `60.seconds` | Heartbeat frequency when tab is visible |
149
+ | `user_id_method` | `:id` | Method to extract ID from user object |
150
+ | `current_user_method` | `:current_user` | Method name on controllers |
151
+ | `namespace` | `"whoisonline:user"` | Redis key namespace prefix |
152
+ | `auto_hook` | `true` | Auto-include controller concern |
153
+ | `logger` | `Rails.logger` | Logger for errors/warnings |
154
+
155
+ ---
156
+
157
+ ## 💡 Examples
158
+
159
+ ### Basic Usage
66
160
 
67
- ## Example Usage in Rails
68
161
  ```ruby
69
- # Somewhere in your controller you can also call manually:
70
- WhoIsOnline.track(current_user)
162
+ # In a controller (automatic via engine)
163
+ # Users are tracked after each action
71
164
 
72
- # In a background job
73
- if WhoIsOnline.online?(user)
74
- # notify
165
+ # Check if user is online
166
+ if WhoIsOnline.online?(current_user)
167
+ # User is currently online
75
168
  end
76
169
 
77
- # In a dashboard
78
- @online_users = WhoIsOnline.users(User).order(last_sign_in_at: :desc)
170
+ # Get online users count
79
171
  @online_count = WhoIsOnline.count
172
+
173
+ # Get online users
174
+ @online_users = WhoIsOnline.users(User)
175
+ ```
176
+
177
+ ### Manual Tracking
178
+
179
+ ```ruby
180
+ # Manually mark user online
181
+ WhoIsOnline.track(current_user)
182
+
183
+ # Manually mark user offline (e.g., on logout)
184
+ WhoIsOnline.offline(current_user)
80
185
  ```
81
186
 
82
- ## Extensibility
83
- - Engine-based hook is easy to extend (e.g., add ActionCable broadcast).
84
- - Tracker service is isolated and unit-testable.
85
- - Configuration is thread-safe and lazy-instantiated.
187
+ ### In Background Jobs
188
+
189
+ ```ruby
190
+ class NotificationJob < ApplicationJob
191
+ def perform(user_id)
192
+ user = User.find(user_id)
193
+
194
+ if WhoIsOnline.online?(user)
195
+ # Only notify if user is online
196
+ NotificationService.deliver(user)
197
+ end
198
+ end
199
+ end
200
+ ```
201
+
202
+ ### Dashboard View
203
+
204
+ ```ruby
205
+ # In your controller
206
+ class DashboardController < ApplicationController
207
+ def index
208
+ @online_count = WhoIsOnline.count
209
+ @online_users = WhoIsOnline.users(User)
210
+ .order(last_sign_in_at: :desc)
211
+ .limit(50)
212
+ end
213
+ end
214
+ ```
215
+
216
+ ```erb
217
+ <!-- In your view -->
218
+ <div class="online-users">
219
+ <h2>Online Now (<%= @online_count %>)</h2>
220
+ <ul>
221
+ <% @online_users.each do |user| %>
222
+ <li><%= user.name %> <span class="badge">●</span></li>
223
+ <% end %>
224
+ </ul>
225
+ </div>
226
+ ```
227
+
228
+ ---
229
+
230
+ ## 🎯 How It Works
231
+
232
+ 1. **Controller Action** - After each action, the engine automatically calls `WhoIsOnline.track(current_user)`
233
+ 2. **Redis Write** - User presence is stored in Redis with TTL: `SET whoisonline:user:123 <timestamp> EX 300`
234
+ 3. **Throttling** - Writes are throttled per user to prevent Redis spam
235
+ 4. **Heartbeat** - When tab is visible, JavaScript sends periodic heartbeats to keep user online
236
+ 5. **Offline Detection** - When browser closes, JavaScript sends offline request
237
+ 6. **TTL Expiry** - If no activity, Redis key expires automatically
238
+
239
+ ---
240
+
241
+ ## ⚡ Performance
242
+
243
+ - **O(1) Writes** - Uses `SET key value EX ttl` for constant-time operations
244
+ - **Throttled** - Prevents hot users from spamming Redis
245
+ - **SCAN-Based** - Counting uses `SCAN` instead of `KEYS` (non-blocking)
246
+ - **Namespace Isolation** - Keys are namespaced for easy management
247
+ - **No Database** - Zero database writes, all in Redis
248
+
249
+ ### Recommended Settings
250
+
251
+ For **high-traffic** applications:
252
+
253
+ ```ruby
254
+ config.ttl = 2.minutes # Shorter TTL = more accurate
255
+ config.throttle = 30.seconds # More frequent updates
256
+ config.heartbeat_interval = 30.seconds # More frequent heartbeats
257
+ ```
258
+
259
+ For **low-traffic** applications:
260
+
261
+ ```ruby
262
+ config.ttl = 10.minutes # Longer TTL = less Redis activity
263
+ config.throttle = 120.seconds # Less frequent updates
264
+ config.heartbeat_interval = 120.seconds # Less frequent heartbeats
265
+ ```
266
+
267
+ ---
268
+
269
+ ## 🔧 Troubleshooting
270
+
271
+ ### Users Not Showing as Online
272
+
273
+ 1. Check Redis connection: `redis-cli ping`
274
+ 2. Verify `current_user` method exists in your controllers
275
+ 3. Check logs for errors: `tail -f log/development.log`
276
+ 4. Verify TTL hasn't expired: `redis-cli TTL whoisonline:user:1`
277
+
278
+ ### Helper Not Available
279
+
280
+ If `whoisonline_offline_script` is not available:
281
+
282
+ 1. Restart Rails server
283
+ 2. Check that gem is loaded: `bundle list | grep whoisonline`
284
+ 3. Verify engine is loaded: Check `config/application.rb` for engine loading
285
+
286
+ ### Heartbeat Not Working
287
+
288
+ 1. Check browser console for JavaScript errors
289
+ 2. Verify CSRF token is present: `<%= csrf_meta_tags %>`
290
+ 3. Check network tab for heartbeat requests
291
+ 4. Verify `heartbeat_interval` is configured
292
+
293
+ ---
294
+
295
+ ## 🛠️ Extensibility
296
+
297
+ The gem is designed to be extensible:
298
+
299
+ - **ActionCable Integration** - Broadcast presence changes
300
+ - **Custom Events** - Hook into track/offline events
301
+ - **Multiple Redis Instances** - Use different Redis for presence
302
+ - **Custom User Models** - Works with any user model structure
303
+
304
+ Example: ActionCable Integration
305
+
306
+ ```ruby
307
+ # In an initializer
308
+ WhoIsOnline.tracker.define_singleton_method(:track) do |user|
309
+ super(user)
310
+ ActionCable.server.broadcast("presence", { user_id: user.id, status: "online" })
311
+ end
312
+ ```
313
+
314
+ ---
315
+
316
+ ## 📝 License
317
+
318
+ MIT License - see [LICENSE](LICENSE) file for details.
319
+
320
+ ---
321
+
322
+ ## 👤 Author
323
+
324
+ **Kapil Dev Pal**
325
+
326
+ - 📧 Email: dev.kapildevpal@gmail.com
327
+ - 🐦 Twitter: [@rails_to_rescue](https://twitter.com/rails_to_rescue)
328
+ - 🚀 Project: [rails_to_rescue](https://github.com/rails-to-rescue)
329
+
330
+ ---
331
+
332
+ ## 🤝 Contributing
333
+
334
+ Contributions are welcome! Please feel free to submit a Pull Request.
335
+
336
+ 1. Fork the repository
337
+ 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
338
+ 3. Commit your changes (`git commit -m 'Add some amazing feature'`)
339
+ 4. Push to the branch (`git push origin feature/amazing-feature`)
340
+ 5. Open a Pull Request
341
+
342
+ ---
343
+
344
+ ## ⭐ Show Your Support
345
+
346
+ If you find this gem useful, please give it a ⭐ on GitHub!
347
+
348
+ ---
349
+
350
+ <div align="center">
86
351
 
87
- ## Author
88
- - Kapil Dev Pal – dev.kapildevpal@gmail.com / @rails_to_rescue
89
- - Project: rails_to_rescue
352
+ **Made with ❤️ for the Rails community**
90
353
 
354
+ [Report Bug](https://github.com/KapilDevPal/WhoIsOnline/issues) • [Request Feature](https://github.com/KapilDevPal/WhoIsOnline/issues) • [View on GitHub](https://github.com/KapilDevPal/WhoIsOnline)
91
355
 
356
+ </div>
@@ -5,7 +5,7 @@ module WhoIsOnline
5
5
  DEFAULT_NAMESPACE = "whoisonline:user".freeze
6
6
 
7
7
  attr_accessor :ttl, :throttle, :user_id_method, :namespace, :auto_hook,
8
- :logger, :current_user_method
8
+ :logger, :current_user_method, :heartbeat_interval
9
9
  attr_writer :redis
10
10
 
11
11
  def initialize
@@ -16,6 +16,7 @@ module WhoIsOnline
16
16
  @namespace = DEFAULT_NAMESPACE
17
17
  @auto_hook = true
18
18
  @logger = default_logger
19
+ @heartbeat_interval = 60.seconds
19
20
  end
20
21
 
21
22
  def redis
@@ -1,5 +1,6 @@
1
1
  require "rails/engine"
2
2
  require_relative "controller"
3
+ require_relative "presence_controller"
3
4
 
4
5
  module WhoIsOnline
5
6
  class Engine < ::Rails::Engine
@@ -10,6 +11,22 @@ module WhoIsOnline
10
11
  include WhoIsOnline::Controller
11
12
  end
12
13
  end
14
+
15
+ initializer "whoisonline.routes" do |app|
16
+ app.routes.append do
17
+ post "/whoisonline/offline", to: "whoisonline/presence#offline", as: :whoisonline_offline
18
+ post "/whoisonline/heartbeat", to: "whoisonline/presence#heartbeat", as: :whoisonline_heartbeat
19
+ end
20
+ end
21
+
22
+ initializer "whoisonline.helpers" do
23
+ ActiveSupport.on_load(:action_view) do
24
+ # Explicitly require the helper module from app directory
25
+ helper_path = File.join(Engine.root, "app", "helpers", "whoisonline", "application_helper.rb")
26
+ require helper_path if File.exist?(helper_path)
27
+ include WhoIsOnline::ApplicationHelper
28
+ end
29
+ end
13
30
  end
14
31
  end
15
32
 
@@ -0,0 +1,37 @@
1
+ require "action_controller"
2
+
3
+ module WhoIsOnline
4
+ class PresenceController < ActionController::Base
5
+ # Skip CSRF for sendBeacon requests (they may not include CSRF token reliably)
6
+ skip_before_action :verify_authenticity_token, raise: false
7
+ protect_from_forgery with: :null_session, only: [:offline]
8
+
9
+ def offline
10
+ user = resolve_whoisonline_user
11
+ WhoIsOnline.offline(user) if user
12
+ head :ok
13
+ rescue StandardError => e
14
+ WhoIsOnline.configuration.logger&.warn("whoisonline offline failed: #{e.class} #{e.message}")
15
+ head :ok
16
+ end
17
+
18
+ def heartbeat
19
+ user = resolve_whoisonline_user
20
+ WhoIsOnline.track(user) if user
21
+ head :ok
22
+ rescue StandardError => e
23
+ WhoIsOnline.configuration.logger&.warn("whoisonline heartbeat failed: #{e.class} #{e.message}")
24
+ head :ok
25
+ end
26
+
27
+ private
28
+
29
+ def resolve_whoisonline_user
30
+ method = WhoIsOnline.configuration.current_user_method
31
+ return public_send(method) if respond_to?(method, true)
32
+
33
+ nil
34
+ end
35
+ end
36
+ end
37
+
@@ -33,6 +33,13 @@ module WhoIsOnline
33
33
  []
34
34
  end
35
35
 
36
+ def delete_presence(key)
37
+ connection.del(key)
38
+ rescue StandardError => e
39
+ log(:warn, "whoisonline delete failed: #{e.class} #{e.message}")
40
+ nil
41
+ end
42
+
36
43
  private
37
44
 
38
45
  def log(level, message)
@@ -19,6 +19,15 @@ module WhoIsOnline
19
19
  @last_write_by_user[uid] = Time.now if result
20
20
  end
21
21
 
22
+ def offline(user)
23
+ uid = extract_id(user)
24
+ return unless uid
25
+
26
+ key = presence_key(uid)
27
+ @redis_store.delete_presence(key)
28
+ @last_write_by_user.delete(uid)
29
+ end
30
+
22
31
  def online?(user)
23
32
  uid = extract_id(user)
24
33
  return false unless uid
@@ -1,5 +1,5 @@
1
1
  module WhoIsOnline
2
- VERSION = "0.1.1"
2
+ VERSION = "0.1.4"
3
3
  end
4
4
 
5
5
 
data/lib/whoisonline.rb CHANGED
@@ -10,7 +10,7 @@ require_relative "whoisonline/engine"
10
10
 
11
11
  module WhoIsOnline
12
12
  class << self
13
- delegate :track, :online?, :count, :user_ids, :users, to: :tracker
13
+ delegate :track, :offline, :online?, :count, :user_ids, :users, to: :tracker
14
14
 
15
15
  def tracker
16
16
  @_tracker ||= Tracker.new(configuration, redis_store)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: whoisonline
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kapil Dev Pal
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-12-27 00:00:00.000000000 Z
11
+ date: 2025-12-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -79,6 +79,7 @@ files:
79
79
  - lib/whoisonline/configuration.rb
80
80
  - lib/whoisonline/controller.rb
81
81
  - lib/whoisonline/engine.rb
82
+ - lib/whoisonline/presence_controller.rb
82
83
  - lib/whoisonline/redis_store.rb
83
84
  - lib/whoisonline/tracker.rb
84
85
  - lib/whoisonline/version.rb