From 4175b1436e192cc1d9a6a47a8c915ba31c033193 Mon Sep 17 00:00:00 2001 From: grendervill Date: Sat, 26 Sep 2026 15:06:26 +0300 Subject: [PATCH] =?UTF-8?q?fix(web):=20=D1=81=D0=B2=D0=B5=D0=B6=D0=B8?= =?UTF-8?q?=D0=B9=20=D0=B7=D0=B2=D1=83=D0=BA=20=D1=81=D0=B5=D1=80=D0=B2?= =?UTF-8?q?=D0=B5=D1=80=D0=B0=20=D0=B2=D0=B8=D0=B4=D0=B5=D0=BD=20=D0=B2=20?= =?UTF-8?q?=D0=BD=D0=B0=D1=81=D1=82=D1=80=D0=BE=D0=B9=D0=BA=D0=B0=D1=85=20?= =?UTF-8?q?=D0=B8=20=D0=BF=D1=80=D0=B8=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA?= =?UTF-8?q?=D0=B5=20=D1=81=D0=BF=D0=B8=D1=81=D0=BA=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `GUILD_SOUNDS_UPDATE` приносит набор звуков целиком, но раздел «Звуки» читает REST-кэш: клиент инвалидировал запрос вместо того, чтобы положить в кэш сам payload события. Под нагрузкой повторный `GET /guilds/{id}/sounds` попадает под лимит частоты (429), а запрос списка идёт с `retry: 0` — список оставался старым, и новый звук не появлялся в разделе. Так падал пункт 5 матрицы 11.6 в полной прогонке (build/e2e-all-run8.log, run9): в логах стенда видно `POST .../sounds 200`, следом `GET .../sounds 200` (первый звук) и `GET .../sounds 429` (второй) — без единой повторной попытки. Теперь событие пишет список в кэш запроса, а ошибка перезапроса не прячет уже полученный список: раздел показывает актуальный набор из снапшота. Тест: web/tests/soundSettings.test.tsx — звук из `GUILD_SOUNDS_UPDATE` появляется, когда список по REST отвечает 429. --- web/src/pages/settings/GuildSoundsSection.tsx | 5 +++- web/src/stores/gateway.ts | 14 +++++---- web/tests/soundSettings.test.tsx | 30 +++++++++++++++++++ 3 files changed, 43 insertions(+), 6 deletions(-) diff --git a/web/src/pages/settings/GuildSoundsSection.tsx b/web/src/pages/settings/GuildSoundsSection.tsx index bd8ba7b..4e719ec 100644 --- a/web/src/pages/settings/GuildSoundsSection.tsx +++ b/web/src/pages/settings/GuildSoundsSection.tsx @@ -73,6 +73,9 @@ export function GuildSoundsSection({ guildId }: GuildSoundsSectionProps) { }); const sounds = list.data ?? []; + // Ошибка перезапроса не должна прятать уже полученный список: под нагрузкой + // ручка отвечает 429, а снапшот события шлюза при этом свежий (7.13). + const failed = list.isError && list.data === undefined; const soundboard = sounds.filter((sound) => sound.kind === 'soundboard'); const palette = sounds.filter((sound) => sound.kind === 'ui'); const current = kind === 'soundboard' ? soundboard : palette; @@ -269,7 +272,7 @@ export function GuildSoundsSection({ guildId }: GuildSoundsSectionProps) { {list.isPending ? ( - ) : list.isError ? ( + ) : failed ? ( void list.refetch()} /> ) : current.length === 0 ? (

{t('settings.servers.sounds.empty')}

