@xh/hoist 73.0.0-SNAPSHOT.1744325910318 → 73.0.0-SNAPSHOT.1744391471093
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/CHANGELOG.md +1 -0
- package/admin/tabs/activity/tracking/ActivityTrackingModel.ts +22 -14
- package/appcontainer/AppStateModel.ts +3 -3
- package/build/types/admin/tabs/activity/tracking/ActivityTrackingModel.d.ts +1 -1
- package/build/types/core/XH.d.ts +4 -4
- package/build/types/security/Types.d.ts +0 -32
- package/build/types/security/msal/MsalClient.d.ts +40 -4
- package/build/types/svc/ClientHealthService.d.ts +2 -1
- package/build/types/svc/WebSocketService.d.ts +35 -14
- package/core/XH.ts +6 -6
- package/data/filter/BaseFilterFieldSpec.ts +6 -2
- package/package.json +1 -1
- package/security/Types.ts +0 -34
- package/security/msal/MsalClient.ts +46 -9
- package/svc/ClientHealthService.ts +6 -5
- package/svc/WebSocketService.ts +67 -31
- package/tsconfig.tsbuildinfo +1 -1
package/svc/WebSocketService.ts
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Copyright © 2025 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
|
-
import {HoistService, XH} from '@xh/hoist/core';
|
|
8
|
-
import {
|
|
7
|
+
import {HoistService, PlainObject, XH} from '@xh/hoist/core';
|
|
8
|
+
import {withFormattedTimestamps} from '@xh/hoist/format';
|
|
9
9
|
import {action, makeObservable, observable} from '@xh/hoist/mobx';
|
|
10
10
|
import {Timer} from '@xh/hoist/utils/async';
|
|
11
11
|
import {SECONDS} from '@xh/hoist/utils/datetime';
|
|
@@ -43,13 +43,14 @@ export class WebSocketService extends HoistService {
|
|
|
43
43
|
readonly FORCE_APP_SUSPEND_TOPIC = 'xhForceAppSuspend';
|
|
44
44
|
readonly REQ_CLIENT_HEALTH_RPT_TOPIC = 'xhRequestClientHealthReport';
|
|
45
45
|
|
|
46
|
+
/** True if WebSockets generally enabled - set statically in code via {@link AppSpec}. */
|
|
47
|
+
enabled: boolean = XH.appSpec.webSocketsEnabled;
|
|
48
|
+
|
|
46
49
|
/** Unique channel assigned by server upon successful connection. */
|
|
47
|
-
@observable
|
|
48
|
-
channelKey: string = null;
|
|
50
|
+
@observable channelKey: string = null;
|
|
49
51
|
|
|
50
52
|
/** Last time a message was received, including heartbeat messages. */
|
|
51
|
-
@observable
|
|
52
|
-
lastMessageTime: Date = null;
|
|
53
|
+
@observable lastMessageTime: Date = null;
|
|
53
54
|
|
|
54
55
|
/** Observable flag indicating service is connected and available for use. */
|
|
55
56
|
get connected(): boolean {
|
|
@@ -59,11 +60,11 @@ export class WebSocketService extends HoistService {
|
|
|
59
60
|
/** Set to true to log all sent/received messages - very chatty. */
|
|
60
61
|
logMessages: boolean = false;
|
|
61
62
|
|
|
63
|
+
telemetry: WebSocketTelemetry = null;
|
|
64
|
+
|
|
62
65
|
private _timer: Timer;
|
|
63
66
|
private _socket: WebSocket;
|
|
64
|
-
private _subsByTopic = {};
|
|
65
|
-
|
|
66
|
-
enabled: boolean = XH.appSpec.webSocketsEnabled;
|
|
67
|
+
private _subsByTopic: Record<string, WebSocketSubscription[]> = {};
|
|
67
68
|
|
|
68
69
|
constructor() {
|
|
69
70
|
super();
|
|
@@ -72,6 +73,7 @@ export class WebSocketService extends HoistService {
|
|
|
72
73
|
|
|
73
74
|
override async initAsync() {
|
|
74
75
|
if (!this.enabled) return;
|
|
76
|
+
|
|
75
77
|
const {environmentService} = XH;
|
|
76
78
|
if (environmentService.get('webSocketsEnabled') === false) {
|
|
77
79
|
this.logError(
|
|
@@ -80,6 +82,7 @@ export class WebSocketService extends HoistService {
|
|
|
80
82
|
this.enabled = false;
|
|
81
83
|
return;
|
|
82
84
|
}
|
|
85
|
+
this.telemetry = {channelKey: null, subscriptionCount: 0, events: {}};
|
|
83
86
|
|
|
84
87
|
this.connect();
|
|
85
88
|
|
|
@@ -112,6 +115,7 @@ export class WebSocketService extends HoistService {
|
|
|
112
115
|
|
|
113
116
|
const newSub = new WebSocketSubscription(topic, fn);
|
|
114
117
|
subs.push(newSub);
|
|
118
|
+
this.telemetry.subscriptionCount++;
|
|
115
119
|
return newSub;
|
|
116
120
|
}
|
|
117
121
|
|
|
@@ -123,6 +127,7 @@ export class WebSocketService extends HoistService {
|
|
|
123
127
|
unsubscribe(subscription: WebSocketSubscription) {
|
|
124
128
|
const subs = this.getSubsForTopic(subscription.topic);
|
|
125
129
|
pull(subs, subscription);
|
|
130
|
+
this.telemetry.subscriptionCount--;
|
|
126
131
|
}
|
|
127
132
|
|
|
128
133
|
/**
|
|
@@ -133,13 +138,24 @@ export class WebSocketService extends HoistService {
|
|
|
133
138
|
throwIf(!this.connected, 'Unable to send message via websocket - not connected.');
|
|
134
139
|
|
|
135
140
|
this._socket.send(JSON.stringify(message));
|
|
141
|
+
|
|
142
|
+
this.noteTelemetryEvent('msgSent');
|
|
136
143
|
this.maybeLogMessage('Sent message', message);
|
|
137
144
|
}
|
|
138
145
|
|
|
146
|
+
shutdown() {
|
|
147
|
+
if (this._timer) this._timer.cancel();
|
|
148
|
+
this.disconnect();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
getFormattedTelemetry(): PlainObject {
|
|
152
|
+
return withFormattedTimestamps(this.telemetry);
|
|
153
|
+
}
|
|
154
|
+
|
|
139
155
|
//------------------------
|
|
140
156
|
// Implementation
|
|
141
157
|
//------------------------
|
|
142
|
-
connect() {
|
|
158
|
+
private connect() {
|
|
143
159
|
try {
|
|
144
160
|
// Create new socket and wire up events. Be sure to ignore obsolete sockets
|
|
145
161
|
const s = new WebSocket(this.buildWebSocketUrl());
|
|
@@ -163,7 +179,7 @@ export class WebSocketService extends HoistService {
|
|
|
163
179
|
this.updateConnectedStatus();
|
|
164
180
|
}
|
|
165
181
|
|
|
166
|
-
disconnect() {
|
|
182
|
+
private disconnect() {
|
|
167
183
|
if (this._socket) {
|
|
168
184
|
this._socket.close();
|
|
169
185
|
this._socket = null;
|
|
@@ -171,12 +187,13 @@ export class WebSocketService extends HoistService {
|
|
|
171
187
|
this.updateConnectedStatus();
|
|
172
188
|
}
|
|
173
189
|
|
|
174
|
-
heartbeatOrReconnect() {
|
|
190
|
+
private heartbeatOrReconnect() {
|
|
175
191
|
this.updateConnectedStatus();
|
|
176
192
|
if (this.connected) {
|
|
177
193
|
this.sendMessage({topic: this.HEARTBEAT_TOPIC, data: 'ping'});
|
|
178
194
|
} else {
|
|
179
195
|
this.logWarn('Heartbeat found websocket not connected - attempting to reconnect...');
|
|
196
|
+
this.noteTelemetryEvent('heartbeatReconnectAttempt');
|
|
180
197
|
this.disconnect();
|
|
181
198
|
this.connect();
|
|
182
199
|
}
|
|
@@ -184,30 +201,29 @@ export class WebSocketService extends HoistService {
|
|
|
184
201
|
|
|
185
202
|
private onServerInstanceChange() {
|
|
186
203
|
this.logWarn('Server instance changed - attempting to connect to new instance.');
|
|
204
|
+
this.noteTelemetryEvent('instanceChangeReconnectAttempt');
|
|
187
205
|
this.disconnect();
|
|
188
206
|
this.connect();
|
|
189
207
|
}
|
|
190
208
|
|
|
191
|
-
shutdown() {
|
|
192
|
-
if (this._timer) this._timer.cancel();
|
|
193
|
-
this.disconnect();
|
|
194
|
-
}
|
|
195
|
-
|
|
196
209
|
//------------------------
|
|
197
210
|
// Socket events impl
|
|
198
211
|
//------------------------
|
|
199
212
|
onOpen(ev) {
|
|
200
213
|
this.logDebug('WebSocket connection opened', ev);
|
|
214
|
+
this.noteTelemetryEvent('connOpened');
|
|
201
215
|
this.updateConnectedStatus();
|
|
202
216
|
}
|
|
203
217
|
|
|
204
218
|
onClose(ev) {
|
|
205
219
|
this.logDebug('WebSocket connection closed', ev);
|
|
220
|
+
this.noteTelemetryEvent('connClosed');
|
|
206
221
|
this.updateConnectedStatus();
|
|
207
222
|
}
|
|
208
223
|
|
|
209
224
|
onError(ev) {
|
|
210
225
|
this.logError('WebSocket connection error', ev);
|
|
226
|
+
this.noteTelemetryEvent('connError');
|
|
211
227
|
this.updateConnectedStatus();
|
|
212
228
|
}
|
|
213
229
|
|
|
@@ -219,6 +235,7 @@ export class WebSocketService extends HoistService {
|
|
|
219
235
|
// Record arrival
|
|
220
236
|
this.updateLastMessageTime();
|
|
221
237
|
this.maybeLogMessage('Received message', rawMsg);
|
|
238
|
+
this.noteTelemetryEvent('msgReceived');
|
|
222
239
|
|
|
223
240
|
// Hoist and app handling
|
|
224
241
|
switch (topic) {
|
|
@@ -244,7 +261,7 @@ export class WebSocketService extends HoistService {
|
|
|
244
261
|
//------------------------
|
|
245
262
|
// Subscription impl
|
|
246
263
|
//------------------------
|
|
247
|
-
notifySubscribers(message) {
|
|
264
|
+
private notifySubscribers(message) {
|
|
248
265
|
const subs = this.getSubsForTopic(message.topic);
|
|
249
266
|
|
|
250
267
|
subs.forEach(sub => {
|
|
@@ -256,7 +273,7 @@ export class WebSocketService extends HoistService {
|
|
|
256
273
|
});
|
|
257
274
|
}
|
|
258
275
|
|
|
259
|
-
getSubsForTopic(topic): WebSocketSubscription[] {
|
|
276
|
+
private getSubsForTopic(topic: string): WebSocketSubscription[] {
|
|
260
277
|
let ret = this._subsByTopic[topic];
|
|
261
278
|
if (!ret) {
|
|
262
279
|
ret = this._subsByTopic[topic] = [];
|
|
@@ -267,7 +284,7 @@ export class WebSocketService extends HoistService {
|
|
|
267
284
|
//------------------------
|
|
268
285
|
// Other impl
|
|
269
286
|
//------------------------
|
|
270
|
-
updateConnectedStatus() {
|
|
287
|
+
private updateConnectedStatus() {
|
|
271
288
|
const socketOpen = this._socket?.readyState === WebSocket.OPEN;
|
|
272
289
|
if (!socketOpen && this.channelKey) {
|
|
273
290
|
this.installChannelKey(null);
|
|
@@ -275,16 +292,17 @@ export class WebSocketService extends HoistService {
|
|
|
275
292
|
}
|
|
276
293
|
|
|
277
294
|
@action
|
|
278
|
-
installChannelKey(key) {
|
|
295
|
+
private installChannelKey(key: string) {
|
|
279
296
|
this.channelKey = key;
|
|
297
|
+
this.telemetry.channelKey = key;
|
|
280
298
|
}
|
|
281
299
|
|
|
282
300
|
@action
|
|
283
|
-
updateLastMessageTime() {
|
|
301
|
+
private updateLastMessageTime() {
|
|
284
302
|
this.lastMessageTime = new Date();
|
|
285
303
|
}
|
|
286
304
|
|
|
287
|
-
buildWebSocketUrl() {
|
|
305
|
+
private buildWebSocketUrl() {
|
|
288
306
|
const protocol = window.location.protocol == 'https:' ? 'wss:' : 'ws:',
|
|
289
307
|
endpoint = 'xhWebSocket?clientAppVersion=' + XH.appVersion;
|
|
290
308
|
return XH.isDevelopmentMode
|
|
@@ -292,16 +310,14 @@ export class WebSocketService extends HoistService {
|
|
|
292
310
|
: `${protocol}//${window.location.host}${XH.baseUrl}${endpoint}`;
|
|
293
311
|
}
|
|
294
312
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
title: 'Test Message',
|
|
298
|
-
icon: Icon.bullhorn(),
|
|
299
|
-
message
|
|
300
|
-
});
|
|
313
|
+
private maybeLogMessage(...args) {
|
|
314
|
+
if (this.logMessages) this.logDebug(args);
|
|
301
315
|
}
|
|
302
316
|
|
|
303
|
-
|
|
304
|
-
|
|
317
|
+
private noteTelemetryEvent(eventKey: keyof WebSocketTelemetry['events']) {
|
|
318
|
+
const evtTel = (this.telemetry.events[eventKey] ??= {count: 0, lastTime: null});
|
|
319
|
+
evtTel.count++;
|
|
320
|
+
evtTel.lastTime = Date.now();
|
|
305
321
|
}
|
|
306
322
|
}
|
|
307
323
|
|
|
@@ -327,3 +343,23 @@ export interface WebSocketMessage {
|
|
|
327
343
|
topic: string;
|
|
328
344
|
data?: any;
|
|
329
345
|
}
|
|
346
|
+
|
|
347
|
+
/** Telemetry collected by this service + included in {@link ClientHealthService} reporting. */
|
|
348
|
+
export interface WebSocketTelemetry {
|
|
349
|
+
channelKey: string;
|
|
350
|
+
subscriptionCount: number;
|
|
351
|
+
events: {
|
|
352
|
+
connOpened?: WebSocketEventTelemetry;
|
|
353
|
+
connClosed?: WebSocketEventTelemetry;
|
|
354
|
+
connError?: WebSocketEventTelemetry;
|
|
355
|
+
msgReceived?: WebSocketEventTelemetry;
|
|
356
|
+
msgSent?: WebSocketEventTelemetry;
|
|
357
|
+
heartbeatReconnectAttempt?: WebSocketEventTelemetry;
|
|
358
|
+
instanceChangeReconnectAttempt?: WebSocketEventTelemetry;
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export interface WebSocketEventTelemetry {
|
|
363
|
+
count: number;
|
|
364
|
+
lastTime: number;
|
|
365
|
+
}
|