Skip to main content

Live Feed

1. Summary

Goal: Система real-time ленты событий для отображения активности всех пользователей платформы. Использует Server-Sent Events (SSE) для push-уведомлений о новых событиях.

User Value: Социальное взаимодействие через наблюдение за успехами других игроков. FOMO-эффект: видя, как другие получают редкие предметы, пользователь мотивирован быть активнее.

Live Feed UI


2. Business Logic

Event Types

Триггер: Пользователь открыл кейс

Фильтрация: Только награды TIER_1+ или Scrap >= 500 или XP >= 1000

Broadcast: С задержкой 9.5 секунд (синхронизация с анимацией барабана)

Core Mechanics

1. Фильтрация событий

Не все события попадают в ленту — показываем только значимые:

Тип наградыПорог
ITEMTIER_1 и выше
SCRAP>= 500
XP>= 1000
FOMO-эффект

Пороги выбраны для создания "вау-эффекта". Мелкие награды (ресурсы, 10 скрапа) не попадают в ленту, чтобы не размывать восприятие редких дропов.

2. Анонимизация

Для защиты приватности username частично скрывается:

@alexandr → @ale***
@jo → @j****
@max → @ma***

3. Broadcast с задержкой

Для событий CASE_OPENED используется задержка 9.5 секунд — это соответствует длительности анимации барабана на фронтенде. Событие появится в ленте других игроков только после завершения анимации у автора.

Protection

ДействиеRate LimitAuthValidation
Get feed-noneGetFeedQuerySchema
Live stream-none-
Публичная лента

Feed endpoints не требуют авторизации — лента доступна всем. Это осознанное решение для привлечения новых пользователей (показать активность до регистрации).

Edge Cases

Что видит пользователь (UI):

СитуацияUI поведение
📴 SSE disconnectКлиент автоматически переподключается
🔄 Initial loadЗагружается 10 последних событий
🎰 Анимация кейсаСобытие появится через 9.5 сек после открытия
Backend Error Codes (для API/тестов)
КодHTTPОписание
-200SSE stream (text/event-stream)
-200JSON response для GET /feed

Feed не возвращает ошибок — это read-only публичный endpoint.


3. ADR (Architectural Decisions)

Почему SSE, а не WebSockets?

Проблема: Нужны real-time обновления ленты.

Решение: Server-Sent Events (SSE) — односторонний поток от сервера к клиенту.

Альтернативы (отклонены):

  • WebSockets — избыточно для одностороннего потока, сложнее инфраструктурно
  • Long Polling — неэффективно, создаёт нагрузку на сервер

Последствия:

  • Простая реализация через нативный HTTP
  • Автоматическое переподключение браузером
  • Heartbeat каждые 30 секунд для поддержания соединения

Почему фильтрация по порогам?

Проблема: При высокой активности лента заспамится мелкими событиями (10 Scrap, ресурсы).

Решение: Показываем только значимые события: TIER_1+ предметы, крупные суммы Scrap/XP.

Альтернативы (отклонены):

  • Показывать всё — лента превратится в мусор
  • Клиентская фильтрация — лишний трафик, нагрузка на клиента

Последствия:

  • Чистая лента с "вау-моментами"
  • FOMO-эффект усиливается (видны только редкие события)
  • Пороги настраиваются в feed.constants.ts

Почему EventEmitter, а не Redis Pub/Sub?

Проблема: Нужен механизм broadcast новых событий всем SSE-подписчикам.

Решение: Node.js EventEmitter — все подписчики в одном процессе.

Альтернативы (отклонены):

  • Redis Pub/Sub — нужно для multi-node, но у нас single instance
  • PostgreSQL NOTIFY — излишне для in-memory broadcast

Последствия:

  • Работает только при single-node deployment
  • При масштабировании потребуется Redis Pub/Sub

Почему создание событий внутри транзакции?

Проблема: FeedEvent должен создаваться только если основное действие успешно (открытие кейса, крафт).

Решение: feedService.createEvent(tx, data) вызывается внутри Prisma $transaction callback.