diff --git a/web/src/stores/gateway.ts b/web/src/stores/gateway.ts index 0abd83d..f403c5a 100644 --- a/web/src/stores/gateway.ts +++ b/web/src/stores/gateway.ts @@ -30,7 +30,7 @@ import { parseTypingStart, parseUserUpdate, } from '@/api/gatewayEvents'; -import { guildSoundsQueryKey } from '@/api/sounds'; +import { guildSoundsQueryKey, type GuildSound } from '@/api/sounds'; import { useFriendsStore } from '@/stores/friends'; import { useMessagesStore } from '@/stores/messages'; import { useSessionStore, type SessionGuild } from '@/stores/session'; @@ -338,7 +338,7 @@ export function dispatchGatewayEvent(event: GatewayDispatch): void { const update = parseGuildSoundsUpdate(event.d); if (update !== null) { session.setGuildSounds(update.guild_id, update.sounds); - void invalidateSoundQueries(update.guild_id); + void applyGuildSounds(update.guild_id, update.sounds); } return; } @@ -567,10 +567,14 @@ async function invalidateEmojiQueries(guildId: string): Promise { await getQueryClient().invalidateQueries({ queryKey: guildEmojisQueryKey(guildId) }); } -/** Инвалидация списка звуков сервера после `GUILD_SOUNDS_UPDATE` (7.13). */ -async function invalidateSoundQueries(guildId: string): Promise { +/** + * Кладёт набор звуков из `GUILD_SOUNDS_UPDATE` в кэш REST-запроса: событие + * несёт список целиком, поэтому повторный запрос не нужен, а его отказ под + * нагрузкой (429) оставлял раздел настроек со старым списком (AGENT.md 7.13). + */ +async function applyGuildSounds(guildId: string, sounds: GuildSound[]): Promise { const { getQueryClient } = await import('@/lib/queryClient'); - await getQueryClient().invalidateQueries({ queryKey: guildSoundsQueryKey(guildId) }); + getQueryClient().setQueryData(guildSoundsQueryKey(guildId), sounds); } /** Держит URL в согласии с выбранным сервером/комнатой после удалений. */ diff --git a/web/tests/soundSettings.test.tsx b/web/tests/soundSettings.test.tsx index 4544643..2e76270 100644 --- a/web/tests/soundSettings.test.tsx +++ b/web/tests/soundSettings.test.tsx @@ -3,8 +3,11 @@ import { fireEvent, screen, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { + apiError, installFetch, + installGatewaySocket, json, + makeReadySnapshot, makeUser, renderApp, requestUrl, @@ -157,6 +160,33 @@ describe('настройки сервера: секция «Звуки»', () => expect(within(dialog).queryByTestId('guild-sounds-counter')).toBeNull(); }); + it('показывает звук из GUILD_SOUNDS_UPDATE, даже если список по REST не отдался', async () => { + // Под нагрузкой ручка списка отвечает 429 (AGENT.md 8.6), а раздел должен + // обновляться снапшотом из события шлюза, а не повтором запроса (7.13). + const gateway = installGatewaySocket( + makeReadySnapshot([{ id: 'g-1', name: 'Сервер', my_permissions: ['MANAGE_SOUNDS'] }]), + ); + installFetch( + routes({ + extra: [ + { + match: '/api/v1/guilds/g-1/sounds', + response: () => apiError('ratelimit.hit', 429), + }, + ], + }), + ); + renderApp('/settings/servers/g-1'); + const dialog = await screen.findByRole('dialog', { name: 'Настройки' }); + await gateway.greet(); + + const fresh: GuildSound = { id: 's-3', name: 'fresh', file_id: '920', kind: 'soundboard' }; + gateway.dispatch('GUILD_SOUNDS_UPDATE', { guild_id: 'g-1', sounds: [airhorn, fresh] }); + + expect(await within(dialog).findByTestId('guild-sound-s-1')).toHaveTextContent('airhorn'); + expect(within(dialog).getByTestId('guild-sound-s-3')).toHaveTextContent('fresh'); + }); + it('загружает звук палитры: kind в URL, событие и имя в multipart', async () => { const { dialog, fetchMock } = await openSoundsSection(); await userEvent.click(within(dialog).getByTestId('guild-sounds-tab-ui'));