deploy #1
@@ -1,7 +1,7 @@
|
||||
import type { GiveawayCreateDashboard, GiveawayDashboard } from '@nexumi/shared';
|
||||
import type { Giveaway } from '@prisma/client';
|
||||
import { prisma } from '../prisma';
|
||||
import { addJobAndAwait, giveawayQueue, giveawayQueueEvents } from '../queues';
|
||||
import { addJobAndAwait, getGiveawayQueue, getGiveawayQueueEvents } from '../queues';
|
||||
|
||||
export class GiveawayCreateTimeoutError extends Error {
|
||||
constructor() {
|
||||
@@ -60,8 +60,8 @@ export async function createGiveawayDashboard(
|
||||
}
|
||||
|
||||
const { confirmed, result } = await addJobAndAwait<{ id: string }>(
|
||||
giveawayQueue,
|
||||
giveawayQueueEvents,
|
||||
getGiveawayQueue(),
|
||||
getGiveawayQueueEvents(),
|
||||
'giveawayCreate',
|
||||
{
|
||||
guildId,
|
||||
@@ -112,12 +112,12 @@ export async function endGiveawayDashboard(guildId: string, giveawayId: string):
|
||||
return null;
|
||||
}
|
||||
|
||||
const existingJob = await giveawayQueue.getJob(`giveaway-end-${giveawayId}`);
|
||||
const existingJob = await getGiveawayQueue().getJob(`giveaway-end-${giveawayId}`);
|
||||
await existingJob?.remove();
|
||||
|
||||
await addJobAndAwait(
|
||||
giveawayQueue,
|
||||
giveawayQueueEvents,
|
||||
getGiveawayQueue(),
|
||||
getGiveawayQueueEvents(),
|
||||
'giveawayEnd',
|
||||
{ giveawayId },
|
||||
{ jobId: `giveaway-end-${giveawayId}`, removeOnComplete: true, removeOnFail: 50, delay: 0 }
|
||||
@@ -133,7 +133,7 @@ export async function deleteGiveawayDashboard(guildId: string, giveawayId: strin
|
||||
return false;
|
||||
}
|
||||
|
||||
const job = await giveawayQueue.getJob(`giveaway-end-${giveawayId}`);
|
||||
const job = await getGiveawayQueue().getJob(`giveaway-end-${giveawayId}`);
|
||||
await job?.remove();
|
||||
|
||||
await prisma.giveaway.delete({ where: { id: giveawayId } });
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { GuildBackup } from '@prisma/client';
|
||||
import { buildGuildBackupPayloadViaRest } from '../guild-backup';
|
||||
import { prisma } from '../prisma';
|
||||
import { assertPremiumLimit, PremiumLimitError } from '../premium';
|
||||
import { guildBackupQueue, guildBackupQueueName } from '../queues';
|
||||
import { getGuildBackupQueue, guildBackupQueueName } from '../queues';
|
||||
|
||||
export class BackupPremiumError extends Error {
|
||||
constructor(public readonly code: 'premium_limit' | 'premium_feature') {
|
||||
@@ -77,7 +77,7 @@ export async function enqueueGuildBackupRestore(
|
||||
guildId: string,
|
||||
requestedById: string
|
||||
): Promise<void> {
|
||||
await guildBackupQueue.add(
|
||||
await getGuildBackupQueue().add(
|
||||
'restoreGuildBackup',
|
||||
{ backupId, guildId, requestedById },
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ScheduledMessageDashboard, ScheduledMessageDashboardCreate } from '@nexumi/shared';
|
||||
import type { ScheduledMessage } from '@prisma/client';
|
||||
import { prisma } from '../prisma';
|
||||
import { scheduleQueue } from '../queues';
|
||||
import { getScheduleQueue } from '../queues';
|
||||
|
||||
export class SchedulerValidationError extends Error {
|
||||
constructor(public readonly code: string) {
|
||||
@@ -92,7 +92,7 @@ export async function createScheduledMessageDashboard(
|
||||
jobOptions.delay = Math.max(0, runAt.getTime() - Date.now());
|
||||
}
|
||||
|
||||
const job = await scheduleQueue.add('scheduleSend', { scheduleId: schedule.id }, jobOptions);
|
||||
const job = await getScheduleQueue().add('scheduleSend', { scheduleId: schedule.id }, jobOptions);
|
||||
const updated = await prisma.scheduledMessage.update({
|
||||
where: { id: schedule.id },
|
||||
data: { jobId: String(job.id) }
|
||||
@@ -108,7 +108,7 @@ export async function deleteScheduledMessageDashboard(guildId: string, scheduleI
|
||||
}
|
||||
|
||||
if (existing.jobId) {
|
||||
const job = await scheduleQueue.getJob(existing.jobId);
|
||||
const job = await getScheduleQueue().getJob(existing.jobId);
|
||||
await job?.remove();
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import type {
|
||||
} from '@nexumi/shared';
|
||||
import type { Suggestion } from '@prisma/client';
|
||||
import { prisma } from '../prisma';
|
||||
import { addJobAndAwait, suggestionsQueue, suggestionsQueueEvents } from '../queues';
|
||||
import { addJobAndAwait, getSuggestionsQueue, getSuggestionsQueueEvents } from '../queues';
|
||||
|
||||
export class SuggestionActionTimeoutError extends Error {
|
||||
constructor() {
|
||||
@@ -105,8 +105,8 @@ export async function applySuggestionAction(
|
||||
}
|
||||
|
||||
const { confirmed } = await addJobAndAwait<{ id: string }>(
|
||||
suggestionsQueue,
|
||||
suggestionsQueueEvents,
|
||||
getSuggestionsQueue(),
|
||||
getSuggestionsQueueEvents(),
|
||||
'suggestionStatusUpdate',
|
||||
{
|
||||
suggestionId,
|
||||
|
||||
@@ -8,6 +8,11 @@ import { redis } from './redis';
|
||||
* 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';
|
||||
@@ -24,43 +29,68 @@ const globalForQueues = globalThis as unknown as {
|
||||
__nexumiSuggestionsQueueEvents?: QueueEvents;
|
||||
};
|
||||
|
||||
export const giveawayQueue: Queue =
|
||||
globalForQueues.__nexumiGiveawayQueue ?? new Queue(giveawayQueueName, { connection: redis });
|
||||
function queueConnection() {
|
||||
return redis;
|
||||
}
|
||||
|
||||
export const scheduleQueue: Queue =
|
||||
globalForQueues.__nexumiScheduleQueue ?? new Queue(scheduleQueueName, { connection: redis });
|
||||
/** QueueEvents needs its own connection; duplicate inherits lazyConnect. */
|
||||
function queueEventsConnection() {
|
||||
return redis.duplicate();
|
||||
}
|
||||
|
||||
export const guildBackupQueue: Queue =
|
||||
globalForQueues.__nexumiGuildBackupQueue ?? new Queue(guildBackupQueueName, { connection: redis });
|
||||
export function getGiveawayQueue(): Queue {
|
||||
globalForQueues.__nexumiGiveawayQueue ??= new Queue(giveawayQueueName, {
|
||||
connection: queueConnection()
|
||||
});
|
||||
return globalForQueues.__nexumiGiveawayQueue;
|
||||
}
|
||||
|
||||
export const suggestionsQueue: Queue =
|
||||
globalForQueues.__nexumiSuggestionsQueue ?? new Queue(suggestionsQueueName, { connection: redis });
|
||||
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). They open their own Redis connection, so they are
|
||||
* cached on the global object the same way as the queues themselves.
|
||||
* is immediately visible).
|
||||
*/
|
||||
export const giveawayQueueEvents: QueueEvents =
|
||||
globalForQueues.__nexumiGiveawayQueueEvents ??
|
||||
new QueueEvents(giveawayQueueName, { connection: redis.duplicate() });
|
||||
export function getGiveawayQueueEvents(): QueueEvents {
|
||||
globalForQueues.__nexumiGiveawayQueueEvents ??= new QueueEvents(giveawayQueueName, {
|
||||
connection: queueEventsConnection()
|
||||
});
|
||||
return globalForQueues.__nexumiGiveawayQueueEvents;
|
||||
}
|
||||
|
||||
export const guildBackupQueueEvents: QueueEvents =
|
||||
globalForQueues.__nexumiGuildBackupQueueEvents ??
|
||||
new QueueEvents(guildBackupQueueName, { connection: redis.duplicate() });
|
||||
export function getGuildBackupQueueEvents(): QueueEvents {
|
||||
globalForQueues.__nexumiGuildBackupQueueEvents ??= new QueueEvents(guildBackupQueueName, {
|
||||
connection: queueEventsConnection()
|
||||
});
|
||||
return globalForQueues.__nexumiGuildBackupQueueEvents;
|
||||
}
|
||||
|
||||
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;
|
||||
export function getSuggestionsQueueEvents(): QueueEvents {
|
||||
globalForQueues.__nexumiSuggestionsQueueEvents ??= new QueueEvents(suggestionsQueueName, {
|
||||
connection: queueEventsConnection()
|
||||
});
|
||||
return globalForQueues.__nexumiSuggestionsQueueEvents;
|
||||
}
|
||||
|
||||
const DEFAULT_WAIT_TIMEOUT_MS = 15_000;
|
||||
|
||||
|
||||
@@ -3,6 +3,11 @@ import { env } from './env';
|
||||
|
||||
const globalForRedis = globalThis as unknown as { __nexumiRedis?: Redis };
|
||||
|
||||
/** Next.js sets this while collecting page data / compiling the production build. */
|
||||
function isNextProductionBuild(): boolean {
|
||||
return process.env.NEXT_PHASE === 'phase-production-build';
|
||||
}
|
||||
|
||||
export const redis: Redis =
|
||||
globalForRedis.__nexumiRedis ??
|
||||
new Redis(env.REDIS_URL, {
|
||||
@@ -16,7 +21,17 @@ export const redis: Redis =
|
||||
});
|
||||
|
||||
redis.on('error', (error) => {
|
||||
console.error('[redis]', error.message);
|
||||
// Redis is intentionally unreachable during `next build` (Docker build
|
||||
// stage uses a placeholder REDIS_URL). Queues are lazy, but any stray
|
||||
// connect attempt should stay silent rather than spam AggregateError.
|
||||
if (isNextProductionBuild()) {
|
||||
return;
|
||||
}
|
||||
const detail =
|
||||
error instanceof Error
|
||||
? error.message || ('code' in error ? String((error as NodeJS.ErrnoException).code) : error.name)
|
||||
: String(error);
|
||||
console.error('[redis]', detail);
|
||||
});
|
||||
|
||||
if (env.NODE_ENV !== 'production') {
|
||||
|
||||
Reference in New Issue
Block a user