queue-tool 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/worker.js ADDED
@@ -0,0 +1,100 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Worker = void 0;
4
+ const factory_1 = require("./factory");
5
+ const logger_1 = require("./utils/logger");
6
+ const crypto_1 = require("crypto");
7
+ class Worker {
8
+ queueName;
9
+ connectionOrAdapter;
10
+ processor;
11
+ adapter;
12
+ running = false;
13
+ workerId;
14
+ concurrency;
15
+ pollIntervalMs;
16
+ taskNameKey;
17
+ ownAdapter = false;
18
+ loops = [];
19
+ constructor(queueName, connectionOrAdapter, processor, options) {
20
+ this.queueName = queueName;
21
+ this.connectionOrAdapter = connectionOrAdapter;
22
+ this.processor = processor;
23
+ this.workerId = `worker-${(0, crypto_1.randomUUID)()}`;
24
+ this.concurrency = options?.concurrency ?? 1;
25
+ this.pollIntervalMs = options?.pollIntervalMs ?? 1000;
26
+ this.taskNameKey = options?.taskNameKey ?? 'task';
27
+ }
28
+ async start() {
29
+ if (this.running) {
30
+ return;
31
+ }
32
+ logger_1.logger.info('Starting worker...', { queue: this.queueName, workerId: this.workerId, concurrency: this.concurrency });
33
+ if (typeof this.connectionOrAdapter === 'string') {
34
+ this.adapter = await factory_1.QueueFactory.create(this.connectionOrAdapter);
35
+ this.ownAdapter = true;
36
+ }
37
+ else {
38
+ this.adapter = this.connectionOrAdapter;
39
+ }
40
+ this.running = true;
41
+ this.loops = [];
42
+ for (let i = 0; i < this.concurrency; i++) {
43
+ this.loops.push(this.runLoop(i));
44
+ }
45
+ }
46
+ async stop() {
47
+ if (!this.running) {
48
+ return;
49
+ }
50
+ logger_1.logger.info('Stopping worker...', { queue: this.queueName, workerId: this.workerId });
51
+ this.running = false;
52
+ // Wait for all concurrent loops to finish their current job
53
+ await Promise.all(this.loops);
54
+ this.loops = [];
55
+ if (this.ownAdapter && this.adapter) {
56
+ await this.adapter.close();
57
+ }
58
+ logger_1.logger.info('Worker stopped successfully', { queue: this.queueName, workerId: this.workerId });
59
+ }
60
+ async runLoop(loopIndex) {
61
+ logger_1.logger.info(`Worker loop ${loopIndex} started`, { queue: this.queueName, workerId: this.workerId });
62
+ while (this.running) {
63
+ try {
64
+ const job = await this.adapter.claimJob(this.queueName, this.workerId);
65
+ if (job) {
66
+ logger_1.logger.info(`Worker processing job`, { queue: this.queueName, jobId: job.id, workerId: this.workerId });
67
+ try {
68
+ if (typeof this.processor === 'function') {
69
+ await this.processor(job);
70
+ }
71
+ else {
72
+ const taskName = job.payload && job.payload[this.taskNameKey];
73
+ if (taskName && typeof this.processor[taskName] === 'function') {
74
+ await this.processor[taskName](job);
75
+ }
76
+ else {
77
+ throw new Error(`No handler registered for task: ${taskName}`);
78
+ }
79
+ }
80
+ await this.adapter.completeJob(job.id);
81
+ logger_1.logger.info(`Worker successfully completed job`, { queue: this.queueName, jobId: job.id, workerId: this.workerId });
82
+ }
83
+ catch (procErr) {
84
+ const errMsg = procErr?.message || String(procErr);
85
+ logger_1.logger.error(`Worker job execution failed`, { queue: this.queueName, jobId: job.id, workerId: this.workerId, error: errMsg });
86
+ await this.adapter.failJob(job.id, errMsg);
87
+ }
88
+ // Process next job immediately
89
+ continue;
90
+ }
91
+ }
92
+ catch (err) {
93
+ logger_1.logger.error(`Worker loop error`, { queue: this.queueName, workerId: this.workerId, error: err.message });
94
+ }
95
+ // No job claimed, wait before polling again
96
+ await new Promise(resolve => setTimeout(resolve, this.pollIntervalMs));
97
+ }
98
+ }
99
+ }
100
+ exports.Worker = Worker;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,89 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const redis_1 = require("./adapters/redis");
7
+ const worker_1 = require("./worker");
8
+ const ioredis_1 = __importDefault(require("ioredis"));
9
+ describe('Worker Integration Tests', () => {
10
+ let adapter;
11
+ let redisClient;
12
+ const queueName = 'test-worker-queue';
13
+ const connectionString = 'redis://localhost:6379';
14
+ beforeAll(async () => {
15
+ redisClient = new ioredis_1.default(connectionString);
16
+ adapter = new redis_1.RedisQueueAdapter(connectionString);
17
+ await adapter.init();
18
+ });
19
+ afterAll(async () => {
20
+ await adapter.close();
21
+ await redisClient.quit();
22
+ });
23
+ beforeEach(async () => {
24
+ const keys = await redisClient.keys(`*${queueName}*`);
25
+ if (keys.length > 0) {
26
+ await redisClient.del(...keys);
27
+ }
28
+ });
29
+ test('should process enqueued jobs automatically', async () => {
30
+ const processedJobs = [];
31
+ const worker = new worker_1.Worker(queueName, adapter, async (job) => {
32
+ processedJobs.push(job);
33
+ }, { pollIntervalMs: 100 });
34
+ await worker.start();
35
+ const payload = { test: 'worker-job', val: 99 };
36
+ await adapter.enqueue(queueName, payload);
37
+ await new Promise(resolve => setTimeout(resolve, 300));
38
+ expect(processedJobs.length).toBe(1);
39
+ expect(processedJobs[0].payload).toEqual(payload);
40
+ const finalJob = await adapter.getJob(processedJobs[0].id);
41
+ expect(finalJob.status).toBe('completed');
42
+ await worker.stop();
43
+ });
44
+ test('should route jobs to named task handlers', async () => {
45
+ const executedTasks = [];
46
+ const worker = new worker_1.Worker(queueName, adapter, {
47
+ sendEmail: async (job) => {
48
+ executedTasks.push(`email:${job.payload.to}`);
49
+ },
50
+ generateReport: async (job) => {
51
+ executedTasks.push(`report:${job.payload.type}`);
52
+ }
53
+ }, { pollIntervalMs: 100, taskNameKey: 'action' });
54
+ await worker.start();
55
+ // Enqueue jobs with different actions
56
+ await adapter.enqueue(queueName, { action: 'sendEmail', to: 'test@example.com' });
57
+ await adapter.enqueue(queueName, { action: 'generateReport', type: 'pdf' });
58
+ await new Promise(resolve => setTimeout(resolve, 400));
59
+ expect(executedTasks).toContain('email:test@example.com');
60
+ expect(executedTasks).toContain('report:pdf');
61
+ expect(executedTasks.length).toBe(2);
62
+ await worker.stop();
63
+ });
64
+ test('should handle job failure and retries automatically', async () => {
65
+ let processAttempts = 0;
66
+ const worker = new worker_1.Worker(queueName, adapter, async (job) => {
67
+ processAttempts++;
68
+ throw new Error('processing failed intentionally');
69
+ }, { pollIntervalMs: 100 });
70
+ await worker.start();
71
+ const job = await adapter.enqueue(queueName, { task: 'will-fail' }, { maxAttempts: 2 });
72
+ await new Promise(resolve => setTimeout(resolve, 200));
73
+ expect(processAttempts).toBe(1);
74
+ const checkJob1 = await adapter.getJob(job.id);
75
+ expect(checkJob1.status).toBe('pending');
76
+ expect(checkJob1.error).toBe('processing failed intentionally');
77
+ const jobKey = `job:${job.id}`;
78
+ const pastTime = Date.now() - 60000;
79
+ await redisClient.hset(jobKey, 'run_at', pastTime.toString());
80
+ const score = pastTime - (job.priority * 10000000000);
81
+ await redisClient.zadd(`queue:${queueName}:pending`, score, job.id);
82
+ await new Promise(resolve => setTimeout(resolve, 300));
83
+ expect(processAttempts).toBe(2);
84
+ const checkJob2 = await adapter.getJob(job.id);
85
+ expect(checkJob2.status).toBe('failed');
86
+ expect(checkJob2.error).toBe('processing failed intentionally');
87
+ await worker.stop();
88
+ });
89
+ });
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "queue-tool",
3
+ "version": "1.0.0",
4
+ "main": "dist/index.js",
5
+ "types": "dist/index.d.ts",
6
+ "files": [
7
+ "dist",
8
+ "README.md"
9
+ ],
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "test": "jest",
13
+ "lint": "eslint src --ext .ts"
14
+ },
15
+ "dependencies": {
16
+ "@msgpack/msgpack": "^2.8.0",
17
+ "ioredis": "^5.4.1",
18
+ "mongodb": "^6.6.2",
19
+ "pg": "^8.11.5"
20
+ },
21
+ "devDependencies": {
22
+ "@types/jest": "^29.5.12",
23
+ "@types/node": "^20.12.12",
24
+ "@types/pg": "^8.11.6",
25
+ "@typescript-eslint/eslint-plugin": "^7.10.0",
26
+ "@typescript-eslint/parser": "^7.10.0",
27
+ "eslint": "^8.57.0",
28
+ "jest": "^29.5.0",
29
+ "ts-jest": "^29.1.0",
30
+ "typescript": "^5.4.5"
31
+ }
32
+ }