Replace the old service worker by network.sw.ts
This commit is contained in:
parent
77dcb8b9ae
commit
e3447195e3
@ -6,7 +6,7 @@
|
|||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"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",
|
"start": "vite --host 0.0.0.0",
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
"deploy": "sudo cp -r dist/* /var/www/html/",
|
"deploy": "sudo cp -r dist/* /var/www/html/",
|
||||||
|
|||||||
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
245
src/service-workers/network.sw.ts
Normal file
245
src/service-workers/network.sw.ts
Normal file
@ -0,0 +1,245 @@
|
|||||||
|
// Service Worker for Network Management
|
||||||
|
// Handles WebSocket connections to backend relays
|
||||||
|
|
||||||
|
/// <reference lib="webworker" />
|
||||||
|
|
||||||
|
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<string, WebSocket> = new Map();
|
||||||
|
const relayAddresses: Map<string, string> = new Map(); // wsUrl -> spAddress
|
||||||
|
const messageQueue: string[] = [];
|
||||||
|
const reconnectTimers: Map<string, any> = 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<ServiceWorkerMessage>) => {
|
||||||
|
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<void> {
|
||||||
|
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<string, string> {
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@ -1,39 +1,49 @@
|
|||||||
import * as Comlink from "comlink";
|
|
||||||
import type { NetworkBackend } from "../../workers/network.worker";
|
|
||||||
import Services from "../service";
|
import Services from "../service";
|
||||||
|
|
||||||
|
interface ServiceWorkerMessage {
|
||||||
|
type: string;
|
||||||
|
payload?: any;
|
||||||
|
id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export class NetworkService {
|
export class NetworkService {
|
||||||
private worker: Comlink.Remote<NetworkBackend>;
|
private serviceWorkerRegistration: ServiceWorkerRegistration | null = null;
|
||||||
private workerInstance: Worker;
|
private messageIdCounter: number = 0;
|
||||||
|
private pendingMessages: Map<string, { resolve: (value: any) => void; reject: (error: any) => void }> = new Map();
|
||||||
|
|
||||||
// Cache local
|
// Relay ready promise mechanism (waits for first relay to become available)
|
||||||
private localRelays: Record<string, string> = {};
|
|
||||||
|
|
||||||
// Mécanisme d'attente (Events)
|
|
||||||
private relayReadyResolver: ((addr: string) => void) | null = null;
|
private relayReadyResolver: ((addr: string) => void) | null = null;
|
||||||
private relayReadyPromise: Promise<string> | null = null;
|
private relayReadyPromise: Promise<string> | null = null;
|
||||||
|
|
||||||
constructor(private bootstrapUrls: string[]) {
|
constructor(private bootstrapUrls: string[]) {
|
||||||
this.workerInstance = new Worker(
|
this.setupMessageListener();
|
||||||
new URL("../../workers/network.worker.ts", import.meta.url),
|
|
||||||
{ type: "module" }
|
|
||||||
);
|
|
||||||
this.worker = Comlink.wrap<NetworkBackend>(this.workerInstance);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async initRelays() {
|
public async initRelays() {
|
||||||
await this.worker.setCallbacks(
|
try {
|
||||||
Comlink.proxy(this.onMessageReceived.bind(this)),
|
// Register Service Worker
|
||||||
Comlink.proxy(this.onStatusChange.bind(this))
|
console.log("[NetworkService] Registering Service Worker...");
|
||||||
);
|
await this.registerServiceWorker();
|
||||||
|
|
||||||
|
// 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) {
|
for (const url of this.bootstrapUrls) {
|
||||||
this.addWebsocketConnection(url);
|
await this.addWebsocketConnection(url);
|
||||||
|
}
|
||||||
|
console.log("[NetworkService] Initialization complete");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[NetworkService] Initialization failed:", error);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async addWebsocketConnection(url: string) {
|
public async addWebsocketConnection(url: string) {
|
||||||
await this.worker.connect(url);
|
await this.sendToServiceWorker({ type: 'CONNECT', payload: { url } });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async connectAllRelays() {
|
public async connectAllRelays() {
|
||||||
@ -43,14 +53,16 @@ export class NetworkService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async sendMessage(flag: string, content: string) {
|
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) {
|
public updateRelay(url: string, spAddress: string) {
|
||||||
this.localRelays[url] = spAddress;
|
// Trigger relay ready promise if someone is waiting
|
||||||
|
|
||||||
// ✨ EVENT TRIGGER : Si quelqu'un attendait un relais, on le débloque !
|
|
||||||
if (spAddress && spAddress !== "" && this.relayReadyResolver) {
|
if (spAddress && spAddress !== "" && this.relayReadyResolver) {
|
||||||
this.relayReadyResolver(spAddress);
|
this.relayReadyResolver(spAddress);
|
||||||
this.relayReadyResolver = null;
|
this.relayReadyResolver = null;
|
||||||
@ -58,27 +70,26 @@ export class NetworkService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public getAllRelays() {
|
public async getAllRelays(): Promise<Record<string, string>> {
|
||||||
return this.localRelays;
|
const response = await this.sendToServiceWorker({ type: 'GET_ALL_RELAYS' });
|
||||||
|
return response?.relays || {};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getAvailableRelayAddress(): Promise<string> {
|
public async getAvailableRelayAddress(): Promise<string> {
|
||||||
// 1. Vérification immédiate (Fast path)
|
// 1. Query Service Worker first (fast path if relay already available)
|
||||||
const existing = Object.values(this.localRelays).find(
|
const response = await this.sendToServiceWorker({ type: 'GET_AVAILABLE_RELAY' });
|
||||||
(addr) => addr && addr !== ""
|
if (response?.relay) return response.relay;
|
||||||
);
|
|
||||||
if (existing) return existing;
|
|
||||||
|
|
||||||
// 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) {
|
if (!this.relayReadyPromise) {
|
||||||
console.log("[NetworkService] ⏳ Attente d'un événement Handshake...");
|
console.log("[NetworkService] ⏳ Waiting for relay Handshake...");
|
||||||
this.relayReadyPromise = new Promise<string>((resolve, reject) => {
|
this.relayReadyPromise = new Promise<string>((resolve, reject) => {
|
||||||
this.relayReadyResolver = resolve;
|
this.relayReadyResolver = resolve;
|
||||||
|
|
||||||
// Timeout de sécurité (10s) pour ne pas bloquer indéfiniment
|
// Timeout after 10s to avoid blocking indefinitely
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (this.relayReadyResolver) {
|
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.relayReadyResolver = null;
|
||||||
this.relayReadyPromise = null;
|
this.relayReadyPromise = null;
|
||||||
}
|
}
|
||||||
@ -91,6 +102,191 @@ export class NetworkService {
|
|||||||
|
|
||||||
// --- INTERNES ---
|
// --- INTERNES ---
|
||||||
|
|
||||||
|
private async registerServiceWorker(): Promise<void> {
|
||||||
|
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<void> {
|
||||||
|
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<ServiceWorkerMessage>) => {
|
||||||
|
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<any> {
|
||||||
|
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) {
|
private async onMessageReceived(flag: string, content: string, url: string) {
|
||||||
const services = await Services.getInstance();
|
const services = await Services.getInstance();
|
||||||
await services.dispatchToWorker(flag, content, url);
|
await services.dispatchToWorker(flag, content, url);
|
||||||
@ -102,10 +298,10 @@ export class NetworkService {
|
|||||||
spAddress?: string
|
spAddress?: string
|
||||||
) {
|
) {
|
||||||
if (status === "OPEN" && spAddress) {
|
if (status === "OPEN" && spAddress) {
|
||||||
// Met à jour et déclenche potentiellement le resolve()
|
// Trigger relay ready promise if someone is waiting
|
||||||
this.updateRelay(url, spAddress);
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,7 +13,6 @@ import { BackUp } from "../types/index";
|
|||||||
import { APP_CONFIG } from "../config/constants";
|
import { APP_CONFIG } from "../config/constants";
|
||||||
import { NetworkService } from "./core/network.service";
|
import { NetworkService } from "./core/network.service";
|
||||||
import type { CoreBackend } from "../workers/core.worker";
|
import type { CoreBackend } from "../workers/core.worker";
|
||||||
import { SWController } from "./sw-controller.service";
|
|
||||||
import Database from "./database.service";
|
import Database from "./database.service";
|
||||||
|
|
||||||
export default class Services {
|
export default class Services {
|
||||||
@ -60,11 +59,7 @@ export default class Services {
|
|||||||
// 2. Initialiser la Database (Main Thread)
|
// 2. Initialiser la Database (Main Thread)
|
||||||
await Database.getInstance();
|
await Database.getInstance();
|
||||||
|
|
||||||
// 3. Initialiser le Service Worker Controller
|
// 3. Configurer les Callbacks
|
||||||
const swController = await SWController.getInstance();
|
|
||||||
await swController.init();
|
|
||||||
|
|
||||||
// 4. Configurer les Callbacks
|
|
||||||
await this.coreWorker.setCallbacks(
|
await this.coreWorker.setCallbacks(
|
||||||
Comlink.proxy(this.handleWorkerNotification.bind(this)),
|
Comlink.proxy(this.handleWorkerNotification.bind(this)),
|
||||||
Comlink.proxy(this.handleWorkerNetworkSend.bind(this)),
|
Comlink.proxy(this.handleWorkerNetworkSend.bind(this)),
|
||||||
@ -72,11 +67,11 @@ export default class Services {
|
|||||||
Comlink.proxy(this.handleWorkerRelayRequest.bind(this))
|
Comlink.proxy(this.handleWorkerRelayRequest.bind(this))
|
||||||
);
|
);
|
||||||
|
|
||||||
// 5. Initialiser le Réseau
|
// 4. Initialiser le Réseau
|
||||||
await this.networkService.initRelays();
|
await this.networkService.initRelays();
|
||||||
|
|
||||||
console.log(
|
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) {
|
public async addWebsocketConnection(url: string) {
|
||||||
await this.networkService.addWebsocketConnection(url);
|
await this.networkService.addWebsocketConnection(url);
|
||||||
}
|
}
|
||||||
public getAllRelays() {
|
public async getAllRelays() {
|
||||||
return this.networkService.getAllRelays();
|
return await this.networkService.getAllRelays();
|
||||||
}
|
}
|
||||||
public updateRelay(url: string, sp: string) {
|
public updateRelay(url: string, sp: string) {
|
||||||
this.networkService.updateRelay(url, sp);
|
this.networkService.updateRelay(url, sp);
|
||||||
}
|
}
|
||||||
public getSpAddress(url: string) {
|
public async getSpAddress(url: string) {
|
||||||
return this.networkService.getAllRelays()[url];
|
const relays = await this.networkService.getAllRelays();
|
||||||
|
return relays[url];
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|||||||
@ -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<SWController> {
|
|
||||||
if (!SWController.instance) {
|
|
||||||
SWController.instance = new SWController();
|
|
||||||
}
|
|
||||||
return SWController.instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async init(): Promise<void> {
|
|
||||||
await this.registerServiceWorker("/data.worker.js");
|
|
||||||
}
|
|
||||||
|
|
||||||
private async registerServiceWorker(path: string): Promise<void> {
|
|
||||||
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<ServiceWorker | null> {
|
|
||||||
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<void> {
|
|
||||||
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<void> {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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());
|
|
||||||
@ -1,31 +1,94 @@
|
|||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
import wasm from 'vite-plugin-wasm';
|
import wasm from 'vite-plugin-wasm';
|
||||||
import { fileURLToPath, URL } from 'node:url';
|
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({
|
export default defineConfig({
|
||||||
// Configuration du serveur de développement
|
// Configuration du serveur de développement
|
||||||
server: {
|
server: {
|
||||||
port: 3003,
|
port: 3003,
|
||||||
host: '0.0.0.0', // Permet l'accès depuis l'extérieur (Docker/Réseau)
|
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: {
|
||||||
// Proxy pour le stockage
|
// Proxy pour le stockage
|
||||||
'/storage': {
|
'/storage': {
|
||||||
target: process.env.VITE_STORAGEURL || 'https://dev2.4nkweb.com',
|
target: process.env.VITE_STORAGEURL || 'https://dev3.4nkweb.com',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
secure: false, // Accepte les certificats auto-signés si besoin
|
secure: false, // Accepte les certificats auto-signés si besoin
|
||||||
rewrite: (path) => path.replace(/^\/storage/, '/storage'),
|
rewrite: (path) => path.replace(/^\/storage/, '/storage'),
|
||||||
},
|
},
|
||||||
// Proxy pour les websockets (si besoin de contourner CORS ou SSL)
|
// Proxy pour les websockets (si besoin de contourner CORS ou SSL)
|
||||||
'/ws': {
|
'/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,
|
ws: true,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
secure: false,
|
secure: false,
|
||||||
},
|
},
|
||||||
// Proxy pour l'API BlindBit
|
// Proxy pour l'API BlindBit
|
||||||
'/blindbit': {
|
'/blindbit': {
|
||||||
target: process.env.VITE_BLINDBITURL || 'https://dev2.4nkweb.com/blindbit',
|
target: process.env.VITE_BLINDBITURL || 'https://dev3.4nkweb.com/blindbit',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
secure: false,
|
secure: false,
|
||||||
rewrite: (path) => path.replace(/^\/blindbit/, ''),
|
rewrite: (path) => path.replace(/^\/blindbit/, ''),
|
||||||
@ -36,6 +99,7 @@ export default defineConfig({
|
|||||||
// Plugins essentiels
|
// Plugins essentiels
|
||||||
plugins: [
|
plugins: [
|
||||||
wasm(), // Indispensable pour ton SDK Rust
|
wasm(), // Indispensable pour ton SDK Rust
|
||||||
|
serviceWorkerPlugin(), // Service Worker handler
|
||||||
],
|
],
|
||||||
|
|
||||||
// Alias pour les imports (ex: import ... from '@/services/...')
|
// Alias pour les imports (ex: import ... from '@/services/...')
|
||||||
@ -52,7 +116,17 @@ export default defineConfig({
|
|||||||
outDir: 'dist',
|
outDir: 'dist',
|
||||||
assetsDir: 'assets',
|
assetsDir: 'assets',
|
||||||
emptyOutDir: true, // Vide le dossier dist avant chaque build
|
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)
|
// Configuration spécifique pour les Workers (Database)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user