Realtime / SSE
MineFlow доставляет live-уведомления через server-sent events (SSE, ADR-0044). Этот рецепт показывает, как подписаться на поток в React, как устроено подключение под капотом (fetch-stream + Bearer вместо EventSource), как ведёт себя авто-reconnect и как сервер фильтрует фид по организации и роли.
Уведомления — это REST-backlog (то, что накопилось) плюс live SSE (то, что приходит сейчас). Backlog читается useNotifications, live-фид подключается useNotificationFeed. Пропущенное за разрыв соединения клиент добирает рефетчем backlog — сервер гарантирует at-most-once в потоке.
Почему не EventSource
Браузерный EventSource не умеет слать заголовок Authorization, а эндпоинт потока (GET /api/v1/notifications/stream) требует Bearer-токен. Поэтому SDK подключается обычным аутентифицированным fetch и читает text/event-stream из response.body (ReadableStream). Bearer добавляет тот же fetch-слой, что и для REST-запросов — отдельной настройки не нужно.
Это полностью инкапсулировано в subscribeToSse (L1) и хуке useNotificationFeed (L2). В обычном приложении ты работаешь только с хуком.
Быстрый старт (React)
Внутри MineflowProvider (см. гайд client-react) вызови useNotificationFeed один раз — обычно высоко в дереве, рядом с тостами:
import { useNotifications, useNotificationFeed } 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)
useNotificationFeed({
onNotification: (n: Notification) => toast(`${n.severity}: ${n.title}`),
});
if (isLoading) return <Spinner />;
return (
<ul>
{data?.items.map((n) => (
<li key={n.id}>{n.title}</li>
))}
</ul>
);
}
Ключевая деталь: useNotificationFeed сам мутирует кэш useNotifications — новое уведомление встаёт в начало items (с дедупом по id). Поэтому компонент, который рендерит список из useNotifications, перерисовывается автоматически, без ручной синхронизации. Колбэк onNotification нужен лишь для побочных эффектов (toast, звук, badge).
useNotificationFeed открывает SSE-соединение на время жизни компонента. Монтируй его в одном месте (корень, layout), а не в каждом экране — иначе получишь несколько параллельных потоков.
Поле уведомления
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 → красный тост и т.п.).
Серверная фильтрация по орг и роли
Фид уже отфильтрован сервером: клиент видит только уведомления своей организации (ADR-0020, multi-tenancy через organization_id) и только адресованные его ролям/пользователю. Поля targetRoles / targetUserId отражают эту адресацию — фильтровать поток на клиенте по организации/роли не нужно и нельзя (д анные чужих тенантов до клиента не доходят).
Транспорт (fetch + REST + SSE) в MineflowProvider строится один раз и НЕ зависит от roles. Адресация считается сервером по токену, поэтому смена набора ролей в UI-контексте не пересоздаёт SSE-соединение.
Reconnect: backoff + jitter
При разрыве соединение восстанавливается автоматически. Задержка растёт экспоненциально с числом подряд идущих неудач и размывается jitter'ом — это разносит во времени переподключения клиентов, кото рых разорвало одним событием (рестарт API на сотнях клиентов), вместо синхронного залпа (thundering-herd).
Параметры (дефолты subscribeToSse):
| Опция | Дефолт | Назначение |
|---|---|---|
reconnectDelayMs | 1000 | базовая задержка; 0 — полностью отключить reconnect |
maxReconnectDelayMs | 30000 | потолок задержки |
backoffFactor | 2 | множитель экспоненциального роста |
Счётчик неудач сбрасывается после успешного подключения — здоровый разрыв реконнектится быстро. Через хук настраивается база backoff:
useNotificationFeed({
reconnectDelayMs: 1000, // база; реальная задержка = exp(attempt) с jitter, потолок 30s
onNotification: (n) => toast(n.title),
});
Формула задержки — «equal jitter»: половина детерминированная, половина случайная. Это даёт и нижнюю границу (не штормить мгновенно), и размывание (не подключаться синхронно). Подробности реализации — reconnectDelay в client-core.
Низкий уровень: subscribeToSse
Для не-React кода (скрипты, воркеры, кастомные интеграции) подписывайся напрямую через subscribeToSse. Нужен аутентифицированный fetch (с Bearer) — его строит провайдер; в обход React можно собрать его через makeAuthenticatedFetch (см. гайд client-core).
import { subscribeToSse } from '@mineflow/client-core';
import type { SseSubscription } from '@mineflow/client-core';
const sub: SseSubscription = subscribeToSse(
`${baseUrl}/api/v1/notifications/stream`,
{
onMessage: (n) => console.log('notification', n),
onOpen: () => console.log('SSE открыт'),
onError: (e) => console.warn('SSE ошибка (будет reconnect)', e),
},
{
fetchImpl: authenticatedFetch, // fetch с Bearer — EventSource не умеет Authorization
reconnectDelayMs: 1000,
maxReconnectDelayMs: 30_000,
backoffFactor: 2,
},
);
// при завершении — обязательно закрыть (abort'ит fetch и останавливает reconnect)
sub.close();
subscribeToSse сам парсит SSE-фреймы (parseSseData), декодирует JSON и игнорирует не-JSON heartbeat/comment-строки. Обработчик onError вызывается на каждой неудаче перед очередной попыткой reconnect — это не терминальная ошибка, а сигнал «соединение упало, переподключаюсь».
subscribeToSse возвращает SseSubscription с методом close(). Без вызова close() поток продолжит реконнектиться в фоне. В React useNotificationFeed делает это за тебя (в cleanup эффекта), но в ручном коде закрытие — на тебе.
React Native
Дефолтный fetch в RN не отдаёт ReadableStream в response.body, поэтому SSE из коробки не заработает. Передай в SDK fetchImpl с потоковой поддержкой (например, react-native-fetch-api) — тогда subscribeToSse / useNotificationFeed будут читать поток так же, как в web. Детали настройки RN-транспорта — в рецепте React Native.
Куда дальше
- Гайды пакетов: client-react (хуки), client-core (
subscribeToSse, транспорт). - API-референс: /api/client-react/, /api/client-core/.
- REST-эндпоинты уведомлений (
/notifications,/notifications/stream): /rest/. - Смежные рецепты: Саги (другой async-паттерн — polling, не SSE), Ошибки.