Принципы интеграции:

  1. Внутри транзакции — гарантия атомарности
  2. После основной логики — сначала действие, потом событие
  3. С проверкой фильтра — для CASE_OPENED/SPIN_WON проверяем shouldCreateForReward

Последствия:

  • При rollback транзакции событие не создаётся
  • Broadcast происходит после commit (через EventEmitter)
  • Консистентность данных гарантирована

Почему гибридный подход (специализированные таблицы + FeedEvent)?

Проблема: Нужны и детальная аналитика по типам событий, и быстрый Live Feed.

Решение: Две системы таблиц:

  1. Специализированные (CaseOpening, SpinResult, CraftHistory, Withdrawal) — детальные данные для аналитики
  2. Универсальная (FeedEvent) — денормализованная лента для быстрого чтения

Альтернативы (отклонены):

  • Только специализированные таблицы — JOIN 4+ таблиц для ленты, медленно
  • Только FeedEvent — потеря детализации для аналитики

Последствия:

  • Live Feed: 1 запрос, быстрая пагинация
  • Аналитика: детальные данные с типизацией
  • Дублирование данных, но атомарность через $transaction

4. Architecture

Services Overview

Key Components

КомпонентПутьОписание
FeedServicebackend/src/domains/feed/services/feed.service.tsСоздание и получение событий
FeedControllerbackend/src/domains/feed/controllers/feed.controller.tsHTTP + SSE handlers
FeedRoutesbackend/src/domains/feed/routes/feed.routes.tsРегистрация endpoints
feedEventEmitterbackend/src/domains/feed/services/feed.service.tsEventEmitter для broadcast
filter utilsbackend/src/domains/feed/utils/feed-filter.utils.tsЛогика фильтрации по порогам
anonymize utilsbackend/src/domains/feed/utils/anonymize.utils.tsАнонимизация username
constantsbackend/src/domains/feed/constants/feed.constants.tsПороги и настройки

5. Database Schema

Models

МодельОписаниеКлючевые поля
FeedEventСобытие в лентеuserId, eventType, rewardId, itemId, caseId, spinId, achievementId, referralId, amount

Relationships

Constants

КонстантаЗначениеОписание
MIN_ITEM_TIERTIER_1Минимальный tier для показа
MIN_SCRAP_AMOUNT500Минимальный Scrap для показа
MIN_XP_AMOUNT1000Минимальный XP для показа
FEED_MAX_EVENTS10Начальная загрузка
FEED_THROTTLE_MS2000Интервал между событиями
FEED_RETENTION_DAYS30Хранение в БД
FEED_BROADCAST_DELAY_MS9500Задержка для CASE_OPENED

6. API Endpoints

МетодЭндпоинтОписаниеDocs
GET/api/feedСобытия с пагинацией
GET/api/feed/liveSSE real-time поток

Query Parameters

ПараметрТипDefaultОписание
limitnumber10Количество событий (1-50)
cursorstring-Cursor для пагинации

SSE Message Format

// Initial load
{ type: "init", events: FeedEvent[] }

// New event
{ type: "new", event: FeedEvent }
Пример ответа FeedEvent
{
"id": "clxxx...",
"userId": "clyyy...",
"eventType": "CASE_OPENED",
"amount": null,
"createdAt": "2024-01-01T12:00:00Z",
"user": {
"id": "clyyy...",
"displayName": "@ale***"
},
"reward": {
"id": "clzzz...",
"type": "ITEM",
"amount": null,
"item": {
"id": "claaa...",
"name": "Dragon Rocket Launcher",
"imageUrl": "https://...",
"tier": "TIER_4",
"itemType": "SKIN"
}
},
"item": null,
"case": {
"id": "clbbb...",
"name": "Daily Case",
"imageUrl": "https://..."
},
"achievement": null
}

  • Cases — источник CASE_OPENED событий
  • Daily Spins — источник SPIN_WON событий
  • Craft — источник ITEM_CRAFTED событий
  • Withdrawals — источник ITEM_WITHDRAWN событий
  • Achievements — источник ACHIEVEMENT_UNLOCKED событий
  • Referrals — источник REFERRAL_BONUS событий
  • Raffle — источник RAFFLE_WIN событий