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;
|
public fetchDaysComplete = false;
|
||||||
/** NodeJS.Timeout handle for the hourly dailyPnL refresh */
|
/** NodeJS.Timeout handle for the hourly dailyPnL refresh */
|
||||||
private daysFetchInterval: ReturnType<typeof setInterval> | null = null;
|
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() */
|
/** Last error per account name from fetchDaysTraded() */
|
||||||
public lastFetchErrors: Record<string, string> = {};
|
public lastFetchErrors: Record<string, string> = {};
|
||||||
/** Raw reports API response sample per account (first 100 chars) for debugging */
|
/** Raw reports API response sample per account (first 100 chars) for debugging */
|
||||||
@@ -87,7 +93,10 @@ export class TradovateClient {
|
|||||||
this.name = name;
|
this.name = name;
|
||||||
this.password = password;
|
this.password = password;
|
||||||
this.callbackOnSyncRequest = callbackOnSyncRequest;
|
this.callbackOnSyncRequest = callbackOnSyncRequest;
|
||||||
|
this.connectAndAuth();
|
||||||
|
}
|
||||||
|
|
||||||
|
private connectAndAuth(): void {
|
||||||
this.login().then((res) => {
|
this.login().then((res) => {
|
||||||
if (!res?.accessToken) {
|
if (!res?.accessToken) {
|
||||||
console.log('Failed to login', res);
|
console.log('Failed to login', res);
|
||||||
@@ -95,19 +104,25 @@ export class TradovateClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log('Logged in', res);
|
console.log('Logged in', res);
|
||||||
|
this.accessInfo = res;
|
||||||
|
|
||||||
// check every 2 minutes if the access token is expired
|
// Clear and restart token renewal interval
|
||||||
setInterval(() => {
|
if (this.tokenRenewalInterval) clearInterval(this.tokenRenewalInterval);
|
||||||
// If we are within 15 minutes of the expiration time, renew the access token
|
this.tokenRenewalInterval = setInterval(() => {
|
||||||
if (
|
if (
|
||||||
new Date(res.expirationTime).getTime() <
|
new Date(this.accessInfo.expirationTime).getTime() <
|
||||||
new Date().getTime() + 15 * 60 * 1000
|
new Date().getTime() + 15 * 60 * 1000
|
||||||
) {
|
) {
|
||||||
this.renewAccessToken();
|
this.renewAccessToken();
|
||||||
}
|
}
|
||||||
}, 2 * 60 * 1000);
|
}, 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);
|
const randomnumber = Math.random().toString(36).substring(2, 15);
|
||||||
this.ws = new WebSocket(`wss://demo.tradovateapi.com/v1/websocket?r=${randomnumber}`);
|
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.ws.send('authorize\n2\n\n' + this.accessInfo.accessToken);
|
||||||
this.directEventCallbacks[2] = (response: any) => {
|
this.directEventCallbacks[2] = (response: any) => {
|
||||||
this.requestSync();
|
this.requestSync();
|
||||||
setInterval(() => this.requestSync(), 60000);
|
this.syncInterval = setInterval(() => this.requestSync(), 60000);
|
||||||
|
|
||||||
// Every 2.5 seconds send a heartbeat
|
// Every 2.5 seconds send a heartbeat
|
||||||
setInterval(() => {
|
this.heartbeatInterval = setInterval(() => {
|
||||||
this.ws.send('[]');
|
this.ws.send('[]');
|
||||||
}, 2500);
|
}, 2500);
|
||||||
};
|
};
|
||||||
@@ -178,8 +193,6 @@ export class TradovateClient {
|
|||||||
if (this.recentEntityEvents.length > 50) this.recentEntityEvents.shift();
|
if (this.recentEntityEvents.length > 50) this.recentEntityEvents.shift();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Update positions from WebSocket position events
|
// Update positions from WebSocket position events
|
||||||
if (response.d?.entityType === 'position' && response.d?.entity) {
|
if (response.d?.entityType === 'position' && response.d?.entity) {
|
||||||
const pos = response.d.entity;
|
const pos = response.d.entity;
|
||||||
@@ -215,6 +228,14 @@ export class TradovateClient {
|
|||||||
this.ws.onerror = (event) => {
|
this.ws.onerror = (event) => {
|
||||||
console.error('Error on websocket', 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. */
|
/** Close the WebSocket connection and stop all intervals. Call before discarding the instance. */
|
||||||
public disconnect(): void {
|
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 */ }
|
try { this.ws?.close(); } catch { /* ignore */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user