@supaproxy/bullmq 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.
@@ -0,0 +1,28 @@
1
+ import type { QueueService, QueueJobCounts, FailedJob, LifecycleHandler } from '@supaproxy/core/ports/queue';
2
+ export declare class BullMqService implements QueueService {
3
+ private readonly redisHost;
4
+ private readonly redisPort;
5
+ private readonly lifecycleQueue;
6
+ private readonly coldMessageQueue;
7
+ private readonly statsQueue;
8
+ private readonly queues;
9
+ private lifecycleWorker;
10
+ private coldMessageWorker;
11
+ private statsWorker;
12
+ constructor(redisHost: string, redisPort: number);
13
+ addColdMessage(data: {
14
+ conversationId: string;
15
+ consumerType: string;
16
+ channel: string;
17
+ externalThreadId: string;
18
+ }): Promise<void>;
19
+ addStatsJob(conversationId: string): Promise<void>;
20
+ getJobCounts(queueName: string): Promise<QueueJobCounts>;
21
+ getFailedJobs(queueName: string, limit: number): Promise<FailedJob[]>;
22
+ retryAllFailed(queueName: string): Promise<number>;
23
+ drainQueue(queueName: string): Promise<void>;
24
+ listQueueNames(): string[];
25
+ queueExists(name: string): boolean;
26
+ startWorkers(handler: LifecycleHandler): Promise<void>;
27
+ stopWorkers(): Promise<void>;
28
+ }
@@ -0,0 +1,110 @@
1
+ import { Queue, Worker } from 'bullmq';
2
+ import { QUEUE_LIFECYCLE, QUEUE_COLD_MESSAGES, QUEUE_CONVERSATION_STATS, LIFECYCLE_SCAN_INTERVAL_MS, COLD_MESSAGE_CONCURRENCY, STATS_WORKER_CONCURRENCY } from '@supaproxy/core/defaults';
3
+ import pino from 'pino';
4
+ const log = pino({ name: 'bullmq-service' });
5
+ export class BullMqService {
6
+ redisHost;
7
+ redisPort;
8
+ lifecycleQueue;
9
+ coldMessageQueue;
10
+ statsQueue;
11
+ queues;
12
+ lifecycleWorker = null;
13
+ coldMessageWorker = null;
14
+ statsWorker = null;
15
+ constructor(redisHost, redisPort) {
16
+ this.redisHost = redisHost;
17
+ this.redisPort = redisPort;
18
+ const connection = { host: this.redisHost, port: this.redisPort };
19
+ this.lifecycleQueue = new Queue(QUEUE_LIFECYCLE, { connection });
20
+ this.coldMessageQueue = new Queue(QUEUE_COLD_MESSAGES, { connection });
21
+ this.statsQueue = new Queue(QUEUE_CONVERSATION_STATS, { connection });
22
+ this.queues = {
23
+ [QUEUE_LIFECYCLE]: this.lifecycleQueue,
24
+ [QUEUE_COLD_MESSAGES]: this.coldMessageQueue,
25
+ [QUEUE_CONVERSATION_STATS]: this.statsQueue,
26
+ };
27
+ }
28
+ async addColdMessage(data) {
29
+ await this.coldMessageQueue.add('send-cold-message', data);
30
+ }
31
+ async addStatsJob(conversationId) {
32
+ await this.statsQueue.add('generate-stats', { conversationId });
33
+ }
34
+ async getJobCounts(queueName) {
35
+ const queue = this.queues[queueName];
36
+ if (!queue)
37
+ return { waiting: 0, active: 0, completed: 0, failed: 0, delayed: 0 };
38
+ const counts = await queue.getJobCounts();
39
+ return {
40
+ waiting: counts.waiting ?? 0,
41
+ active: counts.active ?? 0,
42
+ completed: counts.completed ?? 0,
43
+ failed: counts.failed ?? 0,
44
+ delayed: counts.delayed ?? 0,
45
+ };
46
+ }
47
+ async getFailedJobs(queueName, limit) {
48
+ const queue = this.queues[queueName];
49
+ if (!queue)
50
+ return [];
51
+ const failed = await queue.getFailed(0, limit);
52
+ return failed.map(j => ({
53
+ id: j.id,
54
+ data: j.data,
55
+ failedReason: j.failedReason,
56
+ timestamp: j.timestamp,
57
+ attemptsMade: j.attemptsMade,
58
+ }));
59
+ }
60
+ async retryAllFailed(queueName) {
61
+ const queue = this.queues[queueName];
62
+ if (!queue)
63
+ return 0;
64
+ const failed = await queue.getFailed(0, 100);
65
+ let retried = 0;
66
+ for (const job of failed) {
67
+ await job.retry();
68
+ retried++;
69
+ }
70
+ return retried;
71
+ }
72
+ async drainQueue(queueName) {
73
+ const queue = this.queues[queueName];
74
+ if (queue)
75
+ await queue.drain();
76
+ }
77
+ listQueueNames() {
78
+ return Object.keys(this.queues);
79
+ }
80
+ queueExists(name) {
81
+ return name in this.queues;
82
+ }
83
+ async startWorkers(handler) {
84
+ const connection = { host: this.redisHost, port: this.redisPort };
85
+ this.lifecycleWorker = new Worker(QUEUE_LIFECYCLE, async () => {
86
+ try {
87
+ await handler.runLifecycleScan();
88
+ }
89
+ catch (err) {
90
+ log.error({ error: err.message }, 'Lifecycle scan failed');
91
+ }
92
+ }, { connection });
93
+ this.coldMessageWorker = new Worker(QUEUE_COLD_MESSAGES, async (job) => {
94
+ await handler.sendColdMessage(job.data);
95
+ }, { connection, concurrency: COLD_MESSAGE_CONCURRENCY });
96
+ this.statsWorker = new Worker(QUEUE_CONVERSATION_STATS, async (job) => {
97
+ await handler.generateStats(job.data.conversationId);
98
+ }, { connection, concurrency: STATS_WORKER_CONCURRENCY });
99
+ this.lifecycleWorker.on('failed', (job, err) => log.error({ job: job?.id, error: err.message }, 'Lifecycle job failed'));
100
+ this.coldMessageWorker.on('failed', (job, err) => log.error({ job: job?.id, error: err.message }, 'Cold message job failed'));
101
+ this.statsWorker.on('failed', (job, err) => log.error({ job: job?.id, error: err.message }, 'Stats job failed'));
102
+ await this.lifecycleQueue.upsertJobScheduler('scan', { every: LIFECYCLE_SCAN_INTERVAL_MS }, { name: 'lifecycle-scan' });
103
+ log.info(`BullMQ workers started (scan every ${LIFECYCLE_SCAN_INTERVAL_MS / 1000}s, ${COLD_MESSAGE_CONCURRENCY} cold message workers, ${STATS_WORKER_CONCURRENCY} stats workers)`);
104
+ }
105
+ async stopWorkers() {
106
+ await this.lifecycleWorker?.close();
107
+ await this.coldMessageWorker?.close();
108
+ await this.statsWorker?.close();
109
+ }
110
+ }
@@ -0,0 +1,3 @@
1
+ import type { QueueService } from '@supaproxy/core/ports/queue';
2
+ export declare function createBullMqQueue(redisHost: string, redisPort: number): QueueService;
3
+ export { BullMqService } from './BullMqService.js';
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ import { BullMqService } from './BullMqService.js';
2
+ export function createBullMqQueue(redisHost, redisPort) {
3
+ return new BullMqService(redisHost, redisPort);
4
+ }
5
+ export { BullMqService } from './BullMqService.js';
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@supaproxy/bullmq",
3
+ "version": "1.0.0",
4
+ "description": "BullMQ queue adapter for SupaProxy: job queues, workers, and lifecycle scheduling",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "peerDependencies": {
18
+ "@supaproxy/core": ">=1.0.0"
19
+ },
20
+ "dependencies": {
21
+ "bullmq": "^5.74.1",
22
+ "pino": "^9.6.0"
23
+ },
24
+ "devDependencies": {
25
+ "@supaproxy/core": "^1.0.0",
26
+ "@types/node": "^22.0.0",
27
+ "typescript": "^5.8.0",
28
+ "vitest": "^3.1.0"
29
+ },
30
+ "license": "MIT",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "https://github.com/NumstackPtyLtd/supaproxy-bullmq.git"
34
+ },
35
+ "publishConfig": {
36
+ "access": "public"
37
+ },
38
+ "packageManager": "pnpm@9.15.0",
39
+ "scripts": {
40
+ "build": "tsc",
41
+ "dev": "tsc --watch",
42
+ "test": "vitest run",
43
+ "lint": "tsc --noEmit"
44
+ }
45
+ }