diff --git a/package.json b/package.json index 2f5332a..fda7bc4 100755 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "main": "dist/index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "build_wasm": "wasm-pack build --out-dir ../ihm_client_dev2/pkg ../sdk_client --target bundler --dev", + "build_wasm": "wasm-pack build --out-dir ../ihm_client_dev3/pkg ../sdk_client --target bundler --dev", "start": "vite --host 0.0.0.0", "build": "tsc && vite build", "deploy": "sudo cp -r dist/* /var/www/html/", diff --git a/public/data.worker.js b/public/data.worker.js deleted file mode 100644 index 278df7c..0000000 --- a/public/data.worker.js +++ /dev/null @@ -1,202 +0,0 @@ -// public/data.worker.js - -const DB_NAME = "4nk"; -const DB_VERSION = 1; -const EMPTY32BYTES = String("").padStart(64, "0"); - -// ============================================ -// SERVICE WORKER LIFECYCLE -// ============================================ - -self.addEventListener("install", (event) => { - event.waitUntil(self.skipWaiting()); -}); - -self.addEventListener("activate", (event) => { - event.waitUntil(self.clients.claim()); -}); - -// ============================================ -// INDEXEDDB DIRECT ACCESS (READ-ONLY) -// ============================================ - -/** - * Ouvre une connexion à la BDD directement depuis le Service Worker - */ -function openDB() { - return new Promise((resolve, reject) => { - const request = indexedDB.open(DB_NAME, DB_VERSION); - request.onerror = () => reject(request.error); - request.onsuccess = () => resolve(request.result); - }); -} - -/** - * Récupère un objet spécifique (équivalent à GET_OBJECT) - */ -function getObject(db, storeName, key) { - return new Promise((resolve, reject) => { - const transaction = db.transaction(storeName, "readonly"); - const store = transaction.objectStore(storeName); - const request = store.get(key); - request.onerror = () => reject(request.error); - request.onsuccess = () => resolve(request.result); - }); -} - -/** - * Récupère plusieurs objets d'un coup (équivalent à GET_MULTIPLE_OBJECTS) - * Optimisé pour utiliser une seule transaction. - */ -function getMultipleObjects(db, storeName, keys) { - return new Promise((resolve, reject) => { - const transaction = db.transaction(storeName, "readonly"); - const store = transaction.objectStore(storeName); - const results = []; - - let completed = 0; - if (keys.length === 0) resolve([]); - - keys.forEach((key) => { - const request = store.get(key); - request.onsuccess = () => { - if (request.result) results.push(request.result); - completed++; - if (completed === keys.length) resolve(results); - }; - request.onerror = () => { - console.warn(`[SW] Erreur lecture clé ${key}`); - completed++; - if (completed === keys.length) resolve(results); - }; - }); - }); -} - -// ============================================ -// SCAN LOGIC -// ============================================ - -async function scanMissingData(processesToScan) { - let db; - try { - db = await openDB(); - } catch (e) { - console.error("[SW] Impossible d'ouvrir la BDD:", e); - return { toDownload: [], diffsToCreate: [] }; - } - - // 1. Récupération directe des processus - const myProcesses = await getMultipleObjects( - db, - "processes", - processesToScan - ); - - let toDownload = new Set(); - let diffsToCreate = []; - - if (myProcesses && myProcesses.length !== 0) { - for (const process of myProcesses) { - if (!process || !process.states) continue; - - const firstState = process.states[0]; - if (!firstState) continue; - - const processId = firstState.commited_in; - - for (const state of process.states) { - if (state.state_id === EMPTY32BYTES) continue; - - for (const [field, hash] of Object.entries(state.pcd_commitment)) { - if ( - (state.public_data && state.public_data[field] !== undefined) || - field === "roles" - ) - continue; - - // 2. Vérification directe dans 'data' - const existingData = await getObject(db, "data", hash); - - if (!existingData) { - toDownload.add(hash); - - // 3. Vérification directe dans 'diffs' - const existingDiff = await getObject(db, "diffs", hash); - - if (!existingDiff) { - diffsToCreate.push({ - process_id: processId, - state_id: state.state_id, - value_commitment: hash, - roles: state.roles, - field: field, - description: null, - previous_value: null, - new_value: null, - notify_user: false, - need_validation: false, - validation_status: "None", - }); - } - } else { - if (toDownload.has(hash)) { - toDownload.delete(hash); - } - } - } - } - } - } - - // On ferme la connexion BDD - db.close(); - - // ✅ LOG PERTINENT UNIQUEMENT : On n'affiche que si on a trouvé quelque chose - if (toDownload.size > 0 || diffsToCreate.length > 0) { - console.log("[Service Worker] 🔄 Scan found items:", { - toDownload: toDownload.size, - diffsToCreate: diffsToCreate.length, - }); - } - - return { - toDownload: Array.from(toDownload), - diffsToCreate: diffsToCreate, - }; -} - -// ============================================ -// MESSAGE HANDLER -// ============================================ - -self.addEventListener("message", async (event) => { - const data = event.data; - - if (data.type === "SCAN") { - try { - const myProcessesId = data.payload; - if (myProcessesId && myProcessesId.length !== 0) { - // Appel direct de la nouvelle fonction optimisée - const scanResult = await scanMissingData(myProcessesId); - - if (scanResult.toDownload.length !== 0) { - event.source.postMessage({ - type: "TO_DOWNLOAD", - data: scanResult.toDownload, - }); - } - - if (scanResult.diffsToCreate.length > 0) { - event.source.postMessage({ - type: "DIFFS_TO_CREATE", - data: scanResult.diffsToCreate, - }); - } - } - } catch (error) { - console.error("[Service Worker] Scan error:", error); - // On évite de spammer l'UI avec des erreurs internes du worker - } - } -}); diff --git a/src/service-workers/network.sw.ts b/src/service-workers/network.sw.ts new file mode 100644 index 0000000..68211c3 --- /dev/null +++ b/src/service-workers/network.sw.ts @@ -0,0 +1,245 @@ +// Service Worker for Network Management +// Handles WebSocket connections to backend relays + +/// + +declare const self: ServiceWorkerGlobalScope; + +type AnkFlag = 'Handshake' | 'NewTx' | 'Cipher' | 'Commit' | 'Faucet' | 'Ping'; + +interface ServiceWorkerMessage { + type: string; + payload?: any; + id?: string; +} + +// State management +const sockets: Map = new Map(); +const relayAddresses: Map = new Map(); // wsUrl -> spAddress +const messageQueue: string[] = []; +const reconnectTimers: Map = new Map(); +let heartbeatInterval: any = null; + +// ========================================== +// SERVICE WORKER LIFECYCLE +// ========================================== + +self.addEventListener('install', (event: ExtendableEvent) => { + console.log('[NetworkSW] Installing...'); + event.waitUntil(self.skipWaiting()); +}); + +self.addEventListener('activate', (event: ExtendableEvent) => { + console.log('[NetworkSW] Activating...'); + event.waitUntil(self.clients.claim()); + startHeartbeat(); +}); + +// ========================================== +// MESSAGE HANDLING +// ========================================== + +self.addEventListener('message', async (event: ExtendableMessageEvent) => { + const { type, payload, id } = event.data; + console.log(`[NetworkSW] Received message: ${type} (id: ${id})`); + + // Get the client to respond to + const client = event.source as Client | null; + if (!client) { + console.error('[NetworkSW] No client source available'); + return; + } + + switch (type) { + case 'CONNECT': + connect(payload.url).then(() => { + respondToClient(client, { type: 'CONNECTED', id, payload: { url: payload.url } }); + }).catch((error) => { + respondToClient(client, { type: 'ERROR', id, payload: { error: error.message } }); + }); + break; + + case 'SEND_MESSAGE': + sendMessage(payload.flag, payload.content); + respondToClient(client, { type: 'MESSAGE_SENT', id, payload: {} }); + break; + + case 'GET_AVAILABLE_RELAY': + const relay = getAvailableRelay(); + respondToClient(client, { type: 'AVAILABLE_RELAY', id, payload: { relay } }); + break; + + case 'GET_ALL_RELAYS': + const relays = getAllRelays(); + respondToClient(client, { type: 'ALL_RELAYS', id, payload: { relays } }); + break; + + default: + console.warn('[NetworkSW] Unknown message type:', type); + } +}); + +// ========================================== +// WEBSOCKET MANAGEMENT +// ========================================== + +async function connect(url: string): Promise { + if (sockets.has(url) && sockets.get(url)?.readyState === WebSocket.OPEN) { + return; + } + + console.log(`[NetworkSW] 🔌 Connexion à ${url}...`); + const ws = new WebSocket(url); + + ws.onopen = () => { + console.log(`[NetworkSW] ✅ Connecté à ${url}`); + sockets.set(url, ws); + + // Reset reconnect timer if exists + if (reconnectTimers.has(url)) { + clearTimeout(reconnectTimers.get(url)); + reconnectTimers.delete(url); + } + + // Flush message queue + flushQueue(); + + // Notify all clients + broadcastToClients({ type: 'STATUS_CHANGE', payload: { url, status: 'OPEN' } }); + }; + + ws.onmessage = (event) => { + try { + const msg = JSON.parse(event.data); + + // If it's a Handshake, update the local map + if (msg.flag === 'Handshake' && msg.content) { + const handshake = JSON.parse(msg.content); + if (handshake.sp_address) { + relayAddresses.set(url, handshake.sp_address); + broadcastToClients({ + type: 'STATUS_CHANGE', + payload: { url, status: 'OPEN', spAddress: handshake.sp_address } + }); + } + } + + // Forward all messages to clients + broadcastToClients({ + type: 'MESSAGE_RECEIVED', + payload: { flag: msg.flag, content: msg.content, url } + }); + } catch (e) { + console.error('[NetworkSW] Erreur parsing message:', e); + } + }; + + ws.onerror = (e) => { + // Silently handle errors (reconnection will be handled by onclose) + }; + + ws.onclose = () => { + console.warn(`[NetworkSW] ❌ Déconnecté de ${url}.`); + sockets.delete(url); + relayAddresses.set(url, ''); // Reset spAddress + + broadcastToClients({ type: 'STATUS_CHANGE', payload: { url, status: 'CLOSED' } }); + scheduleReconnect(url); + }; +} + +function sendMessage(flag: AnkFlag, content: string) { + const msgStr = JSON.stringify({ flag, content }); + + // Broadcast to all connected relays + let sent = false; + for (const [url, ws] of sockets) { + if (ws.readyState === WebSocket.OPEN) { + ws.send(msgStr); + sent = true; + } + } + + if (!sent) { + messageQueue.push(msgStr); + } +} + +function getAvailableRelay(): string | null { + for (const sp of relayAddresses.values()) { + if (sp && sp !== '') return sp; + } + return null; +} + +function getAllRelays(): Record { + return Object.fromEntries(relayAddresses); +} + +// ========================================== +// INTERNAL HELPERS +// ========================================== + +function flushQueue() { + while (messageQueue.length > 0) { + const msg = messageQueue.shift(); + if (!msg) break; + for (const ws of sockets.values()) { + if (ws.readyState === WebSocket.OPEN) ws.send(msg); + } + } +} + +function scheduleReconnect(url: string) { + if (reconnectTimers.has(url)) return; + + console.log(`[NetworkSW] ⏳ Reconnexion à ${url} dans 3s...`); + const timer = setTimeout(() => { + reconnectTimers.delete(url); + connect(url); + }, 3000); + + reconnectTimers.set(url, timer); +} + +function startHeartbeat() { + if (heartbeatInterval) clearInterval(heartbeatInterval); + heartbeatInterval = setInterval(() => { + // Heartbeat logic can be added here if needed + // sendMessage('Ping', ''); + }, 30000); +} + +// ========================================== +// CLIENT COMMUNICATION +// ========================================== + +function respondToClient(client: Client | null, message: any) { + if (!client) { + console.error('[NetworkSW] Cannot respond: client is null'); + return; + } + + try { + console.log(`[NetworkSW] Sending response: ${message.type} (id: ${message.id})`); + client.postMessage(message); + } catch (error) { + console.error('[NetworkSW] Error sending message to client:', error); + // Fallback: try to get the client by ID or use broadcast + if (client.id) { + self.clients.get(client.id).then((c) => { + if (c) c.postMessage(message); + }).catch((err) => { + console.error('[NetworkSW] Failed to get client by ID:', err); + }); + } + } +} + +async function broadcastToClients(message: any) { + const clients = await self.clients.matchAll({ includeUncontrolled: true }); + clients.forEach((client) => { + client.postMessage(message); + }); +} + diff --git a/src/services/core/network.service.ts b/src/services/core/network.service.ts index c59b409..17cd1ad 100644 --- a/src/services/core/network.service.ts +++ b/src/services/core/network.service.ts @@ -1,39 +1,49 @@ -import * as Comlink from "comlink"; -import type { NetworkBackend } from "../../workers/network.worker"; import Services from "../service"; +interface ServiceWorkerMessage { + type: string; + payload?: any; + id?: string; +} + export class NetworkService { - private worker: Comlink.Remote; - private workerInstance: Worker; + private serviceWorkerRegistration: ServiceWorkerRegistration | null = null; + private messageIdCounter: number = 0; + private pendingMessages: Map void; reject: (error: any) => void }> = new Map(); - // Cache local - private localRelays: Record = {}; - - // Mécanisme d'attente (Events) + // Relay ready promise mechanism (waits for first relay to become available) private relayReadyResolver: ((addr: string) => void) | null = null; private relayReadyPromise: Promise | null = null; 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); + this.setupMessageListener(); } public async initRelays() { - await this.worker.setCallbacks( - Comlink.proxy(this.onMessageReceived.bind(this)), - Comlink.proxy(this.onStatusChange.bind(this)) - ); + try { + // Register Service Worker + console.log("[NetworkService] Registering Service Worker..."); + await this.registerServiceWorker(); - for (const url of this.bootstrapUrls) { - this.addWebsocketConnection(url); + // Wait for Service Worker to be ready + console.log("[NetworkService] Waiting for Service Worker to be ready..."); + await this.waitForServiceWorkerReady(); + console.log("[NetworkService] Service Worker is ready"); + + // Connect to bootstrap URLs + console.log("[NetworkService] Connecting to bootstrap URLs..."); + for (const url of this.bootstrapUrls) { + await this.addWebsocketConnection(url); + } + console.log("[NetworkService] Initialization complete"); + } catch (error) { + console.error("[NetworkService] Initialization failed:", error); + throw error; } } public async addWebsocketConnection(url: string) { - await this.worker.connect(url); + await this.sendToServiceWorker({ type: 'CONNECT', payload: { url } }); } public async connectAllRelays() { @@ -43,14 +53,16 @@ export class NetworkService { } public async sendMessage(flag: string, content: string) { - await this.worker.sendMessage(flag as any, content); + await this.sendToServiceWorker({ + type: 'SEND_MESSAGE', + payload: { flag, content } + }); } - // Cette méthode est appelée par le Worker (via Services.ts) ou par onStatusChange + // Called by onStatusChange when a relay becomes available + // Triggers the relay ready promise if someone is waiting public updateRelay(url: string, spAddress: string) { - this.localRelays[url] = spAddress; - - // ✨ EVENT TRIGGER : Si quelqu'un attendait un relais, on le débloque ! + // Trigger relay ready promise if someone is waiting if (spAddress && spAddress !== "" && this.relayReadyResolver) { this.relayReadyResolver(spAddress); this.relayReadyResolver = null; @@ -58,27 +70,26 @@ export class NetworkService { } } - public getAllRelays() { - return this.localRelays; + public async getAllRelays(): Promise> { + const response = await this.sendToServiceWorker({ type: 'GET_ALL_RELAYS' }); + return response?.relays || {}; } public async getAvailableRelayAddress(): Promise { - // 1. Vérification immédiate (Fast path) - const existing = Object.values(this.localRelays).find( - (addr) => addr && addr !== "" - ); - if (existing) return existing; + // 1. Query Service Worker first (fast path if relay already available) + const response = await this.sendToServiceWorker({ type: 'GET_AVAILABLE_RELAY' }); + if (response?.relay) return response.relay; - // 2. Si pas encore là, on crée une "barrière" (Promise) + // 2. If no relay yet, wait for one to become available if (!this.relayReadyPromise) { - console.log("[NetworkService] ⏳ Attente d'un événement Handshake..."); + console.log("[NetworkService] ⏳ Waiting for relay Handshake..."); this.relayReadyPromise = new Promise((resolve, reject) => { this.relayReadyResolver = resolve; - // Timeout de sécurité (10s) pour ne pas bloquer indéfiniment + // Timeout after 10s to avoid blocking indefinitely setTimeout(() => { if (this.relayReadyResolver) { - reject(new Error("Timeout: Aucun relais reçu après 10s")); + reject(new Error("Timeout: No relay received after 10s")); this.relayReadyResolver = null; this.relayReadyPromise = null; } @@ -91,6 +102,191 @@ export class NetworkService { // --- INTERNES --- + private async registerServiceWorker(): Promise { + if (!("serviceWorker" in navigator)) { + throw new Error("Service Workers are not supported"); + } + + try { + // Check if already registered + const registrations = await navigator.serviceWorker.getRegistrations(); + const existing = registrations.find((r) => { + const url = r.active?.scriptURL || r.installing?.scriptURL || r.waiting?.scriptURL; + return url && url.includes("network.sw.js"); + }); + + if (existing) { + console.log("[NetworkService] Found existing Service Worker registration"); + this.serviceWorkerRegistration = existing; + + // Listen for controller change in case it activates + navigator.serviceWorker.addEventListener('controllerchange', () => { + console.log("[NetworkService] Service Worker controller changed"); + }); + + // Try to update + try { + await existing.update(); + } catch (updateError) { + console.warn("[NetworkService] Service Worker update failed:", updateError); + } + } else { + // Register new Service Worker + console.log("[NetworkService] Registering new Service Worker at /network.sw.js"); + this.serviceWorkerRegistration = await navigator.serviceWorker.register( + "/network.sw.js", + { type: "module", scope: "/" } + ); + console.log("[NetworkService] Service Worker registered:", this.serviceWorkerRegistration); + } + + // Listen for registration errors + this.serviceWorkerRegistration.addEventListener('error', (event) => { + console.error("[NetworkService] Service Worker error:", event); + }); + } catch (error) { + console.error("[NetworkService] Failed to register Service Worker:", error); + // Check if it's a 404 error + if (error instanceof Error && error.message.includes('404')) { + throw new Error("Service Worker file not found at /network.sw.js. Check Vite configuration."); + } + throw error; + } + } + + private async waitForServiceWorkerReady(): Promise { + if (!this.serviceWorkerRegistration) { + throw new Error("Service Worker registration is null"); + } + + // Wait for the Service Worker to be ready (installed and activated) + await this.serviceWorkerRegistration.ready; + + // Also ensure it's active + if (this.serviceWorkerRegistration.active) { + // Wait a bit for it to become the controller + await new Promise(resolve => setTimeout(resolve, 100)); + return; + } + + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Error("Service Worker activation timeout after 10 seconds")); + }, 10000); + + const checkState = () => { + if (this.serviceWorkerRegistration?.active) { + clearTimeout(timeout); + // Wait a bit for it to become the controller + setTimeout(resolve, 100); + } else if (this.serviceWorkerRegistration?.installing) { + // Service Worker is installing, wait for it + this.serviceWorkerRegistration.installing.addEventListener('statechange', () => { + if (this.serviceWorkerRegistration?.active) { + clearTimeout(timeout); + setTimeout(resolve, 100); + } + }); + } else if (this.serviceWorkerRegistration?.waiting) { + // Service Worker is waiting, skip waiting + this.serviceWorkerRegistration.waiting.postMessage({ type: 'SKIP_WAITING' }); + setTimeout(checkState, 100); + } else { + setTimeout(checkState, 100); + } + }; + checkState(); + }); + } + + private setupMessageListener(): void { + if (!navigator.serviceWorker) { + console.warn("[NetworkService] Service Workers not supported, message listener not set up"); + return; + } + + const messageHandler = (event: MessageEvent) => { + const { type, payload, id } = event.data; + console.log(`[NetworkService] Received message from SW: ${type} (id: ${id})`); + + // Handle response messages + if (id && this.pendingMessages.has(id)) { + const { resolve, reject } = this.pendingMessages.get(id)!; + this.pendingMessages.delete(id); + + if (type === "ERROR") { + reject(new Error(payload?.error || "Unknown error")); + } else { + resolve(payload); + } + return; + } + + // Handle event messages (not responses to requests) + switch (type) { + case "MESSAGE_RECEIVED": + this.onMessageReceived(payload.flag, payload.content, payload.url); + break; + case "STATUS_CHANGE": + this.onStatusChange(payload.url, payload.status, payload.spAddress); + break; + } + }; + + // Listen on both the serviceWorker and controller + navigator.serviceWorker.addEventListener("message", messageHandler); + + // Also listen on the controller if it exists + if (navigator.serviceWorker.controller) { + navigator.serviceWorker.controller.addEventListener("message", messageHandler); + } + + // Listen for controller changes + navigator.serviceWorker.addEventListener("controllerchange", () => { + console.log("[NetworkService] Service Worker controller changed"); + if (navigator.serviceWorker.controller) { + navigator.serviceWorker.controller.addEventListener("message", messageHandler); + } + }); + } + + private async sendToServiceWorker(message: ServiceWorkerMessage): Promise { + if (!this.serviceWorkerRegistration) { + throw new Error("Service Worker is not registered"); + } + + // Use the controller if available, otherwise use active + const target = navigator.serviceWorker.controller || this.serviceWorkerRegistration.active; + + if (!target) { + throw new Error(`Service Worker is not active. State: ${this.serviceWorkerRegistration.installing ? 'installing' : this.serviceWorkerRegistration.waiting ? 'waiting' : 'unknown'}`); + } + + const id = `msg_${++this.messageIdCounter}`; + message.id = id; + + return new Promise((resolve, reject) => { + this.pendingMessages.set(id, { resolve, reject }); + + // Timeout after 10 seconds (reduced from 30) + const timeout = setTimeout(() => { + if (this.pendingMessages.has(id)) { + this.pendingMessages.delete(id); + reject(new Error(`Service Worker message timeout after 10s. Message type: ${message.type}`)); + } + }, 10000); + + try { + target.postMessage(message); + console.log(`[NetworkService] Sent message to SW via ${target === navigator.serviceWorker.controller ? 'controller' : 'active'}: ${message.type} (id: ${id})`); + } catch (error) { + clearTimeout(timeout); + this.pendingMessages.delete(id); + reject(new Error(`Failed to send message to Service Worker: ${error instanceof Error ? error.message : String(error)}`)); + } + }); + } + private async onMessageReceived(flag: string, content: string, url: string) { const services = await Services.getInstance(); await services.dispatchToWorker(flag, content, url); @@ -102,10 +298,10 @@ export class NetworkService { spAddress?: string ) { if (status === "OPEN" && spAddress) { - // Met à jour et déclenche potentiellement le resolve() + // Trigger relay ready promise if someone is waiting this.updateRelay(url, spAddress); - } else if (status === "CLOSED") { - this.localRelays[url] = ""; } + // Note: Service worker is the source of truth for relay state + // We don't need to track CLOSED here since we query the SW directly } } diff --git a/src/services/service.ts b/src/services/service.ts index 519d8ae..f0dc928 100755 --- a/src/services/service.ts +++ b/src/services/service.ts @@ -13,7 +13,6 @@ import { BackUp } from "../types/index"; import { APP_CONFIG } from "../config/constants"; import { NetworkService } from "./core/network.service"; import type { CoreBackend } from "../workers/core.worker"; -import { SWController } from "./sw-controller.service"; import Database from "./database.service"; export default class Services { @@ -60,11 +59,7 @@ export default class Services { // 2. Initialiser la Database (Main Thread) await Database.getInstance(); - // 3. Initialiser le Service Worker Controller - const swController = await SWController.getInstance(); - await swController.init(); - - // 4. Configurer les Callbacks + // 3. Configurer les Callbacks await this.coreWorker.setCallbacks( Comlink.proxy(this.handleWorkerNotification.bind(this)), Comlink.proxy(this.handleWorkerNetworkSend.bind(this)), @@ -72,11 +67,11 @@ export default class Services { Comlink.proxy(this.handleWorkerRelayRequest.bind(this)) ); - // 5. Initialiser le Réseau + // 4. Initialiser le Réseau await this.networkService.initRelays(); console.log( - "[Services] ✅ Proxy connecté au CoreWorker, SWController et NetworkService." + "[Services] ✅ Proxy connecté au CoreWorker et NetworkService." ); } @@ -155,14 +150,15 @@ export default class Services { public async addWebsocketConnection(url: string) { await this.networkService.addWebsocketConnection(url); } - public getAllRelays() { - return this.networkService.getAllRelays(); + public async getAllRelays() { + return await this.networkService.getAllRelays(); } public updateRelay(url: string, sp: string) { this.networkService.updateRelay(url, sp); } - public getSpAddress(url: string) { - return this.networkService.getAllRelays()[url]; + public async getSpAddress(url: string) { + const relays = await this.networkService.getAllRelays(); + return relays[url]; } // ========================================== diff --git a/src/services/sw-controller.service.ts b/src/services/sw-controller.service.ts deleted file mode 100644 index f697c15..0000000 --- a/src/services/sw-controller.service.ts +++ /dev/null @@ -1,194 +0,0 @@ -import Services from "./service"; -import Database from "./database.service"; - -/** - * Service Worker Controller - Manages SW registration and communication - */ -export class SWController { - private static instance: SWController; - private serviceWorkerRegistration: ServiceWorkerRegistration | null = null; - private serviceWorkerCheckIntervalId: number | null = null; - - private constructor() { - // Singleton - } - - public static async getInstance(): Promise { - if (!SWController.instance) { - SWController.instance = new SWController(); - } - return SWController.instance; - } - - public async init(): Promise { - await this.registerServiceWorker("/data.worker.js"); - } - - private async registerServiceWorker(path: string): Promise { - if (!("serviceWorker" in navigator)) return; - console.log("[SWController] Initializing Service Worker:", path); - - try { - // Nettoyage des anciens workers si nécessaire (logique conservée) - const registrations = await navigator.serviceWorker.getRegistrations(); - for (const registration of registrations) { - const scriptURL = - registration.active?.scriptURL || - registration.installing?.scriptURL || - registration.waiting?.scriptURL; - const scope = registration.scope; - - if ( - scope.includes("/src/service-workers/") || - (scriptURL && scriptURL.includes("/src/service-workers/")) - ) { - console.warn(`[SWController] Removing old Service Worker (${scope})`); - await registration.unregister(); - } - } - - const existingValidWorker = registrations.find((r) => { - const url = - r.active?.scriptURL || - r.installing?.scriptURL || - r.waiting?.scriptURL; - return url && url.endsWith(path.replace(/^\//, "")); - }); - - if (!existingValidWorker) { - console.log("[SWController] Registering new Service Worker"); - this.serviceWorkerRegistration = await navigator.serviceWorker.register( - path, - { type: "module", scope: "/" } - ); - } else { - console.log("[SWController] Service Worker already active"); - this.serviceWorkerRegistration = existingValidWorker; - await this.serviceWorkerRegistration.update(); - } - - navigator.serviceWorker.addEventListener("message", async (event) => { - await this.handleServiceWorkerMessage(event.data); - }); - - // Boucle de scan périodique - if (this.serviceWorkerCheckIntervalId) - clearInterval(this.serviceWorkerCheckIntervalId); - - this.serviceWorkerCheckIntervalId = window.setInterval(async () => { - const activeWorker = - this.serviceWorkerRegistration?.active || - (await this.waitForServiceWorkerActivation( - this.serviceWorkerRegistration! - )); - - // On récupère les processus via le proxy Services - const service = await Services.getInstance(); - const payload = await service.getMyProcesses(); - - if (payload && Object.keys(payload).length !== 0) { - activeWorker?.postMessage({ type: "SCAN", payload }); - } - }, 5000); - } catch (error) { - console.error("[SWController] Service Worker error:", error); - } - } - - private async waitForServiceWorkerActivation( - registration: ServiceWorkerRegistration - ): Promise { - return new Promise((resolve) => { - if (registration.active) { - resolve(registration.active); - } else { - const listener = () => { - if (registration.active) { - navigator.serviceWorker.removeEventListener( - "controllerchange", - listener - ); - resolve(registration.active); - } - }; - navigator.serviceWorker.addEventListener("controllerchange", listener); - } - }); - } - - // ============================================ - // MESSAGE HANDLERS - // ============================================ - - private async handleServiceWorkerMessage(message: any) { - switch (message.type) { - case "TO_DOWNLOAD": - await this.handleDownloadList(message.data); - break; - case "DIFFS_TO_CREATE": - await this.handleDiffsToCreate(message.data); - break; - default: - console.warn("[SWController] Unknown message type received:", message); - } - } - - private async handleDiffsToCreate(diffs: any[]): Promise { - console.log( - `[SWController] Creating ${diffs.length} diffs from Service Worker scan` - ); - try { - const db = await Database.getInstance(); - await db.saveDiffs(diffs); - console.log("[SWController] Diffs created successfully"); - } catch (error) { - console.error("[SWController] Error creating diffs:", error); - } - } - - private async handleDownloadList(downloadList: string[]): Promise { - let requestedStateId: string[] = []; - - // On a besoin de Services pour la logique métier (fetch, network) - const service = await Services.getInstance(); - - for (const hash of downloadList) { - const diff = await service.getDiffByValue(hash); - if (!diff) { - console.warn(`[SWController] Missing a diff for hash ${hash}`); - continue; - } - - const processId = diff.process_id; - const stateId = diff.state_id; - const roles = diff.roles; - - try { - const valueBytes = await service.fetchValueFromStorage(hash); - if (valueBytes) { - const blob = new Blob([valueBytes], { - type: "application/octet-stream", - }); - - await service.saveBlobToDb(hash, blob); - - document.dispatchEvent( - new CustomEvent("newDataReceived", { - detail: { processId, stateId, hash }, - }) - ); - } else { - console.log( - "[SWController] Request data from managers of the process" - ); - if (!requestedStateId.includes(stateId)) { - await service.requestDataFromPeers(processId, [stateId], [roles]); - requestedStateId.push(stateId); - } - } - } catch (e) { - console.error(e); - } - } - } -} diff --git a/src/workers/network.worker.ts b/src/workers/network.worker.ts deleted file mode 100644 index d576930..0000000 --- a/src/workers/network.worker.ts +++ /dev/null @@ -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 = 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 diff --git a/vite.config.ts b/vite.config.ts index c819f89..c141c59 100755 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,31 +1,94 @@ import { defineConfig } from 'vite'; import wasm from 'vite-plugin-wasm'; import { fileURLToPath, URL } from 'node:url'; +import { resolve } from 'path'; +import type { Plugin } from 'vite'; + +// Plugin to handle Service Worker in dev and build +function serviceWorkerPlugin(): Plugin { + const swPath = resolve(__dirname, 'src/service-workers/network.sw.ts'); + const swUrl = '/src/service-workers/network.sw.ts'; + + return { + name: 'service-worker-plugin', + enforce: 'pre', // Run before other plugins + configureServer(server) { + // In dev mode, serve the Service Worker from src + server.middlewares.use('/network.sw.js', async (req, res, next) => { + console.log('[Service Worker Plugin] Request for /network.sw.js'); + try { + // Try using the URL format that Vite expects + let result = await server.transformRequest(swUrl, { ssr: false }); + + // If that doesn't work, try with the file path + if (!result || !result.code) { + console.log('[Service Worker Plugin] Trying with file path...'); + result = await server.transformRequest(swPath, { ssr: false }); + } + + if (result && result.code) { + console.log('[Service Worker Plugin] Successfully transformed Service Worker'); + res.setHeader('Content-Type', 'application/javascript'); + res.setHeader('Service-Worker-Allowed', '/'); + res.setHeader('Cache-Control', 'no-cache'); + res.end(result.code); + return; + } + + // Final fallback: use pluginContainer directly + console.log('[Service Worker Plugin] Fallback: using pluginContainer.transform'); + const { readFileSync } = await import('fs'); + const code = readFileSync(swPath, 'utf-8'); + const transformed = await server.pluginContainer.transform(code, swUrl); + + if (transformed && transformed.code) { + console.log('[Service Worker Plugin] Successfully transformed via pluginContainer'); + res.setHeader('Content-Type', 'application/javascript'); + res.setHeader('Service-Worker-Allowed', '/'); + res.setHeader('Cache-Control', 'no-cache'); + res.end(transformed.code); + } else { + console.error('[Service Worker Plugin] Failed to transform Service Worker'); + res.statusCode = 500; + res.end('Failed to load Service Worker'); + } + } catch (err) { + console.error('[Service Worker Plugin] Error serving SW:', err); + if (err instanceof Error) { + console.error('[Service Worker Plugin] Error details:', err.stack); + } + res.statusCode = 500; + res.end(`Service Worker error: ${err instanceof Error ? err.message : String(err)}`); + } + }); + }, + }; +} export default defineConfig({ // Configuration du serveur de développement server: { port: 3003, host: '0.0.0.0', // Permet l'accès depuis l'extérieur (Docker/Réseau) - allowedHosts: ['dev2.4nkweb.com'], + allowedHosts: ['dev3.4nkweb.com'], proxy: { // Proxy pour le stockage '/storage': { - target: process.env.VITE_STORAGEURL || 'https://dev2.4nkweb.com', + target: process.env.VITE_STORAGEURL || 'https://dev3.4nkweb.com', changeOrigin: true, secure: false, // Accepte les certificats auto-signés si besoin rewrite: (path) => path.replace(/^\/storage/, '/storage'), }, // Proxy pour les websockets (si besoin de contourner CORS ou SSL) '/ws': { - target: process.env.VITE_BOOTSTRAPURL?.replace('ws', 'http') || 'https://dev2.4nkweb.com', + target: process.env.VITE_BOOTSTRAPURL?.replace('ws', 'http') || 'https://dev3.4nkweb.com', ws: true, changeOrigin: true, secure: false, }, // Proxy pour l'API BlindBit '/blindbit': { - target: process.env.VITE_BLINDBITURL || 'https://dev2.4nkweb.com/blindbit', + target: process.env.VITE_BLINDBITURL || 'https://dev3.4nkweb.com/blindbit', changeOrigin: true, secure: false, rewrite: (path) => path.replace(/^\/blindbit/, ''), @@ -36,6 +99,7 @@ export default defineConfig({ // Plugins essentiels plugins: [ wasm(), // Indispensable pour ton SDK Rust + serviceWorkerPlugin(), // Service Worker handler ], // Alias pour les imports (ex: import ... from '@/services/...') @@ -52,7 +116,17 @@ export default defineConfig({ outDir: 'dist', assetsDir: 'assets', emptyOutDir: true, // Vide le dossier dist avant chaque build - // On retire la config "lib" car c'est maintenant une App autonome + rollupOptions: { + input: { + main: resolve(__dirname, 'index.html'), + 'network.sw': resolve(__dirname, 'src/service-workers/network.sw.ts'), + }, + output: { + entryFileNames: (chunkInfo) => { + return chunkInfo.name === 'network.sw' ? 'network.sw.js' : 'assets/[name]-[hash].js'; + }, + }, + }, }, // Configuration spécifique pour les Workers (Database)