Compare commits
No commits in common. "4f86b26890b6b213b0bdddd049878f282d722621" and "f7b912940134698bb9273b9e2daea162d1927088" have entirely different histories.
4f86b26890
...
f7b9129401
@ -29,7 +29,6 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.7.8",
|
"axios": "^1.7.8",
|
||||||
"comlink": "^4.4.2",
|
|
||||||
"jose": "^6.0.11",
|
"jose": "^6.0.11",
|
||||||
"vite-plugin-wasm": "^3.3.0"
|
"vite-plugin-wasm": "^3.3.0"
|
||||||
}
|
}
|
||||||
|
|||||||
46
src/main.ts
46
src/main.ts
@ -1,3 +1,4 @@
|
|||||||
|
import Database from './services/database.service';
|
||||||
import Services from './services/service';
|
import Services from './services/service';
|
||||||
import { Router } from './router/index';
|
import { Router } from './router/index';
|
||||||
import './components/header/Header';
|
import './components/header/Header';
|
||||||
@ -5,58 +6,53 @@ import './App';
|
|||||||
import { IframeController } from './services/iframe-controller.service';
|
import { IframeController } from './services/iframe-controller.service';
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
console.log("🚀 Démarrage de l'application 4NK (Multi-Worker Architecture)...");
|
console.log("🚀 Démarrage de l'application 4NK...");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. Initialisation des Services (Proxy vers Core & Network Workers)
|
// 1. Initialisation des Services (WASM, Sockets, Database...)
|
||||||
// Cela va lancer les workers en arrière-plan
|
|
||||||
const services = await Services.getInstance();
|
const services = await Services.getInstance();
|
||||||
|
|
||||||
// ❌ SUPPRIMÉ : await Database.getInstance();
|
// 2. Initialisation de la base de données (Web Worker + Service Worker)
|
||||||
// La BDD est maintenant gérée de manière autonome par le CoreWorker.
|
await Database.getInstance();
|
||||||
|
|
||||||
// Injection du Header
|
// Injection du Header dans le slot prévu dans index.html
|
||||||
const headerSlot = document.getElementById('header-slot');
|
const headerSlot = document.getElementById('header-slot');
|
||||||
if (headerSlot) {
|
if (headerSlot) {
|
||||||
headerSlot.innerHTML = '<app-header></app-header>';
|
headerSlot.innerHTML = '<app-header></app-header>';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Vérification / Création de l'appareil (via le Worker)
|
// Vérification basique de l'appareil (logique reprise de ton ancien router.ts)
|
||||||
const device = await services.getDeviceFromDatabase();
|
const device = await services.getDeviceFromDatabase();
|
||||||
if (!device) {
|
if (!device) {
|
||||||
console.log('✨ Nouvel appareil détecté, création en cours via Worker...');
|
console.log('✨ Nouvel appareil détecté, création en cours...');
|
||||||
await services.createNewDevice();
|
await services.createNewDevice();
|
||||||
} else {
|
} else {
|
||||||
console.log("Restauration de l'appareil...");
|
console.log("Restauration de l'appareil...");
|
||||||
await services.restoreDevice(device);
|
services.restoreDevice(device);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Initialisation du contrôleur d'Iframe (Reste sur le Main Thread pour écouter window)
|
// Initialisation du contrôleur d'Iframe (API listeners)
|
||||||
await IframeController.init();
|
await IframeController.init();
|
||||||
|
|
||||||
// 4. Restauration des données (Appels asynchrones au Worker)
|
// 3. Restauration des données
|
||||||
await services.restoreProcessesFromDB();
|
await services.restoreProcessesFromDB();
|
||||||
|
await services.restoreSecretsFromDB();
|
||||||
|
|
||||||
// ⚠️ Assurez-vous d'avoir ajouté 'restoreSecretsFromDB' dans votre CoreWorker & Services
|
// 4. Connexion réseau
|
||||||
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();
|
await services.connectAllRelays();
|
||||||
|
|
||||||
// 6. Gestion du Routing
|
// 5. Démarrage du Routeur (Affichage de la page)
|
||||||
const isIframe = window.self !== window.top;
|
const isIframe = window.self !== window.top;
|
||||||
const isPaired = await services.isPaired(); // Appel async maintenant
|
|
||||||
|
|
||||||
if (isPaired && !isIframe) {
|
// On redirige vers 'process' SEULEMENT si on est appairé ET qu'on n'est PAS dans une iframe
|
||||||
|
if (services.isPaired() && !isIframe) {
|
||||||
console.log('✅ Mode Standalone & Appairé : Redirection vers Process.');
|
console.log('✅ Mode Standalone & Appairé : Redirection vers Process.');
|
||||||
window.history.replaceState({}, '', 'process');
|
window.history.replaceState({}, '', 'process');
|
||||||
Router.handleLocation();
|
Router.handleLocation();
|
||||||
} else {
|
} else {
|
||||||
console.log(isIframe ? '📡 Mode Iframe détecté : Attente API.' : '🆕 Non appairé : Démarrage sur Home.');
|
// 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.');
|
||||||
Router.init();
|
Router.init();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -64,4 +60,6 @@ async function bootstrap() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bootstrap();
|
// Lancement
|
||||||
|
bootstrap();
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
// src/pages/process/ProcessList.ts
|
||||||
|
|
||||||
import processHtml from "./process.html?raw";
|
import processHtml from "./process.html?raw";
|
||||||
import globalCss from "../../assets/styles/style.css?inline";
|
import globalCss from "../../assets/styles/style.css?inline";
|
||||||
import Services from "../../services/service";
|
import Services from "../../services/service";
|
||||||
@ -26,9 +28,11 @@ export class ProcessListPage extends HTMLElement {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (this.shadowRoot) {
|
if (this.shadowRoot) {
|
||||||
|
// Le CSS et le HTML de base sont statiques, donc innerHTML est OK ici.
|
||||||
this.shadowRoot.innerHTML = `
|
this.shadowRoot.innerHTML = `
|
||||||
<style>
|
<style>
|
||||||
${globalCss}
|
${globalCss}
|
||||||
|
/* ... (styles identiques à ton fichier d'origine, je ne les répète pas pour abréger) ... */
|
||||||
:host { display: block; width: 100%; }
|
:host { display: block; width: 100%; }
|
||||||
.process-layout { padding: 2rem; display: flex; justify-content: center; }
|
.process-layout { padding: 2rem; display: flex; justify-content: center; }
|
||||||
.dashboard-container { width: 100%; max-width: 800px; display: flex; flex-direction: column; gap: 1.5rem; max-height: 85vh; overflow-y: auto; }
|
.dashboard-container { width: 100%; max-width: 800px; display: flex; flex-direction: column; gap: 1.5rem; max-height: 85vh; overflow-y: auto; }
|
||||||
@ -73,10 +77,18 @@ export class ProcessListPage extends HTMLElement {
|
|||||||
|
|
||||||
this.wrapper = root.querySelector("#autocomplete-wrapper") as HTMLElement;
|
this.wrapper = root.querySelector("#autocomplete-wrapper") as HTMLElement;
|
||||||
this.inputInput = root.querySelector("#process-input") as HTMLInputElement;
|
this.inputInput = root.querySelector("#process-input") as HTMLInputElement;
|
||||||
this.autocompleteList = root.querySelector("#autocomplete-list") as HTMLUListElement;
|
this.autocompleteList = root.querySelector(
|
||||||
this.tagsContainer = root.querySelector("#selected-tags-container") as HTMLElement;
|
"#autocomplete-list"
|
||||||
this.detailsContainer = root.querySelector("#process-details") as HTMLElement;
|
) as HTMLUListElement;
|
||||||
this.okButton = root.querySelector("#go-to-process-btn") as HTMLButtonElement;
|
this.tagsContainer = root.querySelector(
|
||||||
|
"#selected-tags-container"
|
||||||
|
) as HTMLElement;
|
||||||
|
this.detailsContainer = root.querySelector(
|
||||||
|
"#process-details"
|
||||||
|
) as HTMLElement;
|
||||||
|
this.okButton = root.querySelector(
|
||||||
|
"#go-to-process-btn"
|
||||||
|
) as HTMLButtonElement;
|
||||||
|
|
||||||
this.inputInput.addEventListener("keyup", () => this.handleInput());
|
this.inputInput.addEventListener("keyup", () => this.handleInput());
|
||||||
this.inputInput.addEventListener("click", () => this.openDropdown());
|
this.inputInput.addEventListener("click", () => this.openDropdown());
|
||||||
@ -100,7 +112,7 @@ export class ProcessListPage extends HTMLElement {
|
|||||||
// --- Logique Autocomplete Sécurisée ---
|
// --- Logique Autocomplete Sécurisée ---
|
||||||
|
|
||||||
async populateList(query: string) {
|
async populateList(query: string) {
|
||||||
this.autocompleteList.innerHTML = "";
|
this.autocompleteList.innerHTML = ""; // Vide la liste proprement
|
||||||
|
|
||||||
const mineArray = (await this.services.getMyProcesses()) ?? [];
|
const mineArray = (await this.services.getMyProcesses()) ?? [];
|
||||||
const allProcesses = await this.services.getProcesses();
|
const allProcesses = await this.services.getProcesses();
|
||||||
@ -115,7 +127,7 @@ export class ProcessListPage extends HTMLElement {
|
|||||||
const process = allProcesses[pid];
|
const process = allProcesses[pid];
|
||||||
if (!process) continue;
|
if (!process) continue;
|
||||||
|
|
||||||
const name = (await this.services.getProcessName(process)) || pid;
|
const name = this.services.getProcessName(process) || pid;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
query &&
|
query &&
|
||||||
@ -137,7 +149,7 @@ export class ProcessListPage extends HTMLElement {
|
|||||||
const small = document.createElement("small");
|
const small = document.createElement("small");
|
||||||
small.style.opacity = "0.6";
|
small.style.opacity = "0.6";
|
||||||
small.style.marginLeft = "8px";
|
small.style.marginLeft = "8px";
|
||||||
small.textContent = "(Mien)";
|
small.textContent = "(Mien)"; // Texte statique sûr
|
||||||
li.appendChild(small);
|
li.appendChild(small);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -216,19 +228,18 @@ export class ProcessListPage extends HTMLElement {
|
|||||||
// --- Détails du processus Sécurisés ---
|
// --- Détails du processus Sécurisés ---
|
||||||
|
|
||||||
async showProcessDetails(pid: string) {
|
async showProcessDetails(pid: string) {
|
||||||
this.detailsContainer.textContent = "Chargement...";
|
this.detailsContainer.textContent = "Chargement..."; // Safe loader
|
||||||
|
|
||||||
const process = await this.services.getProcess(pid);
|
const process = await this.services.getProcess(pid);
|
||||||
if (!process) return;
|
if (!process) return;
|
||||||
|
|
||||||
this.detailsContainer.innerHTML = "";
|
this.detailsContainer.innerHTML = "";
|
||||||
|
|
||||||
const name = (await this.services.getProcessName(process)) || "Sans nom";
|
const name = this.services.getProcessName(process) || "Sans nom";
|
||||||
|
|
||||||
// Description
|
// Description
|
||||||
let description = "Pas de description";
|
let description = "Pas de description";
|
||||||
const lastState = await this.services.getLastCommitedState(process);
|
const lastState = this.services.getLastCommitedState(process);
|
||||||
|
|
||||||
if (lastState?.pcd_commitment["description"]) {
|
if (lastState?.pcd_commitment["description"]) {
|
||||||
const diff = await this.services.getDiffByValue(
|
const diff = await this.services.getDiffByValue(
|
||||||
lastState.pcd_commitment["description"]
|
lastState.pcd_commitment["description"]
|
||||||
@ -242,14 +253,14 @@ export class ProcessListPage extends HTMLElement {
|
|||||||
// Titre
|
// Titre
|
||||||
const titleDiv = document.createElement("div");
|
const titleDiv = document.createElement("div");
|
||||||
titleDiv.className = "process-title-display";
|
titleDiv.className = "process-title-display";
|
||||||
titleDiv.textContent = name;
|
titleDiv.textContent = name; // Safe
|
||||||
containerDiv.appendChild(titleDiv);
|
containerDiv.appendChild(titleDiv);
|
||||||
|
|
||||||
// Description
|
// Description
|
||||||
const descDiv = document.createElement("div");
|
const descDiv = document.createElement("div");
|
||||||
descDiv.style.fontSize = "0.9rem";
|
descDiv.style.fontSize = "0.9rem";
|
||||||
descDiv.style.marginBottom = "10px";
|
descDiv.style.marginBottom = "10px";
|
||||||
descDiv.textContent = description;
|
descDiv.textContent = description; // Safe
|
||||||
containerDiv.appendChild(descDiv);
|
containerDiv.appendChild(descDiv);
|
||||||
|
|
||||||
// ID
|
// ID
|
||||||
@ -257,7 +268,7 @@ export class ProcessListPage extends HTMLElement {
|
|||||||
idDiv.style.fontSize = "0.8rem";
|
idDiv.style.fontSize = "0.8rem";
|
||||||
idDiv.style.opacity = "0.7";
|
idDiv.style.opacity = "0.7";
|
||||||
idDiv.style.marginBottom = "10px";
|
idDiv.style.marginBottom = "10px";
|
||||||
idDiv.textContent = `ID: ${pid}`;
|
idDiv.textContent = `ID: ${pid}`; // Safe
|
||||||
containerDiv.appendChild(idDiv);
|
containerDiv.appendChild(idDiv);
|
||||||
|
|
||||||
// Label "États en attente"
|
// Label "États en attente"
|
||||||
@ -267,12 +278,13 @@ export class ProcessListPage extends HTMLElement {
|
|||||||
labelDiv.textContent = "États en attente :";
|
labelDiv.textContent = "États en attente :";
|
||||||
containerDiv.appendChild(labelDiv);
|
containerDiv.appendChild(labelDiv);
|
||||||
|
|
||||||
const uncommitted = await this.services.getUncommitedStates(process);
|
const uncommitted = this.services.getUncommitedStates(process);
|
||||||
|
|
||||||
if (uncommitted.length > 0) {
|
if (uncommitted.length > 0) {
|
||||||
uncommitted.forEach((state) => {
|
uncommitted.forEach((state) => {
|
||||||
const el = document.createElement("div");
|
const el = document.createElement("div");
|
||||||
el.className = "state-element";
|
el.className = "state-element";
|
||||||
|
// textContent ici aussi, même si state_id est technique
|
||||||
el.textContent = `État: ${state.state_id.substring(0, 16)}...`;
|
el.textContent = `État: ${state.state_id.substring(0, 16)}...`;
|
||||||
|
|
||||||
el.addEventListener("click", () => {
|
el.addEventListener("click", () => {
|
||||||
@ -307,4 +319,4 @@ export class ProcessListPage extends HTMLElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define("process-list-page", ProcessListPage);
|
customElements.define("process-list-page", ProcessListPage);
|
||||||
|
|||||||
@ -1,88 +1,83 @@
|
|||||||
import * as Comlink from 'comlink';
|
import { initWebsocket, sendMessage } from '../websockets.service.ts';
|
||||||
import type { NetworkBackend } from '../../workers/network.worker';
|
import { AnkFlag } from '../../../pkg/sdk_client';
|
||||||
import Services from '../service'; // Attention à la dépendance circulaire, on va gérer ça
|
|
||||||
|
|
||||||
export class NetworkService {
|
export class NetworkService {
|
||||||
private worker: Comlink.Remote<NetworkBackend>;
|
private relayAddresses: { [wsurl: string]: string } = {};
|
||||||
private workerInstance: Worker;
|
private relayReadyResolver: (() => void) | null = null;
|
||||||
|
private relayReadyPromise: Promise<void> | null = null;
|
||||||
|
|
||||||
// Cache local pour répondre instantanément aux demandes synchrones de l'UI
|
constructor(private bootstrapUrls: string[]) {}
|
||||||
private localRelays: Record<string, string> = {};
|
|
||||||
|
|
||||||
constructor(private bootstrapUrls: string[]) {
|
public async connectAllRelays(): Promise<void> {
|
||||||
this.workerInstance = new Worker(
|
const connectedUrls: string[] = [];
|
||||||
new URL('../../workers/network.worker.ts', import.meta.url),
|
for (const wsurl of Object.keys(this.relayAddresses)) {
|
||||||
{ type: 'module' }
|
try {
|
||||||
);
|
await this.addWebsocketConnection(wsurl);
|
||||||
this.worker = Comlink.wrap<NetworkBackend>(this.workerInstance);
|
connectedUrls.push(wsurl);
|
||||||
}
|
} catch (error) {
|
||||||
|
console.error(`[Network] ❌ Échec connexion ${wsurl}:`, error);
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- MÉTHODES PUBLIQUES (API inchangée) ---
|
public async addWebsocketConnection(url: string): Promise<void> {
|
||||||
|
console.log(`[Network] 🔌 Connexion à: ${url}`);
|
||||||
public async addWebsocketConnection(url: string) {
|
await initWebsocket(url);
|
||||||
await this.worker.connect(url);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async connectAllRelays() {
|
public initRelays() {
|
||||||
for (const url of this.bootstrapUrls) {
|
for (const wsurl of this.bootstrapUrls) {
|
||||||
this.addWebsocketConnection(url);
|
this.updateRelay(wsurl, '');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
public updateRelay(url: string, spAddress: string) {
|
||||||
// Cette méthode était utilisée pour update l'état local.
|
console.log(`[Network] Mise à jour relais ${url} -> ${spAddress}`);
|
||||||
// Maintenant c'est le worker qui gère la vérité, mais on garde le cache local.
|
this.relayAddresses[url] = spAddress;
|
||||||
this.localRelays[url] = spAddress;
|
if (spAddress) this.resolveRelayReady();
|
||||||
|
}
|
||||||
|
|
||||||
|
public getAvailableRelayAddress(): Promise<string> {
|
||||||
|
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<void> {
|
||||||
|
if (!this.relayReadyPromise) {
|
||||||
|
this.relayReadyPromise = new Promise<void>((resolve) => {
|
||||||
|
this.relayReadyResolver = resolve;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return this.relayReadyPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
private resolveRelayReady(): void {
|
||||||
|
if (this.relayReadyResolver) {
|
||||||
|
this.relayReadyResolver();
|
||||||
|
this.relayReadyResolver = null;
|
||||||
|
this.relayReadyPromise = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public getAllRelays() {
|
public getAllRelays() {
|
||||||
return this.localRelays;
|
return this.relayAddresses;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getAvailableRelayAddress(): Promise<string> {
|
public sendMessage(flag: AnkFlag, message: string) {
|
||||||
// On demande au worker qui a la "vraie" info temps réel
|
sendMessage(flag, message);
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
136
src/services/websockets.service.ts
Executable file
136
src/services/websockets.service.ts
Executable file
@ -0,0 +1,136 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,859 +0,0 @@
|
|||||||
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<string, Member> = {};
|
|
||||||
private notifications: any[] | null = null;
|
|
||||||
private currentBlockHeight: number = -1;
|
|
||||||
private pendingKeyRequests: Map<string, (key: string) => 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<string>) | 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<void> {
|
|
||||||
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<string>
|
|
||||||
) {
|
|
||||||
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<string, Process>) { 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<string, any>) { 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<void> {
|
|
||||||
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<string, Member> };
|
|
||||||
}
|
|
||||||
|
|
||||||
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<string, Process> = {};
|
|
||||||
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<string, RoleDefinition>[] = [];
|
|
||||||
|
|
||||||
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<string[] | null> {
|
|
||||||
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<void> {
|
|
||||||
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<Member>();
|
|
||||||
|
|
||||||
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<string, any> | 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<string>();
|
|
||||||
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<ApiReturn | null> {
|
|
||||||
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<void> {
|
|
||||||
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<ApiReturn> {
|
|
||||||
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<ApiReturn> {
|
|
||||||
// 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<ApiReturn> {
|
|
||||||
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<ApiReturn> {
|
|
||||||
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<any> {
|
|
||||||
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<string, string>)) 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<string | null> {
|
|
||||||
return await this.db.getSharedSecret(address);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getAllDiffs(): Promise<Record<string, UserDiff>> {
|
|
||||||
return await this.db.getAllDiffs();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getDiffByValue(value: string): Promise<UserDiff | null> {
|
|
||||||
return await this.db.getDiff(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getAllSecrets(): Promise<SecretsStore> {
|
|
||||||
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<any | null> {
|
|
||||||
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<string>((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());
|
|
||||||
@ -1,150 +0,0 @@
|
|||||||
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<string, WebSocket> = new Map();
|
|
||||||
private relayAddresses: Map<string, string> = 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<string, any> = 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());
|
|
||||||
Loading…
x
Reference in New Issue
Block a user