141 lines
4.1 KiB
TypeScript
141 lines
4.1 KiB
TypeScript
import { io, Socket } from 'socket.io-client'
|
|
|
|
const SOCKET_URL = (() => {
|
|
const url = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8008'
|
|
try {
|
|
return new URL(url).origin
|
|
} catch {
|
|
return url
|
|
}
|
|
})()
|
|
|
|
type Listener = (data: any) => void
|
|
|
|
const HEARTBEAT_INTERVAL_MS = 30_000
|
|
|
|
class SocketService {
|
|
private socket: Socket | null = null
|
|
private token: string | null = null
|
|
private listeners = new Map<string, Set<Listener>>()
|
|
private heartbeatTimer: ReturnType<typeof setInterval> | null = null
|
|
|
|
// ── Conexão ────────────────────────────────────────────────────────────────
|
|
|
|
connect(token?: string) {
|
|
if (token) this.token = token
|
|
if (this.socket?.connected) return
|
|
|
|
this.socket = io(SOCKET_URL, {
|
|
auth: { token: this.token },
|
|
transports: ['websocket', 'polling'],
|
|
reconnectionAttempts: 10,
|
|
reconnectionDelay: 2000,
|
|
})
|
|
|
|
this.socket.on('connect', () => {
|
|
this._emit('connected', { id: this.socket?.id })
|
|
this._startHeartbeat()
|
|
})
|
|
|
|
this.socket.on('disconnect', (reason) => {
|
|
this._stopHeartbeat()
|
|
this._emit('disconnected', { reason })
|
|
})
|
|
|
|
this.socket.on('connect_error', (err) => {
|
|
console.warn('[Socket] connect_error:', err.message)
|
|
})
|
|
|
|
this.socket.onAny((event, data) => {
|
|
console.log('[Socket Event Received]', event, data)
|
|
this._emit(event, data)
|
|
if (event !== '*') this._emit('*', { event, data })
|
|
})
|
|
}
|
|
|
|
disconnect() {
|
|
this._stopHeartbeat()
|
|
this.socket?.disconnect()
|
|
this.socket = null
|
|
this.token = null
|
|
}
|
|
|
|
// ── Emit para o servidor ───────────────────────────────────────────────────
|
|
|
|
emit(event: string, ...args: any[]) {
|
|
this.socket?.emit(event, ...args)
|
|
}
|
|
|
|
joinInstance(instanceId: string) {
|
|
if (this.socket?.connected) {
|
|
this.socket.emit('join:instance', instanceId)
|
|
} else {
|
|
// Aguarda conexão e então entra na sala
|
|
const onConnect = () => {
|
|
this.socket?.emit('join:instance', instanceId)
|
|
this.off('connected', onConnect)
|
|
}
|
|
this.on('connected', onConnect)
|
|
}
|
|
}
|
|
|
|
joinChat(chatId: string) {
|
|
if (this.socket?.connected) {
|
|
this.socket.emit('join:chat', chatId)
|
|
} else {
|
|
const onConnect = () => {
|
|
this.socket?.emit('join:chat', chatId)
|
|
this.off('connected', onConnect)
|
|
}
|
|
this.on('connected', onConnect)
|
|
}
|
|
}
|
|
|
|
leaveChat(chatId: string) {
|
|
this.socket?.emit('leave:chat', chatId)
|
|
}
|
|
|
|
// ── Listeners ─────────────────────────────────────────────────────────────
|
|
|
|
on(event: string, cb: Listener) {
|
|
if (!this.listeners.has(event)) this.listeners.set(event, new Set())
|
|
this.listeners.get(event)!.add(cb)
|
|
}
|
|
|
|
off(event: string, cb: Listener) {
|
|
this.listeners.get(event)?.delete(cb)
|
|
}
|
|
|
|
// ── Estado ────────────────────────────────────────────────────────────────
|
|
|
|
get connected() {
|
|
return this.socket?.connected ?? false
|
|
}
|
|
|
|
getSocket() {
|
|
return this.socket
|
|
}
|
|
|
|
// ── Privado ───────────────────────────────────────────────────────────────
|
|
|
|
private _startHeartbeat() {
|
|
this._stopHeartbeat()
|
|
this.heartbeatTimer = setInterval(() => {
|
|
if (this.socket?.connected) this.socket.emit('user:heartbeat')
|
|
}, HEARTBEAT_INTERVAL_MS)
|
|
}
|
|
|
|
private _stopHeartbeat() {
|
|
if (this.heartbeatTimer) {
|
|
clearInterval(this.heartbeatTimer)
|
|
this.heartbeatTimer = null
|
|
}
|
|
}
|
|
|
|
private _emit(event: string, data: any) {
|
|
this.listeners.get(event)?.forEach((cb) => cb(data))
|
|
}
|
|
}
|
|
|
|
export const socketService = new SocketService()
|