Files
the_order/packages/jobs/src/queue.ts
defiQUG 92cc41d26d Add Legal Office seal and complete Azure CDN deployment
- Add Legal Office of the Master seal (SVG design with Maltese Cross, scales of justice, legal scroll)
- Create legal-office-manifest-template.json for Legal Office credentials
- Update SEAL_MAPPING.md and DESIGN_GUIDE.md with Legal Office seal documentation
- Complete Azure CDN infrastructure deployment:
  - Resource group, storage account, and container created
  - 17 PNG seal files uploaded to Azure Blob Storage
  - All manifest templates updated with Azure URLs
  - Configuration files generated (azure-cdn-config.env)
- Add comprehensive Azure CDN setup scripts and documentation
- Fix manifest URL generation to prevent double slashes
- Verify all seals accessible via HTTPS
2025-11-12 22:03:42 -08:00

241 lines
5.7 KiB
TypeScript

/**
* Background job queue using BullMQ
*/
import { Queue, QueueOptions, Worker, Job, JobsOptions } from 'bullmq';
import IORedis from 'ioredis';
import { getEnv } from '@the-order/shared';
export interface JobQueueConfig {
connection?: {
host?: string;
port?: number;
password?: string;
db?: number;
};
defaultJobOptions?: JobsOptions;
}
export interface JobData {
[key: string]: unknown;
}
export type JobHandler<T = JobData> = (job: Job<T>) => Promise<unknown>;
/**
* Job Queue Manager
*/
export class JobQueue {
private queues: Map<string, Queue> = new Map();
private workers: Map<string, Worker> = new Map();
private connection: IORedis;
constructor(private config?: JobQueueConfig) {
const env = getEnv();
const redisUrl = env.REDIS_URL;
// Create Redis connection
if (redisUrl) {
this.connection = new IORedis(redisUrl, {
maxRetriesPerRequest: null,
enableReadyCheck: false,
});
} else {
// Use connection config or defaults
this.connection = new IORedis({
host: config?.connection?.host || 'localhost',
port: config?.connection?.port || 6379,
password: config?.connection?.password,
db: config?.connection?.db || 0,
maxRetriesPerRequest: null,
enableReadyCheck: false,
});
}
}
/**
* Create or get a queue
*/
createQueue<T = JobData>(name: string, options?: QueueOptions): Queue<T> {
if (this.queues.has(name)) {
return this.queues.get(name) as Queue<T>;
}
const queue = new Queue<T>(name, {
connection: this.connection,
defaultJobOptions: {
attempts: 3,
backoff: {
type: 'exponential',
delay: 2000,
},
removeOnComplete: {
age: 24 * 3600, // Keep completed jobs for 24 hours
count: 1000, // Keep last 1000 completed jobs
},
removeOnFail: {
age: 7 * 24 * 3600, // Keep failed jobs for 7 days
},
...this.config?.defaultJobOptions,
...options?.defaultJobOptions,
},
...options,
});
this.queues.set(name, queue);
return queue;
}
/**
* Create a worker for a queue
*/
createWorker<T = JobData>(
queueName: string,
handler: JobHandler<T>,
options?: { concurrency?: number }
): Worker<T> {
if (this.workers.has(queueName)) {
return this.workers.get(queueName) as Worker<T>;
}
const worker = new Worker<T>(
queueName,
async (job: Job<T>) => {
try {
return await handler(job);
} catch (error) {
console.error(`Job ${job.id} failed:`, error);
throw error;
}
},
{
connection: this.connection,
concurrency: options?.concurrency || 1,
}
);
// Set up event handlers
worker.on('completed', (job: Job<T>) => {
console.log(`Job ${job.id} completed`);
});
worker.on('failed', (job: Job<T> | undefined, err: Error) => {
console.error(`Job ${job?.id || 'unknown'} failed:`, err);
});
this.workers.set(queueName, worker);
return worker;
}
/**
* Add a job to a queue
*/
async addJob<T = JobData>(
queueName: string,
data: T,
options?: JobsOptions
): Promise<Job<T>> {
const queue = this.queues.get(queueName) || this.createQueue<T>(queueName);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return queue.add('default' as any, data as any, options);
}
/**
* Add a scheduled job
*/
async addScheduledJob<T = JobData>(
queueName: string,
data: T,
delay: number | Date,
options?: JobsOptions
): Promise<Job<T>> {
const queue = this.queues.get(queueName) || this.createQueue<T>(queueName);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return queue.add('default' as any, data as any, {
...options,
delay: typeof delay === 'number' ? delay : delay.getTime() - Date.now(),
});
}
/**
* Add a recurring job (cron)
*/
async addRecurringJob<T = JobData>(
queueName: string,
data: T,
cronPattern: string,
options?: JobsOptions
): Promise<Job<T>> {
const queue = this.queues.get(queueName) || this.createQueue<T>(queueName);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return queue.add('default' as any, data as any, {
...options,
repeat: {
pattern: cronPattern,
},
});
}
/**
* Get job status
*/
async getJobStatus(queueName: string, jobId: string): Promise<unknown> {
const queue = this.queues.get(queueName);
if (!queue) {
throw new Error(`Queue ${queueName} not found`);
}
const job = await queue.getJob(jobId);
if (!job) {
return null;
}
return {
id: job.id,
name: job.name,
data: job.data,
state: await job.getState(),
progress: job.progress,
returnvalue: job.returnvalue,
failedReason: job.failedReason,
timestamp: job.timestamp,
processedOn: job.processedOn,
finishedOn: job.finishedOn,
};
}
/**
* Close all queues and workers
*/
async close(): Promise<void> {
// Close all workers
for (const worker of this.workers.values()) {
await worker.close();
}
this.workers.clear();
// Close all queues
for (const queue of this.queues.values()) {
await queue.close();
}
this.queues.clear();
// Close Redis connection
await this.connection.quit();
}
}
/**
* Default job queue instance
*/
let defaultJobQueue: JobQueue | null = null;
/**
* Get or create default job queue
*/
export function getJobQueue(config?: JobQueueConfig): JobQueue {
if (!defaultJobQueue) {
defaultJobQueue = new JobQueue(config);
}
return defaultJobQueue;
}