Class: WhereIsWaldo::Adapters::RedisAdapter

Inherits:
BaseAdapter
  • Object
show all
Defined in:
app/services/where_is_waldo/adapters/redis_adapter.rb

Overview

Redis backing needs more plumbing (keying, TTLs, pipelines) than the DB adapter, so it legitimately runs past the default class-length limit.

Instance Method Summary collapse

Instance Method Details

#cleanup(timeout: nil) ⇒ Object



126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'app/services/where_is_waldo/adapters/redis_adapter.rb', line 126

def cleanup(timeout: nil)
  threshold = Time.current.to_i - (timeout || default_timeout)
  cleaned = 0

  # Get stale subject IDs
  stale_subject_ids = redis.zrangebyscore(online_subjects_key, "-inf", threshold)

  stale_subject_ids.each do |subject_id|
    # Check if any sessions are still active
    session_ids = redis.smembers(subject_sessions_key(subject_id))
    all_stale = session_ids.all? do |sid|
      data = get_presence_data(subject_id, sid)
      !data || data["last_heartbeat"].to_i < threshold
    end

    if all_stale
      disconnect_subject(subject_id)
      cleaned += 1
    end
  end

  # Remove stale entries from sorted set
  redis.zremrangebyscore(online_subjects_key, "-inf", threshold)

  cleaned
end

#connect(session_id:, subject_id:, metadata: {}) ⇒ Object

rubocop:disable Metrics/ClassLength



8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# File 'app/services/where_is_waldo/adapters/redis_adapter.rb', line 8

def connect(session_id:, subject_id:, metadata: {})
  now = Time.current.to_i

  presence_data = {
    session_id: session_id,
    subject_id: subject_id,
    connected_at: now,
    last_heartbeat: now,
    tab_visible: true,
    subject_active: true,
    last_activity: now,
    metadata: 
  }

  redis.multi do |tx|
    # Session key includes subject_id so two subjects with the same
    # caller-supplied session_id can't overwrite each other's row.
    tx.set(session_key(subject_id, session_id), presence_data.to_json, ex: ttl)

    # Add to online subjects sorted set with timestamp as score
    tx.zadd(online_subjects_key, now, subject_id)

    # Add to subject's sessions set
    tx.sadd(subject_sessions_key(subject_id), session_id)
  end

  true
rescue StandardError => e
  Rails.logger.error "[WhereIsWaldo] Redis connect failed: #{e.message}"
  false
end

#disconnect(session_id: nil, subject_id: nil) ⇒ Object



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# File 'app/services/where_is_waldo/adapters/redis_adapter.rb', line 40

def disconnect(session_id: nil, subject_id: nil)
  if session_id
    raise ArgumentError, "disconnect(session_id:) requires subject_id:" unless subject_id

    disconnect_session(subject_id, session_id)
  elsif subject_id
    disconnect_subject(subject_id)
  end

  true
rescue ArgumentError
  raise
rescue StandardError => e
  Rails.logger.error "[WhereIsWaldo] Redis disconnect failed: #{e.message}"
  false
end

#heartbeat(session_id:, subject_id:, tab_visible: true, subject_active: true, last_activity_at: nil, metadata: {}) ⇒ Object

rubocop:disable Metrics/ParameterLists, Layout/LineLength



58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# File 'app/services/where_is_waldo/adapters/redis_adapter.rb', line 58

def heartbeat(session_id:, subject_id:, tab_visible: true, subject_active: true, last_activity_at: nil, metadata: {})
  # rubocop:enable Metrics/ParameterLists, Layout/LineLength
  data = get_presence_data(subject_id, session_id)
  return false unless data

  now = Time.current.to_i
  data["last_heartbeat"] = now
  data["tab_visible"] = tab_visible
  data["subject_active"] = subject_active
  # Update last_activity: use JS timestamp if provided, otherwise use current time when active
  if last_activity_at
    data["last_activity"] = (last_activity_at / 1000.0).to_i
  elsif subject_active
    data["last_activity"] = now
  end
  data["metadata"] = data["metadata"].merge() if .present?

  redis.multi do |tx|
    tx.set(session_key(subject_id, session_id), data.to_json, ex: ttl)
    tx.zadd(online_subjects_key, now, data["subject_id"])
  end

  true
rescue StandardError => e
  Rails.logger.error "[WhereIsWaldo] Redis heartbeat failed: #{e.message}"
  false
end

#online_subject_ids(timeout: nil) ⇒ Object



86
87
88
89
90
91
# File 'app/services/where_is_waldo/adapters/redis_adapter.rb', line 86

def online_subject_ids(timeout: nil)
  threshold = Time.current.to_i - (timeout || default_timeout)

  # Get subject IDs with heartbeat after threshold
  redis.zrangebyscore(online_subjects_key, threshold, "+inf").map(&:to_i)
end

#session_status(session_id, subject_id) ⇒ Object



119
120
121
122
123
124
# File 'app/services/where_is_waldo/adapters/redis_adapter.rb', line 119

def session_status(session_id, subject_id)
  data = get_presence_data(subject_id, session_id)
  return nil unless data

  build_presence_hash(data)
end

#sessions_for_subject(subject_id) ⇒ Object



93
94
95
96
97
98
99
100
# File 'app/services/where_is_waldo/adapters/redis_adapter.rb', line 93

def sessions_for_subject(subject_id)
  session_ids = redis.smembers(subject_sessions_key(subject_id))

  session_ids.filter_map do |sid|
    data = get_presence_data(subject_id, sid)
    build_presence_hash(data) if data
  end
end

#sessions_for_subjects(subject_ids, timeout: nil) ⇒ Object



102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'app/services/where_is_waldo/adapters/redis_adapter.rb', line 102

def sessions_for_subjects(subject_ids, timeout: nil)
  ids = Array(subject_ids).compact.uniq
  return {} if ids.empty?

  threshold = Time.current.to_i - (timeout || default_timeout)
  ids.each_with_object({}) do |sid, memo|
    session_ids = redis.smembers(subject_sessions_key(sid))
    live = session_ids.filter_map do |cid|
      data = get_presence_data(sid, cid)
      next unless data && data["last_heartbeat"].to_i >= threshold

      build_presence_hash(data)
    end
    memo[sid] = live if live.any?
  end
end