@xh/hoist 74.0.0-SNAPSHOT.1748890930922 → 74.0.0-SNAPSHOT.1749036244810

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 CHANGED
@@ -14,6 +14,8 @@
14
14
  ### 🐞 Bug Fixes
15
15
  * Improved `ViewManagerModel.settleTime` by delegating to individual `PersistenceProviders`.
16
16
  * Fixed bug where grid column state could become unintentionally dirty when columns were hidden.
17
+ * Improved `WebsocketService` heartbeat detection to auto-reconnect when the socket reports as open
18
+ and heartbeats can be sent, but no heartbeat acknowledgements are being received from the server.
17
19
 
18
20
  ## v73.0.1 - 2025-05-19
19
21
 
@@ -45,6 +45,8 @@ export declare class WebSocketService extends HoistService {
45
45
  private _timer;
46
46
  private _socket;
47
47
  private _subsByTopic;
48
+ private _lastHeartbeatSent;
49
+ private _lastHeartbeatReceived;
48
50
  constructor();
49
51
  initAsync(): Promise<void>;
50
52
  /**
@@ -70,7 +72,9 @@ export declare class WebSocketService extends HoistService {
70
72
  getFormattedTelemetry(): PlainObject;
71
73
  private connect;
72
74
  private disconnect;
75
+ private reconnect;
73
76
  private heartbeatOrReconnect;
77
+ private get heartbeatWasUnacknowledged();
74
78
  private onServerInstanceChange;
75
79
  onOpen(ev: any): void;
76
80
  onClose(ev: any): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xh/hoist",
3
- "version": "74.0.0-SNAPSHOT.1748890930922",
3
+ "version": "74.0.0-SNAPSHOT.1749036244810",
4
4
  "description": "Hoist add-on for building and deploying React Applications.",
5
5
  "repository": "github:xh/hoist-react",
6
6
  "homepage": "https://xh.io",
@@ -70,6 +70,9 @@ export class WebSocketService extends HoistService {
70
70
  private _socket: WebSocket;
71
71
  private _subsByTopic: Record<string, WebSocketSubscription[]> = {};
72
72
 
73
+ private _lastHeartbeatSent: number = null;
74
+ private _lastHeartbeatReceived: number = null;
75
+
73
76
  constructor() {
74
77
  super();
75
78
  makeObservable(this);
@@ -177,6 +180,10 @@ export class WebSocketService extends HoistService {
177
180
  if (s === this._socket) this.onMessage(data);
178
181
  };
179
182
  this._socket = s;
183
+
184
+ // Reset heartbeat tracking - any prior values no longer relevant.
185
+ this._lastHeartbeatReceived = null;
186
+ this._lastHeartbeatSent = null;
180
187
  } catch (e) {
181
188
  this.logError('Failure creating WebSocket', e);
182
189
  }
@@ -192,24 +199,41 @@ export class WebSocketService extends HoistService {
192
199
  this.updateConnectedStatus();
193
200
  }
194
201
 
202
+ private reconnect() {
203
+ this.disconnect();
204
+ this.connect();
205
+ }
206
+
195
207
  private heartbeatOrReconnect() {
196
208
  this.updateConnectedStatus();
197
- if (this.connected) {
198
- this.sendMessage({topic: this.HEARTBEAT_TOPIC, data: 'ping'});
199
- this.noteTelemetryEvent('heartbeatSent');
200
- } else {
201
- this.logWarn('Heartbeat found websocket not connected - attempting to reconnect...');
209
+
210
+ // If there is a problem, attempt to reconnect and come back on the next cycle.
211
+ const {connected, heartbeatWasUnacknowledged} = this;
212
+ if (!connected || heartbeatWasUnacknowledged) {
213
+ this.logWarn(
214
+ `Heartbeat found ${!connected ? 'websocket not connected' : 'last heartbeat not acknowledged'} - attempting to reconnect...`
215
+ );
202
216
  this.noteTelemetryEvent('heartbeatReconnectAttempt');
203
- this.disconnect();
204
- this.connect();
217
+ this.reconnect();
218
+ return;
205
219
  }
220
+
221
+ // If all looks OK, send a heartbeat message.
222
+ this.sendMessage({topic: this.HEARTBEAT_TOPIC, data: 'ping'});
223
+ this.noteTelemetryEvent('heartbeatSent');
224
+ this._lastHeartbeatSent = Date.now();
225
+ }
226
+
227
+ // We expect the server to respond immediately to every heartbeat. There will be a tiny window
228
+ // while the message is round-tripping, but that's not material to our check on HEARTBEAT_INTERVAL.
229
+ private get heartbeatWasUnacknowledged() {
230
+ return this._lastHeartbeatSent > this._lastHeartbeatReceived;
206
231
  }
207
232
 
208
233
  private onServerInstanceChange() {
209
234
  this.logWarn('Server instance changed - attempting to connect to new instance.');
210
235
  this.noteTelemetryEvent('instanceChangeReconnectAttempt');
211
- this.disconnect();
212
- this.connect();
236
+ this.reconnect();
213
237
  }
214
238
 
215
239
  //------------------------
@@ -256,6 +280,7 @@ export class WebSocketService extends HoistService {
256
280
  XH.clientHealthService.sendReportAsync();
257
281
  break;
258
282
  case this.HEARTBEAT_TOPIC:
283
+ this._lastHeartbeatReceived = Date.now();
259
284
  this.noteTelemetryEvent('heartbeatReceived');
260
285
  break;
261
286
  }