- Trigger immediate quota poll when main window gains focus. - Add 10s debounce to prevent API spam on frequent window switching. - Add concurrency guard to prevent overlapping poll requests. - Reset background polling interval after manual refresh. - Add unit tests for polling, debounce, and concurrency logic.
133 lines
4.3 KiB
TypeScript
133 lines
4.3 KiB
TypeScript
import { CloudAccountRepo } from '../ipc/database/cloudHandler';
|
|
import { GoogleAPIService } from './GoogleAPIService';
|
|
import { AutoSwitchService } from './AutoSwitchService';
|
|
import { logger } from '../utils/logger';
|
|
|
|
export class CloudMonitorService {
|
|
private static intervalId: NodeJS.Timeout | null = null;
|
|
private static POLL_INTERVAL = 1000 * 60 * 5; // 5 minutes
|
|
private static DEBOUNCE_TIME = 10000; // 10 seconds
|
|
private static lastFocusTime: number = 0;
|
|
private static isPolling: boolean = false;
|
|
|
|
// Helper for testing
|
|
static resetStateForTesting() {
|
|
this.lastFocusTime = 0;
|
|
this.isPolling = false;
|
|
this.stop();
|
|
}
|
|
|
|
static start() {
|
|
if (this.intervalId) return;
|
|
logger.info('Starting CloudMonitorService...');
|
|
|
|
// Set lastFocusTime to now to prevent "double-dip" on startup (focus event immediately after start)
|
|
this.lastFocusTime = Date.now();
|
|
|
|
// Initial Poll
|
|
this.poll().catch((e) => logger.error('Initial poll failed', e));
|
|
|
|
this.startInterval();
|
|
}
|
|
|
|
static stop() {
|
|
if (this.intervalId) {
|
|
clearInterval(this.intervalId);
|
|
this.intervalId = null;
|
|
logger.info('Stopped CloudMonitorService');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called when the application window gains focus.
|
|
* Triggers an immediate poll if not rate-limited by debounce.
|
|
*/
|
|
static async handleAppFocus() {
|
|
const now = Date.now();
|
|
|
|
// 1. Concurrency Guard: If we are already polling, don't pile up requests
|
|
if (this.isPolling) {
|
|
logger.info('Monitor: App focused, but polling is already in progress. Skipping.');
|
|
return;
|
|
}
|
|
|
|
// 2. Debounce: If we focused recently, don't poll again
|
|
if (now - this.lastFocusTime < this.DEBOUNCE_TIME) {
|
|
logger.info('Monitor: App focused, skipping poll (debounce active).');
|
|
return;
|
|
}
|
|
|
|
logger.info('Monitor: App focused, triggering immediate poll...');
|
|
this.lastFocusTime = now;
|
|
|
|
// 3. Trigger Poll
|
|
await this.poll().catch((e) => {
|
|
logger.error('Monitor: Focus poll failed', e);
|
|
});
|
|
// 4. Reset the background interval so we don't double-poll shortly after
|
|
this.resetInterval();
|
|
}
|
|
|
|
private static startInterval() {
|
|
if (this.intervalId) {
|
|
clearInterval(this.intervalId);
|
|
}
|
|
this.intervalId = setInterval(() => {
|
|
this.poll().catch((e) => logger.error('Scheduled poll failed', e));
|
|
}, this.POLL_INTERVAL);
|
|
}
|
|
|
|
private static resetInterval() {
|
|
if (this.intervalId) {
|
|
clearInterval(this.intervalId);
|
|
this.startInterval(); // Restart the 5-minute timer
|
|
}
|
|
}
|
|
|
|
static async poll() {
|
|
if (this.isPolling) {
|
|
return; // Extra safety
|
|
}
|
|
this.isPolling = true;
|
|
|
|
try {
|
|
logger.info('CloudMonitor: Polling quotas...');
|
|
const accounts = await CloudAccountRepo.getAccounts();
|
|
const now = Math.floor(Date.now() / 1000);
|
|
|
|
for (const account of accounts) {
|
|
try {
|
|
// 1. Check/Refresh Token if needed (give it a 10 min buffer here for safety)
|
|
let accessToken = account.token.access_token;
|
|
if (account.token.expiry_timestamp < now + 600) {
|
|
logger.info(`Monitor: Refreshing token for ${account.email}`);
|
|
const newToken = await GoogleAPIService.refreshAccessToken(account.token.refresh_token);
|
|
account.token.access_token = newToken.access_token;
|
|
account.token.expires_in = newToken.expires_in;
|
|
account.token.expiry_timestamp = now + newToken.expires_in;
|
|
await CloudAccountRepo.updateToken(account.id, account.token);
|
|
accessToken = newToken.access_token;
|
|
}
|
|
|
|
// 2. Fetch Quota
|
|
// We delay slightly between requests to act human/avoid spike
|
|
await new Promise((r) => setTimeout(r, 1000));
|
|
const quota = await GoogleAPIService.fetchQuota(accessToken);
|
|
|
|
// 3. Update DB
|
|
await CloudAccountRepo.updateQuota(account.id, quota);
|
|
await CloudAccountRepo.updateLastUsed(account.id);
|
|
} catch (error) {
|
|
logger.error(`Monitor: Failed to update ${account.email}`, error);
|
|
// Could mark status as 'error' or 'rate_limited' if 429
|
|
}
|
|
}
|
|
|
|
// 4. Check for Auto-Switch
|
|
await AutoSwitchService.checkAndSwitchIfNeeded();
|
|
} finally {
|
|
this.isPolling = false;
|
|
}
|
|
}
|
|
}
|