@byscott-io/where-is-waldo 0.1.0 → 0.1.2

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.
package/dist/index.js CHANGED
@@ -3,336 +3,171 @@
3
3
  var actioncable = require('@rails/actioncable');
4
4
  var React = require('react');
5
5
 
6
- function _arrayLikeToArray(r, a) {
7
- (null == a || a > r.length) && (a = r.length);
8
- for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e];
9
- return n;
10
- }
11
- function _arrayWithHoles(r) {
12
- if (Array.isArray(r)) return r;
13
- }
14
- function _defineProperty(e, r, t) {
15
- return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
16
- value: t,
17
- enumerable: true,
18
- configurable: true,
19
- writable: true
20
- }) : e[r] = t, e;
21
- }
22
- function _iterableToArrayLimit(r, l) {
23
- var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"];
24
- if (null != t) {
25
- var e,
26
- n,
27
- i,
28
- u,
29
- a = [],
30
- f = true,
31
- o = false;
32
- try {
33
- if (i = (t = t.call(r)).next, 0 === l) ; else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0);
34
- } catch (r) {
35
- o = true, n = r;
36
- } finally {
37
- try {
38
- if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return;
39
- } finally {
40
- if (o) throw n;
41
- }
42
- }
43
- return a;
44
- }
45
- }
46
- function _nonIterableRest() {
47
- throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
48
- }
49
- function ownKeys(e, r) {
50
- var t = Object.keys(e);
51
- if (Object.getOwnPropertySymbols) {
52
- var o = Object.getOwnPropertySymbols(e);
53
- r && (o = o.filter(function (r) {
54
- return Object.getOwnPropertyDescriptor(e, r).enumerable;
55
- })), t.push.apply(t, o);
56
- }
57
- return t;
58
- }
59
- function _objectSpread2(e) {
60
- for (var r = 1; r < arguments.length; r++) {
61
- var t = null != arguments[r] ? arguments[r] : {};
62
- r % 2 ? ownKeys(Object(t), true).forEach(function (r) {
63
- _defineProperty(e, r, t[r]);
64
- }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) {
65
- Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r));
66
- });
67
- }
68
- return e;
69
- }
70
- function _slicedToArray(r, e) {
71
- return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest();
72
- }
73
- function _toPrimitive(t, r) {
74
- if ("object" != typeof t || !t) return t;
75
- var e = t[Symbol.toPrimitive];
76
- if (void 0 !== e) {
77
- var i = e.call(t, r);
78
- if ("object" != typeof i) return i;
79
- throw new TypeError("@@toPrimitive must return a primitive value.");
80
- }
81
- return ("string" === r ? String : Number)(t);
82
- }
83
- function _toPropertyKey(t) {
84
- var i = _toPrimitive(t, "string");
85
- return "symbol" == typeof i ? i : i + "";
86
- }
87
- function _unsupportedIterableToArray(r, a) {
88
- if (r) {
89
- if ("string" == typeof r) return _arrayLikeToArray(r, a);
90
- var t = {}.toString.call(r).slice(8, -1);
91
- return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0;
92
- }
93
- }
94
-
95
- var consumer = null;
96
- var config = {
97
- url: '/cable',
6
+ let consumer = null;
7
+ let config = {
8
+ url: "/cable",
98
9
  getToken: null
99
10
  };
100
-
101
- // Subscribe-by-event-type registry: message type -> Set of listener fns.
102
- // Components subscribe to a raw event type (see subscribeToEvent /
103
- // useWaldoEvent) and decide for themselves whether they care about a given
104
- // payload. handleMessage dispatches every incoming message to the listeners
105
- // registered for its type.
106
- var listeners = new Map();
107
-
108
- /**
109
- * Subscribe to a raw cable event type. Returns an unsubscribe function.
110
- * Multiple subscribers per type are supported; each receives (data, message).
111
- * @param {string} type - The message type to listen for
112
- * @param {Function} callback - Called with (data, message) on each matching event
113
- * @returns {Function} unsubscribe
114
- */
11
+ const listeners = /* @__PURE__ */ new Map();
115
12
  function subscribeToEvent(type, callback) {
116
- if (!type || typeof callback !== 'function') return function () {};
117
- var set = listeners.get(type);
13
+ if (!type || typeof callback !== "function") return () => {
14
+ };
15
+ let set = listeners.get(type);
118
16
  if (!set) {
119
- set = new Set();
17
+ set = /* @__PURE__ */ new Set();
120
18
  listeners.set(type, set);
121
19
  }
122
20
  set.add(callback);
123
21
  return function unsubscribe() {
124
- var current = listeners.get(type);
22
+ const current = listeners.get(type);
125
23
  if (!current) return;
126
- current["delete"](callback);
127
- if (current.size === 0) listeners["delete"](type);
24
+ current.delete(callback);
25
+ if (current.size === 0) listeners.delete(type);
128
26
  };
129
27
  }
130
-
131
- /**
132
- * Configure the ActionCable connection
133
- * @param {Object} options
134
- * @param {string} options.url - WebSocket URL (default: '/cable')
135
- * @param {Function} options.getToken - Function that returns auth token
136
- */
137
- function configureCable() {
138
- var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
139
- config = _objectSpread2(_objectSpread2({}, config), options);
140
- // Reset consumer when config changes
28
+ function configureCable(options = {}) {
29
+ config = { ...config, ...options };
141
30
  if (consumer) {
142
31
  consumer.disconnect();
143
32
  consumer = null;
144
33
  }
145
34
  }
146
-
147
- /**
148
- * Get or create the ActionCable consumer
149
- * @returns {Consumer}
150
- */
151
35
  function getConsumer() {
152
36
  if (!consumer) {
153
- var url = config.url;
37
+ let url = config.url;
154
38
  if (config.getToken) {
155
- var token = config.getToken();
39
+ const token = config.getToken();
156
40
  if (token) {
157
- var separator = url.includes('?') ? '&' : '?';
158
- url = "".concat(url).concat(separator, "token=").concat(encodeURIComponent(token));
41
+ const separator = url.includes("?") ? "&" : "?";
42
+ url = `${url}${separator}token=${encodeURIComponent(token)}`;
159
43
  }
160
44
  }
161
45
  consumer = actioncable.createConsumer(url);
162
46
  }
163
47
  return consumer;
164
48
  }
165
-
166
- /**
167
- * Disconnect and reset the consumer
168
- */
169
49
  function disconnectCable() {
170
50
  if (consumer) {
171
51
  consumer.disconnect();
172
52
  consumer = null;
173
53
  }
174
54
  }
175
-
176
- /**
177
- * Get current cable configuration
178
- * @returns {Object}
179
- */
180
55
  function getCableConfig() {
181
- return _objectSpread2({}, config);
56
+ return { ...config };
182
57
  }
183
-
184
- /**
185
- * Handle an incoming message by dispatching to every listener subscribed to
186
- * its type.
187
- * @param {Object} message - The message object with type and data
188
- * @returns {boolean} - Whether at least one listener was invoked
189
- */
190
58
  function handleMessage(message) {
191
- var type = message.type,
192
- data = message.data;
193
- var set = listeners.get(type);
59
+ const { type, data } = message;
60
+ const set = listeners.get(type);
194
61
  if (!set || set.size === 0) return false;
195
-
196
- // Iterate a snapshot so a listener that unsubscribes during dispatch is safe.
197
- Array.from(set).forEach(function (cb) {
62
+ Array.from(set).forEach((cb) => {
198
63
  try {
199
64
  cb(data, message);
200
65
  } catch (err) {
201
- // One bad listener must not block the others.
202
- // eslint-disable-next-line no-console
203
- console.error("[where-is-waldo] listener for \"".concat(type, "\" threw:"), err);
66
+ console.error(`[where-is-waldo] listener for "${type}" threw:`, err);
204
67
  }
205
68
  });
206
69
  return true;
207
70
  }
208
71
 
209
- var DEFAULTS = {
210
- channelName: 'WhereIsWaldo::PresenceChannel',
211
- heartbeatInterval: 30000,
72
+ const DEFAULTS = {
73
+ channelName: "WhereIsWaldo::PresenceChannel",
74
+ heartbeatInterval: 3e4,
212
75
  // ms
213
- activityTimeout: 30000,
76
+ activityTimeout: 3e4,
214
77
  // ms of no activity -> subject_active=false
215
78
  metadata: {},
216
79
  debug: false
217
80
  };
218
- function createPresenceReporter() {
219
- var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
220
- var config = _objectSpread2(_objectSpread2({}, DEFAULTS), options);
221
- var now = config.now || function () {
222
- return Date.now();
81
+ function createPresenceReporter(options = {}) {
82
+ const config = { ...DEFAULTS, ...options };
83
+ const now = config.now || (() => Date.now());
84
+ const resolveConsumer = config.getConsumer || getConsumer;
85
+ const resolveCableConfig = config.getCableConfig || getCableConfig;
86
+ const dispatch = config.handleMessage || handleMessage;
87
+ const log = (...args) => {
88
+ if (config.debug) console.log("[PresenceReporter]", ...args);
223
89
  };
224
- var resolveConsumer = config.getConsumer || getConsumer;
225
- var resolveCableConfig = config.getCableConfig || getCableConfig;
226
- var dispatch = config.handleMessage || handleMessage;
227
- var log = function log() {
228
- var _console;
229
- for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
230
- args[_key] = arguments[_key];
231
- }
232
- // eslint-disable-next-line no-console
233
- if (config.debug) (_console = console).log.apply(_console, ['[PresenceReporter]'].concat(args));
234
- };
235
- var subscription = null;
236
- var heartbeatTimer = null;
237
- var activityTimer = null;
238
- var lastActivityAt = 0;
239
-
240
- // visible = tab/app is foreground & visible; active = recent user activity.
241
- var state = {
242
- connected: false,
243
- visible: true,
244
- active: true,
245
- sessionId: null
90
+ let subscription = null;
91
+ let heartbeatTimer = null;
92
+ let activityTimer = null;
93
+ let lastActivityAt = 0;
94
+ const state = { connected: false, visible: true, active: true, sessionId: null };
95
+ const emitChange = () => {
96
+ if (config.onChange) config.onChange({ ...state });
246
97
  };
247
- var emitChange = function emitChange() {
248
- if (config.onChange) config.onChange(_objectSpread2({}, state));
249
- };
250
- var buildPayload = function buildPayload() {
251
- var _overrides$tab_visibl, _overrides$subject_ac;
252
- var overrides = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
253
- var payload = {
254
- tab_visible: (_overrides$tab_visibl = overrides.tab_visible) !== null && _overrides$tab_visibl !== void 0 ? _overrides$tab_visibl : state.visible,
255
- subject_active: (_overrides$subject_ac = overrides.subject_active) !== null && _overrides$subject_ac !== void 0 ? _overrides$subject_ac : state.active,
98
+ const buildPayload = (overrides = {}) => {
99
+ const payload = {
100
+ tab_visible: overrides.tab_visible ?? state.visible,
101
+ subject_active: overrides.subject_active ?? state.active,
256
102
  metadata: config.metadata || {}
257
103
  };
258
104
  if (lastActivityAt) payload.last_activity_at = lastActivityAt;
259
105
  return payload;
260
106
  };
261
- var sendHeartbeat = function sendHeartbeat() {
262
- var overrides = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
107
+ const sendHeartbeat = (overrides = {}) => {
263
108
  if (!subscription) return;
264
- log('heartbeat', overrides);
265
- subscription.perform('heartbeat', buildPayload(overrides));
109
+ log("heartbeat", overrides);
110
+ subscription.perform("heartbeat", buildPayload(overrides));
266
111
  };
267
-
268
- // (Re)arm the inactivity timer: after activityTimeout with no activity, flip
269
- // to inactive and send an immediate heartbeat announcing it.
270
- var scheduleInactivity = function scheduleInactivity() {
112
+ const scheduleInactivity = () => {
271
113
  if (activityTimer) clearTimeout(activityTimer);
272
- activityTimer = setTimeout(function () {
114
+ activityTimer = setTimeout(() => {
273
115
  if (!state.active) return;
274
116
  state.active = false;
275
117
  emitChange();
276
- sendHeartbeat({
277
- subject_active: false
278
- });
118
+ sendHeartbeat({ subject_active: false });
279
119
  }, config.activityTimeout);
280
120
  };
281
- var reportActivity = function reportActivity() {
121
+ const reportActivity = () => {
282
122
  lastActivityAt = now();
283
123
  if (!state.active) {
284
124
  state.active = true;
285
125
  emitChange();
286
- sendHeartbeat({
287
- subject_active: true
288
- }); // immediate: inactive -> active
126
+ sendHeartbeat({ subject_active: true });
289
127
  }
290
128
  scheduleInactivity();
291
129
  };
292
- var setVisible = function setVisible(visible) {
130
+ const setVisible = (visible) => {
293
131
  if (visible === state.visible) return;
294
132
  state.visible = visible;
295
133
  emitChange();
296
- sendHeartbeat(); // immediate: visibility change
134
+ sendHeartbeat();
297
135
  if (visible) reportActivity();
298
136
  };
299
- var startHeartbeatLoop = function startHeartbeatLoop() {
137
+ const startHeartbeatLoop = () => {
300
138
  if (heartbeatTimer) clearInterval(heartbeatTimer);
301
- heartbeatTimer = setInterval(function () {
302
- return sendHeartbeat();
303
- }, config.heartbeatInterval);
139
+ heartbeatTimer = setInterval(() => sendHeartbeat(), config.heartbeatInterval);
304
140
  };
305
- var start = function start() {
141
+ const start = () => {
306
142
  if (subscription) return subscription;
307
- subscription = resolveConsumer().subscriptions.create({
308
- channel: config.channelName,
309
- metadata: config.metadata || {}
310
- }, {
311
- connected: function connected() {
312
- state.connected = true;
313
- var cable = resolveCableConfig();
314
- if (cable && cable.sessionId) state.sessionId = cable.sessionId;
315
- emitChange();
316
- if (config.onConnected) config.onConnected();
317
- startHeartbeatLoop();
318
- },
319
- disconnected: function disconnected() {
320
- state.connected = false;
321
- emitChange();
322
- if (config.onDisconnected) config.onDisconnected();
323
- },
324
- received: function received(message) {
325
- // Honor targeted (per-session) messages, then route to the event bus.
326
- if (message && message._target_session && message._target_session !== state.sessionId) {
327
- return;
143
+ subscription = resolveConsumer().subscriptions.create(
144
+ { channel: config.channelName, metadata: config.metadata || {} },
145
+ {
146
+ connected() {
147
+ state.connected = true;
148
+ const cable = resolveCableConfig();
149
+ if (cable && cable.sessionId) state.sessionId = cable.sessionId;
150
+ emitChange();
151
+ if (config.onConnected) config.onConnected();
152
+ startHeartbeatLoop();
153
+ },
154
+ disconnected() {
155
+ state.connected = false;
156
+ emitChange();
157
+ if (config.onDisconnected) config.onDisconnected();
158
+ },
159
+ received(message) {
160
+ if (message && message._target_session && message._target_session !== state.sessionId) {
161
+ return;
162
+ }
163
+ dispatch(message);
328
164
  }
329
- dispatch(message);
330
165
  }
331
- });
166
+ );
332
167
  scheduleInactivity();
333
168
  return subscription;
334
169
  };
335
- var stop = function stop() {
170
+ const stop = () => {
336
171
  if (heartbeatTimer) {
337
172
  clearInterval(heartbeatTimer);
338
173
  heartbeatTimer = null;
@@ -348,76 +183,36 @@ function createPresenceReporter() {
348
183
  state.connected = false;
349
184
  };
350
185
  return {
351
- start: start,
352
- stop: stop,
353
- setVisible: setVisible,
354
- reportActivity: reportActivity,
355
- sendHeartbeat: sendHeartbeat,
356
- getState: function getState() {
357
- return _objectSpread2({}, state);
358
- }
186
+ start,
187
+ stop,
188
+ setVisible,
189
+ reportActivity,
190
+ sendHeartbeat,
191
+ getState: () => ({ ...state })
359
192
  };
360
193
  }
361
194
 
362
- var DEFAULT_OPTIONS = {
363
- channelName: 'WhereIsWaldo::PresenceChannel',
364
- heartbeatInterval: 30000,
365
- activityTimeout: 30000,
195
+ const DEFAULT_OPTIONS = {
196
+ channelName: "WhereIsWaldo::PresenceChannel",
197
+ heartbeatInterval: 3e4,
198
+ activityTimeout: 3e4,
366
199
  trackActivity: true,
367
200
  trackVisibility: true,
368
201
  debug: false
369
202
  };
370
- var initialVisible = function initialVisible() {
371
- return typeof document === 'undefined' ? true : document.visibilityState !== 'hidden';
372
- };
373
- var initialFocused = function initialFocused() {
374
- return typeof document === 'undefined' ? true : document.hasFocus();
375
- };
376
-
377
- /**
378
- * usePresence - browser presence tracking. This is now a thin DOM-sensor
379
- * wrapper around the framework-agnostic core (../core/presenceReporter): it
380
- * binds `document`/`window` events to the reporter's `setVisible` /
381
- * `reportActivity` inputs and mirrors the reporter's state into React state.
382
- * The heartbeat cadence, activity/visibility state machine, and cable
383
- * subscription all live in the core, which a React Native reporter reuses.
384
- *
385
- * @param {Object} options - see DEFAULT_OPTIONS
386
- * @returns {{ connected, sessionId, tabVisible, windowFocused, subjectActive, sendHeartbeat }}
387
- */
388
- function usePresence() {
389
- var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
390
- var config = _objectSpread2(_objectSpread2({}, DEFAULT_OPTIONS), options);
391
- var _useState = React.useState(false),
392
- _useState2 = _slicedToArray(_useState, 2),
393
- connected = _useState2[0],
394
- setConnected = _useState2[1];
395
- var _useState3 = React.useState(null),
396
- _useState4 = _slicedToArray(_useState3, 2),
397
- sessionId = _useState4[0],
398
- setSessionId = _useState4[1];
399
- var _useState5 = React.useState(true),
400
- _useState6 = _slicedToArray(_useState5, 2),
401
- subjectActive = _useState6[0],
402
- setSubjectActive = _useState6[1];
403
- var _useState7 = React.useState(initialVisible),
404
- _useState8 = _slicedToArray(_useState7, 2),
405
- tabVisible = _useState8[0],
406
- setTabVisible = _useState8[1];
407
- var _useState9 = React.useState(initialFocused),
408
- _useState0 = _slicedToArray(_useState9, 2),
409
- windowFocused = _useState0[0],
410
- setWindowFocused = _useState0[1];
411
- var reporterRef = React.useRef(null);
412
- // Latest sensor readings, read by the DOM handlers to compute foreground.
413
- var sensorRef = React.useRef({
414
- tab: initialVisible(),
415
- focus: initialFocused()
416
- });
417
-
418
- // Create + start the reporter once (re-created only if the channel changes).
419
- React.useEffect(function () {
420
- var reporter = createPresenceReporter({
203
+ const initialVisible = () => typeof document === "undefined" ? true : document.visibilityState !== "hidden";
204
+ const initialFocused = () => typeof document === "undefined" ? true : document.hasFocus();
205
+ function usePresence(options = {}) {
206
+ const config = { ...DEFAULT_OPTIONS, ...options };
207
+ const [connected, setConnected] = React.useState(false);
208
+ const [sessionId, setSessionId] = React.useState(null);
209
+ const [subjectActive, setSubjectActive] = React.useState(true);
210
+ const [tabVisible, setTabVisible] = React.useState(initialVisible);
211
+ const [windowFocused, setWindowFocused] = React.useState(initialFocused);
212
+ const reporterRef = React.useRef(null);
213
+ const sensorRef = React.useRef({ tab: initialVisible(), focus: initialFocused() });
214
+ React.useEffect(() => {
215
+ const reporter = createPresenceReporter({
421
216
  channelName: config.channelName,
422
217
  heartbeatInterval: config.heartbeatInterval,
423
218
  activityTimeout: config.activityTimeout,
@@ -425,7 +220,7 @@ function usePresence() {
425
220
  debug: config.debug,
426
221
  onConnected: config.onConnected,
427
222
  onDisconnected: config.onDisconnected,
428
- onChange: function onChange(state) {
223
+ onChange: (state) => {
429
224
  setConnected(state.connected);
430
225
  setSubjectActive(state.active);
431
226
  if (state.sessionId) setSessionId(state.sessionId);
@@ -433,437 +228,236 @@ function usePresence() {
433
228
  });
434
229
  reporterRef.current = reporter;
435
230
  reporter.start();
436
- // Seed foreground state from the current sensors.
437
231
  reporter.setVisible(sensorRef.current.tab && sensorRef.current.focus);
438
- return function () {
232
+ return () => {
439
233
  reporter.stop();
440
234
  reporterRef.current = null;
441
235
  };
442
- }, [config.channelName]); // eslint-disable-line react-hooks/exhaustive-deps
443
-
444
- // Activity sensors -> reporter.reportActivity()
445
- React.useEffect(function () {
446
- if (!config.trackActivity) return undefined;
447
- var onActivity = function onActivity() {
448
- var _reporterRef$current;
449
- return (_reporterRef$current = reporterRef.current) === null || _reporterRef$current === void 0 ? void 0 : _reporterRef$current.reportActivity();
450
- };
451
- var events = ['mousemove', 'keydown', 'scroll', 'touchstart', 'click'];
452
- events.forEach(function (event) {
453
- return window.addEventListener(event, onActivity, {
454
- passive: true
455
- });
456
- });
457
- return function () {
458
- return events.forEach(function (event) {
459
- return window.removeEventListener(event, onActivity);
460
- });
461
- };
236
+ }, [config.channelName]);
237
+ React.useEffect(() => {
238
+ if (!config.trackActivity) return void 0;
239
+ const onActivity = () => reporterRef.current?.reportActivity();
240
+ const events = ["mousemove", "keydown", "scroll", "touchstart", "click"];
241
+ events.forEach((event) => window.addEventListener(event, onActivity, { passive: true }));
242
+ return () => events.forEach((event) => window.removeEventListener(event, onActivity));
462
243
  }, [config.trackActivity]);
463
-
464
- // Visibility + focus sensors -> reporter.setVisible(foreground)
465
- React.useEffect(function () {
466
- if (!config.trackVisibility) return undefined;
467
- var pushForeground = function pushForeground() {
468
- var _reporterRef$current2;
469
- (_reporterRef$current2 = reporterRef.current) === null || _reporterRef$current2 === void 0 || _reporterRef$current2.setVisible(sensorRef.current.tab && sensorRef.current.focus);
244
+ React.useEffect(() => {
245
+ if (!config.trackVisibility) return void 0;
246
+ const pushForeground = () => {
247
+ reporterRef.current?.setVisible(sensorRef.current.tab && sensorRef.current.focus);
470
248
  };
471
- var onVisibility = function onVisibility() {
472
- var visible = document.visibilityState !== 'hidden';
249
+ const onVisibility = () => {
250
+ const visible = document.visibilityState !== "hidden";
473
251
  sensorRef.current.tab = visible;
474
252
  setTabVisible(visible);
475
253
  pushForeground();
476
254
  };
477
- var onFocus = function onFocus() {
255
+ const onFocus = () => {
478
256
  sensorRef.current.focus = true;
479
257
  setWindowFocused(true);
480
258
  pushForeground();
481
259
  };
482
- var onBlur = function onBlur() {
260
+ const onBlur = () => {
483
261
  sensorRef.current.focus = false;
484
262
  setWindowFocused(false);
485
263
  pushForeground();
486
264
  };
487
- document.addEventListener('visibilitychange', onVisibility);
488
- window.addEventListener('focus', onFocus);
489
- window.addEventListener('blur', onBlur);
490
- return function () {
491
- document.removeEventListener('visibilitychange', onVisibility);
492
- window.removeEventListener('focus', onFocus);
493
- window.removeEventListener('blur', onBlur);
265
+ document.addEventListener("visibilitychange", onVisibility);
266
+ window.addEventListener("focus", onFocus);
267
+ window.addEventListener("blur", onBlur);
268
+ return () => {
269
+ document.removeEventListener("visibilitychange", onVisibility);
270
+ window.removeEventListener("focus", onFocus);
271
+ window.removeEventListener("blur", onBlur);
494
272
  };
495
273
  }, [config.trackVisibility]);
496
- var sendHeartbeat = React.useCallback(function (overrides) {
497
- var _reporterRef$current3;
498
- (_reporterRef$current3 = reporterRef.current) === null || _reporterRef$current3 === void 0 || _reporterRef$current3.sendHeartbeat(overrides);
274
+ const sendHeartbeat = React.useCallback((overrides) => {
275
+ reporterRef.current?.sendHeartbeat(overrides);
499
276
  }, []);
500
- return {
501
- connected: connected,
502
- sessionId: sessionId,
503
- tabVisible: tabVisible,
504
- windowFocused: windowFocused,
505
- subjectActive: subjectActive,
506
- sendHeartbeat: sendHeartbeat
507
- };
277
+ return { connected, sessionId, tabVisible, windowFocused, subjectActive, sendHeartbeat };
508
278
  }
509
279
 
510
- var PresenceContext = /*#__PURE__*/React.createContext(null);
511
- var DEFAULT_CONFIG = {
512
- channelName: 'WhereIsWaldo::PresenceChannel',
513
- heartbeatInterval: 30000,
514
- activityTimeout: 30000,
280
+ const PresenceContext = React.createContext(null);
281
+ const DEFAULT_CONFIG = {
282
+ channelName: "WhereIsWaldo::PresenceChannel",
283
+ heartbeatInterval: 3e4,
284
+ activityTimeout: 3e4,
515
285
  trackActivity: true,
516
286
  trackVisibility: true,
517
287
  debug: false
518
288
  };
519
-
520
- /**
521
- * Get merged config from defaults, cable config, and overrides
522
- */
523
289
  function getMergedConfig(configOverrides) {
524
- var cableConfig = getCableConfig();
525
- var presenceConfig = cableConfig.presence || {};
526
- return _objectSpread2(_objectSpread2(_objectSpread2({}, DEFAULT_CONFIG), presenceConfig), configOverrides);
290
+ const cableConfig = getCableConfig();
291
+ const presenceConfig = cableConfig.presence || {};
292
+ return { ...DEFAULT_CONFIG, ...presenceConfig, ...configOverrides };
527
293
  }
528
-
529
- /**
530
- * PresenceProvider - Provides real-time presence tracking and message handling
531
- *
532
- * @param {Object} props
533
- * @param {React.ReactNode} props.children
534
- * @param {Object} props.metadata - Optional metadata to attach to presence
535
- * @param {Object} props.config - Optional configuration overrides
536
- * @param {Function} props.onConnected - Callback when connected
537
- * @param {Function} props.onDisconnected - Callback when disconnected
538
- */
539
- function PresenceProvider(_ref) {
540
- var children = _ref.children,
541
- _ref$metadata = _ref.metadata,
542
- metadata = _ref$metadata === void 0 ? {} : _ref$metadata,
543
- _ref$config = _ref.config,
544
- configOverrides = _ref$config === void 0 ? {} : _ref$config,
545
- onConnected = _ref.onConnected,
546
- onDisconnected = _ref.onDisconnected;
547
- var config = React.useMemo(function () {
548
- return getMergedConfig(configOverrides);
549
- }, [configOverrides]);
550
-
551
- // Use the shared usePresence hook for all presence tracking
552
- var presence = usePresence(_objectSpread2(_objectSpread2({}, config), {}, {
553
- metadata: metadata,
554
- onConnected: onConnected,
555
- onDisconnected: onDisconnected
556
- }));
557
- return /*#__PURE__*/React.createElement(PresenceContext.Provider, {
558
- value: presence
559
- }, children);
294
+ function PresenceProvider({
295
+ children,
296
+ metadata = {},
297
+ config: configOverrides = {},
298
+ onConnected,
299
+ onDisconnected
300
+ }) {
301
+ const config = React.useMemo(() => getMergedConfig(configOverrides), [configOverrides]);
302
+ const presence = usePresence({
303
+ ...config,
304
+ metadata,
305
+ onConnected,
306
+ onDisconnected
307
+ });
308
+ return /* @__PURE__ */ React.createElement(PresenceContext.Provider, { value: presence }, children);
560
309
  }
561
-
562
- /**
563
- * Hook to access presence context
564
- * @returns {Object} Presence context value
565
- */
566
310
  function usePresenceContext() {
567
- var context = React.useContext(PresenceContext);
311
+ const context = React.useContext(PresenceContext);
568
312
  if (!context) {
569
- throw new Error('usePresenceContext must be used within a PresenceProvider');
313
+ throw new Error("usePresenceContext must be used within a PresenceProvider");
570
314
  }
571
315
  return context;
572
316
  }
573
317
 
574
- /**
575
- * useWaldoEvent - subscribe a component to one or more raw real-time event types.
576
- *
577
- * The component subscribes to a message `type` (or an array of types) and
578
- * receives the payload on every matching cable event; it decides for itself
579
- * whether it cares about a given record (e.g. `if (data.project_id === myId)
580
- * ...`). The subscription is automatically torn down when the component
581
- * unmounts ("out of view"), exactly like a useEffect cleanup.
582
- *
583
- * The handler is held in a ref so it always sees the latest props/state without
584
- * re-subscribing on every render — callers do NOT need to memoize it. The
585
- * subscription is re-created only when the type(s) / `enabled` change.
586
- *
587
- * This is a pure subscription primitive — it has no timing/debounce concern.
588
- * To coalesce a burst of events, compose with a debounce at the call site, e.g.
589
- * corebyscott's `useDebouncedCallback`:
590
- *
591
- * const refetch = useDebouncedCallback(loadAll, 250);
592
- * useWaldoEvent(['issue_update', 'project_update'], refetch);
593
- *
594
- * @param {string|string[]} type - Cable message type(s), e.g. 'issue_update'
595
- * @param {Function} handler - Called with (data, message) on each event
596
- * @param {Object} [options]
597
- * @param {boolean} [options.enabled=true] - When false, no subscription is made
598
- *
599
- * @example
600
- * useWaldoEvent('issue_update', (data) => {
601
- * if (data.id === issueId) setIssue((prev) => ({ ...prev, ...data }));
602
- * });
603
- */
604
- function useWaldoEvent(type, handler) {
605
- var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
606
- var _options$enabled = options.enabled,
607
- enabled = _options$enabled === void 0 ? true : _options$enabled;
608
- var handlerRef = React.useRef(handler);
609
-
610
- // Keep the ref pointing at the latest handler every render.
611
- React.useEffect(function () {
318
+ function useWaldoEvent(type, handler, options = {}) {
319
+ const { enabled = true } = options;
320
+ const handlerRef = React.useRef(handler);
321
+ React.useEffect(() => {
612
322
  handlerRef.current = handler;
613
323
  });
614
-
615
- // Stable, order-insensitive key so an inline array literal doesn't
616
- // re-subscribe on every render.
617
- var types = Array.isArray(type) ? type : [type];
618
- var typesKey = types.filter(Boolean).join(' ');
619
- React.useEffect(function () {
620
- if (!enabled || !typesKey) return undefined;
621
- var typeList = typesKey.split(' ');
622
- var onEvent = function onEvent(data, message) {
324
+ const types = Array.isArray(type) ? type : [type];
325
+ const typesKey = types.filter(Boolean).join(" ");
326
+ React.useEffect(() => {
327
+ if (!enabled || !typesKey) return void 0;
328
+ const typeList = typesKey.split(" ");
329
+ const onEvent = (data, message) => {
623
330
  if (handlerRef.current) handlerRef.current(data, message);
624
331
  };
625
- var unsubscribes = typeList.map(function (t) {
626
- return subscribeToEvent(t, onEvent);
627
- });
628
- return function () {
629
- unsubscribes.forEach(function (unsub) {
630
- return unsub();
631
- });
332
+ const unsubscribes = typeList.map((t) => subscribeToEvent(t, onEvent));
333
+ return () => {
334
+ unsubscribes.forEach((unsub) => unsub());
632
335
  };
633
336
  }, [typesKey, enabled]);
634
337
  }
635
338
 
636
- // Framework-agnostic roster store: pure JS, no DOM and no React Native APIs, so
637
- // it is safe to import from the web package AND the mobile (React Native)
638
- // package. It owns the "materialize full state from a snapshot + transition
639
- // deltas" logic and the derivations over it (per-device roll-up, sorting,
640
- // labels, colors) — everything except the sensors and the pixels, which are
641
- // necessarily platform-specific.
642
-
643
- // Cable message types exchanged on the roster stream.
644
- var ROSTER_SNAPSHOT = 'roster_snapshot';
645
- var ROSTER_DELTA = 'roster_delta';
646
- // Content-free "re-poll now" trigger (:nudge mode). Carries no data.
647
- var ROSTER_NUDGE = 'roster_nudge';
648
-
649
- // Activity ranking, most-present first. Shared by sorting and by the "highest
650
- // across devices" roll-up so web and mobile agree.
651
- var STATUS_RANK = {
652
- active: 3,
653
- idle: 2,
654
- background: 1,
655
- offline: 0
656
- };
657
-
658
- // Status -> presence dot color. Platform UIs may theme these, but sharing a
659
- // default keeps web and mobile visually consistent out of the box.
660
- var STATUS_COLORS = {
661
- active: '#22c55e',
339
+ const ROSTER_SNAPSHOT = "roster_snapshot";
340
+ const ROSTER_DELTA = "roster_delta";
341
+ const ROSTER_NUDGE = "roster_nudge";
342
+ const STATUS_RANK = { active: 3, idle: 2, background: 1, offline: 0 };
343
+ const STATUS_COLORS = {
344
+ active: "#22c55e",
662
345
  // visible + working
663
- idle: '#f59e0b',
346
+ idle: "#f59e0b",
664
347
  // visible but not actively using
665
- background: '#9ca3af',
348
+ background: "#9ca3af",
666
349
  // only backgrounded/hidden sessions
667
- offline: '#d1d5db' // no live sessions
350
+ offline: "#d1d5db"
351
+ // no live sessions
668
352
  };
669
353
  function presenceColor(status) {
670
354
  return STATUS_COLORS[status] || STATUS_COLORS.offline;
671
355
  }
672
-
673
- /**
674
- * Apply an incoming cable message to a members Map (keyed by subject id),
675
- * returning a NEW Map (immutable update) so callers can trigger re-renders by
676
- * identity. Unknown message types return the same Map unchanged.
677
- *
678
- * - snapshot: replaces the whole roster.
679
- * - delta: upserts one member. Offline members are KEPT (status "offline") so
680
- * the roster stays the full team with live dots rather than vanishing rows.
681
- *
682
- * @param {Map<any, Object>} map - current members map
683
- * @param {Object} message - { type, members? , member? }
684
- * @returns {Map<any, Object>}
685
- */
686
356
  function applyRosterMessage(map, message) {
687
357
  if (!message || !message.type) return map;
688
358
  if (message.type === ROSTER_SNAPSHOT) {
689
- var next = new Map();
690
- (message.members || []).forEach(function (m) {
691
- return next.set(m.id, m);
692
- });
359
+ const next = /* @__PURE__ */ new Map();
360
+ (message.members || []).forEach((m) => next.set(m.id, m));
693
361
  return next;
694
362
  }
695
363
  if (message.type === ROSTER_DELTA && message.member) {
696
- var _next = new Map(map);
364
+ const next = new Map(map);
697
365
  if (message.member._removed) {
698
- // Member left the viewer's visible scope (a visibility change) — drop it.
699
- // Distinct from going offline, which arrives as status "offline".
700
- _next["delete"](message.member.id);
366
+ next.delete(message.member.id);
701
367
  } else {
702
- _next.set(message.member.id, message.member);
368
+ next.set(message.member.id, message.member);
703
369
  }
704
- return _next;
370
+ return next;
705
371
  }
706
372
  return map;
707
373
  }
708
-
709
- /**
710
- * Modes that require the client to poll the server for deltas (vs. passively
711
- * receiving pushed broadcasts). The server declares the mode in the snapshot.
712
- */
713
374
  function isPollMode(mode) {
714
- return mode === 'poll' || mode === 'nudge';
375
+ return mode === "poll" || mode === "nudge";
715
376
  }
716
-
717
- /**
718
- * Members Map -> array, sorted most-present first then by display label.
719
- */
720
377
  function sortedMembers(map) {
721
- var list = Array.from(map.values());
722
- list.sort(function (a, b) {
723
- var _STATUS_RANK$b$status, _STATUS_RANK$a$status;
724
- var rank = ((_STATUS_RANK$b$status = STATUS_RANK[b.status]) !== null && _STATUS_RANK$b$status !== void 0 ? _STATUS_RANK$b$status : 0) - ((_STATUS_RANK$a$status = STATUS_RANK[a.status]) !== null && _STATUS_RANK$a$status !== void 0 ? _STATUS_RANK$a$status : 0);
378
+ const list = Array.from(map.values());
379
+ list.sort((a, b) => {
380
+ const rank = (STATUS_RANK[b.status] ?? 0) - (STATUS_RANK[a.status] ?? 0);
725
381
  if (rank !== 0) return rank;
726
382
  return String(memberLabel(a)).localeCompare(String(memberLabel(b)));
727
383
  });
728
384
  return list;
729
385
  }
730
-
731
- /**
732
- * Only members with a live session (status !== "offline").
733
- */
734
386
  function onlineMembers(members) {
735
- return members.filter(function (m) {
736
- return m.status && m.status !== 'offline';
737
- });
387
+ return members.filter((m) => m.status && m.status !== "offline");
738
388
  }
739
-
740
- /**
741
- * That member's status on a specific device/platform, e.g.
742
- * deviceStatus(member, 'mobile') // => "idle"
743
- * Answers "is the user active on mobile?" vs the overall member.status.
744
- */
745
389
  function deviceStatus(member, platform) {
746
- return member && member.devices && member.devices[platform] || 'offline';
390
+ return member && member.devices && member.devices[platform] || "offline";
747
391
  }
748
-
749
- /**
750
- * Best-effort display label from a member's subject data.
751
- */
752
392
  function memberLabel(member) {
753
- if (!member) return '';
754
- return member.name || [member.first_name, member.last_name].filter(Boolean).join(' ') || member.email || member.username || "#".concat(member.id);
393
+ if (!member) return "";
394
+ return member.name || [member.first_name, member.last_name].filter(Boolean).join(" ") || member.email || member.username || `#${member.id}`;
755
395
  }
756
396
 
757
- var DEFAULT_CHANNEL = 'WhereIsWaldo::RosterChannel';
758
- var DEFAULT_POLL_INTERVAL = 15; // seconds; overridden by the server's snapshot
759
- var DEFAULT_NUDGE_JITTER = 0.5; // seconds; overridden by the server's snapshot
760
-
761
- /**
762
- * usePresenceRoster - live "who's around" roster as data (bring your own UI).
763
- *
764
- * The server picks the delivery mode per account and declares it in the initial
765
- * snapshot. This hook adapts transparently — no client mode config:
766
- * - broadcast/fanout : passively receive pushed deltas.
767
- * - pull/nudge : poll the server (`perform('poll')`) on the interval the
768
- * server specifies; the server replies with a filtered
769
- * snapshot (resync) or diff.
770
- * Either way the shared reducer (../core/rosterStore) keeps `members` as the
771
- * full, live set (snapshot seeded, deltas patched, removals dropped).
772
- *
773
- * Each member: { id, status, devices: { web, mobile, ... }, ...subjectData }.
774
- *
775
- * @param {Object} [options]
776
- * @param {string} [options.channelName] - override the roster channel name
777
- * @param {boolean} [options.enabled=true] - when false, no subscription is made
778
- * @param {Function} [options.filter] - (member) => boolean. Client-side filter
779
- * applied to the returned members. COSMETIC ONLY — intended for `:broadcast`
780
- * mode, where the full account roster reaches the client and you want to hide
781
- * some rows in the UI. It is NOT an access-control boundary (the unfiltered
782
- * data is already on the client). For enforced visibility use a server-side
783
- * mode (`:poll`/`:nudge`/`:fanout`).
784
- * @returns {{ members: Array, online: Array, onlineCount: number,
785
- * byId: Object, connected: boolean, mode: string|null }}
786
- */
787
- function usePresenceRoster() {
788
- var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
789
- var _options$channelName = options.channelName,
790
- channelName = _options$channelName === void 0 ? DEFAULT_CHANNEL : _options$channelName,
791
- _options$enabled = options.enabled,
792
- enabled = _options$enabled === void 0 ? true : _options$enabled,
793
- filter = options.filter;
794
- var _useState = React.useState([]),
795
- _useState2 = _slicedToArray(_useState, 2),
796
- members = _useState2[0],
797
- setMembers = _useState2[1];
798
- var _useState3 = React.useState(false),
799
- _useState4 = _slicedToArray(_useState3, 2),
800
- connected = _useState4[0],
801
- setConnected = _useState4[1];
802
- var _useState5 = React.useState(null),
803
- _useState6 = _slicedToArray(_useState5, 2),
804
- mode = _useState6[0],
805
- setMode = _useState6[1];
806
- var mapRef = React.useRef(new Map());
807
- var pollTimerRef = React.useRef(null);
808
- var nudgeTimerRef = React.useRef(null);
809
- var nudgeJitterRef = React.useRef(DEFAULT_NUDGE_JITTER);
810
- var flush = React.useCallback(function () {
397
+ const DEFAULT_CHANNEL = "WhereIsWaldo::RosterChannel";
398
+ const DEFAULT_POLL_INTERVAL = 15;
399
+ const DEFAULT_NUDGE_JITTER = 0.5;
400
+ function usePresenceRoster(options = {}) {
401
+ const { channelName = DEFAULT_CHANNEL, enabled = true, filter } = options;
402
+ const [members, setMembers] = React.useState([]);
403
+ const [connected, setConnected] = React.useState(false);
404
+ const [mode, setMode] = React.useState(null);
405
+ const mapRef = React.useRef(/* @__PURE__ */ new Map());
406
+ const pollTimerRef = React.useRef(null);
407
+ const nudgeTimerRef = React.useRef(null);
408
+ const nudgeJitterRef = React.useRef(DEFAULT_NUDGE_JITTER);
409
+ const flush = React.useCallback(() => {
811
410
  setMembers(sortedMembers(mapRef.current));
812
411
  }, []);
813
- React.useEffect(function () {
814
- if (!enabled) return undefined;
815
- var consumer = getConsumer();
816
- var startPolling = function startPolling(subscription, intervalSeconds) {
817
- if (pollTimerRef.current) return; // already polling
818
- var ms = (intervalSeconds || DEFAULT_POLL_INTERVAL) * 1000;
819
- // Small random offset so clients don't poll in lockstep (herd control).
820
- var jitter = function jitter() {
821
- return 0.85 + Math.random() * 0.3;
822
- };
823
- var _tick = function tick() {
824
- subscription.perform('poll');
825
- pollTimerRef.current = setTimeout(_tick, ms * jitter());
412
+ React.useEffect(() => {
413
+ if (!enabled) return void 0;
414
+ const consumer = getConsumer();
415
+ const startPolling = (subscription2, intervalSeconds) => {
416
+ if (pollTimerRef.current) return;
417
+ const ms = (intervalSeconds || DEFAULT_POLL_INTERVAL) * 1e3;
418
+ const jitter = () => 0.85 + Math.random() * 0.3;
419
+ const tick = () => {
420
+ subscription2.perform("poll");
421
+ pollTimerRef.current = setTimeout(tick, ms * jitter());
826
422
  };
827
- pollTimerRef.current = setTimeout(_tick, ms * jitter());
423
+ pollTimerRef.current = setTimeout(tick, ms * jitter());
828
424
  };
829
-
830
- // :nudge — on a content-free trigger, re-poll off-cycle after a random
831
- // jitter (herd control), debounced to one pending re-poll at a time.
832
- var scheduleNudgePoll = function scheduleNudgePoll(subscription) {
425
+ const scheduleNudgePoll = (subscription2) => {
833
426
  if (nudgeTimerRef.current) return;
834
- var delay = Math.random() * nudgeJitterRef.current * 1000;
835
- nudgeTimerRef.current = setTimeout(function () {
427
+ const delay = Math.random() * nudgeJitterRef.current * 1e3;
428
+ nudgeTimerRef.current = setTimeout(() => {
836
429
  nudgeTimerRef.current = null;
837
- subscription.perform('poll');
430
+ subscription2.perform("poll");
838
431
  }, delay);
839
432
  };
840
- var subscription = consumer.subscriptions.create({
841
- channel: channelName
842
- }, {
843
- connected: function connected() {
844
- setConnected(true);
845
- },
846
- disconnected: function disconnected() {
847
- setConnected(false);
848
- },
849
- received: function received(message) {
850
- if (message && message.type === ROSTER_SNAPSHOT) {
851
- setMode(message.mode || null);
852
- if (message.nudge_jitter != null) nudgeJitterRef.current = message.nudge_jitter;
853
- if (isPollMode(message.mode)) startPolling(subscription, message.poll_interval);
854
- }
855
- if (message && message.type === ROSTER_NUDGE) {
856
- scheduleNudgePoll(subscription);
857
- return; // content-free trigger; nothing to reduce
858
- }
859
- var next = applyRosterMessage(mapRef.current, message);
860
- if (next !== mapRef.current) {
861
- mapRef.current = next;
862
- flush();
433
+ const subscription = consumer.subscriptions.create(
434
+ { channel: channelName },
435
+ {
436
+ connected() {
437
+ setConnected(true);
438
+ },
439
+ disconnected() {
440
+ setConnected(false);
441
+ },
442
+ received(message) {
443
+ if (message && message.type === ROSTER_SNAPSHOT) {
444
+ setMode(message.mode || null);
445
+ if (message.nudge_jitter != null) nudgeJitterRef.current = message.nudge_jitter;
446
+ if (isPollMode(message.mode)) startPolling(subscription, message.poll_interval);
447
+ }
448
+ if (message && message.type === ROSTER_NUDGE) {
449
+ scheduleNudgePoll(subscription);
450
+ return;
451
+ }
452
+ const next = applyRosterMessage(mapRef.current, message);
453
+ if (next !== mapRef.current) {
454
+ mapRef.current = next;
455
+ flush();
456
+ }
863
457
  }
864
458
  }
865
- });
866
- return function () {
459
+ );
460
+ return () => {
867
461
  if (pollTimerRef.current) {
868
462
  clearTimeout(pollTimerRef.current);
869
463
  pollTimerRef.current = null;
@@ -875,22 +469,13 @@ function usePresenceRoster() {
875
469
  subscription.unsubscribe();
876
470
  };
877
471
  }, [channelName, enabled, flush]);
878
-
879
- // Client-side (cosmetic) filter — see options.filter.
880
- var visibleMembers = filter ? members.filter(filter) : members;
881
- var online = onlineMembers(visibleMembers);
882
- var byId = {};
883
- visibleMembers.forEach(function (m) {
472
+ const visibleMembers = filter ? members.filter(filter) : members;
473
+ const online = onlineMembers(visibleMembers);
474
+ const byId = {};
475
+ visibleMembers.forEach((m) => {
884
476
  byId[m.id] = m;
885
477
  });
886
- return {
887
- members: visibleMembers,
888
- online: online,
889
- onlineCount: online.length,
890
- byId: byId,
891
- connected: connected,
892
- mode: mode
893
- };
478
+ return { members: visibleMembers, online, onlineCount: online.length, byId, connected, mode };
894
479
  }
895
480
 
896
481
  exports.PresenceContext = PresenceContext;