Throttle request report refreshes and add retries

This commit is contained in:
Brandon Li
2026-03-21 04:30:36 -05:00
parent 4252adfbea
commit c54073e4b8
+56 -13
View File
@@ -40,8 +40,11 @@ export class TradovateClient {
/** True once fetchDaysTraded() has finished its last full run */ /** True once fetchDaysTraded() has finished its last full run */
public fetchDaysComplete = false; public fetchDaysComplete = false;
/** NodeJS.Timeout handle for the hourly dailyPnL refresh */ /** Prevent overlapping report fetches and throttle them to hourly cadence with retry backoff. */
private daysFetchInterval: ReturnType<typeof setInterval> | null = null; private daysFetchInFlight = false;
private lastDaysFetchStartedAt = 0;
private lastDaysFetchSucceededAt = 0;
private nextDaysFetchRetryAt = 0;
/** Interval handles tracked so they can be cleared on reconnect */ /** Interval handles tracked so they can be cleared on reconnect */
private syncInterval: ReturnType<typeof setInterval> | null = null; private syncInterval: ReturnType<typeof setInterval> | null = null;
private heartbeatInterval: ReturnType<typeof setInterval> | null = null; private heartbeatInterval: ReturnType<typeof setInterval> | null = null;
@@ -315,14 +318,7 @@ export class TradovateClient {
console.log(`[requestSync] ${this.accountList.length} account(s), ${Object.keys(this.accountCashBalances).length} balance(s)`); console.log(`[requestSync] ${this.accountList.length} account(s), ${Object.keys(this.accountCashBalances).length} balance(s)`);
this.fetchDaysTraded(); this.maybeFetchDaysTraded();
// Refresh daily P&L once per hour — avoids hammering the reports API
if (this.daysFetchInterval) clearInterval(this.daysFetchInterval);
this.daysFetchInterval = setInterval(
() => this.fetchDaysTraded().catch((err) => console.error('[hourly fetchDaysTraded]', err)),
60 * 60 * 1_000
);
if (this.products.length > 0) { if (this.products.length > 0) {
this.syncComplete = true; this.syncComplete = true;
@@ -347,9 +343,53 @@ export class TradovateClient {
this.ws.send('user/syncrequest\n3\n\n{"splitResponses":false}'); this.ws.send('user/syncrequest\n3\n\n{"splitResponses":false}');
} }
public async fetchDaysTraded(): Promise<void> { private maybeFetchDaysTraded(): void {
if (!this.accessInfo?.accessToken) return; if (this.daysFetchInFlight) return;
const now = Date.now();
const initialFetchDue = this.lastDaysFetchStartedAt === 0;
const retryPending = this.nextDaysFetchRetryAt > 0;
const retryDue = retryPending && now >= this.nextDaysFetchRetryAt;
const hourlyRefreshDue =
!retryPending &&
this.lastDaysFetchSucceededAt > 0 &&
now - this.lastDaysFetchSucceededAt >= 60 * 60 * 1_000;
if (!initialFetchDue && !retryDue && !hourlyRefreshDue) return;
const reason = retryDue
? 'retry after failure'
: initialFetchDue
? 'initial sync'
: 'hourly refresh';
this.daysFetchInFlight = true;
this.lastDaysFetchStartedAt = now;
if (retryDue) this.nextDaysFetchRetryAt = 0;
this.fetchDaysTraded()
.then(({ failedAccounts }) => {
if (failedAccounts > 0) {
this.nextDaysFetchRetryAt = Date.now() + 5 * 60 * 1_000;
console.warn(`[fetchDaysTraded] ${failedAccounts} account(s) failed during ${reason} — retrying in 5 minutes`);
return;
}
this.lastDaysFetchSucceededAt = Date.now();
})
.catch((err) => {
this.nextDaysFetchRetryAt = Date.now() + 5 * 60 * 1_000;
console.error(`[fetchDaysTraded] ${reason} failed — retrying in 5 minutes`, err);
})
.finally(() => {
this.daysFetchInFlight = false;
});
}
public async fetchDaysTraded(): Promise<{ failedAccounts: number }> {
if (!this.accessInfo?.accessToken) return { failedAccounts: 0 };
this.fetchDaysComplete = false; this.fetchDaysComplete = false;
let failedAccounts = 0;
const authHeaders = { Authorization: `Bearer ${this.accessInfo.accessToken}` }; const authHeaders = { Authorization: `Bearer ${this.accessInfo.accessToken}` };
@@ -533,6 +573,7 @@ export class TradovateClient {
} catch (err) { } catch (err) {
const msg = err instanceof Error ? err.message : String(err); const msg = err instanceof Error ? err.message : String(err);
console.error(`[fetchDaysTraded] both reports failed for ${account.name}:`, msg); console.error(`[fetchDaysTraded] both reports failed for ${account.name}:`, msg);
failedAccounts++;
// Cache already loaded and filtered at top of loop — just log the error // Cache already loaded and filtered at top of loop — just log the error
if (cached.length > 0) { if (cached.length > 0) {
this.lastFetchErrors[account.name] = `${msg} (using ${cached.length} cached entries from database)`; this.lastFetchErrors[account.name] = `${msg} (using ${cached.length} cached entries from database)`;
@@ -545,6 +586,7 @@ export class TradovateClient {
} }
} }
this.fetchDaysComplete = true; this.fetchDaysComplete = true;
return { failedAccounts };
} }
private async login(): Promise<AuthLoginResponse> { private async login(): Promise<AuthLoginResponse> {
@@ -798,7 +840,8 @@ export class TradovateClient {
if (this.syncInterval) { clearInterval(this.syncInterval); this.syncInterval = null; } if (this.syncInterval) { clearInterval(this.syncInterval); this.syncInterval = null; }
if (this.heartbeatInterval) { clearInterval(this.heartbeatInterval); this.heartbeatInterval = null; } if (this.heartbeatInterval) { clearInterval(this.heartbeatInterval); this.heartbeatInterval = null; }
if (this.tokenRenewalInterval) { clearInterval(this.tokenRenewalInterval); this.tokenRenewalInterval = null; } if (this.tokenRenewalInterval) { clearInterval(this.tokenRenewalInterval); this.tokenRenewalInterval = null; }
if (this.daysFetchInterval) { clearInterval(this.daysFetchInterval); this.daysFetchInterval = null; } this.daysFetchInFlight = false;
this.nextDaysFetchRetryAt = 0;
try { this.ws?.close(); } catch { /* ignore */ } try { this.ws?.close(); } catch { /* ignore */ }
} }