Перейти к основному содержимому

Entity / Aggregate: CallSessionEntity — сигналинг и лог звонка (media отложен)

Дом полей агрегата «Сессия звонка» контекста messenger. Все проекции (карточка звонка/лог, payload доменных событий, hub-сообщения) ссылаются сюда и перечисляют только дельту.

Назначение

CallSessionEntity — контракт сигналинга аудио/видео звонка поверх контактов (мессенджер/ВКС) и журнал звонков (входящие/исходящие/пропущенные). Агрегат владеет только жизненным циклом и логом: StartCall → Ringing → Answered → Ended (плюс Declined/Cancelled/Missed/Failed). Полный разбор путей создания/ закрытия и подключения участников — ../calls/session-lifecycle.md.

Media-стек активен через внешний SFU (ADR-0062, feature flag CALLS_MEDIA_ENABLED; исторический CALLS_SIGNALING_ENABLED — стадия signaling-only, поглощён медиа-фазой). 1:1-звонок соответствует SessionKind = Direct (call-recording.md) и владеет медиа-комнатой (Room, owned VO MediaRoom). Медиа (звук/видео/демонстрация экрана) идёт клиент ↔ SFU; бэкенд не в медиа-плоскости — SDP/ICE и токены доступа не персистятся (INV-CALL-4/6), в БД только факты жизненного цикла. Вход в звонок раздаётся приглашением (комната + короткоживущий токен доступа) через SignalR-hub, а не relay-ом SDP. Демонстрация экрана в 1:1 — эфемерный сигнал, не персистится. Запись — по политике RecordingPolicy (call-recording.md). Звонок разрешён только между Accepted-контактами (contact.md) вне чёрного списка (blacklist.md) — сводный гейт CommunicationPolicy.CanCall.

Классификация

ПолеЗначение
Контекстmessenger
KindAggregateRoot
Source of truthmessenger
Таблицаmessenger.call_sessions
Tenant isolationНе применяется — мессенджер платформенный (звонок между пользователями); изоляция доступа — через CallerUserId/CalleeUserId + гейт контакта

Поля

ПолеТипОбязательностьОграничения (PK/FK/index/constraint)ИсточникОписание
IdGuidДаPKappИдентификатор сессии.
CallerUserIdGuidДаindex ix_call_caller (CallerUserId,StartedAt desc)из hub-контекстаИнициатор звонка. Логическая ссылка на пользователя (messenger-user.md; SoT — identity), без FK.
CalleeUserIdGuidДаindex ix_call_callee (CalleeUserId,StartedAt desc)appВызываемый пользователь (messenger-user.md), без FK.
ContactIdGuidДаFK fk_call_sessions_contact_id → messenger.contacts(id), indexappКонтакт-разрешение (пара Caller↔Callee, contact.md). Через него — проверка Accepted и чёрного списка.
MediaTypeCallMediaTypeДаenum (text), varchar(8)appАудио/видео.
RoomMediaRoom (owned VO)Дасм. Value objects; минтится при → Ringing, освобождается в терминале (INV-CALL-6)appИдентификатор внешней медиа-комнаты (SFU, ADR-0062).
StatusCallStatusДаenum (text), varchar(16); partial index ix_call_active (см. ниже)app / workerСм. enum и stateDiagram.
StartedAtDateTimeOffsetДачасть ix_call_caller/ix_call_callee (desc)appМомент StartCall (переход → Ringing).
AnsweredAtDateTimeOffset?НетappМомент Answer (→ Answered).
EndedAtDateTimeOffset?НетappМомент завершения (→ Ended) или фиксации Declined/Missed/Failed.
EndReasonCallEndReason?Нетenum (text), varchar(24)app / workerПричина завершения; обязателен при терминальном статусе кроме Missed.
DurationSecondsint?Нет>= 0 (check)domainВычисляется EndedAt − AnsweredAt при Answered → Ended; иначе null.
CreatedAtDateTimeOffsetДаappСоздание записи (= StartedAt при StartCall).
UpdatedAtDateTimeOffsetДаappПоследний переход.

Enum CallMediaType

ЗначениеОписание
AudioГолосовой звонок.
VideoВидеозвонок (ВКС).

Enum CallStatus

ЗначениеОписание
RingingИдёт вызов, ждём ответа callee (окно Missed, default 45с).
AnsweredCallee принял; активная медиа-сессия (клиент ↔ SFU).
DeclinedCallee отклонил вызов (decline-call). Терминальный.
CancelledCaller отменил вызов до ответа (hangup в Ringing, end-call инициатором). Терминальный; у callee — несостоявшийся входящий.
MissedНе ответили в окно (worker call-timeout-reaper). Терминальный.
EndedШтатное завершение принятого звонка (hangup любой стороной в Answered). Терминальный.
FailedОшибка сигналинга/сети до соединения. Терминальный.

Enum CallEndReason

ЗначениеОписание
HangupCallerЗавершил инициатор (в Answered).
HangupCalleeЗавершил вызываемый (в Answered).
DeclinedОтклонён callee (сопровождает Status = Declined).
CancelledByCallerCaller отменил до ответа (сопровождает Status = Cancelled).
TimeoutИстекло окно ответа (сопровождает Status = Missed).
ErrorСбой сигналинга (сопровождает Status = Failed).

Value objects

MediaRoom (owned)

Ссылка на внешнюю медиа-комнату SFU (ADR-0062). Не медиа и не PII: имя комнаты — маршрутный идентификатор для реконнекта и привязки записи к звонку; персистится. Токены доступа, SDP/ICE, TURN-креды — эфемерны, минтятся при входе, не хранятся (INV-CALL-6).

ПолеТипОбязательностьОграниченияКолонка
RoomNamestringДаvarchar(128); детерминирован из Id + nonce; уникален среди активных сессийroom_name
DomainstringДаvarchar(128)room_domain

Модели JSONB

JSONB-модели не используются. SDP/ICE и токены доступа не хранятся: медиа-негоциация идёт клиент ↔ SFU, токен минтится при входе и не попадает в БД (INV-CALL-4/6 — нет PII/медиа/секретов в persist-слое).

Связи

СвязьТипКлючПравило загрузкиDelete behavior
ContactEntityreference (разрешение связи)ContactIdв StartCall — проверка Accepted + чёрного списка (contact.md)restrict
Caller / Callee (messenger-user)replica (логическая)CallerUserId / CalleeUserIdбез FK; адресация hub-групп user:{id} (messenger-user.md)

Инварианты и переходы состояний

ИнвариантУсловиеГде проверяетсяОшибка
INV-CALL-1Звонок разрешён только при Accepted ContactEntity пары (contact.md) и ¬blacklist любой стороной (blacklist.md)CommunicationPolicy.CanCall при StartCallMSG_CALL_NOT_CONTACT / MSG_CALL_BLOCKED
INV-CALL-2Ringing без Answer дольше окна (default 45с) → Missedworker call-timeout-reaper, идемпотентно (только записи в статусе Ringing)
INV-CALL-3Переходы: Ringing → Answered/Declined/Cancelled/Missed/Failed; Answered → Ended (hangup любой стороной). Declined = callee отклонил (decline-call); Cancelled = caller отменил в Ringing (end-call инициатором); Ended = hangup в Answeredкомандные методы агрегатаMSG_CALL_INVALID_STATE
INV-CALL-4Медиа-негоциация (SDP/ICE/RTP) идёт клиент ↔ SFU; бэкенд её не релеит и не персистит — в БД нет медиа/PII звонкамедиа-плоскость (SFU), не БД
INV-CALL-5Не более одного активного (Ringing/Answered) звонка на пару (Caller,Callee) одновременноguard в StartCall (по индексу активных звонков ix_call_active)MSG_CALL_ALREADY_ACTIVE
INV-CALL-6Room.RoomName детерминирован и уникален среди активных сессий; минтится при → Ringing, освобождается в терминале. Токен доступа/ICE/TURN — эфемерны, минтятся при входе, не персистятся и не логируются. Демонстрация экрана в 1:1 — эфемерный сигнал, не персиститсяStartCall + сигналинг-слой

