@pikku/queue-bullmq 0.7.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/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@pikku/queue-bullmq",
3
+ "version": "0.7.0",
4
+ "author": "yasser.fadl@gmail.com",
5
+ "license": "MIT",
6
+ "module": "dist/index.js",
7
+ "main": "dist/index.js",
8
+ "type": "module",
9
+ "scripts": {
10
+ "tsc": "tsc",
11
+ "ncu": "npx npm-check-updates",
12
+ "build": "tsc -b",
13
+ "release": "npm run build && npm test"
14
+ },
15
+ "peerDependencies": {
16
+ "@pikku/core": "^0.7.0"
17
+ },
18
+ "dependencies": {
19
+ "bullmq": "^5.56.0",
20
+ "ioredis": "^5.6.1"
21
+ },
22
+ "devDependencies": {
23
+ "typescript": "^5.6"
24
+ }
25
+ }
package/run-tests.sh ADDED
@@ -0,0 +1,3 @@
1
+ #!/bin/bash
2
+ cd "$(dirname "$0")"
3
+ exec node --test --experimental-test-coverage --loader=tsx/esm dist/**/*.test.js
@@ -0,0 +1,118 @@
1
+ import Bull, {
2
+ ConnectionOptions,
3
+ JobsOptions,
4
+ Queue,
5
+ RedisConnection,
6
+ } from 'bullmq'
7
+ import type { QueueService, QueueJob, JobOptions } from '@pikku/core/queue'
8
+ import { mapBullJobToQueueJob } from './utils.js'
9
+
10
+ export const mapPikkuJobToBull = (options?: JobOptions): JobsOptions => {
11
+ const bullOptions: JobsOptions = {}
12
+
13
+ if (options?.priority !== undefined) {
14
+ bullOptions.priority = options.priority
15
+ }
16
+
17
+ if (options?.delay !== undefined) {
18
+ bullOptions.delay = options.delay
19
+ }
20
+
21
+ if (options?.attempts !== undefined) {
22
+ bullOptions.attempts = options.attempts
23
+ }
24
+
25
+ if (options?.removeOnComplete !== undefined) {
26
+ bullOptions.removeOnComplete = options.removeOnComplete
27
+ }
28
+
29
+ if (options?.removeOnFail !== undefined) {
30
+ bullOptions.removeOnFail = options.removeOnFail
31
+ }
32
+
33
+ if (options?.jobId !== undefined) {
34
+ bullOptions.jobId = options.jobId
35
+ }
36
+
37
+ return bullOptions
38
+ }
39
+
40
+ class PikkuRedisConnection extends RedisConnection {
41
+ constructor(options: ConnectionOptions) {
42
+ super(options)
43
+ this.on('error', (error) => {
44
+ console.error('Redis connection error:', error)
45
+ })
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Bull/Redis queue service implementation
51
+ * Supports job results, retries, and most queue features
52
+ */
53
+ export class BullQueueService implements QueueService {
54
+ readonly name = 'bull'
55
+ readonly supportsResults = true
56
+ protected queues = new Map<string, Bull.Queue>()
57
+ protected queueEvents = new Map<string, Bull.QueueEvents>()
58
+
59
+ constructor(private redisConnectionOptions: ConnectionOptions = {}) {}
60
+
61
+ /**
62
+ * Create a Bull queue with translated configuration
63
+ */
64
+ protected async createQueue(
65
+ queueName: string,
66
+ config?: Bull.QueueOptions
67
+ ): Promise<Bull.Queue> {
68
+ if (this.queues.has(queueName)) {
69
+ return this.queues.get(queueName)!
70
+ }
71
+
72
+ const queue = new Queue(queueName, config, PikkuRedisConnection)
73
+ await queue.waitUntilReady()
74
+
75
+ this.queues.set(queueName, queue)
76
+ return queue
77
+ }
78
+
79
+ /**
80
+ * Add a job to the queue
81
+ */
82
+ public async add<In>(
83
+ queueName: string,
84
+ data: In,
85
+ options?: JobOptions
86
+ ): Promise<string> {
87
+ const queue = await this.createQueue(queueName)
88
+ const bullJob = await queue.add(queueName, data, mapPikkuJobToBull(options))
89
+ if (!bullJob.id) {
90
+ throw new Error(`Failed to add job to queue '${queueName}'.`)
91
+ }
92
+ return bullJob.id
93
+ }
94
+
95
+ /**
96
+ * Get job by ID
97
+ */
98
+ async getJob<T, R>(
99
+ queueName: string,
100
+ jobId: string
101
+ ): Promise<QueueJob<T, R> | null> {
102
+ const queue = this.queues.get(queueName)
103
+ if (!queue) {
104
+ throw new Error(`Queue '${queueName}' not found.`)
105
+ }
106
+
107
+ const bullJob = await queue.getJob(jobId)
108
+ if (!bullJob) {
109
+ return null
110
+ }
111
+
112
+ return mapBullJobToQueueJob<T, R>(
113
+ bullJob,
114
+ this.redisConnectionOptions,
115
+ this.queueEvents
116
+ )
117
+ }
118
+ }
@@ -0,0 +1,195 @@
1
+ import Bull, {
2
+ ConnectionOptions,
3
+ WorkerOptions,
4
+ Worker,
5
+ QueueEvents,
6
+ } from 'bullmq'
7
+ import type {
8
+ QueueWorkers,
9
+ PikkuWorkerConfig,
10
+ QueueConfigMapping,
11
+ ConfigValidationResult,
12
+ } from '@pikku/core/queue'
13
+ import { runQueueJob, registerqueueWorkers } from '@pikku/core/queue'
14
+ import {
15
+ CoreServices,
16
+ CoreSingletonServices,
17
+ CreateSessionServices,
18
+ } from '@pikku/core'
19
+ import { mapBullJobToQueueJob } from './utils.js'
20
+
21
+ export const mapPikkuWorkerToBull = (
22
+ workerConfig?: PikkuWorkerConfig
23
+ ): Omit<WorkerOptions, 'connection'> => {
24
+ const workerOptions: Omit<WorkerOptions, 'connection'> = {}
25
+
26
+ // Direct mappings
27
+ if (workerConfig?.name !== undefined) {
28
+ workerOptions.name = workerConfig.name
29
+ }
30
+
31
+ if (workerConfig?.batchSize !== undefined) {
32
+ workerOptions.concurrency = workerConfig.batchSize
33
+ }
34
+
35
+ if (workerConfig?.autorun !== undefined) {
36
+ workerOptions.autorun = workerConfig.autorun
37
+ }
38
+
39
+ if (workerConfig?.lockDuration !== undefined) {
40
+ workerOptions.lockDuration = workerConfig.lockDuration
41
+ }
42
+
43
+ if (workerConfig?.drainDelay !== undefined) {
44
+ workerOptions.drainDelay = workerConfig.drainDelay
45
+ }
46
+
47
+ if (workerConfig?.maxStalledCount !== undefined) {
48
+ workerOptions.maxStalledCount = workerConfig.maxStalledCount
49
+ }
50
+
51
+ // Job retention
52
+ if (workerConfig?.removeOnComplete !== undefined) {
53
+ workerOptions.removeOnComplete = { count: workerConfig.removeOnComplete }
54
+ }
55
+
56
+ if (workerConfig?.removeOnFail !== undefined) {
57
+ workerOptions.removeOnFail = { count: workerConfig.removeOnFail }
58
+ }
59
+
60
+ return workerOptions
61
+ }
62
+
63
+ /**
64
+ * Bull/Redis queue service implementation
65
+ * Supports job results, retries, and most queue features
66
+ */
67
+ export class BullQueueWorkers implements QueueWorkers {
68
+ readonly name = 'bull'
69
+ readonly supportsResults = true
70
+
71
+ /**
72
+ * Configuration mapping rules for BullMQ
73
+ * This defines how Pikku worker configs map to BullMQ configurations
74
+ */
75
+ readonly configMappings: QueueConfigMapping = {
76
+ // Configurations that are directly supported
77
+ supported: {
78
+ name: {
79
+ queueProperty: 'name',
80
+ description: 'Worker name for identification and monitoring',
81
+ },
82
+ batchSize: {
83
+ queueProperty: 'concurrency',
84
+ description:
85
+ "Number of jobs to process concurrently, this is Bull's concurrency setting",
86
+ },
87
+ autorun: {
88
+ queueProperty: 'autorun',
89
+ description: 'Whether to start processing jobs automatically',
90
+ },
91
+ lockDuration: {
92
+ queueProperty: 'lockDuration',
93
+ description: 'Duration of job lock in milliseconds',
94
+ },
95
+ drainDelay: {
96
+ queueProperty: 'drainDelay',
97
+ description: 'Delay when queue is empty before polling again',
98
+ },
99
+ maxStalledCount: {
100
+ queueProperty: 'maxStalledCount',
101
+ description:
102
+ 'Maximum number of times a job can be recovered from stalled state',
103
+ },
104
+ removeOnComplete: {
105
+ queueProperty: 'removeOnComplete',
106
+ description: 'Number of completed jobs to keep for inspection',
107
+ },
108
+ removeOnFail: {
109
+ queueProperty: 'removeOnFail',
110
+ description: 'Number of failed jobs to keep for inspection',
111
+ },
112
+ },
113
+
114
+ // Configurations that are not supported
115
+ unsupported: {
116
+ visibilityTimeout: {
117
+ reason: 'Bull does not use visibility timeout',
118
+ explanation:
119
+ 'BullMQ locks jobs during processing instead of using visibility timeouts',
120
+ },
121
+ pollInterval: {
122
+ reason: 'Bull is push-based and does not use polling intervals',
123
+ explanation:
124
+ 'BullMQ uses Redis pub/sub for real-time job notifications',
125
+ },
126
+ prefetch: {
127
+ reason: 'Bull manages job prefetching internally',
128
+ explanation:
129
+ 'BullMQ optimizes job fetching automatically based on concurrency settings',
130
+ },
131
+ },
132
+
133
+ // No fallback configurations for BullMQ currently
134
+ fallbacks: {},
135
+ }
136
+
137
+ private workers = new Map<string, Bull.Worker>()
138
+ private queueEvents = new Map<string, QueueEvents>()
139
+
140
+ constructor(
141
+ private redisConnectionOptions: ConnectionOptions,
142
+ private singletonServices: CoreSingletonServices,
143
+ private createSessionServices?: CreateSessionServices<
144
+ CoreSingletonServices,
145
+ CoreServices,
146
+ any
147
+ >
148
+ ) {}
149
+
150
+ /**
151
+ * Scan state and register all compatible processors
152
+ */
153
+ async registerQueues(): Promise<Record<string, ConfigValidationResult[]>> {
154
+ return await registerqueueWorkers(
155
+ this.configMappings,
156
+ this.singletonServices.logger,
157
+ async (queueName, processor) => {
158
+ const worker = new Worker(
159
+ processor.queueName,
160
+ async (job: Bull.Job) => {
161
+ return await runQueueJob({
162
+ singletonServices: this.singletonServices,
163
+ createSessionServices: this.createSessionServices,
164
+ job: await mapBullJobToQueueJob(
165
+ job,
166
+ this.redisConnectionOptions,
167
+ this.queueEvents
168
+ ),
169
+ })
170
+ },
171
+ {
172
+ connection: this.redisConnectionOptions,
173
+ ...mapPikkuWorkerToBull(processor.config),
174
+ }
175
+ )
176
+ worker.on('error', console.error)
177
+ this.workers.set(queueName, worker)
178
+ }
179
+ )
180
+ }
181
+
182
+ /**
183
+ * Close all queues and connections
184
+ */
185
+ async close(): Promise<void> {
186
+ await Promise.all([
187
+ ...Array.from(this.workers.values()).map((worker) => worker.close()),
188
+ ...Array.from(this.queueEvents.values()).map((queueEvents) =>
189
+ queueEvents.close()
190
+ ),
191
+ ])
192
+ this.workers.clear()
193
+ this.queueEvents.clear()
194
+ }
195
+ }
package/src/index.ts ADDED
@@ -0,0 +1,18 @@
1
+ /**
2
+ * @module @pikku/queue-bullmq
3
+ *
4
+ * Bull/Redis queue service for Pikku
5
+ * Provides job results, retries, priorities, and most queue features
6
+ */
7
+
8
+ export { BullQueueService } from './bull-queue-service.js'
9
+ export { BullQueueWorkers } from './bull-queue-worker.js'
10
+
11
+ // Re-export core queue types for convenience
12
+ export type {
13
+ QueueService,
14
+ PikkuWorkerConfig,
15
+ PikkuJobConfig,
16
+ QueueJob,
17
+ JobOptions,
18
+ } from '@pikku/core/queue'
package/src/utils.ts ADDED
@@ -0,0 +1,64 @@
1
+ import { QueueJob, QueueJobStatus } from '@pikku/core/queue'
2
+ import { ConnectionOptions, Job, QueueEvents } from 'bullmq'
3
+
4
+ /**
5
+ * Map Bull job state to our queue job status
6
+ */
7
+ const mapBullStateToStatus = (state: string): QueueJobStatus => {
8
+ switch (state) {
9
+ case 'waiting':
10
+ return 'waiting'
11
+ case 'active':
12
+ return 'active'
13
+ case 'completed':
14
+ return 'completed'
15
+ case 'failed':
16
+ return 'failed'
17
+ case 'delayed':
18
+ return 'delayed'
19
+ default:
20
+ return 'waiting'
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Map Bull job to QueueJob interface
26
+ */
27
+ export const mapBullJobToQueueJob = async <In, Out>(
28
+ bullJob: Job,
29
+ redisConnection: ConnectionOptions | undefined,
30
+ queueEventsMap: Map<string, QueueEvents>
31
+ ): Promise<QueueJob<In, Out>> => {
32
+ return {
33
+ queueName: bullJob.name,
34
+ id: bullJob.id || '',
35
+ data: bullJob.data,
36
+ result: bullJob.returnvalue as Out,
37
+ status: async () => mapBullStateToStatus(await bullJob.getState()),
38
+ waitForCompletion: async (ttl?: number): Promise<Out> => {
39
+ let queueEvents = queueEventsMap.get(bullJob.name)
40
+ if (!queueEvents) {
41
+ queueEvents = new QueueEvents(
42
+ bullJob.name,
43
+ redisConnection ? { connection: redisConnection } : undefined
44
+ )
45
+ queueEventsMap.set(bullJob.name, queueEvents)
46
+ }
47
+ return await bullJob.waitUntilFinished(queueEvents, ttl)
48
+ },
49
+ metadata: () => ({
50
+ progress: bullJob.progress,
51
+ attemptsMade: bullJob.attemptsMade,
52
+ maxAttempts: bullJob.opts.attempts || 1,
53
+ createdAt: new Date(bullJob.timestamp),
54
+ processedAt: bullJob.processedOn
55
+ ? new Date(bullJob.processedOn)
56
+ : undefined,
57
+ completedAt: bullJob.finishedOn
58
+ ? new Date(bullJob.finishedOn)
59
+ : undefined,
60
+ failedAt: bullJob.finishedOn ? new Date(bullJob.finishedOn) : undefined,
61
+ error: bullJob.failedReason,
62
+ }),
63
+ }
64
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "rootDir": "./src",
5
+ "module": "Node16",
6
+ "outDir": "dist",
7
+ "target": "esnext",
8
+ "declaration": true
9
+ },
10
+ "include": ["src/**/*.ts"],
11
+ "exclude": ["**/*.test.ts", "node_modules"],
12
+ "references": [
13
+ {
14
+ "path": "../../core/tsconfig.json"
15
+ }
16
+ ]
17
+ }