w-lab-app/src/services/socket/index.ts
2025-07-26 20:22:00 +04:00

184 lines
5.1 KiB
TypeScript

"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<ServerToClientEvents, ClientToServerEvents> | null;
status: ESocketStatus;
error: string | null;
reconnectAttempt: number;
// queue: { event: keyof ClientToServerEvents; args: unknown[] }[];
reconnectTimeoutId?: ReturnType<typeof setTimeout>;
}
interface SocketActions {
connect: (userId: string) => void;
disconnect: () => void;
emit: <E extends keyof ClientToServerEvents>(
event: E,
...args: Parameters<ClientToServerEvents[E]>
) => void;
clearReconnectTimer: () => void;
// enqueue: <E extends keyof ClientToServerEvents>(
// event: E,
// ...args: Parameters<ClientToServerEvents[E]>
// ) => 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<SocketStore>()(
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<ServerToClientEvents, ClientToServerEvents> = 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);