import { Queue, QueueEvents } from 'bullmq'; import { redis } from './redis'; /** * BullMQ queue/job names mirrored 1:1 from `apps/bot/src/queues.ts` and * `apps/bot/src/jobs.ts`. The WebUI has no discord.js Client, so any * dashboard action that needs to touch Discord (posting a giveaway message, * drawing winners, restoring a guild backup) is enqueued here and picked up * by the bot's existing workers - the WebUI never talks to Discord directly * for these actions. */ export const giveawayQueueName = 'giveaways'; export const scheduleQueueName = 'schedules'; export const guildBackupQueueName = 'guild-backups'; export const suggestionsQueueName = 'suggestions'; const globalForQueues = globalThis as unknown as { __nexumiGiveawayQueue?: Queue; __nexumiScheduleQueue?: Queue; __nexumiGuildBackupQueue?: Queue; __nexumiSuggestionsQueue?: Queue; __nexumiGiveawayQueueEvents?: QueueEvents; __nexumiGuildBackupQueueEvents?: QueueEvents; __nexumiSuggestionsQueueEvents?: QueueEvents; }; export const giveawayQueue: Queue = globalForQueues.__nexumiGiveawayQueue ?? new Queue(giveawayQueueName, { connection: redis }); export const scheduleQueue: Queue = globalForQueues.__nexumiScheduleQueue ?? new Queue(scheduleQueueName, { connection: redis }); export const guildBackupQueue: Queue = globalForQueues.__nexumiGuildBackupQueue ?? new Queue(guildBackupQueueName, { connection: redis }); export const suggestionsQueue: Queue = globalForQueues.__nexumiSuggestionsQueue ?? new Queue(suggestionsQueueName, { connection: redis }); /** * QueueEvents instances are only needed for jobs where the dashboard needs * to wait for the bot to finish (e.g. ending a giveaway so the winners list * is immediately visible). They open their own Redis connection, so they are * cached on the global object the same way as the queues themselves. */ export const giveawayQueueEvents: QueueEvents = globalForQueues.__nexumiGiveawayQueueEvents ?? new QueueEvents(giveawayQueueName, { connection: redis.duplicate() }); export const guildBackupQueueEvents: QueueEvents = globalForQueues.__nexumiGuildBackupQueueEvents ?? new QueueEvents(guildBackupQueueName, { connection: redis.duplicate() }); export const suggestionsQueueEvents: QueueEvents = globalForQueues.__nexumiSuggestionsQueueEvents ?? new QueueEvents(suggestionsQueueName, { connection: redis.duplicate() }); globalForQueues.__nexumiGiveawayQueue = giveawayQueue; globalForQueues.__nexumiScheduleQueue = scheduleQueue; globalForQueues.__nexumiGuildBackupQueue = guildBackupQueue; globalForQueues.__nexumiSuggestionsQueue = suggestionsQueue; globalForQueues.__nexumiGiveawayQueueEvents = giveawayQueueEvents; globalForQueues.__nexumiGuildBackupQueueEvents = guildBackupQueueEvents; globalForQueues.__nexumiSuggestionsQueueEvents = suggestionsQueueEvents; const DEFAULT_WAIT_TIMEOUT_MS = 15_000; /** * Adds a job and waits (bounded) for the bot worker to finish it, so the * dashboard can immediately reflect the result instead of polling. Falls * back to "not confirmed" (does not throw) on timeout - the DB row created * by the worker is still the source of truth and will show up on next load. */ export async function addJobAndAwait( queue: Queue, queueEvents: QueueEvents, jobName: string, data: unknown, options: Parameters[2] = {}, timeoutMs = DEFAULT_WAIT_TIMEOUT_MS ): Promise<{ confirmed: boolean; result: T | null }> { const job = await queue.add(jobName, data, options); try { const result = (await job.waitUntilFinished(queueEvents, timeoutMs)) as T; return { confirmed: true, result }; } catch { return { confirmed: false, result: null }; } }