feat: certificates, landing pages, ai tutor, website update, compliance
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
import type { JobsOptions, QueueOptions } from 'bullmq';
|
||||
|
||||
import { QUEUE_NAMES, type QueueName } from './names';
|
||||
|
||||
/**
|
||||
* Per-queue defaults. Tuned roughly for the workload:
|
||||
*
|
||||
* - Heavy/external work (media, transcription, webhooks): more attempts,
|
||||
* longer backoff, longer retention so we can replay.
|
||||
* - Light/internal work (emails, notifications, outbox): faster retry,
|
||||
* short retention.
|
||||
*/
|
||||
export const QUEUE_DEFAULTS: Record<QueueName, JobsOptions> = {
|
||||
[QUEUE_NAMES.media]: {
|
||||
attempts: 3,
|
||||
backoff: { type: 'exponential', delay: 30_000 },
|
||||
removeOnComplete: { age: 86_400, count: 1_000 },
|
||||
removeOnFail: { age: 7 * 86_400 }
|
||||
},
|
||||
[QUEUE_NAMES.mediaTranscribe]: {
|
||||
attempts: 4,
|
||||
backoff: { type: 'exponential', delay: 60_000 },
|
||||
removeOnComplete: { age: 86_400, count: 500 },
|
||||
removeOnFail: { age: 14 * 86_400 }
|
||||
},
|
||||
[QUEUE_NAMES.emails]: {
|
||||
attempts: 5,
|
||||
backoff: { type: 'exponential', delay: 5_000 },
|
||||
removeOnComplete: { age: 6 * 3_600, count: 5_000 },
|
||||
removeOnFail: { age: 7 * 86_400 }
|
||||
},
|
||||
[QUEUE_NAMES.notifications]: {
|
||||
attempts: 3,
|
||||
backoff: { type: 'exponential', delay: 5_000 },
|
||||
removeOnComplete: { age: 6 * 3_600, count: 5_000 },
|
||||
removeOnFail: { age: 7 * 86_400 }
|
||||
},
|
||||
[QUEUE_NAMES.webhooks]: {
|
||||
attempts: 8,
|
||||
backoff: { type: 'exponential', delay: 30_000 },
|
||||
removeOnComplete: { age: 86_400, count: 5_000 },
|
||||
removeOnFail: { age: 14 * 86_400 }
|
||||
},
|
||||
[QUEUE_NAMES.courseImports]: {
|
||||
attempts: 3,
|
||||
backoff: { type: 'exponential', delay: 30_000 },
|
||||
removeOnComplete: { age: 7 * 86_400, count: 500 },
|
||||
removeOnFail: { age: 14 * 86_400 }
|
||||
},
|
||||
[QUEUE_NAMES.onboardingBootstrap]: {
|
||||
attempts: 3,
|
||||
backoff: { type: 'exponential', delay: 30_000 },
|
||||
removeOnComplete: { age: 7 * 86_400, count: 500 },
|
||||
removeOnFail: { age: 14 * 86_400 }
|
||||
},
|
||||
[QUEUE_NAMES.maintenance]: {
|
||||
attempts: 2,
|
||||
backoff: { type: 'exponential', delay: 60_000 },
|
||||
removeOnComplete: { age: 86_400, count: 100 },
|
||||
removeOnFail: { age: 7 * 86_400 }
|
||||
}
|
||||
};
|
||||
|
||||
export function buildQueueOptions(name: QueueName, override?: Partial<QueueOptions>): QueueOptions {
|
||||
return {
|
||||
...override,
|
||||
defaultJobOptions: {
|
||||
...QUEUE_DEFAULTS[name],
|
||||
...(override?.defaultJobOptions ?? {})
|
||||
}
|
||||
} as QueueOptions;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { FlowProducer, Queue } from 'bullmq';
|
||||
|
||||
import { getSharedRedisConnection } from '../connection';
|
||||
import { QUEUE_NAMES, type QueueName } from './names';
|
||||
import { buildQueueOptions } from './defaults';
|
||||
|
||||
/**
|
||||
* Lazy queue + flow-producer accessors backed by the shared Redis
|
||||
* connection. Workers should NOT use these — they need their own Redis
|
||||
* connection per process. These are for the API enqueue path.
|
||||
*/
|
||||
|
||||
const queues = new Map<QueueName, Queue>();
|
||||
let flowProducer: FlowProducer | undefined;
|
||||
|
||||
export function getQueue(name: QueueName): Queue {
|
||||
let queue = queues.get(name);
|
||||
if (!queue) {
|
||||
queue = new Queue(name, {
|
||||
...buildQueueOptions(name),
|
||||
connection: getSharedRedisConnection()
|
||||
});
|
||||
queues.set(name, queue);
|
||||
}
|
||||
|
||||
return queue;
|
||||
}
|
||||
|
||||
export function getFlowProducer(): FlowProducer {
|
||||
if (!flowProducer) {
|
||||
flowProducer = new FlowProducer({ connection: getSharedRedisConnection() });
|
||||
}
|
||||
|
||||
return flowProducer;
|
||||
}
|
||||
|
||||
/** Eagerly close every cached queue/flow producer. Useful in tests. */
|
||||
export async function closeAllQueues(): Promise<void> {
|
||||
await Promise.all(Array.from(queues.values()).map((queue) => queue.close()));
|
||||
queues.clear();
|
||||
|
||||
if (flowProducer) {
|
||||
await flowProducer.close();
|
||||
flowProducer = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns every queue name in registration order. */
|
||||
export function listQueueNames(): QueueName[] {
|
||||
return Object.values(QUEUE_NAMES);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './names';
|
||||
export * from './defaults';
|
||||
export * from './factories';
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* BullMQ queue names. Always import from here so producers and workers stay
|
||||
* in sync, and so it's trivial to grep for everything that touches a queue.
|
||||
*/
|
||||
export const QUEUE_NAMES = {
|
||||
media: 'media',
|
||||
mediaTranscribe: 'media-transcribe',
|
||||
emails: 'emails',
|
||||
notifications: 'notifications',
|
||||
webhooks: 'webhooks',
|
||||
courseImports: 'course-imports',
|
||||
onboardingBootstrap: 'onboarding-bootstrap',
|
||||
maintenance: 'maintenance'
|
||||
} as const;
|
||||
|
||||
export type QueueName = (typeof QUEUE_NAMES)[keyof typeof QUEUE_NAMES];
|
||||
|
||||
/**
|
||||
* BullMQ job names within each queue. Workers route on `(queueName, jobName)`,
|
||||
* so keep the strings here too.
|
||||
*/
|
||||
export const JOB_NAMES = {
|
||||
media: {
|
||||
probeMetadata: 'probe-metadata',
|
||||
generateThumbnail: 'generate-thumbnail',
|
||||
extractAudio: 'extract-audio',
|
||||
compressVideo: 'compress-video'
|
||||
},
|
||||
mediaTranscribe: {
|
||||
transcribeAudio: 'transcribe-audio'
|
||||
},
|
||||
emails: {
|
||||
send: 'send'
|
||||
},
|
||||
webhooks: {
|
||||
deliver: 'deliver'
|
||||
},
|
||||
maintenance: {
|
||||
retentionCompact: 'retention-compact',
|
||||
deadLetterCleanup: 'dead-letter-cleanup'
|
||||
}
|
||||
} as const;
|
||||
Reference in New Issue
Block a user