where_is_waldo 0.1.6 → 0.1.8

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: 33656f9699b2331d830131c39118af0a671b32f3f7f173faec64bde7a522b27b
4
- data.tar.gz: 374c04bea26515d249cf4720694699e73ae43f5e9b63601a11d871f16eabe01c
3
+ metadata.gz: 14d08eaaa5203c578066b674469638145e6bd1a095260ba5874a78e7299b2ab3
4
+ data.tar.gz: 7a96f733e8dd421884e0bd798f38b5f5de128faaf1eff0fc7126c2f29b556dd0
5
5
  SHA512:
6
- metadata.gz: d16fdffe075e626fef57fceb95c20b39833c67db2c799ad9099d3873093054152bf68d989958ae432b2ae613b6cb7805a4ebf49a5838ef18b26e6256508a8ac4
7
- data.tar.gz: 46024ca78a424b725eace01ad965006691e1c4081ee77ab14c22b6aed26356d11542cd8188abbdc35e588adbc7a7ed04d156bc378e6e7e8d42f8659027a72b21
6
+ metadata.gz: e5a08ff00777508b95e44767dffc2bb24c0efa7a17a62dddb4386a83a07a222847da426000cc61ec166c177afdf8caec51b4fadd67351db775f96a09579e7d75
7
+ data.tar.gz: b6268730110e341d4f518899ccc57d1bf4f82c39cdb82ff21e6b8ea4beaa1a3739770f4d8d32995d1969174249d61815de13bb8fb1114c793727a86c723e901c
data/CHANGELOG.md CHANGED
@@ -3,6 +3,74 @@
3
3
  Notable changes to where_is_waldo. Format loosely follows
