/** * ServiceWorker Communication Manager */ import type { SwMockConfig } from '../types'; export interface MessageResponse { success: boolean; data?: T; error?: string; } export interface SwMessage { type: string; payload?: any; } export class SwCommunication { private serviceWorker: ServiceWorker | null = null; private isInitialized = false; private workerUrl: string; private scope: string; constructor(workerUrl = '/sw-mock-worker.js', scope = '/') { this.workerUrl = workerUrl; this.scope = scope; } /** * Check if ServiceWorker file is accessible */ private async checkWorkerFile(): Promise { try { const response = await fetch(this.workerUrl, { method: 'HEAD' }); if (!response.ok) { throw new Error(`ServiceWorker file not found: ${this.workerUrl} (${response.status})`); } } catch (error) { if (error instanceof TypeError) { throw new Error(`Cannot access ServiceWorker file: ${this.workerUrl}. Please ensure the file exists and is accessible.`); } throw error; } } /** * Initialize ServiceWorker connection */ async init(): Promise { if (this.isInitialized) return; if (!('serviceWorker' in navigator)) { throw new Error('ServiceWorker is not supported in this browser'); } try { console.log(`[SW Mock] Checking ServiceWorker file: ${this.workerUrl}`); await this.checkWorkerFile(); console.log(`[SW Mock] Registering ServiceWorker: ${this.workerUrl} with scope: ${this.scope}`); const registration = await navigator.serviceWorker.register(this.workerUrl, { scope: this.scope, updateViaCache: 'none' }); console.log('[SW Mock] ServiceWorker registration successful, waiting for activation...'); // Wait for ServiceWorker to be ready await this.waitForServiceWorker(registration); this.serviceWorker = registration.active || registration.waiting || registration.installing; if (!this.serviceWorker) { throw new Error('ServiceWorker is not available after registration'); } this.isInitialized = true; console.log('[SW Mock] ServiceWorker initialized successfully'); // 测试ServiceWorker是否真正准备好 await this.testServiceWorkerReady(); } catch (error) { console.error('[SW Mock] ServiceWorker initialization failed:', error); throw error; } } /** * Wait for ServiceWorker to be activated and ready */ private async waitForServiceWorker(registration: ServiceWorkerRegistration): Promise { return new Promise((resolve, reject) => { // Set timeout for ServiceWorker activation const timeout = setTimeout(() => { reject(new Error('ServiceWorker activation timeout')); }, 20000); // 增加到20秒超时 const cleanup = () => { clearTimeout(timeout); }; if (registration.active) { cleanup(); // 额外等待一小段时间确保ServiceWorker完全准备好 setTimeout(resolve, 100); return; } const worker = registration.installing || registration.waiting; if (worker) { const onStateChange = () => { console.log(`[SW Mock] ServiceWorker state changed to: ${worker.state}`); if (worker.state === 'activated') { worker.removeEventListener('statechange', onStateChange); cleanup(); // 额外等待一小段时间确保ServiceWorker完全准备好 setTimeout(resolve, 100); } else if (worker.state === 'redundant') { worker.removeEventListener('statechange', onStateChange); cleanup(); reject(new Error('ServiceWorker became redundant')); } }; worker.addEventListener('statechange', onStateChange); } else { // Fallback: wait for controllerchange event const onControllerChange = () => { cleanup(); setTimeout(resolve, 100); }; navigator.serviceWorker.addEventListener('controllerchange', onControllerChange, { once: true }); } }); } /** * Test if ServiceWorker is ready to receive messages */ private async testServiceWorkerReady(): Promise { console.log('[SW Mock] Testing ServiceWorker readiness...'); try { // 尝试获取配置来测试通信 await this.sendMessageOnce('GET_CONFIG'); console.log('[SW Mock] ServiceWorker is ready and responding'); } catch (error) { console.warn('[SW Mock] ServiceWorker readiness test failed:', error); throw new Error('ServiceWorker is not ready to receive messages'); } } /** * Send message to ServiceWorker with retry mechanism */ async sendMessage(type: string, payload?: any, retries = 3): Promise { if (!this.isInitialized) { await this.init(); } if (!this.serviceWorker) { throw new Error('ServiceWorker is not available'); } for (let attempt = 1; attempt <= retries; attempt++) { try { return await this.sendMessageOnce(type, payload); } catch (error) { console.warn(`[SW Mock] Message attempt ${attempt}/${retries} failed:`, error); if (attempt === retries) { throw error; } // Wait before retry await new Promise(resolve => setTimeout(resolve, 1000 * attempt)); // Check if ServiceWorker is still available if (!this.serviceWorker || this.serviceWorker.state === 'redundant') { console.log('[SW Mock] ServiceWorker became unavailable, reinitializing...'); this.isInitialized = false; this.serviceWorker = null; await this.init(); } } } throw new Error('All message attempts failed'); } /** * Send a single message to ServiceWorker */ private async sendMessageOnce(type: string, payload?: any): Promise { console.log(`[SW Mock] Sending message: ${type}`, payload); return new Promise((resolve, reject) => { const messageChannel = new MessageChannel(); // Set timeout for message response (增加到15秒) const timeout = setTimeout(() => { console.error(`[SW Mock] Message timeout for type: ${type}`); reject(new Error(`ServiceWorker message timeout for type: ${type}`)); }, 15000); messageChannel.port1.onmessage = (event) => { clearTimeout(timeout); console.log(`[SW Mock] Received response for: ${type}`, event.data); const response: MessageResponse = event.data; if (response.success) { resolve(response.data as T); } else { reject(new Error(response.error || 'Unknown error')); } }; if (!this.serviceWorker) { clearTimeout(timeout); reject(new Error('ServiceWorker is not available')); return; } console.log(`[SW Mock] Posting message to ServiceWorker: ${type}`); this.serviceWorker.postMessage( { type, payload } as SwMessage, [messageChannel.port2] ); }); } /** * Get current configuration */ async getConfig(): Promise { return await this.sendMessage('GET_CONFIG'); } /** * Update configuration */ async updateConfig(config: Partial): Promise { return await this.sendMessage('UPDATE_CONFIG', config); } /** * Reset configuration */ async resetConfig(): Promise { return await this.sendMessage('RESET_CONFIG'); } /** * Enable mock functionality */ async enableMock(): Promise { return await this.updateConfig({ enabled: true }); } /** * Disable mock functionality */ async disableMock(): Promise { return await this.updateConfig({ enabled: false }); } /** * Toggle mock functionality */ async toggleMock(): Promise { const config = await this.getConfig(); const newState = !config.enabled; await this.updateConfig({ enabled: newState }); return newState; } /** * Enable network failure simulation */ async enableNetworkFailure(): Promise { return await this.sendMessage('ENABLE_NETWORK_FAILURE'); } /** * Disable network failure simulation */ async disableNetworkFailure(): Promise { return await this.sendMessage('DISABLE_NETWORK_FAILURE'); } /** * Update network failure configuration */ async updateNetworkFailureConfig(config: any): Promise { return await this.sendMessage('UPDATE_NETWORK_FAILURE_CONFIG', config); } /** * Reset network failure state */ async resetNetworkFailureState(): Promise { return await this.sendMessage('RESET_NETWORK_FAILURE_STATE'); } /** * Start request capture */ async startCapture(): Promise { return await this.sendMessage('START_CAPTURE'); } /** * Stop request capture */ async stopCapture(): Promise { return await this.sendMessage('STOP_CAPTURE'); } /** * Clear database */ async clearDatabase(): Promise { return await this.sendMessage('CLEAR_DATABASE'); } /** * Check if ServiceWorker is ready */ isReady(): boolean { return this.isInitialized && !!this.serviceWorker; } /** * Get current scope */ getScope(): string { return this.scope; } /** * Get ServiceWorker registration */ async getRegistration(): Promise { if (!('serviceWorker' in navigator)) { return null; } return (await navigator.serviceWorker.getRegistration()) || null; } /** * Unregister ServiceWorker */ async unregister(): Promise { const registration = await this.getRegistration(); if (registration) { const result = await registration.unregister(); this.isInitialized = false; this.serviceWorker = null; return result; } return false; } }