// Service de surveillance en arrière-plan pour Node.js // Équivalent du service worker pour la détection des données manquantes import { Service } from './service'; import Database from './database.service'; import { config } from './config'; import { retrieveData } from './storage.service'; export class BackgroundSyncService { private static instance: BackgroundSyncService; private service: Service; private db: Database | null = null; private scanInterval: NodeJS.Timeout | null = null; private isRunning: boolean = false; private readonly SCAN_INTERVAL_MS = 30000; // 30 secondes private readonly EMPTY32BYTES = '0'.repeat(64); private constructor() { this.service = Service.getInstance(); console.log('🔧 BackgroundSyncService initialized'); } static async getInstance(): Promise { if (!BackgroundSyncService.instance) { BackgroundSyncService.instance = new BackgroundSyncService(); await BackgroundSyncService.instance.init(); } return BackgroundSyncService.instance; } private async init(): Promise { this.db = await Database.getInstance(); } /** * Démarre le service de surveillance en arrière-plan */ public async start(): Promise { if (this.isRunning) { console.log('⚠️ BackgroundSyncService already running'); return; } console.log('🚀 Starting background sync service...'); this.isRunning = true; // Scan immédiat au démarrage await this.scanMissingData(); // Puis scan périodique this.scanInterval = setInterval(async () => { try { await this.scanMissingData(); } catch (error) { console.error('❌ Error in background scan:', error); } }, this.SCAN_INTERVAL_MS); console.log('✅ Background sync service started'); } /** * Arrête le service de surveillance */ public stop(): void { if (!this.isRunning) { return; } console.log('🛑 Stopping background sync service...'); this.isRunning = false; if (this.scanInterval) { clearInterval(this.scanInterval); this.scanInterval = null; } console.log('✅ Background sync service stopped'); } /** * Scan manuel des données manquantes (équivalent à scanMissingData du service worker) */ public async scanMissingData(): Promise { console.log('🔍 Scanning for missing data...'); try { const myProcesses = await this.getMyProcesses(); if (!myProcesses || myProcesses.length === 0) { console.log('No processes to scan'); return []; } const toDownload = new Set(); for (const processId of myProcesses) { const process = await this.service.getProcess(processId); if (!process) continue; for (const state of process.states) { if (state.state_id === this.EMPTY32BYTES) continue; // Vérifier chaque pcd_commitment for (const [field, hash] of Object.entries(state.pcd_commitment)) { // Ignorer les champs publics if (state.public_data[field] !== undefined || field === 'roles') continue; // Vérifier que hash est une string if (typeof hash !== 'string') continue; // Vérifier si on a déjà les données const existingData = await this.getBlob(hash); if (!existingData) { toDownload.add(hash); // Ajouter une entrée diff si elle n'existe pas await this.addDiff(processId, state.state_id, hash, state.roles, field); } } } } const missingHashes = Array.from(toDownload); if (missingHashes.length > 0) { console.log(`📥 Found ${missingHashes.length} missing data hashes:`, missingHashes); await this.requestMissingData(missingHashes); } else { console.log('✅ No missing data found'); } return missingHashes; } catch (error) { console.error('❌ Error scanning missing data:', error); throw error; } } /** * Récupère les processus de l'utilisateur */ private async getMyProcesses(): Promise { try { return await this.service.getMyProcesses() || []; } catch (error) { console.error('Error getting my processes:', error); return []; } } /** * Vérifie si un blob existe en base */ private async getBlob(hash: string): Promise { try { return await this.service.getBufferFromDb(hash); } catch (error) { return null; } } /** * Ajoute une entrée diff pour tracking */ private async addDiff( processId: string, stateId: string, hash: string, roles: any, field: string ): Promise { try { if (!this.db) { console.error('Database not initialized'); return; } const existingDiff = await this.db.getObject('diffs', hash); if (!existingDiff) { const newDiff = { process_id: processId, state_id: stateId, value_commitment: hash, roles: roles, field: field, description: null, previous_value: null, new_value: null, notify_user: false, need_validation: false, validation_status: 'None' }; await this.db.addObject({ storeName: 'diffs', object: newDiff, key: hash }); console.log(`📝 Added diff entry for hash: ${hash}`); } } catch (error) { console.error('Error adding diff:', error); } } /** * Demande les données manquantes aux pairs */ private async requestMissingData(hashes: string[]): Promise { try { console.log('🔄 Requesting missing data from peers...'); // Récupérer tous les processus pour déterminer les rôles const myProcesses = await this.getMyProcesses(); const processesToRequest: Record = {}; for (const processId of myProcesses) { const process = await this.service.getProcess(processId); if (process) { processesToRequest[processId] = process; } } // Pour chaque hash manquant, essayer de le récupérer for (const hash of hashes) { try { // Trouver le diff correspondant const diffs = await this.service.getDiffsFromDb(); const diff = Object.values(diffs).find((d: any) => d.value_commitment === hash); if (diff) { const processId = diff.process_id; const stateId = diff.state_id; const roles = diff.roles; if (processesToRequest[processId]) { // D'abord essayer de récupérer depuis les serveurs de stockage const retrievedFromStorage = await this.tryRetrieveFromStorage(hash, roles); if (retrievedFromStorage) { console.log(`✅ Data retrieved from storage for hash ${hash}`); // Sauvegarder les données récupérées en base await this.service.saveBufferToDb(hash, Buffer.from(retrievedFromStorage)); continue; // Passer au hash suivant } // Si pas trouvé en storage, demander aux pairs console.log(`🔄 Requesting data for hash ${hash} from process ${processId}`); await this.service.requestDataFromPeers(processId, [stateId], [roles]); } } } catch (error) { console.error(`Error requesting data for hash ${hash}:`, error); } } } catch (error) { console.error('Error requesting missing data:', error); } } /** * Essaie de récupérer les données depuis les serveurs de stockage */ private async tryRetrieveFromStorage(hash: string, roles: any): Promise { try { // Extraire les URLs de stockage depuis les rôles const storageUrls = this.extractStorageUrls(roles); if (storageUrls.length === 0) { console.log(`No storage URLs found for hash ${hash}`); return null; } console.log(`🔍 Trying to retrieve hash ${hash} from storage servers:`, storageUrls); // Essayer de récupérer depuis les serveurs de stockage const data = await retrieveData(storageUrls, hash); if (data) { console.log(`✅ Successfully retrieved data for hash ${hash} from storage`); return data; } else { console.log(`❌ Data not found in storage for hash ${hash}`); return null; } } catch (error) { console.error(`Error retrieving data from storage for hash ${hash}:`, error); return null; } } /** * Extrait les URLs de stockage depuis les rôles */ private extractStorageUrls(roles: any): string[] { const storageUrls = new Set(); try { if (roles && typeof roles === 'object') { for (const role of Object.values(roles)) { if (role && typeof role === 'object' && 'storages' in role && Array.isArray((role as any).storages)) { for (const storageUrl of (role as any).storages) { if (typeof storageUrl === 'string' && storageUrl.trim()) { storageUrls.add(storageUrl.trim()); } } } } } } catch (error) { console.error('Error extracting storage URLs from roles:', error); } return Array.from(storageUrls); } /** * Force un scan immédiat (pour tests ou usage manuel) */ public async forceScan(): Promise { console.log('🔄 Forcing immediate scan...'); return await this.scanMissingData(); } /** * Obtient le statut du service */ public getStatus(): { isRunning: boolean; scanInterval: number } { return { isRunning: this.isRunning, scanInterval: this.SCAN_INTERVAL_MS }; } /** * Met à jour l'intervalle de scan */ public setScanInterval(intervalMs: number): void { if (this.isRunning && this.scanInterval) { clearInterval(this.scanInterval); this.scanInterval = setInterval(async () => { try { await this.scanMissingData(); } catch (error) { console.error('❌ Error in background scan:', error); } }, intervalMs); } } }