184 lines
5.1 KiB
TypeScript
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);
|