@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/README.md +3 -2
- 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 +7 -7
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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 !==
|
|
117
|
-
|
|
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
|
-
|
|
22
|
+
const current = listeners.get(type);
|
|
125
23
|
if (!current) return;
|
|
126
|
-
current
|
|
127
|
-
if (current.size === 0) listeners
|
|
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
|
-
|
|
37
|
+
let url = config.url;
|
|
154
38
|
if (config.getToken) {
|
|
155
|
-
|
|
39
|
+
const token = config.getToken();
|
|
156
40
|
if (token) {
|
|
157
|
-
|
|
158
|
-
url =
|
|
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
|
|
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
|
-
|
|
192
|
-
|
|
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
|
-
|
|
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
|
-
|
|
210
|
-
channelName:
|
|
211
|
-
heartbeatInterval:
|
|
72
|
+
const DEFAULTS = {
|
|
73
|
+
channelName: "WhereIsWaldo::PresenceChannel",
|
|
74
|
+
heartbeatInterval: 3e4,
|
|
212
75
|
// ms
|
|
213
|
-
activityTimeout:
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
262
|
-
var overrides = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
|
|
107
|
+
const sendHeartbeat = (overrides = {}) => {
|
|
263
108
|
if (!subscription) return;
|
|
264
|
-
log(
|
|
265
|
-
subscription.perform(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
130
|
+
const setVisible = (visible) => {
|
|
293
131
|
if (visible === state.visible) return;
|
|
294
132
|
state.visible = visible;
|
|
295
133
|
emitChange();
|
|
296
|
-
sendHeartbeat();
|
|
134
|
+
sendHeartbeat();
|
|
297
135
|
if (visible) reportActivity();
|
|
298
136
|
};
|
|
299
|
-
|
|
137
|
+
const startHeartbeatLoop = () => {
|
|
300
138
|
if (heartbeatTimer) clearInterval(heartbeatTimer);
|
|
301
|
-
heartbeatTimer = setInterval(
|
|
302
|
-
return sendHeartbeat();
|
|
303
|
-
}, config.heartbeatInterval);
|
|
139
|
+
heartbeatTimer = setInterval(() => sendHeartbeat(), config.heartbeatInterval);
|
|
304
140
|
};
|
|
305
|
-
|
|
141
|
+
const start = () => {
|
|
306
142
|
if (subscription) return subscription;
|
|
307
|
-
subscription = resolveConsumer().subscriptions.create(
|
|
308
|
-
channel: config.channelName,
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
-
|
|
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
|
|
352
|
-
stop
|
|
353
|
-
setVisible
|
|
354
|
-
reportActivity
|
|
355
|
-
sendHeartbeat
|
|
356
|
-
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
|
-
|
|
363
|
-
channelName:
|
|
364
|
-
heartbeatInterval:
|
|
365
|
-
activityTimeout:
|
|
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
|
-
|
|
371
|
-
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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:
|
|
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
|
|
232
|
+
return () => {
|
|
439
233
|
reporter.stop();
|
|
440
234
|
reporterRef.current = null;
|
|
441
235
|
};
|
|
442
|
-
}, [config.channelName]);
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
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
|
-
|
|
465
|
-
|
|
466
|
-
|
|
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
|
-
|
|
472
|
-
|
|
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
|
-
|
|
255
|
+
const onFocus = () => {
|
|
478
256
|
sensorRef.current.focus = true;
|
|
479
257
|
setWindowFocused(true);
|
|
480
258
|
pushForeground();
|
|
481
259
|
};
|
|
482
|
-
|
|
260
|
+
const onBlur = () => {
|
|
483
261
|
sensorRef.current.focus = false;
|
|
484
262
|
setWindowFocused(false);
|
|
485
263
|
pushForeground();
|
|
486
264
|
};
|
|
487
|
-
document.addEventListener(
|
|
488
|
-
window.addEventListener(
|
|
489
|
-
window.addEventListener(
|
|
490
|
-
return
|
|
491
|
-
document.removeEventListener(
|
|
492
|
-
window.removeEventListener(
|
|
493
|
-
window.removeEventListener(
|
|
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
|
-
|
|
497
|
-
|
|
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
|
-
|
|
511
|
-
|
|
512
|
-
channelName:
|
|
513
|
-
heartbeatInterval:
|
|
514
|
-
activityTimeout:
|
|
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
|
-
|
|
525
|
-
|
|
526
|
-
return
|
|
290
|
+
const cableConfig = getCableConfig();
|
|
291
|
+
const presenceConfig = cableConfig.presence || {};
|
|
292
|
+
return { ...DEFAULT_CONFIG, ...presenceConfig, ...configOverrides };
|
|
527
293
|
}
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
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
|
-
|
|
311
|
+
const context = React.useContext(PresenceContext);
|
|
568
312
|
if (!context) {
|
|
569
|
-
throw new Error(
|
|
313
|
+
throw new Error("usePresenceContext must be used within a PresenceProvider");
|
|
570
314
|
}
|
|
571
315
|
return context;
|
|
572
316
|
}
|
|
573
317
|
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
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
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
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
|
-
|
|
626
|
-
|
|
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
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
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:
|
|
346
|
+
idle: "#f59e0b",
|
|
664
347
|
// visible but not actively using
|
|
665
|
-
background:
|
|
348
|
+
background: "#9ca3af",
|
|
666
349
|
// only backgrounded/hidden sessions
|
|
667
|
-
offline:
|
|
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
|
-
|
|
690
|
-
(message.members || []).forEach(
|
|
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
|
-
|
|
364
|
+
const next = new Map(map);
|
|
697
365
|
if (message.member._removed) {
|
|
698
|
-
|
|
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
|
-
|
|
368
|
+
next.set(message.member.id, message.member);
|
|
703
369
|
}
|
|
704
|
-
return
|
|
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 ===
|
|
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
|
-
|
|
722
|
-
list.sort(
|
|
723
|
-
|
|
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(
|
|
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] ||
|
|
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(
|
|
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
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
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(
|
|
814
|
-
if (!enabled) return
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
if (pollTimerRef.current) return;
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
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(
|
|
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
|
-
|
|
835
|
-
nudgeTimerRef.current = setTimeout(
|
|
427
|
+
const delay = Math.random() * nudgeJitterRef.current * 1e3;
|
|
428
|
+
nudgeTimerRef.current = setTimeout(() => {
|
|
836
429
|
nudgeTimerRef.current = null;
|
|
837
|
-
|
|
430
|
+
subscription2.perform("poll");
|
|
838
431
|
}, delay);
|
|
839
432
|
};
|
|
840
|
-
|
|
841
|
-
channel: channelName
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
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
|
|
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
|
-
|
|
880
|
-
|
|
881
|
-
|
|
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;
|