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