fix(realtime): объединение частых синхронизаций и лог неожиданных ошибок

- клиент откладывает перечитывание комнат (окно 250 мс) и не запускает второй
  запрос, пока идёт первый: серия событий ролей и участников больше не даёт
  лавину GET members/roles и 429 на лимите API;
- неожиданные ошибки API пишутся в лог (и в chi-, и в huma-ветке): под
  параллельной нагрузкой на стенде видели разовые 500 на GET members/roles,
  но без текста ошибки разобраться было нельзя.
This commit is contained in:
2026-09-22 20:58:49 +03:00
parent 63466fb75b
commit e4bbc27a7c
4 changed files with 48 additions and 4 deletions
+4 -4
View File
@@ -211,7 +211,7 @@ export function dispatchGatewayEvent(event: GatewayDispatch): void {
// Права на комнату персональные: тело события их не несёт, поэтому
// перечитываем список — иначе новая или переименованная комната не
// появится в сайдбаре без перезагрузки (AGENT.md 11.6).
void session.reloadGuildChannels(guildId);
session.scheduleGuildChannelsReload(guildId);
}
return;
}
@@ -291,7 +291,7 @@ export function dispatchGatewayEvent(event: GatewayDispatch): void {
const guildId = parseGuildId(event.d);
if (guildId !== null) {
void invalidateMembershipQueries(guildId);
void session.reloadGuildChannels(guildId);
session.scheduleGuildChannelsReload(guildId);
}
return;
}
@@ -301,7 +301,7 @@ export function dispatchGatewayEvent(event: GatewayDispatch): void {
const role = parseRoleEvent(event.d);
if (role !== null) {
void invalidateMembershipQueries(role.guild_id);
void session.reloadGuildChannels(role.guild_id);
session.scheduleGuildChannelsReload(role.guild_id);
}
return;
}
@@ -366,7 +366,7 @@ export function dispatchGatewayEvent(event: GatewayDispatch): void {
// участников, поэтому список комнат перечитываем целиком (AGENT.md 11.6).
const sync = parseGuildChannelsSyncEvent(event.d);
if (sync !== null) {
void session.reloadGuildChannels(sync.guild_id);
session.scheduleGuildChannelsReload(sync.guild_id);
}
return;
}
+35
View File
@@ -116,6 +116,8 @@ interface SessionState {
removeChannel: (guildId: string, channelId: string) => void;
/** GUILD_CHANNELS_SYNC: перечитать видимые комнаты сервера (права изменились). */
reloadGuildChannels: (guildId: string) => Promise<void>;
/** То же, но с объединением частых вызовов в один запрос. */
scheduleGuildChannelsReload: (guildId: string) => void;
/** Первая доступная (видимая) комната сервера — для выбора после удалений. */
firstChannelId: (guildId: string) => string | null;
selectGuild: (guildId: string | null) => void;
@@ -195,6 +197,35 @@ function toSessionGuild(guild: GatewayGuild): SessionGuild {
return summary;
}
/** Таймеры отложенной синхронизации комнат по серверам. */
const pendingChannelSync = new Map<string, ReturnType<typeof setTimeout>>();
/** Серверы, для которых синхронизация уже идёт: второй запрос не запускаем. */
const channelSyncInFlight = new Set<string>();
/**
* scheduleChannelSync откладывает перечитывание комнат: серия событий (правка
* роли, выдача роли участнику, пакет изменений) даёт один запрос вместо
* десятка — иначе срабатывает лимит API (AGENT.md 8.6).
*/
function scheduleChannelSync(guildId: string, run: () => Promise<void>): void {
const existing = pendingChannelSync.get(guildId);
if (existing !== undefined) {
clearTimeout(existing);
}
pendingChannelSync.set(
guildId,
setTimeout(() => {
pendingChannelSync.delete(guildId);
if (channelSyncInFlight.has(guildId)) {
scheduleChannelSync(guildId, run);
return;
}
channelSyncInFlight.add(guildId);
void run().finally(() => channelSyncInFlight.delete(guildId));
}, 250),
);
}
export const useSessionStore = create<SessionState>((set, get) => ({
...defaults,
@@ -354,6 +385,10 @@ export const useSessionStore = create<SessionState>((set, get) => ({
});
},
scheduleGuildChannelsReload: (guildId) => {
scheduleChannelSync(guildId, () => get().reloadGuildChannels(guildId));
},
reloadGuildChannels: async (guildId) => {
try {
const channels = await fetchChannels(guildId);