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

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-а

ПолеЗначение
ProviderIn-App (внутренний, Talent.Common.Realtime)
Сервисnotifications
Capabilitymessaging (in-app realtime)
Adapter classInAppChannelProvider : INotificationChannelProvider
Typed clientIRealtimeProducer (SDK) → hub /hubs/notifications
Internal command/queryвызывается dispatcher-ом при Notification.Status = Pending, Channel = InApp
Реализацияреальная (в отличие от внешних каналов-заглушек)

Контракты Provider-а

Операция Provider-аDirectionInternal modelProvider-specific model
pushoutboundNotificationDispatchRequestRealtimeNotificationMessage (SignalR payload)
delivered/readcallbackMarkReadCommand (REST mark-read)— (нет вебхука вендора; read — из клиента через API)

Модель Internal NotificationDispatchRequest

Общая входная модель всех INotificationChannelProvider (дом полей — ../domain/notification.md).

ПолеТипОбязательностьОписаниеMapping
NotificationIdGuidДаЭкземпляр доставкиRealtimeNotificationMessage.Id
RecipientUserIdGuidДаАдрес in-app (RecipientAddress.InAppUserId)→ SignalR user-group user:{id}
KindNotificationKindДаВид (иконка/группировка в UI)Kind
RenderedTitlestring?НетЗаголовокTitle
RenderedBodystringДаТелоBody
DeepLinkstring?НетДиплинк карточкиDeepLink
CreatedAtDateTimeOffsetДаМеткаCreatedAt

Модель Provider-Specific RealtimeNotificationMessage

ПолеТипОбязательностьProvider formatValidationInternal mapping
IdGuidДаSignalR JSONNotificationId
KindstringДаenum-строказначение NotificationKindKind
Titlestring?Нет≤ 256RenderedTitle
BodystringДаtextне пустоеRenderedBody
DeepLinkstring?Нетuri≤ 1024DeepLink

Предусловия диспатча и статусная семантика

  • Когда адаптер вызывается. Для 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).

Безопасность

КонтрольПравило
AuthenticationSignalR-подключение — JWT identity (тот же токен, что REST); группа user:{RecipientUserId}
Signature validationне применяется (внутренний транспорт)
Secretsнет внешних секретов; backplane (Redis) — по инфраструктурным креденшелам SDK
PIIтело/заголовок доставляются только в личную группу получателя; в логах маскируются
Tenant bindingдоставка по RecipientUserId; кросс-пользовательская утечка исключена группой

Resilience И Устойчивость

АспектПравило
Timeoutpush — best-effort, неблокирующий (клиент оффлайн — не ошибка)
Retryне требуется: NotificationEntity персистнут; оффлайн-клиент получит при list-my-notifications/reconnect
Circuit breakerне применяется (внутренний hub)
Idempotencyпо NotificationId (клиент дедуплицирует по Id)
Rate limitbackpressure hub-а SDK

Обработка Callback

ШагПравило
Verifyread-квитанция приходит не вебхуком, а REST ../api/public/mark-notification-read.md (JWT владельца)
Normalize
ValidateRecipientUserId = actor (INV-NTF-6)
DispatchMarkNotificationReadCommandNotification.MarkRead (Sent → Read)
StoreReadAt на NotificationEntity

Маппинг Ошибок

Provider codeInternal errorПовторяемостьВидно пользователю
hub недоступен— (не ошибка доставки)Нет (персист есть)Нет
клиент оффлайнНетНет (догонит при reconnect)

Успех push → Notification.MarkSent (Sent); статус Read — только по REST mark-read.

Наблюдаемость

ТипПравило
Logspushed/recipient-offline/marked-read с NotificationId, RecipientUserId (тело маскируется)
Metricsnotifications_inapp_pushed_total, notifications_inapp_online_ratio, notifications_inapp_read_total
TracescorrelationId, NotificationId, hub-span
Redactionтело/заголовок в логах — маска

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

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

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