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:
co-authored by
Claude Sonnet 4.6
parent
d0065ab047
commit
15ef2d7adc
+35
-9
@@ -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 */ }
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user