Гонки сигналинга. Встречный вызов (одновременно A→B и B→A): ix_call_active гейтит по направленной паре (Caller, Callee), поэтому две встречные сессии формально допустимы — UI разрешает glare, показывая обеим сторонам входящий; принявшая сторона отменяет свой исходящий (Cancelled). Ответ callee с двух устройств: первый Answer переводит сессию → Answered, повторный — MSG_CALL_INVALID_STATE (идемпотентно проигрывает). Reaper (INV-CALL-2) и штатное завершение конкурируют за терминал: первый перевод побеждает (INV-CALL-3), опоздавший переход — no-op на уже терминальной записи.

Доменные и интеграционные события

СобытиеТипКогдаSnapshot/поля
CallStartedEventдоменное[*] → Ringingpush CallInvite (комната + токен доступа минтится при доставке, без хранения) в hub-группу user:{CalleeUserId}.
CallAnsweredEventдоменное→ Answeredpush обеим сторонам (user:{Caller}, user:{Callee}).
CallDeclinedEventдоменное→ Declinedpush обеим сторонам (CallSignal(Declined)).
CallCancelledEventдоменное→ Cancelled (caller до ответа)push обеим сторонам (CallSignal(Cancelled)); у callee — как несостоявшийся входящий (звонок пропал).
CallMissedEventдоменное→ Missed (worker)push caller; системная запись «пропущенный звонок» — отложено (ниже).
CallEndedEventдоменное→ Endedpush обеим сторонам; DurationSeconds.

Доменные события агрегата инициируют hub-push через SignalR — eventName в snake_case, только non-PII (Id/статус/тип). Медиа-негоциация (SDP/ICE) идёт клиент ↔ SFU вне БД и вне hub (INV-CALL-4); вход в звонок раздаётся приглашением CallInvite (комната + токен минтится при доставке).

Интеграционные (Kafka) события 1:1-звонок не публикует (нет внешнего потребителя для person-to-person жизненного цикла — лог звонков и hub-события покрывают UI). Если для 1:1 включена запись, её готовность публикует агрегат записи (pin.messenger.call-recording-available.v1, call-recording.md), а не сессия звонка. Уведомление о пропущенном звонке (системная строка через message.md) — вне scope MVP.

Индексы, хранение, безопасность

АспектЗначение
ИндексыPK; ix_call_caller (CallerUserId,StartedAt desc); ix_call_callee (CalleeUserId,StartedAt desc) — журнал/лента; partial ix_call_active (Status) where status in ('Ringing','Answered') — выборка reaper-а (INV-CALL-2) и guard активного звонка (INV-CALL-5)
Партиционирование / retentionnone
Concurrencyappend-only лог; единственный активный звонок на пару — command-guard (INV-CALL-5); reaper идемпотентен (обновление только записей в статусе Ringing)
Permissions / PIIPermission messenger.calls.use (участник — только Caller/Callee). Медиа/SDP/ICE и токены доступа не персистятся (INV-CALL-4/6). Media-стек за feature flag CALLS_MEDIA_ENABLED (ADR-0062); запись — RECORDINGS_ENABLED + RecordingPolicy
AuditUpdatedAt + фазовые метки (StartedAt/AnsweredAt/EndedAt) — иммутабельные факты

Обратные ссылки

Автоссылки: где используется этот документ.

Связанные документы

  • contact.md — SoT контактов и Accepted-права звонка (гейт INV-CALL-1, INV-CT-5); blacklist.md — чёрный список (вторая половина CommunicationPolicy.CanCall).
  • messenger-user.md — replica пользователей (адресация Caller/Callee, hub-группы).
  • conference-session.md — групповой звонок (конференция); call-recording.md — запись (1:1 → SessionKind = Direct).
  • message.md — механизм системной записи «пропущенный звонок» (отложено, вне MVP).
  • SDK: 10-signalr-realtime.md — hub push.
  • Интеграция media-стека: ../calls/README.md; решение: ADR-0062.
  • Границы: service-boundaries.md (ADR-0055 — собственный мини-мессенджер, SignalR).