import type { Logger } from '../utils/logger.js'; import type { ChatService } from './chat.js'; import { denyAccountAuth, type AccountAuthGate } from './account-auth-context.js'; export interface MediaSyncStatus { isSyncing: boolean; lastSyncAt: number | null; lastSyncDurationMs: number | null; lastSyncChats: number | null; lastError: string | null; nextSyncAt: number | null; } export class MediaSyncService { private syncing = false; private lastSyncAt: number | null = null; private lastSyncDurationMs: number | null = null; private lastSyncChats: number | null = null; private lastError: string | null = null; private intervalHandle: NodeJS.Timeout | null = null; private readonly intervalMs: number; constructor( private chatService: ChatService, private logger: Logger, intervalMs: number = 30 * 60 * 1000, // default 30 minutes private accountAuth: AccountAuthGate = denyAccountAuth, ) { this.intervalMs = intervalMs; } /** * Start periodic background syncing. */ start(): void { if (this.intervalHandle) return; this.logger.info(`Media sync: starting background sync every ${Math.round(this.intervalMs / 60000)} min`); // Run first sync after a short delay to let the app settle const initialDelay = setTimeout(() => this.runSync(), 15000); this.intervalHandle = setInterval(() => this.runSync(), this.intervalMs); // Ensure the initial timeout is cleaned up if stop() is called early const origStop = this.stop.bind(this); this.stop = () => { clearTimeout(initialDelay); origStop(); }; } /** * Stop periodic syncing. */ stop(): void { if (this.intervalHandle) { clearInterval(this.intervalHandle); this.intervalHandle = null; this.logger.info('Media sync: background sync stopped'); } } /** * Trigger a sync manually (e.g. from the dashboard). * Returns immediately if a sync is already in progress. */ async triggerSync(full = false): Promise { if (!this.accountAuth.isFullyAuthenticated()) { this.logger.warn('Media sync: blocked until the account is fully authenticated'); return false; } if (this.syncing) { this.logger.info('Media sync: sync already in progress, skipping trigger'); return false; } await this.runSync(full); return true; } getStatus(): MediaSyncStatus { return { isSyncing: this.syncing, lastSyncAt: this.lastSyncAt, lastSyncDurationMs: this.lastSyncDurationMs, lastSyncChats: this.lastSyncChats, lastError: this.lastError, nextSyncAt: this.intervalHandle && this.lastSyncAt ? this.lastSyncAt + this.intervalMs : null, }; } private async runSync(full = false): Promise { if (!this.accountAuth.isFullyAuthenticated()) { this.logger.debug('Media sync: skipped because the account is not fully authenticated'); return; } if (this.syncing) return; this.syncing = true; this.lastError = null; const start = Date.now(); try { if (full) { this.logger.info('Media sync: running full sync (all conversations)...'); const chats = await this.chatService.fetchAllMessages(); this.lastSyncChats = chats.length; } else { this.logger.info('Media sync: running incremental sync (top 10 chats)...'); await this.chatService.refreshTopChats(10); this.lastSyncChats = 10; } this.lastSyncAt = Date.now(); this.lastSyncDurationMs = Date.now() - start; this.logger.info( `Media sync: completed in ${Math.round(this.lastSyncDurationMs / 1000)}s` + ` (${this.lastSyncChats} chats)` ); } catch (error) { this.lastError = error instanceof Error ? error.message : String(error); this.lastSyncDurationMs = Date.now() - start; this.logger.error('Media sync: failed:', error); } finally { this.syncing = false; } } }