308 lines
11 KiB
TypeScript
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
|
|
}
|
|
}
|