Socket — Підключення оператора
src/socket/index.ts · src/socket/online-users.service.ts
Описано лише сокет-рівень: підключення, відключення, перепідключення. Логіка
OperatorRunner— окремо.
Ключові структури даних
userConnections: Map<userId, UserConnection>
Центральний стан усіх живих операторів. Ключ — chathouse.id оператора (family ID).
| Поле | Тип | Опис |
|---|---|---|
socket | StackOperatorSocket | Поточний активний сокет |
runner | OperatorRunner | Запущений runner оператора |
timeout | NodeJS.Timeout | Таймер очищення після відключення (встановлюється при disconnect, скасовується при reconnect) |
onlineUsers: (OperatorOnline | UserOnline)[]
Плаский масив усіх онлайн-користувачів (оператори + team lead-и). Використовується для перевірки checkOnlineOperatorByStackId і розсилки onlineOperators team lead-у.
Flow 1 — Підключення (connection)
Client connects
→ socketAuthMiddlevare (JWT → socket.user: ITokenUser)
→ RMQ: StackUser (отримати повний профіль, ролі)
→ Визначити роль: operator або supervisor
Для оператора
- RMQ
StackOperatorWithFamily— отриматиstackOperator+chathousefamily. - Заповнити
socket.user(IChathouseOperatorRunner) іsocket.userOnline(OperatorOnline). checkUserCanConnect— перевіркаisBlocked/isDeleted, записlastConnectAtв БД (RMQFamilyOperatorUpdate).- Якщо перевірка провалилась → emit
forceDisconnect+socket.disconnect(). socket.join(userId)— оператор входить до власної кімнати (room = family ID).addOperatorToOnline(socket.userOnline)— додати доonlineUsers; якщо team lead онлайн — він отримає оновлений списокonlineOperators.setSocketEventsOperator(socket)— прив’язати бізнес-події оператора.- Emit
connectionResultклієнту. - → Перейти до логіки
userConnections(нове підключення або перепідключення).
Flow 2 — Нове підключення (перший раз / після timeout)
Умова: userConnections не має запису для цього userId.
Створюється новий OperatorRunner, зберігається в userConnections. Деталі запуску runner’а — у operator-runner.
Flow 3 — Перепідключення (reconnection)
Умова: userConnections вже має запис для userId (оператор перепідключився до завершення timeout або миттєво).
1. Скасувати timeout (clearTimeout) якщо він є
2. Відключити старий сокет (forceDisconnectSocket) якщо відрізняється від нового
3. userConnection.socket = новий socket
4. socket.runner = existingRunner (прив'язати runner до нового сокета)
Далі — обирається дія залежно від поточного стану OperatorRunner (RUNNING / STARTING / CREATED / STOPPING / STOPPED). Деталі — у operator-runner.
У всіх гілках перед дією перевіряється isOperatorSocketValid — якщо сокет вже замінено іншим підключенням, дія скасовується.
Flow 4 — Відключення (disconnect)
Тригер: socket.on('disconnect').
1. removeOperatorFromOnline(socket.userOnline)
→ оновлює onlineUsers
→ якщо team lead онлайн — отримає оновлений onlineOperators
2. Перевірка: чи є інші активні сокети оператора в кімнаті?
→ io.in(userId).fetchSockets()
→ якщо є — виходимо (skip disconnect logic)
3. Кікнути всіх team lead-ів з кімнати оператора:
→ emit connectToOperator { success: false }
→ socket.leave(userId)
4. RMQ FamilyOperatorUpdate: { lastDisconnectAt: new Date() }
5. Встановлюється таймер. Після його спрацювання — зупинка [[services/operator-runner|OperatorRunner]], видалення запису з `userConnections`.
Якщо оператор перепідключиться до спрацювання timeout → Flow 3 скасує його (clearTimeout).
Діаграма станів userConnections
stateDiagram-v2 [*] --> Empty : перший connect Empty --> Active : createRunner + setUserConnection Active --> Active : reconnect (Flow 3) Active --> TimerPending : disconnect → setTimeout TimerPending --> Active : новий connect (clearTimeout + reconnect) TimerPending --> [*] : timeout спрацював → runner.stop() + delete
Допоміжні функції
| Функція | Де | Що робить |
|---|---|---|
createRunner(socket) | socket/index.ts | Створює OperatorRunner, передає управління runner’у, повертає runner або null |
setUserConnection(id, s, r) | socket/index.ts | Записує / оновлює запис у userConnections |
isOperatorSocketValid(id, sid) | socket/index.ts | Перевіряє, що userConnections.get(id).socket.id === sid (захист від race) |
forceDisconnectSocket(s, msg) | socket/index.ts | Emit forceDisconnect + socket.disconnect() |
addOperatorToOnline | online-users.service.ts | Push в onlineUsers, сповіщає team lead |
removeOperatorFromOnline | online-users.service.ts | Splice з onlineUsers, сповіщає team lead |
checkOnlineOperatorByStackId | online-users.service.ts | Повертає boolean — чи оператор онлайн (за stackId) |
onlineUsers — список онлайн
src/socket/online-users.service.ts
Плаский масив у пам’яті процесу з усіма підключеними операторами і team lead-ами. Не персистується — при рестарті сервісу скидається в порожній стан.
Що зберігається
UserOnline — базовий запис (team lead-и та будь-який інший користувач):
| Поле | Опис |
|---|---|
stackId | Ідентифікатор на рівні Stack |
role | Роль (operator / supervisor) |
OperatorOnline — розширює UserOnline, тільки для операторів:
| Поле | Опис |
|---|---|
familyId | Chathouse family ID оператора (ключ у userConnections) |
supervisorStackId | Stack ID прив’язаного team lead-а |
supervisorFamilyId | Chathouse family ID прив’язаного team lead-а |
Логіка змін
| Подія | Що відбувається |
|---|---|
| Оператор підключився | Додається до списку. Якщо team lead цього оператора зараз онлайн — він отримає оновлений список своїх онлайн-операторів по сокету |
| Оператор відключився | Видаляється зі списку. Те саме сповіщення team lead-у |
| Team lead підключився | Додається до списку. Жодних сповіщень |
| Team lead відключився | Видаляється зі списку. Жодних сповіщень |
Сповіщення про список онлайн-операторів (onlineOperators) надсилається team lead-у тільки у відповідь на зміну стану одного з його операторів. При першому підключенні team lead-а список не надсилається автоматично — він отримає його коли перший оператор зміниться.
Team lead при підключенні до workspace відправляє запит на отримання списку онлайн операторів.
Нюанси
- Якщо у оператора немає прив’язаного team lead-а — сповіщення не надсилається.
- Під час перепідключення оператора (Flow 3) список коротко містить два записи одного оператора: новий сокет вже додано, а старий ще не видалено. Самокоригується, коли примусово відключений старий сокет тригерить видалення (по
stackId, незалежно від socket ID). - Перевірка «оператор онлайн» — за
stackId(ідентифікатор на рівні Stack), не за family ID. Використовується уconnectToOperatorперед тим як team lead приєднується до кімнати оператора.