4
4
  [Keep a Changelog](https://keepachangelog.com/).
5
5
 
6
+ ## 0.1.8
7
+
8
+ ### Fixed
9
+
10
+ - **`WhereIsWaldo.subject_online?`, `.sessions_for_subject`, `.session_status`
11
+ no longer raise `uninitialized constant` when called.** These three methods
12
+ were defined with ActiveSupport's `delegate :method, to: :PresenceService`
13
+ inside a `class << self` block. `delegate` generates the method body via
14
+ `class_eval` with a **string**, and a string-eval'd body's `Module.nesting`
15
+ is just the target class — for the WhereIsWaldo module's singleton class,
16
+ that's `[#<Class:WhereIsWaldo>]`, *without* the enclosing `WhereIsWaldo`.
17
+ Constant lookup for `PresenceService` inside the generated method then
18
+ couldn't walk up to the enclosing module and failed with:
19
+
20
+ ```
21
+ uninitialized constant #<Class:WhereIsWaldo>::PresenceService (NameError)
22
+ ```
23
+
24
+ Replaced the three `delegate` calls with plain `def` methods matching the
25
+ pattern of the other class methods on the module (`.connect`, `.online`,
26
+ `.online_ids`, `.cleanup`, etc.), which inherit the correct lexical scope
27
+ and resolve `PresenceService` as `WhereIsWaldo::PresenceService`.
28
+
29
+ Waldo's own internal code (`PresenceChannel`, `Roster`) references
30
+ `PresenceService` at its fully-qualified name from correctly-scoped
31
+ modules, so this bug never affected the actual presence flow — only host
32
+ code that calls the top-level convenience wrappers. Affects every
33
+ released version up to and including `0.1.7`.
34
+
35
+ ### Tests
36
+
37
+ - Added `spec/where_is_waldo_spec.rb` exercising every top-level convenience
38
+ method at the public boundary — the only place a `class_eval`-scope
39
+ regression in the wrapper can be caught, since internal callers bypass it.
40
+
41
+ ## 0.1.7
42
+
43
+ ### Added
44
+
45
+ - **`last_activity` in roster member payloads.** Each member in a
46
+ `roster_snapshot` / `roster_delta` message now carries a `last_activity`
47
+ ISO8601 timestamp — the most recent `subject_active: true` heartbeat time
48
+ across all of the subject's live sessions (web, mobile, whichever). `nil`
49
+ when the subject has no live sessions. Lets hosts derive "seconds idle"
50
+ client-side (`Date.now() - new Date(last_activity)`) and apply their own
51
+ thresholds instead of being tied to the gem's active↔idle boundary.
52
+
53
+ Example: a picker that wants "stay green until 3 minutes without input"
54
+ no longer needs a server-side `activityTimeout` override — it reads
55
+ `member.last_activity` and thresholds against its own clock. The
56
+ server's own active/idle enum still ships in `status` for consumers who
57
+ do want it, and the roster still broadcasts only on transitions (no
58
+ extra bandwidth beyond the added field).
59
+
60
+ Payload shape:
61
+ ```json
62
+ { "id": 7, "status": "idle",
63
+ "devices": { "web": "idle", "mobile": "active" },
64
+ "last_activity": "2026-07-27T15:23:04Z",
65
+ "name": "Alan Smith" }
66
+ ```
67
+
68
+ Both adapters were already tracking `last_activity` per session — this
69
+ release plumbs it through the roster aggregation (max across sessions)
70
+ and normalizes to an ISO string in the broadcast payload, absorbing the
71
+ DB-adapter-returns-string / Redis-adapter-returns-Time mismatch at the
72
+ roster boundary.
73
+
6
74
  ## 0.1.6
7
75
 
8
76
  ### Security
data/README.md CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  Real-time presence tracking for Rails + React using ActionCable.
4
4
 
5
+ [![CI](https://github.com/byscott-io/where_is_waldo/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/byscott-io/where_is_waldo/actions/workflows/ci.yml)
6
+ [![Gem Version](https://img.shields.io/gem/v/where_is_waldo)](https://rubygems.org/gems/where_is_waldo)
7
+ [![npm version](https://img.shields.io/npm/v/@byscott-io/where-is-waldo)](https://www.npmjs.com/package/@byscott-io/where-is-waldo)
8
+ [![License](https://img.shields.io/github/license/byscott-io/where_is_waldo)](LICENSE)
9
+
5
10
  ## Features
6
11
 
7
12
  - **Presence tracking** - know who's online
@@ -233,7 +238,8 @@ See `docs/PRESENCE_ROSTER_PLAN.md` for the full mode matrix and tradeoffs.
233
238
  their live sessions (multiple browser tabs, mobile, etc.):
234
239
 
235
240
  ```
236
- { id: 7, status: "active", devices: { web: "idle", mobile: "active" } }
241
+ { id: 7, status: "active", devices: { web: "idle", mobile: "active" },
242
+ last_activity: "2026-07-27T15:23:04Z" }
237
243
  ```
238
244
 
239
245
  - `status` — highest activity across devices (the "active anywhere?" answer):
@@ -243,6 +249,11 @@ their live sessions (multiple browser tabs, mobile, etc.):
243
249
  - `offline` — no live sessions
244
250
  - `devices[platform]` — that platform's own status (answers "active on
245
251
  **mobile**?" vs. "active at all?").
252
+ - `last_activity` — ISO8601 timestamp of the most recent input across **all**
253
+ the subject's live sessions (`null` when offline). The roster only
254
+ broadcasts on `status` transitions, so subtract against your own clock to
255
+ derive seconds-idle and apply client-side thresholds without a server
256
+ `timeout` change: `(Date.now() - new Date(m.last_activity)) / 1000`.
246
257
 
247
258
  #### Configure
248
259
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.6
1
+ 0.1.8
@@ -8,12 +8,18 @@ module WhereIsWaldo
8
8
  #
9
9
  # A member's presence is reported per device AND as an overall roll-up:
10
10
  #
11
- # { id: 7, status: "active", devices: { "web" => "active", "mobile" => "idle" } }
11
+ # { id: 7, status: "active",
12
+ # devices: { "web" => "active", "mobile" => "idle" },
13
+ # last_activity: "2026-07-27T15:23:04Z" }
12
14
  #
13
15
  # `devices[platform]` is that platform's own aggregate status (several browser
14
16
  # tabs roll up into one "web" status); `status` is the highest level across
15
17
  # all platforms — the "is the user active anywhere?" answer, while
16
- # `devices["mobile"]` answers "is the user active on mobile?". Each status is:
18
+ # `devices["mobile"]` answers "is the user active on mobile?". `last_activity`
19
+ # is the most recent input timestamp across all of the subject's live sessions
20
+ # (nil when offline), letting the client subtract from its own clock to derive
21
+ # seconds-idle and apply its own thresholds without new broadcasts. Each
22
+ # status is:
17
23
  #
18
24
  # active - a live session on that device is tab-visible AND working
19
25
  # idle - a live session is tab-visible but not actively using
@@ -182,15 +188,21 @@ module WhereIsWaldo
182
188
  # Reduce a subject's live sessions to per-device statuses plus an overall
183
189
  # roll-up. Sessions are grouped by platform (so several browser tabs form
184
190
  # one "web" status); the overall status is the highest across platforms.
191
+ # `last_activity` is the max across all sessions (any platform), so it
192
+ # answers "when did any of this subject's tabs last see input?".
185
193
  # @param sessions [Array<Hash>] presence hashes from the adapter
186
- # @return [Hash] { status: "active", devices: { "web" => "active", ... } }
194
+ # @return [Hash] { status:, devices:, last_activity: }
187
195
  def aggregate(sessions)
188
196
  devices = sessions.group_by { |s| platform(s) }
189
197
  .transform_values { |sess| platform_level(sess) }
190
198
  overall = devices.values.max_by { |level| RANK[level] } || :offline
199
+ # DB adapter returns last_activity as ISO string, Redis as Time; both sort
200
+ # correctly among themselves. Normalize to ISO string in the payload.
201
+ last = sessions.filter_map { |s| s[:last_activity] }.max
191
202
  {
192
203
  status: overall.to_s,
193
- devices: devices.transform_values(&:to_s)
204
+ devices: devices.transform_values(&:to_s),
205
+ last_activity: last.respond_to?(:iso8601) ? last.iso8601 : last
194
206
  }
195
207
  end
196
208
 
@@ -229,12 +241,13 @@ module WhereIsWaldo
229
241
  data.merge(
230
242
  id: id || record&.id,
231
243
  status: state[:status],
232
- devices: state[:devices]
244
+ devices: state[:devices],
245
+ last_activity: state[:last_activity]
233
246
  )
234
247
  end
235
248
 
236
249
  def offline_state
237
- { status: "offline", devices: {} }
250
+ { status: "offline", devices: {}, last_activity: nil }
238
251
  end
239
252
 
240
253
  def find_subject(subject_id)
@@ -55,11 +55,25 @@ module WhereIsWaldo
55
55
  PresenceService.all_online_ids(timeout: timeout)
56
56
  end
57
57
 
58
- delegate :subject_online?, to: :PresenceService
58
+ # NOTE: don't use ActiveSupport's `delegate` here. `delegate` generates
59
+ # the method via `class_eval` with a STRING, and a string-eval'd body's
60
+ # `Module.nesting` is just the target class — for a `class << self` inside
61
+ # `module WhereIsWaldo`, that's `[#<Class:WhereIsWaldo>]`, WITHOUT
62
+ # `WhereIsWaldo` itself. Constant lookup for `PresenceService` inside the
63
+ # generated method then can't walk up to the enclosing module and raises
64
+ # `uninitialized constant #<Class:WhereIsWaldo>::PresenceService`. A plain
65
+ # `def` inherits the enclosing lexical nesting and resolves correctly.
66
+ def subject_online?(...)
67
+ PresenceService.subject_online?(...)
68
+ end
59
69
 
60
- delegate :sessions_for_subject, to: :PresenceService
70
+ def sessions_for_subject(...)
71
+ PresenceService.sessions_for_subject(...)
72
+ end
61
73
 
62
- delegate :session_status, to: :PresenceService
74
+ def session_status(...)
75
+ PresenceService.session_status(...)
76
+ end
63
77
 
64
78
  def cleanup(timeout: nil)
65
79
  PresenceService.cleanup(timeout: timeout)
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.6
4
+ version: 0.1.8
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-25 00:00:00.000000000 Z
11
+ date: 2026-07-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails