fix(web): свежий звук сервера виден в настройках и при ошибке списка

`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.
This commit is contained in:
2026-09-26 15:06:26 +03:00
parent ad7b3d21bb
commit 4175b1436e
3 changed files with 43 additions and 6 deletions
@@ -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 ? (
<SkeletonLines className="mt-4" rows={3} />
) : list.isError ? (
) : failed ? (
<ErrorNotice className="mt-4" error={list.error} onRetry={() => void list.refetch()} />
) : current.length === 0 ? (
<p className="mt-4 text-sm text-fg-muted">{t('settings.servers.sounds.empty')}</p>
+9 -5
View File
@@ -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<void> {
await getQueryClient().invalidateQueries({ queryKey: guildEmojisQueryKey(guildId) });
}
/** Инвалидация списка звуков сервера после `GUILD_SOUNDS_UPDATE` (7.13). */
async function invalidateSoundQueries(guildId: string): Promise<void> {
/**
* Кладёт набор звуков из `GUILD_SOUNDS_UPDATE` в кэш REST-запроса: событие
* несёт список целиком, поэтому повторный запрос не нужен, а его отказ под
* нагрузкой (429) оставлял раздел настроек со старым списком (AGENT.md 7.13).
*/
async function applyGuildSounds(guildId: string, sounds: GuildSound[]): Promise<void> {
const { getQueryClient } = await import('@/lib/queryClient');
await getQueryClient().invalidateQueries({ queryKey: guildSoundsQueryKey(guildId) });
getQueryClient().setQueryData(guildSoundsQueryKey(guildId), sounds);
}
/** Держит URL в согласии с выбранным сервером/комнатой после удалений. */
+30
View File
@@ -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'));