371 lines
10 KiB
TypeScript
371 lines
10 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
import { handlerRegistry, QueueManager } from '../src';
|
|
|
|
// Suppress Redis connection errors in tests
|
|
process.on('unhandledRejection', (reason, promise) => {
|
|
if (reason && typeof reason === 'object' && 'message' in reason) {
|
|
const message = (reason as Error).message;
|
|
if (
|
|
message.includes('Connection is closed') ||
|
|
message.includes('Connection is in monitoring mode')
|
|
) {
|
|
return;
|
|
}
|
|
}
|
|
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
|
|
});
|
|
|
|
describe('QueueManager', () => {
|
|
let queueManager: QueueManager;
|
|
|
|
// Use local Redis/Dragonfly
|
|
const redisConfig = {
|
|
host: 'localhost',
|
|
port: 6379,
|
|
password: '',
|
|
db: 0,
|
|
};
|
|
|
|
beforeEach(() => {
|
|
handlerRegistry.clear();
|
|
});
|
|
|
|
afterEach(async () => {
|
|
if (queueManager) {
|
|
try {
|
|
await Promise.race([
|
|
queueManager.shutdown(),
|
|
new Promise((_, reject) => setTimeout(() => reject(new Error('Shutdown timeout')), 3000)),
|
|
]);
|
|
} catch (error) {
|
|
console.warn('Shutdown error:', error.message);
|
|
} finally {
|
|
queueManager = null as any;
|
|
}
|
|
}
|
|
|
|
handlerRegistry.clear();
|
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
});
|
|
|
|
describe('Basic Operations', () => {
|
|
test('should initialize queue manager', async () => {
|
|
queueManager = new QueueManager({
|
|
redis: redisConfig,
|
|
});
|
|
|
|
// No need to initialize anymore - constructor handles everything
|
|
// QueueManager now manages multiple queues, not just one
|
|
expect(queueManager).toBeDefined();
|
|
});
|
|
|
|
test('should add and process a job', async () => {
|
|
let processedPayload: any;
|
|
|
|
// Register handler
|
|
handlerRegistry.register('test-handler', {
|
|
'test-operation': async payload => {
|
|
processedPayload = payload;
|
|
return { success: true, data: payload };
|
|
},
|
|
});
|
|
|
|
queueManager = new QueueManager({
|
|
redis: redisConfig,
|
|
});
|
|
|
|
// No need to initialize anymore - constructor handles everything
|
|
// Get or create a queue
|
|
const queue = queueManager.getQueue('test-queue', {
|
|
workers: 1,
|
|
});
|
|
|
|
// Add job
|
|
const job = await queue.add('test-job', {
|
|
handler: 'test-handler',
|
|
operation: 'test-operation',
|
|
payload: { message: 'Hello, Queue!' },
|
|
});
|
|
|
|
expect(job.name).toBe('test-job');
|
|
|
|
// Wait for processing
|
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
|
|
expect(processedPayload).toEqual({ message: 'Hello, Queue!' });
|
|
});
|
|
|
|
test('should handle missing handler gracefully', async () => {
|
|
queueManager = new QueueManager({
|
|
queueName: 'test-queue',
|
|
redis: redisConfig,
|
|
workers: 1,
|
|
});
|
|
|
|
// No need to initialize anymore - constructor handles everything
|
|
|
|
const job = await queueManager.add('test-job', {
|
|
handler: 'non-existent',
|
|
operation: 'test-operation',
|
|
payload: { test: true },
|
|
});
|
|
|
|
// Wait for job to fail
|
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
|
|
const failed = await job.isFailed();
|
|
expect(failed).toBe(true);
|
|
});
|
|
|
|
test('should add multiple jobs in bulk', async () => {
|
|
let processedCount = 0;
|
|
|
|
handlerRegistry.register('bulk-handler', {
|
|
process: async _payload => {
|
|
processedCount++;
|
|
return { processed: true };
|
|
},
|
|
});
|
|
|
|
queueManager = new QueueManager({
|
|
queueName: 'test-queue',
|
|
redis: redisConfig,
|
|
workers: 2,
|
|
concurrency: 5,
|
|
});
|
|
|
|
// No need to initialize anymore - constructor handles everything
|
|
|
|
const jobs = await queueManager.addBulk([
|
|
{
|
|
name: 'job1',
|
|
data: { handler: 'bulk-handler', operation: 'process', payload: { id: 1 } },
|
|
},
|
|
{
|
|
name: 'job2',
|
|
data: { handler: 'bulk-handler', operation: 'process', payload: { id: 2 } },
|
|
},
|
|
{
|
|
name: 'job3',
|
|
data: { handler: 'bulk-handler', operation: 'process', payload: { id: 3 } },
|
|
},
|
|
]);
|
|
|
|
expect(jobs.length).toBe(3);
|
|
|
|
// Wait for processing
|
|
await new Promise(resolve => setTimeout(resolve, 200));
|
|
|
|
expect(processedCount).toBe(3);
|
|
});
|
|
|
|
test('should get queue statistics', async () => {
|
|
queueManager = new QueueManager({
|
|
queueName: 'test-queue',
|
|
redis: redisConfig,
|
|
workers: 0, // No workers, jobs will stay in waiting
|
|
});
|
|
|
|
// No need to initialize anymore - constructor handles everything
|
|
|
|
// Add some jobs
|
|
await queueManager.add('job1', {
|
|
handler: 'test',
|
|
operation: 'test',
|
|
payload: { id: 1 },
|
|
});
|
|
|
|
await queueManager.add('job2', {
|
|
handler: 'test',
|
|
operation: 'test',
|
|
payload: { id: 2 },
|
|
});
|
|
|
|
const stats = await queueManager.getStats();
|
|
|
|
expect(stats.waiting).toBe(2);
|
|
expect(stats.active).toBe(0);
|
|
expect(stats.completed).toBe(0);
|
|
expect(stats.failed).toBe(0);
|
|
});
|
|
|
|
test('should pause and resume queue', async () => {
|
|
let processedCount = 0;
|
|
|
|
handlerRegistry.register('pause-test', {
|
|
process: async () => {
|
|
processedCount++;
|
|
return { ok: true };
|
|
},
|
|
});
|
|
|
|
queueManager = new QueueManager({
|
|
queueName: 'test-queue',
|
|
redis: redisConfig,
|
|
workers: 1,
|
|
});
|
|
|
|
// No need to initialize anymore - constructor handles everything
|
|
|
|
// Pause queue
|
|
await queueManager.pause();
|
|
|
|
// Add job while paused
|
|
await queueManager.add('job1', {
|
|
handler: 'pause-test',
|
|
operation: 'process',
|
|
payload: {},
|
|
});
|
|
|
|
// Wait a bit - job should not be processed
|
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
expect(processedCount).toBe(0);
|
|
|
|
// Resume queue
|
|
await queueManager.resume();
|
|
|
|
// Wait for processing
|
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
expect(processedCount).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe('Scheduled Jobs', () => {
|
|
test('should register and process scheduled jobs', async () => {
|
|
let executionCount = 0;
|
|
|
|
handlerRegistry.registerWithSchedule({
|
|
name: 'scheduled-handler',
|
|
operations: {
|
|
'scheduled-task': async _payload => {
|
|
executionCount++;
|
|
return { executed: true, timestamp: Date.now() };
|
|
},
|
|
},
|
|
scheduledJobs: [
|
|
{
|
|
type: 'test-schedule',
|
|
operation: 'scheduled-task',
|
|
payload: { test: true },
|
|
cronPattern: '*/1 * * * * *', // Every second
|
|
description: 'Test scheduled job',
|
|
},
|
|
],
|
|
});
|
|
|
|
queueManager = new QueueManager({
|
|
queueName: 'test-queue',
|
|
redis: redisConfig,
|
|
workers: 1,
|
|
enableScheduledJobs: true,
|
|
});
|
|
|
|
// No need to initialize anymore - constructor handles everything
|
|
|
|
// Wait for scheduled job to execute
|
|
await new Promise(resolve => setTimeout(resolve, 2500));
|
|
|
|
expect(executionCount).toBeGreaterThanOrEqual(2);
|
|
});
|
|
});
|
|
|
|
describe('Error Handling', () => {
|
|
test('should handle job errors with retries', async () => {
|
|
let attemptCount = 0;
|
|
|
|
handlerRegistry.register('retry-handler', {
|
|
'failing-operation': async () => {
|
|
attemptCount++;
|
|
if (attemptCount < 3) {
|
|
throw new Error(`Attempt ${attemptCount} failed`);
|
|
}
|
|
return { success: true };
|
|
},
|
|
});
|
|
|
|
queueManager = new QueueManager({
|
|
queueName: 'test-queue',
|
|
redis: redisConfig,
|
|
workers: 1,
|
|
defaultJobOptions: {
|
|
attempts: 3,
|
|
backoff: {
|
|
type: 'fixed',
|
|
delay: 50,
|
|
},
|
|
},
|
|
});
|
|
|
|
// No need to initialize anymore - constructor handles everything
|
|
|
|
const job = await queueManager.add('retry-job', {
|
|
handler: 'retry-handler',
|
|
operation: 'failing-operation',
|
|
payload: {},
|
|
});
|
|
|
|
// Wait for retries
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
|
|
const completed = await job.isCompleted();
|
|
expect(completed).toBe(true);
|
|
expect(attemptCount).toBe(3);
|
|
});
|
|
});
|
|
|
|
describe('Multiple Handlers', () => {
|
|
test('should handle multiple handlers with different operations', async () => {
|
|
const results: any[] = [];
|
|
|
|
handlerRegistry.register('handler-a', {
|
|
'operation-1': async payload => {
|
|
results.push({ handler: 'a', op: '1', payload });
|
|
return { handler: 'a', op: '1' };
|
|
},
|
|
'operation-2': async payload => {
|
|
results.push({ handler: 'a', op: '2', payload });
|
|
return { handler: 'a', op: '2' };
|
|
},
|
|
});
|
|
|
|
handlerRegistry.register('handler-b', {
|
|
'operation-1': async payload => {
|
|
results.push({ handler: 'b', op: '1', payload });
|
|
return { handler: 'b', op: '1' };
|
|
},
|
|
});
|
|
|
|
queueManager = new QueueManager({
|
|
queueName: 'test-queue',
|
|
redis: redisConfig,
|
|
workers: 2,
|
|
});
|
|
|
|
// No need to initialize anymore - constructor handles everything
|
|
|
|
// Add jobs for different handlers
|
|
await queueManager.addBulk([
|
|
{
|
|
name: 'job1',
|
|
data: { handler: 'handler-a', operation: 'operation-1', payload: { id: 1 } },
|
|
},
|
|
{
|
|
name: 'job2',
|
|
data: { handler: 'handler-a', operation: 'operation-2', payload: { id: 2 } },
|
|
},
|
|
{
|
|
name: 'job3',
|
|
data: { handler: 'handler-b', operation: 'operation-1', payload: { id: 3 } },
|
|
},
|
|
]);
|
|
|
|
// Wait for processing
|
|
await new Promise(resolve => setTimeout(resolve, 200));
|
|
|
|
expect(results.length).toBe(3);
|
|
expect(results).toContainEqual({ handler: 'a', op: '1', payload: { id: 1 } });
|
|
expect(results).toContainEqual({ handler: 'a', op: '2', payload: { id: 2 } });
|
|
expect(results).toContainEqual({ handler: 'b', op: '1', payload: { id: 3 } });
|
|
});
|
|
});
|
|
});
|