Страница нашей команды в реальном времени показывает, чем именно каждый из нас сейчас занят в 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-ID | cdn.discordapp.com/app-assets/{appId}/{id} |
Сами URL картинок были в полном порядке. Оказалось, что нас просто поджаривала наша
Content-Security-Policy (CSP). Правила img-src не разрешали i.scdn.co, поэтому
браузер тихо блокировал CDN Spotify, ничего нам не сообщая. Мы добавили его в
allow-list, и обложки наконец-то загрузились повсюду.
Вывод
У интерфейсов реального времени две огромные ловушки: жизненный цикл компонентов (сокеты живут дольше самой страницы) и границы доверия (CSP молча выбрасывает сторонние ассеты).
Хуже всего то, что ни одна из них не выдаёт громкой ошибки в консоли. Ваш интерфейс просто превращается в призрака и показывает пустую карточку. Если сокеты ведут себя странно, точно проверяйте сначала эти две вещи!