diff --git a/src/main.ts b/src/main.ts
index 6161d68..c833ca7 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,4 +1,3 @@
-import Database from './services/database.service';
import Services from './services/service';
import { Router } from './router/index';
import './components/header/Header';
@@ -6,53 +5,58 @@ import './App';
import { IframeController } from './services/iframe-controller.service';
async function bootstrap() {
- console.log("🚀 Démarrage de l'application 4NK...");
+ console.log("🚀 Démarrage de l'application 4NK (Multi-Worker Architecture)...");
try {
- // 1. Initialisation des Services (WASM, Sockets, Database...)
+ // 1. Initialisation des Services (Proxy vers Core & Network Workers)
+ // Cela va lancer les workers en arrière-plan
const services = await Services.getInstance();
- // 2. Initialisation de la base de données (Web Worker + Service Worker)
- await Database.getInstance();
+ // ❌ SUPPRIMÉ : await Database.getInstance();
+ // La BDD est maintenant gérée de manière autonome par le CoreWorker.
- // Injection du Header dans le slot prévu dans index.html
+ // Injection du Header
const headerSlot = document.getElementById('header-slot');
if (headerSlot) {
headerSlot.innerHTML = '';
}
- // Vérification basique de l'appareil (logique reprise de ton ancien router.ts)
+ // 2. Vérification / Création de l'appareil (via le Worker)
const device = await services.getDeviceFromDatabase();
if (!device) {
- console.log('✨ Nouvel appareil détecté, création en cours...');
+ console.log('✨ Nouvel appareil détecté, création en cours via Worker...');
await services.createNewDevice();
} else {
console.log("Restauration de l'appareil...");
- services.restoreDevice(device);
+ await services.restoreDevice(device);
}
- // Initialisation du contrôleur d'Iframe (API listeners)
+ // 3. Initialisation du contrôleur d'Iframe (Reste sur le Main Thread pour écouter window)
await IframeController.init();
- // 3. Restauration des données
+ // 4. Restauration des données (Appels asynchrones au Worker)
await services.restoreProcessesFromDB();
- await services.restoreSecretsFromDB();
- // 4. Connexion réseau
+ // ⚠️ Assurez-vous d'avoir ajouté 'restoreSecretsFromDB' dans votre CoreWorker & Services
+ if (services.restoreSecretsFromDB) {
+ await services.restoreSecretsFromDB();
+ } else {
+ console.warn("restoreSecretsFromDB non implémenté dans le proxy Services");
+ }
+
+ // 5. Connexion réseau (Network Worker)
await services.connectAllRelays();
- // 5. Démarrage du Routeur (Affichage de la page)
+ // 6. Gestion du Routing
const isIframe = window.self !== window.top;
+ const isPaired = await services.isPaired(); // Appel async maintenant
- // On redirige vers 'process' SEULEMENT si on est appairé ET qu'on n'est PAS dans une iframe
- if (services.isPaired() && !isIframe) {
+ if (isPaired && !isIframe) {
console.log('✅ Mode Standalone & Appairé : Redirection vers Process.');
window.history.replaceState({}, '', 'process');
Router.handleLocation();
} else {
- // Cas 1 : Pas appairé
- // Cas 2 : Mode Iframe (même si appairé, on reste sur Home pour attendre le parent)
- console.log(isIframe ? '📡 Mode Iframe détecté : Démarrage sur Home pour attente API.' : '🆕 Non appairé : Démarrage sur Home.');
+ console.log(isIframe ? '📡 Mode Iframe détecté : Attente API.' : '🆕 Non appairé : Démarrage sur Home.');
Router.init();
}
} catch (error) {
@@ -60,6 +64,4 @@ async function bootstrap() {
}
}
-// Lancement
-bootstrap();
-
+bootstrap();
\ No newline at end of file
diff --git a/src/services/core/network.service.ts b/src/services/core/network.service.ts
index 4550461..e4a29bc 100644
--- a/src/services/core/network.service.ts
+++ b/src/services/core/network.service.ts
@@ -1,83 +1,88 @@
-import { initWebsocket, sendMessage } from '../websockets.service.ts';
-import { AnkFlag } from '../../../pkg/sdk_client';
+import * as Comlink from 'comlink';
+import type { NetworkBackend } from '../../workers/network.worker';
+import Services from '../service'; // Attention à la dépendance circulaire, on va gérer ça
export class NetworkService {
- private relayAddresses: { [wsurl: string]: string } = {};
- private relayReadyResolver: (() => void) | null = null;
- private relayReadyPromise: Promise | null = null;
+ private worker: Comlink.Remote;
+ private workerInstance: Worker;
- constructor(private bootstrapUrls: string[]) {}
+ // Cache local pour répondre instantanément aux demandes synchrones de l'UI
+ private localRelays: Record = {};
- public async connectAllRelays(): Promise {
- const connectedUrls: string[] = [];
- for (const wsurl of Object.keys(this.relayAddresses)) {
- try {
- await this.addWebsocketConnection(wsurl);
- connectedUrls.push(wsurl);
- } catch (error) {
- console.error(`[Network] ❌ Échec connexion ${wsurl}:`, error);
- }
+ constructor(private bootstrapUrls: string[]) {
+ this.workerInstance = new Worker(
+ new URL('../../workers/network.worker.ts', import.meta.url),
+ { type: 'module' }
+ );
+ this.worker = Comlink.wrap(this.workerInstance);
+ }
+
+ // Initialisation appelée par Services.ts
+ public async initRelays() {
+ // 1. Setup des callbacks : Quand le worker reçoit un message, il appelle ici
+ await this.worker.setCallbacks(
+ Comlink.proxy(this.onMessageReceived.bind(this)),
+ Comlink.proxy(this.onStatusChange.bind(this))
+ );
+
+ // 2. Lancer les connexions
+ for (const url of this.bootstrapUrls) {
+ this.addWebsocketConnection(url);
}
}
- public async addWebsocketConnection(url: string): Promise {
- console.log(`[Network] 🔌 Connexion à: ${url}`);
- await initWebsocket(url);
+ // --- MÉTHODES PUBLIQUES (API inchangée) ---
+
+ public async addWebsocketConnection(url: string) {
+ await this.worker.connect(url);
}
- public initRelays() {
- for (const wsurl of this.bootstrapUrls) {
- this.updateRelay(wsurl, '');
+ public async connectAllRelays() {
+ for (const url of this.bootstrapUrls) {
+ this.addWebsocketConnection(url);
}
}
+ public async sendMessage(flag: string, content: string) {
+ // On transmet au worker
+ // Note: Le type 'any' est utilisé pour simplifier la compatibilité avec AnkFlag
+ await this.worker.sendMessage(flag as any, content);
+ }
+
public updateRelay(url: string, spAddress: string) {
- console.log(`[Network] Mise à jour relais ${url} -> ${spAddress}`);
- this.relayAddresses[url] = spAddress;
- if (spAddress) this.resolveRelayReady();
- }
-
- public getAvailableRelayAddress(): Promise {
- let relayAddress = Object.values(this.relayAddresses).find((addr) => addr !== '');
- if (relayAddress) return Promise.resolve(relayAddress);
-
- console.log("[Network] ⏳ Attente d'un relais disponible...");
- return this.getRelayReadyPromise().then(() => {
- const addr = Object.values(this.relayAddresses).find((a) => a !== '');
- if (!addr) throw new Error('Aucun relais disponible');
- return addr;
- });
- }
-
- public printAllRelays(): void {
- console.log('[Network] Adresses relais actuelles:');
- for (const [wsurl, spAddress] of Object.entries(this.relayAddresses)) {
- console.log(`${wsurl} -> ${spAddress}`);
- }
- }
-
- private getRelayReadyPromise(): Promise {
- if (!this.relayReadyPromise) {
- this.relayReadyPromise = new Promise((resolve) => {
- this.relayReadyResolver = resolve;
- });
- }
- return this.relayReadyPromise;
- }
-
- private resolveRelayReady(): void {
- if (this.relayReadyResolver) {
- this.relayReadyResolver();
- this.relayReadyResolver = null;
- this.relayReadyPromise = null;
- }
+ // Cette méthode était utilisée pour update l'état local.
+ // Maintenant c'est le worker qui gère la vérité, mais on garde le cache local.
+ this.localRelays[url] = spAddress;
}
public getAllRelays() {
- return this.relayAddresses;
+ return this.localRelays;
}
- public sendMessage(flag: AnkFlag, message: string) {
- sendMessage(flag, message);
+ public async getAvailableRelayAddress(): Promise {
+ // On demande au worker qui a la "vraie" info temps réel
+ const addr = await this.worker.getAvailableRelay();
+ if (addr) return addr;
+
+ // Fallback ou attente...
+ throw new Error('Aucun relais disponible (NetworkWorker)');
}
-}
+
+ // --- INTERNES (CALLBACKS) ---
+
+ private async onMessageReceived(flag: string, content: string, url: string) {
+ // C'est ici qu'on fait le pont : NetworkWorker -> Main -> CoreWorker
+ // On passe par l'instance Singleton de Services pour atteindre le CoreWorker
+ const services = await Services.getInstance();
+ await services.dispatchToWorker(flag, content, url);
+ }
+
+ private onStatusChange(url: string, status: 'OPEN' | 'CLOSED', spAddress?: string) {
+ if (status === 'OPEN' && spAddress) {
+ this.localRelays[url] = spAddress;
+ } else if (status === 'CLOSED') {
+ this.localRelays[url] = '';
+ }
+ // On pourrait notifier l'UI ici de l'état de la connexion
+ }
+}
\ No newline at end of file
diff --git a/src/services/service.ts b/src/services/service.ts
index 2ab3245..b2b8adf 100755
--- a/src/services/service.ts
+++ b/src/services/service.ts
@@ -1,44 +1,29 @@
-import { ApiReturn, Device, HandshakeMessage, Member, MerkleProofResult, NewTxMessage, OutPointProcessMap, Process, ProcessState, RoleDefinition, SecretsStore, UserDiff } from '../../pkg/sdk_client';
-import Database from './database.service';
-import { storeData, retrieveData } from './storage.service';
-import { BackUp } from '../types/index';
+import * as Comlink from 'comlink';
+import { ApiReturn, Device, Member, MerkleProofResult, Process, ProcessState, SecretsStore, UserDiff, BackUp } from '../../pkg/sdk_client';
import { APP_CONFIG } from '../config/constants';
-
-// Services
-import { SdkService } from './core/sdk.service';
import { NetworkService } from './core/network.service';
-import { WalletService } from './domain/wallet.service';
-import { ProcessService } from './domain/process.service';
-import { CryptoService } from './domain/crypto.service';
+import type { CoreBackend } from '../workers/core.worker';
export default class Services {
private static instance: Services;
private static initializing: Promise | null = null;
- private sdkService: SdkService;
+ // Worker References
+ private coreWorker: Comlink.Remote;
+ private workerInstance: Worker;
+
+ // Services Locaux
public networkService: NetworkService;
- private walletService!: WalletService;
- private processService!: ProcessService;
- private cryptoService: CryptoService;
-
- private processId: string | null = null;
- private stateId: string | null = null;
- private membersList: Record = {};
- private notifications: any[] | null = null;
- private db!: Database;
- private currentBlockHeight: number = -1;
- private pendingKeyRequests: Map void> = new Map();
-
- public device1: boolean = false;
- public device2Ready: boolean = false;
private constructor() {
- this.sdkService = new SdkService();
- // Utilisation de la config
this.networkService = new NetworkService(APP_CONFIG.URLS.BOOTSTRAP);
- this.cryptoService = new CryptoService(this.sdkService);
- this.walletService = new WalletService(this.sdkService, null as any);
- this.processService = new ProcessService(this.sdkService, null as any);
+
+ // Initialisation du Core Worker
+ this.workerInstance = new Worker(
+ new URL('../workers/core.worker.ts', import.meta.url),
+ { type: 'module' }
+ );
+ this.coreWorker = Comlink.wrap(this.workerInstance);
}
public static async getInstance(): Promise {
@@ -55,902 +40,199 @@ export default class Services {
}
public async init(): Promise {
- console.log('[Services] ⏳ Initialisation...');
- this.notifications = this.getNotifications();
- await this.sdkService.init();
- this.db = await Database.getInstance();
- this.walletService = new WalletService(this.sdkService, this.db);
- this.processService = new ProcessService(this.sdkService, this.db);
-
- this.networkService.initRelays();
- console.log('[Services] ✅ Initialisé.');
+ console.log('[Services] 🚀 Démarrage Proxy...');
+
+ // 1. Initialiser le Core Worker
+ await this.coreWorker.init();
+
+ // 2. Configurer les Callbacks (Le worker pilote le main thread pour le réseau)
+ await this.coreWorker.setCallbacks(
+ Comlink.proxy(this.handleWorkerNotification.bind(this)), // notifier
+ Comlink.proxy(this.handleWorkerNetworkSend.bind(this)), // networkSender
+ Comlink.proxy(this.handleWorkerRelayUpdate.bind(this)), // relayUpdater
+ Comlink.proxy(this.handleWorkerRelayRequest.bind(this)) // relayGetter
+ );
+
+ // 3. Initialiser le Réseau (Network Worker via Proxy)
+ await this.networkService.initRelays();
+
+ console.log('[Services] ✅ Proxy connecté au CoreWorker et NetworkService.');
}
- // --- Getters & Setters ---
- public get sdkClient() {
- return this.sdkService.getClient();
- }
- public setProcessId(id: string | null) {
- this.processId = id;
- }
- public setStateId(id: string | null) {
- this.stateId = id;
- }
- public getProcessId() {
- return this.processId;
- }
- public getStateId() {
- return this.stateId;
+ // ==========================================
+ // CALLBACKS DU WORKER
+ // ==========================================
+
+ private handleWorkerNotification(event: string, data?: any) {
+ document.dispatchEvent(new CustomEvent(event, { detail: data }));
}
- // --- Network Proxy ---
- public async connectAllRelays() {
- await this.networkService.connectAllRelays();
- if (Object.keys(this.networkService.getAllRelays()).length > 0) {
- try {
- await this.waitForHandshakeMessage();
- } catch (e: any) {
- console.error(e.message);
- }
- }
+ private handleWorkerNetworkSend(flag: string, content: string) {
+ this.networkService.sendMessage(flag as any, content);
}
- public async addWebsocketConnection(url: string): Promise {
- await this.networkService.addWebsocketConnection(url);
- }
- public getAllRelays() {
- const relays = this.networkService.getAllRelays();
- return Object.entries(relays).map(([wsurl, spAddress]) => ({ wsurl, spAddress }));
- }
- public updateRelay(url: string, sp: string) {
+
+ private handleWorkerRelayUpdate(url: string, sp: string) {
this.networkService.updateRelay(url, sp);
}
- public getSpAddress(url: string) {
- return this.networkService.getAllRelays()[url];
+
+ private async handleWorkerRelayRequest(): Promise {
+ return await this.networkService.getAvailableRelayAddress();
}
+
+ // ==========================================
+ // DISPATCH WEBSOCKET VERS WORKER
+ // ==========================================
+ public async dispatchToWorker(flag: string, content: string, url: string) {
+ switch (flag) {
+ case 'Handshake': await this.coreWorker.handleHandshakeMsg(url, content); break;
+ case 'NewTx': await this.coreWorker.parseNewTx(content); break;
+ case 'Cipher': await this.coreWorker.parseCipher(content); break;
+ case 'Commit': await this.coreWorker.handleCommitError(content); break;
+ }
+ }
+
+ // ==========================================
+ // PROXY - PROPERTIES PUBLIC
+ // ==========================================
+ public async getDevice1() { return await this.coreWorker.getDevice1(); }
+ public async getDevice2Ready() { return await this.coreWorker.getDevice2Ready(); }
+
+ // NOTE: getSdkClient renvoie null car l'objet WASM n'est pas transférable hors du worker.
+ // Toute la logique utilisant le client doit être dans le worker.
+ public get sdkClient() { return null; }
+
+ public async setProcessId(id: string | null) { return await this.coreWorker.setProcessId(id); }
+ public async setStateId(id: string | null) { return await this.coreWorker.setStateId(id); }
+ public async getProcessId() { return await this.coreWorker.getProcessId(); }
+ public async getStateId() { return await this.coreWorker.getStateId(); }
+ public async resetState() { return await this.coreWorker.resetState(); }
+
+ // ==========================================
+ // PROXY - NETWORK PROXY
+ // ==========================================
+ public async connectAllRelays() { await this.networkService.connectAllRelays(); }
+ public async addWebsocketConnection(url: string) { await this.networkService.addWebsocketConnection(url); }
+ public getAllRelays() { return this.networkService.getAllRelays(); }
+ public updateRelay(url: string, sp: string) { this.networkService.updateRelay(url, sp); }
+ public getSpAddress(url: string) { return this.networkService.getAllRelays()[url]; }
+ // Délégation directe au NetworkService
public printAllRelays() {
- this.networkService.printAllRelays();
- }
-
- // --- Wallet Proxy ---
- public isPaired() {
- return this.walletService.isPaired();
- }
- public getAmount() {
- return this.walletService.getAmount();
- }
- public getDeviceAddress() {
- return this.walletService.getDeviceAddress();
- }
- public dumpDeviceFromMemory() {
- return this.walletService.dumpDeviceFromMemory();
- }
- public dumpNeuteredDevice() {
- return this.walletService.dumpNeuteredDevice();
- }
- public getPairingProcessId() {
- return this.walletService.getPairingProcessId();
- }
- public async getDeviceFromDatabase() {
- return this.walletService.getDeviceFromDatabase();
- }
- public restoreDevice(d: Device) {
- this.walletService.restoreDevice(d);
- }
- public pairDevice(pid: string, list: string[]) {
- this.walletService.pairDevice(pid, list);
- }
- public async unpairDevice() {
- await this.walletService.unpairDevice();
- }
- public async saveDeviceInDatabase(d: Device) {
- await this.walletService.saveDeviceInDatabase(d);
- }
- public async createNewDevice() {
- return this.walletService.createNewDevice(this.currentBlockHeight > 0 ? this.currentBlockHeight : 0);
- }
- public async dumpWallet() {
- return this.walletService.dumpWallet();
- }
- public async getMemberFromDevice() {
- return this.walletService.getMemberFromDevice();
- }
-
- // --- Process Proxy ---
- public async getProcess(id: string) {
- return this.processService.getProcess(id);
- }
- public async getProcesses() {
- return this.processService.getProcesses();
- }
- public async restoreProcessesFromDB() {
- await this.processService.getProcesses();
- }
- public getLastCommitedState(p: Process) {
- return this.processService.getLastCommitedState(p);
- }
- public getUncommitedStates(p: Process) {
- return this.processService.getUncommitedStates(p);
- }
- public getStateFromId(p: Process, id: string) {
- return this.processService.getStateFromId(p, id);
- }
- public getRoles(p: Process) {
- return this.processService.getRoles(p);
- }
- public getLastCommitedStateIndex(p: Process) {
- return this.processService.getLastCommitedStateIndex(p);
- }
- public async batchSaveProcessesToDb(p: Record) {
- return this.processService.batchSaveProcesses(p);
- }
-
- // --- Helpers Crypto Proxy ---
- public decodeValue(val: number[]) {
- return this.sdkService.decodeValue(val);
- }
- public hexToBlob(hex: string) {
- return this.cryptoService.hexToBlob(hex);
- }
- public hexToUInt8Array(hex: string) {
- return this.cryptoService.hexToUInt8Array(hex);
- }
- public async blobToHex(blob: Blob) {
- return this.cryptoService.blobToHex(blob);
- }
- public getHashForFile(c: string, l: string, f: any) {
- return this.cryptoService.getHashForFile(c, l, f);
- }
- public getMerkleProofForFile(s: ProcessState, a: string) {
- return this.cryptoService.getMerkleProofForFile(s, a);
- }
- public validateMerkleProof(p: MerkleProofResult, h: string) {
- return this.cryptoService.validateMerkleProof(p, h);
- }
- private splitData(obj: Record) {
- return this.cryptoService.splitData(obj);
- }
-
- // --- Membres ---
- public getAllMembers() {
- return this.membersList;
- }
- public getAllMembersSorted() {
- return Object.fromEntries(Object.entries(this.membersList).sort(([keyA], [keyB]) => keyA.localeCompare(keyB)));
- }
- public async ensureMembersAvailable(): Promise {
- if (Object.keys(this.membersList).length > 0) return;
- console.warn('[Services] Tentative de récupération des membres...');
- await this.connectAllRelays();
- }
- public getAddressesForMemberId(memberId: string): string[] | null {
- if (!this.membersList[memberId]) return null;
- return this.membersList[memberId].sp_addresses;
- }
- public compareMembers(memberA: string[], memberB: string[]): boolean {
- if (!memberA || !memberB) return false;
- if (memberA.length !== memberB.length) return false;
- return memberA.every((item) => memberB.includes(item)) && memberB.every((item) => memberA.includes(item));
- }
-
- // --- Utilitaires ---
- public createFaucetMessage() {
- return this.sdkClient.create_faucet_msg();
- }
-
- public isChildRole(parent: any, child: any): boolean {
- try {
- this.sdkClient.is_child_role(JSON.stringify(parent), JSON.stringify(child));
- return true;
- } catch (e) {
- console.error(e);
- return false;
- }
- }
-
- public resetState() {
- this.device1 = false;
- this.device2Ready = false;
- }
-
- // --- Logique Handshake ---
- public async handleHandshakeMsg(url: string, parsedMsg: any) {
- try {
- const handshakeMsg: HandshakeMessage = JSON.parse(parsedMsg);
-
- if (handshakeMsg.sp_address) {
- this.updateRelay(url, handshakeMsg.sp_address);
- }
- this.currentBlockHeight = handshakeMsg.chain_tip;
-
- if (!this.isPaired()) {
- console.log(`[Services] ⏳ Non pairé. Le Handshake de ${url} est en pause...`);
- while (!this.isPaired()) {
- await new Promise(r => setTimeout(r, 500));
- }
- console.log(`[Services] ▶️ Appareil pairé ! Reprise du traitement Handshake de ${url}.`);
- }
-
- this.updateDeviceBlockHeight();
-
- if (handshakeMsg.peers_list) {
- this.membersList = { ...this.membersList, ...handshakeMsg.peers_list as Record };
- }
-
- if (handshakeMsg.processes_list) {
- this.syncProcessesFromHandshake(handshakeMsg.processes_list);
- }
-
- } catch(e) {
- console.error("Handshake Error", e);
- }
- }
-
- private async waitForHandshakeMessage(timeoutMs = APP_CONFIG.TIMEOUTS.HANDSHAKE): Promise {
- const start = Date.now();
- while (Date.now() - start < timeoutMs) {
- if (Object.keys(this.membersList).length > 0 || Object.values(this.networkService.getAllRelays()).some((a) => a !== '')) return;
- await new Promise((r) => setTimeout(r, APP_CONFIG.TIMEOUTS.POLLING_INTERVAL));
- }
- throw new Error('Timeout waiting for handshake');
- }
-
- public async updateDeviceBlockHeight() {
- if (this.currentBlockHeight <= 0) return;
- const device = await this.walletService.getDeviceFromDatabase();
- if (!device) return;
-
- if (device.sp_wallet.birthday === 0) {
- device.sp_wallet.birthday = this.currentBlockHeight;
- device.sp_wallet.last_scan = this.currentBlockHeight;
- await this.walletService.saveDeviceInDatabase(device);
- this.walletService.restoreDevice(device);
- } else if (device.sp_wallet.last_scan < this.currentBlockHeight) {
- console.log(`[Services] Scan requis de ${device.sp_wallet.last_scan} à ${this.currentBlockHeight}`);
- try {
- await this.sdkClient.scan_blocks(this.currentBlockHeight, APP_CONFIG.URLS.BLINDBIT);
- const updatedDevice = this.walletService.dumpDeviceFromMemory();
- await this.walletService.saveDeviceInDatabase(updatedDevice);
- } catch (e) {
- console.error('Scan error', e);
- }
- }
- }
-
- // --- Logique Métier ---
- public async getMyProcesses(): Promise {
- try {
- const pid = this.getPairingProcessId();
- return await this.processService.getMyProcesses(pid);
- } catch (e) {
- return null;
- }
- }
-
- public async ensureConnections(process: Process, stateId: string | null = null): Promise {
- console.info(`[ConnectionCheck] 🔄 Check connexions (StateID: ${stateId || 'default'})`);
- if (!process) return;
-
- let state: ProcessState | null = null;
- if (stateId) state = this.processService.getStateFromId(process, stateId);
- if (!state && process.states.length >= 2) state = process.states[process.states.length - 2];
- if (!state) return;
-
- await this.ensureMembersAvailable();
- const members = new Set();
-
- if (state.roles) {
- for (const role of Object.values(state.roles)) {
- for (const memberId of role.members) {
- const addrs = this.getAddressesForMemberId(memberId);
- if (addrs) members.add({ sp_addresses: addrs });
- }
- }
- }
-
- if (members.size === 0) {
- let publicData: Record | null = null;
- for (let i = process.states.length - 1; i >= 0; i--) {
- const s = process.states[i];
- if (s.public_data && s.public_data['pairedAddresses']) {
- publicData = s.public_data;
- break;
- }
- }
- if (publicData && publicData['pairedAddresses']) {
- const decoded = this.decodeValue(publicData['pairedAddresses']);
- if (decoded) members.add({ sp_addresses: decoded });
- }
- }
-
- if (members.size === 0) return;
-
- const unconnected = new Set();
- const myAddress = this.getDeviceAddress();
- for (const member of Array.from(members)) {
- if (!member.sp_addresses) continue;
- for (const address of member.sp_addresses) {
- if (address === myAddress) continue;
- if ((await this.getSecretForAddress(address)) === null) unconnected.add(address);
- }
- }
-
- if (unconnected.size > 0) {
- console.log(`[ConnectionCheck] 📡 ${unconnected.size} non connectés. Connexion...`);
- await this.connectAddresses(Array.from(unconnected));
- }
- }
-
- public async connectAddresses(addresses: string[]): Promise {
- if (addresses.length === 0) return null;
- const feeRate = APP_CONFIG.FEE_RATE;
- try {
- return this.sdkClient.create_transaction(addresses, feeRate);
- } catch (error: any) {
- if (String(error).includes('Insufficient funds')) {
- await this.getTokensFromFaucet();
- return this.sdkClient.create_transaction(addresses, feeRate);
- } else {
- throw error;
- }
- }
- }
-
- private async getTokensFromFaucet(): Promise {
- console.log('[Services] 🚰 Demande Faucet...');
- const availableAmt = this.getAmount();
- const target: BigInt = APP_CONFIG.DEFAULT_AMOUNT * BigInt(10);
- if (availableAmt < target) {
- const msg = this.sdkClient.create_faucet_msg();
- this.networkService.sendMessage('Faucet', msg);
- let attempts = 3;
- while (attempts > 0) {
- if (this.getAmount() >= target) return;
- attempts--;
- await new Promise((r) => setTimeout(r, APP_CONFIG.TIMEOUTS.RETRY_DELAY));
- }
- throw new Error('Montant insuffisant après faucet');
- }
- }
-
- private async syncProcessesFromHandshake(newProcesses: OutPointProcessMap) {
- if (!newProcesses || Object.keys(newProcesses).length === 0) return;
- console.log(`[Services] Synchro ${Object.keys(newProcesses).length} processus...`);
-
- const toSave: Record = {};
- const currentProcesses = await this.getProcesses();
-
- if (Object.keys(currentProcesses).length === 0) {
- await this.processService.batchSaveProcesses(newProcesses);
- } else {
- for (const [processId, process] of Object.entries(newProcesses)) {
- const existing = currentProcesses[processId];
- if (existing) {
- let newStates: string[] = [];
- let newRoles: Record[] = [];
-
- for (const state of process.states) {
- if (!state || !state.state_id) continue;
-
- if (state.state_id === APP_CONFIG.EMPTY_32_BYTES) {
- const existingTip = existing.states[existing.states.length - 1].commited_in;
- if (existingTip !== state.commited_in) {
- existing.states.pop();
- existing.states.push(state);
- toSave[processId] = existing;
- }
- } else if (!this.processService.getStateFromId(existing, state.state_id)) {
- const existingLast = existing.states.pop();
- if (existingLast) {
- existing.states.push(state);
- existing.states.push(existingLast);
- toSave[processId] = existing;
- if (this.rolesContainsUs(state.roles)) {
- newStates.push(state.state_id);
- newRoles.push(state.roles);
- }
- }
- } else {
- const existingState = this.processService.getStateFromId(existing, state.state_id);
- if (existingState && (!existingState.keys || Object.keys(existingState.keys).length === 0)) {
- if (this.rolesContainsUs(state.roles)) {
- newStates.push(state.state_id);
- newRoles.push(state.roles);
- }
- }
- }
- }
-
- if (newStates.length > 0) {
- await this.ensureConnections(existing);
- await this.requestDataFromPeers(processId, newStates, newRoles);
- }
- } else {
- toSave[processId] = process;
- }
- }
- if (Object.keys(toSave).length > 0) {
- await this.processService.batchSaveProcesses(toSave);
- }
- }
- document.dispatchEvent(new CustomEvent('processes-updated'));
- }
-
- public async createPairingProcess(userName: string, pairWith: string[]): Promise {
- if (this.isPaired()) throw new Error('Déjà appairé');
- const myAddress = this.getDeviceAddress();
- pairWith.push(myAddress);
- const privateData = { description: 'pairing', counter: 0 };
- const publicData = { memberPublicName: userName, pairedAddresses: pairWith };
- const validation_fields = [...Object.keys(privateData), ...Object.keys(publicData), 'roles'];
- const roles = {
- pairing: {
- members: [],
- validation_rules: [{ quorum: 1.0, fields: validation_fields, min_sig_member: 1.0 }],
- storages: [APP_CONFIG.URLS.STORAGE],
- },
- };
- return this.createProcess(privateData, publicData, roles);
- }
-
- public async createProcess(privateData: any, publicData: any, roles: any, feeRate = APP_CONFIG.FEE_RATE): Promise {
- const relay = await this.networkService.getAvailableRelayAddress();
- const { encodedPrivateData, encodedPublicData } = await this.prepareProcessData(privateData, publicData);
- const members = this.membersList;
- try {
- return await this.attemptCreateProcess(encodedPrivateData, roles, encodedPublicData, relay, feeRate, members);
- } catch (e: any) {
- if (String(e).includes('Insufficient funds')) {
- await this.getTokensFromFaucet();
- return await this.attemptCreateProcess(encodedPrivateData, roles, encodedPublicData, relay, feeRate, members);
- }
- throw e;
- }
- }
-
- private async attemptCreateProcess(priv: any, roles: any, pub: any, relay: string, fee: number, members: any): Promise {
- const res = this.sdkClient.create_new_process(priv, roles, pub, relay, fee, members);
- if (res.updated_process) {
- await this.ensureConnections(res.updated_process.current_process);
- }
- return res;
- }
-
- public async updateProcess(processId: string, newData: any, privateFields: string[], roles: any): Promise {
- const process = await this.processService.getProcess(processId);
- if (!process) throw new Error('Process not found');
-
- let lastState = this.processService.getLastCommitedState(process);
- let currentProcess = process;
-
- if (!lastState) {
- const first = process.states[0];
- if (this.rolesContainsUs(first.roles)) {
- const appRes = await this.approveChange(processId, first.state_id);
- await this.handleApiReturn(appRes);
- const prdRes = await this.createPrdUpdate(processId, first.state_id);
- await this.handleApiReturn(prdRes);
- } else if (first.validation_tokens.length > 0) {
- const res = await this.createPrdUpdate(processId, first.state_id);
- await this.handleApiReturn(res);
- }
- const updated = await this.processService.getProcess(processId);
- if (updated) currentProcess = updated;
- lastState = this.processService.getLastCommitedState(currentProcess);
- if (!lastState) throw new Error('Still no commited state');
- }
-
- const lastStateIndex = this.getLastCommitedStateIndex(currentProcess);
- if (lastStateIndex === null) throw new Error('Index commited introuvable');
-
- const privateData: any = {};
- const publicData: any = {};
-
- for (const field of Object.keys(newData)) {
- if (lastState.public_data[field]) {
- publicData[field] = newData[field];
- continue;
- }
- if (privateFields.includes(field)) {
- privateData[field] = newData[field];
- continue;
- }
- let isPrivate = false;
- for (let i = lastStateIndex; i >= 0; i--) {
- if (currentProcess.states[i].pcd_commitment[field]) {
- privateData[field] = newData[field];
- isPrivate = true;
- break;
- }
- }
- if (!isPrivate) publicData[field] = newData[field];
- }
-
- const finalRoles = roles || this.processService.getRoles(currentProcess);
- const { encodedPrivateData, encodedPublicData } = await this.prepareProcessData(privateData, publicData);
-
- const res = this.sdkClient.update_process(currentProcess, encodedPrivateData, finalRoles, encodedPublicData, this.membersList);
- if (res.updated_process) await this.ensureConnections(res.updated_process.current_process);
- return res;
- }
-
- // public async confirmPairing() {
- // console.log('[Services] Confirm Pairing...');
- // const pid = this.walletService.getPairingProcessId();
- // const process = await this.processService.getProcess(pid);
- // if (!process) return;
-
- // let state = this.processService.getLastCommitedState(process);
- // if (!state && process.states.length > 0) state = process.states[process.states.length - 1];
- // if (!state) return;
-
- // const encodedAddr = state.public_data['pairedAddresses'];
- // if (!encodedAddr) return;
-
- // const addresses = this.decodeValue(encodedAddr);
- // if (!addresses || addresses.length === 0) return;
-
- // this.sdkClient.unpair_device();
- // this.walletService.pairDevice(pid, addresses);
-
- // if (this.walletService.isPaired()) {
- // const d = this.walletService.dumpDeviceFromMemory();
- // if (!this.walletService.isPaired()) d.pairing_process_commitment = pid;
- // await this.walletService.saveDeviceInDatabase(d);
- // console.log('✅ Pairing confirmed & Saved');
- // }
- // }
-
- private async prepareProcessData(priv: any, pub: any) {
- const p1 = this.splitData(priv);
- const p2 = this.splitData(pub);
- return {
- encodedPrivateData: { ...this.sdkClient.encode_json(p1.jsonCompatibleData), ...this.sdkClient.encode_binary(p1.binaryData) },
- encodedPublicData: { ...this.sdkClient.encode_json(p2.jsonCompatibleData), ...this.sdkClient.encode_binary(p2.binaryData) },
- };
- }
-
- // API Methods
- public async createPrdUpdate(pid: string, sid: string) {
- const p = await this.getProcess(pid);
- await this.ensureConnections(p!);
- return this.sdkClient.create_update_message(p, sid, this.membersList);
- }
- public async createPrdResponse(pid: string, sid: string) {
- const p = await this.getProcess(pid);
- return this.sdkClient.create_response_prd(p, sid, this.membersList);
- }
- public async approveChange(pid: string, sid: string) {
- const p = await this.getProcess(pid);
- const res = this.sdkClient.validate_state(p, sid, this.membersList);
- if (res.updated_process) await this.ensureConnections(res.updated_process.current_process);
- return res;
- }
- public async rejectChange(pid: string, sid: string) {
- const p = await this.getProcess(pid);
- return this.sdkClient.refuse_state(p, sid);
- }
- public async requestDataFromPeers(pid: string, sids: string[], roles: any) {
- const res = this.sdkClient.request_data(pid, sids, roles, this.membersList);
- await this.handleApiReturn(res);
- }
-
- public async resetDevice() {
- // console.warn("[Services:resetDevice] ⚠️ RÉINITIALISATION COMPLÈTE de l'appareil et de la BDD...");
- this.sdkClient.reset_device();
-
- // Clear all stores
- await this.db.clearMultipleStores(['wallet', 'shared_secrets', 'unconfirmed_secrets', 'processes', 'diffs']);
- // console.warn('[Services:resetDevice] ✅ Réinitialisation terminée.');
- }
-
- public async handleApiReturn(res: ApiReturn) {
- if (!res || Object.keys(res).length === 0) return;
- try {
- const txData = (res.partial_tx ? await this.handlePartialTx(res.partial_tx) : null) || res.new_tx_to_send;
- if (txData && txData.transaction.length != 0) {
- this.networkService.sendMessage('NewTx', JSON.stringify(txData));
- await new Promise((r) => setTimeout(r, APP_CONFIG.TIMEOUTS.API_DELAY));
- }
- if (res.secrets) await this.handleSecrets(res.secrets);
- if (res.updated_process) await this.handleUpdatedProcess(res.updated_process);
- if (res.push_to_storage) await this.handlePushToStorage(res.push_to_storage);
- if (res.commit_to_send) this.networkService.sendMessage('Commit', JSON.stringify(res.commit_to_send));
- if (res.ciphers_to_send) for (const c of res.ciphers_to_send) this.networkService.sendMessage('Cipher', c);
- } catch (e) {
- console.error('ApiReturn Error:', e);
- }
- }
-
- private async handlePartialTx(partialTx: any): Promise {
- try {
- return this.sdkClient.sign_transaction(partialTx).new_tx_to_send;
- } catch (e) {
- return null;
- }
- }
-
- private async handleSecrets(secrets: any) {
- const { unconfirmed_secrets, shared_secrets } = secrets;
-
- const unconfirmedList = unconfirmed_secrets && unconfirmed_secrets.length > 0 ? unconfirmed_secrets : [];
- const sharedList = shared_secrets && Object.keys(shared_secrets).length > 0
- ? Object.entries(shared_secrets).map(([key, value]) => ({ key, value }))
- : [];
-
- if (unconfirmedList.length > 0 || sharedList.length > 0) {
- try {
- await this.db.saveSecretsBatch(unconfirmedList, sharedList);
- } catch (e) {
- console.error('[Services:handleSecrets] 💥 Échec de sauvegarde batch des secrets:', e);
- }
- }
- }
-
- private async handleUpdatedProcess(updated: any) {
- const pid = updated.process_id;
- if (updated.encrypted_data) {
- for (const [h, c] of Object.entries(updated.encrypted_data as Record)) await this.saveBlobToDb(h, this.hexToBlob(c));
- }
- await this.processService.saveProcessToDb(pid, updated.current_process);
- if (updated.diffs) await this.saveDiffsToDb(updated.diffs);
-
- this._resolvePendingKeyRequests(pid, updated.current_process);
- const dev = await this.walletService.getDeviceFromDatabase();
- if (dev && dev.pairing_process_commitment === pid) {
- const last = updated.current_process.states[updated.current_process.states.length - 1];
- // if (last?.public_data['pairedAddresses']) await this.confirmPairing();
- }
- }
-
- public async saveDiffsToDb(diffs: UserDiff[]) {
- await this.db.saveDiffs(diffs);
- }
-
- private _resolvePendingKeyRequests(processId: string, process: Process) {
- if (this.pendingKeyRequests.size === 0) return;
- for (const state of process.states) {
- if (!state.keys) continue;
- for (const [attr, key] of Object.entries(state.keys)) {
- const rid = `${processId}_${state.state_id}_${attr}`;
- if (this.pendingKeyRequests.has(rid)) {
- this.pendingKeyRequests.get(rid)?.(key as string);
- this.pendingKeyRequests.delete(rid);
- }
- }
- }
- }
-
- private async handlePushToStorage(hashes: string[]) {
- for (const hash of hashes) {
- try {
- const blob = await this.getBlobFromDb(hash);
- const diff = await this.getDiffByValue(hash);
- if (blob && diff) await this.saveDataToStorage(diff.storages, hash, blob, null);
- } catch (e) {
- console.error('Push error', e);
- }
- }
- }
-
- public async handleCommitError(response: string) {
- const content = JSON.parse(response);
- const errorMsg = content.error['GenericError'];
- if (!['State is identical to the previous state', 'Not enough valid proofs'].includes(errorMsg)) {
- setTimeout(() => this.networkService.sendMessage('Commit', JSON.stringify(content)), APP_CONFIG.TIMEOUTS.RETRY_DELAY);
- }
- }
-
- public rolesContainsUs(roles: any) {
- return this.processService.rolesContainsMember(roles, this.getPairingProcessId());
- }
-
- public async getSecretForAddress(address: string): Promise {
- return await this.db.getSharedSecret(address);
- }
-
- public async getAllDiffs(): Promise> {
- return await this.db.getAllDiffs();
- }
-
- public async getDiffByValue(value: string): Promise {
- return await this.db.getDiff(value);
- }
-
- public async getAllSecrets(): Promise {
- return await this.db.getAllSecrets();
- }
-
- // Storage & DB
- public async saveBlobToDb(h: string, d: Blob) {
- await this.db.saveBlob(h, d);
- }
- public async getBlobFromDb(h: string) {
- return await this.db.getBlob(h);
- }
- public async fetchValueFromStorage(h: string) {
- return retrieveData([APP_CONFIG.URLS.STORAGE], h);
- }
- public async saveDataToStorage(s: string[], h: string, d: Blob, ttl: number | null) {
- return storeData(s, h, d, ttl);
- }
-
- // Helpers
- public getProcessName(p: Process) {
- const pub = this.getPublicData(p);
- if (pub && pub['processName']) return this.decodeValue(pub['processName']);
- return null;
- }
- public getPublicData(p: Process) {
- const last = this.getLastCommitedState(p);
- return last ? last.public_data : p.states[0]?.public_data || null;
- }
-
- // UI helpers
- public getNotifications() {
- return this.notifications;
- }
- public setNotifications(n: any[]) {
- this.notifications = n;
- }
-
- async parseCipher(msg: string) {
- try {
- const res = this.sdkClient.parse_cipher(msg, this.membersList, await this.getProcesses());
- await this.handleApiReturn(res);
- } catch (e) {
- console.error('Cipher Error', e);
- }
- }
-
- async parseNewTx(msg: string) {
- const parsed = JSON.parse(msg);
- if (parsed.error) return;
-
- const prevouts = this.sdkClient.get_prevouts(parsed.transaction);
- for (const p of Object.values(await this.getProcesses())) {
- const tip = p.states[p.states.length - 1].commited_in;
- if (prevouts.includes(tip)) {
- const newTip = this.sdkClient.get_txid(parsed.transaction);
- const newStateId = this.sdkClient.get_opreturn(parsed.transaction);
- const updated = this.sdkClient.process_commit_new_state(p, newStateId, newTip);
- break;
- }
- }
-
- try {
- const res = this.sdkClient.parse_new_tx(msg, 0, this.membersList);
- if (res && (res.partial_tx || res.new_tx_to_send || res.secrets || res.updated_process)) {
- await this.handleApiReturn(res);
- const d = this.dumpDeviceFromMemory();
- const old = await this.getDeviceFromDatabase();
- if (old && old.pairing_process_commitment) d.pairing_process_commitment = old.pairing_process_commitment;
- await this.saveDeviceInDatabase(d);
- }
- } catch (e) {}
- }
-
- public updateMemberPublicName(pid: string, name: string) {
- return this.updateProcess(pid, { memberPublicName: name }, [], null);
- }
-
- public async importJSON(backup: BackUp) {
- await this.resetDevice();
- await this.walletService.saveDeviceInDatabase(backup.device);
- this.walletService.restoreDevice(backup.device);
- await this.processService.batchSaveProcesses(backup.processes);
- await this.restoreSecretsFromBackUp(backup.secrets);
- }
- public async restoreSecretsFromBackUp(secretsStore: SecretsStore) {
- const sharedList = Object.entries(secretsStore.shared_secrets).map(([key, value]) => ({ key, value }));
- await this.db.saveSecretsBatch(secretsStore.unconfirmed_secrets, sharedList);
- await this.restoreSecretsFromDB();
- }
- public async restoreSecretsFromDB() {
- const secretsStore = await this.db.getAllSecrets();
- this.sdkClient.set_shared_secrets(JSON.stringify(secretsStore));
- }
- public async createBackUp() {
- const device = await this.walletService.getDeviceFromDatabase();
- if (!device) return null;
- return { device, processes: await this.processService.getProcesses(), secrets: await this.getAllSecrets() };
- }
-
- public async decryptAttribute(processId: string, state: ProcessState, attribute: string): Promise {
- console.groupCollapsed(`[Services:decryptAttribute] 🔑 Déchiffrement de '${attribute}' (Process: ${processId})`);
-
- try {
- let hash: string | null | undefined = state.pcd_commitment[attribute];
- let key: string | null | undefined = state.keys[attribute];
- const pairingProcessId = this.getPairingProcessId();
-
- if (!hash) {
- console.warn(`⚠️ L'attribut n'existe pas (pas de hash).`);
- return null;
- }
-
- if (!key) {
- if (!this._checkAccess(state, attribute, pairingProcessId)) {
- console.log(`⛔ Accès non autorisé. Abandon.`);
- return null;
- }
- const result = await this._fetchMissingKey(processId, state, attribute);
- hash = result.hash;
- key = result.key;
- }
-
- if (hash && key) {
- const blob = await this.getBlobFromDb(hash);
- if (!blob) {
- console.error(`💥 Échec: Blob non trouvé en BDD pour le hash ${hash}`);
- return null;
- }
-
- try {
- const buf = await blob.arrayBuffer();
- const cipher = new Uint8Array(buf);
- const keyUIntArray = this.hexToUInt8Array(key);
-
- const clear = this.sdkClient.decrypt_data(keyUIntArray, cipher);
- if (!clear) throw new Error('decrypt_data returned null');
-
- const decoded = this.sdkClient.decode_value(clear);
- console.log(`✅ Attribut '${attribute}' déchiffré avec succès.`);
- return decoded;
- } catch (e) {
- console.error(`💥 Échec du déchiffrement: ${e}`);
- return null;
- }
- }
- return null;
- } catch (error) {
- console.error(`💥 Erreur:`, error);
- return null;
- } finally {
- console.groupEnd();
- }
- }
-
- private _checkAccess(state: ProcessState, attribute: string, pairingProcessId: string): boolean {
- const roles = state.roles;
- return Object.values(roles).some((role) => {
- const isMember = role.members.includes(pairingProcessId);
- if (!isMember) return false;
- return Object.values(role.validation_rules).some((rule) => rule.fields.includes(attribute));
- });
- }
-
- private async _fetchMissingKey(processId: string, state: ProcessState, attribute: string): Promise<{ hash: string | null; key: string | null }> {
- try {
- const process = await this.getProcess(processId);
- if (!process) return { hash: null, key: null };
-
- await this.ensureConnections(process);
- await this.requestDataFromPeers(processId, [state.state_id], [state.roles]);
-
- const requestId = `${processId}_${state.state_id}_${attribute}`;
- const keyRequestPromise = new Promise((resolve, reject) => {
- const timeout = setTimeout(() => {
- this.pendingKeyRequests.delete(requestId);
- reject(new Error(`Timeout waiting for key: ${attribute}`));
- }, APP_CONFIG.TIMEOUTS.KEY_REQUEST);
-
- this.pendingKeyRequests.set(requestId, (key: string) => {
- clearTimeout(timeout);
- resolve(key);
- });
- });
-
- const receivedKey = await keyRequestPromise;
- const updatedProcess = await this.getProcess(processId);
- if (!updatedProcess) return { hash: null, key: null };
-
- const updatedState = this.getStateFromId(updatedProcess, state.state_id);
- const updatedHash = updatedState ? updatedState.pcd_commitment[attribute] : state.pcd_commitment[attribute];
-
- return { hash: updatedHash, key: receivedKey };
- } catch (e) {
- return { hash: null, key: null };
- }
- }
-}
-
+ // Si NetworkService ne l'expose pas directement dans sa version proxy, on l'ajoute ou on log ici
+ console.log('Relays:', this.networkService.getAllRelays());
+ }
+
+ // ==========================================
+ // PROXY - WALLET
+ // ==========================================
+ public async isPaired() { return await this.coreWorker.isPaired(); }
+ public async getAmount() { return await this.coreWorker.getAmount(); }
+ public async getDeviceAddress() { return await this.coreWorker.getDeviceAddress(); }
+ public async dumpDeviceFromMemory() { return await this.coreWorker.dumpDeviceFromMemory(); }
+ public async dumpNeuteredDevice() { return await this.coreWorker.dumpNeuteredDevice(); }
+ public async getPairingProcessId() { return await this.coreWorker.getPairingProcessId(); }
+ public async getDeviceFromDatabase() { return await this.coreWorker.getDeviceFromDatabase(); }
+ public async restoreDevice(d: Device) { await this.coreWorker.restoreDevice(d); }
+ public async pairDevice(pid: string, list: string[]) { await this.coreWorker.pairDevice(pid, list); }
+ public async unpairDevice() { await this.coreWorker.unpairDevice(); }
+ public async saveDeviceInDatabase(d: Device) { await this.coreWorker.saveDeviceInDatabase(d); }
+ public async createNewDevice() { return await this.coreWorker.createNewDevice(); }
+ public async dumpWallet() { return await this.coreWorker.dumpWallet(); }
+ public async getMemberFromDevice() { return await this.coreWorker.getMemberFromDevice(); }
+
+ // ==========================================
+ // PROXY - PROCESS
+ // ==========================================
+ public async getProcess(id: string) { return await this.coreWorker.getProcess(id); }
+ public async getProcesses() { return await this.coreWorker.getProcesses(); }
+ public async restoreProcessesFromDB() { await this.coreWorker.restoreProcessesFromDB(); }
+ public async getLastCommitedState(p: Process) { return await this.coreWorker.getLastCommitedState(p); }
+ public async getUncommitedStates(p: Process) { return await this.coreWorker.getUncommitedStates(p); }
+ public async getStateFromId(p: Process, id: string) { return await this.coreWorker.getStateFromId(p, id); }
+ public async getRoles(p: Process) { return await this.coreWorker.getRoles(p); }
+ public async getLastCommitedStateIndex(p: Process) { return await this.coreWorker.getLastCommitedStateIndex(p); }
+ public async batchSaveProcessesToDb(p: Record) { return await this.coreWorker.batchSaveProcessesToDb(p); }
+
+ // ==========================================
+ // PROXY - CRYPTO
+ // ==========================================
+ public async decodeValue(val: number[]) { return await this.coreWorker.decodeValue(val); }
+ public async hexToBlob(hex: string) { return await this.coreWorker.hexToBlob(hex); }
+ public async hexToUInt8Array(hex: string) { return await this.coreWorker.hexToUInt8Array(hex); }
+ public async blobToHex(blob: Blob) { return await this.coreWorker.blobToHex(blob); }
+ public async getHashForFile(c: string, l: string, f: any) { return await this.coreWorker.getHashForFile(c, l, f); }
+ public async getMerkleProofForFile(s: ProcessState, a: string) { return await this.coreWorker.getMerkleProofForFile(s, a); }
+ public async validateMerkleProof(p: MerkleProofResult, h: string) { return await this.coreWorker.validateMerkleProof(p, h); }
+
+ // ==========================================
+ // PROXY - MEMBERS
+ // ==========================================
+ public async getAllMembers() { return await this.coreWorker.getAllMembers(); }
+ public async getAllMembersSorted() { return await this.coreWorker.getAllMembersSorted(); }
+ public async ensureMembersAvailable() { await this.coreWorker.ensureMembersAvailable(); }
+ public async getAddressesForMemberId(memberId: string) { return await this.coreWorker.getAddressesForMemberId(memberId); }
+ public async compareMembers(mA: string[], mB: string[]) { return await this.coreWorker.compareMembers(mA, mB); }
+
+ // ==========================================
+ // PROXY - UTILS
+ // ==========================================
+ public async createFaucetMessage() { return await this.coreWorker.createFaucetMessage(); }
+ public async isChildRole(parent: any, child: any) { return await this.coreWorker.isChildRole(parent, child); }
+
+ // ==========================================
+ // PROXY - LOGIQUE METIER & API
+ // ==========================================
+ public async getMyProcesses() { return await this.coreWorker.getMyProcesses(); }
+ public async ensureConnections(p: Process, sId?: string) { await this.coreWorker.ensureConnections(p, sId || null); }
+ public async connectAddresses(addr: string[]) { return await this.coreWorker.connectAddresses(addr); }
+
+ public async createPairingProcess(name: string, pairWith: string[]) { return await this.coreWorker.createPairingProcess(name, pairWith); }
+ public async createProcess(priv: any, pub: any, roles: any) { return await this.coreWorker.createProcess(priv, pub, roles); }
+ public async updateProcess(pid: string, newData: any, priv: string[], roles: any) { return await this.coreWorker.updateProcess(pid, newData, priv, roles); }
+
+ public async createPrdUpdate(pid: string, sid: string) { return await this.coreWorker.createPrdUpdate(pid, sid); }
+ public async createPrdResponse(pid: string, sid: string) { return await this.coreWorker.createPrdResponse(pid, sid); }
+ public async approveChange(pid: string, sid: string) { return await this.coreWorker.approveChange(pid, sid); }
+ public async rejectChange(pid: string, sid: string) { return await this.coreWorker.rejectChange(pid, sid); }
+ public async requestDataFromPeers(pid: string, sids: string[], roles: any) { await this.coreWorker.requestDataFromPeers(pid, sids, roles); }
+
+ public async resetDevice() { await this.coreWorker.resetDevice(); }
+ public async handleApiReturn(res: ApiReturn) { await this.coreWorker.handleApiReturn(res); }
+ public async saveDiffsToDb(diffs: UserDiff[]) { await this.coreWorker.saveDiffsToDb(diffs); }
+ public async handleCommitError(res: string) { await this.coreWorker.handleCommitError(res); }
+
+ public async rolesContainsUs(roles: any) { return await this.coreWorker.rolesContainsUs(roles); }
+ public async getSecretForAddress(addr: string) { return await this.coreWorker.getSecretForAddress(addr); }
+ public async getAllDiffs() { return await this.coreWorker.getAllDiffs(); }
+ public async getDiffByValue(val: string) { return await this.coreWorker.getDiffByValue(val); }
+ public async getAllSecrets() { return await this.coreWorker.getAllSecrets(); }
+
+ // ==========================================
+ // PROXY - STORAGE & DB
+ // ==========================================
+ public async saveBlobToDb(h: string, d: Blob) { await this.coreWorker.saveBlobToDb(h, d); }
+ public async getBlobFromDb(h: string) { return await this.coreWorker.getBlobFromDb(h); }
+ public async fetchValueFromStorage(h: string) { return await this.coreWorker.fetchValueFromStorage(h); }
+ public async saveDataToStorage(s: string[], h: string, d: Blob, ttl: number | null) { return await this.coreWorker.saveDataToStorage(s, h, d, ttl); }
+
+ // ==========================================
+ // PROXY - HELPERS UI & DATA
+ // ==========================================
+ public async getProcessName(p: Process) { return await this.coreWorker.getProcessName(p); }
+ public async getPublicData(p: Process) { return await this.coreWorker.getPublicData(p); }
+ public async getNotifications() { return await this.coreWorker.getNotifications(); }
+ public async setNotifications(n: any[]) { await this.coreWorker.setNotifications(n); }
+
+ public async parseCipher(msg: string) { await this.coreWorker.parseCipher(msg); }
+ public async parseNewTx(msg: string) { await this.coreWorker.parseNewTx(msg); }
+ public async updateMemberPublicName(pid: string, name: string) { return await this.coreWorker.updateMemberPublicName(pid, name); }
+
+ // ==========================================
+ // PROXY - BACKUP & DECRYPT
+ // ==========================================
+ public async importJSON(b: BackUp) { await this.coreWorker.importJSON(b); }
+ public async restoreSecretsFromBackUp(s: SecretsStore) { await this.coreWorker.restoreSecretsFromBackUp(s); }
+ public async restoreSecretsFromDB() { await this.coreWorker.restoreSecretsFromDB(); }
+ public async createBackUp() { return await this.coreWorker.createBackUp(); }
+
+ public async decryptAttribute(pid: string, s: ProcessState, attr: string) { return await this.coreWorker.decryptAttribute(pid, s, attr); }
+}
\ No newline at end of file
diff --git a/src/services/websockets.service.ts b/src/services/websockets.service.ts
deleted file mode 100755
index 117e2fa..0000000
--- a/src/services/websockets.service.ts
+++ /dev/null
@@ -1,136 +0,0 @@
-import { AnkFlag } from '../../pkg/sdk_client'; // Vérifie le chemin vers pkg
-import Services from './service';
-import { APP_CONFIG } from '../config/constants';
-
-let ws: WebSocket | null = null;
-let messageQueue: string[] = [];
-let reconnectInterval = APP_CONFIG.TIMEOUTS.RETRY_DELAY;
-const MAX_RECONNECT_INTERVAL = APP_CONFIG.TIMEOUTS.WS_RECONNECT_MAX;
-let isConnecting = false;
-let urlReference: string = '';
-let pingIntervalId: any = null;
-
-export async function initWebsocket(url: string) {
- urlReference = url;
- connect();
-}
-
-function connect() {
- if (isConnecting || (ws && ws.readyState === WebSocket.OPEN)) return;
- isConnecting = true;
-
- console.log(`[WS] 🔌 Tentative de connexion à ${urlReference}...`);
- ws = new WebSocket(urlReference);
-
- ws.onopen = async () => {
- console.log('[WS] ✅ Connexion établie !');
- isConnecting = false;
- reconnectInterval = APP_CONFIG.TIMEOUTS.RETRY_DELAY; // Reset du délai
-
- // Démarrer le Heartbeat (Ping pour garder la connexion vivante)
- startHeartbeat();
-
- // Vider la file d'attente (messages envoyés pendant la coupure)
- while (messageQueue.length > 0) {
- const message = messageQueue.shift();
- if (message) ws?.send(message);
- }
- };
-
- ws.onmessage = (event) => {
- const msgData = event.data;
- if (typeof msgData === 'string') {
- (async () => {
- try {
- const parsedMessage = JSON.parse(msgData);
- const services = await Services.getInstance();
-
- // Gestion des messages
- switch (parsedMessage.flag) {
- case 'Handshake':
- await services.handleHandshakeMsg(urlReference, parsedMessage.content);
- break;
- case 'NewTx':
- await services.parseNewTx(parsedMessage.content);
- break;
- case 'Cipher':
- await services.parseCipher(parsedMessage.content);
- break;
- case 'Commit':
- await services.handleCommitError(parsedMessage.content);
- break;
- // Ajoute d'autres cas si nécessaire
- default:
- // console.log('[WS] Message reçu:', parsedMessage.flag);
- }
- } catch (error) {
- console.error('[WS] Erreur traitement message:', error);
- }
- })();
- }
- };
-
- ws.onerror = (event) => {
- console.error('[WS] 💥 Erreur:', event);
- // Pas besoin de reconnecter ici, onclose sera appelé juste après
- };
-
- ws.onclose = (event) => {
- isConnecting = false;
- stopHeartbeat();
- console.warn(`[WS] ⚠️ Déconnecté (Code: ${event.code}). Reconnexion dans ${reconnectInterval / 1000}s...`);
-
- // Reconnexion exponentielle (1s, 1.5s, 2.25s...)
- setTimeout(() => {
- connect();
- reconnectInterval = Math.min(reconnectInterval * 1.5, MAX_RECONNECT_INTERVAL);
- }, reconnectInterval);
- };
-}
-
-function startHeartbeat() {
- stopHeartbeat();
- // Envoie un ping toutes les 30 secondes pour éviter que le serveur ou le navigateur ne coupe la connexion
- pingIntervalId = setInterval(() => {
- if (ws && ws.readyState === WebSocket.OPEN) {
- // Adapter selon ce que ton serveur attend comme Ping, ou envoyer un message vide
- // ws.send(JSON.stringify({ flag: 'Ping', content: '' }));
- }
- }, APP_CONFIG.TIMEOUTS.WS_HEARTBEAT);
-}
-
-function stopHeartbeat() {
- if (pingIntervalId) clearInterval(pingIntervalId);
-}
-
-export function sendMessage(flag: AnkFlag, message: string): void {
- if (ws && ws.readyState === WebSocket.OPEN) {
- const networkMessage = {
- flag: flag,
- content: message,
- };
- ws.send(JSON.stringify(networkMessage));
- } else {
- console.warn(`[WS] Pas connecté. Message '${flag}' mis en file d'attente.`);
- const networkMessage = {
- flag: flag,
- content: message,
- };
- messageQueue.push(JSON.stringify(networkMessage));
-
- // Si on n'est pas déjà en train de se connecter, on force une tentative
- if (!isConnecting) connect();
- }
-}
-
-export function getUrl(): string {
- return urlReference;
-}
-
-export function close(): void {
- if (ws) {
- ws.onclose = null; // On évite la reconnexion auto si fermeture volontaire
- stopHeartbeat();
- ws.close();
- }
-}
diff --git a/src/workers/core.worker.ts b/src/workers/core.worker.ts
new file mode 100644
index 0000000..c2d29b2
--- /dev/null
+++ b/src/workers/core.worker.ts
@@ -0,0 +1,859 @@
+import * as Comlink from 'comlink';
+import {
+ ApiReturn, Device, HandshakeMessage, Member, MerkleProofResult,
+ OutPointProcessMap, Process, ProcessState, RoleDefinition, SecretsStore, UserDiff
+} from '../../pkg/sdk_client';
+import Database from '../services/database.service';
+import { storeData, retrieveData } from '../services/storage.service';
+import { BackUp } from '../types/index';
+import { APP_CONFIG } from '../config/constants';
+import { splitPrivateData } from '../utils/service.utils';
+
+// Services internes au worker
+import { SdkService } from '../services/core/sdk.service';
+import { WalletService } from '../services/domain/wallet.service';
+import { ProcessService } from '../services/domain/process.service';
+import { CryptoService } from '../services/domain/crypto.service';
+
+export class CoreBackend {
+ // Services
+ private sdkService: SdkService;
+ private walletService!: WalletService;
+ private processService!: ProcessService;
+ private cryptoService: CryptoService;
+ private db!: Database;
+
+ // État (State)
+ private processId: string | null = null;
+ private stateId: string | null = null;
+ private membersList: Record = {};
+ private notifications: any[] | null = null;
+ private currentBlockHeight: number = -1;
+ private pendingKeyRequests: Map void> = new Map();
+
+ // Flags publics (State)
+ public device1: boolean = false;
+ public device2Ready: boolean = false;
+
+ private isInitialized = false;
+
+ // Callbacks vers le Main Thread
+ private notifier: ((event: string, data?: any) => void) | null = null;
+ private networkSender: ((flag: string, content: string) => void) | null = null;
+ private relayUpdater: ((url: string, sp: string) => void) | null = null;
+ private relayGetter: (() => Promise) | null = null;
+
+ constructor() {
+ this.sdkService = new SdkService();
+ this.cryptoService = new CryptoService(this.sdkService);
+ // Initialisation temporaire
+ this.walletService = new WalletService(this.sdkService, null as any);
+ this.processService = new ProcessService(this.sdkService, null as any);
+ }
+
+ public async init(): Promise {
+ if (this.isInitialized) return;
+
+ console.log('[CoreWorker] ⚙️ Initialisation du Backend...');
+ await this.sdkService.init(); // Charge le WASM
+ this.db = await Database.getInstance(); // Lance le Database Worker
+
+ this.walletService = new WalletService(this.sdkService, this.db);
+ this.processService = new ProcessService(this.sdkService, this.db);
+
+ this.notifications = this.getNotifications();
+ this.isInitialized = true;
+ console.log('[CoreWorker] ✅ Backend prêt.');
+ }
+
+ // --- CONFIGURATION DES CALLBACKS ---
+ public setCallbacks(
+ notifier: (event: string, data?: any) => void,
+ networkSender: (flag: string, content: string) => void,
+ relayUpdater: (url: string, sp: string) => void,
+ relayGetter: () => Promise
+ ) {
+ this.notifier = notifier;
+ this.networkSender = networkSender;
+ this.relayUpdater = relayUpdater;
+ this.relayGetter = relayGetter;
+ }
+
+ // ==========================================
+ // GETTERS & SETTERS (STATE)
+ // ==========================================
+ public setProcessId(id: string | null) { this.processId = id; }
+ public setStateId(id: string | null) { this.stateId = id; }
+ public getProcessId() { return this.processId; }
+ public getStateId() { return this.stateId; }
+
+ public getDevice1() { return this.device1; } // Ajouté
+ public getDevice2Ready() { return this.device2Ready; } // Ajouté
+
+ public resetState() {
+ this.device1 = false;
+ this.device2Ready = false;
+ }
+
+ // ==========================================
+ // WALLET PROXY
+ // ==========================================
+ public isPaired() { return this.walletService.isPaired(); }
+ public getAmount() { return this.walletService.getAmount(); }
+ public getDeviceAddress() { return this.walletService.getDeviceAddress(); }
+ public dumpDeviceFromMemory() { return this.walletService.dumpDeviceFromMemory(); }
+ public dumpNeuteredDevice() { return this.walletService.dumpNeuteredDevice(); }
+ public getPairingProcessId() { return this.walletService.getPairingProcessId(); }
+ public async getDeviceFromDatabase() { return this.walletService.getDeviceFromDatabase(); }
+ public restoreDevice(d: Device) { this.walletService.restoreDevice(d); }
+ public pairDevice(pid: string, list: string[]) { this.walletService.pairDevice(pid, list); }
+ public async unpairDevice() { await this.walletService.unpairDevice(); }
+ public async saveDeviceInDatabase(d: Device) { await this.walletService.saveDeviceInDatabase(d); }
+ public async createNewDevice() {
+ return this.walletService.createNewDevice(this.currentBlockHeight > 0 ? this.currentBlockHeight : 0);
+ }
+ public async dumpWallet() { return this.walletService.dumpWallet(); }
+ public async getMemberFromDevice() { return this.walletService.getMemberFromDevice(); }
+
+ // ==========================================
+ // PROCESS PROXY
+ // ==========================================
+ public async getProcess(id: string) { return this.processService.getProcess(id); }
+ public async getProcesses() { return this.processService.getProcesses(); }
+ public async restoreProcessesFromDB() { await this.processService.getProcesses(); }
+ public getLastCommitedState(p: Process) { return this.processService.getLastCommitedState(p); }
+ public getUncommitedStates(p: Process) { return this.processService.getUncommitedStates(p); }
+ public getStateFromId(p: Process, id: string) { return this.processService.getStateFromId(p, id); }
+ public getRoles(p: Process) { return this.processService.getRoles(p); }
+ public getLastCommitedStateIndex(p: Process) { return this.processService.getLastCommitedStateIndex(p); }
+ public async batchSaveProcessesToDb(p: Record) { return this.processService.batchSaveProcesses(p); }
+
+ // ==========================================
+ // CRYPTO HELPERS
+ // ==========================================
+ public decodeValue(val: number[]) { return this.sdkService.decodeValue(val); }
+ public hexToBlob(hex: string) { return this.cryptoService.hexToBlob(hex); }
+ public hexToUInt8Array(hex: string) { return this.cryptoService.hexToUInt8Array(hex); }
+ public async blobToHex(blob: Blob) { return this.cryptoService.blobToHex(blob); }
+ public getHashForFile(c: string, l: string, f: any) { return this.cryptoService.getHashForFile(c, l, f); }
+ public getMerkleProofForFile(s: ProcessState, a: string) { return this.cryptoService.getMerkleProofForFile(s, a); }
+ public validateMerkleProof(p: MerkleProofResult, h: string) { return this.cryptoService.validateMerkleProof(p, h); }
+ private splitData(obj: Record) { return this.cryptoService.splitData(obj); }
+
+ // ==========================================
+ // MEMBERS
+ // ==========================================
+ public getAllMembers() { return this.membersList; }
+ public getAllMembersSorted() {
+ return Object.fromEntries(Object.entries(this.membersList).sort(([keyA], [keyB]) => keyA.localeCompare(keyB)));
+ }
+ public async ensureMembersAvailable(): Promise {
+ if (Object.keys(this.membersList).length > 0) return;
+ console.warn('[CoreWorker] Tentative de récupération des membres...');
+ }
+ public getAddressesForMemberId(memberId: string): string[] | null {
+ if (!this.membersList[memberId]) return null;
+ return this.membersList[memberId].sp_addresses;
+ }
+ public compareMembers(memberA: string[], memberB: string[]): boolean {
+ if (!memberA || !memberB) return false;
+ if (memberA.length !== memberB.length) return false;
+ return memberA.every((item) => memberB.includes(item)) && memberB.every((item) => memberA.includes(item));
+ }
+
+ // ==========================================
+ // UTILITAIRES DIVERS
+ // ==========================================
+ public createFaucetMessage() {
+ return this.sdkService.getClient().create_faucet_msg();
+ }
+
+ public isChildRole(parent: any, child: any): boolean {
+ try {
+ this.sdkService.getClient().is_child_role(JSON.stringify(parent), JSON.stringify(child));
+ return true;
+ } catch (e) {
+ console.error(e);
+ return false;
+ }
+ }
+
+ // ==========================================
+ // LOGIQUE HANDSHAKE
+ // ==========================================
+ public async handleHandshakeMsg(url: string, parsedMsg: any) {
+ try {
+ const handshakeMsg: HandshakeMessage = JSON.parse(parsedMsg);
+
+ if (handshakeMsg.sp_address && this.relayUpdater) {
+ await this.relayUpdater(url, handshakeMsg.sp_address);
+ }
+ this.currentBlockHeight = handshakeMsg.chain_tip;
+
+ if (!this.isPaired()) {
+ console.log(`[CoreWorker] ⏳ Non pairé. Attente appairage...`);
+ }
+
+ this.updateDeviceBlockHeight();
+
+ if (handshakeMsg.peers_list) {
+ this.membersList = { ...this.membersList, ...handshakeMsg.peers_list as Record };
+ }
+
+ if (handshakeMsg.processes_list) {
+ await this.syncProcessesFromHandshake(handshakeMsg.processes_list);
+ }
+ } catch (e) {
+ console.error("Handshake Error", e);
+ }
+ }
+
+ public async updateDeviceBlockHeight() {
+ if (this.currentBlockHeight <= 0) return;
+ const device = await this.walletService.getDeviceFromDatabase();
+ if (!device) return;
+
+ if (device.sp_wallet.birthday === 0) {
+ device.sp_wallet.birthday = this.currentBlockHeight;
+ device.sp_wallet.last_scan = this.currentBlockHeight;
+ await this.walletService.saveDeviceInDatabase(device);
+ this.walletService.restoreDevice(device);
+ } else if (device.sp_wallet.last_scan < this.currentBlockHeight) {
+ console.log(`[CoreWorker] Scan requis de ${device.sp_wallet.last_scan} à ${this.currentBlockHeight}`);
+ try {
+ await this.sdkService.getClient().scan_blocks(this.currentBlockHeight, APP_CONFIG.URLS.BLINDBIT);
+ const updatedDevice = this.walletService.dumpDeviceFromMemory();
+ await this.walletService.saveDeviceInDatabase(updatedDevice);
+ } catch (e) {
+ console.error('Scan error', e);
+ }
+ }
+ }
+
+ private async syncProcessesFromHandshake(newProcesses: OutPointProcessMap) {
+ if (!newProcesses || Object.keys(newProcesses).length === 0) return;
+ console.log(`[CoreWorker] Synchro ${Object.keys(newProcesses).length} processus...`);
+
+ const toSave: Record = {};
+ const currentProcesses = await this.getProcesses();
+
+ if (Object.keys(currentProcesses).length === 0) {
+ await this.processService.batchSaveProcesses(newProcesses);
+ } else {
+ for (const [processId, process] of Object.entries(newProcesses)) {
+ const existing = currentProcesses[processId];
+ if (existing) {
+ let newStates: string[] = [];
+ let newRoles: Record[] = [];
+
+ for (const state of process.states) {
+ if (!state || !state.state_id) continue;
+
+ if (state.state_id === APP_CONFIG.EMPTY_32_BYTES) {
+ const existingTip = existing.states[existing.states.length - 1].commited_in;
+ if (existingTip !== state.commited_in) {
+ existing.states.pop();
+ existing.states.push(state);
+ toSave[processId] = existing;
+ }
+ } else if (!this.processService.getStateFromId(existing, state.state_id)) {
+ const existingLast = existing.states.pop();
+ if (existingLast) {
+ existing.states.push(state);
+ existing.states.push(existingLast);
+ toSave[processId] = existing;
+ if (this.rolesContainsUs(state.roles)) {
+ newStates.push(state.state_id);
+ newRoles.push(state.roles);
+ }
+ }
+ } else {
+ const existingState = this.processService.getStateFromId(existing, state.state_id);
+ if (existingState && (!existingState.keys || Object.keys(existingState.keys).length === 0)) {
+ if (this.rolesContainsUs(state.roles)) {
+ newStates.push(state.state_id);
+ newRoles.push(state.roles);
+ }
+ }
+ }
+ }
+
+ if (newStates.length > 0) {
+ await this.ensureConnections(existing);
+ await this.requestDataFromPeers(processId, newStates, newRoles);
+ }
+ } else {
+ toSave[processId] = process;
+ }
+ }
+ if (Object.keys(toSave).length > 0) {
+ await this.processService.batchSaveProcesses(toSave);
+ }
+ }
+ if (this.notifier) this.notifier('processes-updated');
+ }
+
+ // ==========================================
+ // LOGIQUE MÉTIER
+ // ==========================================
+ public async getMyProcesses(): Promise {
+ try {
+ const pid = this.getPairingProcessId();
+ return await this.processService.getMyProcesses(pid);
+ } catch (e) {
+ return null;
+ }
+ }
+
+ public async ensureConnections(process: Process, stateId: string | null = null): Promise {
+ console.info(`[CoreWorker] 🔄 Check connexions (StateID: ${stateId || 'default'})`);
+ if (!process) return;
+
+ let state: ProcessState | null = null;
+ if (stateId) state = this.processService.getStateFromId(process, stateId);
+ if (!state && process.states.length >= 2) state = process.states[process.states.length - 2];
+ if (!state) return;
+
+ await this.ensureMembersAvailable();
+ const members = new Set();
+
+ if (state.roles) {
+ for (const role of Object.values(state.roles)) {
+ for (const memberId of role.members) {
+ const addrs = this.getAddressesForMemberId(memberId);
+ if (addrs) members.add({ sp_addresses: addrs });
+ }
+ }
+ }
+
+ if (members.size === 0) {
+ let publicData: Record | null = null;
+ for (let i = process.states.length - 1; i >= 0; i--) {
+ const s = process.states[i];
+ if (s.public_data && s.public_data['pairedAddresses']) {
+ publicData = s.public_data;
+ break;
+ }
+ }
+ if (publicData && publicData['pairedAddresses']) {
+ const decoded = this.decodeValue(publicData['pairedAddresses']);
+ if (decoded) members.add({ sp_addresses: decoded });
+ }
+ }
+
+ if (members.size === 0) return;
+
+ const unconnected = new Set();
+ const myAddress = this.getDeviceAddress();
+ for (const member of Array.from(members)) {
+ if (!member.sp_addresses) continue;
+ for (const address of member.sp_addresses) {
+ if (address === myAddress) continue;
+ if ((await this.getSecretForAddress(address)) === null) unconnected.add(address);
+ }
+ }
+
+ if (unconnected.size > 0) {
+ console.log(`[CoreWorker] 📡 ${unconnected.size} non connectés. Connexion...`);
+ await this.connectAddresses(Array.from(unconnected));
+ }
+ }
+
+ public async connectAddresses(addresses: string[]): Promise {
+ if (addresses.length === 0) return null;
+ const feeRate = APP_CONFIG.FEE_RATE;
+ try {
+ return this.sdkService.getClient().create_transaction(addresses, feeRate);
+ } catch (error: any) {
+ if (String(error).includes('Insufficient funds')) {
+ await this.getTokensFromFaucet();
+ return this.sdkService.getClient().create_transaction(addresses, feeRate);
+ } else {
+ throw error;
+ }
+ }
+ }
+
+ private async getTokensFromFaucet(): Promise {
+ console.log('[CoreWorker] 🚰 Demande Faucet...');
+ const availableAmt = this.getAmount();
+ const target: BigInt = APP_CONFIG.DEFAULT_AMOUNT * BigInt(10);
+ if (availableAmt < target) {
+ const msg = this.sdkService.getClient().create_faucet_msg();
+ if (this.networkSender) this.networkSender('Faucet', msg);
+
+ let attempts = 3;
+ while (attempts > 0) {
+ if (this.getAmount() >= target) return;
+ attempts--;
+ await new Promise((r) => setTimeout(r, APP_CONFIG.TIMEOUTS.RETRY_DELAY));
+ }
+ throw new Error('Montant insuffisant après faucet');
+ }
+ }
+
+ public async createPairingProcess(userName: string, pairWith: string[]): Promise {
+ if (this.isPaired()) throw new Error('Déjà appairé');
+ const myAddress = this.getDeviceAddress();
+ pairWith.push(myAddress);
+ const privateData = { description: 'pairing', counter: 0 };
+ const publicData = { memberPublicName: userName, pairedAddresses: pairWith };
+ const validation_fields = [...Object.keys(privateData), ...Object.keys(publicData), 'roles'];
+ const roles = {
+ pairing: {
+ members: [],
+ validation_rules: [{ quorum: 1.0, fields: validation_fields, min_sig_member: 1.0 }],
+ storages: [APP_CONFIG.URLS.STORAGE],
+ },
+ };
+ return this.createProcess(privateData, publicData, roles);
+ }
+
+ public async createProcess(privateData: any, publicData: any, roles: any, feeRate = APP_CONFIG.FEE_RATE): Promise {
+ // Appel au main thread pour avoir l'adresse du relais
+ const relay = this.relayGetter ? await this.relayGetter() : '';
+ if (!relay) throw new Error("Aucun relais disponible");
+
+ const { encodedPrivateData, encodedPublicData } = await this.prepareProcessData(privateData, publicData);
+ const members = this.membersList;
+ try {
+ return await this.attemptCreateProcess(encodedPrivateData, roles, encodedPublicData, relay, feeRate, members);
+ } catch (e: any) {
+ if (String(e).includes('Insufficient funds')) {
+ await this.getTokensFromFaucet();
+ return await this.attemptCreateProcess(encodedPrivateData, roles, encodedPublicData, relay, feeRate, members);
+ }
+ throw e;
+ }
+ }
+
+ private async attemptCreateProcess(priv: any, roles: any, pub: any, relay: string, fee: number, members: any): Promise {
+ const res = this.sdkService.getClient().create_new_process(priv, roles, pub, relay, fee, members);
+ if (res.updated_process) {
+ await this.ensureConnections(res.updated_process.current_process);
+ }
+ return res;
+ }
+
+ public async updateProcess(processId: string, newData: any, privateFields: string[], roles: any): Promise {
+ const process = await this.processService.getProcess(processId);
+ if (!process) throw new Error('Process not found');
+
+ let lastState = this.processService.getLastCommitedState(process);
+ let currentProcess = process;
+
+ if (!lastState) {
+ const first = process.states[0];
+ if (this.rolesContainsUs(first.roles)) {
+ const appRes = await this.approveChange(processId, first.state_id);
+ await this.handleApiReturn(appRes);
+ const prdRes = await this.createPrdUpdate(processId, first.state_id);
+ await this.handleApiReturn(prdRes);
+ } else if (first.validation_tokens.length > 0) {
+ const res = await this.createPrdUpdate(processId, first.state_id);
+ await this.handleApiReturn(res);
+ }
+ const updated = await this.processService.getProcess(processId);
+ if (updated) currentProcess = updated;
+ lastState = this.processService.getLastCommitedState(currentProcess);
+ if (!lastState) throw new Error('Still no commited state');
+ }
+
+ const lastStateIndex = this.getLastCommitedStateIndex(currentProcess);
+ if (lastStateIndex === null) throw new Error('Index commited introuvable');
+
+ const privateData: any = {};
+ const publicData: any = {};
+
+ for (const field of Object.keys(newData)) {
+ if (lastState.public_data[field]) {
+ publicData[field] = newData[field];
+ continue;
+ }
+ if (privateFields.includes(field)) {
+ privateData[field] = newData[field];
+ continue;
+ }
+ let isPrivate = false;
+ for (let i = lastStateIndex; i >= 0; i--) {
+ if (currentProcess.states[i].pcd_commitment[field]) {
+ privateData[field] = newData[field];
+ isPrivate = true;
+ break;
+ }
+ }
+ if (!isPrivate) publicData[field] = newData[field];
+ }
+
+ const finalRoles = roles || this.processService.getRoles(currentProcess);
+ const { encodedPrivateData, encodedPublicData } = await this.prepareProcessData(privateData, publicData);
+
+ const res = this.sdkService.getClient().update_process(currentProcess, encodedPrivateData, finalRoles, encodedPublicData, this.membersList);
+ if (res.updated_process) await this.ensureConnections(res.updated_process.current_process);
+ return res;
+ }
+
+ private async prepareProcessData(priv: any, pub: any) {
+ const p1 = this.splitData(priv);
+ const p2 = this.splitData(pub);
+ return {
+ encodedPrivateData: { ...this.sdkService.getClient().encode_json(p1.jsonCompatibleData), ...this.sdkService.getClient().encode_binary(p1.binaryData) },
+ encodedPublicData: { ...this.sdkService.getClient().encode_json(p2.jsonCompatibleData), ...this.sdkService.getClient().encode_binary(p2.binaryData) },
+ };
+ }
+
+ // ==========================================
+ // API METHODS (Actions)
+ // ==========================================
+ public async createPrdUpdate(pid: string, sid: string) {
+ const p = await this.getProcess(pid);
+ await this.ensureConnections(p!);
+ return this.sdkService.getClient().create_update_message(p, sid, this.membersList);
+ }
+ public async createPrdResponse(pid: string, sid: string) {
+ const p = await this.getProcess(pid);
+ return this.sdkService.getClient().create_response_prd(p, sid, this.membersList);
+ }
+ public async approveChange(pid: string, sid: string) {
+ const p = await this.getProcess(pid);
+ const res = this.sdkService.getClient().validate_state(p, sid, this.membersList);
+ if (res.updated_process) await this.ensureConnections(res.updated_process.current_process);
+ return res;
+ }
+ public async rejectChange(pid: string, sid: string) {
+ const p = await this.getProcess(pid);
+ return this.sdkService.getClient().refuse_state(p, sid);
+ }
+ public async requestDataFromPeers(pid: string, sids: string[], roles: any) {
+ const res = this.sdkService.getClient().request_data(pid, sids, roles, this.membersList);
+ await this.handleApiReturn(res);
+ }
+
+ public async resetDevice() {
+ this.sdkService.getClient().reset_device();
+ await this.db.clearMultipleStores(['wallet', 'shared_secrets', 'unconfirmed_secrets', 'processes', 'diffs']);
+ }
+
+ public async handleApiReturn(res: ApiReturn) {
+ if (!res || Object.keys(res).length === 0) return;
+ try {
+ const txData = (res.partial_tx ? await this.handlePartialTx(res.partial_tx) : null) || res.new_tx_to_send;
+ if (txData && txData.transaction.length != 0) {
+ if (this.networkSender) this.networkSender('NewTx', JSON.stringify(txData));
+ await new Promise((r) => setTimeout(r, APP_CONFIG.TIMEOUTS.API_DELAY));
+ }
+ if (res.secrets) await this.handleSecrets(res.secrets);
+ if (res.updated_process) await this.handleUpdatedProcess(res.updated_process);
+ if (res.push_to_storage) await this.handlePushToStorage(res.push_to_storage);
+
+ if (res.commit_to_send && this.networkSender) this.networkSender('Commit', JSON.stringify(res.commit_to_send));
+ if (res.ciphers_to_send && this.networkSender) for (const c of res.ciphers_to_send) this.networkSender('Cipher', c);
+ } catch (e) {
+ console.error('ApiReturn Error:', e);
+ }
+ }
+
+ private async handlePartialTx(partialTx: any): Promise {
+ try {
+ return this.sdkService.getClient().sign_transaction(partialTx).new_tx_to_send;
+ } catch (e) {
+ return null;
+ }
+ }
+
+ private async handleSecrets(secrets: any) {
+ const { unconfirmed_secrets, shared_secrets } = secrets;
+ const unconfirmedList = unconfirmed_secrets && unconfirmed_secrets.length > 0 ? unconfirmed_secrets : [];
+ const sharedList = shared_secrets && Object.keys(shared_secrets).length > 0
+ ? Object.entries(shared_secrets).map(([key, value]) => ({ key, value }))
+ : [];
+
+ if (unconfirmedList.length > 0 || sharedList.length > 0) {
+ await this.db.saveSecretsBatch(unconfirmedList, sharedList);
+ }
+ }
+
+ private async handleUpdatedProcess(updated: any) {
+ const pid = updated.process_id;
+ if (updated.encrypted_data) {
+ for (const [h, c] of Object.entries(updated.encrypted_data as Record)) await this.saveBlobToDb(h, this.hexToBlob(c));
+ }
+ await this.processService.saveProcessToDb(pid, updated.current_process);
+ if (updated.diffs) await this.saveDiffsToDb(updated.diffs);
+
+ this._resolvePendingKeyRequests(pid, updated.current_process);
+ // Notification UI
+ if (this.notifier) this.notifier('processes-updated');
+ }
+
+ public async saveDiffsToDb(diffs: UserDiff[]) {
+ await this.db.saveDiffs(diffs);
+ }
+
+ private _resolvePendingKeyRequests(processId: string, process: Process) {
+ if (this.pendingKeyRequests.size === 0) return;
+ for (const state of process.states) {
+ if (!state.keys) continue;
+ for (const [attr, key] of Object.entries(state.keys)) {
+ const rid = `${processId}_${state.state_id}_${attr}`;
+ if (this.pendingKeyRequests.has(rid)) {
+ this.pendingKeyRequests.get(rid)?.(key as string);
+ this.pendingKeyRequests.delete(rid);
+ }
+ }
+ }
+ }
+
+ private async handlePushToStorage(hashes: string[]) {
+ for (const hash of hashes) {
+ try {
+ const blob = await this.getBlobFromDb(hash);
+ const diff = await this.getDiffByValue(hash);
+ if (blob && diff) await this.saveDataToStorage(diff.storages, hash, blob, null);
+ } catch (e) {
+ console.error('Push error', e);
+ }
+ }
+ }
+
+ public async handleCommitError(response: string) {
+ const content = JSON.parse(response);
+ const errorMsg = content.error['GenericError'];
+ if (!['State is identical to the previous state', 'Not enough valid proofs'].includes(errorMsg)) {
+ // Retry via network callback
+ if (this.networkSender) {
+ setTimeout(() => this.networkSender!('Commit', JSON.stringify(content)), APP_CONFIG.TIMEOUTS.RETRY_DELAY);
+ }
+ }
+ }
+
+ public rolesContainsUs(roles: any) {
+ return this.processService.rolesContainsMember(roles, this.getPairingProcessId());
+ }
+
+ public async getSecretForAddress(address: string): Promise {
+ return await this.db.getSharedSecret(address);
+ }
+
+ public async getAllDiffs(): Promise> {
+ return await this.db.getAllDiffs();
+ }
+
+ public async getDiffByValue(value: string): Promise {
+ return await this.db.getDiff(value);
+ }
+
+ public async getAllSecrets(): Promise {
+ return await this.db.getAllSecrets();
+ }
+
+ // ==========================================
+ // STORAGE & DB
+ // ==========================================
+ public async saveBlobToDb(h: string, d: Blob) {
+ await this.db.saveBlob(h, d);
+ }
+ public async getBlobFromDb(h: string) {
+ return await this.db.getBlob(h);
+ }
+ public async fetchValueFromStorage(h: string) {
+ return retrieveData([APP_CONFIG.URLS.STORAGE], h);
+ }
+ public async saveDataToStorage(s: string[], h: string, d: Blob, ttl: number | null) {
+ return storeData(s, h, d, ttl);
+ }
+
+ // ==========================================
+ // HELPERS
+ // ==========================================
+ public getProcessName(p: Process) {
+ const pub = this.getPublicData(p);
+ if (pub && pub['processName']) return this.decodeValue(pub['processName']);
+ return null;
+ }
+ public getPublicData(p: Process) {
+ const last = this.getLastCommitedState(p);
+ return last ? last.public_data : p.states[0]?.public_data || null;
+ }
+ public updateMemberPublicName(pid: string, name: string) {
+ return this.updateProcess(pid, { memberPublicName: name }, [], null);
+ }
+
+ // ==========================================
+ // UI HELPERS
+ // ==========================================
+ public getNotifications() {
+ return this.notifications;
+ }
+ public setNotifications(n: any[]) {
+ this.notifications = n;
+ }
+
+ // ==========================================
+ // PARSING & RESEAU ENTRANT
+ // ==========================================
+ async parseCipher(msg: string) {
+ try {
+ const res = this.sdkService.getClient().parse_cipher(msg, this.membersList, await this.getProcesses());
+ await this.handleApiReturn(res);
+ } catch (e) {
+ console.error('Cipher Error', e);
+ }
+ }
+
+ async parseNewTx(msg: string) {
+ const parsed = JSON.parse(msg);
+ if (parsed.error) return;
+
+ const prevouts = this.sdkService.getClient().get_prevouts(parsed.transaction);
+ for (const p of Object.values(await this.getProcesses())) {
+ const tip = p.states[p.states.length - 1].commited_in;
+ if (prevouts.includes(tip)) {
+ const newTip = this.sdkService.getClient().get_txid(parsed.transaction);
+ const newStateId = this.sdkService.getClient().get_opreturn(parsed.transaction);
+ this.sdkService.getClient().process_commit_new_state(p, newStateId, newTip);
+ break;
+ }
+ }
+
+ try {
+ const res = this.sdkService.getClient().parse_new_tx(msg, 0, this.membersList);
+ if (res && (res.partial_tx || res.new_tx_to_send || res.secrets || res.updated_process)) {
+ await this.handleApiReturn(res);
+ const d = this.dumpDeviceFromMemory();
+ const old = await this.getDeviceFromDatabase();
+ if (old && old.pairing_process_commitment) d.pairing_process_commitment = old.pairing_process_commitment;
+ await this.saveDeviceInDatabase(d);
+ }
+ } catch (e) { }
+ }
+
+ // ==========================================
+ // BACKUP & RESTORE
+ // ==========================================
+ public async importJSON(backup: BackUp) {
+ await this.resetDevice();
+ await this.walletService.saveDeviceInDatabase(backup.device);
+ this.walletService.restoreDevice(backup.device);
+ await this.processService.batchSaveProcesses(backup.processes);
+ await this.restoreSecretsFromBackUp(backup.secrets);
+ }
+ public async restoreSecretsFromBackUp(secretsStore: SecretsStore) {
+ const sharedList = Object.entries(secretsStore.shared_secrets).map(([key, value]) => ({ key, value }));
+ await this.db.saveSecretsBatch(secretsStore.unconfirmed_secrets, sharedList);
+ await this.restoreSecretsFromDB();
+ }
+ public async restoreSecretsFromDB() {
+ const secretsStore = await this.db.getAllSecrets();
+ this.sdkService.getClient().set_shared_secrets(JSON.stringify(secretsStore));
+ console.log("[CoreWorker] 🔐 Secrets restaurés depuis la DB");
+ }
+ public async createBackUp() {
+ const device = await this.walletService.getDeviceFromDatabase();
+ if (!device) return null;
+ return { device, processes: await this.processService.getProcesses(), secrets: await this.getAllSecrets() };
+ }
+
+ // ==========================================
+ // DECRYPT ATTRIBUTE
+ // ==========================================
+ public async decryptAttribute(processId: string, state: ProcessState, attribute: string): Promise {
+ console.groupCollapsed(`[CoreWorker] 🔑 Déchiffrement de '${attribute}' (Process: ${processId})`);
+
+ try {
+ let hash: string | null | undefined = state.pcd_commitment[attribute];
+ let key: string | null | undefined = state.keys[attribute];
+ const pairingProcessId = this.getPairingProcessId();
+
+ if (!hash) {
+ console.warn(`⚠️ L'attribut n'existe pas (pas de hash).`);
+ return null;
+ }
+
+ if (!key) {
+ if (!this._checkAccess(state, attribute, pairingProcessId)) {
+ console.log(`⛔ Accès non autorisé. Abandon.`);
+ return null;
+ }
+ const result = await this._fetchMissingKey(processId, state, attribute);
+ hash = result.hash;
+ key = result.key;
+ }
+
+ if (hash && key) {
+ const blob = await this.getBlobFromDb(hash);
+ if (!blob) {
+ console.error(`💥 Échec: Blob non trouvé en BDD pour le hash ${hash}`);
+ return null;
+ }
+
+ try {
+ const buf = await blob.arrayBuffer();
+ const cipher = new Uint8Array(buf);
+ const keyUIntArray = this.hexToUInt8Array(key);
+
+ const clear = this.sdkService.getClient().decrypt_data(keyUIntArray, cipher);
+ if (!clear) throw new Error('decrypt_data returned null');
+
+ const decoded = this.sdkService.getClient().decode_value(clear);
+ console.log(`✅ Attribut '${attribute}' déchiffré avec succès.`);
+ return decoded;
+ } catch (e) {
+ console.error(`💥 Échec du déchiffrement: ${e}`);
+ return null;
+ }
+ }
+ return null;
+ } catch (error) {
+ console.error(`💥 Erreur:`, error);
+ return null;
+ } finally {
+ console.groupEnd();
+ }
+ }
+
+ private _checkAccess(state: ProcessState, attribute: string, pairingProcessId: string): boolean {
+ const roles = state.roles;
+ return Object.values(roles).some((role) => {
+ const isMember = role.members.includes(pairingProcessId);
+ if (!isMember) return false;
+ return Object.values(role.validation_rules).some((rule) => rule.fields.includes(attribute));
+ });
+ }
+
+ private async _fetchMissingKey(processId: string, state: ProcessState, attribute: string): Promise<{ hash: string | null; key: string | null }> {
+ try {
+ const process = await this.getProcess(processId);
+ if (!process) return { hash: null, key: null };
+
+ await this.ensureConnections(process);
+ await this.requestDataFromPeers(processId, [state.state_id], [state.roles]);
+
+ const requestId = `${processId}_${state.state_id}_${attribute}`;
+ const keyRequestPromise = new Promise((resolve, reject) => {
+ const timeout = setTimeout(() => {
+ this.pendingKeyRequests.delete(requestId);
+ reject(new Error(`Timeout waiting for key: ${attribute}`));
+ }, APP_CONFIG.TIMEOUTS.KEY_REQUEST);
+
+ this.pendingKeyRequests.set(requestId, (key: string) => {
+ clearTimeout(timeout);
+ resolve(key);
+ });
+ });
+
+ const receivedKey = await keyRequestPromise;
+ const updatedProcess = await this.getProcess(processId);
+ if (!updatedProcess) return { hash: null, key: null };
+
+ const updatedState = this.getStateFromId(updatedProcess, state.state_id);
+ const updatedHash = updatedState ? updatedState.pcd_commitment[attribute] : state.pcd_commitment[attribute];
+
+ return { hash: updatedHash, key: receivedKey };
+ } catch (e) {
+ return { hash: null, key: null };
+ }
+ }
+}
+
+Comlink.expose(new CoreBackend());
\ No newline at end of file
diff --git a/src/workers/network.worker.ts b/src/workers/network.worker.ts
new file mode 100644
index 0000000..d576930
--- /dev/null
+++ b/src/workers/network.worker.ts
@@ -0,0 +1,150 @@
+import * as Comlink from 'comlink';
+import { APP_CONFIG } from '../config/constants';
+
+// On redéfinit le type localement pour éviter d'importer tout le SDK WASM ici
+type AnkFlag = 'Handshake' | 'NewTx' | 'Cipher' | 'Commit' | 'Faucet' | 'Ping';
+
+export class NetworkBackend {
+ private sockets: Map = new Map();
+ private relayAddresses: Map = new Map(); // wsUrl -> spAddress
+ private messageQueue: string[] = [];
+
+ // Callback pour notifier le Main Thread
+ private msgCallback: ((flag: string, content: string, url: string) => void) | null = null;
+ private statusCallback: ((url: string, status: 'OPEN' | 'CLOSED', spAddress?: string) => void) | null = null;
+
+ // Timers pour la gestion des reconnexions
+ private reconnectTimers: Map = new Map();
+ private heartbeatInterval: any = null;
+
+ constructor() {
+ this.startHeartbeat();
+ }
+
+ public setCallbacks(
+ msgCb: (flag: string, content: string, url: string) => void,
+ statusCb: (url: string, status: 'OPEN' | 'CLOSED', spAddress?: string) => void
+ ) {
+ this.msgCallback = msgCb;
+ this.statusCallback = statusCb;
+ }
+
+ public async connect(url: string) {
+ if (this.sockets.has(url) && this.sockets.get(url)?.readyState === WebSocket.OPEN) return;
+
+ console.log(`[NetworkWorker] 🔌 Connexion à ${url}...`);
+ const ws = new WebSocket(url);
+
+ ws.onopen = () => {
+ console.log(`[NetworkWorker] ✅ Connecté à ${url}`);
+ this.sockets.set(url, ws);
+
+ // Reset timer reconnexion si existant
+ if (this.reconnectTimers.has(url)) {
+ clearTimeout(this.reconnectTimers.get(url));
+ this.reconnectTimers.delete(url);
+ }
+
+ // Vider la file d'attente (si message en attente pour ce socket ou broadcast)
+ this.flushQueue();
+
+ if (this.statusCallback) this.statusCallback(url, 'OPEN');
+ };
+
+ ws.onmessage = (event) => {
+ try {
+ const msg = JSON.parse(event.data);
+ // Si c'est un Handshake, on met à jour la map locale
+ if (msg.flag === 'Handshake' && msg.content) {
+ const handshake = JSON.parse(msg.content);
+ if (handshake.sp_address) {
+ this.relayAddresses.set(url, handshake.sp_address);
+ if (this.statusCallback) this.statusCallback(url, 'OPEN', handshake.sp_address);
+ }
+ }
+
+ // On remonte TOUT au Main Thread (qui passera au Core)
+ if (this.msgCallback) this.msgCallback(msg.flag, msg.content, url);
+ } catch (e) {
+ console.error('[NetworkWorker] Erreur parsing message:', e);
+ }
+ };
+
+ ws.onerror = (e) => {
+ // console.error(`[NetworkWorker] Erreur sur ${url}`, e);
+ };
+
+ ws.onclose = () => {
+ console.warn(`[NetworkWorker] ❌ Déconnecté de ${url}.`);
+ this.sockets.delete(url);
+ this.relayAddresses.set(url, ''); // Reset spAddress
+
+ if (this.statusCallback) this.statusCallback(url, 'CLOSED');
+ this.scheduleReconnect(url);
+ };
+ }
+
+ public sendMessage(flag: AnkFlag, content: string) {
+ const msgStr = JSON.stringify({ flag, content });
+
+ // Stratégie simple : On envoie à TOUS les relais connectés (Broadcast)
+ // Ou on pourrait cibler un relais spécifique si besoin.
+ let sent = false;
+ for (const [url, ws] of this.sockets) {
+ if (ws.readyState === WebSocket.OPEN) {
+ ws.send(msgStr);
+ sent = true;
+ }
+ }
+
+ if (!sent) {
+ // console.warn(`[NetworkWorker] Pas de connexion. Message ${flag} mis en file.`);
+ this.messageQueue.push(msgStr);
+ }
+ }
+
+ public getAvailableRelay(): string | null {
+ // Retourne l'adresse SP d'un relais connecté
+ for (const sp of this.relayAddresses.values()) {
+ if (sp && sp !== '') return sp;
+ }
+ return null;
+ }
+
+ public getAllRelays() {
+ return Object.fromEntries(this.relayAddresses);
+ }
+
+ // --- INTERNES ---
+
+ private flushQueue() {
+ while (this.messageQueue.length > 0) {
+ const msg = this.messageQueue.shift();
+ if (!msg) break;
+ for (const ws of this.sockets.values()) {
+ if (ws.readyState === WebSocket.OPEN) ws.send(msg);
+ }
+ }
+ }
+
+ private scheduleReconnect(url: string) {
+ if (this.reconnectTimers.has(url)) return;
+
+ console.log(`[NetworkWorker] ⏳ Reconnexion à ${url} dans 3s...`);
+ const timer = setTimeout(() => {
+ this.reconnectTimers.delete(url);
+ this.connect(url);
+ }, 3000); // Délai fixe ou APP_CONFIG.TIMEOUTS.RETRY_DELAY
+
+ this.reconnectTimers.set(url, timer);
+ }
+
+ private startHeartbeat() {
+ this.heartbeatInterval = setInterval(() => {
+ // Envoi d'un ping léger ou gestion du keep-alive
+ // this.sendMessage('Ping', '');
+ }, 30000);
+ }
+}
+
+Comlink.expose(new NetworkBackend());
\ No newline at end of file