Spaces:
Sleeping
Sleeping
| 'use client'; | |
| /** | |
| * ServiceWorker管理モジュール | |
| * ベストプラクティスに基づいた初期化と状態管理を提供 | |
| */ | |
| export interface ServiceWorkerState { | |
| isSupported: boolean; | |
| isRegistered: boolean; | |
| isReady: boolean; | |
| registration: ServiceWorkerRegistration | null; | |
| hasUpdate: boolean; | |
| } | |
| export interface ServiceWorkerConfig { | |
| swUrl: string; | |
| scope?: string; | |
| enableDevMode?: boolean; | |
| updateCheckInterval?: number; | |
| } | |
| type ServiceWorkerEventHandler = (state: ServiceWorkerState) => void; | |
| class ServiceWorkerManager { | |
| private config: ServiceWorkerConfig; | |
| private state: ServiceWorkerState; | |
| private eventHandlers: Set<ServiceWorkerEventHandler> = new Set(); | |
| private updateCheckTimer: NodeJS.Timeout | null = null; | |
| constructor(config: Partial<ServiceWorkerConfig>) { | |
| this.config = Object.assign({ | |
| swUrl: '/sw.js', | |
| scope: '/', | |
| enableDevMode: process.env.NODE_ENV === 'development', | |
| updateCheckInterval: 30 * 60 * 1000, // 30分 | |
| }, config); | |
| this.state = { | |
| isSupported: false, | |
| isRegistered: false, | |
| isReady: false, | |
| registration: null, | |
| hasUpdate: false, | |
| }; | |
| } | |
| /** | |
| * ServiceWorkerの初期化 | |
| * 遅延初期化でパフォーマンスを最適化 | |
| */ | |
| async initialize(): Promise<ServiceWorkerState> { | |
| // サポート確認 | |
| if (!this.checkSupport()) { | |
| this.log('warn', 'ServiceWorker is not supported in this browser'); | |
| return this.state; | |
| } | |
| try { | |
| // 既存の登録をチェック | |
| const existingRegistration = await navigator.serviceWorker.getRegistration(this.config.scope); | |
| if (existingRegistration) { | |
| this.log('info', 'Existing ServiceWorker registration found'); | |
| await this.handleExistingRegistration(existingRegistration); | |
| } else { | |
| this.log('info', 'No existing registration, registering new ServiceWorker'); | |
| await this.register(); | |
| } | |
| // 定期的な更新チェックを開始 | |
| this.startUpdateCheck(); | |
| return this.state; | |
| } catch (error) { | |
| this.log('error', 'ServiceWorker initialization failed:', error); | |
| throw error; | |
| } | |
| } | |
| /** | |
| * ServiceWorkerの登録 | |
| */ | |
| private async register(): Promise<ServiceWorkerRegistration> { | |
| try { | |
| const registration = await navigator.serviceWorker.register(this.config.swUrl, { | |
| scope: this.config.scope, | |
| }); | |
| this.log('info', 'ServiceWorker registered successfully:', registration.scope); | |
| this.state.registration = registration; | |
| this.state.isRegistered = true; | |
| await this.setupRegistrationEventHandlers(registration); | |
| await this.checkReadyState(registration); | |
| this.notifyStateChange(); | |
| return registration; | |
| } catch (error) { | |
| this.log('error', 'ServiceWorker registration failed:', error); | |
| throw new Error(`ServiceWorker registration failed: ${error}`); | |
| } | |
| } | |
| /** | |
| * 既存の登録を処理 | |
| */ | |
| private async handleExistingRegistration(registration: ServiceWorkerRegistration): Promise<void> { | |
| this.state.registration = registration; | |
| this.state.isRegistered = true; | |
| await this.setupRegistrationEventHandlers(registration); | |
| await this.checkReadyState(registration); | |
| await this.checkForUpdates(registration); | |
| this.notifyStateChange(); | |
| } | |
| /** | |
| * 登録イベントハンドラーの設定 | |
| */ | |
| private async setupRegistrationEventHandlers(registration: ServiceWorkerRegistration): Promise<void> { | |
| // 更新があった場合 | |
| registration.addEventListener('updatefound', () => { | |
| this.log('info', 'ServiceWorker update found'); | |
| const newWorker = registration.installing; | |
| if (newWorker) { | |
| this.state.hasUpdate = true; | |
| this.notifyStateChange(); | |
| newWorker.addEventListener('statechange', () => { | |
| if (newWorker.state === 'installed' && navigator.serviceWorker.controller) { | |
| this.log('info', 'New ServiceWorker installed, update available'); | |
| this.state.hasUpdate = true; | |
| this.notifyStateChange(); | |
| } | |
| }); | |
| } | |
| }); | |
| // ServiceWorkerがアクティブになった場合 | |
| navigator.serviceWorker.addEventListener('controllerchange', () => { | |
| this.log('info', 'ServiceWorker controller changed'); | |
| this.checkReadyState(registration); | |
| this.notifyStateChange(); | |
| }); | |
| } | |
| /** | |
| * 準備状態の確認 | |
| */ | |
| private async checkReadyState(registration: ServiceWorkerRegistration): Promise<void> { | |
| const isReady = !!registration.active || !!navigator.serviceWorker.controller; | |
| this.state.isReady = isReady; | |
| if (isReady) { | |
| this.log('info', 'ServiceWorker is ready'); | |
| } | |
| } | |
| /** | |
| * 更新の確認 | |
| */ | |
| private async checkForUpdates(registration: ServiceWorkerRegistration): Promise<void> { | |
| try { | |
| await registration.update(); | |
| this.log('info', 'ServiceWorker update check completed'); | |
| } catch (error) { | |
| this.log('warn', 'ServiceWorker update check failed:', error); | |
| } | |
| } | |
| /** | |
| * 定期的な更新チェックを開始 | |
| */ | |
| private startUpdateCheck(): void { | |
| if (!this.config.updateCheckInterval || this.updateCheckTimer) { | |
| return; | |
| } | |
| this.updateCheckTimer = setInterval(async () => { | |
| if (this.state.registration) { | |
| await this.checkForUpdates(this.state.registration); | |
| } | |
| }, this.config.updateCheckInterval); | |
| this.log('info', `Update check scheduled every ${this.config.updateCheckInterval}ms`); | |
| } | |
| /** | |
| * 更新チェックを停止 | |
| */ | |
| private stopUpdateCheck(): void { | |
| if (this.updateCheckTimer) { | |
| clearInterval(this.updateCheckTimer); | |
| this.updateCheckTimer = null; | |
| } | |
| } | |
| /** | |
| * ServiceWorkerの更新を適用 | |
| */ | |
| async applyUpdate(): Promise<void> { | |
| if (!this.state.hasUpdate || !this.state.registration) { | |
| throw new Error('No update available'); | |
| } | |
| const waitingWorker = this.state.registration.waiting; | |
| if (waitingWorker) { | |
| this.log('info', 'Applying ServiceWorker update'); | |
| // 新しいServiceWorkerにメッセージを送信 | |
| waitingWorker.postMessage({ type: 'SKIP_WAITING' }); | |
| // ページリロードを促す | |
| return new Promise((resolve) => { | |
| navigator.serviceWorker.addEventListener( | |
| 'controllerchange', | |
| () => { | |
| this.log('info', 'ServiceWorker update applied, reloading page'); | |
| window.location.reload(); | |
| resolve(); | |
| }, | |
| { once: true }, | |
| ); | |
| }); | |
| } | |
| } | |
| /** | |
| * ServiceWorkerの登録解除 | |
| */ | |
| async unregister(): Promise<boolean> { | |
| if (!this.state.registration) { | |
| return false; | |
| } | |
| try { | |
| const unregistered = await this.state.registration.unregister(); | |
| if (unregistered) { | |
| this.log('info', 'ServiceWorker unregistered successfully'); | |
| this.state.isRegistered = false; | |
| this.state.isReady = false; | |
| this.state.registration = null; | |
| this.state.hasUpdate = false; | |
| this.stopUpdateCheck(); | |
| this.notifyStateChange(); | |
| } | |
| return unregistered; | |
| } catch (error) { | |
| this.log('error', 'ServiceWorker unregistration failed:', error); | |
| throw error; | |
| } | |
| } | |
| /** | |
| * ブラウザサポートの確認 | |
| */ | |
| private checkSupport(): boolean { | |
| const isSupported = 'serviceWorker' in navigator; | |
| this.state.isSupported = isSupported; | |
| return isSupported; | |
| } | |
| /** | |
| * 状態変更の通知 | |
| */ | |
| private notifyStateChange(): void { | |
| this.eventHandlers.forEach((handler) => { | |
| try { | |
| handler(this.state); | |
| } catch (error) { | |
| this.log('error', 'Error in state change handler:', error); | |
| } | |
| }); | |
| } | |
| /** | |
| * イベントハンドラーの追加 | |
| */ | |
| onStateChange(handler: ServiceWorkerEventHandler): () => void { | |
| this.eventHandlers.add(handler); | |
| // 現在の状態を即座に通知 | |
| handler(this.state); | |
| // アンサブスクライブ関数を返す | |
| return () => { | |
| this.eventHandlers.delete(handler); | |
| }; | |
| } | |
| /** | |
| * 現在の状態を取得 | |
| */ | |
| getState(): ServiceWorkerState { | |
| return { ...this.state }; | |
| } | |
| /** | |
| * ログ出力(開発モード対応) | |
| */ | |
| private log(level: 'info' | 'warn' | 'error', message: string, ...args: unknown[]): void { | |
| if (!this.config.enableDevMode && level === 'info') { | |
| return; | |
| } | |
| const prefix = '[ServiceWorkerManager]'; | |
| switch (level) { | |
| case 'info': | |
| console.log(prefix, message, ...args); | |
| break; | |
| case 'warn': | |
| console.warn(prefix, message, ...args); | |
| break; | |
| case 'error': | |
| console.error(prefix, message, ...args); | |
| break; | |
| } | |
| } | |
| /** | |
| * リソースのクリーンアップ | |
| */ | |
| destroy(): void { | |
| this.stopUpdateCheck(); | |
| this.eventHandlers.clear(); | |
| } | |
| } | |
| // シングルトンインスタンス | |
| let serviceWorkerManager: ServiceWorkerManager | null = null; | |
| /** | |
| * ServiceWorkerマネージャーのインスタンスを取得 | |
| */ | |
| export function getServiceWorkerManager(config?: Partial<ServiceWorkerConfig>): ServiceWorkerManager { | |
| if (!serviceWorkerManager) { | |
| serviceWorkerManager = new ServiceWorkerManager({ swUrl: '/sw.js', ...config }); | |
| } | |
| return serviceWorkerManager; | |
| } | |
| /** | |
| * ServiceWorkerの初期化(遅延実行) | |
| */ | |
| export function initializeServiceWorker(config?: Partial<ServiceWorkerConfig>): Promise<ServiceWorkerState> { | |
| return new Promise((resolve) => { | |
| // window.loadイベント後に実行してパフォーマンスを最適化 | |
| const initialize = () => { | |
| const manager = getServiceWorkerManager(config); | |
| resolve(manager.initialize()); | |
| }; | |
| if (document.readyState === 'complete') { | |
| // 既にロード完了している場合は即座に実行 | |
| setTimeout(initialize, 0); | |
| } else { | |
| // ロード完了を待つ | |
| window.addEventListener('load', initialize, { once: true }); | |
| } | |
| }); | |
| } | |
| /** | |
| * ServiceWorkerが利用可能かチェック | |
| */ | |
| export function isServiceWorkerSupported(): boolean { | |
| return 'serviceWorker' in navigator; | |
| } | |