130 lines
3.9 KiB
TypeScript
130 lines
3.9 KiB
TypeScript
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<boolean> {
|
|
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<void> {
|
|
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;
|
|
}
|
|
}
|
|
}
|