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)