All posts
engineeringfrontend

Показываем все наши активности Discord сразу: чиним живое присутствие

Страница нашей команды в реальном времени показывает, чем именно каждый из нас сейчас занят в Discord. Гриндим ли мы в VSCode, играем или залипаем под Spotify. Работает это через Lanyard по WebSocket-соединению. Но, честно говоря, три вещи были полностью сломаны, и чинить их оказалось куда интереснее, чем мы ожидали.

1. Показывалась только одна активность за раз

Discord позволяет держать несколько активностей одновременно, например открытый VSCode и параллельно Spotify. Раньше наш сайт просто брал первую попавшуюся и игнорировал остальные.

Чтобы это исправить, мы перестали искать "главную" активность. Вместо этого мы просто рендерим весь список. Spotify получил собственную строку (с обложкой альбома, названием и исполнителем), а все остальные игры и приложения мы складываем прямо под ней.

2. Всё ломалось напрочь, если уйти со страницы и вернуться

Вот это была забавная история. Наш клиент присутствия это синглтон с единственным WebSocket. Когда вы уходили со страницы /team и кликали обратно, React перемонтировал компонент и начисто сносил соединение сокета, чтобы открыть новое.

В чём подвох? Обработчик onclose старого сокета всё ещё срабатывал в фоне и обнулял новый сокет, который мы только что создали.

Вот как мы это починили: мы привязали каждый обработчик к конкретному экземпляру сокета, которому он принадлежит, и велели ему выходить, если это не текущий сокет.

ws.onclose = () => {
  if (ws !== this.ws) return; // устаревший сокет, игнорируем
  this.reconnect();
};

Ещё мы сделали так, чтобы при каждом новом подключении начальные данные брались с REST-эндпоинта, и страница не сидела пустой несколько секунд, пока шлюз делает свой хендшейк.

3. Обложка альбома Spotify наотрез отказывалась загружаться

Ассеты присутствия Discord приходят в трёх разных форматах URL, и нужно писать код, который определяет, какой из них какой:

Префикс ассетаКуда он на самом деле ведёт
mp:...media.discordapp.net/...
spotify:...i.scdn.co/image/...
сырой snowflake-IDcdn.discordapp.com/app-assets/{appId}/{id}

Сами URL картинок были в полном порядке. Оказалось, что нас просто поджаривала наша Content-Security-Policy (CSP). Правила img-src не разрешали i.scdn.co, поэтому браузер тихо блокировал CDN Spotify, ничего нам не сообщая. Мы добавили его в allow-list, и обложки наконец-то загрузились повсюду.

Вывод

У интерфейсов реального времени две огромные ловушки: жизненный цикл компонентов (сокеты живут дольше самой страницы) и границы доверия (CSP молча выбрасывает сторонние ассеты).

Хуже всего то, что ни одна из них не выдаёт громкой ошибки в консоли. Ваш интерфейс просто превращается в призрака и показывает пустую карточку. Если сокеты ведут себя странно, точно проверяйте сначала эти две вещи!