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