where_is_waldo 0.1.4 → 0.1.5
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 +4 -4
- data/CHANGELOG.md +45 -0
- data/VERSION +1 -1
- data/app/channels/where_is_waldo/presence_channel.rb +2 -1
- data/app/services/where_is_waldo/adapters/base_adapter.rb +12 -5
- data/app/services/where_is_waldo/adapters/database_adapter.rb +19 -7
- data/app/services/where_is_waldo/adapters/redis_adapter.rb +28 -31
- data/app/services/where_is_waldo/broadcaster.rb +5 -3
- data/app/services/where_is_waldo/presence_service.rb +9 -1
- data/lib/generators/where_is_waldo/install/templates/create_presences.rb.tt +9 -4
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9caee3a6cb5747d9ce5ab460315fdaca5a9f05f7521dbe07f36a6d9fe5de48d9
|
|
4
|
+
data.tar.gz: f7e293f87b04039c7a2dbc21e6a80ef574a773438800588a9f87255b9336497a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 95e63474cdbfff6c224b4bc4882bba3fb96cb157bf73a7769a1755f8b6326cba60e19aee6b06fc8fb58fc483496a68f8aa3e6bd03513a1782802ca123f8b9cb7
|
|
7
|
+
data.tar.gz: 9abb3e965ddf1a13722e5700c2c3c8927c9b6b43593801e592670ec4ab09483ce2a3cf2da6a764dfbfb204e238a0107cbd9628067c5e06b0d7920a24412a0726
|
data/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,51 @@
|
|
|
3
3
|
Notable changes to where_is_waldo. Format loosely follows
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/).
|
|
5
5
|
|
|
6
|
+
## 0.1.5
|
|
7
|
+
|
|
8
|
+
### Security / correctness
|
|
9
|
+
|
|
10
|
+
- **Session keys are now namespaced by subject_id.** Both adapters previously
|
|
11
|
+
keyed session rows by `session_id` alone (`waldo:session:<sid>` in Redis;
|
|
12
|
+
`unique_by: session_column` upsert in the DB). If two authenticated
|
|
13
|
+
subjects independently supplied the same `session_id` — client-supplied
|
|
14
|
+
values, JWT `jti` reuse across users, etc. — one subject's `connect`
|
|
15
|
+
would overwrite the other's row, and subsequent `heartbeat`/`disconnect`
|
|
16
|
+
calls could reach into the wrong subject's presence. Now:
|
|
17
|
+
- **RedisAdapter**: session key is `waldo:session:<subject_id>:<session_id>`.
|
|
18
|
+
The reverse-map key `waldo:session_subject:<session_id>` is removed —
|
|
19
|
+
callers must pass `subject_id` to `heartbeat`, `session_status`, and
|
|
20
|
+
session-scoped `disconnect`, so the disambiguation is at the API
|
|
21
|
+
boundary rather than a lookup that could return the wrong subject.
|
|
22
|
+
- **DatabaseAdapter**: `Presence.upsert(unique_by: [subject_column,
|
|
23
|
+
session_column])`. `heartbeat` / `session_status` / session-scoped
|
|
24
|
+
`disconnect` all scope by both keys. The install generator's migration
|
|
25
|
+
template now creates a composite unique index on `(subject_column,
|
|
26
|
+
session_column)` instead of a unique index on `session_column` alone.
|
|
27
|
+
|
|
28
|
+
### Breaking API changes
|
|
29
|
+
|
|
30
|
+
- `heartbeat` — `subject_id:` is now a required kwarg
|
|
31
|
+
(`heartbeat(session_id:, subject_id:, ...)`).
|
|
32
|
+
- `session_status` — signature is `session_status(session_id, subject_id)`
|
|
33
|
+
(previously `session_status(session_id)`).
|
|
34
|
+
- `disconnect(session_id:)` — now raises `ArgumentError` when called with
|
|
35
|
+
`session_id:` but no `subject_id:`. Subject-only `disconnect(subject_id:)`
|
|
36
|
+
and paired `disconnect(session_id:, subject_id:)` are the two supported
|
|
37
|
+
shapes.
|
|
38
|
+
- `Broadcaster.broadcast_to_session(session_id, subject_id, message_type, data)`
|
|
39
|
+
— `subject_id` inserted as the second positional argument (previously
|
|
40
|
+
`broadcast_to_session(session_id, message_type, data)`).
|
|
41
|
+
|
|
42
|
+
### DB migration
|
|
43
|
+
|
|
44
|
+
Hosts on the `:database` adapter must swap the unique index on the presences
|
|
45
|
+
table:
|
|
46
|
+
```ruby
|
|
47
|
+
remove_index :presences, :<session_column>
|
|
48
|
+
add_index :presences, [:<subject_column>, :<session_column>], unique: true
|
|
49
|
+
```
|
|
50
|
+
|
|
6
51
|
## 0.1.4
|
|
7
52
|
|
|
8
53
|
### Fixed
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.1.
|
|
1
|
+
0.1.5
|
|
@@ -25,7 +25,7 @@ module WhereIsWaldo
|
|
|
25
25
|
def unsubscribed
|
|
26
26
|
return if presence_suppressed?
|
|
27
27
|
|
|
28
|
-
WhereIsWaldo.disconnect(session_id: waldo_session_id)
|
|
28
|
+
WhereIsWaldo.disconnect(session_id: waldo_session_id, subject_id: waldo_subject_id)
|
|
29
29
|
|
|
30
30
|
# Recompute the subject's aggregate (they may still be present in another
|
|
31
31
|
# tab/device) and announce the change to the org roster.
|
|
@@ -42,6 +42,7 @@ module WhereIsWaldo
|
|
|
42
42
|
|
|
43
43
|
WhereIsWaldo.heartbeat(
|
|
44
44
|
session_id: waldo_session_id,
|
|
45
|
+
subject_id: waldo_subject_id,
|
|
45
46
|
tab_visible: tab_visible,
|
|
46
47
|
subject_active: subject_active,
|
|
47
48
|
last_activity_at: data[:last_activity_at],
|
|
@@ -12,9 +12,11 @@ module WhereIsWaldo
|
|
|
12
12
|
raise NotImplementedError
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
-
# Remove a presence
|
|
16
|
-
#
|
|
17
|
-
#
|
|
15
|
+
# Remove a presence. Session-scoped disconnect requires both keys so
|
|
16
|
+
# a caller-supplied session_id can't disconnect another subject's row.
|
|
17
|
+
# Subject-only disconnect removes every session for that subject.
|
|
18
|
+
# @param session_id [String] Session identifier (requires subject_id when set)
|
|
19
|
+
# @param subject_id [Integer/String] Subject identifier
|
|
18
20
|
# @return [Boolean] success
|
|
19
21
|
def disconnect(session_id: nil, subject_id: nil)
|
|
20
22
|
raise NotImplementedError
|
|
@@ -22,11 +24,14 @@ module WhereIsWaldo
|
|
|
22
24
|
|
|
23
25
|
# Update heartbeat
|
|
24
26
|
# @param session_id [String] Session identifier
|
|
27
|
+
# @param subject_id [Integer/String] Subject identifier (required —
|
|
28
|
+
# pairs with session_id to disambiguate colliding session ids across
|
|
29
|
+
# subjects)
|
|
25
30
|
# @param tab_visible [Boolean] Is tab in foreground
|
|
26
31
|
# @param subject_active [Boolean] Recent activity
|
|
27
32
|
# @param metadata [Hash] Additional data
|
|
28
33
|
# @return [Boolean] success
|
|
29
|
-
def heartbeat(session_id:, tab_visible: true, subject_active: true, metadata: {})
|
|
34
|
+
def heartbeat(session_id:, subject_id:, tab_visible: true, subject_active: true, metadata: {})
|
|
30
35
|
raise NotImplementedError
|
|
31
36
|
end
|
|
32
37
|
|
|
@@ -65,8 +70,10 @@ module WhereIsWaldo
|
|
|
65
70
|
|
|
66
71
|
# Get session status
|
|
67
72
|
# @param session_id [String] Session identifier
|
|
73
|
+
# @param subject_id [Integer/String] Subject identifier — required
|
|
74
|
+
# for the same reason as heartbeat (see above)
|
|
68
75
|
# @return [Hash, nil] Presence record or nil
|
|
69
|
-
def session_status(session_id)
|
|
76
|
+
def session_status(session_id, subject_id)
|
|
70
77
|
raise NotImplementedError
|
|
71
78
|
end
|
|
72
79
|
|
|
@@ -19,8 +19,12 @@ module WhereIsWaldo
|
|
|
19
19
|
updated_at: now
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
# (subject, session) is the unique key — see the install migration
|
|
23
|
+
# template. Keying on session alone would let a caller-supplied
|
|
24
|
+
# session_id colliding across two subjects upsert onto each other's
|
|
25
|
+
# row.
|
|
22
26
|
# rubocop:disable Rails/SkipsModelValidations -- intentional for performance
|
|
23
|
-
Presence.upsert(attrs, unique_by: session_column)
|
|
27
|
+
Presence.upsert(attrs, unique_by: [subject_column, session_column])
|
|
24
28
|
# rubocop:enable Rails/SkipsModelValidations
|
|
25
29
|
true
|
|
26
30
|
rescue StandardError => e
|
|
@@ -29,15 +33,21 @@ module WhereIsWaldo
|
|
|
29
33
|
end
|
|
30
34
|
|
|
31
35
|
def disconnect(session_id: nil, subject_id: nil)
|
|
36
|
+
raise ArgumentError, "disconnect(session_id:) requires subject_id:" if session_id && !subject_id
|
|
37
|
+
|
|
32
38
|
scope = build_lookup_scope(session_id: session_id, subject_id: subject_id)
|
|
33
39
|
scope.delete_all
|
|
34
40
|
true
|
|
41
|
+
rescue ArgumentError
|
|
42
|
+
raise
|
|
35
43
|
rescue StandardError => e
|
|
36
44
|
Rails.logger.error "[WhereIsWaldo] Disconnect failed: #{e.message}"
|
|
37
45
|
false
|
|
38
46
|
end
|
|
39
47
|
|
|
40
|
-
|
|
48
|
+
# rubocop:disable Metrics/ParameterLists, Layout/LineLength
|
|
49
|
+
def heartbeat(session_id:, subject_id:, tab_visible: true, subject_active: true, last_activity_at: nil, metadata: {})
|
|
50
|
+
# rubocop:enable Metrics/ParameterLists, Layout/LineLength
|
|
41
51
|
now = Time.current
|
|
42
52
|
updates = {
|
|
43
53
|
last_heartbeat: now,
|
|
@@ -53,7 +63,9 @@ module WhereIsWaldo
|
|
|
53
63
|
end
|
|
54
64
|
updates[:metadata] = metadata if metadata.present?
|
|
55
65
|
|
|
56
|
-
|
|
66
|
+
# Scope by both — a caller-supplied session_id shouldn't be able to
|
|
67
|
+
# heartbeat another subject's row.
|
|
68
|
+
scope = Presence.where(session_column => session_id, subject_column => subject_id)
|
|
57
69
|
|
|
58
70
|
# rubocop:disable Rails/SkipsModelValidations -- intentional for performance
|
|
59
71
|
scope.update_all(updates).positive?
|
|
@@ -88,8 +100,8 @@ module WhereIsWaldo
|
|
|
88
100
|
.transform_values { |rows| rows.map(&:as_presence_hash) }
|
|
89
101
|
end
|
|
90
102
|
|
|
91
|
-
def session_status(session_id)
|
|
92
|
-
scope = Presence.where(session_column => session_id)
|
|
103
|
+
def session_status(session_id, subject_id)
|
|
104
|
+
scope = Presence.where(session_column => session_id, subject_column => subject_id)
|
|
93
105
|
scope = scope.includes(:subject) if config.subject_class_constant
|
|
94
106
|
scope.first&.as_presence_hash
|
|
95
107
|
end
|
|
@@ -102,8 +114,8 @@ module WhereIsWaldo
|
|
|
102
114
|
private
|
|
103
115
|
|
|
104
116
|
def build_lookup_scope(session_id: nil, subject_id: nil)
|
|
105
|
-
if session_id
|
|
106
|
-
Presence.where(session_column => session_id)
|
|
117
|
+
if session_id && subject_id
|
|
118
|
+
Presence.where(session_column => session_id, subject_column => subject_id)
|
|
107
119
|
elsif subject_id
|
|
108
120
|
Presence.where(subject_column => subject_id)
|
|
109
121
|
else
|
|
@@ -20,17 +20,15 @@ module WhereIsWaldo
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
redis.multi do |tx|
|
|
23
|
-
#
|
|
24
|
-
|
|
23
|
+
# Session key includes subject_id so two subjects with the same
|
|
24
|
+
# caller-supplied session_id can't overwrite each other's row.
|
|
25
|
+
tx.set(session_key(subject_id, session_id), presence_data.to_json, ex: ttl)
|
|
25
26
|
|
|
26
27
|
# Add to online subjects sorted set with timestamp as score
|
|
27
28
|
tx.zadd(online_subjects_key, now, subject_id)
|
|
28
29
|
|
|
29
30
|
# Add to subject's sessions set
|
|
30
31
|
tx.sadd(subject_sessions_key(subject_id), session_id)
|
|
31
|
-
|
|
32
|
-
# Map session to subject for reverse lookup
|
|
33
|
-
tx.set(session_subject_key(session_id), subject_id, ex: ttl)
|
|
34
32
|
end
|
|
35
33
|
|
|
36
34
|
true
|
|
@@ -41,19 +39,25 @@ module WhereIsWaldo
|
|
|
41
39
|
|
|
42
40
|
def disconnect(session_id: nil, subject_id: nil)
|
|
43
41
|
if session_id
|
|
44
|
-
|
|
42
|
+
raise ArgumentError, "disconnect(session_id:) requires subject_id:" unless subject_id
|
|
43
|
+
|
|
44
|
+
disconnect_session(subject_id, session_id)
|
|
45
45
|
elsif subject_id
|
|
46
46
|
disconnect_subject(subject_id)
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
true
|
|
50
|
+
rescue ArgumentError
|
|
51
|
+
raise
|
|
50
52
|
rescue StandardError => e
|
|
51
53
|
Rails.logger.error "[WhereIsWaldo] Redis disconnect failed: #{e.message}"
|
|
52
54
|
false
|
|
53
55
|
end
|
|
54
56
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
+
# rubocop:disable Metrics/ParameterLists, Layout/LineLength
|
|
58
|
+
def heartbeat(session_id:, subject_id:, tab_visible: true, subject_active: true, last_activity_at: nil, metadata: {})
|
|
59
|
+
# rubocop:enable Metrics/ParameterLists, Layout/LineLength
|
|
60
|
+
data = get_presence_data(subject_id, session_id)
|
|
57
61
|
return false unless data
|
|
58
62
|
|
|
59
63
|
now = Time.current.to_i
|
|
@@ -69,7 +73,7 @@ module WhereIsWaldo
|
|
|
69
73
|
data["metadata"] = data["metadata"].merge(metadata) if metadata.present?
|
|
70
74
|
|
|
71
75
|
redis.multi do |tx|
|
|
72
|
-
tx.set(session_key(session_id), data.to_json, ex: ttl)
|
|
76
|
+
tx.set(session_key(subject_id, session_id), data.to_json, ex: ttl)
|
|
73
77
|
tx.zadd(online_subjects_key, now, data["subject_id"])
|
|
74
78
|
end
|
|
75
79
|
|
|
@@ -90,7 +94,7 @@ module WhereIsWaldo
|
|
|
90
94
|
session_ids = redis.smembers(subject_sessions_key(subject_id))
|
|
91
95
|
|
|
92
96
|
session_ids.filter_map do |sid|
|
|
93
|
-
data = get_presence_data(sid)
|
|
97
|
+
data = get_presence_data(subject_id, sid)
|
|
94
98
|
build_presence_hash(data) if data
|
|
95
99
|
end
|
|
96
100
|
end
|
|
@@ -103,7 +107,7 @@ module WhereIsWaldo
|
|
|
103
107
|
ids.each_with_object({}) do |sid, memo|
|
|
104
108
|
session_ids = redis.smembers(subject_sessions_key(sid))
|
|
105
109
|
live = session_ids.filter_map do |cid|
|
|
106
|
-
data = get_presence_data(cid)
|
|
110
|
+
data = get_presence_data(sid, cid)
|
|
107
111
|
next unless data && data["last_heartbeat"].to_i >= threshold
|
|
108
112
|
|
|
109
113
|
build_presence_hash(data)
|
|
@@ -112,8 +116,8 @@ module WhereIsWaldo
|
|
|
112
116
|
end
|
|
113
117
|
end
|
|
114
118
|
|
|
115
|
-
def session_status(session_id)
|
|
116
|
-
data = get_presence_data(session_id)
|
|
119
|
+
def session_status(session_id, subject_id)
|
|
120
|
+
data = get_presence_data(subject_id, session_id)
|
|
117
121
|
return nil unless data
|
|
118
122
|
|
|
119
123
|
build_presence_hash(data)
|
|
@@ -130,7 +134,7 @@ module WhereIsWaldo
|
|
|
130
134
|
# Check if any sessions are still active
|
|
131
135
|
session_ids = redis.smembers(subject_sessions_key(subject_id))
|
|
132
136
|
all_stale = session_ids.all? do |sid|
|
|
133
|
-
data = get_presence_data(sid)
|
|
137
|
+
data = get_presence_data(subject_id, sid)
|
|
134
138
|
!data || data["last_heartbeat"].to_i < threshold
|
|
135
139
|
end
|
|
136
140
|
|
|
@@ -160,8 +164,11 @@ module WhereIsWaldo
|
|
|
160
164
|
config.redis_prefix || "where_is_waldo"
|
|
161
165
|
end
|
|
162
166
|
|
|
163
|
-
|
|
164
|
-
|
|
167
|
+
# Session data key. Namespaced by subject_id so a caller-supplied
|
|
168
|
+
# session_id colliding across two authenticated subjects doesn't let one
|
|
169
|
+
# clobber the other's presence row (see CHANGELOG 0.1.5).
|
|
170
|
+
def session_key(subject_id, session_id)
|
|
171
|
+
"#{key_prefix}:session:#{subject_id}:#{session_id}"
|
|
165
172
|
end
|
|
166
173
|
|
|
167
174
|
def online_subjects_key
|
|
@@ -172,27 +179,17 @@ module WhereIsWaldo
|
|
|
172
179
|
"#{key_prefix}:subject:#{subject_id}:sessions"
|
|
173
180
|
end
|
|
174
181
|
|
|
175
|
-
def
|
|
176
|
-
|
|
177
|
-
end
|
|
178
|
-
|
|
179
|
-
def get_presence_data(session_id)
|
|
180
|
-
json = redis.get(session_key(session_id))
|
|
182
|
+
def get_presence_data(subject_id, session_id)
|
|
183
|
+
json = redis.get(session_key(subject_id, session_id))
|
|
181
184
|
return nil unless json
|
|
182
185
|
|
|
183
186
|
JSON.parse(json)
|
|
184
187
|
end
|
|
185
188
|
|
|
186
|
-
def disconnect_session(session_id)
|
|
187
|
-
data = get_presence_data(session_id)
|
|
188
|
-
return unless data
|
|
189
|
-
|
|
190
|
-
subject_id = data["subject_id"]
|
|
191
|
-
|
|
189
|
+
def disconnect_session(subject_id, session_id)
|
|
192
190
|
redis.multi do |tx|
|
|
193
|
-
tx.del(session_key(session_id))
|
|
191
|
+
tx.del(session_key(subject_id, session_id))
|
|
194
192
|
tx.srem(subject_sessions_key(subject_id), session_id)
|
|
195
|
-
tx.del(session_subject_key(session_id))
|
|
196
193
|
end
|
|
197
194
|
|
|
198
195
|
# If no more sessions for this subject, remove from online set
|
|
@@ -202,7 +199,7 @@ module WhereIsWaldo
|
|
|
202
199
|
|
|
203
200
|
def disconnect_subject(subject_id)
|
|
204
201
|
session_ids = redis.smembers(subject_sessions_key(subject_id))
|
|
205
|
-
session_ids.each { |sid| disconnect_session(sid) }
|
|
202
|
+
session_ids.each { |sid| disconnect_session(subject_id, sid) }
|
|
206
203
|
redis.zrem(online_subjects_key, subject_id)
|
|
207
204
|
end
|
|
208
205
|
|
|
@@ -35,14 +35,16 @@ module WhereIsWaldo
|
|
|
35
35
|
|
|
36
36
|
# Broadcast to a specific session
|
|
37
37
|
# @param session_id [String] Session identifier
|
|
38
|
+
# @param subject_id [Integer/String] Subject identifier — required
|
|
39
|
+
# to disambiguate colliding session ids across subjects
|
|
38
40
|
# @param message_type [String, Symbol] Message type
|
|
39
41
|
# @param data [Hash] Message payload
|
|
40
|
-
def broadcast_to_session(session_id, message_type, data = {})
|
|
41
|
-
status = PresenceService.session_status(session_id)
|
|
42
|
+
def broadcast_to_session(session_id, subject_id, message_type, data = {})
|
|
43
|
+
status = PresenceService.session_status(session_id, subject_id)
|
|
42
44
|
return false unless status
|
|
43
45
|
|
|
44
46
|
message = build_message(message_type, data, target_session: session_id)
|
|
45
|
-
ActionCable.server.broadcast(subject_stream(
|
|
47
|
+
ActionCable.server.broadcast(subject_stream(subject_id), message)
|
|
46
48
|
true
|
|
47
49
|
end
|
|
48
50
|
|
|
@@ -29,14 +29,20 @@ module WhereIsWaldo
|
|
|
29
29
|
|
|
30
30
|
# Update heartbeat for a session
|
|
31
31
|
# @param session_id [String] Session identifier
|
|
32
|
+
# @param subject_id [Integer/String] Subject identifier (required —
|
|
33
|
+
# pairs with session_id to disambiguate colliding session ids across
|
|
34
|
+
# subjects)
|
|
32
35
|
# @param tab_visible [Boolean] Is tab in foreground
|
|
33
36
|
# @param subject_active [Boolean] Recent activity
|
|
34
37
|
# @param last_activity_at [Integer] Unix timestamp (ms) of last user activity
|
|
35
38
|
# @param metadata [Hash] Additional data to merge
|
|
36
39
|
# @return [Boolean] success
|
|
37
|
-
|
|
40
|
+
# rubocop:disable Metrics/ParameterLists, Layout/LineLength
|
|
41
|
+
def heartbeat(session_id:, subject_id:, tab_visible: true, subject_active: true, last_activity_at: nil, metadata: {})
|
|
42
|
+
# rubocop:enable Metrics/ParameterLists, Layout/LineLength
|
|
38
43
|
adapter.heartbeat(
|
|
39
44
|
session_id: session_id,
|
|
45
|
+
subject_id: subject_id,
|
|
40
46
|
tab_visible: tab_visible,
|
|
41
47
|
subject_active: subject_active,
|
|
42
48
|
last_activity_at: last_activity_at,
|
|
@@ -85,6 +91,8 @@ module WhereIsWaldo
|
|
|
85
91
|
|
|
86
92
|
# Get status of a specific session
|
|
87
93
|
# @param session_id [String] Session identifier
|
|
94
|
+
# @param subject_id [Integer/String] Subject identifier — required
|
|
95
|
+
# for the same reason as heartbeat
|
|
88
96
|
# @return [Hash, nil] Presence record or nil
|
|
89
97
|
delegate :session_status, to: :adapter
|
|
90
98
|
|
|
@@ -22,10 +22,15 @@ class Create<%= table_name.camelize %> < ActiveRecord::Migration[7.0]
|
|
|
22
22
|
t.timestamps
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
#
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
#
|
|
25
|
+
# (subject, session) must be unique together. Keying by session alone
|
|
26
|
+
# would let two authenticated subjects that happen to collide on a
|
|
27
|
+
# session id (client-supplied values, JWT jti reuse, etc.) upsert onto
|
|
28
|
+
# each other's row.
|
|
29
|
+
add_index :<%= table_name %>, [:<%= subject_column %>, :<%= session_column %>], unique: true
|
|
30
|
+
|
|
31
|
+
# Session lookups (heartbeat, disconnect, session_status) always scope
|
|
32
|
+
# by (subject, session); the composite unique index above serves those.
|
|
33
|
+
# Query by subject alone
|
|
29
34
|
add_index :<%= table_name %>, :<%= subject_column %>
|
|
30
35
|
|
|
31
36
|
# Cleanup old records
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: where_is_waldo
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Scott Gibson
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-07-
|
|
11
|
+
date: 2026-07-24 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|