diff --git a/lib/tradovate-class.ts b/lib/tradovate-class.ts index ca21f4e..f43cb27 100644 --- a/lib/tradovate-class.ts +++ b/lib/tradovate-class.ts @@ -36,6 +36,12 @@ export class TradovateClient { public fetchDaysComplete = false; /** NodeJS.Timeout handle for the hourly dailyPnL refresh */ private daysFetchInterval: ReturnType | null = null; + /** Interval handles tracked so they can be cleared on reconnect */ + private syncInterval: ReturnType | null = null; + private heartbeatInterval: ReturnType | null = null; + private tokenRenewalInterval: ReturnType | 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 = {}; /** 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 */ } }