// Archivage automatique des sessions terminées (P10). Scheduler calqué sur DiscoveryService : // start()/stop() + setInterval().unref(). Soft-archive uniquement (jamais de DELETE dans cette // itération ; la purge définitive est conçue mais désactivée par défaut, cf. retention-settings). // Démarré dans runDaemon (jamais dans buildApp → ne tourne pas pendant les tests unitaires). import { EventEmitter } from 'node:events'; import { archiveExpiredSessions, type Db } from '../db/index.js'; import { recordAudit } from './audit-log.js'; import { readRetentionDays } from './retention-settings.js'; /** Balayage toutes les 6 h : la rétention se compte en jours, pas besoin de plus fréquent. */ const DEFAULT_SWEEP_INTERVAL_MS = 6 * 60 * 60 * 1000; const MS_PER_DAY = 24 * 60 * 60 * 1000; export interface SessionArchiveEvents { /** une session vient d'être archivée (relayée en `session_archived` par la gateway). */ session_archived: [{ id: string; archivedAt: string }]; } export interface SessionArchiveOptions { db: Db; intervalMs?: number; } export class SessionArchiveService extends EventEmitter { private readonly db: Db; private readonly intervalMs: number; private timer: NodeJS.Timeout | null = null; constructor(opts: SessionArchiveOptions) { super(); this.db = opts.db; this.intervalMs = opts.intervalMs ?? DEFAULT_SWEEP_INTERVAL_MS; } start(): void { if (this.timer) return; this.sweep(); // passage immédiat au boot (synchrone, bon marché : un seul UPDATE borné) this.timer = setInterval(() => this.sweep(), this.intervalMs); this.timer.unref(); // ne maintient pas le process en vie } stop(): void { if (this.timer) { clearInterval(this.timer); this.timer = null; } } /** * Archive les sessions terminées plus anciennes que la rétention configurée. Retourne le nombre * archivé. Tolérant : ne lève jamais (une erreur ne doit pas tuer le scheduler). `0 jour` = OFF. */ sweep(): number { try { const days = readRetentionDays(this.db); if (days <= 0) return 0; // rétention désactivée const now = new Date(); const archivedAt = now.toISOString(); const cutoffIso = new Date(now.getTime() - days * MS_PER_DAY).toISOString(); const ids = archiveExpiredSessions(this.db, archivedAt, cutoffIso); for (const id of ids) { this.emit('session_archived', { id, archivedAt }); recordAudit(this.db, { actor: 'system', action: 'session.autoArchive', resourceId: id }); } return ids.length; } catch { return 0; } } /** Diffuse l'archivage d'une session (déjà archivée + auditée par l'appelant, ex. route manuelle). */ emitArchived(id: string, archivedAt: string): void { this.emit('session_archived', { id, archivedAt }); } }