@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 +25 -0
- package/run-tests.sh +3 -0
- package/src/bull-queue-service.ts +118 -0
- package/src/bull-queue-worker.ts +195 -0
- package/src/index.ts +18 -0
- package/src/utils.ts +64 -0
- package/tsconfig.json +17 -0
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,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
|
+
}
|