@xh/hoist 72.4.0 → 72.5.1
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 +21 -8
- package/admin/tabs/activity/tracking/ActivityTrackingModel.ts +22 -14
- package/admin/tabs/cluster/instances/logs/LogDisplayModel.ts +10 -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 +39 -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 +80 -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 = {channelKey: null, subscriptionCount: 0, events: {}};
|
|
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(
|
|
@@ -104,6 +108,7 @@ export class WebSocketService extends HoistService {
|
|
|
104
108
|
* dispose of their subs on destroy.
|
|
105
109
|
*/
|
|
106
110
|
subscribe(topic: string, fn: (msg: WebSocketMessage) => any): WebSocketSubscription {
|
|
111
|
+
this.ensureEnabled();
|
|
107
112
|
const subs = this.getSubsForTopic(topic),
|
|
108
113
|
existingSub = find(subs, {fn});
|
|
109
114
|
|
|
@@ -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
|
|
|
@@ -120,25 +126,39 @@ export class WebSocketService extends HoistService {
|
|
|
120
126
|
* @param subscription - WebSocketSubscription returned when the subscription was established.
|
|
121
127
|
*/
|
|
122
128
|
unsubscribe(subscription: WebSocketSubscription) {
|
|
129
|
+
this.ensureEnabled();
|
|
123
130
|
const subs = this.getSubsForTopic(subscription.topic);
|
|
124
131
|
pull(subs, subscription);
|
|
132
|
+
this.telemetry.subscriptionCount--;
|
|
125
133
|
}
|
|
126
134
|
|
|
127
135
|
/**
|
|
128
136
|
* Send a message back to the server via the connected websocket.
|
|
129
137
|
*/
|
|
130
138
|
sendMessage(message: WebSocketMessage) {
|
|
139
|
+
this.ensureEnabled();
|
|
131
140
|
this.updateConnectedStatus();
|
|
132
141
|
throwIf(!this.connected, 'Unable to send message via websocket - not connected.');
|
|
133
142
|
|
|
134
143
|
this._socket.send(JSON.stringify(message));
|
|
144
|
+
|
|
145
|
+
this.noteTelemetryEvent('msgSent');
|
|
135
146
|
this.maybeLogMessage('Sent message', message);
|
|
136
147
|
}
|
|
137
148
|
|
|
149
|
+
shutdown() {
|
|
150
|
+
if (this._timer) this._timer.cancel();
|
|
151
|
+
this.disconnect();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
getFormattedTelemetry(): PlainObject {
|
|
155
|
+
return withFormattedTimestamps(this.telemetry);
|
|
156
|
+
}
|
|
157
|
+
|
|
138
158
|
//------------------------
|
|
139
159
|
// Implementation
|
|
140
160
|
//------------------------
|
|
141
|
-
connect() {
|
|
161
|
+
private connect() {
|
|
142
162
|
try {
|
|
143
163
|
// Create new socket and wire up events. Be sure to ignore obsolete sockets
|
|
144
164
|
const s = new WebSocket(this.buildWebSocketUrl());
|
|
@@ -162,7 +182,7 @@ export class WebSocketService extends HoistService {
|
|
|
162
182
|
this.updateConnectedStatus();
|
|
163
183
|
}
|
|
164
184
|
|
|
165
|
-
disconnect() {
|
|
185
|
+
private disconnect() {
|
|
166
186
|
if (this._socket) {
|
|
167
187
|
this._socket.close();
|
|
168
188
|
this._socket = null;
|
|
@@ -170,12 +190,13 @@ export class WebSocketService extends HoistService {
|
|
|
170
190
|
this.updateConnectedStatus();
|
|
171
191
|
}
|
|
172
192
|
|
|
173
|
-
heartbeatOrReconnect() {
|
|
193
|
+
private heartbeatOrReconnect() {
|
|
174
194
|
this.updateConnectedStatus();
|
|
175
195
|
if (this.connected) {
|
|
176
196
|
this.sendMessage({topic: this.HEARTBEAT_TOPIC, data: 'ping'});
|
|
177
197
|
} else {
|
|
178
198
|
this.logWarn('Heartbeat found websocket not connected - attempting to reconnect...');
|
|
199
|
+
this.noteTelemetryEvent('heartbeatReconnectAttempt');
|
|
179
200
|
this.disconnect();
|
|
180
201
|
this.connect();
|
|
181
202
|
}
|
|
@@ -183,34 +204,33 @@ export class WebSocketService extends HoistService {
|
|
|
183
204
|
|
|
184
205
|
private onServerInstanceChange() {
|
|
185
206
|
this.logWarn('Server instance changed - attempting to connect to new instance.');
|
|
207
|
+
this.noteTelemetryEvent('instanceChangeReconnectAttempt');
|
|
186
208
|
this.disconnect();
|
|
187
209
|
this.connect();
|
|
188
210
|
}
|
|
189
211
|
|
|
190
|
-
shutdown() {
|
|
191
|
-
if (this._timer) this._timer.cancel();
|
|
192
|
-
this.disconnect();
|
|
193
|
-
}
|
|
194
|
-
|
|
195
212
|
//------------------------
|
|
196
213
|
// Socket events impl
|
|
197
214
|
//------------------------
|
|
198
215
|
onOpen(ev) {
|
|
199
216
|
this.logDebug('WebSocket connection opened', ev);
|
|
217
|
+
this.noteTelemetryEvent('connOpened');
|
|
200
218
|
this.updateConnectedStatus();
|
|
201
219
|
}
|
|
202
220
|
|
|
203
221
|
onClose(ev) {
|
|
204
222
|
this.logDebug('WebSocket connection closed', ev);
|
|
223
|
+
this.noteTelemetryEvent('connClosed');
|
|
205
224
|
this.updateConnectedStatus();
|
|
206
225
|
}
|
|
207
226
|
|
|
208
227
|
onError(ev) {
|
|
209
228
|
this.logError('WebSocket connection error', ev);
|
|
229
|
+
this.noteTelemetryEvent('connError');
|
|
210
230
|
this.updateConnectedStatus();
|
|
211
231
|
}
|
|
212
232
|
|
|
213
|
-
onMessage(rawMsg) {
|
|
233
|
+
onMessage(rawMsg: MessageEvent) {
|
|
214
234
|
try {
|
|
215
235
|
const msg = JSON.parse(rawMsg.data),
|
|
216
236
|
{topic, data} = msg;
|
|
@@ -218,6 +238,7 @@ export class WebSocketService extends HoistService {
|
|
|
218
238
|
// Record arrival
|
|
219
239
|
this.updateLastMessageTime();
|
|
220
240
|
this.maybeLogMessage('Received message', rawMsg);
|
|
241
|
+
this.noteTelemetryEvent('msgReceived');
|
|
221
242
|
|
|
222
243
|
// Hoist and app handling
|
|
223
244
|
switch (topic) {
|
|
@@ -228,6 +249,9 @@ export class WebSocketService extends HoistService {
|
|
|
228
249
|
XH.suspendApp({reason: 'SERVER_FORCE', message: data});
|
|
229
250
|
XH.track({category: 'App', message: 'App suspended via WebSocket'});
|
|
230
251
|
break;
|
|
252
|
+
case this.REQ_CLIENT_HEALTH_RPT_TOPIC:
|
|
253
|
+
XH.clientHealthService.sendReportAsync();
|
|
254
|
+
break;
|
|
231
255
|
}
|
|
232
256
|
|
|
233
257
|
this.notifySubscribers(msg);
|
|
@@ -240,7 +264,7 @@ export class WebSocketService extends HoistService {
|
|
|
240
264
|
//------------------------
|
|
241
265
|
// Subscription impl
|
|
242
266
|
//------------------------
|
|
243
|
-
notifySubscribers(message) {
|
|
267
|
+
private notifySubscribers(message) {
|
|
244
268
|
const subs = this.getSubsForTopic(message.topic);
|
|
245
269
|
|
|
246
270
|
subs.forEach(sub => {
|
|
@@ -252,7 +276,7 @@ export class WebSocketService extends HoistService {
|
|
|
252
276
|
});
|
|
253
277
|
}
|
|
254
278
|
|
|
255
|
-
getSubsForTopic(topic): WebSocketSubscription[] {
|
|
279
|
+
private getSubsForTopic(topic: string): WebSocketSubscription[] {
|
|
256
280
|
let ret = this._subsByTopic[topic];
|
|
257
281
|
if (!ret) {
|
|
258
282
|
ret = this._subsByTopic[topic] = [];
|
|
@@ -263,7 +287,7 @@ export class WebSocketService extends HoistService {
|
|
|
263
287
|
//------------------------
|
|
264
288
|
// Other impl
|
|
265
289
|
//------------------------
|
|
266
|
-
updateConnectedStatus() {
|
|
290
|
+
private updateConnectedStatus() {
|
|
267
291
|
const socketOpen = this._socket?.readyState === WebSocket.OPEN;
|
|
268
292
|
if (!socketOpen && this.channelKey) {
|
|
269
293
|
this.installChannelKey(null);
|
|
@@ -271,33 +295,36 @@ export class WebSocketService extends HoistService {
|
|
|
271
295
|
}
|
|
272
296
|
|
|
273
297
|
@action
|
|
274
|
-
installChannelKey(key) {
|
|
298
|
+
private installChannelKey(key: string) {
|
|
275
299
|
this.channelKey = key;
|
|
300
|
+
this.telemetry.channelKey = key;
|
|
276
301
|
}
|
|
277
302
|
|
|
278
303
|
@action
|
|
279
|
-
updateLastMessageTime() {
|
|
304
|
+
private updateLastMessageTime() {
|
|
280
305
|
this.lastMessageTime = new Date();
|
|
281
306
|
}
|
|
282
307
|
|
|
283
|
-
buildWebSocketUrl() {
|
|
308
|
+
private buildWebSocketUrl() {
|
|
284
309
|
const protocol = window.location.protocol == 'https:' ? 'wss:' : 'ws:',
|
|
285
|
-
endpoint =
|
|
310
|
+
endpoint = `xhWebSocket?${this.METADATA_FOR_HANDSHAKE.map(key => `${key}=${XH[key]}`).join('&')}`;
|
|
286
311
|
return XH.isDevelopmentMode
|
|
287
312
|
? `${protocol}//${XH.baseUrl.split('//')[1]}${endpoint}`
|
|
288
313
|
: `${protocol}//${window.location.host}${XH.baseUrl}${endpoint}`;
|
|
289
314
|
}
|
|
290
315
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
title: 'Test Message',
|
|
294
|
-
icon: Icon.bullhorn(),
|
|
295
|
-
message
|
|
296
|
-
});
|
|
316
|
+
private maybeLogMessage(...args) {
|
|
317
|
+
if (this.logMessages) this.logDebug(args);
|
|
297
318
|
}
|
|
298
319
|
|
|
299
|
-
|
|
300
|
-
|
|
320
|
+
private noteTelemetryEvent(eventKey: keyof WebSocketTelemetry['events']) {
|
|
321
|
+
const evtTel = (this.telemetry.events[eventKey] ??= {count: 0, lastTime: null});
|
|
322
|
+
evtTel.count++;
|
|
323
|
+
evtTel.lastTime = Date.now();
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
private ensureEnabled() {
|
|
327
|
+
throwIf(!this.enabled, 'Operation not available. WebSocketService is disabled.');
|
|
301
328
|
}
|
|
302
329
|
}
|
|
303
330
|
|
|
@@ -323,3 +350,23 @@ export interface WebSocketMessage {
|
|
|
323
350
|
topic: string;
|
|
324
351
|
data?: any;
|
|
325
352
|
}
|
|
353
|
+
|
|
354
|
+
/** Telemetry collected by this service + included in {@link ClientHealthService} reporting. */
|
|
355
|
+
export interface WebSocketTelemetry {
|
|
356
|
+
channelKey: string;
|
|
357
|
+
subscriptionCount: number;
|
|
358
|
+
events: {
|
|
359
|
+
connOpened?: WebSocketEventTelemetry;
|
|
360
|
+
connClosed?: WebSocketEventTelemetry;
|
|
361
|
+
connError?: WebSocketEventTelemetry;
|
|
362
|
+
msgReceived?: WebSocketEventTelemetry;
|
|
363
|
+
msgSent?: WebSocketEventTelemetry;
|
|
364
|
+
heartbeatReconnectAttempt?: WebSocketEventTelemetry;
|
|
365
|
+
instanceChangeReconnectAttempt?: WebSocketEventTelemetry;
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export interface WebSocketEventTelemetry {
|
|
370
|
+
count: number;
|
|
371
|
+
lastTime: number;
|
|
372
|
+
}
|