Provider Adapter: In-App (Talent.Common.Realtime) Messaging
Назначение
InAppChannelProvider : INotificationChannelProvider — реальная доставка канала InApp
(../domain/channel.md): уведомление уже персистнуто как NotificationEntity
(../domain/notification.md) в PostgreSQL (source of truth «колокольчика»),
адаптер push-ит realtime-сигнал подключённому клиенту через Talent.Common.Realtime.*
(IRealtimeProducer → SignalR-hub /hubs/notifications). Это единственный провайдер без внешнего вендора —
anti-corruption boundary минимальна (внутренняя инфраструктура), но контракт INotificationChannelProvider
общий с остальными каналами. Общие требования — Общие Требования К Документам.
Границы Adapter-а
| Поле | Значение |
|---|
| Provider | In-App (внутренний, Talent.Common.Realtime) |
| Сервис | notifications |
| Capability | messaging (in-app realtime) |
| Adapter class | InAppChannelProvider : INotificationChannelProvider |
| Typed client | IRealtimeProducer (SDK) → hub /hubs/notifications |
| Internal command/query | вызывается dispatcher-ом при Notification.Status = Pending, Channel = InApp |
| Реализация | реальная (в отличие от внешних каналов-заглушек) |
Контракты Provider-а
| Операция Provider-а | Direction | Internal model | Provider-specific model |
|---|
push | outbound | NotificationDispatchRequest | RealtimeNotificationMessage (SignalR payload) |
| delivered/read | callback | MarkReadCommand (REST mark-read) | — (нет вебхука вендора; read — из клиента через API) |
Модель Internal NotificationDispatchRequest
Общая входная модель всех INotificationChannelProvider (дом полей — ../domain/notification.md).
| Поле | Тип | Обязательность | Описание | Mapping |
|---|
NotificationId | Guid | Да | Экземпляр доставки | → RealtimeNotificationMessage.Id |
RecipientUserId | Guid | Да | Адрес in-app (RecipientAddress.InAppUserId) | → SignalR user-group user:{id} |
Kind | NotificationKind | Да | Вид (иконка/группировка в UI) | → Kind |
RenderedTitle | string? | Нет | Заголовок | → Title |
RenderedBody | string | Да | Тело | → Body |
DeepLink | string? | Нет | Диплинк карточки | → DeepLink |
CreatedAt | DateTimeOffset | Да | Метка | → CreatedAt |
Модель Provider-Specific RealtimeNotificationMessage
| Поле | Тип | Обязательность | Provider format | Validation | Internal mapping |
|---|
Id | Guid | Да | SignalR JSON | — | NotificationId |
Kind | string | Да | enum-строка | значение NotificationKind | Kind |
Title | string? | Нет | ≤ 256 | — | RenderedTitle |
Body | string | Да | text | не пустое | RenderedBody |
DeepLink | string? | Нет | uri | ≤ 1024 | DeepLink |
Предусловия диспатча и статусная семантика
- Когда адаптер вызывается. Для
NotificationEntity с Channel = InApp, Status = Pending; канал InApp
всегда включён (IsEnabled = true) — реальная доставка.
Sent = доступно в «колокольчике». Переход Pending → Sent фиксируется на диспатче как факт
доступности уведомления в inbox (source of truth — БД notifications), а не как подтверждение доставки на
устройство. Realtime-push через SignalR — best-effort наложение: оффлайн-клиент на статус не влияет
(Sent остаётся), «hub недоступен» — не ошибка доставки.
- Не-interruptive канал.
InApp не interruptive (../domain/channel.md) → тихие часы
к нему не применяются, ScheduledAt для in-app не выставляется — доставляется немедленно независимо от
Urgency.
- Прочтение.
Sent → Read — только по REST mark-read владельцем (RecipientUserId = actor, INV-NTF-6);
push прочтение не выставляет.
- Оффлайн-догоняние (eventual). Клиент оффлайн/переподключился → получает уведомление при
list-my-notifications/reconnect; дедуп на клиенте по Id (повторный push того же Id игнорируется).
- Счётчик непрочитанных/бейдж. Двигается доменными событиями
NotificationCreated (создание) и
NotificationStatusChanged (Read) — синхронно с обработкой (../domain/notification.md).
Безопасность
| Контроль | Правило |
|---|
| Authentication | SignalR-подключение — JWT identity (тот же токен, что REST); группа user:{RecipientUserId} |
| Signature validation | не применяется (внутренний транспорт) |
| Secrets | нет внешних секретов; backplane (Redis) — по инфраструктурным креденшелам SDK |
| PII | тело/заголовок доставляются только в личную группу получателя; в логах маскируются |
| Tenant binding | доставка по RecipientUserId; кросс-пользовательская утечка исключена группой |
Resilience И Устойчивость
| Аспект | Правило |
|---|
| Timeout | push — best-effort, неблокирующий (клиент оффлайн — не ошибка) |
| Retry | не требуется: NotificationEntity персистнут; оффлайн-клиент получит при list-my-notifications/reconnect |
| Circuit breaker | не применяется (внутренний hub) |
| Idempotency | по NotificationId (клиент дедуплицирует по Id) |
| Rate limit | backpressure hub-а SDK |
Обработка Callback
| Шаг | Правило |
|---|
| Verify | read-квитанция приходит не вебхуком, а REST ../api/public/mark-notification-read.md (JWT владельца) |
| Normalize | — |
| Validate | RecipientUserId = actor (INV-NTF-6) |
| Dispatch | MarkNotificationReadCommand → Notification.MarkRead (Sent → Read) |
| Store | ReadAt на NotificationEntity |
Маппинг Ошибок
| Provider code | Internal error | Повторяемость | Видно пользователю |
|---|
| hub недоступен | — (не ошибка доставки) | Нет (персист есть) | Нет |
| клиент оффлайн | — | Нет | Нет (догонит при reconnect) |
Успех push → Notification.MarkSent (Sent); статус Read — только по REST mark-read.
Наблюдаемость
| Тип | Правило |
|---|
| Logs | pushed/recipient-offline/marked-read с NotificationId, RecipientUserId (тело маскируется) |
| Metrics | notifications_inapp_pushed_total, notifications_inapp_online_ratio, notifications_inapp_read_total |
| Traces | correlationId, NotificationId, hub-span |
| Redaction | тело/заголовок в логах — маска |
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы