import Services from "../service"; interface ServiceWorkerMessage { type: string; payload?: any; id?: string; } export class NetworkService { private serviceWorkerRegistration: ServiceWorkerRegistration | null = null; private messageIdCounter: number = 0; private pendingMessages: Map void; reject: (error: any) => void }> = new Map(); // 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.setupMessageListener(); } public async initRelays() { try { // Register Service Worker 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) { 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.sendToServiceWorker({ type: 'CONNECT', payload: { url } }); } public async connectAllRelays() { for (const url of this.bootstrapUrls) { this.addWebsocketConnection(url); } } public async sendMessage(flag: string, content: string) { await this.sendToServiceWorker({ type: 'SEND_MESSAGE', payload: { flag, content } }); } // Called by onStatusChange when a relay becomes available // Triggers the relay ready promise if someone is waiting public updateRelay(url: string, spAddress: string) { // Trigger relay ready promise if someone is waiting if (spAddress && spAddress !== "" && this.relayReadyResolver) { this.relayReadyResolver(spAddress); this.relayReadyResolver = null; this.relayReadyPromise = null; } } public async getAllRelays(): Promise> { const response = await this.sendToServiceWorker({ type: 'GET_ALL_RELAYS' }); return response?.relays || {}; } public async getAvailableRelayAddress(): Promise { // 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. If no relay yet, wait for one to become available if (!this.relayReadyPromise) { console.log("[NetworkService] ⏳ Waiting for relay Handshake..."); this.relayReadyPromise = new Promise((resolve, reject) => { this.relayReadyResolver = resolve; // Timeout after 10s to avoid blocking indefinitely setTimeout(() => { if (this.relayReadyResolver) { reject(new Error("Timeout: No relay received after 10s")); this.relayReadyResolver = null; this.relayReadyPromise = null; } }, 10000); }); } return this.relayReadyPromise; } // --- 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); } private onStatusChange( url: string, status: "OPEN" | "CLOSED", spAddress?: string ) { if (status === "OPEN" && spAddress) { // Trigger relay ready promise if someone is waiting this.updateRelay(url, spAddress); } // 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 } }