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

Realtime / Centrifugo

MineFlow доставляет live-уведомления и presence через Centrifugo (WebSocket, ADR-0054). Этот рецепт показывает, как поднять realtime в React: смонтировать RealtimeProvider, подписаться на уведомления и presence хуками, и что происходит, когда транспорт выключен.

SSE удалён

Раньше realtime был на server-sent events (subscribeToSse, useNotificationFeed, /notifications/stream). С ADR-0054 этот слой удалён целиком — единственный live-транспорт теперь Centrifugo, а соответствующие экспорты @mineflow/client-core (subscribeToSse, parseSseData, SseSubscription) больше не существуют. Если встретишь их в коде/доках — это устаревшее.

Backlog + live

Уведомления — это REST-backlog (то, что накопилось, источник истины — ADR-0044) плюс live-доставка через Centrifugo (то, что приходит сейчас). Backlog читается useNotifications, live — useRealtimeNotifications. Пропущенное за разрыв соединения клиент добирает рефетчем backlog — он остаётся источником истины.

Как устроено подключение

Клиент не выбирает каналы и не держит секретов. RealtimeProvider дёргает GET /api/v1/realtime/connection (тем же аутентифицированным fetch, что и REST), а api после Keycloak-аутентификации возвращает:

{
"enabled": true,
"url": "wss://<realtime-host>/connection/websocket",
"token": "<connection-JWT>",
"subscriptions": [
{ "channel": "personal:#<uid>", "token": "<sub-JWT>" },
{ "channel": "role:<org>.<role>", "token": "<sub-JWT>" },
{ "channel": "presence:<org>", "token": "<sub-JWT>" }
]
}

Затем провайдер открывает одно WebSocket-соединение к Centrifugo, подписывается на выданные каналы и раздаёт публикации/presence через React-контекст. Токены короткоживущие — SDK сам их рефрешит (getToken), повторно дёргая тот же эндпоинт.

Tenant-изоляция и RBAC наследуются от REST-стека: targeting зашит в имена каналов сервером (ADR-0020organization_id в role:/presence:), клиент не фильтрует — чужие тенанты до него не доходят.

КаналСодержимое
personal:#<uid>уведомления, адресованные лично пользователю
role:<org>.<role>уведомления, адресованные ролям пользователя в его орг
presence:<org>presence (кто из коллег организации сейчас онлайн)

Установка провайдера

MineflowProvider не монтирует realtime автоматически — RealtimeProvider добавляется отдельно, внутри MineflowProvider (он тянет authFetch + baseUrl из контекста SDK). Обычно — один раз высоко в дереве:

import { MineflowProvider, RealtimeProvider } from '@mineflow/client-react';

<MineflowProvider
baseUrl={import.meta.env.VITE_API_BASE}
tokenProvider={tokenProvider}
generateId={() => crypto.randomUUID()}
roles={tokenProvider.getRoles()}
>
<RealtimeProvider>
<App />
</RealtimeProvider>
</MineflowProvider>;

Без RealtimeProvider live-хуки просто no-op'ят (см. ниже) — приложение работает на REST-backlog.

Быстрый старт (React)

useRealtimeNotifications — единственный хук, который нужен ленте: входящие из Centrifugo он префиксит прямо в кэш useNotifications (дедуп по id), поэтому список из useNotifications перерисовывается сам, без ручной синхронизации. Колбэк onNotification — для побочных эффектов (toast, звук, badge).

import { useNotifications, useRealtimeNotifications } from '@mineflow/client-react';
import type { Notification } from '@mineflow/client-react';

function NotificationsCenter() {
// backlog: REST, cursor-пагинация, новые сверху
const { data, isLoading } = useNotifications({ limit: 50 });

// live: новые префиксятся в кэш useNotifications (дедуп по id)
const { enabled } = useRealtimeNotifications({
onNotification: (n: Notification) => toast(`${n.severity}: ${n.title}`),
});

if (isLoading) return <Spinner />;
return (
<>
{!enabled && <Banner>Live-обновления недоступны — потяните для обновления</Banner>}
<ul>
{data?.items.map((n) => (
<li key={n.id}>{n.title}</li>
))}
</ul>
</>
);
}
Вызывай ленточный хук один раз

useRealtimeNotifications слушает общее соединение из RealtimeProvider. Подписку на кэш достаточно держать в одном месте (корень, layout). Само WebSocket-соединение всё равно одно на приложение — его держит провайдер, а не хук.

Низкоуровневый колбэк

Если запись в кэш не нужна (только счётчик/тост) — useOnNotification(cb). Колбэк хранится в ref, поэтому передавать можно inline без ре-подписки. No-op при выключенном Centrifugo.

import { useOnNotification } from '@mineflow/client-react';

useOnNotification((n) => bumpUnreadBadge(n));

Presence

usePresence() отдаёт { online, enabled } — сколько коллег организации сейчас онлайн в realtime (по presence:<org>-каналу). При выключенном транспорте — { online: 0, enabled: false }, индикатор стоит прятать.

import { usePresence } from '@mineflow/client-react';

function OnlineBadge() {
const { online, enabled } = usePresence();
if (!enabled) return null;
return <span>{online} онлайн</span>;
}

Когда Centrifugo выключен

При enabled=false (Centrifugo не сконфигурирован/выключен на бэке — CENTRIFUGO_ENABLED=0) все realtime-хуки no-op'ят: useRealtimeNotifications().enabled === false, useOnNotification не зовётся, usePresence(){ online: 0, enabled: false }. Это «не настроено», а не fallback-транспорт — лента продолжает жить на REST-backlog (useNotifications + pull-to-refresh), доставка уведомлений в БД при этом не теряется (durable, ADR-0044). Завязывай UI на возвращаемый enabled, чтобы прятать live-индикаторы, а не показывать пустоту.

Поле уведомления

Notification приходит и в backlog (useNotifications), и в live (onNotification). Тип ре-экспортится из @mineflow/client-react (источник — OpenAPI-схема Notification_Output):

import type { Notification } from '@mineflow/client-react';

// {
// id: string;
// eventType: string;
// severity: 'info' | 'warning' | 'critical';
// title: string;
// body: string;
// targetRoles: ...; // кому адресовано по ролям
// targetUserId: ...; // персональный адресат (если есть)
// resourceType: ...; // тип сущности-источника
// resourceId: ...; // id сущности (для deep-link)
// createdAt: string;
// }

Используй resourceType + resourceId для перехода к сущности-источнику, а severity — для визуального приоритета (critical → красный тост и т.п.).

React Native

В RN ничего особого не нужно: centrifuge использует нативный WebSocket, который RN/Hermes поддерживают из коробки. Потоковый fetchImpl (который раньше требовался для SSE) для realtime больше не нуженfetchImpl остаётся опциональным и влияет только на REST-транспорт. Те же хуки (useRealtimeNotifications / usePresence) работают в web и RN без изменений. Детали RN-стенда — рецепт React Native.

Куда дальше

  • Гайды пакетов: client-react (хуки + RealtimeProvider).
  • API-референс: /api/client-react/ (useRealtimeNotifications, useOnNotification, usePresence).
  • REST-эндпоинты уведомлений (/notifications, /realtime/connection): /rest/.
  • Смежные рецепты: Саги (другой async-паттерн — polling), Ошибки.