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 */
|
/** 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 */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user