279 lines
7.7 KiB
TypeScript
279 lines
7.7 KiB
TypeScript
/**
|
|
* 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<string, ConnectionState> = new Map()
|
|
private reconnectIntervals: Map<string, number> = 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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
// 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<Record<string, unknown>>,
|
|
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()
|