Источник:
smarty-code/smarty-backend-stable/smarty-socket/docs/ARCHITECTURE.md
Актуальность: 2026-06-13
Версия socket.io: 4.8.x (server) / 4.8.x (client)
Транспорт: WebSocket-only (transports: ['websocket'])
smarty-socket — сервис-шлюз реального времени на базе socket.io v4. Единственный компонент системы, открывающий прямой WebSocket-канал до клиентов. Предоставляет:
Сервис не содержит бизнес-логики — он транслирует события из RabbitMQ и Redis pub/sub в WebSocket. Генерация событий происходит в smarty-notification, smarty-dialog, smarty-sip, smarty-ext-messenger и др.
service.js
├── require('../lib/microservice') → MicroService (HTTP + MQ фреймворк)
├── addQueue('socket-commands', queueHandler) → подписка на RabbitMQ
├── addHTTP(config.host.ws, config.port.ws) → HTTP-сервер (порт 3042, хост 0.0.0.0)
└── run(() => startServer(ms.http.server)) → передаёт HTTP в socket.io
startServer(ms.http.server) в events.js делает io.listen(httpServer) — socket.io прикрепляется к уже запущенному HTTP-серверу MicroService.
| Файл | Роль |
|---|---|
service.js |
Точка входа. Определяет HTTP-порт (3042), очередь RabbitMQ socket-commands, запуск. |
events.js |
Обработчик очереди RabbitMQ. Карта команд: disconnect-session, disconnect-other-sessions. Утилиты disconnectClients, getRoomsClients поверх socket.io v4 API (fetchSockets/disconnectSockets). |
lib/io.js |
Создаёт socket.io Server, настраивает Redis-adapter, middleware-цепочку (io.use), подключает обработчик соединений. |
lib/authHandler.js |
Middleware аутентификации (io.use). Извлекает сессию из HTTP-заголовков, валидирует аккаунт, запрещает WorkspaceApi (только реальные пользователи). |
lib/socketEventsHandler.js |
Обработчик connection. Регистрирует join/leave комнат, heartbeat (pong), online-status, disconnect. |
lib/connectionsSet.js |
Обёртка над Redis SET socket_connections — глобальный счётчик активных соединений. |
lib/clearConnections.js |
Startup-очистка: обнуляет Redis SET, сбрасывает activeSocketSessions/isOnline у User/Employee, помечает sync_sessions оффлайн. |
lib/utils/socket-emitter.js |
@socket.io/redis-emitter — публикация событий из любого микросервиса через Redis, без прямого доступа к socket.io Server. |
Клиент smarty-socket Redis / MongoDB
│ │ │
│── WS upgrade ──────────────────►│ │
│ │ │
│ authHandler (io.use) │
│ getAccountFromRequest(headers) │
│ validateAccount(account) │
│ reject WorkspaceApi │
│ requestInfo → platform │
│ socket.request = {sessionId, user, │
│ connectionId, platform} │
│ │ │
│ │◄─── connection ─── │
│ socketEventsHandler │
│ │ │
│── "join" (employeeData) ────────►│ │
│ validateAccount(employee) │
│ clearWsState(employee, sessionId) │
│ clearSessionsSyncro │
│ activeSocketSessions.$pull │
│ socket.join(employee_N, ws_M, session_K) │
│ isOnlineCheck │
│ SET online-user:N = true │
│ SET online-employee:M = true │
│ Employee.setOnlineStatus(true) │
│ emit employeeOnlineStatusChanged │
│ sadd socket_connections │
│ │ │
│── "pong" (heartbeat) ──────────►│ │
│ User.updateLastActivityTime │
│ │ │
│◄── "dataChanged" ───────────────│ (от серверных сервисов) │
│◄── "sessionTerminated" ─────────│ (от RabbitMQ команд) │
│ │ │
│── disconnect ──────────────────►│ │
│ srem socket_connections │
│ isOnlineCheck (может → offline) │
│ employeeGoneOffline / sessionGoneOffline│
│ emit employeeOnlineStatusChanged │
smarty-socket/
├── service.js # entrypoint, конфигурация MicroService
├── events.js # RabbitMQ-обработчики команд
├── package.json # main: service.js
└── lib/
├── io.js # socket.io Server + Redis-adapter + middleware
├── authHandler.js # аутентификация через HTTP-заголовки сессии
├── socketEventsHandler.js # обработка join/leave/pong/disconnect
├── connectionsSet.js # Redis SET для подсчёта соединений
└── clearConnections.js # startup-очистка состояния
lib/utils/
└── socket-emitter.js # @socket.io/redis-emitter (используется всеми сервисами)
| Паттерн | Назначение | Кто join/leave |
|---|---|---|
employee_{_id} |
Сотрудник (все его сессии) | join / leave при подключении |
ws_{_wsId} |
Workspace — все сотрудники воркспейса | join при leave+join в socketEventsHandler |
session_{sessionId} |
Конкретная сессия (для disconnect по sessionId) | join при подключении |
user_{_id} |
Пользователь (все его соединения) | join для обычных пользователей (не сотрудников) |
Эмиттеры из других сервисов адресуют комнаты через socket-emitter (Redis):
io.to('employee_XXX').emit(...) — конкретному сотруднику;io.to('ws_XXX').emit(...) — всем сотрудникам воркспейса;io.to('session_XXX').emit(...) — конкретной сессии.// lib/io.js
const subClient = redis.duplicate();
io.adapter(createAdapter(redis, subClient));
redis) — из smarty-db, общий с БД-слоем.subClient — отдельное соединение для Redis SUBSCRIBE (требование @socket.io/redis-adapter).smarty-socket — все события через Redis pub/sub рассылаются по всем инстансам.Инвариант: Redis-клиент должен быть жив на протяжении всей жизни сервиса. Падение Redis = потеря всех WS-соединений.
Startup-очистка обязательна. clearConnections() вызывается при старте (не показано в service.js, но вызывается через startServer-цепочку). Без неё в MongoDB останутся «зависшие» isOnline: true и activeSocketSessions от предыдущего запуска.
pingInterval: 5000 без pingTimeout. Сервер отправляет ping каждые 5 сек. Клиент обязан ответить pong (Engine.IO v4 packet). Таймаут не задан явно — используется дефолт socket.io v4 (20 сек). Клиентский pong — это пакет Engine.IO уровня 3, а не socket.io-событие; сервер его обрабатывает автоматически, но в socketEventsHandler есть отдельный socket.io-событие 'pong' от клиента для обновления lastActivityTime.
Двойной pong. Engine.IO pong (автоматический, для keepalive) ≠ socket.io 'pong'-событие (ручное, для lastActivityTime). Это два разных механизма на одном соединении.
connectionId = JSON.stringify([socket.id, sid]). Составной ключ — используется в Employee для привязки сокета к сессии. Не путать с sessionId.
WorkspaceApi запрещён. authHandler отклоняет API-ключи воркспейсов — WS доступен только реальным пользователям (User/Employee).
transports: ['websocket'] — polling отключён. Клиент без поддержки WS не подключится.
io.of('/').adapter.on('error') — ошибки Redis-adapter логируются, но не приводят к краху сервиса. Потеря pub/sub = каждый инстанс работает изолированно (не видит чужих клиентов).
clearWsState при reconnect. При новом join сначала pull-ится старый socketId из activeSocketSessions и помечаются resync: true в sync_sessions. Это защита от «зомби»-сессий при неграциозном отключении.
sendOnlineStatus — почемуческая логика. Employee.setOnlineStatus находит сотрудников, у которых isOnline отличается от целевого, и обновляет. Возвращает список «изменившихся» — на них эмитится employeeOnlineStatusChanged. Это значит: если сотрудник уже онлайн (другая вкладка), повторный join не генерирует событие.
Goroutine-safety отсутствует. Node.js однопоточный, но await между проверкой и обновлением isOnline может привести к гонке при одновременном disconnect двух сокетов одного сотрудника. MongoDB updateMany атомарен на уровне документа, но find + updateMany — нет. На практике последний updateMany выиграет и состояние будет консистентным, но emit(employeeOnlineStatusChanged) может дублироваться.