Socket — Підключення оператора

src/socket/index.ts · src/socket/online-users.service.ts

Описано лише сокет-рівень: підключення, відключення, перепідключення. Логіка OperatorRunner — окремо.


Ключові структури даних

userConnections: Map<userId, UserConnection>

Центральний стан усіх живих операторів. Ключ — chathouse.id оператора (family ID).

ПолеТипОпис
socketStackOperatorSocketПоточний активний сокет
runnerOperatorRunnerЗапущений runner оператора
timeoutNodeJS.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

Для оператора

  1. RMQ StackOperatorWithFamily — отримати stackOperator + chathouse family.
  2. Заповнити socket.user (IChathouseOperatorRunner) і socket.userOnline (OperatorOnline).
  3. checkUserCanConnect — перевірка isBlocked / isDeleted, запис lastConnectAt в БД (RMQ FamilyOperatorUpdate).
  4. Якщо перевірка провалилась → emit forceDisconnect + socket.disconnect().
  5. socket.join(userId) — оператор входить до власної кімнати (room = family ID).
  6. addOperatorToOnline(socket.userOnline) — додати до onlineUsers; якщо team lead онлайн — він отримає оновлений список onlineOperators.
  7. setSocketEventsOperator(socket) — прив’язати бізнес-події оператора.
  8. Emit connectionResult клієнту.
  9. → Перейти до логіки 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.tsEmit forceDisconnect + socket.disconnect()
addOperatorToOnlineonline-users.service.tsPush в onlineUsers, сповіщає team lead
removeOperatorFromOnlineonline-users.service.tsSplice з onlineUsers, сповіщає team lead
checkOnlineOperatorByStackIdonline-users.service.tsПовертає boolean — чи оператор онлайн (за stackId)

onlineUsers — список онлайн

src/socket/online-users.service.ts

Плаский масив у пам’яті процесу з усіма підключеними операторами і team lead-ами. Не персистується — при рестарті сервісу скидається в порожній стан.

Що зберігається

UserOnline — базовий запис (team lead-и та будь-який інший користувач):

ПолеОпис
stackIdІдентифікатор на рівні Stack
roleРоль (operator / supervisor)

OperatorOnline — розширює UserOnline, тільки для операторів:

ПолеОпис
familyIdChathouse family ID оператора (ключ у userConnections)
supervisorStackIdStack ID прив’язаного team lead-а
supervisorFamilyIdChathouse 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 приєднується до кімнати оператора.