whoisonline 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: 94f2c31318e63780cdee77f18a8aaad0ff39133c1334205a50016713d9aa2de6
4
+ data.tar.gz: b52a4bc959ffdfc54449cbee0cd27c74d2a5930d47487fb4f024d62e2a9c4e85
5
+ SHA512:
6
+ metadata.gz: 1fb7d27b02f1c7d897b412387cc8fd3236f3ad2851f0e3d164b521b54f8f51357ee0fde5cac5e0a87ace9a12776fab2c20d35ce904fc6924dc5bf1195e534444
7
+ data.tar.gz: 38d09284b895047ca3107c22bfc3c7407781a2f6fe8bfd30ed48eb5e56ae0c8ba2fc9b18cbd09c20acea8ed1ab41029be8280eaa458dfa320a748771188df407
data/README.md ADDED
@@ -0,0 +1,91 @@
1
+ # WhoIsOnline
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.
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.
12
+
13
+ ## Installation
14
+ Add to your Gemfile:
15
+
16
+ ```ruby
17
+ gem "whoisonline", github: "rails-to-rescue/whoisonline"
18
+ ```
19
+
20
+ Or install directly:
21
+
22
+ ```bash
23
+ bundle add whoisonline
24
+ ```
25
+
26
+ ## Quick Start
27
+ Create an initializer `config/initializers/whoisonline.rb`:
28
+
29
+ ```ruby
30
+ WhoIsOnline.configure do |config|
31
+ config.redis = -> { Redis.new(url: ENV.fetch("REDIS_URL")) }
32
+ config.ttl = 5.minutes
33
+ config.throttle = 60.seconds
34
+ config.user_id_method = :id
35
+ end
36
+ ```
37
+
38
+ The engine auto-adds a concern that runs after each controller action to mark the `current_user` as online. Nothing else is required.
39
+
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.
46
+
47
+ ## Configuration
48
+ ```ruby
49
+ WhoIsOnline.configure do |config|
50
+ 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)
58
+ end
59
+ ```
60
+
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.
66
+
67
+ ## Example Usage in Rails
68
+ ```ruby
69
+ # Somewhere in your controller you can also call manually:
70
+ WhoIsOnline.track(current_user)
71
+
72
+ # In a background job
73
+ if WhoIsOnline.online?(user)
74
+ # notify
75
+ end
76
+
77
+ # In a dashboard
78
+ @online_users = WhoIsOnline.users(User).order(last_sign_in_at: :desc)
79
+ @online_count = WhoIsOnline.count
80
+ ```
81
+
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.
86
+
87
+ ## Author
88
+ - Kapil Dev Pal – dev.kapildevpal@gmail.com / @rails_to_rescue
89
+ - Project: rails_to_rescue
90
+
91
+
@@ -0,0 +1,44 @@
1
+ require "logger"
2
+
3
+ module WhoIsOnline
4
+ class Configuration
5
+ DEFAULT_NAMESPACE = "whoisonline:user".freeze
6
+
7
+ attr_accessor :ttl, :throttle, :user_id_method, :namespace, :auto_hook,
8
+ :logger, :current_user_method
9
+ attr_writer :redis
10
+
11
+ def initialize
12
+ @ttl = 5.minutes
13
+ @throttle = 60.seconds
14
+ @user_id_method = :id
15
+ @current_user_method = :current_user
16
+ @namespace = DEFAULT_NAMESPACE
17
+ @auto_hook = true
18
+ @logger = default_logger
19
+ end
20
+
21
+ def redis
22
+ @redis ||= lambda do
23
+ Redis.new(url: ENV.fetch("REDIS_URL", "redis://127.0.0.1:6379/0"))
24
+ end
25
+ end
26
+
27
+ def redis_connection
28
+ client = redis.respond_to?(:call) ? redis.call : redis
29
+ raise ArgumentError, "config.redis must return a Redis-compatible client" unless client.respond_to?(:set)
30
+
31
+ client
32
+ end
33
+
34
+ private
35
+
36
+ def default_logger
37
+ return Rails.logger if defined?(Rails)
38
+
39
+ Logger.new($stdout, progname: "WhoIsOnline")
40
+ end
41
+ end
42
+ end
43
+
44
+
@@ -0,0 +1,32 @@
1
+ require "active_support/concern"
2
+
3
+ module WhoIsOnline
4
+ module Controller
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ after_action :who_is_online_track_user
9
+ end
10
+
11
+ private
12
+
13
+ def who_is_online_track_user
14
+ return unless WhoIsOnline.configuration.auto_hook
15
+
16
+ user = resolve_whoisonline_user
17
+ return unless user
18
+
19
+ WhoIsOnline.track(user)
20
+ rescue StandardError => e
21
+ WhoIsOnline.configuration.logger&.warn("whoisonline track failed: #{e.class} #{e.message}")
22
+ true
23
+ end
24
+
25
+ def resolve_whoisonline_user
26
+ method = WhoIsOnline.configuration.current_user_method
27
+ return public_send(method) if respond_to?(method, true)
28
+
29
+ nil
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,16 @@
1
+ require "rails/engine"
2
+ require_relative "controller"
3
+
4
+ module WhoIsOnline
5
+ class Engine < ::Rails::Engine
6
+ isolate_namespace WhoIsOnline
7
+
8
+ initializer "whoisonline.controller" do
9
+ ActiveSupport.on_load(:action_controller) do
10
+ include WhoIsOnline::Controller
11
+ end
12
+ end
13
+ end
14
+ end
15
+
16
+
@@ -0,0 +1,47 @@
1
+ require "redis"
2
+
3
+ module WhoIsOnline
4
+ class RedisStore
5
+ def initialize(configuration)
6
+ @configuration = configuration
7
+ end
8
+
9
+ def connection
10
+ @connection ||= @configuration.redis_connection
11
+ end
12
+
13
+ def write_presence(key, value, ttl_seconds)
14
+ connection.set(key, value, ex: ttl_seconds)
15
+ rescue StandardError => e
16
+ log(:warn, "whoisonline write failed: #{e.class} #{e.message}")
17
+ nil
18
+ end
19
+
20
+ def exists?(key)
21
+ connection.exists?(key)
22
+ rescue StandardError => e
23
+ log(:warn, "whoisonline exists? failed: #{e.class} #{e.message}")
24
+ false
25
+ end
26
+
27
+ def scan_keys(match:)
28
+ return enum_for(:scan_keys, match: match) unless block_given?
29
+
30
+ connection.scan_each(match: match) { |key| yield key }
31
+ rescue StandardError => e
32
+ log(:warn, "whoisonline scan failed: #{e.class} #{e.message}")
33
+ []
34
+ end
35
+
36
+ private
37
+
38
+ def log(level, message)
39
+ logger = @configuration.logger
40
+ return unless logger
41
+
42
+ logger.public_send(level, message)
43
+ end
44
+ end
45
+ end
46
+
47
+
@@ -0,0 +1,70 @@
1
+ require "concurrent/hash"
2
+
3
+ module WhoIsOnline
4
+ class Tracker
5
+ def initialize(configuration, redis_store)
6
+ @configuration = configuration
7
+ @redis_store = redis_store
8
+ @last_write_by_user = Concurrent::Hash.new
9
+ end
10
+
11
+ def track(user)
12
+ uid = extract_id(user)
13
+ return unless uid
14
+ return if throttled?(uid)
15
+
16
+ key = presence_key(uid)
17
+ now = Time.now.to_i
18
+ result = @redis_store.write_presence(key, now, ttl_seconds)
19
+ @last_write_by_user[uid] = Time.now if result
20
+ end
21
+
22
+ def online?(user)
23
+ uid = extract_id(user)
24
+ return false unless uid
25
+
26
+ @redis_store.exists?(presence_key(uid))
27
+ end
28
+
29
+ def count
30
+ user_ids.size
31
+ end
32
+
33
+ def user_ids
34
+ keys = @redis_store.scan_keys(match: "#{@configuration.namespace}:*")
35
+ keys.lazy.map { |key| key.split(":").last }.to_a
36
+ end
37
+
38
+ def users(model_class)
39
+ model_class.where(id: user_ids)
40
+ end
41
+
42
+ private
43
+
44
+ def extract_id(user)
45
+ return nil unless user
46
+ return user if user.is_a?(String) || user.is_a?(Numeric)
47
+ return user.public_send(@configuration.user_id_method) if user.respond_to?(@configuration.user_id_method)
48
+
49
+ nil
50
+ end
51
+
52
+ def presence_key(uid)
53
+ "#{@configuration.namespace}:#{uid}"
54
+ end
55
+
56
+ def ttl_seconds
57
+ @configuration.ttl.to_i
58
+ end
59
+
60
+ def throttled?(uid)
61
+ return false unless @configuration.throttle
62
+
63
+ last = @last_write_by_user[uid]
64
+ return false unless last
65
+
66
+ (Time.now - last) < @configuration.throttle
67
+ end
68
+ end
69
+ end
70
+
@@ -0,0 +1,5 @@
1
+ module WhoIsOnline
2
+ VERSION = "0.1.0"
3
+ end
4
+
5
+
@@ -0,0 +1,33 @@
1
+ require "active_support"
2
+ require "active_support/core_ext/numeric/time"
3
+ require "active_support/core_ext/module/delegation"
4
+
5
+ require_relative "whoisonline/version"
6
+ require_relative "whoisonline/configuration"
7
+ require_relative "whoisonline/redis_store"
8
+ require_relative "whoisonline/tracker"
9
+ require_relative "whoisonline/engine"
10
+
11
+ module WhoIsOnline
12
+ class << self
13
+ delegate :track, :online?, :count, :user_ids, :users, to: :tracker
14
+
15
+ def tracker
16
+ @_tracker ||= Tracker.new(configuration, redis_store)
17
+ end
18
+
19
+ def redis_store
20
+ @_redis_store ||= RedisStore.new(configuration)
21
+ end
22
+
23
+ def configuration
24
+ @configuration ||= Configuration.new
25
+ end
26
+
27
+ def configure
28
+ yield(configuration)
29
+ configuration
30
+ end
31
+ end
32
+ end
33
+
data/logowho.png ADDED
Binary file
metadata ADDED
@@ -0,0 +1,114 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: whoisonline
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Kapil Dev Pal
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2025-12-27 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '7.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '7.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: railties
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '7.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '7.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: redis
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '4.0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '4.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: concurrent-ruby
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.2'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.2'
69
+ description: Production-ready Rails 7/8 online presence tracking using Redis TTL and
70
+ controller auto-hook.
71
+ email:
72
+ - dev.kapildevpal@gmail.com
73
+ executables: []
74
+ extensions: []
75
+ extra_rdoc_files: []
76
+ files:
77
+ - README.md
78
+ - lib/whoisonline.rb
79
+ - lib/whoisonline/configuration.rb
80
+ - lib/whoisonline/controller.rb
81
+ - lib/whoisonline/engine.rb
82
+ - lib/whoisonline/redis_store.rb
83
+ - lib/whoisonline/tracker.rb
84
+ - lib/whoisonline/version.rb
85
+ - logowho.png
86
+ homepage: https://github.com/KapilDevPal/WhoIsOnline
87
+ licenses:
88
+ - MIT
89
+ metadata:
90
+ homepage_uri: https://github.com/KapilDevPal/WhoIsOnline
91
+ source_code_uri: https://github.com/KapilDevPal/WhoIsOnline
92
+ changelog_uri: https://github.com/KapilDevPal/WhoIsOnline/blob/main/CHANGELOG.md
93
+ bug_tracker_uri: https://github.com/KapilDevPal/WhoIsOnline/issues
94
+ rubygems_mfa_required: 'true'
95
+ post_install_message:
96
+ rdoc_options: []
97
+ require_paths:
98
+ - lib
99
+ required_ruby_version: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '3.1'
104
+ required_rubygems_version: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - ">="
107
+ - !ruby/object:Gem::Version
108
+ version: '0'
109
+ requirements: []
110
+ rubygems_version: 3.4.19
111
+ signing_key:
112
+ specification_version: 4
113
+ summary: Redis-backed online presence for Rails without database writes
114
+ test_files: []