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:
@@ -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>
|
||||
|
||||
@@ -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 в согласии с выбранным сервером/комнатой после удалений. */
|
||||
|
||||
@@ -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'));
|
||||
|
||||
Reference in New Issue
Block a user