Throttle request report refreshes and add retries
This commit is contained in:
+56
-13
@@ -40,8 +40,11 @@ export class TradovateClient {
|
||||
|
||||
/** True once fetchDaysTraded() has finished its last full run */
|
||||
public fetchDaysComplete = false;
|
||||
/** NodeJS.Timeout handle for the hourly dailyPnL refresh */
|
||||
private daysFetchInterval: ReturnType<typeof setInterval> | null = null;
|
||||
/** Prevent overlapping report fetches and throttle them to hourly cadence with retry backoff. */
|
||||
private daysFetchInFlight = false;
|
||||
private lastDaysFetchStartedAt = 0;
|
||||
private lastDaysFetchSucceededAt = 0;
|
||||
private nextDaysFetchRetryAt = 0;
|
||||
/** Interval handles tracked so they can be cleared on reconnect */
|
||||
private syncInterval: 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)`);
|
||||
|
||||
this.fetchDaysTraded();
|
||||
|
||||
// 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
|
||||
);
|
||||
this.maybeFetchDaysTraded();
|
||||
|
||||
if (this.products.length > 0) {
|
||||
this.syncComplete = true;
|
||||
@@ -347,9 +343,53 @@ export class TradovateClient {
|
||||
this.ws.send('user/syncrequest\n3\n\n{"splitResponses":false}');
|
||||
}
|
||||
|
||||
public async fetchDaysTraded(): Promise<void> {
|
||||
if (!this.accessInfo?.accessToken) return;
|
||||
private maybeFetchDaysTraded(): void {
|
||||
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;
|
||||
let failedAccounts = 0;
|
||||
|
||||
const authHeaders = { Authorization: `Bearer ${this.accessInfo.accessToken}` };
|
||||
|
||||
@@ -533,6 +573,7 @@ export class TradovateClient {
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
console.error(`[fetchDaysTraded] both reports failed for ${account.name}:`, msg);
|
||||
failedAccounts++;
|
||||
// Cache already loaded and filtered at top of loop — just log the error
|
||||
if (cached.length > 0) {
|
||||
this.lastFetchErrors[account.name] = `${msg} (using ${cached.length} cached entries from database)`;
|
||||
@@ -545,6 +586,7 @@ export class TradovateClient {
|
||||
}
|
||||
}
|
||||
this.fetchDaysComplete = true;
|
||||
return { failedAccounts };
|
||||
}
|
||||
|
||||
private async login(): Promise<AuthLoginResponse> {
|
||||
@@ -798,7 +840,8 @@ export class TradovateClient {
|
||||
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; }
|
||||
this.daysFetchInFlight = false;
|
||||
this.nextDaysFetchRetryAt = 0;
|
||||
try { this.ws?.close(); } catch { /* ignore */ }
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user