ihm_client/src/services/core/network.service.ts

308 lines
11 KiB
TypeScript

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<string, { resolve: (value: any) => 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<string> | 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<Record<string, string>> {
const response = await this.sendToServiceWorker({ type: 'GET_ALL_RELAYS' });
return response?.relays || {};
}
public async getAvailableRelayAddress(): Promise<string> {
// 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<string>((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<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) {
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
}
}