where_is_waldo 0.1.11 → 0.1.12
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 +30 -0
- data/README.md +26 -0
- data/VERSION +1 -1
- data/app/channels/where_is_waldo/presence_channel.rb +32 -3
- data/app/channels/where_is_waldo/roster_channel.rb +13 -6
- data/app/services/where_is_waldo/roster/org_keys.rb +81 -0
- data/app/services/where_is_waldo/roster.rb +28 -11
- data/app/services/where_is_waldo/roster_delivery.rb +16 -9
- data/lib/where_is_waldo/configuration.rb +23 -5
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e9b514019f64edaaea98c2d51266e3668f5a3bd270c5727013bb38bd7db51232
|
|
4
|
+
data.tar.gz: c4313e9f5f1f05b3640c2e019dcbe7883f4a59f027944f6395878baa60bdfdf4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 02aaa7a93acfa3c4732eec460b1fad4f6361dc0c64e9ed60539914b025d785db4dd4ed8acb8b0e55ae92a4cda116c31acb1f46745c1567196b461470b2ea8e27
|
|
7
|
+
data.tar.gz: 955493cf1cab229f550e25ffc3a8e54069ba297bf9bf9e0ee6ed0acc5e092bae61c00278b684a44d14833fe23210b0b35409fc2a09f6234cabcd10df2a44bf91
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,36 @@ Notable changes to where_is_waldo. Format loosely follows
|
|
|
5
5
|
|
|
6
6
|
## Unreleased
|
|
7
7
|
|
|
8
|
+
## 0.1.12 - 2026-10-27
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **A subject in more than one org could be put on the wrong roster.**
|
|
13
|
+
`roster_org` receives only the subject, so an app whose users belong to
|
|
14
|
+
several orgs had to guess, and the obvious guess (`accounts.first`) is
|
|
15
|
+
association order rather than the org the person is actually working in.
|
|
16
|
+
They were then invisible to the teammates they were with, and shown to an
|
|
17
|
+
org they were not in.
|
|
18
|
+
|
|
19
|
+
New `roster_org_for` receives `(subject, container)`, where the container is
|
|
20
|
+
the one the CONNECTION is scoped to -- the only place that answer exists. It
|
|
21
|
+
applies on BOTH sides: the presence row stores the resolved org at connect
|
|
22
|
+
and later broadcasts read it back, and the roster subscription resolves the
|
|
23
|
+
same way, so a viewer and the broadcasts meant for them can no longer land
|
|
24
|
+
on different streams. `container` comes from your connection's
|
|
25
|
+
`identified_by :current_container`; without that it is nil and behaviour is
|
|
26
|
+
unchanged. A subject present in two
|
|
27
|
+
orgs now appears in both rosters rather than one arbitrary one.
|
|
28
|
+
|
|
29
|
+
Stored in the existing `metadata` column, so **no migration is required**.
|
|
30
|
+
`roster_org` is untouched and still works for single-org apps.
|
|
31
|
+
|
|
32
|
+
- **A presence transition read a subject's sessions twice.** Publishing needs
|
|
33
|
+
both the org keys and the aggregate state, and both come from the same rows;
|
|
34
|
+
they are now derived from one read. This is the hottest path in the library,
|
|
35
|
+
so the second read was pure overhead.
|
|
36
|
+
|
|
37
|
+
|
|
8
38
|
## 0.1.11 - 2026-10-27
|
|
9
39
|
|
|
10
40
|
### Fixed
|
data/README.md
CHANGED
|
@@ -290,6 +290,32 @@ their live sessions (multiple browser tabs, mobile, etc.):
|
|
|
290
290
|
WhereIsWaldo.configure do |config|
|
|
291
291
|
# The org/account a subject belongs to. Required to enable the roster.
|
|
292
292
|
config.roster_org = ->(subject) { subject.account }
|
|
293
|
+
#
|
|
294
|
+
# MULTI-ORG SUBJECTS: roster_org is handed only the subject, so it has to
|
|
295
|
+
# guess which org the person is in right now -- and `accounts.first` guesses
|
|
296
|
+
# by association order. Someone who has switched is then put on the wrong
|
|
297
|
+
# roster: invisible to the teammates they are with, shown to an org they are
|
|
298
|
+
# not in. Use roster_org_for instead, which also receives the container the
|
|
299
|
+
# CONNECTION is scoped to:
|
|
300
|
+
#
|
|
301
|
+
# config.roster_org_for = ->(user, container) { container || user.accounts.first }
|
|
302
|
+
#
|
|
303
|
+
# `container` is read from your own ActionCable connection, so it is only
|
|
304
|
+
# ever non-nil if that connection identifies one:
|
|
305
|
+
#
|
|
306
|
+
# module ApplicationCable
|
|
307
|
+
# class Connection < ActionCable::Connection::Base
|
|
308
|
+
# identified_by :current_user, :current_container, :session_id
|
|
309
|
+
# end
|
|
310
|
+
# end
|
|
311
|
+
#
|
|
312
|
+
# WhereIsWaldo::JwtConnection already does this. Without it `container` is
|
|
313
|
+
# always nil and the fallback above behaves exactly like plain roster_org --
|
|
314
|
+
# which is to say, like the bug.
|
|
315
|
+
#
|
|
316
|
+
# The resolved org is stored on the presence row at connect and read back by
|
|
317
|
+
# later broadcasts, so the subscriber and the broadcaster always agree on the
|
|
318
|
+
# stream. roster_org keeps working unchanged for single-org apps.
|
|
293
319
|
|
|
294
320
|
# What a VIEWER may see (:poll/:nudge/:fanout). Any visibility rule,
|
|
295
321
|
# server-enforced. Defaults to the viewer's whole org when unset.
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.1.
|
|
1
|
+
0.1.12
|
|
@@ -88,13 +88,43 @@ module WhereIsWaldo
|
|
|
88
88
|
end
|
|
89
89
|
|
|
90
90
|
def register_presence
|
|
91
|
+
# The org key is computed HERE and written over anything the client sent.
|
|
92
|
+
# params[:metadata] is client-supplied, so letting a tab name its own
|
|
93
|
+
# roster would let it subscribe itself into an org it has no business
|
|
94
|
+
# seeing.
|
|
95
|
+
#
|
|
96
|
+
# The client's value is REMOVED FIRST, unconditionally. Only overwriting
|
|
97
|
+
# it when the server resolves an org leaves the hole open in exactly the
|
|
98
|
+
# case that matters: a connection with no org of its own could name any
|
|
99
|
+
# org it liked, the value would persist on the presence row, and later
|
|
100
|
+
# broadcasts read it back verbatim and push that subject's presence into
|
|
101
|
+
# a stream they have no claim on.
|
|
102
|
+
metadata = (params[:metadata] || {}).to_h
|
|
103
|
+
metadata.delete(WhereIsWaldo::Roster::ORG_KEY)
|
|
104
|
+
metadata.delete(WhereIsWaldo::Roster::ORG_KEY.to_sym)
|
|
105
|
+
|
|
106
|
+
key = WhereIsWaldo::Roster.org_key(wiw_connection_org)
|
|
107
|
+
metadata[WhereIsWaldo::Roster::ORG_KEY] = key if key
|
|
108
|
+
|
|
91
109
|
WhereIsWaldo.connect(
|
|
92
110
|
session_id: waldo_session_id,
|
|
93
111
|
subject_id: waldo_subject_id,
|
|
94
|
-
metadata:
|
|
112
|
+
metadata: metadata
|
|
95
113
|
)
|
|
96
114
|
end
|
|
97
115
|
|
|
116
|
+
# The org this CONNECTION is scoped to. The container comes from the host
|
|
117
|
+
# app's own cable connection, which is the only place that knows the org
|
|
118
|
+
# the person is actually working in -- a lambda given just the subject has
|
|
119
|
+
# to guess, and guesses by association order.
|
|
120
|
+
def wiw_connection_org
|
|
121
|
+
return @wiw_connection_org if defined?(@wiw_connection_org)
|
|
122
|
+
|
|
123
|
+
subject = WhereIsWaldo.config.subject_class_constant&.find_by(id: waldo_subject_id)
|
|
124
|
+
container = connection.try(:current_container)
|
|
125
|
+
@wiw_connection_org = subject && WhereIsWaldo.config.resolve_roster_org(subject, container: container)
|
|
126
|
+
end
|
|
127
|
+
|
|
98
128
|
def roster_transition?(tab_visible, subject_active)
|
|
99
129
|
tab_visible != @wiw_tab_visible || subject_active != @wiw_subject_active
|
|
100
130
|
end
|
|
@@ -104,8 +134,7 @@ module WhereIsWaldo
|
|
|
104
134
|
def resolve_roster_strategy
|
|
105
135
|
return nil unless WhereIsWaldo.config.roster_enabled?
|
|
106
136
|
|
|
107
|
-
|
|
108
|
-
org = subject && WhereIsWaldo.config.resolve_roster_org(subject)
|
|
137
|
+
org = wiw_connection_org
|
|
109
138
|
return nil unless org
|
|
110
139
|
|
|
111
140
|
WhereIsWaldo::RosterDelivery.for(WhereIsWaldo.config.resolve_mode(org))
|
|
@@ -13,12 +13,15 @@ module WhereIsWaldo
|
|
|
13
13
|
class RosterChannel < ApplicationCable::Channel
|
|
14
14
|
def subscribed
|
|
15
15
|
subject = current_subject
|
|
16
|
-
|
|
16
|
+
org = resolve_org(subject)
|
|
17
|
+
return reject unless org
|
|
18
|
+
|
|
19
|
+
mode = WhereIsWaldo.config.resolve_mode(org)
|
|
17
20
|
return reject unless mode
|
|
18
21
|
|
|
19
22
|
@wiw_subject = subject
|
|
20
23
|
@wiw_strategy = RosterDelivery.for(mode)
|
|
21
|
-
apply_plan(@wiw_strategy.subscribe_plan(subject, waldo_session_id))
|
|
24
|
+
apply_plan(@wiw_strategy.subscribe_plan(subject, waldo_session_id, org: org))
|
|
22
25
|
end
|
|
23
26
|
|
|
24
27
|
# Client-driven poll (pull/nudge modes). Transmits a filtered snapshot
|
|
@@ -31,11 +34,15 @@ module WhereIsWaldo
|
|
|
31
34
|
|
|
32
35
|
private
|
|
33
36
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
# The org this VIEWER is watching. Resolved from the connection's container
|
|
38
|
+
# like the writer side, so the stream they subscribe to is the one deltas
|
|
39
|
+
# are actually broadcast into. Resolving from the subject alone here was
|
|
40
|
+
# the other half of the same bug: a viewer scoped to their second org sat
|
|
41
|
+
# on their first org's stream and simply never saw anything.
|
|
42
|
+
def resolve_org(subject)
|
|
43
|
+
return nil unless subject
|
|
37
44
|
|
|
38
|
-
WhereIsWaldo.config.
|
|
45
|
+
WhereIsWaldo.config.resolve_roster_org(subject, container: connection.try(:current_container))
|
|
39
46
|
end
|
|
40
47
|
|
|
41
48
|
def apply_plan(plan)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WhereIsWaldo
|
|
4
|
+
class Roster
|
|
5
|
+
# Which roster a session belongs to, and how that is stored.
|
|
6
|
+
#
|
|
7
|
+
# Extracted from Roster so the resolution rules sit together and can be
|
|
8
|
+
# read without the delivery machinery around them.
|
|
9
|
+
module OrgKeys
|
|
10
|
+
# WHICH ROSTER A SESSION BELONGS TO IS DECIDED ONCE, AT CONNECT, AND
|
|
11
|
+
# STORED ON THE PRESENCE ROW.
|
|
12
|
+
#
|
|
13
|
+
# It cannot be re-derived later. The connection knows the container the
|
|
14
|
+
# person is actually working in; a background broadcast an hour later has
|
|
15
|
+
# only a subject id, and a subject can belong to several orgs. Deriving
|
|
16
|
+
# it twice is how the subscriber ends up listening to one stream while
|
|
17
|
+
# the broadcaster writes to another -- which looks exactly like presence
|
|
18
|
+
# being broken, not like a resolution bug.
|
|
19
|
+
#
|
|
20
|
+
# Stored in metadata rather than a column so this needs no migration in
|
|
21
|
+
# any consuming app.
|
|
22
|
+
def org_key(org)
|
|
23
|
+
return nil unless org
|
|
24
|
+
|
|
25
|
+
klass = org.class.respond_to?(:base_class) ? org.class.base_class : org.class
|
|
26
|
+
"#{klass.name}:#{org.id}"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def org_from_key(key)
|
|
30
|
+
type, id = key.to_s.split(":", 2)
|
|
31
|
+
return nil if type.blank? || id.blank?
|
|
32
|
+
|
|
33
|
+
type.safe_constantize&.find_by(id: id)
|
|
34
|
+
rescue StandardError
|
|
35
|
+
nil
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Every org this subject is currently connected in -- usually one, but a
|
|
39
|
+
# person with two tabs in two accounts is present in both and must appear
|
|
40
|
+
# in both rosters. Falls back to resolving from the subject alone for
|
|
41
|
+
# rows written before this existed, and for apps with no container.
|
|
42
|
+
def org_keys_for_subject(subject_id, timeout: nil)
|
|
43
|
+
by_subject = WhereIsWaldo::PresenceService.sessions_for_subjects([subject_id], timeout: timeout)
|
|
44
|
+
org_keys_from(sessions_for(by_subject, subject_id), subject_id)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# The same answer from sessions the caller has ALREADY loaded. A presence
|
|
48
|
+
# transition needs both the org keys and the subject's aggregate state,
|
|
49
|
+
# and both are derived from one set of session rows -- reading them twice
|
|
50
|
+
# doubles the adapter traffic on the hottest path in the library.
|
|
51
|
+
def org_keys_from(sessions, subject_id)
|
|
52
|
+
keys = sessions.filter_map do |session|
|
|
53
|
+
session.dig(:metadata, ORG_KEY) || session.dig("metadata", ORG_KEY)
|
|
54
|
+
end.uniq
|
|
55
|
+
return keys if keys.any?
|
|
56
|
+
|
|
57
|
+
[org_key(WhereIsWaldo.config.resolve_roster_org(find_subject(subject_id)))].compact
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
# THE KEY TYPES ON THE TWO SIDES ARE NOT GUARANTEED TO MATCH.
|
|
63
|
+
#
|
|
64
|
+
# The database adapter keys its result by the AR-typecast column value,
|
|
65
|
+
# typically an Integer, while a subject id arriving from a cable
|
|
66
|
+
# connection is often a String -- authenticate_proc returns whatever the
|
|
67
|
+
# host app decides, and connection params are strings. A plain fetch then
|
|
68
|
+
# misses, and the caller falls back to resolving from the subject alone:
|
|
69
|
+
# silently, and straight back to the guess this exists to replace.
|
|
70
|
+
# Comparing as strings is what stops a type difference becoming a wrong
|
|
71
|
+
# roster.
|
|
72
|
+
def sessions_for(by_subject, subject_id)
|
|
73
|
+
return by_subject[subject_id] if by_subject.key?(subject_id)
|
|
74
|
+
|
|
75
|
+
wanted = subject_id.to_s
|
|
76
|
+
_, sessions = by_subject.find { |key, _| key.to_s == wanted }
|
|
77
|
+
sessions || []
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -34,6 +34,13 @@ module WhereIsWaldo
|
|
|
34
34
|
RANK = { active: 3, idle: 2, background: 1, offline: 0 }.freeze
|
|
35
35
|
DEFAULT_PLATFORM = "web"
|
|
36
36
|
|
|
37
|
+
# The metadata key holding which roster a session belongs to. Defined on
|
|
38
|
+
# the class, not the singleton, so callers can reference
|
|
39
|
+
# WhereIsWaldo::Roster::ORG_KEY.
|
|
40
|
+
ORG_KEY = "wiw_org"
|
|
41
|
+
|
|
42
|
+
extend OrgKeys
|
|
43
|
+
|
|
37
44
|
class << self
|
|
38
45
|
# Shared stream name for an org record (nil when org is absent).
|
|
39
46
|
def stream_name(org)
|
|
@@ -45,8 +52,8 @@ module WhereIsWaldo
|
|
|
45
52
|
|
|
46
53
|
# Shared stream name for the org a subject belongs to (nil when the roster
|
|
47
54
|
# is unconfigured or the subject has no org).
|
|
48
|
-
def stream_for_subject(subject)
|
|
49
|
-
stream_name(WhereIsWaldo.config.resolve_roster_org(subject))
|
|
55
|
+
def stream_for_subject(subject, container: nil)
|
|
56
|
+
stream_name(WhereIsWaldo.config.resolve_roster_org(subject, container: container))
|
|
50
57
|
end
|
|
51
58
|
|
|
52
59
|
# Full roster state for an org: every member subject with their current
|
|
@@ -118,12 +125,23 @@ module WhereIsWaldo
|
|
|
118
125
|
def publish(subject_id, timeout: nil)
|
|
119
126
|
return false if subject_id.blank?
|
|
120
127
|
|
|
121
|
-
subject
|
|
122
|
-
|
|
123
|
-
|
|
128
|
+
# ONE read of the subject's sessions, used for both halves. The org
|
|
129
|
+
# keys and the aggregate state are derived from the same rows, and a
|
|
130
|
+
# transition is the hottest path here -- fetching twice doubled the
|
|
131
|
+
# adapter traffic for no new information.
|
|
132
|
+
by_subject = WhereIsWaldo::PresenceService.sessions_for_subjects([subject_id], timeout: timeout)
|
|
133
|
+
sessions = sessions_for(by_subject, subject_id)
|
|
124
134
|
|
|
125
|
-
|
|
126
|
-
|
|
135
|
+
keys = org_keys_from(sessions, subject_id)
|
|
136
|
+
return false if keys.empty?
|
|
137
|
+
|
|
138
|
+
subject = find_subject(subject_id)
|
|
139
|
+
state = sessions.any? ? aggregate(sessions) : offline_state
|
|
140
|
+
member = merge_member(subject, state, id: subject_id)
|
|
141
|
+
keys.each do |key|
|
|
142
|
+
stream = "where_is_waldo:roster:#{key}"
|
|
143
|
+
ActionCable.server.broadcast(stream, delta_message(member))
|
|
144
|
+
end
|
|
127
145
|
true
|
|
128
146
|
rescue StandardError => e
|
|
129
147
|
Rails.logger&.warn("[WhereIsWaldo::Roster] publish failed for #{subject_id}: #{e.class}: #{e.message}")
|
|
@@ -158,11 +176,10 @@ module WhereIsWaldo
|
|
|
158
176
|
def publish_nudge(subject_id)
|
|
159
177
|
return false if subject_id.blank?
|
|
160
178
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
return false unless stream
|
|
179
|
+
keys = org_keys_for_subject(subject_id)
|
|
180
|
+
return false if keys.empty?
|
|
164
181
|
|
|
165
|
-
ActionCable.server.broadcast(
|
|
182
|
+
keys.each { |key| ActionCable.server.broadcast("where_is_waldo:roster:#{key}", nudge_message) }
|
|
166
183
|
true
|
|
167
184
|
rescue StandardError => e
|
|
168
185
|
Rails.logger&.warn("[WhereIsWaldo::Roster] nudge failed for #{subject_id}: #{e.class}: #{e.message}")
|
|
@@ -28,7 +28,7 @@ module WhereIsWaldo
|
|
|
28
28
|
# Base contract. Channels call these; return values drive the channel's I/O.
|
|
29
29
|
class Base
|
|
30
30
|
# @return [Hash] { streams: [names], messages: [cable messages] }
|
|
31
|
-
def subscribe_plan(_subject, _session_id)
|
|
31
|
+
def subscribe_plan(_subject, _session_id, org: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
32
32
|
{ streams: [], messages: [] }
|
|
33
33
|
end
|
|
34
34
|
|
|
@@ -50,8 +50,11 @@ module WhereIsWaldo
|
|
|
50
50
|
# Instant push to a single shared per-account stream. NO visibility
|
|
51
51
|
# filtering — everyone in the account sees everyone. O(1) per transition.
|
|
52
52
|
class Broadcast < Base
|
|
53
|
-
def subscribe_plan(subject, _session_id)
|
|
54
|
-
org
|
|
53
|
+
def subscribe_plan(subject, _session_id, org: nil)
|
|
54
|
+
# org comes from the channel, which knows the connection's container.
|
|
55
|
+
# Resolving it again here would reach a different answer for a subject
|
|
56
|
+
# in several orgs, and put the viewer on a stream nobody broadcasts to.
|
|
57
|
+
org ||= config.resolve_roster_org(subject)
|
|
55
58
|
stream = Roster.stream_name(org)
|
|
56
59
|
return { streams: [], messages: [] } unless stream
|
|
57
60
|
|
|
@@ -77,10 +80,13 @@ module WhereIsWaldo
|
|
|
77
80
|
:poll
|
|
78
81
|
end
|
|
79
82
|
|
|
80
|
-
def subscribe_plan(subject, session_id)
|
|
83
|
+
def subscribe_plan(subject, session_id, org: nil)
|
|
81
84
|
current = current_members(subject)
|
|
82
85
|
write_baseline(session_id, current)
|
|
83
|
-
{
|
|
86
|
+
{
|
|
87
|
+
streams: subscribe_streams(subject, org: org),
|
|
88
|
+
messages: [Roster.snapshot_message(current.values, mode: mode)]
|
|
89
|
+
}
|
|
84
90
|
end
|
|
85
91
|
|
|
86
92
|
def poll_messages(subject, session_id)
|
|
@@ -101,7 +107,7 @@ module WhereIsWaldo
|
|
|
101
107
|
|
|
102
108
|
# Streams to subscribe on connect. :poll needs none (data rides the poll
|
|
103
109
|
# transmit); :nudge overrides this to also receive the account nudge.
|
|
104
|
-
def subscribe_streams(_subject)
|
|
110
|
+
def subscribe_streams(_subject, org: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
105
111
|
[]
|
|
106
112
|
end
|
|
107
113
|
|
|
@@ -156,8 +162,9 @@ module WhereIsWaldo
|
|
|
156
162
|
private
|
|
157
163
|
|
|
158
164
|
# Also subscribe the account stream so this client receives nudges.
|
|
159
|
-
def subscribe_streams(subject)
|
|
160
|
-
|
|
165
|
+
def subscribe_streams(subject, org: nil)
|
|
166
|
+
org ||= config.resolve_roster_org(subject)
|
|
167
|
+
stream = Roster.stream_name(org)
|
|
161
168
|
stream ? [stream] : []
|
|
162
169
|
end
|
|
163
170
|
end
|
|
@@ -168,7 +175,7 @@ module WhereIsWaldo
|
|
|
168
175
|
# its directional audience (config.roster_viewers_of). O(audience) per
|
|
169
176
|
# transition — the price of instant delivery under restricted visibility.
|
|
170
177
|
class Fanout < Base
|
|
171
|
-
def subscribe_plan(subject, _session_id)
|
|
178
|
+
def subscribe_plan(subject, _session_id, org: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
172
179
|
members = Roster.members_for(config.resolve_visible_to(subject))
|
|
173
180
|
{
|
|
174
181
|
streams: [Roster.viewer_stream(subject.id)],
|
|
@@ -83,6 +83,16 @@ module WhereIsWaldo
|
|
|
83
83
|
# roster_members_association: association used to build the default roster
|
|
84
84
|
# from the org when roster_members is unset (defaults to the pluralized
|
|
85
85
|
# subject_class, e.g. :users).
|
|
86
|
+
# roster_org_for: the container-aware form of roster_org. Receives
|
|
87
|
+
# (subject, container) where container is the one the CONNECTION is
|
|
88
|
+
# scoped to -- the account the person is actually working in, which a
|
|
89
|
+
# subject-only lambda cannot know. Prefer this whenever subjects can
|
|
90
|
+
# belong to more than one org; roster_org picks by association order,
|
|
91
|
+
# which puts a switched user on the wrong roster: invisible to the
|
|
92
|
+
# teammates they are with, visible to an org they are not.
|
|
93
|
+
# config.roster_org_for = ->(user, container) { container || user.accounts.first }
|
|
94
|
+
attr_accessor :roster_org_for
|
|
95
|
+
|
|
86
96
|
attr_accessor :roster_org, :roster_members, :roster_members_association
|
|
87
97
|
|
|
88
98
|
# roster_visible_to: given a VIEWER, return the AR scope of subjects
|
|
@@ -142,6 +152,7 @@ module WhereIsWaldo
|
|
|
142
152
|
|
|
143
153
|
# Presence roster (set per app to enable)
|
|
144
154
|
@roster_org = nil
|
|
155
|
+
@roster_org_for = nil
|
|
145
156
|
@roster_members = nil
|
|
146
157
|
@roster_members_association = nil
|
|
147
158
|
@roster_visible_to = nil
|
|
@@ -165,10 +176,17 @@ module WhereIsWaldo
|
|
|
165
176
|
end
|
|
166
177
|
|
|
167
178
|
# Resolve the org/container record for a subject (nil if unset/absent).
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
179
|
+
# @param container [Object, nil] the connection's container, when known.
|
|
180
|
+
# Only roster_org_for receives it; roster_org keeps its old arity so
|
|
181
|
+
# existing apps are untouched.
|
|
182
|
+
def resolve_roster_org(subject, container: nil)
|
|
183
|
+
return nil unless subject
|
|
184
|
+
|
|
185
|
+
if roster_org_for
|
|
186
|
+
roster_org_for.call(subject, container)
|
|
187
|
+
elsif roster_org
|
|
188
|
+
roster_org.call(subject)
|
|
189
|
+
end
|
|
172
190
|
end
|
|
173
191
|
|
|
174
192
|
# Association name used to derive the default roster from an org when
|
|
@@ -193,7 +211,7 @@ module WhereIsWaldo
|
|
|
193
211
|
|
|
194
212
|
# True when the live-presence roster feature is configured.
|
|
195
213
|
def roster_enabled?
|
|
196
|
-
!roster_org.nil?
|
|
214
|
+
!roster_org.nil? || !roster_org_for.nil?
|
|
197
215
|
end
|
|
198
216
|
|
|
199
217
|
# Resolve the AR scope of subjects a VIEWER may see. Defaults to the
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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.12
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Scott Gibson
|
|
@@ -211,6 +211,7 @@ files:
|
|
|
211
211
|
- app/services/where_is_waldo/broadcaster.rb
|
|
212
212
|
- app/services/where_is_waldo/presence_service.rb
|
|
213
213
|
- app/services/where_is_waldo/roster.rb
|
|
214
|
+
- app/services/where_is_waldo/roster/org_keys.rb
|
|
214
215
|
- app/services/where_is_waldo/roster_delivery.rb
|
|
215
216
|
- lib/generators/where_is_waldo/install/install_generator.rb
|
|
216
217
|
- lib/generators/where_is_waldo/install/templates/channel.rb.tt
|