Auto-reconnect on WebSocket disconnect

- Extract connection logic into connectAndAuth() so it can be called
  on both initial connect and reconnect
- Add ws.onclose handler: if not an intentional disconnect, clear stale
  intervals and retry connectAndAuth() after 5 seconds
- Track sync, heartbeat, and tokenRenewal interval handles so they are
  cleared and recreated cleanly on each reconnect
- Reset syncComplete = false on reconnect so the scheduler waits for
  a fresh sync before trading
- disconnect() sets intentionalDisconnect = true and clears all intervals
  to prevent reconnect loops when a client is removed

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Senofy
2026-03-12 23:21:03 -05:00
co-authored by Claude Sonnet 4.6
parent d0065ab047
commit 15ef2d7adc
+35 -9
View File
@@ -36,6 +36,12 @@ export class TradovateClient {
public fetchDaysComplete = false;
/** NodeJS.Timeout handle for the hourly dailyPnL refresh */
private daysFetchInterval: ReturnType<typeof setInterval> | null = null;
/** Interval handles tracked so they can be cleared on reconnect */
private syncInterval: ReturnType<typeof setInterval> | null = null;
private heartbeatInterval: ReturnType<typeof setInterval> | null = null;
private tokenRenewalInterval: ReturnType<typeof setInterval> | null = null;
/** Set to true by disconnect() to suppress reconnect on close */
private intentionalDisconnect = false;
/** Last error per account name from fetchDaysTraded() */
public lastFetchErrors: Record<string, string> = {};
/** Raw reports API response sample per account (first 100 chars) for debugging */
@@ -87,7 +93,10 @@ export class TradovateClient {
this.name = name;
this.password = password;
this.callbackOnSyncRequest = callbackOnSyncRequest;
this.connectAndAuth();
}
private connectAndAuth(): void {
this.login().then((res) => {
if (!res?.accessToken) {
console.log('Failed to login', res);
@@ -95,19 +104,25 @@ export class TradovateClient {
}
console.log('Logged in', res);
this.accessInfo = res;
// check every 2 minutes if the access token is expired
setInterval(() => {
// If we are within 15 minutes of the expiration time, renew the access token
// Clear and restart token renewal interval
if (this.tokenRenewalInterval) clearInterval(this.tokenRenewalInterval);
this.tokenRenewalInterval = setInterval(() => {
if (
new Date(res.expirationTime).getTime() <
new Date(this.accessInfo.expirationTime).getTime() <
new Date().getTime() + 15 * 60 * 1000
) {
this.renewAccessToken();
}
}, 2 * 60 * 1000);
this.accessInfo = res;
// Clear stale WS intervals before opening a new connection
if (this.syncInterval) { clearInterval(this.syncInterval); this.syncInterval = null; }
if (this.heartbeatInterval) { clearInterval(this.heartbeatInterval); this.heartbeatInterval = null; }
// Reset sync state so scheduler waits for the new sync to complete
this.syncComplete = false;
const randomnumber = Math.random().toString(36).substring(2, 15);
this.ws = new WebSocket(`wss://demo.tradovateapi.com/v1/websocket?r=${randomnumber}`);
@@ -117,10 +132,10 @@ export class TradovateClient {
this.ws.send('authorize\n2\n\n' + this.accessInfo.accessToken);
this.directEventCallbacks[2] = (response: any) => {
this.requestSync();
setInterval(() => this.requestSync(), 60000);
this.syncInterval = setInterval(() => this.requestSync(), 60000);
// Every 2.5 seconds send a heartbeat
setInterval(() => {
this.heartbeatInterval = setInterval(() => {
this.ws.send('[]');
}, 2500);
};
@@ -178,8 +193,6 @@ export class TradovateClient {
if (this.recentEntityEvents.length > 50) this.recentEntityEvents.shift();
}
// Update positions from WebSocket position events
if (response.d?.entityType === 'position' && response.d?.entity) {
const pos = response.d.entity;
@@ -215,6 +228,14 @@ export class TradovateClient {
this.ws.onerror = (event) => {
console.error('Error on websocket', event);
};
this.ws.onclose = (event) => {
if (this.intentionalDisconnect) return;
console.warn(`[tradovate] WebSocket closed (code=${event.code}) — reconnecting in 5s`);
if (this.syncInterval) { clearInterval(this.syncInterval); this.syncInterval = null; }
if (this.heartbeatInterval) { clearInterval(this.heartbeatInterval); this.heartbeatInterval = null; }
setTimeout(() => this.connectAndAuth(), 5_000);
};
});
}
@@ -700,6 +721,11 @@ export class TradovateClient {
/** Close the WebSocket connection and stop all intervals. Call before discarding the instance. */
public disconnect(): void {
this.intentionalDisconnect = true;
if (this.syncInterval) { clearInterval(this.syncInterval); this.syncInterval = null; }
if (this.heartbeatInterval) { clearInterval(this.heartbeatInterval); this.heartbeatInterval = null; }
if (this.tokenRenewalInterval) { clearInterval(this.tokenRenewalInterval); this.tokenRenewalInterval = null; }
if (this.daysFetchInterval) { clearInterval(this.daysFetchInterval); this.daysFetchInterval = null; }
try { this.ws?.close(); } catch { /* ignore */ }
}