Files
RedditBot/src/services/media-sync.ts
T

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;
}
}
}