/** * WebSocket service - manages WebSocket connections and communicates with Service Workers * * Rôle principal : Gestion des connexions WebSocket * Rôles secondaires : Routage des messages, gestion de l'état de connexion * Communication avec Service Workers : via postMessage */ import { SimplePool } from 'nostr-tools' import { swClient } from './swClient' import type { Event, Filter } from 'nostr-tools' interface ConnectionState { relayUrl: string connected: boolean lastConnectedAt: number | null lastDisconnectedAt: number | null reconnectAttempts: number } class WebSocketService { private pool: SimplePool | null = null private initialized = false private connectionStates: Map = new Map() private reconnectIntervals: Map = new Map() private readonly MAX_RECONNECT_ATTEMPTS = 5 private readonly RECONNECT_DELAY_MS = 5000 /** * Initialize the WebSocket service * Rôle principal : Gestion des connexions */ async initialize(): Promise { if (this.initialized) { return } if (typeof window === 'undefined') { return } this.pool = new SimplePool() this.initialized = true // Notify Service Worker of initialization void swClient.sendMessage({ type: 'WEBSOCKET_SERVICE_INITIALIZED', }) console.warn('[WebSocketService] Initialized') } /** * Get the pool instance */ getPool(): SimplePool | null { return this.pool } /** * Get connection state for a relay * Rôle secondaire : Gestion de l'état de connexion */ getConnectionState(relayUrl: string): ConnectionState | null { return this.connectionStates.get(relayUrl) ?? null } /** * Update connection state * Rôle secondaire : Gestion de l'état de connexion */ private updateConnectionState(relayUrl: string, connected: boolean): void { const current = this.connectionStates.get(relayUrl) ?? { relayUrl, connected: false, lastConnectedAt: null, lastDisconnectedAt: null, reconnectAttempts: 0, } const now = Date.now() const updated: ConnectionState = { ...current, connected, lastConnectedAt: connected ? now : current.lastConnectedAt, lastDisconnectedAt: connected ? null : now, reconnectAttempts: connected ? 0 : current.reconnectAttempts, } this.connectionStates.set(relayUrl, updated) // Notify Service Worker of connection state change via postMessage void swClient.sendMessage({ type: 'WEBSOCKET_CONNECTION_STATE_CHANGED', data: { relayUrl, connected, state: updated }, }) } /** * Handle reconnection for a relay * Rôle principal : Gestion des reconnexions */ private async handleReconnection(relayUrl: string): Promise { const state = this.connectionStates.get(relayUrl) if (!state || state.connected) { return } if (state.reconnectAttempts >= this.MAX_RECONNECT_ATTEMPTS) { console.warn(`[WebSocketService] Max reconnect attempts reached for ${relayUrl}`) return } const updated: ConnectionState = { ...state, reconnectAttempts: state.reconnectAttempts + 1, } this.connectionStates.set(relayUrl, updated) // Notify Service Worker of reconnection attempt via postMessage void swClient.sendMessage({ type: 'WEBSOCKET_RECONNECT_ATTEMPT', data: { relayUrl, attempt: updated.reconnectAttempts }, }) // Schedule reconnection const timeoutId = window.setTimeout(() => { void this.attemptReconnection(relayUrl) }, this.RECONNECT_DELAY_MS) this.reconnectIntervals.set(relayUrl, timeoutId) } /** * Attempt to reconnect to a relay */ private async attemptReconnection(relayUrl: string): Promise { // The pool will handle reconnection automatically // We just need to track the state console.warn(`[WebSocketService] Attempting to reconnect to ${relayUrl}`) } /** * Publish event to relays via WebSocket * Rôle secondaire : Routage des messages * Communicates with Service Worker via postMessage */ async publishEvent(event: Event, relays: string[]): Promise<{ success: boolean; error?: string }[]> { if (!this.pool) { await this.initialize() } if (!this.pool) { throw new Error('WebSocket service not initialized') } // Update connection states relays.forEach((relayUrl) => { if (!this.connectionStates.has(relayUrl)) { this.updateConnectionState(relayUrl, true) // Assume connected when publishing } }) // Publish to relays const pubs = this.pool.publish(relays, event) const results = await Promise.allSettled(pubs) const statuses: Array<{ success: boolean; error?: string }> = [] results.forEach((result, index) => { const relayUrl = relays[index] if (!relayUrl) { return } if (result.status === 'fulfilled') { statuses.push({ success: true }) this.updateConnectionState(relayUrl, true) // Notify Service Worker of successful publication via postMessage void swClient.sendMessage({ type: 'WEBSOCKET_PUBLISH_SUCCESS', data: { eventId: event.id, relayUrl }, }) } else { const error = result.reason instanceof Error ? result.reason.message : String(result.reason) statuses.push({ success: false, error }) this.updateConnectionState(relayUrl, false) // Notify Service Worker of failed publication via postMessage void swClient.sendMessage({ type: 'WEBSOCKET_PUBLISH_FAILED', data: { eventId: event.id, relayUrl, error }, }) // Trigger reconnection void this.handleReconnection(relayUrl) } }) return statuses } /** * Subscribe to events from relays * Rôle secondaire : Routage des messages * Communicates with Service Worker via postMessage */ async subscribe( relays: string[], filters: Array>, onEvent: (event: Event) => void ): Promise<() => void> { if (!this.pool) { await this.initialize() } if (!this.pool) { throw new Error('WebSocket service not initialized') } // Update connection states relays.forEach((relayUrl) => { this.updateConnectionState(relayUrl, true) // Assume connected when subscribing }) // Create subscription - use first filter or empty filter const filter: Filter = (filters[0] as Filter) ?? {} const sub = this.pool.subscribe(relays, filter, { onevent: (event: Event): void => { // Notify Service Worker of new event via postMessage void swClient.sendMessage({ type: 'WEBSOCKET_EVENT_RECEIVED', data: { event }, }) onEvent(event) }, oneose: (): void => { // Notify Service Worker that subscription is complete via postMessage void swClient.sendMessage({ type: 'WEBSOCKET_EOSE', data: { relays }, }) }, }) return (): void => { sub.close() } } /** * Close all connections * Rôle principal : Gestion des connexions */ close(): void { // Clear reconnect intervals this.reconnectIntervals.forEach((timeoutId) => { clearTimeout(timeoutId) }) this.reconnectIntervals.clear() if (this.pool) { this.pool.close() this.pool = null } // Update all connection states this.connectionStates.forEach((_state, relayUrl) => { this.updateConnectionState(relayUrl, false) }) this.initialized = false // Notify Service Worker via postMessage void swClient.sendMessage({ type: 'WEBSOCKET_SERVICE_CLOSED', }) } } export const websocketService = new WebSocketService()