"use client"; import { io, Socket } from "socket.io-client"; import { createStore, useStore } from "zustand"; import { subscribeWithSelector } from "zustand/middleware"; import { devLogger } from "@/shared/utils/logger"; import type { ClientToServerEvents, ServerToClientEvents } from "./events"; export enum ESocketStatus { CONNECTING = "connecting", CONNECTED = "connected", DISCONNECTED = "disconnected", ERROR = "error", } interface SocketState { socket: Socket | null; status: ESocketStatus; error: string | null; reconnectAttempt: number; // queue: { event: keyof ClientToServerEvents; args: unknown[] }[]; reconnectTimeoutId?: ReturnType; } interface SocketActions { connect: (userId: string) => void; disconnect: () => void; emit: ( event: E, ...args: Parameters ) => void; clearReconnectTimer: () => void; // enqueue: ( // event: E, // ...args: Parameters // ) => void; // flushQueue: () => void; } export type SocketStore = SocketState & SocketActions; const MAX_RECONNECT = 6; const BASE_DELAY = 2000; const SOCKET_URL = process.env.NEXT_PUBLIC_SOCKET_URL ?? ""; if (!SOCKET_URL) { // eslint-disable-next-line no-console console.error("NEXT_PUBLIC_SOCKET_URL env-variable is not set"); } function expDelay(attempt: number) { return Math.min(BASE_DELAY * 2 ** attempt, 30_000); } export const useSocketStore = createStore()( subscribeWithSelector((set, get) => ({ socket: null, status: ESocketStatus.DISCONNECTED, error: null, reconnectAttempt: 0, reconnectTimeoutId: undefined, // queue: [], clearReconnectTimer: () => { const id = get().reconnectTimeoutId; if (id) clearTimeout(id); set({ reconnectTimeoutId: undefined }); }, connect: (userId: string) => { if (get().socket?.connected || !SOCKET_URL) return; get().clearReconnectTimer(); set({ status: ESocketStatus.CONNECTING, error: null, }); const socket: Socket = io( SOCKET_URL, { query: { userId }, transports: ["websocket"], autoConnect: false, } ); const cleanListeners = () => { socket.removeAllListeners(); }; // Universal incoming event logger socket.onAny((event: string, ...args: unknown[]) => { // Skip logging built-in socket.io events to avoid noise const systemEvents = [ "connect", "disconnect", "connect_error", "reconnect", ]; if (!systemEvents.includes(event)) { devLogger.socketIncoming(event, ...args); } }); socket.on("connect", () => { devLogger.socketConnected(); set({ status: ESocketStatus.CONNECTED, reconnectAttempt: 0, socket }); get().clearReconnectTimer(); // get().flushQueue(); }); socket.on("disconnect", reason => { devLogger.socketDisconnected(reason); set({ status: ESocketStatus.DISCONNECTED }); cleanListeners(); scheduleReconnect(); }); socket.on("connect_error", err => { devLogger.socketError(err); set({ status: ESocketStatus.ERROR, error: err.message }); scheduleReconnect(); }); socket.connect(); set({ socket }); function scheduleReconnect() { const attempt = get().reconnectAttempt + 1; if (attempt > MAX_RECONNECT) return; set({ reconnectAttempt: attempt }); /* CHANGE: сохраняем id таймера в state для дальнейшего clear */ const id = setTimeout(() => get().connect(userId), expDelay(attempt)); set({ reconnectTimeoutId: id }); } }, disconnect: () => { get().clearReconnectTimer(); get().socket?.disconnect(); set({ socket: null, status: ESocketStatus.DISCONNECTED, error: null, reconnectAttempt: 0, // queue: [], }); }, emit: (event, ...args) => { const { socket, status } = get(); if (status === ESocketStatus.CONNECTED && socket && socket.connected) { devLogger.socketOutgoing(event as string, ...args); socket.emit(event, ...args); } else { // get().enqueue(event, ...args); // eslint-disable-next-line no-console console.warn("NO SOCKET, emit not sent", event, args); } }, // enqueue: (event, ...args) => // set(state => ({ // queue: [...state.queue, { event, args }], // })), // flushQueue: () => { // const { queue, socket } = get(); // if (!socket) return; // queue.forEach(({ event, args }) => { // socket.emit(event, ...args); // }); // set({ queue: [] }); // }, })) ); export const useSocketStatus = () => useStore(useSocketStore, state => state.status); export const useSocketEmit = () => useStore(useSocketStore, state => state.emit); export const useSocketEntity = () => useStore(useSocketStore, state => state.socket);