Files
Nexumi/apps/webui/src/lib/queues.ts
TheOnlyMace 6a223892dd Refactor queue management to use lazy initialization and improve Redis connection handling
- Updated queue and QueueEvents initialization to be lazy, preventing unnecessary Redis connections during the build process.
- Refactored queue access methods to ensure consistent connection handling across the application.
- Enhanced error handling for Redis connections during production builds to reduce log noise.
2026-07-22 21:38:56 +02:00

119 lines
4.0 KiB
TypeScript

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.
*
* Queues and QueueEvents are created lazily on first use. Eager construction
* at import time would open Redis connections during `next build` (where
* Redis is intentionally unavailable), flooding the build log with
* ECONNREFUSED errors.
*/
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;
};
function queueConnection() {
return redis;
}
/** QueueEvents needs its own connection; duplicate inherits lazyConnect. */
function queueEventsConnection() {
return redis.duplicate();
}
export function getGiveawayQueue(): Queue {
globalForQueues.__nexumiGiveawayQueue ??= new Queue(giveawayQueueName, {
connection: queueConnection()
});
return globalForQueues.__nexumiGiveawayQueue;
}
export function getScheduleQueue(): Queue {
globalForQueues.__nexumiScheduleQueue ??= new Queue(scheduleQueueName, {
connection: queueConnection()
});
return globalForQueues.__nexumiScheduleQueue;
}
export function getGuildBackupQueue(): Queue {
globalForQueues.__nexumiGuildBackupQueue ??= new Queue(guildBackupQueueName, {
connection: queueConnection()
});
return globalForQueues.__nexumiGuildBackupQueue;
}
export function getSuggestionsQueue(): Queue {
globalForQueues.__nexumiSuggestionsQueue ??= new Queue(suggestionsQueueName, {
connection: queueConnection()
});
return globalForQueues.__nexumiSuggestionsQueue;
}
/**
* 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).
*/
export function getGiveawayQueueEvents(): QueueEvents {
globalForQueues.__nexumiGiveawayQueueEvents ??= new QueueEvents(giveawayQueueName, {
connection: queueEventsConnection()
});
return globalForQueues.__nexumiGiveawayQueueEvents;
}
export function getGuildBackupQueueEvents(): QueueEvents {
globalForQueues.__nexumiGuildBackupQueueEvents ??= new QueueEvents(guildBackupQueueName, {
connection: queueEventsConnection()
});
return globalForQueues.__nexumiGuildBackupQueueEvents;
}
export function getSuggestionsQueueEvents(): QueueEvents {
globalForQueues.__nexumiSuggestionsQueueEvents ??= new QueueEvents(suggestionsQueueName, {
connection: queueEventsConnection()
});
return globalForQueues.__nexumiSuggestionsQueueEvents;
}
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<T = unknown>(
queue: Queue,
queueEvents: QueueEvents,
jobName: string,
data: unknown,
options: Parameters<Queue['add']>[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 };
}
}