@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.
Files changed (34) hide show
  1. package/CHANGELOG.md +21 -8
  2. package/admin/tabs/activity/tracking/ActivityTrackingModel.ts +22 -14
  3. package/admin/tabs/cluster/instances/logs/LogDisplayModel.ts +10 -14
  4. package/admin/tabs/cluster/instances/websocket/WebSocketColumns.ts +24 -2
  5. package/admin/tabs/cluster/instances/websocket/WebSocketModel.ts +76 -25
  6. package/admin/tabs/cluster/instances/websocket/WebSocketPanel.ts +2 -2
  7. package/appcontainer/AppStateModel.ts +3 -3
  8. package/build/types/admin/tabs/activity/tracking/ActivityTrackingModel.d.ts +1 -1
  9. package/build/types/admin/tabs/cluster/instances/websocket/WebSocketColumns.d.ts +4 -1
  10. package/build/types/admin/tabs/cluster/instances/websocket/WebSocketModel.d.ts +5 -2
  11. package/build/types/core/XH.d.ts +4 -4
  12. package/build/types/core/types/Interfaces.d.ts +2 -2
  13. package/build/types/security/Types.d.ts +0 -25
  14. package/build/types/security/msal/MsalClient.d.ts +40 -4
  15. package/build/types/svc/ClientHealthService.d.ts +19 -13
  16. package/build/types/svc/TrackService.d.ts +5 -1
  17. package/build/types/svc/WebSocketService.d.ts +39 -15
  18. package/core/XH.ts +6 -6
  19. package/core/types/Interfaces.ts +2 -2
  20. package/data/filter/BaseFilterFieldSpec.ts +6 -2
  21. package/desktop/appcontainer/AboutDialog.ts +14 -0
  22. package/desktop/cmp/button/AppMenuButton.ts +1 -1
  23. package/desktop/cmp/contextmenu/ContextMenu.ts +1 -1
  24. package/kit/onsen/theme.scss +5 -0
  25. package/mobile/appcontainer/AboutDialog.scss +1 -1
  26. package/mobile/appcontainer/AboutDialog.ts +24 -1
  27. package/mobile/cmp/menu/impl/Menu.ts +2 -2
  28. package/package.json +1 -1
  29. package/security/Types.ts +0 -27
  30. package/security/msal/MsalClient.ts +66 -14
  31. package/svc/ClientHealthService.ts +35 -21
  32. package/svc/TrackService.ts +8 -4
  33. package/svc/WebSocketService.ts +80 -33
  34. package/tsconfig.tsbuildinfo +1 -1
@@ -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 {Icon} from '@xh/hoist/icon';
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 = 'xhWebSocket?clientAppVersion=' + XH.appVersion;
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
- showTestMessageAlert(message) {
292
- XH.alert({
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
- maybeLogMessage(...args) {
300
- if (this.logMessages) this.logDebug(args);
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
+ }