From bf2d04d579423d78a4dc219270547811cb47d778 Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Wed, 9 Sep 2026 05:49:50 +0300 Subject: [PATCH] Answer the search command immediately, and don't wait on a slow source MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A search looked like it did nothing: the log shows the two sources finishing 21 seconds apart, and since the command message is deleted on sight, the channel stayed empty that whole time with no sign the bot had heard anything. The reply now goes out at once as "Ищу …" and is edited into the results, so there is always something on screen, and each source gets 12 seconds before the answer goes out without it — searching several at once otherwise means always waiting for the slowest, which behind a proxy is tens of seconds. Co-Authored-By: Claude Opus 5 --- src/bot/commands.ts | 16 +++++++++++++--- src/bot/index.ts | 12 +++++++++++- src/sources/index.ts | 23 +++++++++++++++++++++-- 3 files changed, 45 insertions(+), 6 deletions(-) diff --git a/src/bot/commands.ts b/src/bot/commands.ts index 8282d1b..5a9efeb 100644 --- a/src/bot/commands.ts +++ b/src/bot/commands.ts @@ -13,10 +13,11 @@ import { trackTitle, } from "./format.js"; -/** What we need back from a sent message to hang reactions on it. */ +/** What we need back from a sent message to work with it afterwards. */ export interface SentMessage { id: string; react(emoji: string): Promise; + edit(content: string): Promise; } export interface CommandContext { @@ -170,18 +171,27 @@ export const commands: Command[] = [ description: "Найти треки и выбрать реакцией", async run(ctx) { if (!ctx.rest) throw new UserFacingError("Укажите поисковый запрос"); + // Searching several sources can take tens of seconds through a proxy, and + // the command message is already deleted by then — without a placeholder + // the chat sits silent and the bot looks broken. + const placeholder = await ctx.reply(`🔎 Ищу **${ctx.rest}**…`); + const tracks = (await ctx.manager.search(ctx.rest, ctx.actor, CHAT_SEARCH_LIMIT)).slice( 0, CHAT_SEARCH_LIMIT, ); if (tracks.length === 0) { - await ctx.reply(`🔎 ${NOTHING_FOUND}`); + const missing = `🔎 ${NOTHING_FOUND}`; + if (placeholder) await placeholder.edit(missing); + else await ctx.reply(missing); return; } const lines = tracks.map((track, index) => trackLine(track, { index: index + 1 })); - const sent = await ctx.reply(`🔎 **${ctx.rest}**\n${lines.join("\n")}`); + const text = `🔎 **${ctx.rest}**\n${lines.join("\n")}`; + const sent = placeholder ?? (await ctx.reply(text)); if (!sent) return; + if (placeholder) await placeholder.edit(text); rememberSearch(ctx, sent.id, tracks); for (const emoji of CHOICE_EMOJI.slice(0, tracks.length)) { diff --git a/src/bot/index.ts b/src/bot/index.ts index 332d33f..09fd01c 100644 --- a/src/bot/index.ts +++ b/src/bot/index.ts @@ -82,7 +82,17 @@ async function handleMessage(manager: MusicManager, message: Message): Promise message.channel?.sendMessage(text); + const reply = async (text: string) => { + const sent = await message.channel?.sendMessage(text); + if (!sent) return undefined; + return { + id: sent.id, + react: (emoji: string) => sent.react(emoji), + edit: async (content: string) => { + await sent.edit({ content }); + }, + }; + }; if (!serverId) { await reply("Команды работают только внутри сервера."); diff --git a/src/sources/index.ts b/src/sources/index.ts index 083e521..7201a93 100644 --- a/src/sources/index.ts +++ b/src/sources/index.ts @@ -135,13 +135,32 @@ export async function searchTracks( } const [youtube, soundcloud, localHits] = await Promise.all([ - ytdlp.search(text, "youtube", limit, requestedBy).catch(() => []), - ytdlp.search(text, "soundcloud", limit, requestedBy).catch(() => []), + withDeadline(ytdlp.search(text, "youtube", limit, requestedBy), "youtube"), + withDeadline(ytdlp.search(text, "soundcloud", limit, requestedBy), "soundcloud"), local.isEnabled() ? local.search(text, 3, requestedBy).catch(() => []) : Promise.resolve([]), ]); return [...localHits, ...interleave(youtube, soundcloud)].slice(0, limit); } +/** + * One slow source should not hold up the whole answer: searching several at once + * means waiting for the slowest, and behind a proxy that can be tens of seconds. + */ +const SOURCE_DEADLINE_MS = 12_000; + +async function withDeadline(search: Promise, source: string): Promise { + let timer: NodeJS.Timeout | undefined; + const deadline = new Promise((resolve) => { + timer = setTimeout(() => { + log.warn({ source }, "search took too long, answering without it"); + resolve([]); + }, SOURCE_DEADLINE_MS); + }); + const tracks = await Promise.race([search.catch(() => []), deadline]); + if (timer) clearTimeout(timer); + return tracks; +} + /** * YouTube silently returns nothing for queries its restricted mode dislikes — * same query, same words, results in a browser but an empty list over the API.