Realtime / Centrifugo
MineFlow доставляет live-уведомления и presence через Centrifugo (WebSocket, ADR-0054). Этот рецепт показывает, как поднять realtime в React: смонтировать RealtimeProvider, подписаться на уведомления и presence хуками, и что происходит, когда транспорт выключен.
Раньше realtime был на server-sent events (subscribeToSse, useNotificationFeed, /notifications/stream). С ADR-0054 этот слой удалён целиком — единственный live-транспорт теперь Centrifugo, а соответствующие экспорты @mineflow/client-core (subscribeToSse, parseSseData, SseSubscription) больше не существуют. Если встретишь их в коде/доках — это устаревшее.
Уведомления — это 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-0020 — organization_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), Ошибки.